diff --git a/.gitignore b/.gitignore index 242ea3b9602..a949381ece6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ format-markdown.py .npmrc package-lock.json lintmd-config.json + diff --git a/.translation_progress.json b/.translation_progress.json new file mode 100644 index 00000000000..47669d069ff --- /dev/null +++ b/.translation_progress.json @@ -0,0 +1,297 @@ +{ + "completed": [ + "/root/gittensor_dir/JavaGuide/README.md", + "/root/gittensor_dir/JavaGuide/docs/README.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/README.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/deprecated-java-technologies.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/dog-that-copies-other-people-essay.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/feelings-after-one-month-of-induction-training.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/internet-addiction-teenager.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/javaguide-100k-star.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/my-college-life.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/writing-technology-blog-six-years.md", + "/root/gittensor_dir/JavaGuide/docs/about-the-author/zhishixingqiu-two-years.md", + "/root/gittensor_dir/JavaGuide/docs/books/README.md", + "/root/gittensor_dir/JavaGuide/docs/books/cs-basics.md", + "/root/gittensor_dir/JavaGuide/docs/books/database.md", + "/root/gittensor_dir/JavaGuide/docs/books/distributed-system.md", + "/root/gittensor_dir/JavaGuide/docs/books/java.md", + "/root/gittensor_dir/JavaGuide/docs/books/search-engine.md", + "/root/gittensor_dir/JavaGuide/docs/books/software-quality.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/string-algorithm-problems.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/algorithms/the-sword-refers-to-offer.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/bloom-filter.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/graph.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/heap.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/linear-data-structure.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/red-black-tree.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/data-structure/tree.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/application-layer-protocol.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/arp.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/computer-network-xiexiren-summary.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/dns.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/http-status-codes.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/http-vs-https.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/http1.0-vs-http1.1.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/nat.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/network-attack-means.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/osi-and-tcp-ip-model.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/other-network-questions.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/other-network-questions2.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/tcp-connection-and-disconnection.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/tcp-reliability-guarantee.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/operating-system/linux-intro.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/operating-system/operating-system-basic-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/operating-system/operating-system-basic-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/cs-basics/operating-system/shell-intro.md", + "/root/gittensor_dir/JavaGuide/docs/database/basis.md", + "/root/gittensor_dir/JavaGuide/docs/database/character-set.md", + "/root/gittensor_dir/JavaGuide/docs/database/elasticsearch/elasticsearch-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/mongodb/mongodb-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/mongodb/mongodb-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/how-sql-executed-in-mysql.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/innodb-implementation-of-mvcc.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-index.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-logs.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-query-cache.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-query-execution-plan.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/mysql-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/some-thoughts-on-database-storage-time.md", + "/root/gittensor_dir/JavaGuide/docs/database/mysql/transaction-isolation-level.md", + "/root/gittensor_dir/JavaGuide/docs/database/nosql.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/cache-basics.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-cluster.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-common-blocking-problems-summary.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-data-structures-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-data-structures-02.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-delayed-task.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-memory-fragmentation.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-persistence.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/database/redis/redis-skiplist.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-questions-03.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-questions-04.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-questions-05.md", + "/root/gittensor_dir/JavaGuide/docs/database/sql/sql-syntax-summary.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/api-gateway.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-configuration-center.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-id-design.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-id.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-lock-implementations.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-lock.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/distributed-transaction.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/protocol/cap-and-base-theorem.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/protocol/consistent-hashing.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/protocol/gossip-protocl.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/protocol/paxos-algorithm.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/protocol/raft-algorithm.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/rpc/dubbo.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/rpc/http&rpc.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/rpc/rpc-intro.md", + "/root/gittensor_dir/JavaGuide/docs/distributed-system/spring-cloud-gateway-questions.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/fallback-and-circuit-breaker.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/high-availability-system-design.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/idempotency.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/limit-request.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/performance-test.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/redundancy.md", + "/root/gittensor_dir/JavaGuide/docs/high-availability/timeout-and-retry.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/cdn.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/data-cold-hot-separation.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/deep-pagination-optimization.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/load-balancing.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/message-queue/disruptor-questions.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/message-queue/kafka-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/message-queue/message-queue.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/message-queue/rabbitmq-questions.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/message-queue/rocketmq-questions.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/read-and-write-separation-and-library-subtable.md", + "/root/gittensor_dir/JavaGuide/docs/high-performance/sql-optimization.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/README.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/work/32-tips-improving-career.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/work/employee-performance.md", + "/root/gittensor_dir/JavaGuide/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md", + "/root/gittensor_dir/JavaGuide/docs/home.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/how-to-handle-interview-nerves.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/internship-experience.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/interview-experience.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/java-roadmap.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/key-points-of-interview.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/project-experience-guide.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/resume-guide.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/self-test-of-common-interview-questions.md", + "/root/gittensor_dir/JavaGuide/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/bigdecimal.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/generics-and-wildcards.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/java-basic-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/java-basic-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/java-basic-questions-03.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/java-keyword-summary.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/proxy.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/reflection.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/serialization.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/spi.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/syntactic-sugar.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/unsafe.md", + "/root/gittensor_dir/JavaGuide/docs/java/basis/why-there-only-value-passing-in-java.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/arrayblockingqueue-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/arraylist-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/concurrent-hash-map-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/copyonwritearraylist-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/delayqueue-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/hashmap-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/java-collection-precautions-for-use.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/java-collection-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/java-collection-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/linkedhashmap-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/linkedlist-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/collection/priorityqueue-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/aqs.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/atomic-classes.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/cas.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/completablefuture-intro.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-concurrent-collections.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-concurrent-questions-01.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-concurrent-questions-02.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-concurrent-questions-03.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-thread-pool-best-practices.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/java-thread-pool-summary.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/jmm.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/reentrantlock.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/threadlocal.md", + "/root/gittensor_dir/JavaGuide/docs/java/concurrent/virtual-thread.md", + "/root/gittensor_dir/JavaGuide/docs/java/io/io-basis.md", + "/root/gittensor_dir/JavaGuide/docs/java/io/io-design-patterns.md", + "/root/gittensor_dir/JavaGuide/docs/java/io/io-model.md", + "/root/gittensor_dir/JavaGuide/docs/java/io/nio-basis.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/class-file-structure.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/class-loading-process.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/classloader.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/jvm-garbage-collection.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/jvm-in-action.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/jvm-intro.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/jvm-parameters-intro.md", + "/root/gittensor_dir/JavaGuide/docs/java/jvm/memory-area.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java10.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java11.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java12-13.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java14-15.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java16.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java17.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java18.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java19.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java20.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java21.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java22-23.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java24.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java25.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java8-common-new-features.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java8-tutorial-translate.md", + "/root/gittensor_dir/JavaGuide/docs/java/new-features/java9.md", + "/root/gittensor_dir/JavaGuide/docs/javaguide/contribution-guideline.md", + "/root/gittensor_dir/JavaGuide/docs/javaguide/faq.md", + "/root/gittensor_dir/JavaGuide/docs/javaguide/history.md", + "/root/gittensor_dir/JavaGuide/docs/javaguide/intro.md", + "/root/gittensor_dir/JavaGuide/docs/javaguide/use-suggestion.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/README.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/big-data.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/machine-learning.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/practical-project.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/system-design.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/tool-library.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/tools.md", + "/root/gittensor_dir/JavaGuide/docs/open-source-project/tutorial.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/article-footer.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/article-header.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/planet.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/planet2.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/small-advertisement.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/snippets/yuanma.snippet.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/J2EE\u57fa\u7840\u77e5\u8bc6.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/basis/RESTfulAPI.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/basis/naming.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/basis/refactoring.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/basis/software-engineering.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/basis/unit-test.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/design-pattern.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/mybatis/mybatis-interview.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/netty.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/Async.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/async.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/ioc-and-aop.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/spring-common-annotations.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/spring-design-patterns-summary.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/spring-transaction.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/framework/spring/springboot-source-code.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/schedule-task.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/advantages-and-disadvantages-of-jwt.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/basis-of-authority-certification.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/data-desensitization.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/data-validation.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/design-of-authority-system.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/encryption-algorithms.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/jwt-intro.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/sentive-words-filter.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/security/sso-intro.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/system-design-questions.md", + "/root/gittensor_dir/JavaGuide/docs/system-design/web-real-time-message-push.md", + "/root/gittensor_dir/JavaGuide/docs/tools/docker/docker-in-action.md", + "/root/gittensor_dir/JavaGuide/docs/tools/docker/docker-intro.md", + "/root/gittensor_dir/JavaGuide/docs/tools/git/git-intro.md", + "/root/gittensor_dir/JavaGuide/docs/tools/git/github-tips.md", + "/root/gittensor_dir/JavaGuide/docs/tools/gradle/gradle-core-concepts.md", + "/root/gittensor_dir/JavaGuide/docs/tools/maven/maven-best-practices.md", + "/root/gittensor_dir/JavaGuide/docs/tools/maven/maven-core-concepts.md", + "/root/gittensor_dir/JavaGuide/docs/zhuanlan/README.md", + "/root/gittensor_dir/JavaGuide/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md", + "/root/gittensor_dir/JavaGuide/docs/zhuanlan/handwritten-rpc-framework.md", + "/root/gittensor_dir/JavaGuide/docs/zhuanlan/java-mian-shi-zhi-bei.md", + "/root/gittensor_dir/JavaGuide/docs/zhuanlan/source-code-reading.md" + ], + "failed": [] +} \ No newline at end of file diff --git a/docs/distributed-system/distributed-configuration-center.md b/docs/distributed-system/distributed-configuration-center.md index 2e00aec70a3..d4f1e83b9c2 100644 --- a/docs/distributed-system/distributed-configuration-center.md +++ b/docs/distributed-system/distributed-configuration-center.md @@ -1,12 +1,778 @@ --- -title: 分布式配置中心常见问题总结(付费) +title: 分布式配置中心完整实践指南 category: 分布式 +tag: + - 分布式配置 + - 微服务 + - Nacos + - Apollo +head: + - - meta + - name: keywords + content: 分布式配置中心,Nacos,Apollo,Spring Cloud Config,配置管理,动态刷新,多环境配置 + - - meta + - name: description + content: 深入讲解分布式配置中心的原理、主流框架对比、实战应用和最佳实践,包括Nacos、Apollo、Spring Cloud Config的详细使用。 --- -**分布式配置中心** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。 +# 分布式配置中心完整实践指南 -![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) +在微服务架构中,配置管理是一个关键问题。分布式配置中心提供了统一的配置管理、动态刷新、版本控制等能力。 - +## 为什么需要配置中心 + +### 传统配置管理的问题 + +**问题1:配置分散** +```java +// application.properties 分散在每个服务中 +spring.datasource.url=jdbc:mysql://localhost:3306/db +spring.datasource.username=root +spring.datasource.password=123456 + +// 修改配置需要: +// 1. 修改每个服务的配置文件 +// 2. 重新打包 +// 3. 重启服务 +``` + +**问题2:环境管理复杂** +```bash +# 需要维护多套配置文件 +application-dev.properties +application-test.properties +application-prod.properties + +# 容易出错,难以追踪 +``` + +**问题3:无法动态更新** +```java +// 修改配置必须重启服务 +// 无法实时生效 +// 影响系统可用性 +``` + +### 配置中心的优势 + +1. **集中管理**:所有配置统一存储和管理 +2. **动态刷新**:配置修改实时生效,无需重启 +3. **版本控制**:配置变更可追溯、可回滚 +4. **环境隔离**:多环境配置管理 +5. **安全控制**:配置加密、权限管理 +6. **灰度发布**:配置分批发布 + +## 主流配置中心对比 + +| 特性 | Nacos | Apollo | Spring Cloud Config | Consul | +|------|-------|--------|---------------------|--------| +| 配置管理 | ✅ | ✅ | ✅ | ✅ | +| 服务发现 | ✅ | ❌ | ❌ | ✅ | +| 动态刷新 | ✅ | ✅ | ✅ | ✅ | +| 版本管理 | ✅ | ✅ | ✅ | ❌ | +| 灰度发布 | ✅ | ✅ | ❌ | ❌ | +| 权限控制 | ✅ | ✅ | ❌ | ✅ | +| 配置加密 | ✅ | ✅ | ✅ | ✅ | +| 多语言支持 | Java/Go/Python | Java | Java | 多语言 | +| UI界面 | 简洁 | 丰富 | 无 | 简单 | +| 学习成本 | 低 | 中 | 低 | 中 | +| 社区活跃度 | 高 | 高 | 中 | 高 | + +## Nacos 配置中心 + +### 快速开始 + +**1. 添加依赖** + +```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + 2022.0.0.0 + +``` + +**2. 配置文件** + +```yaml +# bootstrap.yml +spring: + application: + name: order-service + cloud: + nacos: + config: + server-addr: 127.0.0.1:8848 + namespace: dev + group: DEFAULT_GROUP + file-extension: yaml + # 共享配置 + shared-configs: + - data-id: common-mysql.yaml + group: COMMON_GROUP + refresh: true + - data-id: common-redis.yaml + group: COMMON_GROUP + refresh: true + # 扩展配置 + extension-configs: + - data-id: order-service-ext.yaml + group: DEFAULT_GROUP + refresh: true +``` + +**3. 使用配置** + +```java +@RestController +@RefreshScope // 支持动态刷新 +public class ConfigController { + + @Value("${server.port}") + private String port; + + @Value("${custom.config.name}") + private String configName; + + @GetMapping("/config") + public String getConfig() { + return "Port: " + port + ", Config: " + configName; + } +} + +// 使用 @ConfigurationProperties +@Component +@ConfigurationProperties(prefix = "custom.config") +@RefreshScope +@Data +public class CustomConfig { + private String name; + private Integer timeout; + private List servers; +} +``` + +### 配置监听 + +```java +@Component +public class NacosConfigListener { + + @Autowired + private NacosConfigManager nacosConfigManager; + + @PostConstruct + public void init() throws NacosException { + String dataId = "order-service.yaml"; + String group = "DEFAULT_GROUP"; + + ConfigService configService = nacosConfigManager.getConfigService(); + + // 添加监听器 + configService.addListener(dataId, group, new Listener() { + @Override + public Executor getExecutor() { + return null; + } + + @Override + public void receiveConfigInfo(String configInfo) { + log.info("配置更新: {}", configInfo); + // 处理配置变更 + handleConfigChange(configInfo); + } + }); + } + + private void handleConfigChange(String configInfo) { + // 自定义配置变更处理逻辑 + // 例如:刷新缓存、重新初始化组件等 + } +} +``` + +### 多环境配置 + +```java +@Configuration +public class NacosMultiEnvConfig { + + @Bean + @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev") + public DataSource devDataSource() { + // 开发环境数据源 + return DataSourceBuilder.create() + .url("jdbc:mysql://dev-db:3306/db") + .username("dev_user") + .password("dev_pass") + .build(); + } + + @Bean + @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "prod") + public DataSource prodDataSource() { + // 生产环境数据源 + return DataSourceBuilder.create() + .url("jdbc:mysql://prod-db:3306/db") + .username("prod_user") + .password("prod_pass") + .build(); + } +} +``` + +### 配置加密 + +```java +@Component +public class ConfigEncryption { + + private static final String SECRET_KEY = "your-secret-key"; + + // 加密配置 + public String encrypt(String plainText) throws Exception { + Cipher cipher = Cipher.getInstance("AES"); + SecretKeySpec keySpec = new SecretKeySpec( + SECRET_KEY.getBytes(), "AES"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + byte[] encrypted = cipher.doFinal(plainText.getBytes()); + return Base64.getEncoder().encodeToString(encrypted); + } + + // 解密配置 + public String decrypt(String encryptedText) throws Exception { + Cipher cipher = Cipher.getInstance("AES"); + SecretKeySpec keySpec = new SecretKeySpec( + SECRET_KEY.getBytes(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + byte[] decrypted = cipher.doFinal( + Base64.getDecoder().decode(encryptedText)); + return new String(decrypted); + } +} + +// 在配置文件中使用加密 +// database.password=ENC(encrypted_password_here) + +@Component +public class EncryptedPropertySourcePostProcessor + implements BeanFactoryPostProcessor { + + @Autowired + private ConfigEncryption encryption; + + @Override + public void postProcessBeanFactory( + ConfigurableListableBeanFactory beanFactory) { + ConfigurableEnvironment environment = + (ConfigurableEnvironment) beanFactory.getBean( + "environment", Environment.class); + + for (PropertySource propertySource : + environment.getPropertySources()) { + if (propertySource instanceof EnumerablePropertySource) { + EnumerablePropertySource eps = + (EnumerablePropertySource) propertySource; + for (String name : eps.getPropertyNames()) { + Object value = eps.getProperty(name); + if (value instanceof String) { + String strValue = (String) value; + if (strValue.startsWith("ENC(") && + strValue.endsWith(")")) { + String encrypted = strValue.substring( + 4, strValue.length() - 1); + try { + String decrypted = + encryption.decrypt(encrypted); + // 替换为解密后的值 + } catch (Exception e) { + log.error("解密失败: {}", name, e); + } + } + } + } + } + } + } +} +``` + +## Apollo 配置中心 + +### 快速开始 + +**1. 添加依赖** + +```xml + + com.ctrip.framework.apollo + apollo-client + 2.1.0 + +``` + +**2. 配置文件** + +```properties +# application.properties +app.id=order-service +apollo.meta=http://localhost:8080 +apollo.bootstrap.enabled=true +apollo.bootstrap.namespaces=application,common.mysql,common.redis +``` + +**3. 使用配置** + +```java +@Configuration +@EnableApolloConfig +public class ApolloConfig { + // Apollo 配置自动注入 +} + +@RestController +public class ConfigController { + + // 方式1:使用 @Value + @Value("${server.port:8080}") + private int port; + + // 方式2:使用 @ApolloConfig + @ApolloConfig + private Config config; + + @GetMapping("/config") + public String getConfig() { + String value = config.getProperty("custom.key", "default"); + return "Port: " + port + ", Value: " + value; + } +} +``` + +### 配置监听 + +```java +@Component +public class ApolloConfigListener { + + @ApolloConfig + private Config config; + + @ApolloConfigChangeListener + private void onChange(ConfigChangeEvent changeEvent) { + for (String key : changeEvent.changedKeys()) { + ConfigChange change = changeEvent.getChange(key); + log.info("配置变更 - Key: {}, OldValue: {}, NewValue: {}, ChangeType: {}", + key, + change.getOldValue(), + change.getNewValue(), + change.getChangeType()); + } + } + + // 监听特定命名空间 + @ApolloConfigChangeListener("common.mysql") + private void onMysqlConfigChange(ConfigChangeEvent changeEvent) { + log.info("MySQL配置变更"); + // 重新初始化数据源 + reinitializeDataSource(); + } +} +``` + +### 灰度发布 + +```java +@Service +public class GrayReleaseService { + + @ApolloConfig + private Config config; + + public void processRequest(String userId) { + // 获取灰度配置 + boolean useNewFeature = config.getBooleanProperty( + "feature.new.enabled", false); + + if (useNewFeature) { + // 使用新功能 + processWithNewFeature(userId); + } else { + // 使用旧功能 + processWithOldFeature(userId); + } + } + + // Apollo 控制台配置灰度规则: + // 1. 创建灰度版本 + // 2. 配置灰度IP或灰度规则 + // 3. 灰度发布 + // 4. 全量发布 +} +``` + +## Spring Cloud Config + +### 服务端配置 + +```java +@SpringBootApplication +@EnableConfigServer +public class ConfigServerApplication { + public static void main(String[] args) { + SpringApplication.run(ConfigServerApplication.class, args); + } +} +``` + +```yaml +# application.yml +server: + port: 8888 + +spring: + cloud: + config: + server: + git: + uri: https://github.com/your-org/config-repo + search-paths: '{application}' + username: your-username + password: your-password + default-label: main + # 本地文件系统 + native: + search-locations: classpath:/config +``` + +### 客户端配置 + +```xml + + org.springframework.cloud + spring-cloud-starter-config + +``` + +```yaml +# bootstrap.yml +spring: + application: + name: order-service + cloud: + config: + uri: http://localhost:8888 + profile: dev + label: main +``` + +## 配置管理最佳实践 + +### 1. 配置分层 + +```yaml +# 公共配置 (common.yaml) +spring: + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + +logging: + level: + root: INFO + +# 数据库配置 (common-mysql.yaml) +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + +# 应用特定配置 (order-service.yaml) +server: + port: 8080 + +order: + timeout: 30000 + max-retry: 3 +``` + +### 2. 配置版本管理 + +```java +@Service +public class ConfigVersionService { + + @Autowired + private NacosConfigManager nacosConfigManager; + + // 发布新版本配置 + public boolean publishConfig(String dataId, String group, + String content) throws NacosException { + ConfigService configService = + nacosConfigManager.getConfigService(); + + // 获取当前配置 + String currentConfig = configService.getConfig( + dataId, group, 5000); + + // 备份当前配置 + backupConfig(dataId, group, currentConfig); + + // 发布新配置 + return configService.publishConfig(dataId, group, content); + } + + // 回滚配置 + public boolean rollbackConfig(String dataId, String group, + String version) { + String backupConfig = getBackupConfig(dataId, group, version); + if (backupConfig != null) { + try { + return nacosConfigManager.getConfigService() + .publishConfig(dataId, group, backupConfig); + } catch (NacosException e) { + log.error("配置回滚失败", e); + return false; + } + } + return false; + } + + private void backupConfig(String dataId, String group, + String content) { + String version = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String backupKey = String.format("%s_%s_%s", + dataId, group, version); + // 保存到数据库或文件系统 + saveToBackup(backupKey, content); + } +} +``` + +### 3. 配置安全 + +```java +@Configuration +public class ConfigSecurityConfig { + + // 敏感配置加密 + @Bean + public StringEncryptor stringEncryptor() { + PooledPBEStringEncryptor encryptor = + new PooledPBEStringEncryptor(); + SimpleStringPBEConfig config = new SimpleStringPBEConfig(); + config.setPassword("encryption-password"); + config.setAlgorithm("PBEWithMD5AndDES"); + config.setKeyObtentionIterations("1000"); + config.setPoolSize("1"); + config.setProviderName("SunJCE"); + config.setSaltGeneratorClassName( + "org.jasypt.salt.RandomSaltGenerator"); + config.setStringOutputType("base64"); + encryptor.setConfig(config); + return encryptor; + } +} + +// 使用加密配置 +// database.password=ENC(encrypted_value) +``` + +### 4. 配置变更通知 + +```java +@Component +public class ConfigChangeNotifier { + + @Autowired + private JavaMailSender mailSender; + + @Autowired + private DingTalkClient dingTalkClient; + + @ApolloConfigChangeListener + public void onConfigChange(ConfigChangeEvent changeEvent) { + StringBuilder message = new StringBuilder(); + message.append("配置变更通知:\n"); + + for (String key : changeEvent.changedKeys()) { + ConfigChange change = changeEvent.getChange(key); + message.append(String.format( + "Key: %s, Old: %s, New: %s\n", + key, + change.getOldValue(), + change.getNewValue())); + } + + // 发送邮件通知 + sendEmail("配置变更通知", message.toString()); + + // 发送钉钉通知 + sendDingTalk(message.toString()); + } + + private void sendEmail(String subject, String content) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo("admin@example.com"); + message.setSubject(subject); + message.setText(content); + mailSender.send(message); + } + + private void sendDingTalk(String content) { + dingTalkClient.send(content); + } +} +``` + +### 5. 配置预热 + +```java +@Component +public class ConfigWarmUp implements ApplicationRunner { + + @Autowired + private ConfigService configService; + + @Override + public void run(ApplicationArguments args) throws Exception { + // 预加载关键配置 + List criticalConfigs = Arrays.asList( + "database.url", + "redis.host", + "mq.broker" + ); + + for (String key : criticalConfigs) { + String value = configService.getConfig(key); + if (value == null) { + log.error("关键配置缺失: {}", key); + throw new IllegalStateException( + "关键配置缺失: " + key); + } + log.info("配置预热成功: {} = {}", key, value); + } + } +} +``` + +## 配置中心高可用方案 + +### 1. 客户端缓存 + +```java +@Component +public class ConfigCache { + + private final Map cache = + new ConcurrentHashMap<>(); + + private final String cacheDir = "/data/config-cache"; + + // 缓存配置到本地 + public void cacheConfig(String key, String value) { + cache.put(key, value); + // 持久化到本地文件 + saveToDisk(key, value); + } + + // 从缓存获取配置 + public String getConfig(String key) { + String value = cache.get(key); + if (value == null) { + // 从本地文件加载 + value = loadFromDisk(key); + if (value != null) { + cache.put(key, value); + } + } + return value; + } + + private void saveToDisk(String key, String value) { + try { + Path path = Paths.get(cacheDir, key); + Files.createDirectories(path.getParent()); + Files.write(path, value.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + log.error("保存配置到本地失败", e); + } + } + + private String loadFromDisk(String key) { + try { + Path path = Paths.get(cacheDir, key); + if (Files.exists(path)) { + return new String(Files.readAllBytes(path), + StandardCharsets.UTF_8); + } + } catch (IOException e) { + log.error("从本地加载配置失败", e); + } + return null; + } +} +``` + +### 2. 降级策略 + +```java +@Service +public class ConfigServiceWithFallback { + + @Autowired + private NacosConfigManager nacosConfigManager; + + @Autowired + private ConfigCache configCache; + + public String getConfig(String dataId, String group) { + try { + // 尝试从配置中心获取 + String config = nacosConfigManager.getConfigService() + .getConfig(dataId, group, 3000); + + // 缓存配置 + configCache.cacheConfig(dataId, config); + return config; + + } catch (Exception e) { + log.warn("从配置中心获取配置失败,使用缓存配置", e); + + // 降级:使用缓存配置 + String cachedConfig = configCache.getConfig(dataId); + if (cachedConfig != null) { + return cachedConfig; + } + + // 降级:使用默认配置 + return getDefaultConfig(dataId); + } + } + + private String getDefaultConfig(String dataId) { + // 返回默认配置 + return "default-config"; + } +} +``` + +## 总结 + +分布式配置中心是微服务架构的重要组成部分,选择合适的配置中心需要考虑: + +1. **功能需求**:是否需要服务发现、灰度发布等高级功能 +2. **团队技术栈**:与现有技术栈的兼容性 +3. **运维成本**:部署、维护的复杂度 +4. **性能要求**:配置更新的实时性要求 +5. **安全要求**:配置加密、权限控制的需求 + +**推荐方案**: +- **Nacos**:适合阿里云生态,需要服务发现+配置管理 +- **Apollo**:适合大型企业,需要完善的权限和灰度发布 +- **Spring Cloud Config**:适合简单场景,与Spring Cloud深度集成 diff --git a/docs/distributed-system/distributed-transaction.md b/docs/distributed-system/distributed-transaction.md index fa4c83c743c..1f56a7ad241 100644 --- a/docs/distributed-system/distributed-transaction.md +++ b/docs/distributed-system/distributed-transaction.md @@ -1,12 +1,791 @@ --- -title: 分布式事务常见解决方案总结(付费) +title: 分布式事务完整解决方案详解 category: 分布式 +tag: + - 分布式事务 + - 微服务 +head: + - - meta + - name: keywords + content: 分布式事务,CAP,BASE,2PC,3PC,TCC,Saga,本地消息表,MQ事务消息,Seata + - - meta + - name: description + content: 全面讲解分布式事务的理论基础、常见解决方案和实际应用,包括2PC、3PC、TCC、Saga等模式的详细实现。 --- -**分布式事务** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。 +# 分布式事务完整解决方案详解 -![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) +在微服务架构中,一个业务操作往往需要跨越多个服务和数据库,如何保证数据的一致性成为了一个核心问题。本文将全面介绍分布式事务的理论基础和实践方案。 - +## 什么是分布式事务 + +### 本地事务 vs 分布式事务 + +**本地事务**: + +```java +@Transactional +public void createOrder(Order order) { + // 在同一个数据库中执行 + orderMapper.insert(order); + inventoryMapper.decrease(order.getProductId(), order.getQuantity()); + accountMapper.decrease(order.getUserId(), order.getAmount()); + // 要么全部成功,要么全部回滚 +} +``` + +**分布式事务**: + +```java +// 订单服务 +public void createOrder(Order order) { + orderService.create(order); // 订单数据库 + inventoryService.decrease(order); // 库存数据库 + accountService.decrease(order); // 账户数据库 + // 跨多个服务和数据库,如何保证一致性? +} +``` + +### 分布式事务的挑战 + +1. **网络不可靠**:服务间通信可能失败 +2. **服务独立**:每个服务有自己的数据库 +3. **性能开销**:协调多个服务的事务成本高 +4. **复杂度高**:需要处理各种异常情况 + +## 理论基础 + +### CAP 定理 + +CAP 定理指出,分布式系统最多只能同时满足以下三项中的两项: + +- **C (Consistency)** - 一致性:所有节点在同一时间看到相同的数据 +- **A (Availability)** - 可用性:每个请求都能得到响应(成功或失败) +- **P (Partition Tolerance)** - 分区容错性:系统在网络分区时仍能继续运行 + +**重要结论**: +- 网络分区是客观存在的,所以 P 必须保证 +- 只能在 C 和 A 之间做权衡 +- CP 系统:牺牲可用性保证一致性(如 ZooKeeper) +- AP 系统:牺牲强一致性保证可用性(如 Cassandra) + +### BASE 理论 + +BASE 是对 CAP 中 AP 方案的延伸,核心思想是: + +- **BA (Basically Available)** - 基本可用:允许损失部分可用性 +- **S (Soft State)** - 软状态:允许系统中的数据存在中间状态 +- **E (Eventually Consistent)** - 最终一致性:经过一段时间后,数据最终达到一致 + +**BASE vs ACID**: + +| 特性 | ACID | BASE | +|------|------|------| +| 一致性 | 强一致性 | 最终一致性 | +| 隔离性 | 严格隔离 | 允许中间状态 | +| 可用性 | 可能牺牲 | 优先保证 | +| 适用场景 | 单体应用 | 分布式系统 | + +## 分布式事务解决方案 + +### 1. 两阶段提交(2PC) + +#### 原理 + +2PC 将事务提交分为两个阶段: + +**阶段一:准备阶段(Prepare)** + +``` +协调者 参与者1 参与者2 + | | | + |---准备事务--------->| | + | |---执行但不提交---->| + | |<---准备完成--------| + |<--准备完成----------| | + | | | +``` + +**阶段二:提交阶段(Commit)** + +``` +协调者 参与者1 参与者2 + | | | + |---提交事务--------->| | + | |---提交------------>| + | |<---提交完成--------| + |<--提交完成----------| | +``` + +#### 代码示例 + +```java +public class TwoPhaseCommitCoordinator { + private List participants; + + public boolean executeTransaction(Transaction tx) { + // 阶段一:准备 + boolean allPrepared = true; + for (Participant p : participants) { + if (!p.prepare(tx)) { + allPrepared = false; + break; + } + } + + // 阶段二:提交或回滚 + if (allPrepared) { + for (Participant p : participants) { + p.commit(tx); + } + return true; + } else { + for (Participant p : participants) { + p.rollback(tx); + } + return false; + } + } +} + +interface Participant { + boolean prepare(Transaction tx); + void commit(Transaction tx); + void rollback(Transaction tx); +} +``` + +#### 优缺点 + +**优点**: +- 强一致性保证 +- 实现相对简单 + +**缺点**: +- 同步阻塞:所有参与者在等待期间都处于阻塞状态 +- 单点故障:协调者故障导致整个系统不可用 +- 数据不一致:网络分区可能导致部分提交部分回滚 +- 性能开销大:需要多次网络通信 + +### 2. 三阶段提交(3PC) + +3PC 是 2PC 的改进版本,增加了超时机制和 CanCommit 阶段。 + +#### 三个阶段 + +**阶段一:CanCommit** +- 协调者询问参与者是否可以执行事务 +- 参与者返回 Yes 或 No + +**阶段二:PreCommit** +- 如果都返回 Yes,发送 PreCommit 请求 +- 参与者执行事务但不提交 + +**阶段三:DoCommit** +- 协调者发送 Commit 或 Abort +- 参与者执行最终操作 + +#### 改进点 + +```java +public class ThreePhaseCommitCoordinator { + private static final long TIMEOUT = 30000; // 30秒超时 + + public boolean executeTransaction(Transaction tx) { + // 阶段一:CanCommit + if (!canCommit(tx)) { + return false; + } + + // 阶段二:PreCommit + try { + if (!preCommit(tx)) { + abort(tx); + return false; + } + } catch (TimeoutException e) { + abort(tx); + return false; + } + + // 阶段三:DoCommit + try { + doCommit(tx); + return true; + } catch (TimeoutException e) { + // 超时后参与者会自动提交 + return true; + } + } +} +``` + +### 3. TCC(Try-Confirm-Cancel) + +TCC 是一种补偿型事务,将业务逻辑分为三个阶段。 + +#### 三个阶段 + +**Try 阶段**: +- 尝试执行业务 +- 完成所有业务检查 +- 预留必需的业务资源 + +**Confirm 阶段**: +- 确认执行业务 +- 使用 Try 阶段预留的资源 +- 不做任何业务检查 + +**Cancel 阶段**: +- 取消执行业务 +- 释放 Try 阶段预留的资源 + +#### 实现示例 + +```java +// 订单服务 +@Service +public class OrderService { + + // Try:创建订单,状态为"处理中" + @Transactional + public void tryCreate(Order order) { + order.setStatus("PROCESSING"); + orderMapper.insert(order); + } + + // Confirm:确认订单 + @Transactional + public void confirmCreate(String orderId) { + Order order = orderMapper.selectById(orderId); + order.setStatus("SUCCESS"); + orderMapper.update(order); + } + + // Cancel:取消订单 + @Transactional + public void cancelCreate(String orderId) { + Order order = orderMapper.selectById(orderId); + order.setStatus("CANCELLED"); + orderMapper.update(order); + } +} + +// 库存服务 +@Service +public class InventoryService { + + // Try:冻结库存 + @Transactional + public void tryDecrease(String productId, int quantity) { + Inventory inventory = inventoryMapper.selectById(productId); + + if (inventory.getAvailable() < quantity) { + throw new InsufficientInventoryException(); + } + + // 可用库存减少,冻结库存增加 + inventory.setAvailable(inventory.getAvailable() - quantity); + inventory.setFrozen(inventory.getFrozen() + quantity); + inventoryMapper.update(inventory); + } + + // Confirm:扣减库存 + @Transactional + public void confirmDecrease(String productId, int quantity) { + Inventory inventory = inventoryMapper.selectById(productId); + + // 冻结库存减少 + inventory.setFrozen(inventory.getFrozen() - quantity); + inventoryMapper.update(inventory); + } + + // Cancel:解冻库存 + @Transactional + public void cancelDecrease(String productId, int quantity) { + Inventory inventory = inventoryMapper.selectById(productId); + + // 恢复可用库存,减少冻结库存 + inventory.setAvailable(inventory.getAvailable() + quantity); + inventory.setFrozen(inventory.getFrozen() - quantity); + inventoryMapper.update(inventory); + } +} + +// TCC 协调器 +@Service +public class TCCCoordinator { + + @Autowired + private OrderService orderService; + + @Autowired + private InventoryService inventoryService; + + @Autowired + private AccountService accountService; + + public void createOrder(Order order) { + String txId = UUID.randomUUID().toString(); + + try { + // Try 阶段 + orderService.tryCreate(order); + inventoryService.tryDecrease(order.getProductId(), order.getQuantity()); + accountService.tryDecrease(order.getUserId(), order.getAmount()); + + // 记录事务日志 + saveTxLog(txId, "TRY", "SUCCESS"); + + // Confirm 阶段 + orderService.confirmCreate(order.getId()); + inventoryService.confirmDecrease(order.getProductId(), order.getQuantity()); + accountService.confirmDecrease(order.getUserId(), order.getAmount()); + + saveTxLog(txId, "CONFIRM", "SUCCESS"); + + } catch (Exception e) { + // Cancel 阶段 + orderService.cancelCreate(order.getId()); + inventoryService.cancelDecrease(order.getProductId(), order.getQuantity()); + accountService.cancelDecrease(order.getUserId(), order.getAmount()); + + saveTxLog(txId, "CANCEL", "SUCCESS"); + throw e; + } + } +} +``` + +#### TCC 的优缺点 + +**优点**: +- 性能较好,不需要长时间锁定资源 +- 可以跨数据库、跨服务 +- 业务逻辑清晰 + +**缺点**: +- 业务侵入性强,需要实现 Try、Confirm、Cancel 三个接口 +- 开发成本高 +- 需要考虑幂等性 + +### 4. Saga 模式 + +Saga 模式将长事务拆分为多个本地短事务,每个短事务都有对应的补偿操作。 + +#### 两种实现方式 + +**协同式(Choreography)**: + +```java +// 订单服务 +@Service +public class OrderSagaService { + + @Autowired + private EventPublisher eventPublisher; + + // 步骤1:创建订单 + @Transactional + public void createOrder(Order order) { + orderMapper.insert(order); + + // 发布订单创建事件 + eventPublisher.publish(new OrderCreatedEvent(order)); + } + + // 补偿操作:取消订单 + @Transactional + @EventListener + public void cancelOrder(OrderCancelEvent event) { + Order order = orderMapper.selectById(event.getOrderId()); + order.setStatus("CANCELLED"); + orderMapper.update(order); + } +} + +// 库存服务 +@Service +public class InventorySagaService { + + @Autowired + private EventPublisher eventPublisher; + + // 步骤2:扣减库存 + @Transactional + @EventListener + public void decreaseInventory(OrderCreatedEvent event) { + try { + Inventory inventory = inventoryMapper.selectById(event.getProductId()); + inventory.setQuantity(inventory.getQuantity() - event.getQuantity()); + inventoryMapper.update(inventory); + + // 发布库存扣减成功事件 + eventPublisher.publish(new InventoryDecreasedEvent(event)); + + } catch (Exception e) { + // 发布失败事件,触发补偿 + eventPublisher.publish(new InventoryDecreaseFailedEvent(event)); + } + } + + // 补偿操作:恢复库存 + @Transactional + @EventListener + public void restoreInventory(OrderCancelEvent event) { + Inventory inventory = inventoryMapper.selectById(event.getProductId()); + inventory.setQuantity(inventory.getQuantity() + event.getQuantity()); + inventoryMapper.update(inventory); + } +} +``` + +**编排式(Orchestration)**: + +```java +@Service +public class OrderSagaOrchestrator { + + @Autowired + private OrderService orderService; + + @Autowired + private InventoryService inventoryService; + + @Autowired + private AccountService accountService; + + public void createOrder(Order order) { + SagaDefinition saga = SagaDefinition.create() + .step() + .invoke(() -> orderService.create(order)) + .withCompensation(() -> orderService.cancel(order.getId())) + .step() + .invoke(() -> inventoryService.decrease(order)) + .withCompensation(() -> inventoryService.restore(order)) + .step() + .invoke(() -> accountService.decrease(order)) + .withCompensation(() -> accountService.restore(order)) + .build(); + + sagaExecutor.execute(saga); + } +} +``` + +### 5. 本地消息表 + +本地消息表方案通过在业务数据库中增加消息表来保证最终一致性。 + +#### 实现步骤 + +```java +// 消息表 +@Table(name = "local_message") +public class LocalMessage { + private String id; + private String content; + private String status; // PENDING, SENT, CONFIRMED + private int retryCount; + private Date createTime; + private Date updateTime; +} + +// 订单服务 +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private LocalMessageMapper messageMapper; + + @Autowired + private MessageProducer messageProducer; + + // 步骤1:在本地事务中保存业务数据和消息 + @Transactional + public void createOrder(Order order) { + // 保存订单 + orderMapper.insert(order); + + // 保存本地消息 + LocalMessage message = new LocalMessage(); + message.setId(UUID.randomUUID().toString()); + message.setContent(JSON.toJSONString(order)); + message.setStatus("PENDING"); + messageMapper.insert(message); + } + + // 步骤2:定时任务发送消息 + @Scheduled(fixedDelay = 5000) + public void sendPendingMessages() { + List messages = messageMapper.selectPending(); + + for (LocalMessage message : messages) { + try { + // 发送消息到MQ + messageProducer.send("order-topic", message.getContent()); + + // 更新消息状态 + message.setStatus("SENT"); + messageMapper.update(message); + + } catch (Exception e) { + // 增加重试次数 + message.setRetryCount(message.getRetryCount() + 1); + messageMapper.update(message); + + if (message.getRetryCount() > 3) { + // 告警 + alertService.alert("消息发送失败: " + message.getId()); + } + } + } + } +} + +// 库存服务 +@Service +public class InventoryService { + + @Autowired + private InventoryMapper inventoryMapper; + + @Autowired + private MessageProducer messageProducer; + + // 步骤3:消费消息并执行业务 + @RabbitListener(queues = "order-topic") + public void handleOrderCreated(String message) { + Order order = JSON.parseObject(message, Order.class); + + try { + // 扣减库存 + Inventory inventory = inventoryMapper.selectById(order.getProductId()); + inventory.setQuantity(inventory.getQuantity() - order.getQuantity()); + inventoryMapper.update(inventory); + + // 发送确认消息 + messageProducer.send("order-confirm-topic", order.getId()); + + } catch (Exception e) { + // 消费失败,消息会重新入队 + throw new RuntimeException("库存扣减失败", e); + } + } +} +``` + +### 6. MQ 事务消息 + +以 RocketMQ 为例,支持事务消息来保证分布式事务。 + +#### 实现流程 + +```java +@Service +public class OrderService { + + @Autowired + private TransactionMQProducer producer; + + @Autowired + private OrderMapper orderMapper; + + public void createOrder(Order order) { + // 发送半消息 + Message message = new Message("order-topic", + JSON.toJSONString(order).getBytes()); + + TransactionSendResult result = producer.sendMessageInTransaction( + message, order); + + if (result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) { + log.info("订单创建成功: {}", order.getId()); + } + } + + // 本地事务执行器 + @Component + public class OrderTransactionListener implements TransactionListener { + + @Autowired + private OrderMapper orderMapper; + + // 执行本地事务 + @Override + public LocalTransactionState executeLocalTransaction( + Message msg, Object arg) { + try { + Order order = (Order) arg; + orderMapper.insert(order); + return LocalTransactionState.COMMIT_MESSAGE; + } catch (Exception e) { + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + + // 事务状态回查 + @Override + public LocalTransactionState checkLocalTransaction( + MessageExt msg) { + String orderId = msg.getKeys(); + Order order = orderMapper.selectById(orderId); + + if (order != null) { + return LocalTransactionState.COMMIT_MESSAGE; + } else { + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + } +} +``` + +## 分布式事务框架 + +### Seata + +Seata 是阿里开源的分布式事务解决方案,支持 AT、TCC、Saga、XA 四种模式。 + +#### AT 模式示例 + +```java +// 配置 +@Configuration +public class SeataConfig { + @Bean + public GlobalTransactionScanner globalTransactionScanner() { + return new GlobalTransactionScanner( + "order-service", "default"); + } +} + +// 使用 +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private InventoryFeignClient inventoryClient; + + @Autowired + private AccountFeignClient accountClient; + + @GlobalTransactional(name = "create-order", rollbackFor = Exception.class) + public void createOrder(Order order) { + // 本地事务 + orderMapper.insert(order); + + // 远程调用 + inventoryClient.decrease(order.getProductId(), order.getQuantity()); + accountClient.decrease(order.getUserId(), order.getAmount()); + + // 任何一步失败都会自动回滚 + } +} +``` + +## 方案对比与选择 + +| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 | +|------|--------|------|--------|----------| +| 2PC/3PC | 强一致 | 低 | 中 | 金融核心系统 | +| TCC | 最终一致 | 高 | 高 | 对性能要求高的场景 | +| Saga | 最终一致 | 高 | 中 | 长流程业务 | +| 本地消息表 | 最终一致 | 中 | 低 | 通用场景 | +| MQ事务消息 | 最终一致 | 高 | 低 | 异步场景 | +| Seata AT | 强一致 | 中 | 低 | 快速接入 | + +## 最佳实践 + +### 1. 幂等性设计 + +```java +@Service +public class IdempotentService { + + @Autowired + private RedisTemplate redis; + + public boolean processOrder(String orderId) { + String key = "order:" + orderId; + + // 使用 Redis SETNX 保证幂等 + Boolean success = redis.opsForValue() + .setIfAbsent(key, "1", 24, TimeUnit.HOURS); + + if (Boolean.TRUE.equals(success)) { + // 首次处理 + doProcess(orderId); + return true; + } else { + // 重复请求 + return false; + } + } +} +``` + +### 2. 超时控制 + +```java +@Service +public class TimeoutService { + + @HystrixCommand( + commandProperties = { + @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", + value = "3000") + }, + fallbackMethod = "fallback" + ) + public String callRemoteService() { + return remoteService.call(); + } + + public String fallback() { + return "服务降级"; + } +} +``` + +### 3. 补偿机制 + +```java +@Service +public class CompensationService { + + @Scheduled(fixedDelay = 60000) + public void compensate() { + // 查询超时未完成的事务 + List timeoutTxs = txMapper.selectTimeout(); + + for (Transaction tx : timeoutTxs) { + try { + // 执行补偿逻辑 + compensate(tx); + } catch (Exception e) { + log.error("补偿失败: {}", tx.getId(), e); + } + } + } +} +``` + +## 总结 + +分布式事务没有银弹,需要根据具体业务场景选择合适的方案: + +1. **金融核心系统**:使用 2PC/XA 保证强一致性 +2. **电商订单系统**:使用 TCC 或 Saga 保证最终一致性 +3. **消息通知系统**:使用本地消息表或 MQ 事务消息 +4. **快速接入**:使用 Seata AT 模式 + +关键是要理解 CAP 和 BASE 理论,在一致性、可用性和性能之间做出合理的权衡。 diff --git a/docs_en/about-the-author/README.en.md b/docs_en/about-the-author/README.en.md new file mode 100644 index 00000000000..5c8e902a5e9 --- /dev/null +++ b/docs_en/about-the-author/README.en.md @@ -0,0 +1,70 @@ +--- +title: Personal Introduction Q&A +category: About the Author +--- + + + +In this article, I will briefly introduce myself through a Q&A format. + +## When did I graduate? + +Many long-time readers probably know that I graduated with my bachelor's degree in 2019 and went straight to "retire" at a foreign company. + +My academic background is quite poor. I performed poorly on the college entrance exam, barely scoring 20 points above the cutoff for first-tier universities, and ended up at a very ordinary, non-prestigious university in Jingzhou. However, I'm glad I didn't give up on myself because of my school. Instead, I worked harder than most of my peers, and my university life was quite fulfilling. + +Here is a graduation photo taken at the time (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? + +Time flies! I started blogging in my sophomore year. Back then, I would casually post my study notes and the programs I wrote on blog platforms. For example, the article [Summary of "Computer Networks" by Mr. Xie Xiren](../cs-basics/network/computer-network-xiexiren-summary.md) was something I put together from the textbook while taking the computer networks course in my sophomore year. + +Many friends around me often ask, "Is it too late for me to start blogging now?" + +I think that if you want to do something, you should ask less about whether it's too late and more about whether it's worth it. As long as you feel it's meaningful, just start doing it as soon as possible! Life is wonderful. Every major decision we make affects the trajectory of our future lives. Whether it's for the better or worse, only we will know! + +For me, the decision to stick with blogging has had a very positive impact on my life's trajectory! So, I also recommend that everyone develop the habit of blogging regularly. + +## How much money did I earn during university? + +During my time at university, I earned over 200,000 RMB through running tutoring classes, taking on freelance projects, providing technical training, and participating in programming competitions, successfully achieving "financial independence." I used the money I earned to travel to places like Chongqing, Sanya, Enshi, and Qingdao. I also contributed a lot to my family, reducing my parents' burden. + +This photo was taken during my freshman year when I was running a tutoring class (the last meal before leaving): + +![The last dinner at the tutoring center](https://oss.javaguide.cn/p3-juejin/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.jpeg) + +This photo was taken when I went to Sanya in my junior year: + +![](https://oss.javaguide.cn/javaguide/psc.jpeg) + +Actually, the main reason I started working so hard to earn money in university was because my family's financial situation was very average, and my parents worked very hard to make money. It was precisely because I urgently wanted to lighten my parents' burden that I tried so many ways to earn money. + +I've found that many people in our profession as programmers come from average family backgrounds. A big reason for choosing this industry is not because they love it, but to earn more money. + +If you also want to make money through freelance projects, you can reply with "**freelance**" in my public account's backend to get some of my personal experience sharing. + +::: center + +![](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) + +::: + +## Why do I call myself Guide? + +It's probably because my project is named JavaGuide, which led many people to call me **Brother Guide**. + +Later, to make it easier for readers to address me, I changed my pen name to **Guide**. + +My early pen name for writing articles was SnailClimb. Many people don't know what this name means, but it becomes clear if you break it down. SnailClimb = Snail + Climb. I've been a big fan of Jay Chou's songs since I was a child, especially his song "Snail" 🐌. Also, I performed rather poorly on my college entrance exam, but after starting university, I was quite the "aspiring youth." So, I gave myself the pen name SnailClimb, which means I must keep climbing upwards, hehe 😁 + +![](https://oss.javaguide.cn/p3-juejin/37599546f3b34b92a32db579a225aa45~tplv-k3u1fbpfcp-watermark.png) + +## Afterword + +Follow your heart, travel light. Life is an adverse journey, sail with a single reed. + +Life is a mix of bitter and sweet. Let's encourage each other! + +![JavaGuide Official Public Account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) \ No newline at end of file diff --git a/docs_en/about-the-author/deprecated-java-technologies.en.md b/docs_en/about-the-author/deprecated-java-technologies.en.md new file mode 100644 index 00000000000..210943fdffb --- /dev/null +++ b/docs_en/about-the-author/deprecated-java-technologies.en.md @@ -0,0 +1,102 @@ +--- +title: 已经淘汰的 Java 技术,不要再学了! +category: 走近作者 +tag: + - 杂谈 +--- + +前几天,我在知乎上随手回答了一个问题:“Java 学到 JSP 就学不下去了,怎么办?”。 + +出于不想让别人走弯路的心态,我回答说:已经淘汰的技术就不要学了,并顺带列举了一些在 Java 开发领域中已经被淘汰的技术。 + +## 已经淘汰的 Java 技术 + +我的回答原内容如下,列举了一些在 Java 开发领域中已经被淘汰的技术: + +**JSP** + +- **原因**:JSP 已经过时,无法满足现代 Web 开发需求;前后端分离成为主流。 +- **替代方案**:模板引擎(如 Thymeleaf、Freemarker)在传统全栈开发中更流行;而在前后端分离架构中,React、Vue、Angular 等现代前端框架已取代 JSP 的角色。 +- **注意**:一些国企和央企的老项目可能仍然在使用 JSP,但这种情况越来越少见。 + +**Struts(尤其是 1.x)** + +- **原因**:配置繁琐、开发效率低,且存在严重的安全漏洞(如世界著名的 Apache Struts 2 漏洞)。此外,社区维护不足,生态逐渐萎缩。 +- **替代方案**:Spring MVC 和 Spring WebFlux 提供了更简洁的开发体验、更强大的功能以及完善的社区支持,完全取代了 Struts。 + +**EJB (Enterprise JavaBeans)** + +- **原因**:EJB 过于复杂,开发成本高,学习曲线陡峭,在实际项目中逐步被更轻量化的框架取代。 +- **替代方案**:Spring/Spring Boot 提供了更加简洁且功能强大的企业级开发解决方案,几乎已经成为 Java 企业开发的事实标准。此外,国产的 Solon 和云原生友好的 Quarkus 等框架也非常不错。 + +**Java Applets** + +- **原因**:现代浏览器(如 Chrome、Firefox、Edge)早已全面移除对 Java Applets 的支持,同时 Applets 存在严重的安全性问题。 +- **替代方案**:HTML5、WebAssembly 以及现代 JavaScript 框架(如 React、Vue)可以实现更加安全、高效的交互体验,无需插件支持。 + +**SOAP / JAX-WS** + +- **原因**:SOAP 和 JAX-WS 过于复杂,数据格式冗长(XML),对开发效率和性能不友好。 +- **替代方案**:RESTful API 和 RPC 更轻量、高效,是现代微服务架构的首选。 + +**RMI(Remote Method Invocation)** + +- **原因**:RMI 是一种早期的 Java 远程调用技术,但兼容性差、配置繁琐,且性能较差。 +- **替代方案**:RESTful API 和 PRC 提供了更简单、高效的远程调用解决方案,完全取代了 RMI。 + +**Swing / JavaFX** + +- **原因**:桌面应用在开发领域的份额大幅减少,Web 和移动端成为主流。Swing 和 JavaFX 的生态不如现代跨平台框架丰富。 +- **替代方案**:跨平台桌面开发框架(如 Flutter Desktop、Electron)更具现代化体验。 +- **注意**:一些国企和央企的老项目可能仍然在使用 Swing / JavaFX,但这种情况越来越少见。 + +**Ant** + +- **原因**:Ant 是一种基于 XML 配置的构建工具,缺乏易用性,配置繁琐。 +- **替代方案**:Maven 和 Gradle 提供了更高效的项目依赖管理和构建功能,成为现代构建工具的首选。 + +## 杠精言论 + +没想到,评论区果然出现了一类很常见的杠精: + +> “学的不是技术,是思想。那爬也是人类不需要的技术吗?为啥你一生下来得先学会爬?如果基础思想都不会就去学各种框架,到最后只能是只会 CV 的废物!” + + + +这句话表面上看似有道理,但实际上却暴露了一个人的**无知和偏执**。 + +**知识越贫乏的人,相信的东西就越绝对**,因为他们从未认真了解过与自己观点相对立的角度,也缺乏对技术发展的全局认识。 + +举个例子,我刚开始学习 Java 后端开发的时候,完全没什么经验,就随便买了一本书开始看。当时看的是 **《Java Web 整合开发王者归来》** 这本书(梦开始的地方)。 + +在我上大学那会儿,这本书的很多内容其实已经过时了,比如它花了大量篇幅介绍 JSP、Struts、Hibernate、EJB 和 SVN 等技术。不过,直到现在,我依然非常感谢这本书,带我走进了 Java 后端开发的大门。 + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/prattle/java-web-integration-development-king-returns.png) + +这本书一共 **1010** 页,我当时可以说是废寝忘食地学,花了很长时间才把整本书完全“啃”下来。 + +回头来看,我如果能有意识地避免学习这些已经淘汰的技术,真的可以节省大量时间去学习更加主流和实用的内容。 + +那么,这些被淘汰的技术有用吗?说句实话,**屁用没有,纯粹浪费时间**。 + +**既然都要花时间学习,为什么不去学那些更主流、更有实际价值的技术呢?** + +现在本身就很卷,不管是 Java 方向还是其他技术方向,要学习的技术都很多。 + +想要理解所谓的“底层思想”,与其浪费时间在 JSP 这种已经不具备实际应用价值的技术上,不如深入学习一下 Servlet,研究 Spring 的 AOP 和 IoC 原理,从源码角度理解 Spring MVC 的工作机制。 + +这些内容,不仅能帮助你掌握核心的思想,还能在实际开发中真正派上用场,这难道不比花大量时间在 JSP 上更有意义吗? + +## 还有公司在用的技术就要学吗? + +我把这篇文章的相关言论发表在我的[公众号](https://mp.weixin.qq.com/s/lf2dXHcrUSU1pn28Ercj0w)之后,又收到另外一类在我看来非常傻叉的言论: + +- “虽然 JSP 很老了,但还是得学学,会用就行,因为我们很多老项目还在用。” +- “很多央企和国企的老项目还在用,肯定得学学啊!” + +这种观点完全是钻牛角尖!如果按这种逻辑,那你还需要去学 Struts2、SVN、JavaFX 等过时技术,因为它们也还有公司在用。我有一位大学同学毕业后去了武汉的一家国企,写了一年 JavaFX 就受不了跑了。他在之前从来没有接触过 JavaFX,招聘时也没被问过相关问题。 + +一定不要假设自己要面对的是过时技术栈的项目。你要找工作肯定要用主流技术栈去找,还要尽量找能让自己技术有成长,干着也舒服点。真要是找不到合适的工作,去维护老项目,那都是后话,现学现卖就行了。 + +**对于初学者来说别人劝了还非要学习淘汰的技术,多少脑子有点不够用,基本可以告别这一行了!** + diff --git a/docs_en/about-the-author/dog-that-copies-other-people-essay.en.md b/docs_en/about-the-author/dog-that-copies-other-people-essay.en.md new file mode 100644 index 00000000000..048f6b3de28 --- /dev/null +++ b/docs_en/about-the-author/dog-that-copies-other-people-essay.en.md @@ -0,0 +1,56 @@ +--- +Title: Plagiarism dog, your feet will be cold when you sleep in winter! ! ! +category: Get to know the author +tag: + - Miscellaneous talk +--- + +Plagiarism dogs are really annoying. . . + +I heard from a friend that my article was stolen again on Zhihu, and was used intact by others to divert traffic. + +![](https://oss.javaguide.cn/p3-juejin/39f223bd8d8240b8b7328f7ab6edbc57~tplv-k3u1fbpfcp-zoom-1.png) + +And! ! ! This is not the most annoying thing. + +The original source that this person also noted at the end of the article is not mine. . . + +![](https://oss.javaguide.cn/p3-juejin/fa47e0752f4b4b57af424114bc6bc558~tplv-k3u1fbpfcp-zoom-1.png) + +In other words, there is another plagiarism dog in CSDN who stole this article of mine and claimed the originality. Zhihu plagiarism dog has moved the article of this CSDN plagiarism dog intact. + +It’s so outrageous that his mother opened the door for outrageous things. It’s so outrageous. + +![](https://oss.javaguide.cn/p3-juejin/6f8d281579224b13ad235c28e1d7790e~tplv-k3u1fbpfcp-zoom-1.png) + +I opened the original source link marked by Zhihu Plagiarism Dog. Oh my god, it’s exactly the same content, and it also shows the originality. + +![](https://oss.javaguide.cn/p3-juejin/6a6d7b206b6a43ec9b0055a8f47a30be~tplv-k3u1fbpfcp-zoom-1.png) + +I took a look at the CSDN plagiarism dog's article. He has moved my highly praised answer all over the place. . . Really diligent. . . + +I don’t want to say more about CSDN. It’s just a large article dump, filled with all kinds of non-standard reprints and all kinds of garbage resources for paid downloads. This technology website, which is said to have the largest traffic in the country, is disgusting. It’s so popular that it’s ugly. If you can’t use it, don’t use it! + +For example, when I usually search on Google, I directly block the CSDN site. Just download a Chrome extension called Personal Blocklist and add blog.csdn.net to the blacklist. + +![](https://oss.javaguide.cn/p3-juejin/be151d93cd024c6e911d1a694212d91c~tplv-k3u1fbpfcp-zoom-1.png) + +My articles have basically been stolen. The key is that I don’t get much traffic when I post them myself. On the contrary, the people who steal my articles have more traffic than me, the original author. + +What kind of world is this? Is it the distortion of human nature or the loss of morality? + +However, it doesn’t matter. It doesn’t matter if CSDN, a junk website, doesn’t post articles. + +Take a look at what kind of garbage the articles on the CSDN hot list are. They are either various advertisements or some patchwork articles with no quality. + +![](https://oss.javaguide.cn/p3-juejin/cd07efe86af74ea0a07d29236718ddc8~tplv-k3u1fbpfcp-zoom-1-20230717155426403.png) + +Of course, there are also a very small number of high-quality articles, such as articles by bloggers such as Tao Ge, Er Ge, Binghe, and Micro Technology. + +There are also many video platforms (such as Douyin and Bilibili) where many bloggers directly use other people’s original works to make videos to attract traffic or fans. + +The stolen article mentioned today was once used by a training institution to make a video to attract traffic. + +![](https://oss.javaguide.cn/p3-juejin/9dda1e36ceff4cbb9b0bf9501b279be5~tplv-k3u1fbpfcp-zoom-1.png) + +As individuals, we have no choice but to report everyone we encounter. . . \ No newline at end of file diff --git a/docs_en/about-the-author/feelings-after-one-month-of-induction-training.en.md b/docs_en/about-the-author/feelings-after-one-month-of-induction-training.en.md new file mode 100644 index 00000000000..6a4858e307f --- /dev/null +++ b/docs_en/about-the-author/feelings-after-one-month-of-induction-training.en.md @@ -0,0 +1,24 @@ +--- +title: Reflections After One Month of Induction Training +category: About the Author +tag: + - Personal Experience +--- + +It's hard to believe it's been over a month since I started. Before this, I had never interned or worked at a company, so many things were quite new to me. The transition from school to the workplace brings a change in roles, and the differences vary from person to person. For me, in school, I could selectively absorb what the teacher taught based on my interests, and I could even skip classes I didn't want to attend. It's different at a company. You have to learn the skills the company requires, unless you want to quit. In school, most people code to pass exams or find a good job; very few are driven by genuine interest. In the workplace, we code more because of job requirements, which is generally more challenging and stressful than in school. In school, our main responsibility is to ourselves, constantly learning to arm ourselves with knowledge. But at a company, we are responsible not only to ourselves but also to the company. After all, the company is paying you, not for you to be "on the beach." + +When I first joined the company, I switched to a Mac computer as required. Since I had always used Windows, I was very uncomfortable with it. I was clumsy at first and felt my programming efficiency drop by at least 30%. I was quite frustrated and often complained to myself about why we couldn't just use Windows or Linux. But strangely, after about a week, I slowly started to get used to programming on a Mac and even grew to like it. I don't want to compare the programming experience of Mac and Windows here; I think it varies from person to person. For the same price, a Mac's hardware specs are indeed a few steps behind Windows. However, the programming and user experience on a Mac are excellent. Of course, you can also choose to use Linux for daily development, which I believe would be great. Also, you can't play some mainstream online games on a Mac, which is a good choice for friends who can't resist the temptation to play games. + +I have to say that ThoughtWorks' training program is very good. Graduates are usually scheduled for training after joining. Unlike previous years, this year's training included a local China class (TWU-C). As a member of the first local class, I can honestly say it was great. The 8-week training, in addition to basic technologies needed for work like ES6 and SpringBoot, also included training on essential skills for new employees, such as how to hold efficient meetings, how to give proper feedback, how to refactor code, how to practice TDD, and so on. There were also regular activities during the training, such as Weekend Trips, City Tours, Cake Time, etc. The last three weeks included a simulated real-world project, which was very similar to the actual projects we work on, and I found it to be a great experience. This project has now completed one iteration, and I feel that the biggest takeaway was not the technology used, but how to collaborate as a team, how to use Git for team development, what a complete iteration looks like, what problems might be encountered during a project, and the entire workflow of a project. + +ThoughtWorks strongly encourages sharing and helping others grow, and I have felt this deeply during my time at the company. During the training, each of us had a Trainer who was responsible for our daily classes and projects. One Trainer was responsible for about 5-6 people. The Trainers regularly gave us feedback on our recent performance. I don't think this was just a formality; the Trainers were very responsible and often chatted with us after work. Colleagues are also very enthusiastic. If you have a problem and ask someone, they will usually tell you everything they know without reservation. If it's a problem that most people don't understand, they might even organize a technical session to share knowledge. Last Friday, I gave a technical sharing session in our group about Feign remote calls because the team was not very familiar with this topic, but it was likely to be used in future project development. I had just researched this topic, so I shared it with other colleagues in the group to help the project proceed more smoothly. + +In addition, ThoughtWorks is a company that strongly advocates a feedback culture. Feedback is about telling people our opinion of their performance and how they can do better. At first, I didn't pay much attention to it, but gradually I realized that giving proper feedback can be very helpful to others. This is because when people are doing things, it's hard for them to notice small problems that are obvious to others. For example, there's an interesting phenomenon: if you don't have a tester role in your project, and you've completed your module and tested it many times yourself, you might think there are no problems. However, when it's actually used, problems you never noticed before are very likely to appear. The explanation for this is that everyone has blind spots in their vision, which are closely related to our focus. We often can't find many problems in our own work even after testing it many times, but if we ask someone else to test it for us, they are very likely to find many obvious issues. + +![](https://oss.javaguide.cn/github/about-the-author/feedback.png) + +After starting work, I have less time to update my public account, columns, and maintain my GitHub. In fact, many times after work, I have my own time to do my own things, but I always make excuses like being too tired from work or having fragmented time. Today, I looked at GitHub and suddenly realized that I hadn't handled a PR that someone submitted to me 14 days ago. This is indeed something I haven't done well; I haven't managed my time properly. I actually have a lot of things I want to write, and I will gradually put them on the agenda. After starting work, I've realized how important it is to spend the few hours after work. If you feel you haven't finished your work during the day, you can continue working on it after you get off work. If you can handle your daytime work with ease, you can do things you are interested in and learn technologies you are interested in after you get home. Do everything based on your own foundation, and don't aim too high. + +After starting work, I am also surrounded by many talented people. I think every professional should learn from others. Among the colleagues who trained with us this time, some are very skilled technically, while others may not be as strong technically but have excellent organizational and teamwork skills. There is a particularly brilliant colleague who, while we were still learning the various syntaxes of SpringBoot, wrote a simplified version of SpringBoot in his spare time, covering some common Spring annotations like `@RestController`, `@Autowired`, `@PathVariable`, `@RequestParam`, and so on (I have already contacted this colleague and asked him to open-source it, and I will share it on my public account as soon as possible, so look forward to it!). I think this colleague has a real interest in programming. He seems to have started programming in junior high school and is very interested in various underlying knowledge. He has written and implemented many low-level things himself. His dream is to create a project with over 20k stars on GitHub. I believe that with his abilities, he will definitely achieve this goal. I wish him all the best and hope he can achieve this goal soon. + +These are my personal feelings after more than a month of work. Many parts are just touched upon briefly. I will take the time to share more useful knowledge I have learned at the company or in my spare time with you all. I hope everyone who reads it can gain something. diff --git a/docs_en/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.en.md b/docs_en/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.en.md new file mode 100644 index 00000000000..dd10aebde90 --- /dev/null +++ b/docs_en/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.en.md @@ -0,0 +1,54 @@ +--- +title: Reflections on the Six Months Since Graduation and Starting Work +category: About the Author +tag: + - Personal Experience +--- + +If you've read my previous introductions, you'll know I'm one of the millions of fresh graduates from 2019. This article is mainly about my feelings after working for over half a year. It contains many of my own subjective thoughts, so if you disagree with anything, feel free to say so in the comments. I respect others' opinions. + +Let me briefly describe my situation. I'm currently working at a foreign company, and my daily job, like most people's, is development. It's been over half a year since I graduated, and I've passed the company's 6-month probation period. I've worked on two business-oriented projects, one of which is ongoing. You might find it hard to imagine that the backends of these two projects don't involve distributed systems or microservices, nor have I had practical experience with "high-end" technologies like Redis or Kafka. + +The first project was an internal one—an employee growth system. Beyond the name, the system was essentially for performance appraisals, like evaluating your performance in a project team. The tech stack was Spring Boot, JPA, Spring Security, K8S, Docker, and React. The second project, which I'm currently working on, integrates a game (Cocos), a web admin panel (Spring Boot + Vue), and a mini-program (Taro). + +Yes, most of my time at work is related to CRUD, and I also write front-end pages every day. A friend of mine was baffled when he heard that most of my work involves writing business logic. He thought that just writing business code doesn't lead to growth. What? You're a fresh graduate who can't even write business code properly, and you're telling me this! So, **I'm puzzled as to why so many people who can't even write good business code have an aversion to CRUD. At least in my time working, I feel my code quality has improved, my ability to locate problems has greatly enhanced, I have a deeper understanding of the business, and I can now independently handle some front-end development.** + +Personally, I don't think writing good business code is that easy. Before complaining about doing CRUD work all day, take a look at whether your own CRUD code is well-written. In other words, while writing CRUD, have you figured out the common annotations or classes you use? It's like someone who only knows the simplest annotations like `@Service`, `@Autowired`, and `@RestController` claiming to have mastered Spring Boot. + +I don't know when it started, but everyone seems to think that having practical experience with Redis or MQ is impressive. This might be related to the current interview environment. You need to differentiate yourself from others, and if you want to get into a big tech company, it seems you must be proficient in these technologies. Well, not "seems"—let's be confident and say that for most job seekers, these technologies are considered a prerequisite. + +**To be honest, I fell into this "false proposition" in college.** In my sophomore year, I was exposed to Java because I joined a tech-oriented campus media group. At that time, our goal for learning Java was to develop a campus app. As a sophomore with only a beginner's level of programming skills, it took me some time to master the basics of Java. Then, I started learning Android development. + +It wasn't until the first semester of my junior year that I truly decided to pursue a career in Java backend development. After about three months of learning the basics of web development, I started studying distributed systems concepts like Redis and Dubbo. I learned through a combination of books, videos, and blogs. During my self-study, I completed two full projects by following video tutorials: one was a regular business system, and the other was a distributed system. **At that time, I thought I was pretty awesome after finishing them. I felt that simple CRUD work was beneath my skill level. Haha! Looking back now, I was so naive!** + +And then, problems arose when I worked on a project with a professor during my junior year summer break. The project was a performance appraisal system with moderate business complexity. The tech stack was SSM + Shiro + JSP. At that time, I encountered all sorts of problems. I couldn't write code that I thought I knew how to write. Even a simple CRUD operation took me several days. So, I was reviewing, learning, and coding at the same time. Although it was tiring, I learned a lot and became more grounded in my approach to technology. I think the phrase "**this project is no longer maintainable**" is the biggest negation of the project I worked on. + +Technology is ever-changing; mastering the core principles is what matters. A few years ago, we might have been using Spring with traditional XML for development, but now almost everyone uses Spring Boot to speed up development. Similarly, a few years ago, we might have used ActiveMQ for message queuing, but today, it's hardly used. The more common choices are RocketMQ and Kafka. With technology evolving so quickly, you can't possibly learn every single framework or tool. + +**Many beginners want to learn by doing projects right away, especially in a company setting. I think this is not advisable.** If your Java or Spring Boot fundamentals are weak, I suggest you study them beforehand before starting a project through videos or other means. **Another point is, I don't know why everyone says that learning while working on a project is the most effective way. I think this requires a prerequisite: you must have a basic understanding of the technology or a certain level of programming knowledge.** + +**Key point!!! If your foundation is not solid, simply following along with a video is useless. You'll find that after watching the video, you still can't write the code yourself.** + +I don't know what it's like for programmers at other companies. I feel that technical growth largely depends on what you do in your spare time. Relying solely on work, in most cases, will only make you more proficient at completing tasks. Of course, after writing a lot of code, your understanding of code quality will improve to some extent (provided you are conscious of it). + +Outside of work, I use my spare time to learn things I'm interested in. For example, in my first project at the company, we used Spring Security + JWT. Since I wasn't familiar with it, I spent about a week of my own time learning it and created a demo to share. GitHub address: . This also led me to share: + +- ["Distinguishing Between Authentication, Authorization, and 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) +- ["Analysis of JWT Authentication Pros and Cons and Common Problem Solutions"](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485655&idx=1&sn=583eeeb081ea21a8ec6347c72aa223d6&chksm=cea2471cf9d5ce0aa135f2fb9aa32d98ebb3338292beaccc1aae43d1178b16c0125eb4139ca4&token=1737409938&lang=zh_CN#rd) + +Another recent example is that during the time I was at home due to the pandemic, I taught myself Kafka and am preparing a series of introductory articles. I have already completed: + +1. Kafka Introduction in Plain Language; +2. Kafka Installation and Basic Functionality; +3. Integrating Kafka with Spring Boot to Send and Receive Messages; +4. Handling Transactions and Error Messages with Spring Boot and Kafka. + +Still to be completed: + +1. Analysis of advanced Kafka features like workflow and why Kafka is fast; +2. Source code analysis; +3. ... + +**Therefore, I believe that technical accumulation and growth largely depend on the time spent outside of work (experts and exceptionally talented individuals excluded).** + +**There is still a long way to go. Even with all the energy in the world, you can't learn every technology you want to. Make appropriate trade-offs, compromises, and have some fun.** diff --git a/docs_en/about-the-author/internet-addiction-teenager.en.md b/docs_en/about-the-author/internet-addiction-teenager.en.md new file mode 100644 index 00000000000..c09d23a5a39 --- /dev/null +++ b/docs_en/about-the-author/internet-addiction-teenager.en.md @@ -0,0 +1,152 @@ +--- +title: I Used to Be a Gaming Addict +category: About the Author +tag: + - Personal Experience +--- + +> This article was written on the eve of the 2021 college entrance examination. + +When it comes to the college entrance exam, it seems like countless people have a lot to say. Today, I'll take this opportunity to briefly talk about my high school experience. + +To be honest, my high school journey was anything but ordinary, even a bit surreal, so I have a lot I want to share. + +This article will cover my journey from middle school to high school. I won't go into too much detail in each part, just a simple chat. + +**Everything that follows is true, with no exaggeration, though it might seem a little surreal.** + +## First Contact with Computers + +I first encountered computers when I was in the fifth grade. We didn't have a computer at home, so my first internet experiences were in illegal internet cafes. + +An illegal internet cafe looked something like this: a windowless room packed with old computers, very crowded. + +![Illegal Internet Cafe](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/heiwangba.png) + +My time in these cafes was full of ups and downs. I often ran into police inspections or got harassed by older kids. In over a year of going to these cafes, I was there for two police inspections, which were mainly to check for minors (the cafes were almost entirely filled with them). It felt more like they were trying to get a little something from the owner. Harassment from older kids was more frequent. They would often take my computer and threaten me for all my money. I was quite timid back then, and after being beaten a few times, I tried to go at times when the older kids weren't around and only carried a little money with me. I was a rather independent child and usually didn't tell my family about things that happened outside (because it wouldn't have helped much; my family didn't provide a strong sense of security). + +I can't quite remember if it was my brother or my sister who first took me to an internet cafe. I think it was my sister. + +At first, I just played single-player games like *Meteor Blade* and motorcycle games. I wasn't addicted, but I found them really fun. I could play for a whole afternoon and would be reluctant to leave. + +![](https://oss.javaguide.cn/github/javaguide/books2a6021b9-e7a0-41c4-b69e-a652f7bc3e12-20200802173601289.png) + +## Becoming Addicted After Elementary School + +My addiction started after I graduated from elementary school, after I started playing a game called **QQ Speed** (I think I started at the end of sixth grade). Damn, I was completely hooked. **Every day in class, I would fantasize about drifting in a car. That's right, I thought I was the God of Racing of Mount Akina!** + +My skills were pretty good at the time. It seemed like no one in the entire internet cafe could beat me (we often had open challenges). + +QQ Speed was quite popular back then, and many people born in the 90s should be familiar with it. + +I remember you didn't need an ID to get online back then. You could just get a 10-yuan internet card, and the fee was one yuan per hour. I often skipped breakfast to save money for gaming. Whenever I had money in my pocket, my friends and I would race to the internet cafe to play QQ Speed together. Good times! + +> Speaking of which, I can't help but open my Windows computer, download Wegame, and then download QQ Speed. + +By the second year of middle school, I had stopped playing QQ Speed. My level was permanently stuck at **120**, which was a very high level back in those days when leveling up was incredibly difficult. + +![](https://oss.javaguide.cn/javaguide/b488618c-3c25-4bc9-afd4-7324e27553bd-20200802175534614.png) + +## The Addiction Exploded in Eighth Grade + +My gaming addiction really took off after I started middle school. It was at its worst in the eighth grade. I was completely obsessed with a game called **CrossFire**, even more so than QQ Speed. Every day in class, I would imagine myself holding a gun and sweeping through the enemy's camp. My mind was not on my studies at all. + +I would often wake up early to use the computers left by people who had played all night, since I didn't have much money as a student. I would sneak out almost every Friday night after my family was asleep to play all night. I pulled countless all-nighters in eighth grade, and that's how I became nearsighted. + +Being a gaming addict is terrifying. You'd do anything to get online. My family lived in the insulation layer on the top floor. Every time I snuck out at night to play, I had to climb out of my room's window, cross several buildings, and pass through a few uninhabited top-floor insulation layers to get downstairs, all to avoid being caught. Thinking back, it was quite dangerous. Plus, I've always been afraid of the dark. But for the sake of gaming, I was never scared crossing all those empty top floors. If you asked me to do it now, I wouldn't dare. I really admire my younger self! + +![Snowy scene from my rooftop](https://oss.javaguide.cn/about-the-author/image-20230429114622340.png) + +After an all-nighter on Friday, I would sleep until noon and then go back to the internet cafe in the afternoon. On Sundays, I would basically play from 8 AM to 9 or 10 PM. I had so much energy back then. I never felt tired; in fact, I enjoyed it. + +My final rank was two diamonds. Anyone who played back then knows how many games that took (it's easier to rank up now). + +![](https://oss.javaguide.cn/about-the-author/cf.png) + +ps: I've been back to playing CF for almost a year now, and my current rank is Lieutenant Colonel 3 with two stars. + +My grades were pretty bad back then. Let's put it this way: I was in a very ordinary county-level high school with about 500 students in my grade, and I was usually ranked around 280th. Moreover, I didn't study physics at all in eighth grade. I would just sleep in physics class and turn in blank exam papers. + +Why was I so resistant to physics? It was because of a physics class early in the semester. The physics teacher mistakenly thought I was eating in class and arguing, so he slapped me. I held a grudge until college, thinking that one day I would beat up this physics teacher. + +## Starting to Study in Ninth Grade + +In the first semester of ninth grade, I suddenly had an awakening, as if a switch had been flipped. I realized I was about to go to high school and needed to start studying seriously. + +Well, it wasn't exactly an awakening. The main reason was to be able to go to a school near my home, which would make it easier to get online. My home was near the local No. 2 High School, which had tons of internet cafes nearby, making it super convenient to play games. Plus, I could commute from home. + +In my middle school, you basically had to be in the top 80 of the grade to have a chance of getting into No. 2 High School. Through hard work, in the first monthly exam of the first semester of ninth grade, I jumped from over 280th to over 50th in the grade, giving me a chance to get into No. 2 High School. Because of my significant improvement, I was named a **"Star of Progress"** and had to give a speech to the whole grade, sharing my experience. It was my first time speaking in front of so many people. I was nervous, but it felt great. I really showed off in front of my crush. + +Actually, in ninth grade, my gaming addiction was still quite strong. However, I only played games after I had finished all my homework, and I paid relatively close attention in class. + +In ninth grade, I pulled fewer all-nighters, but I would often sneak out at night after my family was asleep and play until after 2 AM before coming back. + +At that time, our local high schools had a policy where outstanding students from each school could take an **early high school entrance exam**. If you passed, you didn't have to take the main high school entrance exam. I was lucky enough to take this exam and successfully got into our local No. 2 High School. + +The night before my early high school entrance exam, I waited until my mom was asleep at midnight, then ran to the internet cafe to play CF until after 3 AM. That was the one time I got caught red-handed. When I got home, I found my mom sitting in the living room waiting for me. After a scolding, I promised never to sneak out at night again. + +> A note here: I'm self-aware about my intelligence; it's pretty average. The main reason for my great progress was my solid foundation, especially in English and physics. I liked English, and I had learned a lot of the middle school English curriculum in elementary school. Physics was strange. Even though I didn't pay much attention in physics class in eighth grade and didn't know the subject, I suddenly got the hang of it in ninth grade. Really! I still find it strange now. Then, in high school, English and physics remained my two best subjects. When I worked as a tutor in college, I taught high school physics. + +## Dropping from the Advanced Class to a Regular Class in High School + +![A photo taken at my high school after the college entrance exam results came out](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/wodegaozhong.png) + +Because I took the early high school entrance exam, I started high school four months early in an advanced class, learning the high school curriculum. + +After starting high school, I would secretly read novels in class. I read many novels like *Sealed Divine Throne*, *Douluo Dalu*, and *Battle Through the Heavens* during that time. When I went home at noon and in the evening, I would play a few rounds of DNF. My family had bought a computer by then, a gift from my grandpa for getting into No. 2 High School. By the time I uninstalled DNF, I had four max-level characters and two close to max level. + +At that time, I had a dedicated album in my social media space filled with DNF photos and screenshots. I was obsessed with leveling up and running dungeons. + +Less than a month into high school, I broke my leg during a PE class. It was the first time I felt the pain of a broken bone. It was excruciating! + +So, I had to take a leave of absence to recover. I didn't go back to school until a month after high school officially started, and I missed the military training. + +Because I had missed several months of classes, I couldn't rejoin the advanced class and had to transfer to the Olympiad class. In the Olympiad class, I continued to devote my time and energy to games and novels, so my grades were near the bottom. When the classes were reorganized in the second year of high school, I was successfully kicked out of the Olympiad class and placed in the most ordinary regular class. + +**I successfully played my way from the best advanced class to the Olympiad class, and then to a regular class. A bit surreal, right?** + +## Catching Up in My Sophomore Year + +My high school awakening happened in the second semester of my sophomore year. I truly woke up then. Suddenly, games weren't appealing anymore. DNF wasn't fun. Killing monsters and getting gear was all just vanity. Having multiple max-level DNF accounts was useless. Without money, it was all for nothing. + +My mom was quite surprised at the time and asked me strangely, "Why aren't you playing games anymore?" (My mom didn't really control my gaming; she believed it was a matter of self-discipline). + +So, I started studying with all my might, immersing myself in my studies every day (no exaggeration), and I enjoyed it. Even though I got home around 11 PM after evening self-study, I didn't feel tired. Instead, I felt happy and fulfilled. + +**My efforts quickly paid off, and I successfully returned to the Olympiad class.** At that time, there were about seven regular science classes, and each exam was ranked separately among them. The advanced and Olympiad classes were not ranked with us. After that, I was basically able to get first place in the regular classes every time, often leading the second place by about 30 points. Because my grades were quite impressive, I applied to the grade director to join the Olympiad class near the end of the first semester of my senior year. + +## Insomnia Before the College Entrance Exam + +> **After a failure, don't blame external factors. From beginning to end, it's your own problem. You're not strong enough!** Then, the insomnia before the college entrance exam was also my own problem. I can only blame myself; there are no other excuses. + +My college entrance exam experience was quite rough. It's no exaggeration to say that those few days were probably the toughest time I've ever been through, especially at night. + +I had insomnia every night during the college entrance exam. The pain of wanting to sleep but being unable to is something many people may have experienced. + +Actually, I had never had insomnia before. On the eve of the exam, because I was afraid I wouldn't be able to sleep, I asked my mom to buy a few bottles of a calming and brain-boosting liquid recommended by my teacher. I still remember that this liquid was from the Aodong brand. + +![](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/image-20220625194714247.png) + +I think the insomnia during the exam might have been related to drinking the liquid recommended by the teacher, or maybe I was just too nervous. Because during those days, when I tried to sleep, it always felt like ants were crawling on my body, and I got some small pimples (a bit like an allergic reaction). + +I need to make a special point here to avoid misunderstanding: **Insomnia itself is one's own problem. The above statement is not meant to blame the supplement.** Also, I checked various platforms for this calming and brain-boosting liquid and found that the reviews were quite good, similar to the reasons my teacher recommended it. If you need to improve your sleep, you can try it after consulting a doctor. + +I really didn't perform well in the college entrance exam. I was in a daze in the exam hall. When the results came out, they were several tens of points lower than my own estimate. In the end, I only got into a non-prestigious university. However, I was lucky to have chosen a good major, benefited from the computer science boom, and worked hard during college. + +## College Life + +My college life was quite rich. I would occasionally pull all-nighters to code, and occasionally go out with classmates in the middle of the night to walk the ancient city walls or spend a night playing LOL at an internet cafe. + +I wrote a separate article about my college life: [Sigh, It's Been Three Years Since Graduation!](./my-college-life.md). + +## Conclusion + +Throughout middle school, I was something of a gaming addict, though I restrained myself a bit in the ninth grade. It wasn't until the second semester of my sophomore year of high school that I truly wasn't so obsessed with games anymore. + +The reason I became less addicted to games was that I realized that games are ultimately just a pastime, and studying was the most important thing at the time. Besides, my gaming skills weren't great, and I couldn't make a living from it. All the monster-slaying and leveling up were just binary data in a computer in the end. + +**You have to realize this yourself. Otherwise, it's really hard to change just by relying on parental supervision! If your heart isn't in your studies, you can't possibly learn well!** + +I'm really against parents interfering too much in their children's lives. I strongly condemn many parents who blame online games for their children's internet addiction and blame film and television for their children's violence. + +**It's not a reliable thing to always protect your child. They will eventually have to face more and more temptations alone. By the time they get to college, many children who have been overprotected by their parents are completely lost. They have no sense of independence and no willpower to resist temptation!** \ No newline at end of file diff --git a/docs_en/about-the-author/javaguide-100k-star.en.md b/docs_en/about-the-author/javaguide-100k-star.en.md new file mode 100644 index 00000000000..43641135adc --- /dev/null +++ b/docs_en/about-the-author/javaguide-100k-star.en.md @@ -0,0 +1,42 @@ +--- +title: JavaGuide open source project has 100K Stars! +category: Get to know the author +tag: + - personal experience +--- + +2021-03-21, at 12 o'clock in the evening, after finishing a certain front-end function of a project I was working on, I opened [my GitHub homepage](https://github.com/Snailclimb). + +Good guy! I didn’t notice it for a few days, but the [JavaGuide](https://github.com/Snailclimb/JavaGuide) project directly reached 100K stars. + +![JavaGuide 100k milestone](https://oss.javaguide.cn/github/javaguide/1&e=1643644799&token=kIxbL07-8jAj8w1n4s9zv64FuZZNEATmlU_Vm6zD:zANqh9HQEvvLPm6smyrjvjAt-Ik=.png) + +Actually, there's really nothing to complain about. Because the value of tutorials is actually relatively low, and the number of stars is relatively large mainly because the audience is relatively broad. Everyone thinks it is good, and clicking a star is equivalent to collecting it. For many particularly excellent frameworks, the number of stars may only be a few K. Therefore, simply looking at the number of stars is meaningless, just treat it as a joke! + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/20210323132635635.png) + +In the process of maintaining this project, some people also dissed it: "The md project has no gold content and is an embarrassment to the Chinese people!". + +For people who say this kind of thing, I don’t think it has any impact on me, so I will continue to improve and make JavaGuide better! In fact, many foreign projects are also pure MD! For example, the awesome series and job interview series launched by foreign friends. No need to say more, actions speak for themselves! Damn! + +A very important aspect of open source is collaboration. If you open source a project and then no longer maintain it, and others submit issues/prs to you but you don’t handle it, then open source means nothing! + +The friends on my official account all follow me through this project. Taking advantage of my lunch break, I can briefly review it, which can be considered responsible for the friends who follow this project. + +When I started preparing for the fall recruitment interview in my junior year, I created the JavaGuide project. On **2018-05-07**, I submitted the **first commit** on this day. + +As of today (2021-03-23), this warehouse has accumulated **2933** commits, and a total of **207** friends have participated in the project. + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70.png) + +There are **511** **issues** and **575** **PR** in total. All PRs have been taken care of, and there are only about 15 issues that I haven't gotten around to yet. + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/watermark,type_ZmFuZ3poZW5naGVpdGk,shado w_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309224044850.png) + +In fact, compared to the number of stars, the issues and PRs in the warehouse can better illustrate whether your project is valuable. + +I won’t go into details about the behavior of people cheating on stars or even getting stars, it’s a bit embarrassing. If people think your project is good and can provide value, they will naturally give you stars. + +**In the next few years, I will continue to improve JavaGuide. ** + +**I hope I can open source some valuable wheels in the future! Keep up the good work! ** \ No newline at end of file diff --git a/docs_en/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.en.md b/docs_en/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.en.md new file mode 100644 index 00000000000..7f089724ef8 --- /dev/null +++ b/docs_en/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.en.md @@ -0,0 +1,72 @@ +--- +title: A training institution stole my article and made it into a video, which became popular on Bilibili +category: Get to know the author +tag: + - Miscellaneous talk +--- + +Back on 2021-02-25, when I was browsing Bilibili, I discovered that a certain UP owner of Bilibili (a training institution) made one of my answers on Zhihu into a video without permission. + +Original taste! Fuck. I even added the self-deprecation I made at the beginning! Real cowhide! + +If you steal my original work, you can just put your heart into making the video better! At least we can spread such high-quality content! + +As a result, my dear, the video was made like shit, and the dubbing was also inappropriate! + +Please, after reading this article, this training institution can consider changing someone to do similar disgusting things! This person has no brain at all! + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-db6b9cf323930786fa2bec8b1e1bfaad732.png) + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-6395603ab441b74511c6eda28efee8937d7.png) + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-921f60a5c7cee2c5c2eb30f4f7048f648e1.png) + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-acc82a797bd01e27f5b7d5d327b32a21d4e.png) + +I randomly found a video to watch, and found that it was still someone else's original work. + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-48d0c5ab086265ae19b7396bc59de2c2daf.png) + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-366abf0656007ff96551064104e60740a41.png) + +There is no need to read the other videos. Everyone should know whether they are still plagiarizing other people’s original works and making them into videos intact. + +They do this for one purpose: to divert traffic to their own QQ group, and then trick you into buying courses. ** + +I don't think this is entirely a problem with the training institutions. Employees of training institutions do such disgusting things for the sake of traffic, which has led to this kind of thing happening more and more frequently. + +Therefore, you will find that there are more and more small accounts from training institutions on Bilibili and Zhihu, plagiarizing original works and stealing content everywhere. + +Articles written by many original account owners around me are often stolen by some training institutions. + +Sometimes I get really angry because after all, your hard work is original, and someone else just copies and pastes it and it goes to waste! + +However, I believe that this kind of behavior of plagiarizing other people's original works to attract traffic is just the behavior of a clown after all! + +Only those creators who output content carefully can go further and be more stable! + +Later, I posted an article on my official account titled ["Good guy! A training institution stole my article and made it into a video, which became a hit.” m=cea18f2ef9d606384e0265b9318e004646c03b8a69f2801698d2f9e0e6bdfec0a1185ac3ab17&token=2146952532&lang=zh_CN&scene=21#wechat_redirect) article, complaining that his original work was used for free by an organization. + +Who would have thought that people from the training institution would find someone to ask me to delete the article! To be honest, these two people are really weird! + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/8f8ccafcf5b764a2289a9c276c30728d.png) + +![](https://oss.javaguide.cn/javaguide/a0a4a45d7ec7b1a2622b2a38629e9b09.png) + +Do you want me to have a bigger picture? Fuck you! It's obviously my original work, but instead of deleting it yourself, you asked someone to contact me to delete it! Do you have any brains? + +In fact, I am a person who is relatively easy to talk to, and I am also known for my good temper in real life (provided that my principles are not violated). + +What’s funny! While they asked me to delete the article, the videos they had stolen from Station B were still there, and they continued to attract traffic to them. + +![](https://oss.javaguide.cn/javaguide/86f659a93ce5b639526c8d2bd20b2fbe.png) + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/be6e0fd23146de3f6224b4d853c59ce7.png) + +If they cancel the account, I might consider letting it go. However, the article will definitely not be deleted. + +Now, let’s see what happens next! I can use the law to protect my rights at any time, it just depends on whether I want to, after all, it is quite troublesome, right? + +Don't worry, it's all a trivial matter. My girlfriend is studying law at a double first-class law school in China. + +We are not afraid of trouble! Damn! ! ! \ No newline at end of file diff --git a/docs_en/about-the-author/my-college-life.en.md b/docs_en/about-the-author/my-college-life.en.md new file mode 100644 index 00000000000..d312fd380c6 --- /dev/null +++ b/docs_en/about-the-author/my-college-life.en.md @@ -0,0 +1,387 @@ +--- +title: Sigh, It's Been Three Years Since Graduation! +category: About the Author +star: 1 +tag: + - Personal Experience +--- + +> For my life in middle and high school, you can read the article I wrote in 2020: [I Used to Be a Gaming Addict](./internet-addiction-teenager.md). + +I graduated in June 2019, and it has been three years since then. With the college entrance exams and graduation season upon us, I thought I'd briefly talk about my university life. + +Here is the main text. + +I graduated from Yangtze University, Jingzhou campus, a non-prestigious university. + +The four years I spent there were quite happy, and I still miss them very much! + +Overall, my life at university was quite colorful. I would occasionally pull all-nighters to code, and sometimes, in a fit of madness, I'd run out in the middle of the night with classmates to walk the ancient city walls or spend a whole night playing League of Legends at an internet cafe. + +I'm writing this essay to document my past university life! I hope to continue moving forward and never forget why I started! + +## Freshman Year + +During my freshman year, I didn't focus my energy on learning to code; I spent most of my time in extracurricular activities. + +Perhaps it was because I had come to a new city and was curious about everything around me. Or perhaps it was because I was still quite naive and had no clear direction for my studies. + +That year, I explored many places in Jingzhou with a group of new classmates, such as the Jingzhou Museum, the Yangtze River Bridge, the former residence of Zhang Juzheng, and the Guan Di Temple. + +![A freshman class outing](https://oss.javaguide.cn/about-the-author/college-life/41239dd7d18642f7af201292ead94f1a~tplv-k3u1fbpfcp-zoom-1.image.png) + +Even so, I was full of hope for the future and looked forward to life after graduation. + +I still remember a time when my six roommates and I were chatting. The other five thought that finding a job with a salary of 6k would be great. I said, "It should be at least 8k!" They were speechless, thinking my idea was too naive. + +Actually, in my heart, I was thinking of a starting salary of at least 10k per month, but I was too shy to say it out loud. + +I don't like to be in the spotlight and am a bit introverted. When I first started university, I was a little insecure and hesitant in everything I did. I was eager to change myself! + +So, with a burst of passion, I tried many things I had never tried before: **camping**, **outdoor barbecues**, **giving speeches on a bus**, **running around the ancient city wall**, **hiking**, **survival challenges in a strange place**, **selling apples at Christmas**, **performing at the New Year's Eve party**... + +My club mates and I made these during our spare time, and they were almost all sold out during Christmas week. I remember, to sell more, we went door-to-door in every dorm to promote them. + +![](https://oss.javaguide.cn/about-the-author/college-life/7cf1a2da505249a58e1f29834dbac435~tplv-k3u1fbpfcp-zoom-1.image.png) + +I also participated in the freshman New Year's Eve party, but I was still holding back during that performance. To be honest, I feel I didn't perform as well as I should have. + +![](https://oss.javaguide.cn/about-the-author/college-life/850cae1f8c644c5d920140f66ae9303d~tplv-k3u1fbpfcp-zoom-1.image.png) + +After that performance, I realized I truly have no talent for performing; I'm very stiff. And this stiffness is something that I can't change even with effort. + +The picture below was taken by a friend after I had a little too much to drink at a club dinner. + +![](https://oss.javaguide.cn/about-the-author/college-life/82a503e365354bd1bf190540fbf1039a~tplv-k3u1fbpfcp-zoom-1.image.png) + +Back then, I often went for night walks on the Jingzhou ancient city wall with a few friends from the club. + +![A photo I took on the way to a 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. + +Although these experiences didn't directly help my future career and development, they made my university life more complete, filled with more interesting events and memorable experiences. + +While my roommates were cooped up in the dorm playing games and on their phones, I'm glad I did these things. + +Personally, I feel that participating in some good club activities and meeting like-minded friends during freshman year is a great thing! + +**In addition to extracurricular activities, CS majors should try to develop good programming habits as early as possible, master a programming language, and practice algorithm problems in their spare time.** + +### Running a Tutoring Center + +During the summer of my freshman year, I was in charge of running five tutoring centers in the small towns of Xiaogan (originally seven, but two were cut). + +From renting houses and desks to recruiting students, everything was started from scratch. + +Every weekend, I would travel from Jingzhou to Xiaogan, running back and forth between various county towns. Most of the time, I was alone, but occasionally a few friends from the club would join me. + +![](https://oss.javaguide.cn/about-the-author/college-life/6ee6358c236144d8a8a205cc6bc99b9b~tplv-k3u1fbpfcp-zoom-1.image.png) + +I vividly remember that Xiaogan also had severe floods that year. + +![](https://oss.javaguide.cn/javaguide/image-20210820201908759.png) + +Once, I almost couldn't get back to school for my final exams. Although I hadn't prepared, I didn't fail any courses, and even did well in many subjects. However, this still had a significant impact on my GPA, causing me to miss out on scholarships later. + +![](https://oss.javaguide.cn/about-the-author/college-life/3c5fe7af43ba4e348244df1692500fce~tplv-k3u1fbpfcp-zoom-1.image.png) + +I was in a hurry this time, so I took a train back to school. On the train, I even bumped into someone else's suitcase! + +![](https://oss.javaguide.cn/about-the-author/college-life/570f5791aeb54fa1a76892b69e46fec2~tplv-k3u1fbpfcp-zoom-1.image.png) + +When I was in the small towns, the worst place I stayed was a 15-yuan hotel. Yes, 15 yuan, you read that right. It was a small, dilapidated room in an old residential building, with no private bathroom, unsanitary bedding, and no shower. + +The one below was the most luxurious place I stayed in. After taking a bus to Xiaogan, it suddenly started raining heavily, so I found a relatively cheap place near the station. + +![](https://oss.javaguide.cn/about-the-author/college-life/687c3ede3f094c65a72d812ca0f06bb4~tplv-k3u1fbpfcp-zoom-1.image.png) + +To rent a house at a lower price, I often haggled with landlords until I was red in the face. + +To be honest, these were things I was unwilling to do. I'm someone who cares a lot about face and isn't very confident. + +At that time, I had to travel between various towns, directly under the sun every day. Every meal was especially delicious; I could eat several bowls of rice with just a simple stir-fried vegetable. + +I'm usually a picky eater, but this experience made me truly understand that when you're hungry, everything tastes good! + +I cooked for six teachers, about ten students, and the landlords' families for over a month, and my cooking skills improved greatly as a result. + +![](https://oss.javaguide.cn/about-the-author/college-life/2e3b6101abcd46a8a213c08782aeac33~tplv-k3u1fbpfcp-zoom-1.image.png) + +These students were from elementary and middle school, and they were all quite obedient. Many were "left-behind children," whose parents worked elsewhere, leaving them with their grandparents. + +Including me, there were four teachers in total. I mainly taught middle and high school physics. + +The students were all well-behaved and never had any conflicts with us teachers. Only two mischievous elementary school students, after being reprimanded by me, held a grudge and wrote some things that I couldn't help but laugh at! Hahaha! So cute! + +![](https://oss.javaguide.cn/about-the-author/college-life/3680cead2c0f4165bb4865f038326b61~tplv-k3u1fbpfcp-zoom-1.image.png) + +The night before we left, the teachers and I decided to invite some of the nearby students over for dinner. We went out early in the morning to buy groceries. The picture below is the finished meal. Although it was a simple meal, we ate with great relish. + +![The last dinner at the tutoring center](https://oss.javaguide.cn/about-the-author/college-life/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.image.png) + +That evening, a few parents even came over specifically to watch me cook. They said their children loved my cooking, hahaha! On the surface, I acted nonchalant and said my cooking wasn't good, but inside I was secretly pleased. I'm just a "covertly smug" person, hahaha! + +I wonder how those students are doing now. I miss them! + +When the tutoring session ended and I went home, my parents thought I had come back from a famine. + +### Earning Money to Visit an Orphanage + +At the end of my freshman year, I did something very meaningful. My friends and I visited an orphanage (Jingzhou Private Orphanage). This orphanage had been reported on by several TV stations and is now included in Baidu Baike. + +![](https://oss.javaguide.cn/about-the-author/college-life/db8f5c276f4d4a7c9d7bd1e6100de301~tplv-k3u1fbpfcp-zoom-1.image.png) + +Most of the children in the orphanage were either parentless or had been abandoned by their parents due to illness. + +Before we went, we bought a lot of children's toys, stationery, and snacks. The source of this money was also meaningful; it was all money that my club mates and I had earned from part-time jobs. + +![A group photo with the old gentleman who founded the orphanage before leaving](https://oss.javaguide.cn/about-the-author/college-life/cf43853c49bd489a9fc0ee437a2af432~tplv-k3u1fbpfcp-zoom-1.image.png) + +No act of kindness, no matter how small, is ever wasted! To quote a line from the song "The Risk of Love": "If everyone gives a little love, the world will become a beautiful place." + +I wanted to see the current situation of the orphanage, so I searched online and saw a report from Jingzhou News Network from last January. + +![](https://oss.javaguide.cn/about-the-author/college-life/0ac27206389c498882dd7f6f440c6abb~tplv-k3u1fbpfcp-zoom-1.image.png) + +In its 33 years of operation, the orphanage has raised 85 orphans, 5 of whom have joined the army to serve the country, and 20 have gone to university. Some have already started working and have families of their own. + +The uncle is also getting older, with more and more white hair. It's a bit sad, I want to cry. I hope I have the chance to go back and see you again! I definitely will! + +![](https://oss.javaguide.cn/about-the-author/college-life/ea803a99c08149f892ca29e784653503~tplv-k3u1fbpfcp-zoom-1.image.png) + +### Hiking + +Another thing that left a deep impression on me during my freshman year was hiking. + +A group of friends from the club and I hiked for nearly 45 kilometers. We walked from the west campus of our school to a beach in Zhijiang. + +![](https://oss.javaguide.cn/about-the-author/college-life/94ca5b6c5ea84dfb9e12b7a718587ea3~tplv-k3u1fbpfcp-zoom-1.image.png) + +It was truly all on foot. This was the first time I had ever walked so far. + +By the time we reached our destination, my legs were no longer listening to me, and my feet were covered in blisters. + +We camped on the beach, had a barbecue, sang and danced, and didn't start heading back to school until the next morning. + +![](https://oss.javaguide.cn/about-the-author/college-life/8120d45d30254c908f9db20b3c00f514~tplv-k3u1fbpfcp-zoom-1.image.png) + +## Sophomore Year + +By my sophomore year, I began to shift my focus to learning programming. + +However, I encountered a dilemma: my closest friends in the club wanted me to stay and lead the team with them to ensure the club's continuation. + +But I had already planned what I wanted to do in my sophomore year and really wanted to focus my energy on learning to code and solidifying my technical skills. + +Reluctantly, I eventually compromised and chose to lead the club with my friends. After all, true friends are hard to come by! + +### Leading the Club + +Leading a club indeed took up a lot of my spare time. Besides having to take a taxi from the east campus to the west campus every week to lead them in running, we also had to organize various activities for them frequently. + +For example, we went to the banks of the Yangtze River for a barbecue and camping trip. + +![](https://oss.javaguide.cn/about-the-author/college-life/8a6945ccc087017c1f96ee93f3af8178-20220608154206500.png) + +And we also ran around the ancient city wall together. + +![](https://oss.javaguide.cn/about-the-author/college-life/2cfba22049e8b99e11955bcb7662d790.png) + +Back in university, I was very passionate about sports! + +![](https://oss.javaguide.cn/about-the-author/college-life/2dd503a60f814a7a953816bc3b5194cd~tplv-k3u1fbpfcp-zoom-1.image.png) + +During my sophomore year, I had already run around the ancient city wall three times. + +![](https://oss.javaguide.cn/about-the-author/college-life/949543b550e847d5a7314b7e1842489b~tplv-k3u1fbpfcp-zoom-1.image.png) + +### Joining Changda Online + +In my sophomore year, I also joined Changda Online, an organization under the university's Party Committee Propaganda Department. It was a tech-oriented group mainly responsible for creating websites, apps, and other things for the school. + +You can still find an entry for Changda Online on Baidu. + +![](https://oss.javaguide.cn/about-the-author/college-life/34ecf650120a4289a68b7549eb7d00cc~tplv-k3u1fbpfcp-zoom-1.image.png) + +For some reason, I was even issued a press card, hahaha! + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20220606121111042.png) + +I chose the Android group and thus began my journey of learning Android development. + +When I first joined this organization, I didn't even know what terms like HTML, CSS, JS, Java, or Linux meant. + +Later on, I stayed on as a vice-director and continued to serve the organization for over half a year. + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20220608121413761.png) + +### First Competition + +At that time, I also enjoyed participating in some school competitions and won some good rankings. The one that impressed me the most was a PPT competition, which was also the first school competition I ever participated in. + +Before the competition, I was a PPT novice. After studying hard for over a week, one of my works actually won first place. + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20220608121446529.png) + +It was because of this competition that I got my first mechanical keyboard for free. This keyboard accompanied me through the rest of my university life. + +### Deciding on a Technical Direction + +At the end of the first semester of my sophomore year, I finally decided that my future technical direction would be Java backend. So, I started making a study plan and began my journey of leveling up in the Java backend field. + +Every time I worked late and walked alone on campus, it felt great! I really liked that quiet feeling. + +![](https://oss.javaguide.cn/about-the-author/college-life/336fd489ce314d259d6090194f237e1b~tplv-k3u1fbpfcp-zoom-1.image.png) + +My physical fitness was really good back then. After staying up all night, I could get up as usual the next day for classes and study. Now, if I stay up late, I'm wiped out for the next two days! + +By my junior year, I had gone through most of the essential technologies in the Java backend field and had also completed two practical projects using what I had learned. + +Due to the lack of proper guidance, I took many detours while learning and wasted a lot of time (I envy you all for having me, so shameless!). + +At that time, I was also quite narcissistic and liked to take selfies whenever I had nothing to do. + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20210820202341008.png) + +During the National Day holiday, I didn't go home but stayed at school to watch Java videos and read books. + +I remember being very efficient during that National Day holiday, and I was very motivated to study. + +![](https://oss.javaguide.cn/about-the-author/college-life/WX20210820-203458.png) + +## Junior Year + +Throughout my junior year, I still had no weekends and almost no leisure time. Most of the time, I was studying alone in my dorm, and occasionally I would go to the library and the office. + +Although my roommates often played games and watched dramas, it didn't affect me. Once I put on my headphones, it felt like the world was my own. + +Unlike many experts, I was more efficient studying in my dorm than in the library or office. + +### The Birth of JavaGuide + +My open-source project JavaGuide and my public account were both launched this year. + +![](https://oss.javaguide.cn/about-the-author/college-life/the-birth-of-javaguide.jpeg) + +Currently, JavaGuide has reached 100k stars, and my public account has over 150k followers. + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20210820211926742.png) + +### Earning Money from Freelance Work + +Some opportunities also allowed me to take on some freelance work to earn money this year. To ensure timely delivery, I would occasionally stay up late. At that time, I was still happy and fulfilled even when pulling all-nighters. Every time I thought about earning money through my skills, I felt very motivated. + +I have also written an article sharing my experience with freelance work: [Let's Chat! I Earned 30k+ from Freelance Work in College](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247499539&idx=1&sn=ff153f9bd98bb3109b1f14e58ed9a785&chksm=cea1b0d8f9d639cee4744f845042df6b1fc319f4383b87eba76a944c2648c81a51c28d25e3b6&token=2114015135&lang=zh_CN#rd). + +However, the freelance projects I took on were quite varied and not very suitable as project experience for my resume. + +So, to make my resume's project experience look better, I found two projects to do myself. One was a mall-type project that I followed along with a video tutorial. The other was a video website-type project that I created based on my own ideas. + +The approximate architecture of the mall-type project is as follows (I couldn't find the original diagram I drew at the time): + +![](https://oss.javaguide.cn/about-the-author/college-life/206fab84bf5b4c048f8a88bc68c942f6~tplv-k3u1fbpfcp-zoom-1.image.png) + +At that time, mall projects seemed to be everywhere and were widely used. To make my mall project more competitive, after following the video tutorial, I added many of my own elements, such as replacing the message queue ActiveMQ with Kafka and adding a second-level cache. + +During the summer vacation, I also worked with classmates and a teacher on a real enterprise project for employee performance management. This project was very, very similar to the project I worked on when I first joined my company, although the company's version was probably more advanced and the code quality was higher. What a coincidence! + +I remember encountering many problems when I was working on projects independently. **Often, things that are easy to understand when you read them in a book will present small problems when you try to implement them. I usually solved these problems through Google searches. Using a search engine well can really solve 99% of your problems.** + +### Participating in a Software Design Competition + +There was a regret in my junior year. I participated in a software design competition with a few like-minded friends. The system we spent nearly two months building successfully made it to the semi-finals. + +However, I later quit because I personally felt that spending more time on this system wouldn't teach me much and would be a waste of time. Then, the whole team disbanded. + +Actually, looking back now, I could have learned something. My mindset at the time was a bit arrogant and overly ambitious. + +Thinking about it now, I still feel quite sorry for the friends who worked hard with me late into the night. + +Life is like that. It's a long journey, and whenever you look back at your past self, you will definitely have regrets. + +### Giving Up on Graduate School + +At that time, I also struggled with whether to pursue a master's degree. After all, my university was just average, and a master's degree could certainly add some value and improve my academic credentials. + +However, I ultimately gave up on the idea of graduate school. I was quite confident at the time and felt that I could find a good job without a master's degree. + +### Internship + +In my junior year, I also found an internship at a company not far from the school, founded by an alumnus. But to be honest, the overall internship experience was very poor. Not only did I not learn much, but it also delayed many of my planned tasks. + +I remember that many projects at this company were still using JSP, and the technology was very old. It would have been fine if they were old projects, but I saw that projects started just a few months ago were still using JSP, which was just ridiculous... + +It was really tough at the time. They wanted you to start working as soon as you arrived, the workload was huge, and if you couldn't finish, they wanted you to work overtime for free... + +There was nothing I could do at the time because I couldn't find any other companies in Jingzhou for an internship, and I couldn't go to another city for one. This is one of the problems that comes with choosing a university not in a first or second-tier city. + +## 大四 + +### 开始找工作 + +找实习找工作时候,才知道大学所在的城市的重要性。 + +由于,我的学校在荆州,而且本身学校就很一般,因此,基本没有什么比较好的企业来招人。 + +当时,唯一一个还算可以的就是苏宁,不过,我遇到的那个苏宁的 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)这篇文章中有提到。 + +## A Few Suggestions + +Here are a few of my own suggestions. Although I'm not outstanding, you can certainly be better: + +1. **Determine your direction.** Figure out if you want to go to graduate school or find a job. If you want to go to graduate school, take every potential exam subject seriously. You should also code in your spare time and preferably complete a project, which will help with your interview and skill improvement. If you're looking for a job, determine your direction as early as possible, have a plan, and understand your strengths and weaknesses. +2. **Start learning with a job-oriented mindset as early as possible.** This approach is more targeted, can significantly reduce time spent in confusion, and help you avoid many detours. +3. **Self-study is very important.** Develop the habit of self-learning and learn how to learn. +4. **Don't think that skipping class makes you a bad student.** I skipped many classes in university. Most of the time I spent skipping class was used to learn things I felt were more important. The classes I skipped were mostly unimportant and wouldn't affect my graduation. +5. **University relationships are relatively pure.** If you meet someone suitable, try to get to know them. If someone doesn't like you, don't pester them; you can't force these things. You have to admit that the desire to get to know someone often starts with their appearance, not their interesting soul. +6. **Manage your physique.** Go for a run when you have time. Don't become a "greasy" guy. +7. **Don't place too much importance on GPA.** I think GPA has a negligible effect on job hunting and graduate school applications, although not failing courses is important. However, GPA does carry the most weight in scholarship selections and recommendations for graduate school admission. +8. **Don't be too utilitarian.** Don't expect everything you do or learn to bring immediate benefits. Persistence and a utilitarian mindset are often inversely proportional. +9. ... + +## Afterword + +It's inevitable to encounter credential barriers during the job search, especially for those of us from average universities. I don't think this is unfair; if there's anyone to blame, it's ourselves for not getting into a better school. + +**Considering recruitment costs and time, companies are definitely more willing to select talent from better universities.** + +I used to complain about not being in a 211 or 985 university. But, upon reflection, it was my own issue for not getting into one. Besides, in the field of computer science, academic qualifications are relatively more meritocratic compared to other majors. + +There are plenty of people around me who graduated from junior colleges or third-tier universities and got into major tech companies. This isn't just a feel-good story to encourage friends from less prestigious schools. + +**Act more, complain less.** diff --git a/docs_en/about-the-author/writing-technology-blog-six-years.en.md b/docs_en/about-the-author/writing-technology-blog-six-years.en.md new file mode 100644 index 00000000000..15639bf8378 --- /dev/null +++ b/docs_en/about-the-author/writing-technology-blog-six-years.en.md @@ -0,0 +1,173 @@ +--- +title: 坚持写技术博客六年了! +category: 走近作者 +tag: + - 杂谈 +--- + +坚持写技术博客已经有六年了,也算是一个小小的里程碑了。 + +一开始,我写技术博客就是简单地总结自己课堂上学习的课程比如网络、操作系统。渐渐地,我开始撰写一些更为系统化的知识点详解和面试常见问题总结。 + +![JavaGuide 首页](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131717766.png) + +许多人都想写技术博客,但却不清楚这对他们有何好处。有些人开始写技术博客,却不知道如何坚持下去,也不知道该写些什么。这篇文章我会认真聊聊我对记录技术博客的一些看法和心得,或许可以帮助你解决这些问题。 + +## 写技术博客有哪些好处? + +### 学习效果更好,加深知识点的认识 + +**费曼学习法** 大家应该已经比较清楚了,这是一个经过实践证明非常有效的学习方式。费曼学习法的命名源自 Richard Feynman,这位物理学家曾获得过诺贝尔物理学奖,也曾参与过曼哈顿计划。 + +所谓费曼学习法,就是当你学习了一个新知识之后,想象自己是一个老师:用最简单、最浅显直白的话复述、表达复杂深奥的知识,最好不要使用行业术语,让非行业内的人也能听懂。为了达到这种效果,最好想象你是在给一个 80 多岁或 8 岁的小孩子上课,甚至他们都能听懂。 + +![教授别人学习效果最好](https://oss.javaguide.cn/about-the-author/college-life/v2-19373c2e61873c5083ee4b1d1523f8f5_720w.png) + +看书、看视频这类都属于是被动学习,学习效果比较差。费曼学习方法属于主动学习,学习效果非常好。 + +**写技术博客实际就是教别人的一种方式。** 不过,记录技术博客的时候是可以有专业术语(除非你的文章群体是非技术人员),只是你需要用自己的话表述出来,尽量让别人一看就懂。**切忌照搬书籍或者直接复制粘贴其他人的总结!** + +如果我们被动的学习某个知识点,可能大部分时候都是仅仅满足自己能够会用的层面,你并不会深究其原理,甚至很多关键概念都没搞懂。 + +如果你是要将你所学到的知识总结成一篇博客的话,一定会加深你对这个知识点的思考。很多时候,你为了将一个知识点讲清楚,你回去查阅很多资料,甚至需要查看很多源码,这些细小的积累在潜移默化中加深了你对这个知识点的认识。 + +甚至,我还经常会遇到这种情况:**写博客的过程中,自己突然意识到自己对于某个知识点的理解存在错误。** + +**写博客本身就是一个对自己学习到的知识进行总结、回顾、思考的过程。记录博客也是对于自己学习历程的一种记录。随着时间的流逝、年龄的增长,这又何尝不是一笔宝贵的精神财富呢?** + +知识星球的一位球友还提到写技术博客有助于完善自己的知识体系: + +![写技术博客有助于完善自己的知识体系](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121336432.png) + +### 帮助别人的同时获得成就感 + +就像我们程序员希望自己的产品能够得到大家的认可和喜欢一样。我们写技术博客在某一方面当然也是为了能够得到别人的认可。 + +**当你写的东西对别人产生帮助的时候,你会产生成就感和幸福感。** + +![读者的认可](https://oss.javaguide.cn/about-the-author/college-life/image-20230404181906257.png) + +这种成就感和幸福感会作为 **正向反馈** ,继续激励你写博客。 + +但是,即使受到很多读者的赞赏,也要保持谦虚学习的太多。人外有人,比你技术更厉害的读者多了去,一定要虚心学习! + +当然,你可以可能会受到很多非议。可能会有很多人说你写的文章没有深度,还可能会有很多人说你闲的蛋疼,你写的东西网上/书上都有。 + +**坦然对待这些非议,做好自己,走好自己的路就好!用行动自证!** + +### 可能会有额外的收入 + +写博客可能还会为你带来经济收入。输出价值的同时,还能够有合理的经济收入,这是最好的状态! + +为什么说是可能呢? **因为就目前来看,大部分人还是很难短期通过写博客有收入。我也不建议大家一开始写博客就奔着赚钱的目的,这样功利性太强了,效果可能反而不好。就比如说你坚持了写了半年发现赚不到钱,那你可能就会坚持不下去了。** + +我自己从大二开始写博客,大三下学期开始将自己的文章发布到公众号上,一直到大四下学期,才通过写博客赚到属于自己的第一笔钱。 + +第一笔钱是通过微信公众号接某培训机构的推广获得的。没记错的话,当时通过这个推广为自己带来了大约 **500** 元的收入。虽然这不是很多,但对于还在上大学的我来说,这笔钱非常宝贵。那时我才知道,原来写作真的可以赚钱,这也让我更有动力去分享自己的写作。可惜的是,在接了两次这家培训机构的广告之后,它就倒闭了。 + +之后,很长一段时间我都没有接到过广告。直到网易的课程合作找上门,一篇文章 1000 元,每个月接近一篇,发了接近两年,这也算是我在大学期间比较稳定的一份收入来源了。 + +![网易的课程合作](https://oss.javaguide.cn/about-the-author/college-life/image-20230408115720135.png) + +老粉应该大部分都是通过 JavaGuide 这个项目认识我的,这是我在大三开始准备秋招面试时创建的一个项目。没想到这个项目竟然火了一把,一度霸占了 GitHub 榜单。可能当时国内这类开源文档教程类项目太少了,所以这个项目受欢迎程度非常高。 + +![JavaGuide Star 趋势](https://oss.javaguide.cn/about-the-author/college-life/image-20230408131849198.png) + +项目火了之后,有一个国内比较大的云服务公司找到我,说是要赞助 JavaGuide 这个项目。我既惊又喜,担心别人是骗子,反复确认合同之后,最终确定以每月 1000 元的费用在我的项目首页加上对方公司的 banner。 + +随着时间的推移,以及自己后来写了一些比较受欢迎、比较受众的文章,我的博客知名度也有所提升,通过写博客的收入也增加了不少。 + +### 增加个人影响力 + +写技术博客是一种展示自己技术水平和经验的方式,能够让更多的人了解你的专业领域知识和技能。持续分享优质的技术文章,一定能够在技术领域增加个人影响力,这一点是毋庸置疑的。 + +有了个人影响力之后,不论是对你后面找工作,还是搞付费知识分享或者出书,都非常有帮助。 + +拿我自己来说,已经很多知名出版社的编辑找过我,协商出一本的书的事情。这种机会应该也是很多人梦寐以求的。不过,我都一一拒绝了,因为觉得自己远远没有达到能够写书的水平。 + +![电子工业出版社编辑邀约出书](https://oss.javaguide.cn/about-the-author/college-life/image-20230408121132211.png) + +其实不出书最主要的原因还是自己嫌麻烦,整个流程的事情太多了。我自己又是比较佛系随性的人,平时也不想把时间都留给工作。 + +## 怎样才能坚持写技术博客? + +**不可否认,人都是有懒性的,这是人的本性。我们需要一个目标/动力来 Push 一下自己。** + +就技术写作而言,你的目标可以以技术文章的数量为标准,比如: + +- 一年写多少篇技术文章。我个人觉得一年的范围还是太长了,不太容易定一个比较合适的目标。 +- 每月输出一篇高质量的技术文章。这个相对容易实现一些,每月一篇,一年也有十二篇了,也很不错了。 + +不过,以技术文章的数量为目标有点功利化,文章的质量同样很重要。一篇高质量的技术文可能需要花费一周甚至半个月的业余时间才能写完。一定要避免自己刻意追求数量,而忽略质量,迷失技术写作的本心。 + +我个人给自己定的目标是:**每个月至少写一篇原创技术文章或者认真修改完善过去写的三篇技术文章** (像开源项目推荐、开源项目学习、个人经验分享、面经分享等等类型的文章不会被记入)。 + +我的目标对我来说比较容易完成,因此不会出现为了完成目标而应付任务的情况。在我状态比较好,工作也不是很忙的时候,还会经常超额完成任务。下图是我今年 3 月份完成的任务(任务管理工具:Microsoft To-Do)。除了 gossip 协议是去年写的之外,其他都是 3 月份完成的。 + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20230404181033089.png) + +如果觉得以文章数量为标准过于功利的话,也可以比较随性地按照自己的节奏来写作。不过,一般这种情况下,你很可能过段时间就忘了还有这件事,开始慢慢抵触写博客。 + +写完一篇技术文章之后,我们不光要同步到自己的博客,还要分发到国内一些常见的技术社区比如博客园、掘金。**分发到其他平台的原因是获得关注进而收获正向反馈(动力来源之一)与建议,这是技术写作能坚持下去的非常重要的一步,一定要重视!!!** + +To be honest, when you finish writing an article that you think is pretty good, you still feel a sense of happiness and accomplishment. **However, it is still quite painful to let yourself do this. ** Just like it is easy for you to let yourself go out to play, in order to achieve this goal, you can have various excuses. However, if you want to study honestly, you still need some external force to supervise you. + +## Which directions are better for blogging? + +Generally speaking, it would be better to write blogs in the following directions: + +1. **Explain a certain knowledge point in detail**: You must have your own thinking instead of piecing together things. Not only should the basic concepts and principles of knowledge points be introduced, but also examples should be appropriately combined with actual cases and application scenarios. +2. **Troubleshooting/Performance Optimization Experience**: Specific scenarios and solutions need to be described in detail. There must be sufficient detailed description, including the specific scenario where the problem occurred, the root cause of the problem, ideas and specific steps to solve the problem, etc. At the same time, we should pay attention to practicality and operability to help readers learn and understand better. +3. **Source code reading record**: Describe the underlying source code implementation starting from a functional point, and talk about what you learned from the source code. + +The most important thing is to pay attention to the Markdown specification, otherwise no matter how good the content is, it will appear unprofessional. + +For details, see [Markdown Specification](../javaguide/contribution-guideline.md) (very important, try to follow the specification, it will be very helpful for you to write documents in your work) + +## Do you have any writing skills to share? + +### Don’t make sentences too long + +Don’t make sentences too long, and try to use short sentences (but not too short) so they are easier for readers to read and understand. + +### Try to make the article more lively and interesting + +Try to make the article more lively and interesting. For example, you can give some vivid examples, use some interesting jokes, catchphrases or Internet hot words. + +However, this also mainly depends on the style of your article. + +### Use simple and clear language + +Avoid jargon or complex language that readers may not understand. + +Focus on clarity and persuasion, and keep it simple. Simple writing is persuasive, and a good five-sentence argument will impress people more than a 100-sentence brilliant argument. Why words such as aphorisms and proverbs are easy for people to accept has something to do with simplicity and straightforwardness. + +### Use visual effects + +Visuals such as charts and images can make plain text content easier to understand. Remember to use visuals where appropriate to enhance your writing. + +![](https://oss.javaguide.cn/about-the-author/college-life/image-20230404192458759.png) + +### Technical articles should be accompanied by bright colors + +Below are two pictures with the same content, both drawn through drawio. Which one do you guys like better? + +I believe most friends will choose the latter one with a more vivid color! + +The color adjustment only took me less than 30 seconds, but the reading experience was greatly improved! + +![](https://oss.javaguide.cn/2021-1/image-20210104182517226.png) + +### Identify your readers + +Before writing, think about the overall primary audience for your article. Once you have identified your audience, you can adjust your writing style and content difficulty based on your audience's needs and level of understanding. + +### Review and Modify + +Always review and revise your article before publishing. This will help you catch errors, clarify any confusing information, and improve the overall quality of your document. + +**Good writing comes from editing, remember! ! ! ** + +## Summary + +In general, writing a technical blog is a matter of self-interest and mutual benefit. You may gain a lot from it, and what you write may also be of great help to others. However, writing a technical blog is still quite time-consuming, and you need to balance it with work and life. \ No newline at end of file diff --git a/docs_en/about-the-author/zhishixingqiu-two-years.en.md b/docs_en/about-the-author/zhishixingqiu-two-years.en.md new file mode 100644 index 00000000000..4b18d3af2d7 --- /dev/null +++ b/docs_en/about-the-author/zhishixingqiu-two-years.en.md @@ -0,0 +1,148 @@ +--- +title: My Knowledge Planet is 4 Years Old! +category: Knowledge Planet +star: 2 +--- + + + +On **December 29, 2019**, after about a year of hesitation, I officially decided to start my own Planet to help students learning Java and preparing for Java interviews. In the blink of an eye, more than four years have passed. Thank you all for your companionship along the way. I will keep my promise to continue to seriously maintain this pure Java Knowledge Planet and not disappoint the readers who trust me. + +![](https://oss.javaguide.cn/xingqiu/640-20230727145252757.png) + +I was one of the earlier tech bloggers to create a Planet, and I am also one of the few who have persisted (most bloggers just cash in and abandon their Planets). For the first year or two, it was purely a labor of love. The initial pricing was very low (the cost of a meal), and since I was busy with my new job, the services offered were not as extensive as they are now. + +Gradually, as the price increased, the Planet's income also slowly grew. However, considering that my audience is mainly 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 make me more money given my traffic. + +**I have my principles: no get-rich-quick schemes, create content with heart, and genuinely hope to help others!** + +## What is a Knowledge Planet? + +Simply put, a Knowledge Planet is a private communication circle, mainly used by knowledge creators to connect with their most loyal readers/fans. Compared to WeChat groups, Knowledge Planet is more efficient for content retention and information management. + +![](https://oss.javaguide.cn/xingqiu/image-20220211223754566.png) + +## What can my Knowledge Planet offer you? + +I strive to create the highest quality Java interview communication Planet! After joining my Planet, you will get: + +1. Permanent access to 6 high-quality columns covering interviews, source code analysis, practical projects, and more! +2. Free access to multiple original PDF interview manuals. +3. Free resume review services (having helped over 7,000 members so far). +4. One-on-one free Q&A sessions (exclusive advice, heartfelt answers). +5. Exclusive job-hunting guides and suggestions to help you avoid detours and double your efficiency! +6. A massive collection of high-quality Java interview resources. +7. Check-in activities, book clubs, and study groups, so you no longer have to learn alone. +8. Occasional benefits: holiday prize draws, book and course giveaways, offline member meetups, and more. +9. ... + +Any one of these services alone is worth far more than the Planet's admission fee. + +Here is a **30** yuan exclusive coupon for the Planet, limited quantity (price will be increasing soon. Renewals for existing users are half price; you can renew by scanning the QR code with WeChat)! + +![Knowledge Planet 30 yuan coupon](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) + +### 专属专栏 + +星球更新了 **《Java 面试指北》**、**《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、 **《从零开始写一个 RPC 框架》**(已更新完)、**《Kafka 常见面试题/知识点总结》** 等多个优质专栏。 + +![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) + +《Java 面试指北》内容概览: + +![](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) + +进入星球之后,这些专栏即可免费永久阅读,永久同步更新! + +### PDF 面试手册 + +进入星球就免费赠送多本优质 PDF 面试手册。 + +![星球 PDF 面试手册](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) + +### 优质精华主题沉淀 + +星球沉淀了几年的优质精华主题,内容涵盖面经、面试题、工具网站、技术资源、程序员进阶攻略等内容,干货非常多。 + +![](https://oss.javaguide.cn/xingqiu/image-20230421154518800.png) + +并且,每个月都会整理出当月优质的主题,方便大家阅读学习,避免错过优质的内容。毫不夸张,单纯这些优质主题就足够门票价值了。 + +![星球每月优质主题整理概览](https://oss.javaguide.cn/xingqiu/image-20230902091117181.png) + +加入星球之后,一定要记得抽时间把星球精华主题看看,相信你一定会有所收货! + +JavaGuide 知识星球优质主题汇总传送门:(为了避免这里成为知识杂货铺,我会对严格筛选入选的优质主题)。 + +![星球优质主题汇总](https://oss.javaguide.cn/xingqiu/Xnip2023-04-21_15-48-13.png) + +### Resume Review + +During interview season, I review at least 15-30 resumes on an average night. Outside of interview season, the number of requests is slightly lower. Otherwise, it would be truly overwhelming! + +![](https://oss.javaguide.cn/xingqiu/image-20220304123156348.png) + +A quick count shows that I have provided free resume review services to at least **7,000+** members to date. + +![](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) + +I provide detailed suggestions for each resume, revising them with care, and have received high praise! + +![](https://oss.javaguide.cn/xingqiu/image-20220725093504807.png) + +### One-on-One Q&A + +You can have one-on-one free Q&A sessions with me, and I will answer your questions thoughtfully. To date, I have answered **3,000+** questions from readers. + +![](https://oss.javaguide.cn/xingqiu/wecom-temp-151578-45e66ccd48b3b5d3baa8673d33c7b664.jpg) + +![](https://oss.javaguide.cn/xingqiu/image-20220211223559179.png) + +### Study Check-ins + +The Planet's study check-in activities encourage you to learn and communicate with other members. + +![](https://oss.javaguide.cn/xingqiu/image-20220308143815840.png) + +You can also learn from other members' check-ins. Most importantly, this learning atmosphere is very helpful for self-discipline! + +![](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) + +### Reading Activities + +We regularly hold reading activities (with generous rewards), where I lead everyone to read some excellent technical books! + +![](https://oss.javaguide.cn/xingqiu/image-20220211233642079.png) + +The prize-winning rate for each reading activity is very, very, very high! The value directly exceeds the admission fee!!! + +### Irregular Benefits + +From time to time, I give away books, columns, and red envelopes on the Planet. There are many benefits. + +![](https://oss.javaguide.cn/xingqiu/1682063464099.png) + +## 是否收费? + +星球是需要付费才能进入的。 **为什么要收费呢?** + +1. 维护好星球是一件费时费力的事情,每到面试季,我经常凌晨还在看简历和回答球友问题。市面上单单一次简历修改服务也至少需要 200+,而简历修改也只是我的星球提供的服务的冰山一角。除此之外,我还要抽时间写星球专属的一些专栏,单单是这些专栏的价值就远超星球门票了。 +2. 星球提供的服务比较多,如果我是免费提供这些服务的话,是肯定忙不过来的。付费这个门槛可以帮我筛选出真正需要帮助的那批人。 +3. 免费的东西才是最贵的,加入星球之后无任何其他需要付费的项目,统统免费! +4. 合理的收费是对我付出劳动的一种正向激励,促进我继续输出!同时,这份收入还可以让我们家人过上更好的生活。虽然累点,但也是值得的! + +另外,这个是一年的,到明年这个时候结束,差不过够用了。如果服务结束的时候你还需要星球服务的话,可以添加我的微信(**javaguide1024**)领取一个续费优惠卷,半价基础再减 10,记得备注 **“续费”** 。 + +## 如何加入? + +这里赠送一个 **30** 元的星球专属优惠券吧,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! + +![Knowledge Planet 30 yuan coupon](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) + +After entering the planet, remember to check out **[Planet User Guide](https://t.zsxq.com/0d18KSarv)** (must read!!!) and **[Planet High-Quality Theme Summary](https://t.zsxq.com/12uSKgTIm)**. + +**No tricks, no potential charges. Create content carefully and don’t cut the leeks! ** + +However, **must confirm the need before proceeding**. Moreover, **If you are not satisfied with the content within three days, you can get a full refund**. \ No newline at end of file diff --git a/docs_en/books/cs-basics.en.md b/docs_en/books/cs-basics.en.md new file mode 100644 index 00000000000..52375fc8ecc --- /dev/null +++ b/docs_en/books/cs-basics.en.md @@ -0,0 +1,275 @@ +--- +title: 计算机基础必读经典书籍 +category: 计算机书籍 +icon: "computer" +head: + - - meta + - name: keywords + content: 计算机基础书籍精选 +--- + +考虑到很多同学比较喜欢看视频,因此,这部分内容我不光会推荐书籍,还会顺便推荐一些我觉得不错的视频教程和各大高校的 Project。 + +## 操作系统 + +**为什么要学习操作系统?** + +**从对个人能力方面提升来说**,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。比如说我们开发的系统使用的缓存(比如 Redis)和操作系统的高速缓存就很像。CPU 中的高速缓存有很多种,不过大部分都是为了解决 CPU 处理速度和内存处理速度不对等的问题。我们还可以把内存可以看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。同样地,我们使用的 Redis 缓存就是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。高速缓存一般会按照局部性原理(2-8 原则)根据相应的淘汰算法保证缓存中的数据是经常会被访问的。我们平常使用的 Redis 缓存很多时候也会按照 2-8 原则去做,很多淘汰算法都和操作系统中的类似。既说了 2-8 原则,那就不得不提命中率了,这是所有缓存概念都通用的。简单来说也就是你要访问的数据有多少能直接在缓存中直接找到。命中率高的话,一般表明你的缓存设计比较合理,系统处理速度也相对较快。 + +**从面试角度来说**,尤其是校招,对于操作系统方面知识的考察是非常非常多的。 + +**简单来说,学习操作系统能够提高自己思考的深度以及对技术的理解力,并且,操作系统方面的知识也是面试必备。** + +如果你要系统地学习操作系统的话,最硬核最权威的书籍是 **[《操作系统导论》](https://book.douban.com/subject/33463930/)** 。你可以再配套一个 **[《深入理解计算机系统》](https://book.douban.com/subject/1230413/)** 加深你对计算机系统本质的认识,美滋滋! + +![](https://oss.javaguide.cn/github/javaguide/booksimage-20201012191645919.png) + +另外,去年新出的一本国产的操作系统书籍也很不错:**[《现代操作系统:原理与实现》](https://book.douban.com/subject/35208251/)** (夏老师和陈老师团队的力作,值得推荐)。 + +![](https://oss.javaguide.cn/github/javaguide/books/20210406132050845.png) + +如果你比较喜欢动手,对于理论知识比较抵触的话,我推荐你看看 **[《30 天自制操作系统》](https://book.douban.com/subject/11530329/)** ,这本书会手把手教你编写一个操作系统。 + +纸上学来终觉浅 绝知此事要躬行!强烈推荐 CS 专业的小伙伴一定要多多实践!!! + +![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409123802972.png) + +其他相关书籍推荐: + +- **[《自己动手写操作系统》](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) + diff --git a/docs_en/books/database.en.md b/docs_en/books/database.en.md new file mode 100644 index 00000000000..d15b34e07bb --- /dev/null +++ b/docs_en/books/database.en.md @@ -0,0 +1,104 @@ +--- +title: Must-read classic database books +category: computer books +icon: "database" +head: + - - meta + - name: keywords + content: Selection of database books +--- + +## Database basics + +Regarding the basics of databases, if you think the books are boring and you can't persevere, I recommend you to watch some good videos first, such as ["Principles of Database Systems"] by Beijing Normal University (https://www.icourse163.org/cours e/BNU-1002842007) and Harbin Institute of Technology's ["Database System (Part 2): Management and Technology"] (https://www.icourse163.org/course/HIT-1001578001) are very good. + +["Database System Principles"](https://www.icourse163.org/course/BNU-1002842007) The teacher of this course teaches in great detail, and the assignments in each section are designed to fit the knowledge taught, and there are many supporting experiments later. + +![](https://oss.javaguide.cn/github/javaguide/books/up-e113c726a41874ef5fb19f7ac14e38e16ce.png) + +If you prefer hands-on work and are resistant to theoretical knowledge, I recommend you take a look at ["How to develop a simple database"](https://cstack.github.io/db_tutorial/). This project will teach you step by step how to write a simple database. + +![](https://oss.javaguide.cn/github/javaguide/books/up-11de8cb239aa7201cc8d78fa28928b9ec7d.png) + +There is also a big guy on GitHub who has implemented a simple database using Java. The introduction is quite detailed. Interested friends can check it out. Address: [https://github.com/alchemystar/Freedom](https://github.com/alchemystar/Freedom). + +In addition to this one written in Java, **[db_tutorial](https://github.com/cstack/db_tutorial)** This project was written in C language by a foreign boss. Friends can also take a look. + +**As long as you make good use of search engines, you can find database toys implemented in various languages. ** + +![](https://oss.javaguide.cn/github/javaguide/books/up-d32d853f847633ac7ed0efdecf56be1f1d2.png) + +**Learning on paper will eventually make you realize it is shallow. You must know that you have to do this! It is highly recommended that CS majors must practice more! ! ! ** + +### "Database System Concepts" + +["Database System Concepts"](https://book.douban.com/subject/10548379/) This book covers a complete set of concepts of database systems, with a clear knowledge system. It is a very classic textbook for learning database systems! Not a reference book! + +![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409150441742.png) + +### "Database System Implementation" + +If you also want to study the underlying principles of MySQL, I recommend that you read ["Database System Implementation"](https://book.douban.com/subject/4838430/) first. + +![](https://oss.javaguide.cn/github/javaguide/books/database-system-implementation.png) + +Whether it is MySQL or Oracle, their overall framework is similar. The difference is in their internal implementation, such as the data structure of the database index, the implementation of the storage engine, etc. + +The translation of this book is still poor in some places. If you can read the English version, it is recommended to start with the English version. + +"Database System Implementation" is a textbook from Stanford, and there is also a "Basic Tutorial on Database Systems" (https://book.douban.com/subject/3923575/) which is a prerequisite course that can get you started with databases. + +##MySQL + +The data of our website or APP requires the use of a database to store data. + +In general enterprise project development, MySQL is often used. If you want to learn MySQL, you can read the following 3 books: + +- **["MySQL Must Know"](https://book.douban.com/subject/3354490/)**: Very thin! It is very suitable for newbies to MySQL and is a great introductory textbook. +- **["High-Performance MySQL"](https://book.douban.com/subject/23008813/)**: A classic in the MySQL field! A must-read for learning MySQL! It is advanced content and mainly teaches you how to use MySQL better. There is both theory and practice! If you don’t have time to read them all, I suggest you read Chapter 5 (Creating high-performance indexes) and Chapter 6 (Query performance optimization) carefully. +- **["MySQL Technology Insider"](https://book.douban.com/subject/24708143/)**: If you want to learn more about the MySQL storage engine, this is the right book to read! + +![](https://oss.javaguide.cn/github/javaguide/books/up-3d31e762933f9e50cc7170b2ebd8433917b.png) + +For videos, you can check out Power Node’s ["MySQL Database Tutorial Video"](https://www.bilibili.com/video/BV1fx411X7BD). This video basically introduces some introductory knowledge related to MySQL. + +In addition, I strongly recommend **["How MySQL works"](https://book.douban.com/subject/35231266/)** This book, the content is very suitable for preparing for interviews. It's very detailed, but not boring, and the content is very conscientious! + +![](https://oss.javaguide.cn/github/javaguide/csdn/20210703120643370.png) + +## PostgreSQL + +Like MySQL, PostgreSQL is 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) + +In recent years, due to the excellent new features of PostgreSQL, more and more projects use PostgreSQL instead of MySQL. + +If you are still wondering whether to try PostgreSQL, I suggest you take a look at this Zhihu topic: [What are the advantages of PostgreSQL compared with MySQL? - Zhihu](https://www.zhihu.com/question/20010554). + +### "PostgreSQL Guide: Insider Exploration" + +["PostgreSQL Guide: Insider Exploration"](https://book.douban.com/subject/33477094/) This book mainly introduces the internal working principles of PostgreSQL, including the logical organization and physical implementation of database objects, and the architecture of processes and memory. + +When I first started working, I needed to use PostgreSQL. After reading about 1/3 of the content, I felt pretty good. + +![](https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-Guide.png) + +### "PostgreSQL Technology Insider: In-depth Exploration of Query Optimization" + +["PostgreSQL Technology Insider: In-depth Exploration of Query Optimization"](https://book.douban.com/subject/30256561/) This book mainly talks about some technical implementation details of PostgreSQL's query optimization, which can give you a deep understanding of PostgreSQL's query optimizer. + +!["PostgreSQL Technology Insider: In-depth exploration of query optimization"](https://oss.javaguide.cn/github/javaguide/books/PostgreSQL-TechnologyInsider.png) + +## Redis + +**Redis is a database developed using C language**, but unlike traditional databases, **Redis data is stored in memory**, that is, it is an in-memory database, so the read and write speed is very fast, so Redis is widely used in the cache direction. + +If you want to learn Redis, I highly recommend the following two books:- ["Redis Design and Implementation"](https://book.douban.com/subject/25900156/): Mainly content related to Redis theoretical knowledge, relatively comprehensive. I wrote an article before ["7 years ago, at the age of 24, published a Redis Divine Book》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247507030&idx=1&sn=0a5fd669413991b30163ab6f5834a4ad&chksm=cea19 39df9d61a8b93925fae92f4cee0838c449534e60731cfaf533369831192e296780b32a6&token=709354671&lang=zh_CN&scene=21#wechat_redirect) Let me introduce this book. +- ["Redis Core Principles and Practices"](https://book.douban.com/subject/26612779/): Mainly combines source code to analyze important knowledge points of Redis, such as various data structures and advanced features. + +!["Redis Design and Implementation" and "Redis Design and Implementation"](https://oss.javaguide.cn/github/javaguide/books/redis-books.png) + +In addition, ["Redis Development and Operation and Maintenance"](https://book.douban.com/subject/26971561/) is also a very good book. It not only provides a basic introduction, but also shares front-line development and operation and maintenance experience. + +!["Redis Development and Operation and Maintenance"](https://oss.javaguide.cn/github/javaguide/books/redis-kaifa-yu-yunwei.png) \ No newline at end of file diff --git a/docs_en/books/distributed-system.en.md b/docs_en/books/distributed-system.en.md new file mode 100644 index 00000000000..8e3561092b9 --- /dev/null +++ b/docs_en/books/distributed-system.en.md @@ -0,0 +1,85 @@ +--- +title: Distributed must-read classic books +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) + +**["In-depth Understanding of Distributed Systems"](https://book.douban.com/subject/35794814/)** is an original distributed Chinese book published in 2022. It mainly talks about the basic concepts, common challenges and consensus algorithms in the distributed field. + +The author spends a lot of space to introduce the very important consensus algorithm in the distributed field, and also takes you to implement the Paxos algorithm, the originator of the consensus algorithm, from scratch based on the Go language. + +To be honest, I haven't started reading this book yet. but! I have carefully read almost every article related to distribution on the blog of the author of this book. The author began to conceive of "In-depth Understanding of Distributed Systems" in 2019, started writing it in 2020, and took nearly two years to finally submit it. + +![](https://oss.javaguide.cn/github/javaguide/books/image-20220706121952258.png) + +The author wrote a special article to introduce the story behind this book. Interested friends can check it out by themselves: . + +Finally, put the code repository and errata address of this book: . + +## "Data-intensive application system design" + +![](https://oss.javaguide.cn/github/javaguide/books/ddia.png) + +I highly recommend **["Designing Data-Intensive Application"](https://book.douban.com/subject/30329536/)** (DDIA, data-intensive application system design), which is worth reading many times! Nearly 90% of people on Douban gave this book a five-star rating after reading it. + +This book mainly talks about distributed databases, data partitions, transactions, distributed systems, etc. + +You may have heard of most of the concepts introduced in the book before, but after reading the contents of the book, you may suddenly realize: "Wow! That's it! Isn't this the principle of a certain technology?". + +I have previously written an introduction and recommendation for this book in Zhihu answers. Friends who have not read it can check it out: [What programming books have you become addicted to after reading them? ](https://www.zhihu.com/question/50408698/answer/2278198495). In addition, if you find it difficult to read this book and cannot understand many parts, I recommend ["DDIA Chapter-by-Chapter Intensive Reading"] written by the author of "In-depth Understanding of Distributed Systems" (https://ddia.qtmuniao.com). + +## "In-depth understanding of distributed transactions" + +![](https://oss.javaguide.cn/github/javaguide/books/In-depth-understanding-of-distributed-transactions-xiaoyu.png) + +**["In-depth Understanding of Distributed Transactions"](https://book.douban.com/subject/35626925/)** One of the authors of this book is the founder of the Apache ShenYu (incubating) gateway and the founder of distributed transaction frameworks such as Hmily, RainCat, and Myth. + +When learning distributed transactions, you can refer to this book. Although there are some minor errors and logical inconsistencies, it is generally a good introduction to various distributed transaction solutions. + +## "From Paxos to Zookeeper" + +![](https://oss.javaguide.cn/github/javaguide/books/image-20211216161350118.png) + +**["From Paxos to Zookeeper"](https://book.douban.com/subject/26292004/)** is a good book to introduce you to distributed theory. This book mainly introduces several typical distributed consistency protocols and ideas for solving distributed consistency problems, focusing on the Paxos and ZAB protocols. + +PS: Zookeeper is not used much now, so you don’t need to focus on learning it, but Paxos and ZAB protocols are still worthy of in-depth study. + +## "In-depth understanding of distributed consensus algorithms" + +![](https://oss.javaguide.cn/github/javaguide/books/deep-dive-into-distributed-consensus-algorithms.png) + +**["In-depth Understanding of Distributed Consensus Algorithms"](https://book.douban.com/subject/36335459/)** Detailed analysis of the core principles and implementation details of mainstream distributed consensus algorithms such as Paxos, Raft, and Zab. If you want to understand the distributed consensus algorithm, you may wish to refer to the summary of this book. + +## "Microservice Architecture Design Pattern" + +![](https://oss.javaguide.cn/github/javaguide/books/microservices-patterns.png) + +Chris Richardson, the author of **["Microservice Architecture Design Patterns"](https://book.douban.com/subject/33425123/)**, is rated as one of the top ten software architects in the world and a pioneer of microservice architecture. This book brings together 44 proven architectural design patterns that solve difficult problems such as service decomposition, transaction management, querying, and cross-service communication. The content in the book is not only theoretically solid, but also guides readers step by step through rich Java code examples to master the development and deployment of production-level microservice architecture applications. + +## "Phoenix Architecture" + +![](https://oss.javaguide.cn/github/javaguide/books/f5bec14d3b404ac4b041d723153658b5.png) + +**["Phoenix Architecture"](https://book.douban.com/subject/35492898/)** This book is a summary of teacher Zhou Zhiming's many years of architecture and R&D experience. The content is very informative, with both depth and breadth, combining theory with practice! + +As the subtitle of the book title "Building Reliable Large-scale Distributed Systems" says, the main content of this book is: "How to build a reliable distributed large-scale software system", covering the following aspects: + +- The evolution of software architecture from monolithic to microservices to serviceless. +- What issues should architects pay attention to when designing architecture, and what are some better practices. +- The cornerstone of distributed consensus is the common distributed consensus algorithms Paxos and Multi Paxos. +- Immutable infrastructure such as virtualized containers and service grids. +- A guide to avoid pitfalls when moving towards microservices. + +I have recommended this book many times. See the historical article for details: + +- [Another magical book by Teacher Zhou Zhiming! Discover the treasure! ](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247505254&idx=1&sn=04faf3093d6002354f06fffbfc2954e0&chksm=cea1 9aadf9d613bbba7ed0e02ccc4a9ef3a30f4d83530e7ad319c2cc69cd1770e43d1d470046&scene=178&cur_album_id=1646812382221926401#rd) +- [Another magic book in the Java field! Teacher Zhou Zhiming YYDS! ](https://mp.weixin.qq.com/s/9nbzfZGAWM9_qIMp1r6uUQ) + +## Others + +- ["Distributed Systems: Concept and Design"](https://book.douban.com/subject/21624776/): It is a textbook type, the content is complete but boring, it can be used as a reference book; +- ["Distributed Architecture Principles and Practice"](https://book.douban.com/subject/35689350/): Published in 2021, it is not popular and I haven't read it yet. \ No newline at end of file diff --git a/docs_en/books/java.en.md b/docs_en/books/java.en.md new file mode 100644 index 00000000000..1b7f66a3a7b --- /dev/null +++ b/docs_en/books/java.en.md @@ -0,0 +1,246 @@ +--- +title: Java 必读经典书籍 +category: 计算机书籍 +icon: "java" +--- + +## Java 基础 + +**[《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》这本书的内容很轻松有趣,可以说是我学习编程初期最喜欢的几本书之一了。同时,这本书也是我的 Java 启蒙书籍。我在学习 Java 的初期多亏了这本书的帮助,自己才算是跨进 Java 语言的大门。 + +我觉得我在 Java 这块能够坚持下来,这本书有很大的功劳。我身边的的很多朋友学习 Java 初期都是看的这本书。 + +有很多小伙伴就会问了:**这本书适不适合编程新手阅读呢?** + +我个人觉得这本书还是挺适合编程新手阅读的,毕竟是 “Head First” 系列。 + +**[《Java 核心技术卷 1 + 卷 2》](https://book.douban.com/subject/34898994/)** + +![《Java 核心技术卷 1》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424101217849.png) + +这两本书也非常不错。不过,这两本书的内容很多,全看的话比较费时间。我现在是把这两本书当做工具书来用,就比如我平时写文章的时候,碰到一些 Java 基础方面的问题,经常就翻看这两本来当做参考! + +我当时在大学的时候就买了两本放在寝室,没事的时候就翻翻。建议有点 Java 基础之后再读,介绍的还是比较深入和全面的,非常推荐。 + +**[《Java 编程思想》](https://book.douban.com/subject/2130190/)** + +![《Java 编程思想》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424103124893.png) + +另外,这本书的作者去年新出版了[《On Java》](https://book.douban.com/subject/35751619/),我更推荐这本,内容更新,介绍了 Java 的 3 个长期支持版(Java 8、11、17)。 + +![](https://oss.javaguide.cn/github/javaguide/books/on-java/6171657600353_.pic_hd.jpg) + +毕竟,这是市面上目前唯一一本介绍了 Java 的 3 个长期支持版(Java 8、11、17)的技术书籍。 + +**[《Java 8 实战》](https://book.douban.com/subject/26772632/)** + +![《Java 8实战》-豆瓣](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 编程的逻辑》](https://book.douban.com/subject/30133440/)** + +![《Java编程的逻辑》](https://oss.javaguide.cn/github/javaguide/books/image-20230721153650488.png) + +一本非常低调的好书,相比于入门书来说,内容更有深度。适合初学者,同时也适合大家拿来复习 Java 基础知识。 + +## Java 并发 + +**[《Java 并发编程之美》](https://book.douban.com/subject/30351286/)** + +![《Java 并发编程之美》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424112413660.png) + +这本书还是非常适合我们用来学习 Java 多线程的,讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。 + +另外,这本书的作者加多自身也会经常在网上发布各种技术文章。这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! + +**[《实战 Java 高并发程序设计》](https://book.douban.com/subject/30358019/)** + +![《实战 Java 高并发程序设计》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424112554830.png) + +这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 + +**[《深入浅出 Java 多线程》](https://github.com/RedSpider1/concurrent)** + +![《深入浅出 Java 多线程》在线阅读](https://oss.javaguide.cn/github/javaguide/books/image-20220424112927759.png) + +这本开源书籍是几位大厂的大佬开源的。这几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。 + +这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 + +在线阅读:。 + +**[《Java 并发实现原理:JDK 源码剖析》](https://book.douban.com/subject/35013531/)** + +![《Java 并发实现原理:JDK 源码剖析》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/0b1b046af81f4c94a03e292e66dd6f7d.png) + +这本书主要是对 Java Concurrent 包中一些比较重要的源码进行了讲解,另外,像 JMM、happen-before、CAS 等等比较重要的并发知识这本书也都会一并介绍到。 + +不论是你想要深入研究 Java 并发,还是说要准备面试,你都可以看看这本书。 + +## JVM + +**[《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/)** + +![《深入理解 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/20210710104655705.png) + +这本书就一句话形容:**国产书籍中的战斗机,实实在在的优秀!** (真心希望国内能有更多这样的优质书籍出现!加油!💪) + +这本书的第 3 版 2019 年底已经出来了,新增了很多实在的内容比如 ZGC 等新一代 GC 的原理剖析。目前豆瓣上是 9.5 的高分,🐂 不 🐂 我就不多说了! + +不论是你面试还是你想要在 Java 领域学习的更深,你都离不开这本书籍。这本书不光要看,你还要多看几遍,里面都是干货。这本书里面还有一些需要自己实践的东西,我建议你也跟着实践一下。 + +类似的书籍还有 **[《实战 Java 虚拟机》](https://book.douban.com/subject/26354292/)**、**[《虚拟机设计与实现:以 JVM 为例》](https://book.douban.com/subject/34935105/)** ,这两本都是非常不错的! + +![《实战 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113158144.png) + +![《虚拟机设计与实现:以 JVM 为例》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113210153.png) + +如果你对实战比较感兴趣,想要自己动手写一个简易的 JVM 的话,可以看看 **[《自己动手写 Java 虚拟机》](https://book.douban.com/subject/26802084/)** 这本书。 + +![《自己动手写 Java 虚拟机》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113445246.png) + +书中的代码是基于 Go 语言实现的,搞懂了原理之后,你可以使用 Java 语言模仿着写一个,也算是练练手! 如果你当前没有能力独立使用 Java 语言模仿着写一个的话,你也可以在网上找到很多基于 Java 语言版本的实现,比如[《zachaxy 的手写 JVM 系列》](https://zachaxy.github.io/tags/JVM/) 。 + +这本书目前在豆瓣有 8.2 的评分,我个人觉得张秀宏老师写的挺好的,这本书值得更高的评分。 + +另外,R 大在豆瓣发的[《从表到里学习 JVM 实现》](https://www.douban.com/doulist/2545443/)这篇文章中也推荐了很多不错的 JVM 相关的书籍,推荐小伙伴们去看看。 + +## 常用工具 + +非常重要!非常重要!特别是 Git 和 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/) 。 + +除了这些工具之外,我强烈建议你一定要搞懂 GitHub 的使用。一些使用 GitHub 的小技巧,你可以看[Github 实用小技巧总结](https://javaguide.cn/tools/git/github-tips.html)这篇文章。 + +## 常用框架 + +框架部分建议找官方文档或者博客来看。 + +### Spring/SpringBoot + +**Spring 和 SpringBoot 真的很重要!** + +一定要搞懂 AOP 和 IOC 这两个概念。Spring 中 bean 的作用域与生命周期、SpringMVC 工作原理详解等等知识点都是非常重要的,一定要搞懂。 + +企业中做 Java 后端,你一定离不开 SpringBoot ,这个是必备的技能了!一定一定一定要学好! + +像 SpringBoot 和一些常见技术的整合你也要知识怎么做,比如 SpringBoot 整合 MyBatis、 ElasticSearch、SpringSecurity、Redis 等等。 + +下面是一些比较推荐的书籍/专栏。 + +**[《Spring 实战》](https://book.douban.com/subject/34949443/)** + +![《Spring 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113512453.png) + +不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的一个概览,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。 + +**[《Spring 5 高级编程》](https://book.douban.com/subject/30452637/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210328171223638.png) + +对于 Spring5 的新特性介绍的比较详细,也说不上好。另外,感觉全书翻译的有一点蹩脚的味道,还有一点枯燥。全书的内容比较多,我一般拿来当做工具书参考。 + +**[《Spring Boot 编程思想(核心篇)》](https://book.douban.com/subject/33390560/)** + +![《Spring Boot 编程思想(核心篇)》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113546513.png) + +_稍微有点啰嗦,但是原理介绍的比较清楚。_ + +SpringBoot 解析,不适合初学者。我是去年入手的,现在就看了几章,后面没看下去。书很厚,感觉很多很多知识点的讲解过于啰嗦和拖沓,不过,这本书对于 SpringBoot 内部原理讲解的还是很清楚。 + +**[《Spring Boot 实战》](https://book.douban.com/subject/26857423/)** + +![《Spring Boot 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113614768.png) + +比较一般的一本书,可以简单拿来看一下。 + +### MyBatis + +MyBatis 国内用的挺多的,我的建议是不需要花太多时间在上面。当然了,MyBatis 的源码还是非常值得学习的,里面有很多不错的编码实践。这里推荐两本讲解 MyBatis 源码的书籍。 + +**[《手写 MyBatis:渐进式源码实践》](https://book.douban.com/subject/36243250/)** + +![《手写MyBatis:渐进式源码实践》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123402784.png) + +我的好朋友小傅哥出版的一本书。这本书以实践为核心,摒弃 MyBatis 源码中繁杂的内容,聚焦于 MyBaits 中的核心逻辑,简化代码实现过程,以渐进式的开发方式,逐步实现 MyBaits 中的核心功能。 + +这本书的配套项目的仓库地址: 。 + +**[《通用源码阅读指导书――MyBatis 源码详解》](https://book.douban.com/subject/35138963/)** + +![《通用源码阅读指导书――MyBatis源码详解》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123416741.png) + +这本书通过 MyBatis 开源代码讲解源码阅读的流程和方法!一共对 MyBatis 源码中的 300 多个类进行了详细解析,包括其背景知识、组织方式、逻辑结构、实现细节。 + +这本书的配套示例仓库地址: 。 + +### Netty + +**[《Netty 实战》](https://book.douban.com/subject/27038538/)** + +![《Netty 实战》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113715369.png) + +这本书可以用来入门 Netty ,内容从 BIO 聊到了 NIO、之后才详细介绍为什么有 Netty、Netty 为什么好用以及 Netty 重要的知识点讲解。 + +这本书基本把 Netty 一些重要的知识点都介绍到了,而且基本都是通过实战的形式讲解。 + +**[《Netty 进阶之路:跟着案例学 Netty》](https://book.douban.com/subject/30381214/)** + +![《Netty 进阶之路:跟着案例学 Netty》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424113747345.png) + +内容都是关于使用 Netty 的实践案例比如内存泄露这些东西。如果你觉得你的 Netty 已经完全入门了,并且你想要对 Netty 掌握的更深的话,推荐你看一下这本书。 + +**[《跟闪电侠学 Netty:Netty 即时聊天实战与底层原理》](https://book.douban.com/subject/35752082/)** + +![](https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220503085034268.png) + +A book to be published in March 2022. This book is divided into two parts. The first part will introduce you to Netty through a practical case of an instant chat system. The second part will help you understand the more important underlying principles of Netty through Netty source code analysis. + +## Performance tuning + +**["The Definitive Guide to Java Performance"](https://book.douban.com/subject/26740520/)** + +!["The Authoritative Guide to Java Performance" - Douban](https://oss.javaguide.cn/github/javaguide/books/image-20220424113809644.png) + +_Hope there are more good books on Java performance optimization! _ + +O'Reilly family book, an introductory book on performance tuning. I personally think performance tuning is essential knowledge for every Java practitioner. + +The practical content introduced in this book is very good, especially JVM tuning. The shortcoming is also obvious, that is, the content is a bit old. There are very few such books on the market. This book is not suitable for beginners. It is recommended that you have already mastered the Java language before reading it. In addition, before reading, it is best to read "In-depth Understanding of Java Virtual Machine" by Zhou Zhiming. + +## Website architecture + +I have read many books on website architecture, such as "Technical Architecture of Large Websites: Core Principles and Case Analysis", "Core Technologies for Website Architecture with Billions of Traffic", "The Way of Architecture Cultivation - Cultivation Practice of Core Technologies such as Billion-level Gateways, Platform Openness, Distribution, Microservices, Fault Tolerance" and so on. + +At present, the only ones I can recommend are Teacher Li Yunhua's **["Learning Architecture from Scratch"](https://book.douban.com/subject/30335935/)** and Teacher Yu Chunlong's **["Software Architecture Design: How to Integrate Technical Architecture and Business Architecture of Large Websites"](https://book.douban.com/subject/30443578/ ""Software Architecture Design: The Integration of Technical Architecture and Business Architecture of Large Websites"")**. + +![](https://oss.javaguide.cn/github/javaguide/books/20210412224443177.png) + +The book "Learning Architecture from Scratch" corresponds to a Geek Time column - "Learning Architecture from Scratch". A lot of the content in it is from this column. You can just buy one of the two. I read a small part of it, and the content is quite comprehensive. It is a book that really talks about how to do architecture. + +![](https://oss.javaguide.cn/github/javaguide/books/20210412232441459.png) + +Transactions and locks, distribution (CAP, distributed transactions...), high concurrency, and high availability are all introduced in the book "Software Architecture Design: The Integration of Technical Architecture and Business Architecture of Large Websites". + +## Interview + +**"JavaGuide Interview Crash Edition"** + +![](https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830103023493.png) + +![](https://oss.javaguide.cn/github/javaguide-mianshituji/image-20220830102925775.png) + +The interview version of [JavaGuide](https://javaguide.cn/) covers most of the Java back-end knowledge points such as collections, JVM, multi-threading and database MySQL. + +The public account’s backend reply: “**Interview Assault**” is available for free without any tricks. + +![JavaGuide official public account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) \ No newline at end of file diff --git a/docs_en/books/search-engine.en.md b/docs_en/books/search-engine.en.md new file mode 100644 index 00000000000..a2a6a81a8b6 --- /dev/null +++ b/docs_en/books/search-engine.en.md @@ -0,0 +1,33 @@ +--- +title: A must-read classic book for search engines +category: computer books +icon: "search" +--- + +## Lucene + +Elasticsearch is developed on the basis of Apache Lucene. Before learning ES, it is recommended to briefly understand the related concepts of Lucene. + +**["Lucene in Action"](https://book.douban.com/subject/6440615/)** is one of the few Chinese-language books on Lucene in China. It is suitable for learning and understanding Lucene-related concepts and common operations. + +!["Lucene Practical Combat"-Practical Combat](https://oss.javaguide.cn/github/javaguide/books/vAJkdYEyol4e6Nr.png) + +## Elasticsearch + +**["One book explains Elasticsearch thoroughly: Principles, advancements and engineering practices"](https://book.douban.com/subject/36716996/)** + +![](https://oss.javaguide.cn/github/javaguide/books/one-book-guide-to-elasticsearch.png) + +Written based on version 8.x, it is currently the latest Elasticsearch explanation book on the Internet. The content covers the core knowledge points of Elastic's official certification and is derived from real project cases and enterprise-level questions and answers. + +**["Elasticsearch Core Technology and Practical Combat"](http://gk.link/a/10bcT ""Elasticsearch Core Technology and Practical Combat"")** + +This Geek Time course is based on Elasticsearch version 7.1, which is relatively new. Moreover, the author is a senior eBay technical expert with 20 years of industry experience, and the quality of the course is guaranteed! + +!["Elasticsearch Core Technology and Practical Combat"-Geek Time](https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png) + +**["Elasticsearch source code analysis and optimization practice"](https://book.douban.com/subject/30386800/)** + +!["Elasticsearch source code analysis and optimization practice"-Douban](https://oss.javaguide.cn/p3-juejin/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.png) + +If you want to further study the principles of Elasticsearch, you can read this book by Zhang Chao. This is the only book on the market that writes Elasticsearch source code. \ No newline at end of file diff --git a/docs_en/books/software-quality.en.md b/docs_en/books/software-quality.en.md new file mode 100644 index 00000000000..ca936048393 --- /dev/null +++ b/docs_en/books/software-quality.en.md @@ -0,0 +1,131 @@ +--- +title: 软件质量必读经典书籍 +category: 计算机书籍 +icon: "highavailable" +head: + - - meta + - name: keywords + content: 软件质量书籍精选 +--- + +下面推荐都是我看过并且我觉得值得推荐的书籍。 + +不过,这些书籍都比较偏理论,只能帮助你建立一个写优秀代码的意识标准。 如果你想要编写更高质量的代码、更高质量的软件,还是应该多去看优秀的源码,多去学习优秀的代码实践。 + +## 代码整洁之道 + +**[《重构》](https://book.douban.com/subject/30468597/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210328174841577.png) + +必看书籍!无需多言。编程书籍领域的瑰宝。 + +世界顶级、国宝级别的 Martin Fowler 的书籍,可以说是软件开发领域最经典的几本书之一。目前已经出了第二版。 + +这是一本值得你看很多遍的书籍。 + +**[《Clean Code》](https://book.douban.com/subject/4199741/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210328174824891.png) + +《Clean Code》是 Bob 大叔的一本经典著作,强烈建议小伙伴们一定要看看。 + +Bob 大叔将自己对整洁代码的理解浓缩在了这本书中,真可谓是对后生的一大馈赠。 + +**[《Effective Java 》](https://book.douban.com/subject/30412517/)** + +![](https://oss.javaguide.cn/github/javaguide/books/82d510c951384383b325080428af6c0a.png) + +《Effective Java 》这本书是 Java 领域国宝级别的书,非常经典。Java 程序员必看! + +这本书主要介绍了在 Java 编程中很多极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。 + +**[《代码大全》](https://book.douban.com/subject/1477390/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210314173253221.png) + +其实,《代码大全(第 2 版)》这本书我本身是不太想推荐给大家了。但是,看在它的豆瓣评分这么高的份上,还是拿出来说说吧! + +这也是一本非常经典的书籍,第二版对第一版进行了重写。 + +我简单地浏览过全书的内容,感觉内容总体比较虚,对于大部分程序员的作用其实不大。如果你想要切实地提高自己的代码质量,《Clean Code》和 《编写可读代码的艺术》我觉得都要比《代码大全》这本书更好。 + +不过,最重要的还是要多看优秀的源码,多学习优秀的代码实践。 + +**[《编写可读代码的艺术》](https://book.douban.com/subject/10797189/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210314175536443.png) + +《编写可读代码的艺术》这本书要表达的意思和《Clean Code》很像,你看它俩的目录就可以看出来了。 + +![](https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230739963.png) + +在我看来,如果你看过 《Clean Code》 的话,就不需要再看这本书了。当然,如果你有时间和精力,也可以快速过一遍。 + +另外,我这里还要推荐一个叫做 **[write-readable-code](https://github.com/biezhi/write-readable-code)** 的仓库。这个仓库的作者免费分享了一系列基于《编写可读代码的艺术》这本书的视频。这一系列视频会基于 Java 语言来教你如何优化咱们的代码。 + +在实践中学习的效果肯定会更好!推荐小伙伴们都抓紧学起来啊! + +![](https://oss.javaguide.cn/github/javaguide/books/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309230743258.png) + +## 程序员职业素养 + +**[《The Clean Coder》](https://book.douban.com/subject/26919457/)** + +![](https://oss.javaguide.cn/github/javaguide/books/20210314191210273.png) + +《 The Clean Coder》是 Bob 大叔的又一经典著作。 + +《Clean Code》和《 The Clean Coder》这两本书在国内都翻译为 《代码整洁之道》,我觉得这个翻译还是不够优雅的。 + +另外,两者的内容差异也很大。《Clean Code》这本书从代码层面来讲解如何提高自己的代码质量。而《The Clean Coder》这本书则是从如何成为一名更优秀的开发者的角度来写的,比如这书会教你如何在自己的领域更专业、如何说不、如何做时间管理、如何处理压力等等。 + +## 架构整洁之道 + +**[《架构整洁之道》](https://book.douban.com/subject/30333919/)** + +![](https://oss.javaguide.cn/github/javaguide/books/2021031412342771.png) + +你没看错,《架构整洁之道》这本书又是 Bob 大叔的经典之作。 + +这本书我强烈安利!认真读完之后,我保证你对编程本质、编程语言的本质、软件设计、架构设计可以有进一步的认识。 + +国内的很多书籍和专栏都借鉴了《架构整洁之道》 这本书。毫不夸张地说,《架构整洁之道》就是架构领域最经典的书籍之一。 + +正如作者说的那样: + +> 如果深入研究计算机编程的本质,我们就会发现这 50 年来,计算机编程基本没有什么大的变化。编程语言稍微进步了一点,工具的质量大大提升了,但是计算机程序的基本构造没有什么变化。 +> +> 虽然我们有了新的编程语言、新的编程框架、新的编程范式,但是软件架构的规则仍然和 1946 年阿兰·图灵写下第一行机器代码的时候一样。 +> +> 这本书就是为了把这些永恒不变的软件架构规则展现出来。 + +## 项目管理 + +**[《人月神话》](https://book.douban.com/subject/1102259/)** + +![](https://oss.javaguide.cn/2021/03/8ece325c-4491-4ffd-9d3d-77e95159ec40.png) + +这本书主要描述了软件开发的基本定律:**一个需要 10 天才能干完的活,不可能让 10 个人在 1 天干完!** + +看书名的第一眼,感觉不像是技术类的书籍。但是,就是这样一个看似和编程不沾边的书名,却成了编程领域长久相传的经典。 + +**这本书对于现代软件尤其是复杂软件的开发的规范化有深刻的意义。** + +**[《领域驱动设计:软件核心复杂性应对之道》](https://book.douban.com/subject/5344973/)** + +![](https://oss.javaguide.cn/2021/03/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png) + +这本领域驱动设计方面的经典之作一直被各种推荐,但是我还来及读。 + +## 其他 + +- [《代码的未来》](https://book.douban.com/subject/24536403/):这本书的作者是 Ruby 之父松本行弘,算是一本年代比较久远的书籍(13 年出版),不过,还是非常值得一读。这本书的内容主要介绍是编程/编程语言的本质。我个人还是比较喜欢松本行弘的文字风格,并且,你看他的文章也确实能够有所收获。 +- [《深入浅出设计模式》](https://book.douban.com/subject/1488876/):比较有趣的风格,适合设计模式入门。 +- [《软件架构设计:大型网站技术架构与业务架构融合之道》](https://book.douban.com/subject/30443578/):内容非常全面。适合面试前突击一些比较重要的理论知识,也适合拿来扩充/完善自己的技术广度。 +- ["Microservice Architecture Design Patterns"](https://book.douban.com/subject/33425123/): This book is written by Chris Richardson, one of the world's top ten software architects and a pioneer of microservice architecture. It has a Douban score of 9.6. The sample code uses Java language and Spring framework. Helps you design, implement, test and deploy microservices-based applications. + +Finally, I recommend two related documents: + +- **Alibaba Java Development Manual**: +- **Google Java Programming Style Guide**: \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/10-classical-sorting-algorithms.en.md b/docs_en/cs-basics/algorithms/10-classical-sorting-algorithms.en.md new file mode 100644 index 00000000000..1173afcdc46 --- /dev/null +++ b/docs_en/cs-basics/algorithms/10-classical-sorting-algorithms.en.md @@ -0,0 +1,778 @@ +--- +title: 十大经典排序算法总结 +category: 计算机基础 +tag: + - 算法 +head: + - - meta + - name: keywords + content: 排序算法,快速排序,归并排序,堆排序,冒泡排序,选择排序,插入排序,希尔排序,桶排序,计数排序,基数排序,时间复杂度,空间复杂度,稳定性 + - - meta + - name: description + content: 系统梳理十大经典排序算法,附复杂度与稳定性对比,覆盖比较类与非比较类排序的核心原理与实现场景,帮助快速选型与优化。 +--- + +> 本文转自:,JavaGuide 对其做了补充完善。 + + + +## 引言 + +所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。 + +## 简介 + +### 排序算法总结 + +常见的内部排序算法有:**插入排序**、**希尔排序**、**选择排序**、**冒泡排序**、**归并排序**、**快速排序**、**堆排序**、**基数排序**等,本文只讲解内部排序算法。用一张表格概括: + +| 排序算法 | 时间复杂度(平均) | 时间复杂度(最差) | 时间复杂度(最好) | 空间复杂度 | 排序方式 | 稳定性 | +| -------- | ------------------ | ------------------ | ------------------ | ---------- | -------- | ------ | +| 冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 内部排序 | 稳定 | +| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 内部排序 | 不稳定 | +| 插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 内部排序 | 稳定 | +| 希尔排序 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | 内部排序 | 不稳定 | +| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 外部排序 | 稳定 | +| 快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 内部排序 | 不稳定 | +| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 内部排序 | 不稳定 | +| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 外部排序 | 稳定 | +| 桶排序 | O(n+k) | O(n^2) | O(n+k) | O(n+k) | 外部排序 | 稳定 | +| 基数排序 | O(n×k) | O(n×k) | O(n×k) | O(n+k) | 外部排序 | 稳定 | + +**术语解释**: + +- **n**:数据规模,表示待排序的数据量大小。 +- **k**:“桶” 的个数,在某些特定的排序算法中(如基数排序、桶排序等),表示分割成的独立的排序区间或类别的数量。 +- **内部排序**:所有排序操作都在内存中完成,不需要额外的磁盘或其他存储设备的辅助。这适用于数据量小到足以完全加载到内存中的情况。 +- **外部排序**:当数据量过大,不可能全部加载到内存中时使用。外部排序通常涉及到数据的分区处理,部分数据被暂时存储在外部磁盘等存储设备上。 +- **稳定**:如果 A 原本在 B 前面,而 $A=B$,排序之后 A 仍然在 B 的前面。 +- **不稳定**:如果 A 原本在 B 的前面,而 $A=B$,排序之后 A 可能会出现在 B 的后面。 +- **时间复杂度**:定性描述一个算法执行所耗费的时间。 +- **空间复杂度**:定性描述一个算法执行所需内存的大小。 + +### 排序算法分类 + +十种常见排序算法可以分类两大类别:**比较类排序**和**非比较类排序**。 + +![排序算法分类](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/sort2.png) + +常见的**快速排序**、**归并排序**、**堆排序**以及**冒泡排序**等都属于**比较类排序算法**。比较类排序是通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 `O(nlogn)`,因此也称为非线性时间比较类排序。在冒泡排序之类的排序中,问题规模为 `n`,又因为需要比较 `n` 次,所以平均时间复杂度为 `O(n²)`。在**归并排序**、**快速排序**之类的排序中,问题规模通过**分治法**消减为 `logn` 次,所以时间复杂度平均 `O(nlogn)`。 + +比较类排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。 + +而**计数排序**、**基数排序**、**桶排序**则属于**非比较类排序算法**。非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。由于它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度 $O(n)$。 + +非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。 + +## 冒泡排序 (Bubble Sort) + +冒泡排序是一种简单的排序算法。它重复地遍历要排序的序列,依次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作是重复地进行直到没有再需要交换为止,此时说明该序列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端。 + +### 算法步骤 + +1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个; +2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数; +3. 针对所有的元素重复以上的步骤,除了最后一个; +4. 重复步骤 1~3,直到排序完成。 + +### 图解算法 + +![冒泡排序](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/bubble_sort.gif) + +### 代码实现 + +```java +/** + * 冒泡排序 + * @param arr + * @return arr + */ +public static int[] bubbleSort(int[] arr) { + for (int i = 1; i < arr.length; i++) { + // Set a flag, if true, that means the loop has not been swapped, + // that is, the sequence has been ordered, the sorting has been completed. + boolean flag = true; + for (int j = 0; j < arr.length - i; j++) { + if (arr[j] > arr[j + 1]) { + int tmp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = tmp; + // Change flag + flag = false; + } + } + if (flag) { + break; + } + } + return arr; +} +``` + +**此处对代码做了一个小优化,加入了 `is_sorted` Flag,目的是将算法的最佳时间复杂度优化为 `O(n)`,即当原输入序列就是排序好的情况下,该算法的时间复杂度就是 `O(n)`。** + +### 算法分析 + +- **稳定性**:稳定 +- **时间复杂度**:最佳:$O(n)$ ,最差:$O(n^2)$, 平均:$O(n^2)$ +- **空间复杂度**:$O(1)$ +- **排序方式**:In-place + +## 选择排序 (Selection Sort) + +选择排序是一种简单直观的排序算法,无论什么数据进去都是 $O(n^2)$ 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 + +### 算法步骤 + +1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 +2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 +3. 重复第 2 步,直到所有元素均排序完毕。 + +### 图解算法 + +![Selection Sort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/selection_sort.gif) + +### 代码实现 + +```java +/** + * 选择排序 + * @param arr + * @return arr + */ +public static int[] selectionSort(int[] arr) { + for (int i = 0; i < arr.length - 1; i++) { + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[j] < arr[minIndex]) { + minIndex = j; + } + } + if (minIndex != i) { + int tmp = arr[i]; + arr[i] = arr[minIndex]; + arr[minIndex] = tmp; + } + } + return arr; +} +``` + +### 算法分析 + +- **稳定性**:不稳定 +- **时间复杂度**:最佳:$O(n^2)$ ,最差:$O(n^2)$, 平均:$O(n^2)$ +- **空间复杂度**:$O(1)$ +- **排序方式**:In-place + +## 插入排序 (Insertion Sort) + +插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 $O(1)$ 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 + +插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 + +插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。 + +### 算法步骤 + +1. 从第一个元素开始,该元素可以认为已经被排序; +2. 取出下一个元素,在已经排序的元素序列中从后向前扫描; +3. 如果该元素(已排序)大于新元素,将该元素移到下一位置; +4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置; +5. 将新元素插入到该位置后; +6. 重复步骤 2~5。 + +### 图解算法 + +![insertion_sort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/insertion_sort.gif) + +### 代码实现 + +```java +/** + * 插入排序 + * @param arr + * @return arr + */ +public static int[] insertionSort(int[] arr) { + for (int i = 1; i < arr.length; i++) { + int preIndex = i - 1; + int current = arr[i]; + while (preIndex >= 0 && current < arr[preIndex]) { + arr[preIndex + 1] = arr[preIndex]; + preIndex -= 1; + } + arr[preIndex + 1] = current; + } + return arr; +} +``` + +### 算法分析 + +- **稳定性**:稳定 +- **时间复杂度**:最佳:$O(n)$ ,最差:$O(n^2)$, 平均:$O(n2)$ +- **空间复杂度**:O(1)$ +- **排序方式**:In-place + +## 希尔排序 (Shell Sort) + +希尔排序是希尔 (Donald Shell) 于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为递减增量排序算法,同时该算法是冲破 $O(n^2)$ 的第一批算法之一。 + +希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。 + +### 算法步骤 + +我们来看下希尔排序的基本步骤,在此我们选择增量 $gap=length/2$,缩小增量继续以 $gap = gap/2$ 的方式,这种增量选择我们可以用一个序列来表示,$\lbrace \frac{n}{2}, \frac{(n/2)}{2}, \dots, 1 \rbrace$,称为**增量序列**。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。 + +先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: + +- 选择一个增量序列 $\lbrace t_1, t_2, \dots, t_k \rbrace$,其中 $t_i \gt t_j, i \lt j, t_k = 1$; +- 按增量序列个数 k,对序列进行 k 趟排序; +- 每趟排序,根据对应的增量 $t$,将待排序列分割成若干长度为 $m$ 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 + +### 图解算法 + +![shell_sort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/shell_sort.png) + +### 代码实现 + +```java +/** + * 希尔排序 + * + * @param arr + * @return arr + */ +public static int[] shellSort(int[] arr) { + int n = arr.length; + int gap = n / 2; + while (gap > 0) { + for (int i = gap; i < n; i++) { + int current = arr[i]; + int preIndex = i - gap; + // Insertion sort + while (preIndex >= 0 && arr[preIndex] > current) { + arr[preIndex + gap] = arr[preIndex]; + preIndex -= gap; + } + arr[preIndex + gap] = current; + + } + gap /= 2; + } + return arr; +} +``` + +### 算法分析 + +- **稳定性**:不稳定 +- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(n^2)$ 平均:$O(nlogn)$ +- **空间复杂度**:$O(1)$ + +## 归并排序 (Merge Sort) + +归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并。 + +和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 $O(nlogn)$ 的时间复杂度。代价是需要额外的内存空间。 + +### 算法步骤 + +归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下: + +1. 如果输入内只有一个元素,则直接返回,否则将长度为 $n$ 的输入序列分成两个长度为 $n/2$ 的子序列; +2. 分别对这两个子序列进行归并排序,使子序列变为有序状态; +3. 设定两个指针,分别指向两个已经排序子序列的起始位置; +4. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置; +5. 重复步骤 3 ~ 4 直到某一指针达到序列尾; +6. 将另一序列剩下的所有元素直接复制到合并序列尾。 + +### 图解算法 + +![MergeSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/merge_sort.gif) + +### 代码实现 + +```java +/** + * 归并排序 + * + * @param arr + * @return arr + */ +public static int[] mergeSort(int[] arr) { + if (arr.length <= 1) { + return arr; + } + int middle = arr.length / 2; + int[] arr_1 = Arrays.copyOfRange(arr, 0, middle); + int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length); + return merge(mergeSort(arr_1), mergeSort(arr_2)); +} + +/** + * Merge two sorted arrays + * + * @param arr_1 + * @param arr_2 + * @return sorted_arr + */ +public static int[] merge(int[] arr_1, int[] arr_2) { + int[] sorted_arr = new int[arr_1.length + arr_2.length]; + int idx = 0, idx_1 = 0, idx_2 = 0; + while (idx_1 < arr_1.length && idx_2 < arr_2.length) { + if (arr_1[idx_1] < arr_2[idx_2]) { + sorted_arr[idx] = arr_1[idx_1]; + idx_1 += 1; + } else { + sorted_arr[idx] = arr_2[idx_2]; + idx_2 += 1; + } + idx += 1; + } + if (idx_1 < arr_1.length) { + while (idx_1 < arr_1.length) { + sorted_arr[idx] = arr_1[idx_1]; + idx_1 += 1; + idx += 1; + } + } else { + while (idx_2 < arr_2.length) { + sorted_arr[idx] = arr_2[idx_2]; + idx_2 += 1; + idx += 1; + } + } + return sorted_arr; +} +``` + +### 算法分析 + +- **稳定性**:稳定 +- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(nlogn)$, 平均:$O(nlogn)$ +- **空间复杂度**:$O(n)$ + +## 快速排序 (Quick Sort) + +快速排序用到了分治思想,同样的还有归并排序。乍看起来快速排序和归并排序非常相似,都是将问题变小,先排序子串,最后合并。不同的是快速排序在划分子问题的时候经过多一步处理,将划分的两组数据划分为一大一小,这样在最后合并的时候就不必像归并排序那样再进行比较。但也正因为如此,划分的不定性使得快速排序的时间复杂度并不稳定。 + +快速排序的基本思想:通过一趟排序将待排序列分隔成独立的两部分,其中一部分记录的元素均比另一部分的元素小,则可分别对这两部分子序列继续进行排序,以达到整个序列有序。 + +### 算法步骤 + +快速排序使用[分治法](https://zh.wikipedia.org/wiki/分治法)(Divide and conquer)策略来把一个序列分为较小和较大的 2 个子序列,然后递归地排序两个子序列。具体算法描述如下: + +1. **选择基准(Pivot)** :从数组中选一个元素作为基准。为了避免最坏情况,通常会随机选择。 +2. **分区(Partition)** :重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。 +3. **递归(Recurse)** :递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。 + +**关于性能,这也是它与归并排序的关键区别:** + +- **平均和最佳情况:** 它的时间复杂度是 $O(nlogn)$。这种情况发生在每次分区都能把数组分成均等的两半。 +- **最坏情况:** 它的时间复杂度会退化到 $O(n^2)$。这发生在每次我们选的基准都是当前数组的最小值或最大值时,比如对一个已经排好序的数组,每次都选第一个元素做基准,这就会导致分区极其不均,算法退化成类似冒泡排序。这就是为什么**随机选择基准**非常重要。 + +### 图解算法 + +![RandomQuickSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/random_quick_sort.gif) + +### 代码实现 + +```java +import java.util.concurrent.ThreadLocalRandom; + +class Solution { + public int[] sortArray(int[] a) { + quick(a, 0, a.length - 1); + return a; + } + + // 快速排序的核心递归函数 + void quick(int[] a, int left, int right) { + if (left >= right) { // 递归终止条件:区间只有一个或没有元素 + return; + } + int p = partition(a, left, right); // 分区操作,返回分区点索引 + quick(a, left, p - 1); // 对左侧子数组递归排序 + quick(a, p + 1, right); // 对右侧子数组递归排序 + } + + // 分区函数:将数组分为两部分,小于基准值的在左,大于基准值的在右 + int partition(int[] a, int left, int right) { + // 随机选择一个基准点,避免最坏情况(如数组接近有序) + int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left; + swap(a, left, idx); // 将基准点放在数组的最左端 + int pv = a[left]; // 基准值 + int i = left + 1; // 左指针,指向当前需要检查的元素 + int j = right; // 右指针,从右往左寻找比基准值小的元素 + + while (i <= j) { + // 左指针向右移动,直到找到一个大于等于基准值的元素 + while (i <= j && a[i] < pv) { + i++; + } + // 右指针向左移动,直到找到一个小于等于基准值的元素 + while (i <= j && a[j] > pv) { + j--; + } + // 如果左指针尚未越过右指针,交换两个不符合位置的元素 + if (i <= j) { + swap(a, i, j); + i++; + j--; + } + } + // 将基准值放到分区点位置,使得基准值左侧小于它,右侧大于它 + swap(a, j, left); + return j; + } + + // 交换数组中两个元素的位置 + void swap(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; + } +} +``` + +### Algorithm analysis + +- **STABILITY**: Unstable +- **Time Complexity**: Best: $O(nlogn)$, Worst: $O(n^2)$, Average: $O(nlogn)$ +- **Space complexity**: $O(logn)$ + +## Heap Sort + +Heap sort refers to a sorting algorithm designed using the data structure of a heap. A heap is a structure that approximates a complete binary tree and satisfies the properties of a heap: that is, the value of a child node is always less than (or greater than) its parent node. + +### Algorithm steps + +1. Construct the initial to-be-sorted sequence $(R_1, R_2, \dots, R_n)$ into a large top heap, which is the initial unordered area; +2. Exchange the top element $R_1$ with the last element $R_n$, and get the new unordered area $(R_1, R_2, \dots, R_{n-1})$ and the new ordered area $R_n$, and satisfy $R_i \leqslant R_n (i \in 1, 2,\dots, n-1)$; +3. Since the new heap top $R_1$ after the exchange may violate the properties of the heap, it is necessary to adjust the current unordered area $(R_1, R_2, \dots, R_{n-1})$ to a new heap, and then exchange $R_1$ with the last element of the unordered area again to obtain a new unordered area $(R_1, R_2, \dots, R_{n-2})$ and a new ordered area $(R_{n-1}, R_n)$. Repeat this process until the number of elements in the ordered area is $n-1$, then the entire sorting process is completed. + +### Graphical algorithm + +![HeapSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/heap_sort.gif) + +### Code implementation + +```java +// Global variable that records the length of an array; +static int heapLen; + +/** + * Swap the two elements of an array + * @param arr + * @param i + * @param j + */ +private static void swap(int[] arr, int i, int j) { + int tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +/** + * Build Max Heap + * @param arr + */ +private static void buildMaxHeap(int[] arr) { + for (int i = arr.length / 2 - 1; i >= 0; i--) { + heapify(arr, i); + } +} + +/** + * Adjust it to the maximum heap + * @param arr + * @param i + */ +private static void heapify(int[] arr, int i) { + int left = 2 * i + 1; + int right = 2 * i + 2; + int largest = i; + if (right < heapLen && arr[right] > arr[largest]) { + largest = right; + } + if (left < heapLen && arr[left] > arr[largest]) { + largest = left; + } + if (largest != i) { + swap(arr, largest, i); + heapify(arr, largest); + } +} + +/** + *Heap Sort + * @param arr + * @return + */ +public static int[] heapSort(int[] arr) { + // index at the end of the heap + heapLen = arr.length; + // build MaxHeap + buildMaxHeap(arr); + for (int i = arr.length - 1; i > 0; i--) { + // Move the top of the heap to the tail of the heap in turn + swap(arr, 0, i); + heapLen -= 1; + heapify(arr, 0); + } + return arr; +} +``` + +### Algorithm analysis + +- **STABILITY**: Unstable +- **Time Complexity**: Best: $O(nlogn)$, Worst: $O(nlogn)$, Average: $O(nlogn)$ +- **Space Complexity**: $O(1)$ + +## Counting Sort + +The core of counting sort is to convert the input data values into keys and store them in the additionally opened array space. As a linear time complexity sort, **counting sort requires that the input data must be an integer with a certain range**. + +Counting sort is a stable sorting algorithm. Counting sort uses an additional array `C`, where the `i`th element is the number of elements in the array `A` to be sorted whose value is equal to `i`. Then arrange the elements in `A` to the correct position according to the array `C`. **It can only sort integers**. + +### Algorithm steps + +1. Find the maximum value `max` and the minimum value `min` in the array; +2. Create a new array `C`, whose length is `max-min+1`, and the default values of its elements are 0; +3. Traverse the element `A[i]` in the original array `A`, use `A[i] - min` as the index of the `C` array, and use the number of occurrences of the element in `A` as the value of `C[A[i] - min]`; +4. For the `C` array deformation, the value of the new element is the sum of the value of the element and the previous element, that is, when `i>1`, `C[i] = C[i] + C[i-1]`; +5. Create the result array `R` with the same length as the original array. +6. **Traverse the elements `A[i]` in the original array `A` from back to front, use `A[i]` minus the minimum value `min` as the index, find the corresponding value `C[A[i] - min]`, `C[A[i] - min] - 1` in the count array `C`, which is `A[i]` in the result array `R` After completing the above operations, reduce `count[A[i] - min]` by 1. + +### Graphical algorithm + +![CountingSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/counting_sort.gif) + +### Code implementation + +```java +/** + * Gets the maximum and minimum values in the array + * + * @param arr + * @return + */ +private static int[] getMinAndMax(int[] arr) { + int maxValue = arr[0]; + int minValue = arr[0]; + for (int i = 0; i < arr.length; i++) { + if (arr[i] > maxValue) { + maxValue = arr[i]; + } else if (arr[i] < minValue) { + minValue = arr[i]; + } + } + return new int[] { minValue, maxValue }; +} + +/** + * Counting Sort + * + * @param arr + * @return + */ +public static int[] countingSort(int[] arr) { + if (arr. length < 2) { + return arr; + } + int[] extremum = getMinAndMax(arr); + int minValue = extremum[0]; + int maxValue = extremum[1]; + int[] countArr = new int[maxValue - minValue + 1]; + int[] result = new int[arr.length]; + + for (int i = 0; i < arr.length; i++) { + countArr[arr[i] - minValue] += 1; + } + for (int i = 1; i < countArr.length; i++) { + countArr[i] += countArr[i - 1]; + } + for (int i = arr.length - 1; i >= 0; i--) { + int idx = countArr[arr[i] - minValue] - 1; + result[idx] = arr[i]; + countArr[arr[i] - minValue] -= 1; + } + return result; +}``` + +### Algorithm analysis + +When the input elements are `n` integers between `0` and `k`, its running time is $O(n+k)$. Counting sort is not comparison sort, and sorting is faster than any comparison sort algorithm. Since the length of the array `C` used for counting depends on the range of the data in the array to be sorted (equal to the difference between the maximum value and the minimum value of the array to be sorted plus 1**), this makes counting sorting require a lot of additional memory space for arrays with large data ranges. + +- **STABILITY**: Stable +- **Time Complexity**: Best: $O(n+k)$ Worst: $O(n+k)$ Average: $O(n+k)$ +- **Space Complexity**: $O(k)$ + +## Bucket Sort + +Bucket sort is an upgraded version of counting sort. It makes use of the mapping relationship of functions. The key to efficiency lies in the determination of this mapping function. In order to make bucket sorting more efficient, we need to do these two things: + +1. If there is sufficient extra space, increase the number of buckets as much as possible +2. The mapping function used can evenly distribute the input N data into K buckets. + +The working principle of bucket sorting: Assume that the input data obeys a uniform distribution, divide the data into a limited number of buckets, and then sort each bucket separately (it is possible to use other sorting algorithms or continue to use bucket sorting in a recursive manner. + +### Algorithm steps + +1. Set a BucketSize as how many different values can be placed in each bucket; +2. Traverse the input data and map the data to the corresponding buckets in turn; +3. To sort each non-empty bucket, you can use other sorting methods, or you can use bucket sorting recursively; +4. Splice the sorted data from non-empty buckets. + +### Graphical algorithm + +![BucketSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/bucket_sort.gif) + +### Code implementation + +```java +/** + * Gets the maximum and minimum values in the array + * @param arr + * @return + */ +private static int[] getMinAndMax(List arr) { + int maxValue = arr.get(0); + int minValue = arr.get(0); + for (int i : arr) { + if (i > maxValue) { + maxValue = i; + } else if (i < minValue) { + minValue = i; + } + } + return new int[] { minValue, maxValue }; +} + +/** + *Bucket Sort + * @param arr + * @return + */ +public static List bucketSort(List arr, int bucket_size) { + if (arr.size() < 2 || bucket_size == 0) { + return arr; + } + int[] extremum = getMinAndMax(arr); + int minValue = extremum[0]; + int maxValue = extremum[1]; + int bucket_cnt = (maxValue - minValue) / bucket_size + 1; + List> buckets = new ArrayList<>(); + for (int i = 0; i < bucket_cnt; i++) { + buckets.add(new ArrayList()); + } + for (int element : arr) { + int idx = (element - minValue) / bucket_size; + buckets.get(idx).add(element); + } + for (int i = 0; i < buckets.size(); i++) { + if (buckets.get(i).size() > 1) { + buckets.set(i, sort(buckets.get(i), bucket_size / 2)); + } + } + ArrayList result = new ArrayList<>(); + for (List bucket : buckets) { + for (int element : bucket) { + result.add(element); + } + } + return result; +} +``` + +### Algorithm analysis + +- **STABILITY**: Stable +- **Time Complexity**: Best: $O(n+k)$ Worst: $O(n^2)$ Average: $O(n+k)$ +- **Space Complexity**: $O(n+k)$ + +## Radix Sort + +Radix sorting is also a non-comparative sorting algorithm. It sorts each digit in the element, starting from the lowest digit. The complexity is $O(n×k)$, $n$ is the length of the array, and $k$ is the maximum number of digits in the element in the array; + +Radix sorting is to sort by low order first, and then collect; then sort by high order, and then collect; and so on, until the highest order. Sometimes some attributes have a priority order, sorted by low priority first, and then by high priority. The final order is that those with higher priority come first, and those with the same high priority and lower priority come first. Radix sorting is based on separate sorting and separate collection, so it is stable. + +### Algorithm steps + +1. Get the maximum number in the array and the number of digits, which is the number of iterations $N$ (for example: the maximum value in the array is 1000, then $N=4$); +2. `A` is the original array, and each bit is taken from the lowest bit to form the `radix` array; +3. Perform counting sorting on `radix` (using the feature that counting sorting is suitable for small range numbers); +4. Assign `radix` to the original array in turn; +5. Repeat steps 2~4 $N$ times + +### Graphical algorithm + +![RadixSort](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/radix_sort.gif) + +### Code implementation + +```java +/** + * Radix Sort + * + * @param arr + * @return + */ +public static int[] radixSort(int[] arr) { + if (arr. length < 2) { + return arr; + } + int N = 1; + int maxValue = arr[0]; + for (int element : arr) { + if (element > maxValue) { + maxValue = element; + } + } + while (maxValue / 10 != 0) { + maxValue = maxValue / 10; + N += 1; + } + for (int i = 0; i < N; i++) { + List> radix = new ArrayList<>(); + for (int k = 0; k < 10; k++) { + radix.add(new ArrayList()); + } + for (int element : arr) { + int idx = (element / (int) Math.pow(10, i)) % 10; + radix.get(idx).add(element); + } + int idx = 0; + for (List l : radix) { + for (int n : l) { + arr[idx++] = n; + } + } + } + return arr; +} +``` + +### Algorithm analysis + +- **STABILITY**: Stable +- **Time Complexity**: Best: $O(n×k)$ Worst: $O(n×k)$ Average: $O(n×k)$ +- **Space Complexity**: $O(n+k)$ + +**radix sort vs counting sort vs bucket sort** + +These three sorting algorithms all use the concept of buckets, but there are obvious differences in how they are used: + +- Radix sort: allocate buckets based on each digit of the key value +- Counting sort: Each bucket only stores a single key value +- Bucket sorting: Each bucket stores a certain range of values + +## Reference article + +- - +- + + \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/classical-algorithm-problems-recommendations.en.md b/docs_en/cs-basics/algorithms/classical-algorithm-problems-recommendations.en.md new file mode 100644 index 00000000000..19a7ee7f755 --- /dev/null +++ b/docs_en/cs-basics/algorithms/classical-algorithm-problems-recommendations.en.md @@ -0,0 +1,120 @@ +--- +title: Summary of classic algorithm ideas (including LeetCode topic recommendations) +category: Computer Basics +tag: + - Algorithm +head: + - - meta + - name: keywords + content: Greedy, divide and conquer, backtracking, dynamic programming, bisection, double pointers, algorithmic ideas, topic recommendations + - - meta + - name: description + content: Summarizes common algorithm ideas and problem-solving templates, and recommends typical questions, emphasizing thinking paths and complexity trade-offs to quickly build a problem-solving system. +--- + +## Greedy Algorithm + +### Algorithmic Thoughts + +The essence of greed is to select the local optimum at each stage to achieve the global optimum. + +### General problem solving steps + +- Decompose the problem into several sub-problems +- Find a suitable greedy strategy +- Solve the optimal solution for each sub-problem +- Stack local optimal solutions into global optimal solutions + +### LeetCode + +455. Distribute cookies: + +121. The best time to buy and sell stocks: + +122. Best time to buy and sell stocks II: + +55. Jump game: + +45. Jump Game II: + +## Dynamic programming + +### Algorithmic Thoughts + +Each state in dynamic programming must be derived from the previous state. This is different from greed. Greedy has no state derivation, but directly selects the optimal one from the local area. + +Classic questions: 01 backpack, complete backpack + +### General problem solving steps + +- Determine the meaning of dp array (dp table) and subscripts +- Determine the recurrence formula +- How to initialize the dp array +- Determine the order of traversal +- Take an example to derive the dp array + +### LeetCode + +509. Fibonacci number: + +746. Use the minimum cost to climb stairs: + +416. Partition equal sum subset: + +518. Change exchange: + +647. Palindromic substrings: + +516. The longest palindromic subsequence: + +## Backtracking algorithm + +### Algorithmic Thoughts + +The backtracking algorithm is actually a search attempt process similar to enumeration. It mainly searches for the solution to the problem during the search attempt process. When it is found that the solution conditions are no longer met, + +When the problem occurs, "backtrack" and try other paths. Its essence is exhaustion. + +Classic topic: 8 queens + +### General problem solving steps + +- For a given problem, define the solution space of the problem, which contains at least one (optimal) solution to the problem. +- Determine the structure of the solution space that is easy to search, so that the entire solution space can be easily searched using the backtracking method. +- Search the solution space in a depth-first manner, and use pruning functions to avoid invalid searches during the search process. + +### leetcode + +77. Combinations: + +39. Combination sum: + +40. Combination sum II: + +78. Subsets: + +90. Subset II: + +51.N Queen: + +## Divide and conquer algorithm + +### Algorithmic Thoughts + +Decompose a problem of size N into K sub-problems of smaller size, which are independent of each other and have the same properties as the original problem. By finding the solution to the subproblem, you can get the solution to the original problem. + +Classic questions: Binary search, Tower of Hanoi problem + +### General problem solving steps + +- Decompose the original problem into several smaller, independent sub-problems with the same form as the original problem; +- If the sub-problem is small and easy to solve, solve it directly, otherwise solve each sub-problem recursively +- Combine the solutions of each subproblem into the solution of the original problem. + +### LeetCode + +108. Convert an ordered array into a binary search tree: + +148. Sort list: + +23. Merge k ascending linked lists: \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/common-data-structures-leetcode-recommendations.en.md b/docs_en/cs-basics/algorithms/common-data-structures-leetcode-recommendations.en.md new file mode 100644 index 00000000000..1049ca81291 --- /dev/null +++ b/docs_en/cs-basics/algorithms/common-data-structures-leetcode-recommendations.en.md @@ -0,0 +1,71 @@ +--- +title: Recommended classic LeetCode questions on common data structures +category: Computer Basics +tag: + - Algorithm +head: + - - meta + - name: keywords + content: LeetCode, array, linked list, stack, queue, binary tree, question recommendation, question brushing + - - meta + - name: description + content: A list of classic LeetCode questions is organized according to data structure categories, focusing on high-frequency and core test points to help systematic review and consolidation. +--- + +## Array + +704. Binary search: + +80. Remove duplicates from sorted array II: + +977. Squares of a sorted array: + +## Linked list + +707. Design linked list: + +206. Reverse linked list: + +92. Reverse linked list II: + +61. Rotating linked list: + +## Stack and Queue + +232. Use stack to implement queue: + +225. Use queues to implement stacks: + +347. Top K high-frequency elements: + +239. Maximum sliding window: + +## Binary tree + +105. Construct a binary tree from preorder and inorder traversal: + +117. Populating the next right node pointer in each node II: + +236. The latest common ancestor of a binary tree: + +129. Find the sum of numbers from root node to leaf node: + +102. Level-order traversal of binary trees: + +530. Minimum absolute difference of binary search tree: + +## Picture + +200. Number of islands: + +207. Course schedule: + +210. Course Schedule II: + +## Heap + +215. The Kth largest element in an array: + +216. Median of data stream: + +217. Top K high-frequency elements: \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/linkedlist-algorithm-problems.en.md b/docs_en/cs-basics/algorithms/linkedlist-algorithm-problems.en.md new file mode 100644 index 00000000000..37468160db8 --- /dev/null +++ b/docs_en/cs-basics/algorithms/linkedlist-algorithm-problems.en.md @@ -0,0 +1,395 @@ +--- +title: Several common linked list algorithm questions +category: Computer Basics +tag: + - Algorithm +head: + - - meta + - name: keywords + content: Linked list algorithm, adding two numbers, reversing linked lists, ring detection, merging linked lists, complexity analysis + - - meta + - name: description + content: Selected ideas and implementation of high-frequency linked list questions, covering scenarios such as adding two numbers, reversing, and ring detection, emphasizing boundary processing and complexity analysis. +--- + + + +## 1. Add two numbers + +### Title description + +> Leetcode: Given two non-empty linked lists to represent two non-negative integers. Digits are stored in reverse order, and each of their nodes stores only a single digit. Add two numbers and return a new linked list. +> +> You can assume that except for the number 0, neither number will start with a zero. + +Example: + +```plain +Input: (2 -> 4 -> 3) + (5 -> 6 -> 4) +Output: 7 -> 0 -> 8 +Reason: 342 + 465 = 807 +``` + +### Problem analysis + +Leetcode official detailed answer address: + + + +> When operating on the head node, consider creating a dummy node dummy, and use dummy->next to represent the real head node. This avoids dealing with boundary issues where the head node is empty. + +We use variables to track carry and simulate carry-by starting from the head of the table containing the least significant bit +The process of bit addition. + +![Figure 1, visualization of the method of adding two numbers: 342 + 465 = 807, each node contains a number, and the numbers are stored in reverse bit order. ](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/34910956.jpg) + +### Solution + +**We start adding from the least significant bit, which is the head of the lists l1 and l2. Note that carry needs to be considered! ** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ + //https://leetcode-cn.com/problems/add-two-numbers/description/ +class Solution { +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode dummyHead = new ListNode(0); + ListNode p = l1, q = l2, curr = dummyHead; + //carry represents the carry number + int carry = 0; + while (p != null || q != null) { + int x = (p != null) ? p.val : 0; + int y = (q != null) ? q.val : 0; + int sum = carry + x + y; + //carry number + carry = sum / 10; + //The value of the new node is sum % 10 + curr.next = new ListNode(sum % 10); + curr = curr.next; + if (p != null) p = p.next; + if (q != null) q = q.next; + } + if (carry > 0) { + curr.next = new ListNode(carry); + } + return dummyHead.next; +} +} +``` + +## 2. Flip the linked list + +### Title description + +> Jianzhi offer: Input a linked list, after reversing the linked list, output all elements of the linked list. + +![Flip linked list](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/81431871.jpg) + +### Problem analysis + +This algorithm question, to put it bluntly, is: how to make the next node point to the previous node! A next node is defined in the following code. This node is mainly used to save the node to be reversed to the beginning to prevent the linked list from "breaking". + +### Solution + +```java +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +} +``` + +```java +/** + * + * @author Snailclimb + * @date September 19, 2018 + * @Description: TODO + */ +public class Solution { + + public ListNode ReverseList(ListNode head) { + + ListNode next = null; + ListNode pre = null; + + while (head != null) { + //Save the node to be reversed to the beginning + next = head.next; + //The node to be reversed points to the previous node that has been reversed (note: it will point to null when reversed for the first time) + head.next = pre; + //The previous node that has been reversed to the head + pre = head; + // Go all the way to the end of the linked list + head = next; + } + return pre; + } + +} +``` + +Test method: + +```java + public static void main(String[] args) { + + ListNode a = new ListNode(1); + ListNode b = new ListNode(2); + ListNode c = new ListNode(3); + ListNode d = new ListNode(4); + ListNode e = new ListNode(5); + a.next = b; + b.next = c; + c.next = d; + d.next = e; + new Solution().ReverseList(a); + while (e != null) { + System.out.println(e.val); + e = e.next; + } + } +``` + +Output: + +```plain +5 +4 +3 +2 +1 +``` + +## 3. The k-th node from the last in the linked list + +### Title description + +> Sword-finger offer: Input a linked list and output the k-th node from the last in the linked list. + +### Problem analysis + +> **The k-th node from the last in the linked list is also the positive (L-K+1) node. If you only know a little bit, this question is basically no problem! ** + +First, there are two nodes/pointers. One node node1 starts running first. After the pointer node1 reaches k-1 nodes, the other node node2 starts running. When node1 reaches the end, the node pointed by node2 is the k-th node from the bottom, which is the positive (L-K+1) node. + +### Solution + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ + +// Time complexity O(n), just one traversal +// https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking +public class Solution { + public ListNode FindKthToTail(ListNode head, int k) { + // If the linked list is empty or k is less than or equal to 0 + if (head == null || k <= 0) { + return null; + } + // Declare two nodes pointing to the head node + ListNode node1 = head, node2 = head; + //Record the number of nodes + int count = 0; + //Record the k value, which will be used later + int index = k; + // The p pointer runs first and records the number of nodes. When the node1 node runs k-1 nodes, the node2 node starts running. + // When the node1 node reaches the end, the node pointed by the node2 node is the k-th node from the bottom + while (node1 != null) { + node1 = node1.next; + count++; + if (k < 1) { + node2 = node2.next; + } + k--; + } + // If the number of nodes is less than the required k-th node from the last, return empty + if (count < index) + return null; + return node2; + + } +}``` + +## 4. Delete the Nth node from the bottom of the linked list + +> Leetcode: Given a linked list, delete the nth node from the bottom of the linked list and return the head node of the linked list. + +**Example:** + +```plain +Given a linked list: 1->2->3->4->5, and n = 2. + +When the penultimate node is deleted, the linked list becomes 1->2->3->5. + +``` + +**Description:** + +The given n is guaranteed to be valid. + +**Advanced:** + +Could you try using a one-pass scan implementation? + +This question has a detailed answer on leetcode. For details, please refer to Leetcode. + +### Problem analysis + +We note that this problem can be easily reduced to another problem: delete the (L - n + 1)th node from the beginning of the list, where L is the length of the list. This problem is easy to solve as long as we find the length L of the list. + +![Figure 1. Delete the L - n + 1 element in the list](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/94354387.jpg) + +### Solution + +**Two traversal method** + +First we will add a **dumb node** as an auxiliary node at the head of the list. Dummy nodes are used to simplify some corner cases, such as the list containing only one node, or the head of the list needing to be deleted. In the first pass, we find the length L of the list. Then set a pointer to the dummy node and move it through the list until it reaches the (L - n)th node. **We relink the next pointer of the (L - n)th node to the (L - n + 2)th node to complete this algorithm. ** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +// https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/ +public class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + // Dumb nodes are used to simplify some extreme situations, such as the list containing only one node, or the head of the list that needs to be deleted. + ListNode dummy = new ListNode(0); + // The dumb node points to the head node + dummy.next = head; + //Save the length of the linked list + int length = 0; + ListNode len = head; + while (len != null) { + length++; + len = len.next; + } + length = length - n; + ListNode target = dummy; + // Find the node at position L-n + while (length > 0) { + target = target.next; + length--; + } + //Relink the next pointer of the (L - n)th node to the (L - n + 2)th node + target.next = target.next.next; + return dummy.next; + } +} +``` + +**Advanced - One-time traversal method:** + +> The penultimate N node in the linked list is also the positive (L - n + 1) node. + +In fact, this method is the same as the idea used in our fourth question above to find "the k-th node from the last in the linked list". **The basic idea is:** Define two nodes node1 and node2; the node1 node runs first, and when the node1 node reaches the n+1th node, the node2 node starts to run. When the node1 node runs to the last node, the location of the node2 node is the (L-n)th node (L represents the total linked list length, which is the n+1th node from the bottom) + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +public class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + + ListNode dummy = new ListNode(0); + dummy.next = head; + // Declare two nodes pointing to the head node + ListNode node1 = dummy, node2 = dummy; + + // The node1 node runs first. When the node1 node reaches the nth node, the node2 node starts running. + // When the node1 node reaches the last node, the location of the node2 node is the (L-n)th node, which is the n+1th from the bottom (L represents the total linked list length) + while (node1 != null) { + node1 = node1.next; + if (n < 1 && node1 != null) { + node2 = node2.next; + } + n--; + } + + node2.next = node2.next.next; + + return dummy.next; + + } +} +``` + +## 5. Merge two sorted linked lists + +### Title description + +> Sword finger offer: Input two monotonically increasing linked lists, and output the combined linked list of the two linked lists. Of course, we need the combined linked list to satisfy the monotonically non-decreasing rule. + +### Problem analysis + +We can analyze it like this: + +1. Suppose we have two linked lists A and B; +2. Compare the value of A's head node A1 with the value of B's head node B1. If A1 is small, then A1 is the head node; +3. A2 is compared with B1. Assuming B1 is small, then A1 points to B1; +4. A2 is compared with B2 + Just keep repeating it like this, it should be fairly easy to understand. + +Consider implementing it recursively! + +### Solution + +**Recursive version:** + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ +//https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking +public class Solution { + public ListNode Merge(ListNode list1, ListNode list2) { + if (list1 == null) { + return list2; + } + if (list2 == null) { + return list1; + } + if (list1.val <= list2.val) { + list1.next = Merge(list1.next, list2); + return list1; + } else { + list2.next = Merge(list1, list2.next); + return list2; + } + } +} +``` + + \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/string-algorithm-problems.en.md b/docs_en/cs-basics/algorithms/string-algorithm-problems.en.md new file mode 100644 index 00000000000..fa4ba4f930e --- /dev/null +++ b/docs_en/cs-basics/algorithms/string-algorithm-problems.en.md @@ -0,0 +1,468 @@ +--- +title: Several common string algorithm questions +category: Computer Basics +tag: + - Algorithm +head: + - - meta + - name: keywords + content: String algorithm, KMP, BM, sliding window, substring, matching, complexity + - - meta + - name: description + content: Summarizes string high-frequency algorithms and question types, focusing on KMP/BM principles, sliding windows and other techniques to help efficient matching and implementation. +--- + +> Author: wwwxmu +> +> Original address: + +## 1. KMP algorithm + +When talking about string problems, we have to mention the KMP algorithm, which is used to solve the problem of string search. It can find the position where a substring (W) appears in a string (S). The KMP algorithm reduces the time complexity of character matching to O(m+n), and the space complexity is only O(m). Because the "brute force search" method will repeatedly backtrack the main string, resulting in low efficiency, the KMP algorithm can use the effective information of partial matching to keep the pointer on the main string from backtracking. By modifying the pointer of the substring, the pattern string can be moved to a valid position as much as possible. + +For specific algorithm details, please refer to: + +- [Understand KMP thoroughly from beginning to end:](https://blog.csdn.net/v_july_v/article/details/7041827) +- [How to better understand and master the KMP algorithm?](https://www.zhihu.com/question/21923021) +- [Detailed analysis of KMP algorithm](https://blog.sengxian.com/algorithms/kmp) +- [Illustration of KMP algorithm](http://blog.jobbole.com/76611/) +- [KMP string matching algorithm that everyone can understand [bilingual subtitles]](https://www.bilibili.com/video/av3246487/?from=search&seid=17173603269940723925) +- [KMP String Matching Algorithm 1](https://www.bilibili.com/video/av11866460?from=search&seid=12730654434238709250) + +**In addition, let’s learn more about the BM algorithm! ** + +> The BM algorithm is also an exact string matching algorithm. It uses a right-to-left comparison method and applies two heuristic rules, namely the bad character rule and the good suffix rule, to determine the distance to jump to the right. The basic idea is to match characters from right to left. When encountering unmatched characters, find the largest right shift value from the bad character table and good suffix table, and shift the pattern string to the right to continue matching. +> "KMP Algorithm for String Matching": + +## 2. Replace spaces + +> Sword-pointing offer: Please implement a function to replace each space in a string with "%20". For example, when the string is We Are Happy., the replaced string is We%20Are%20Happy. + +Here I provide two methods: ① conventional method; ② use API to solve it. + +```java +//https://www.weiweiblog.cn/replacespace/ +public class Solution { + + /** + * The first method: conventional method. Use String.charAt(i) and String.valueOf(char).equals(" " + *) Traverse the string and determine whether the element is a space. If yes, replace it with "%20", otherwise do not replace it. + */ + public static String replaceSpace(StringBuffer str) { + + int length = str.length(); + // System.out.println("length=" + length); + StringBuffer result = new StringBuffer(); + for (int i = 0; i < length; i++) { + char b = str.charAt(i); + if (String.valueOf(b).equals(" ")) { + result.append("%20"); + } else { + result.append(b); + } + } + return result.toString(); + + } + + /** + * The second method: Use API to replace all spaces and solve the problem with one line of code + */ + public static String replaceSpace2(StringBuffer str) { + + return str.toString().replaceAll("\\s", "%20"); + } +} + +``` + +For replacing fixed characters (such as spaces), the second method can actually use the `replace` method to replace, which has better performance! + +```java +str.toString().replace(" ","%20"); +``` + +## 3. Longest common prefix + +> Leetcode: Write a function to find the longest common prefix in an array of strings. If no common prefix exists, the empty string "" is returned. + +Example 1: + +```plain +Input: ["flower","flow","flight"] +Output: "fl" +``` + +Example 2: + +```plain +Input: ["dog","racecar","car"] +Output: "" +Explanation: No common prefix exists for the input. +``` + +The idea is very simple! First use Arrays.sort(strs) to sort the array, and then compare the characters of the first element and the last element of the array from front to back! + +```java +public class Main { + public static String replaceSpace(String[] strs) { + + //If the check value is illegal and returns an empty string + if (!checkStrs(strs)) { + return ""; + } + //array length + int len = strs.length; + // Used to save results + StringBuilder res = new StringBuilder(); + // Sort the elements of the string array in ascending order (if it contains numbers, the numbers will be sorted first) + Arrays.sort(strs); + int m = strs[0].length(); + int n = strs[len - 1].length(); + int num = Math.min(m, n); + for (int i = 0; i < num; i++) { + if (strs[0].charAt(i) == strs[len - 1].charAt(i)) { + res.append(strs[0].charAt(i)); + } else + break; + + } + return res.toString(); + + } + + private static boolean checkStrs(String[] strs) { + boolean flag = false; + if (strs != null) { + // Traverse strs to check element values + for (int i = 0; i < strs.length; i++) { + if (strs[i] != null && strs[i].length() != 0) { + flag = true; + } else { + flag = false; + break; + } + } + } + return flag; + } + + // test + public static void main(String[] args) { + String[] strs = { "customer", "car", "cat" }; + // String[] strs = { "customer", "car", null }; // Empty string + // String[] strs = {}; // empty string + // String[] strs = null; // empty string + System.out.println(Main.replaceSpace(strs));// c + } +} + +``` + +## 4. Palindrome string + +### 4.1. The longest palindrome string + +> LeetCode: Given a string containing uppercase and lowercase letters, find the longest palindrome string constructed from these letters. During construction, be aware of case sensitivity. For example, `"Aa"` cannot be treated as a palindrome string. Note +> Meaning: Assume that the length of the string will not exceed 1010. +> +> Palindrome string: "Palindrome string" is a string that has the same forward and backward reading, such as "level" or "noon", etc. It is a palindrome string. ——Baidu Encyclopedia Address: + +Example 1: + +```plain +Input: +"abccccdd" + +Output: +7 + +Explanation: +The longest palindrome we can construct is "dccaccd", which has a length of 7.``` + +We already know above what is a palindrome string? Now let's consider two situations in which a palindrome can be formed: + +- A combination of characters appearing an even number of times +- **A combination of characters with an even number of occurrences + the character with the most odd number of occurrences in a single character** (see **[issue665](https://github.com/Snailclimb/JavaGuide/issues/665)** ) + +Just count the number of times characters appear, and even numbers can constitute a palindrome. Because the middle number is allowed to appear alone, such as "abcba", if there is a single letter at the end, the total length can be increased by 1. First convert the string into a character array. Then iterate through the array to determine whether the corresponding character is in the hashset. If it is not there, add it. If it is there, count++, and then remove the character! This way you can find the number of characters with an even number of occurrences. + +```java +//https://leetcode-cn.com/problems/longest-palindrome/description/ +class Solution { + public int longestPalindrome(String s) { + if (s.length() == 0) + return 0; + // used to store characters + HashSet hashset = new HashSet(); + char[] chars = s.toCharArray(); + int count = 0; + for (int i = 0; i < chars.length; i++) { + if (!hashset.contains(chars[i])) {// If the hashset does not have the character, save it + hashset.add(chars[i]); + } else {// If there is, let count++ (indicating that a paired character is found), and then remove the character + hashset.remove(chars[i]); + count++; + } + } + return hashset.isEmpty() ? count * 2 : count * 2 + 1; + } +} +``` + +### 4.2. Verify palindrome string + +> LeetCode: Given a string, verify whether it is a palindrome string. Only alphabetic and numeric characters are considered, and the case of letters can be ignored. Note: In this question, we define the empty string as a valid palindrome string. + +Example 1: + +```plain +Input: "A man, a plan, a canal: Panama" +Output: true +``` + +Example 2: + +```plain +Input: "race a car" +Output: false +``` + +```java +//https://leetcode-cn.com/problems/valid-palindrome/description/ +class Solution { + public boolean isPalindrome(String s) { + if (s.length() == 0) + return true; + int l = 0, r = s.length() - 1; + while (l < r) { + //Traverse from the beginning and the end to the middle + if (!Character.isLetterOrDigit(s.charAt(l))) {// Characters are not letters and numbers + l++; + } else if (!Character.isLetterOrDigit(s.charAt(r))) {// Characters are not letters and numbers + r--; + } else { + // Determine whether the two are equal + if (Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r))) + return false; + l++; + r--; + } + } + return true; + } +} +``` + +### 4.3. The longest palindrome substring + +> Leetcode: LeetCode: Longest Palindrome Substring Given a string s, find the longest palindrome substring in s. You can assume that the maximum length of s is 1000. + +Example 1: + +```plain +Input: "babad" +Output: "bab" +Note: "aba" is also a valid answer. +``` + +Example 2: + +```plain +Input: "cbbd" +Output: "bb" +``` + +Taking a certain element as the center, calculate the maximum length of the even-length palindrome and the maximum length of the odd-length palindrome respectively. + +```java +//https://leetcode-cn.com/problems/longest-palindromic-substring/description/ +class Solution { + private int index, len; + + public String longestPalindrome(String s) { + if (s.length() < 2) + return s; + for (int i = 0; i < s.length() - 1; i++) { + PalindromeHelper(s, i, i); + PalindromeHelper(s, i, i + 1); + } + return s.substring(index, index + len); + } + + public void PalindromeHelper(String s, int l, int r) { + while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { + l--; + r++; + } + if (len < r - l - 1) { + index = l + 1; + len = r - l - 1; + } + } +} +``` + +### 4.4. The longest palindrome subsequence + +> LeetCode: longest palindrome subsequence +> Given a string s, find the longest palindrome subsequence in it. We can assume that the maximum length of s is 1000. +> **The difference between the longest palindrome subsequence and the longest palindrome substring of the previous question is that a substring is a continuous sequence in a string, while a subsequence is a character sequence that maintains a relative position in the string. For example, "bbbb" can be a subsequence of the string "bbbab" but not a substring. ** + +Given a string s, find the longest palindrome subsequence in it. We can assume that the maximum length of s is 1000. + +Example 1: + +```plain +Input: +"bbbab" +Output: +4 +``` + +The longest possible palindromic subsequence is "bbbb". + +Example 2: + +```plain +Input: +"cbbd" +Output: +2 +``` + +The longest possible palindromic subsequence is "bb". + +**Dynamic programming:** `dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])` + +```java +class Solution { + public int longestPalindromeSubseq(String s) { + int len = s.length(); + int [][] dp = new int[len][len]; + for(int i = len - 1; i>=0; i--){ + dp[i][i] = 1; + for(int j = i+1; j < len; j++){ + if(s.charAt(i) == s.charAt(j)) + dp[i][j] = dp[i+1][j-1] + 2; + else + dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]); + } + } + return dp[0][len-1]; + } +} +``` + +## 5. Bracket matching depth + +> iQIYI 2018 Autumn Recruitment Java: +> A valid bracket matching sequence is defined as follows: +> +> 1. The empty string "" is a legal bracket matching sequence +> 2. If "X" and "Y" are both legal bracket matching sequences, "XY" is also a legal bracket matching sequence. +> 3. If "X" is a legal bracket matching sequence, then "(X)" is also a legal bracket matching sequence +> 4. Every legal bracket sequence can be generated by the above rules. +> +> For example: "","()","()()","((()))" are all legal bracket sequences +> For a legal bracket sequence we have the following definition of its depth: +> +> 1. The depth of the empty string "" is 0 +> 2. If the depth of string "X" is x and the depth of string "Y" is y, then the depth of string "XY" is max(x,y)> 3. If the depth of "X" is x, then the depth of the string "(X)" is x+1 +> +> For example: "()()()" has a depth of 1, "((()))" has a depth of 3. Niuniu now gives you a legal bracket sequence, and you need to calculate its depth. + +```plain +Enter description: +The input includes a legal bracket sequence s, the length of s is length (2 ≤ length ≤ 50), and the sequence only contains '(' and ')'. + +Output description: +Output a positive integer, which is the depth of this sequence. +``` + +Example: + +```plain +Input: +(()) +Output: +2 +``` + +The code is as follows: + +```java +import java.util.Scanner; + +/** + * https://www.nowcoder.com/test/8246651/summary + * + * @author Snailclimb + * @date September 6, 2018 + * @Description: TODO Find the depth of a given legal bracket sequence + */ +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.nextLine(); + int cnt = 0, max = 0, i; + for (i = 0; i < s.length(); ++i) { + if (s.charAt(i) == '(') + cnt++; + else + cnt--; + max = Math.max(max, cnt); + } + sc.close(); + System.out.println(max); + } +} + +``` + +## 6. Convert string to integer + +> Jianzhi offer: Convert a string into an integer (implement the function of Integer.valueOf(string), but return 0 when the string does not meet the numerical requirements). It is required that the library function for converting a string into an integer cannot be used. If the value is 0 or the string is not a legal value, 0 is returned. + +```java +//https://www.weiweiblog.cn/strtoint/ +public class Main { + + public static int StrToInt(String str) { + if (str.length() == 0) + return 0; + char[] chars = str.toCharArray(); + // Determine whether there is a sign bit + int flag = 0; + if (chars[0] == '+') + flag = 1; + else if (chars[0] == '-') + flag = 2; + int start = flag > 0 ? 1 : 0; + int res = 0;//Save the result + for (int i = start; i < chars.length; i++) { + if (Character.isDigit(chars[i])) {// Call the Character.isDigit(char) method to determine whether it is a number, and return True if it is, otherwise False + int temp = chars[i] - '0'; + res = res * 10 + temp; + } else { + return 0; + } + } + return flag != 2 ? res : -res; + + } + + public static void main(String[] args) { + // TODO Auto-generated method stub + String s = "-12312312"; + System.out.println("Use library function to convert: " + Integer.valueOf(s)); + int res = Main.StrToInt(s); + System.out.println("Use your own method to convert: " + res); + + } + +} + +``` + + \ No newline at end of file diff --git a/docs_en/cs-basics/algorithms/the-sword-refers-to-offer.en.md b/docs_en/cs-basics/algorithms/the-sword-refers-to-offer.en.md new file mode 100644 index 00000000000..8815b2146da --- /dev/null +++ b/docs_en/cs-basics/algorithms/the-sword-refers-to-offer.en.md @@ -0,0 +1,686 @@ +--- +title: 剑指offer部分编程题 +category: 计算机基础 +tag: + - 算法 +head: + - - meta + - name: keywords + content: 剑指Offer,斐波那契,递归,迭代,链表,数组,面试题 + - - meta + - name: description + content: 选编《剑指 Offer》常见编程题,给出递归与迭代等多种思路与示例,实现对高频题型的高效复盘。 +--- + +## 斐波那契数列 + +**题目描述:** + +大家都知道斐波那契数列,现在要求输入一个整数 n,请你输出斐波那契数列的第 n 项。 +n<=39 + +**问题分析:** + +可以肯定的是这一题通过递归的方式是肯定能做出来,但是这样会有一个很大的问题,那就是递归大量的重复计算会导致内存溢出。另外可以使用迭代法,用 fn1 和 fn2 保存计算过程中的结果,并复用起来。下面我会把两个方法示例代码都给出来并给出两个方法的运行时间对比。 + +**示例代码:** + +采用迭代法: + +```java +int Fibonacci(int number) { + if (number <= 0) { + return 0; + } + if (number == 1 || number == 2) { + return 1; + } + int first = 1, second = 1, third = 0; + for (int i = 3; i <= number; i++) { + third = first + second; + first = second; + second = third; + } + return third; +} +``` + +采用递归: + +```java +public int Fibonacci(int n) { + if (n <= 0) { + return 0; + } + if (n == 1||n==2) { + return 1; + } + + return Fibonacci(n - 2) + Fibonacci(n - 1); +} +``` + +## 跳台阶问题 + +**题目描述:** + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +**问题分析:** + +正常分析法: + +> a.如果两种跳法,1 阶或者 2 阶,那么假定第一次跳的是一阶,那么剩下的是 n-1 个台阶,跳法是 f(n-1); +> b.假定第一次跳的是 2 阶,那么剩下的是 n-2 个台阶,跳法是 f(n-2) +> c.由 a,b 假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) +> d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2 + +找规律分析法: + +> f(1) = 1, f(2) = 2, f(3) = 3, f(4) = 5, 可以总结出 f(n) = f(n-1) + f(n-2)的规律。但是为什么会出现这样的规律呢?假设现在 6 个台阶,我们可以从第 5 跳一步到 6,这样的话有多少种方案跳到 5 就有多少种方案跳到 6,另外我们也可以从 4 跳两步跳到 6,跳到 4 有多少种方案的话,就有多少种方案跳到 6,其他的不能从 3 跳到 6 什么的啦,所以最后就是 f(6) = f(5) + f(4);这样子也很好理解变态跳台阶的问题了。 + +**所以这道题其实就是斐波那契数列的问题。** + +代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8……而上一题为 1 1 2 3 5 ……。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 + +**示例代码:** + +```java +int jumpFloor(int number) { + if (number <= 0) { + return 0; + } + if (number == 1) { + return 1; + } + if (number == 2) { + return 2; + } + int first = 1, second = 2, third = 0; + for (int i = 3; i <= number; i++) { + third = first + second; + first = second; + second = third; + } + return third; +} +``` + +## 变态跳台阶问题 + +**题目描述:** + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级……它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +**问题分析:** + +假设 n>=2,第一步有 n 种跳法:跳 1 级、跳 2 级、到跳 n 级 +跳 1 级,剩下 n-1 级,则剩下跳法是 f(n-1) +跳 2 级,剩下 n-2 级,则剩下跳法是 f(n-2) +…… +跳 n-1 级,剩下 1 级,则剩下跳法是 f(1) +跳 n 级,剩下 0 级,则剩下跳法是 f(0) +所以在 n>=2 的情况下: +f(n)=f(n-1)+f(n-2)+...+f(1) +因为 f(n-1)=f(n-2)+f(n-3)+...+f(1) +所以 f(n)=2\*f(n-1) 又 f(1)=1,所以可得**f(n)=2^(number-1)** + +**示例代码:** + +```java +int JumpFloorII(int number) { + return 1 << --number;//2^(number-1)用位移操作进行,更快 +} +``` + +**补充:** + +java 中有三种移位运算符: + +1. “<<” : **左移运算符**,等同于乘 2 的 n 次方 +2. “>>”: **右移运算符**,等同于除 2 的 n 次方 +3. “>>>” : **无符号右移运算符**,不管移动前最高位是 0 还是 1,右移后左侧产生的空位部分都以 0 来填充。与>>类似。 + +```java +int a = 16; +int b = a << 2;//左移2,等同于16 * 2的2次方,也就是16 * 4 +int c = a >> 2;//右移2,等同于16 / 2的2次方,也就是16 / 4 +``` + +## 二维数组查找 + +**题目描述:** + +在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 + +**问题解析:** + +这一道题还是比较简单的,我们需要考虑的是如何做,效率最快。这里有一种很好理解的思路: + +> 矩阵是有序的,从左下角来看,向上数字递减,向右数字递增, +> 因此从左下角开始查找,当要查找数字比左下角数字大时。右移 +> 要查找数字比左下角数字小时,上移。这样找的速度最快。 + +**示例代码:** + +```java +public boolean Find(int target, int [][] array) { + //基本思路从左下角开始找,这样速度最快 + int row = array.length-1;//行 + int column = 0;//列 + //当行数大于0,当前列数小于总列数时循环条件成立 + while((row >= 0)&& (column< array[0].length)){ + if(array[row][column] > target){ + row--; + }else if(array[row][column] < target){ + column++; + }else{ + return true; + } + } + return false; +} +``` + +## 替换空格 + +**题目描述:** + +请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy.则经过替换之后的字符串为 We%20Are%20Happy。 + +**问题分析:** + +这道题不难,我们可以通过循环判断字符串的字符是否为空格,是的话就利用 append()方法添加追加“%20”,否则还是追加原字符。 + +或者最简单的方法就是利用:replaceAll(String regex,String replacement)方法了,一行代码就可以解决。 + +**示例代码:** + +常规做法: + +```java +public String replaceSpace(StringBuffer str) { + StringBuffer out = new StringBuffer(); + for (int i = 0; i < str.toString().length(); i++) { + char b = str.charAt(i); + if(String.valueOf(b).equals(" ")){ + out.append("%20"); + }else{ + out.append(b); + } + } + return out.toString(); +} +``` + +One line of code to solve: + +```java +public String replaceSpace(StringBuffer str) { + //return str.toString().replaceAll(" ", "%20"); + //public String replaceAll(String regex,String replacement) + //Replace every substring of this string that matches the given regular expression with the given replacement. + //\ escape character. If you want to use "\" itself, you should use "\\". Spaces in the String type are represented by "\s", so I guess "\\s" here means spaces. + return str.toString().replaceAll("\\s", "%20"); +} +``` + +## Integer power of the value + +**Title description:** + +Given a floating-point number base of type double and an integer exponent of type int. Find base raised to the exponent power. + +**Problem analysis:** + +This question is a bit more troublesome and difficult. What I use here is the idea of ​​**half power**, of course, you can also use **fast power**. +More details in the offer book, the solution to this problem is as follows: 1. When the base is 0 and the exponent <0, the reciprocal of 0 will occur, and error handling needs to be performed, setting a global variable; 2. Determine whether the base is equal to 0, because base is a double type, so it cannot be judged directly with == 3. Optimize the exponentiation function (power of two). +When n is an even number, a^n = (a^n/2)_(a^n/2); +When n is an odd number, a^n = a^[(n-1)/2]_ a^[(n-1)/2] \* a. Time complexity O(logn) + +**Time complexity**: O(logn) + +**Sample code:** + +```java +public class Solution { + boolean invalidInput=false; + public double Power(double base, int exponent) { + //If the base is equal to 0 and the exponent is less than 0 + //Since base is of double type, it cannot be judged directly using == + if(equal(base,0.0)&&exponent<0){ + invalidInput=true; + return 0.0; + } + int absexponent=exponent; + //If the index is less than 0, turn the index positive + if(exponent<0) + absexponent=-exponent; + //The getPower method finds the exponent power of base. + double res=getPower(base,absexponent); + //If the index is less than 0, the result obtained is the reciprocal of the result obtained above. + if(exponent<0) + res=1.0/res; + return res; + } + //Method to compare whether two double variables are equal + boolean equal(double num1,double num2){ + if(num1-num2>-0.000001&&num1-num2<0.000001) + return true; + else + return false; + } + //Method to find b raised to the power of e + double getPower(double b,int e){ + //If the index is 0, return 1 + if(e==0) + return 1.0; + //If the index is 1, return b + if(e==1) + return b; + //e>>1 is equal to e/2, here is to find a^n = (a^n/2)*(a^n/2) + double result=getPower(b,e>>1); + result*=result; + //If the index n is an odd number, multiply the base again + if((e&1)==1) + result*=b; + return result; + } +} +``` + +Of course, this question can also be solved using a stupid method: cumulative multiplication. However, the time complexity of this method is O(n), which is not as efficient as the previous method. + +```java +// Use cumulative multiplication +public double powerAnother(double base, int exponent) { + double result = 1.0; + for (int i = 0; i < Math.abs(exponent); i++) { + result *= base; + } + if (exponent >= 0) + return result; + else + return 1/result; +} +``` + +## Adjust the order of the array so that odd numbers are in front of even numbers + +**Title description:** + +Input an array of integers and implement a function to adjust the order of the numbers in the array so that all odd numbers are located in the first half of the array and all even numbers are located in the second half of the array, while ensuring that the relative positions between odd numbers and odd numbers and even numbers and even numbers remain unchanged. + +**Problem analysis:** + +There are many ways to solve this question. I would like to introduce a method that I think is easy to understand: +We first count the number of odd numbers, assuming it is n, then create a new equal-length array, and then use a loop to determine whether the elements in the original array are even or odd. If it is, add the odd number to the new array starting from the element with array index 0; if it is an even number, add the even number to the new array starting from the element with array index n. + +**Sample code:** + +An algorithm with time complexity O(n) and space complexity O(n) + +```java +public class Solution { + public void reOrderArray(int [] array) { + //If the array length is equal to 0 or equal to 1, do nothing and return directly + if(array.length==0||array.length==1) + return; + //oddCount: save odd numbers + //oddBegin: odd numbers are added from the head of the array + int oddCount=0,oddBegin=0; + //Create a new array + int[] newArray=new int[array.length]; + //Calculate (the odd number in the array) and start adding elements + for(int i=0;i stack1 = new Stack(); + Stack stack2 = new Stack(); + + //When performing a push operation, add elements to stack1 + public void push(int node) { + stack1.push(node); + } + + public int pop() { + //If both queues are empty, an exception is thrown, indicating that the user has not pushed any elements. + if(stack1.empty()&&stack2.empty()){ + throw new RuntimeException("Queue is empty!"); + } + //If stack2 is not empty, perform pop operation directly on stack2. + if(stack2.empty()){ + while(!stack1.empty()){ + //Push the elements of stack1 into stack2 according to last in first out + stack2.push(stack1.pop()); + } + } + return stack2.pop(); + } +} +``` + +## Stack push and pop sequence + +**Title description:** + +Input two integer sequences. The first sequence represents the push sequence of the stack. Please determine whether the second sequence is the pop sequence of the stack. Assume that all numbers pushed onto the stack are not equal. For example, the sequence 1,2,3,4,5 is the push sequence of a certain stack, and the sequence 4,5,3,2,1 is a pop sequence corresponding to the push sequence, but 4,3,5,1,2 cannot be the pop sequence of the push sequence. (Note: the lengths of these two sequences are equal) + +**Question Analysis:** + +I have been thinking about this question for a long time and I have no idea. I refer to [Alias's answer](https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106). His ideas are also very detailed and should be easy to understand. + +[Idea] Borrow an auxiliary stack to traverse the stack pushing sequence. First, put the first element on the stack, here is 1, and then determine whether the top element of the stack is the first element in the popping sequence, here is 4. Obviously 1≠4, so we continue to push the stack until they are equal and start popping. If one element is popped from the stack, the popping sequence will be moved backward by one until they are not equal. In this way, the loop waits for the stack sequence traversal to be completed. If the auxiliary stack is not empty yet, it means that the popping sequence is not the popping sequence of the stack. + +Example: + +Push 1,2,3,4,5 + +Pop 4,5,3,2,1 + +First, 1 is pushed into the auxiliary stack. At this time, the top of the stack is 1≠4. Continue to push 2 into the stack. + +At this time, the top of the stack is 2≠4, continue to push 3 to the stack + +At this time, the top of the stack is 3≠4, continue to push 4 to the stack + +At this time, the top of the stack is 4 = 4, pop 4 from the stack, and the pop sequence is one bit backward, which is 5 at this time, and the auxiliary stack is 1, 2, 3 + +At this time, the top of the stack is 3≠5, continue to push 5 to the stack + +At this time, the top of the stack is 5=5, pop 5 from the stack, and the pop sequence is one bit backward, which is 3 at this time, and the auxiliary stack is 1, 2, 3 + +…. +Execute in sequence, and finally the auxiliary stack is empty. If it is not empty, the pop sequence is not the pop sequence of the stack. + +**Inspection content:** + +stack + +**Sample code:** + +```java +import java.util.ArrayList; +import java.util.Stack; +//I didn't think of this question, so I referred to Alias's answer: https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 +public class Solution { + public boolean IsPopOrder(int [] pushA,int [] popA) { + if(pushA.length == 0 || popA.length == 0) + return false; + Stack s = new Stack(); + //Used to identify the position of the pop-up sequence + int popIndex = 0; + for(int i = 0; i< pushA.length;i++){ + s.push(pushA[i]); + //If the stack is not empty and the top element of the stack is equal to the pop sequence + while(!s.empty() &&s.peek() == popA[popIndex]){ + //pop + s.pop(); + //The popup sequence goes backward one bit + popIndex++; + } + } + return s.empty(); + } +} +``` + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/bloom-filter.en.md b/docs_en/cs-basics/data-structure/bloom-filter.en.md new file mode 100644 index 00000000000..155bbe0de55 --- /dev/null +++ b/docs_en/cs-basics/data-structure/bloom-filter.en.md @@ -0,0 +1,319 @@ +--- +title: 布隆过滤器 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 布隆过滤器,Bloom Filter,误判率,哈希函数,位数组,去重,缓存穿透 + - - meta + - name: description + content: 解析 Bloom Filter 的原理与误判特性,结合哈希与位数组实现,适用于海量数据去重与缓存穿透防护。 +--- + +布隆过滤器相信大家没用过的话,也已经听过了。 + +布隆过滤器主要是为了解决海量数据的存在性问题。对于海量数据中判定某个数据是否存在且容忍轻微误差这一场景(比如缓存穿透、海量数据去重)来说,非常适合。 + +文章内容概览: + +1. 什么是布隆过滤器? +2. 布隆过滤器的原理介绍。 +3. 布隆过滤器使用场景。 +4. 通过 Java 编程手动实现布隆过滤器。 +5. 利用 Google 开源的 Guava 中自带的布隆过滤器。 +6. Redis 中的布隆过滤器。 + +## 什么是布隆过滤器? + +首先,我们需要了解布隆过滤器的概念。 + +布隆过滤器(Bloom Filter,BF)是一个叫做 Bloom 的老哥于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 + +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) + +总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。** + +## 布隆过滤器的原理介绍 + +**当一个元素加入布隆过滤器中的时候,会进行如下操作:** + +1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 +2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 + +**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:** + +1. 对给定元素再次进行相同的哈希计算; +2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +Bloom Filter 的简单原理图如下: + +![Bloom Filter 的简单原理示意图](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-simple-schematic-diagram.png) + +如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 + +如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +**不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。** + +综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** + +## 布隆过滤器使用场景 + +1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。 +2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。 + +去重场景也需要用到判断给定数据是否存在,因此布隆过滤器主要是为了解决海量数据的存在性问题。 + +## 编码实战 + +### 通过 Java 编程手动实现布隆过滤器 + +我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。 + +如果你想要手动实现一个的话,你需要: + +1. 一个合适大小的位数组保存数据 +2. 几个不同的哈希函数 +3. 添加元素到位数组(布隆过滤器)的方法实现 +4. 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。 + +下面给出一个我觉得写的还算不错的代码(参考网上已有代码改进得到,对于所有类型对象皆适用): + +```java +import java.util.BitSet; + +public class MyBloomFilter { + + /** + * 位数组的大小 + */ + private static final int DEFAULT_SIZE = 2 << 24; + /** + * 通过这个数组可以创建 6 个不同的哈希函数 + */ + private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; + + /** + * 位数组。数组中的元素只能是 0 或者 1 + */ + private BitSet bits = new BitSet(DEFAULT_SIZE); + + /** + * 存放包含 hash 函数的类的数组 + */ + private SimpleHash[] func = new SimpleHash[SEEDS.length]; + + /** + * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 + */ + public MyBloomFilter() { + // 初始化多个不同的 Hash 函数 + for (int i = 0; i < SEEDS.length; i++) { + func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); + } + } + + /** + * 添加元素到位数组 + */ + public void add(Object value) { + for (SimpleHash f : func) { + bits.set(f.hash(value), true); + } + } + + /** + * 判断指定元素是否存在于位数组 + */ + public boolean contains(Object value) { + boolean ret = true; + for (SimpleHash f : func) { + ret = ret && bits.get(f.hash(value)); + } + return ret; + } + + /** + * 静态内部类。用于 hash 操作! + */ + public static class SimpleHash { + + private int cap; + private int seed; + + public SimpleHash(int cap, int seed) { + this.cap = cap; + this.seed = seed; + } + + /** + * 计算 hash 值 + */ + public int hash(Object value) { + int h; + return (value == null) ? 0 : Math.abs((cap - 1) & seed * ((h = value.hashCode()) ^ (h >>> 16))); + } + + } +} +``` + +Test: + +```java +String value1 = "https://javaguide.cn/"; +String value2 = "https://github.com/Snailclimb"; +MyBloomFilter filter = new MyBloomFilter(); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +filter.add(value1); +filter.add(value2); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +``` + +Output: + +```plain +false +false +true +true +``` + +Test: + +```java +Integer value1 = 13423; +Integer value2 = 22131; +MyBloomFilter filter = new MyBloomFilter(); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +filter.add(value1); +filter.add(value2); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +``` + +Output: + +```java +false +false +true +true +``` + +### Use the Bloom filter that comes with Google’s open source Guava + +The purpose of implementing it myself is mainly to let myself understand the principle of Bloom filter. The implementation of Bloom filter in Guava is relatively authoritative, so in actual projects we do not need to manually implement a Bloom filter. + +First we need to introduce Guava dependencies into the project: + +```java + + com.google.guava + guava + 28.0-jre + +``` + +The actual usage is as follows: + +We create a Bloom filter that can store up to 1500 integers, and we can tolerate a false positive probability of 0.01 percent + +```java +//Create Bloom filter object +BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 1500, + 0.01); +// Determine whether the specified element exists +System.out.println(filter.mightContain(1)); +System.out.println(filter.mightContain(2)); +// Add elements to the bloom filter +filter.put(1); +filter.put(2); +System.out.println(filter.mightContain(1)); +System.out.println(filter.mightContain(2)); +``` + +In our example, when the `mightContain()` method returns _true_ we are 99% sure that the element is in the filter, and when the filter returns _false_ we are 100% sure that the element is not present in the filter. + +**The implementation of the Bloom filter provided by Guava is still very good (if you want to know more about it, you can take a look at its source code implementation), but it has a major flaw that it can only be used on a single machine (in addition, capacity expansion is not easy), and now the Internet is generally a distributed scenario. In order to solve this problem, we need to use the Bloom filter in Redis. ** + +## Bloom filter in Redis + +### Introduction + +Redis v4.0 has the Module (module/plug-in) function. Redis Modules allow Redis to use external modules to extend its functionality. Bloom filter is one of the modules. For details, please see Redis’ official introduction to Redis Modules: + +In addition, the official website recommends a RedisBloom module as the Redis Bloom filter, address: +Others include: + +- redis-lua-scaling-bloom-filter (lua script implementation): +- pyreBloom (fast Redis Bloom filter in Python): +-… + +RedisBloom provides client support in multiple languages, including: Python, Java, JavaScript, and PHP. + +### Install using Docker + +If we need to experience Bloom filters in Redis, it’s very simple, just do it through Docker! We directly searched **docker redis bloomfilter** on Google and found the answer we wanted in the first search result of excluding ads (this is my usual way to solve problems, please share it), the specific address: (the introduction is very detailed). + +**The specific operations are as follows:** + +```bash +➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest +➜ ~ docker exec -it redis-redisbloom bash +root@21396d02c252:/data# redis-cli +127.0.0.1:6379> +``` + +**Note: The current rebloom image has been abandoned, and it is officially recommended to use [redis-stack](https://hub.docker.com/r/redis/redis-stack)** + +### List of commonly used commands + +> Note: key: the name of the Bloom filter, item: the added element. + +1. `BF.ADD`: Adds elements to a Bloom filter, creating the filter if it does not already exist. Format: `BF.ADD {key} {item}`. +2. `BF.MADD`: Adds one or more elements to a "Bloom filter" and creates a filter that does not yet exist. This command operates in the same manner as `BF.ADD`, except that it allows multiple inputs and returns multiple values. Format: `BF.MADD {key} {item} [item ...]` . +3. `BF.EXISTS`: Determine whether the element exists in the Bloom filter. Format: `BF.EXISTS {key} {item}`. +4. `BF.MEXISTS`: Determine whether one or more elements exist in the Bloom filter in the format: `BF.MEXISTS {key} {item} [item ...]`. + +In addition, the `BF.RESERVE` command needs to be introduced separately: + +The format of this command is as follows: + +`BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]` . + +The following is a brief introduction to the specific meaning of each parameter: + +1. key: name of Bloom filter +2. error_rate: expected false positive rate. The value must be between 0 and 1. For example, for a desired false positive rate of 0.1% (1 in 1000), error_rate should be set to 0.001. The closer this number is to zero, the greater the memory consumption per item and the higher the CPU usage per operation. +3. capacity: the capacity of the filter. When the actual number of stored elements exceeds this value, performance will begin to decrease. Actual downgrade will depend on how far the limit is exceeded. As the number of filter elements grows exponentially, performance decreases linearly. + +Optional parameters: + +- expansion: If a new subfilter is created, its size will be the size of the current filter multiplied by `expansion`. The default extension value is 2. This means that each subsequent sub-filter will be twice as large as the previous sub-filter. + +### Actual use + +```shell +127.0.0.1:6379> BF.ADD myFilter java +(integer) 1 +127.0.0.1:6379> BF.ADD myFilter javaguide +(integer) 1 +127.0.0.1:6379> BF.EXISTS myFilter java +(integer) 1 +127.0.0.1:6379> BF.EXISTS myFilter javaguide +(integer) 1 +127.0.0.1:6379> BF.EXISTS myFilter github +(integer) 0 +``` + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/graph.en.md b/docs_en/cs-basics/data-structure/graph.en.md new file mode 100644 index 00000000000..759f7966331 --- /dev/null +++ b/docs_en/cs-basics/data-structure/graph.en.md @@ -0,0 +1,167 @@ +--- +title: 图 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 图,邻接表,邻接矩阵,DFS,BFS,度,有向图,无向图,连通性 + - - meta + - name: description + content: 介绍图的基本概念与常用表示,结合 DFS/BFS 等核心算法与应用场景,掌握图论入门必备知识。 +--- + +图是一种较为复杂的非线性结构。 **为啥说其较为复杂呢?** + +根据前面的内容,我们知道: + +- 线性数据结构的元素满足唯一的线性关系,每个元素(除第一个和最后一个外)只有一个直接前趋和一个直接后继。 +- 树形数据结构的元素之间有着明显的层次关系。 + +但是,图形结构的元素之间的关系是任意的。 + +**何为图呢?** 简单来说,图就是由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为:**G(V,E)**,其中,G 表示一个图,V 表示顶点的集合,E 表示边的集合。 + +下图所展示的就是图这种数据结构,并且还是一张有向图。 + +![有向图](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/directed-graph.png) + +图在我们日常生活中的例子很多!比如我们在社交软件上好友关系就可以用图来表示。 + +## 图的基本概念 + +### 顶点 + +图中的数据元素,我们称之为顶点,图至少有一个顶点(非空有穷集合) + +对应到好友关系图,每一个用户就代表一个顶点。 + +### 边 + +顶点之间的关系用边表示。 + +对应到好友关系图,两个用户是好友的话,那两者之间就存在一条边。 + +### 度 + +度表示一个顶点包含多少条边,在有向图中,还分为出度和入度,出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。 + +对应到好友关系图,度就代表了某个人的好友数量。 + +### 无向图和有向图 + +边表示的是顶点之间的关系,有的关系是双向的,比如同学关系,A 是 B 的同学,那么 B 也肯定是 A 的同学,那么在表示 A 和 B 的关系时,就不用关注方向,用不带箭头的边表示,这样的图就是无向图。 + +有的关系是有方向的,比如父子关系,师生关系,微博的关注关系,A 是 B 的爸爸,但 B 肯定不是 A 的爸爸,A 关注 B,B 不一定关注 A。在这种情况下,我们就用带箭头的边表示二者的关系,这样的图就是有向图。 + +### 无权图和带权图 + +对于一个关系,如果我们只关心关系的有无,而不关心关系有多强,那么就可以用无权图表示二者的关系。 + +对于一个关系,如果我们既关心关系的有无,也关心关系的强度,比如描述地图上两个城市的关系,需要用到距离,那么就用带权图来表示,带权图中的每一条边一个数值表示权值,代表关系的强度。 + +下图就是一个带权有向图。 + +![带权有向图](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/weighted-directed-graph.png) + +## 图的存储 + +### 邻接矩阵存储 + +邻接矩阵将图用二维矩阵存储,是一种较为直观的表示方式。 + +如果第 i 个顶点和第 j 个顶点之间有关系,且关系权值为 n,则 `A[i][j]=n` 。 + +在无向图中,我们只关心关系的有无,所以当顶点 i 和顶点 j 有关系时,`A[i][j]`=1,当顶点 i 和顶点 j 没有关系时,`A[i][j]`=0。如下图所示: + +![无向图的邻接矩阵存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/adjacency-matrix-representation-of-undirected-graph.png) + +值得注意的是:**无向图的邻接矩阵是一个对称矩阵,因为在无向图中,顶点 i 和顶点 j 有关系,则顶点 j 和顶点 i 必有关系。** + +![有向图的邻接矩阵存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/adjacency-matrix-representation-of-directed-graph.png) + +邻接矩阵存储的方式优点是简单直接(直接使用一个二维数组即可),并且,在获取两个定点之间的关系的时候也非常高效(直接获取指定位置的数组元素的值即可)。但是,这种存储方式的缺点也比较明显,那就是比较浪费空间, + +### 邻接表存储 + +针对上面邻接矩阵比较浪费内存空间的问题,诞生了图的另外一种存储方法—**邻接表** 。 + +邻接链表使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点 Vi,把所有邻接于 Vi 的顶点 Vj 链成一个单链表,这个单链表称为顶点 Vi 的 **邻接表**。如下图所示: + +![无向图的邻接表存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/adjacency-list-representation-of-undirected-graph.png) + +![有向图的邻接表存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/adjacency-list-representation-of-directed-graph.png) + +大家可以数一数邻接表中所存储的元素的个数以及图中边的条数,你会发现: + +- 在无向图中,邻接表元素个数等于边的条数的两倍,如左图所示的无向图中,边的条数为 7,邻接表存储的元素个数为 14。 +- 在有向图中,邻接表元素个数等于边的条数,如右图所示的有向图中,边的条数为 8,邻接表存储的元素个数为 8。 + +## 图的搜索 + +### 广度优先搜索 + +广度优先搜索就像水面上的波纹一样一层一层向外扩展,如下图所示: + +![广度优先搜索图示](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search.png) + +**广度优先搜索的具体实现方式用到了之前所学过的线性数据结构——队列** 。具体过程如下图所示: + +**第 1 步:** + +![广度优先搜索1](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search1.png) + +**第 2 步:** + +![广度优先搜索2](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search2.png) + +**第 3 步:** + +![广度优先搜索3](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search3.png) + +**第 4 步:** + +![广度优先搜索4](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search4.png) + +**第 5 步:** + +![广度优先搜索5](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search5.png) + +**第 6 步:** + +![广度优先搜索6](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/breadth-first-search6.png) + +### 深度优先搜索 + +深度优先搜索就是“一条路走到黑”,从源顶点开始,一直走到没有后继节点,才回溯到上一顶点,然后继续“一条路走到黑”,如下图所示: + +![深度优先搜索图示](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search.png) + +**和广度优先搜索类似,深度优先搜索的具体实现用到了另一种线性数据结构——栈** 。具体过程如下图所示: + +**第 1 步:** + +![深度优先搜索1](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search1.png) + +**第 2 步:** + +![深度优先搜索2](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search2.png) + +**第 3 步:** + +![Depth-first search 3](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search3.png) + +**Step 4:** + +![Depth-first search 4](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search4.png) + +**Step 5:** + +![Depth-first search 5](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search5.png) + +**Step 6:** + +![Depth-first search 6](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search6.png) + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/heap.en.md b/docs_en/cs-basics/data-structure/heap.en.md new file mode 100644 index 00000000000..5f99c770d3e --- /dev/null +++ b/docs_en/cs-basics/data-structure/heap.en.md @@ -0,0 +1,209 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 堆,最大堆,最小堆,优先队列,堆化,上浮,下沉,堆排序 + - - meta + - name: description + content: 解析堆的性质与操作,理解优先队列实现与堆排序性能优势,掌握插入/删除的复杂度与实践场景。 +--- + +# 堆 + +## 什么是堆 + +堆是一种满足以下条件的树: + +堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。 + +> 大家可以把堆(最大堆)理解为一个公司,这个公司很公平,谁能力强谁就当老大,不存在弱的人当老大,老大手底下的人一定不会比他强。这样有助于理解后续堆的操作。 + +**!!!特别提示:** + +- 很多博客说堆是完全二叉树,其实并非如此,**堆不一定是完全二叉树**,只是为了方便存储和索引,我们通常用完全二叉树的形式来表示堆,事实上,广为人知的斐波那契堆和二项堆就不是完全二叉树,它们甚至都不是二叉树。 +- (**二叉**)堆是一个数组,它可以被看成是一个 **近似的完全二叉树**。——《算法导论》第三版 + +大家可以尝试判断下面给出的图是否是堆? + +![](./pictures/堆/堆1.png) + +第 1 个和第 2 个是堆。第 1 个是最大堆,每个节点都比子树中所有节点大。第 2 个是最小堆,每个节点都比子树中所有节点小。 + +第 3 个不是,第三个中,根结点 1 比 2 和 15 小,而 15 却比 3 大,19 比 5 大,不满足堆的性质。 + +## 堆的用途 + +当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。 + +有小伙伴可能会想到用有序数组,初始化一个有序数组时间复杂度是 `O(nlog(n))`,查找最大值或者最小值时间复杂度都是 `O(1)`,但是,涉及到更新(插入或删除)数据时,时间复杂度为 `O(n)`,即使是使用复杂度为 `O(log(n))` 的二分法找到要插入或者删除的数据,在移动数据时也需要 `O(n)` 的时间复杂度。 + +**相对于有序数组而言,堆的主要优势在于插入和删除数据效率较高。** 因为堆是基于完全二叉树实现的,所以在插入和删除数据时,只需要在二叉树中上下移动节点,时间复杂度为 `O(log(n))`,相比有序数组的 `O(n)`,效率更高。 + +不过,需要注意的是:Heap 初始化的时间复杂度为 `O(n)`,而非`O(nlogn)`。 + +## 堆的分类 + +堆分为 **最大堆** 和 **最小堆**。二者的区别在于节点的排序方式。 + +- **最大堆**:堆中的每一个节点的值都大于等于子树中所有节点的值 +- **最小堆**:堆中的每一个节点的值都小于等于子树中所有节点的值 + +如下图所示,图 1 是最大堆,图 2 是最小堆 + +![](./pictures/堆/堆2.png) + +## 堆的存储 + +之前介绍树的时候说过,由于完全二叉树的优秀性质,利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为 1,那么对于树中任意节点 i,其左子节点序号为 `2*i`,右子节点序号为 `2*i+1`)。 + +为了方便存储和索引,(二叉)堆可以用完全二叉树的形式进行存储。存储的方式如下图所示: + +![堆的存储](./pictures/堆/堆的存储.png) + +## 堆的操作 + +堆的更新操作主要包括两种 : **插入元素** 和 **删除堆顶元素**。操作过程需要着重掌握和理解。 + +> 在进入正题之前,再重申一遍,堆是一个公平的公司,有能力的人自然会走到与他能力所匹配的位置 + +### 插入元素 + +> 插入元素,作为一个新入职的员工,初来乍到,这个员工需要从基层做起 + +**1.将要插入的元素放到最后** + +![堆-插入元素-1](./pictures/堆/堆-插入元素1.png) + +> 有能力的人会逐渐升职加薪,是金子总会发光的!!! + +**2.从底向上,如果父结点比该元素小,则该节点和父结点交换,直到无法交换** + +![堆-插入元素2](./pictures/堆/堆-插入元素2.png) + +![堆-插入元素3](./pictures/堆/堆-插入元素3.png) + +### 删除堆顶元素 + +根据堆的性质可知,最大堆的堆顶元素为所有元素中最大的,最小堆的堆顶元素是所有元素中最小的。当我们需要多次查找最大元素或者最小元素的时候,可以利用堆来实现。 + +删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,我们将这个过程称之为"**堆化**",堆化的方法分为两种: + +- 一种是自底向上的堆化,上述的插入元素所使用的就是自底向上的堆化,元素从最底部向上移动。 +- 另一种是自顶向下堆化,元素由最顶部向下移动。在讲解删除堆顶元素的方法时,我将阐述这两种操作的过程,大家可以体会一下二者的不同。 + +#### 自底向上堆化 + +> 在堆这个公司中,会出现老大离职的现象,老大离职之后,他的位置就空出来了 + +首先删除堆顶元素,使得数组中下标为 1 的位置空出。 + +![删除堆顶元素1](./pictures/堆/删除堆顶元素1.png) + +> 那么他的位置由谁来接替呢,当然是他的直接下属了,谁能力强就让谁上呗 + +比较根结点的左子节点和右子节点,也就是下标为 2,3 的数组元素,将较大的元素填充到根结点(下标为 1)的位置。 + +![删除堆顶元素2](./pictures/堆/删除堆顶元素2.png) + +> 这个时候又空出一个位置了,老规矩,谁有能力谁上 + +一直循环比较空出位置的左右子节点,并将较大者移至空位,直到堆的最底部 + +![删除堆顶元素3](./pictures/堆/删除堆顶元素3.png) + +这个时候已经完成了自底向上的堆化,没有元素可以填补空缺了,但是,我们可以看到数组中出现了“气泡”,这会导致存储空间的浪费。接下来我们试试自顶向下堆化。 + +#### 自顶向下堆化 + +自顶向下的堆化用一个词形容就是“石沉大海”,那么第一件事情,就是把石头抬起来,从海面扔下去。这个石头就是堆的最后一个元素,我们将最后一个元素移动到堆顶。 + +![删除堆顶元素4](./pictures/堆/删除堆顶元素4.png) + +然后开始将这个石头沉入海底,不停与左右子节点的值进行比较,和较大的子节点交换位置,直到无法交换位置。 + +![删除堆顶元素5](./pictures/堆/删除堆顶元素5.png) + +![删除堆顶元素6](./pictures/堆/删除堆顶元素6.png) + +### 堆的操作总结 + +- **插入元素**:先将元素放至数组末尾,再自底向上堆化,将末尾元素上浮 +- **删除堆顶元素**:删除堆顶元素,将末尾元素放至堆顶,再自顶向下堆化,将堆顶元素下沉。也可以自底向上堆化,只是会产生“气泡”,浪费存储空间。最好采用自顶向下堆化的方式。 + +## 堆排序 + +堆排序的过程分为两步: + +- 第一步是建堆,将一个无序的数组建立为一个堆 +- 第二步是排序,将堆顶元素取出,然后对剩下的元素进行堆化,反复迭代,直到所有元素被取出为止。 + +### 建堆 + +如果你已经足够了解堆化的过程,那么建堆的过程掌握起来就比较容易了。建堆的过程就是一个对所有非叶节点的自顶向下堆化过程。 + +首先要了解哪些是非叶节点,最后一个节点的父结点及它之前的元素,都是非叶节点。也就是说,如果节点个数为 n,那么我们需要对 n/2 到 1 的节点进行自顶向下(沉底)堆化。 + +具体过程如下图: + +![建堆1](./pictures/堆/建堆1.png) + +将初始的无序数组抽象为一棵树,图中的节点个数为 6,所以 4,5,6 节点为叶节点,1,2,3 节点为非叶节点,所以要对 1-3 号节点进行自顶向下(沉底)堆化,注意,顺序是从后往前堆化,从 3 号节点开始,一直到 1 号节点。 +3 号节点堆化结果: + +![建堆1](./pictures/堆/建堆2.png) + +2 号节点堆化结果: + +![建堆1](./pictures/堆/建堆3.png) + +1 号节点堆化结果: + +![建堆1](./pictures/堆/建堆4.png) + +至此,数组所对应的树已经成为了一个最大堆,建堆完成! + +### 排序 + +由于堆顶元素是所有元素中最大的,所以我们重复取出堆顶元素,将这个最大的堆顶元素放至数组末尾,并对剩下的元素进行堆化即可。 + +现在思考两个问题: + +- 删除堆顶元素后需要执行自顶向下(沉底)堆化还是自底向上(上浮)堆化? +- 取出的堆顶元素存在哪,新建一个数组存? + +先回答第一个问题,我们需要执行自顶向下(沉底)堆化,这个堆化一开始要将末尾元素移动至堆顶,这个时候末尾的位置就空出来了,由于堆中元素已经减小,这个位置不会再被使用,所以我们可以将取出的元素放在末尾。 + +机智的小伙伴已经发现了,这其实是做了一次交换操作,将堆顶和末尾元素调换位置,从而将取出堆顶元素和堆化的第一步(将末尾元素放至根结点位置)进行合并。 + +详细过程如下图所示: + +取出第一个元素并堆化: + +![堆排序1](./pictures/堆/堆排序1.png) + +取出第二个元素并堆化: + +![堆排序2](./pictures/堆/堆排序2.png) + +取出第三个元素并堆化: + +![堆排序3](./pictures/堆/堆排序3.png) + +取出第四个元素并堆化: + +![HeapSort4](./pictures/Heap/HeapSort4.png) + +Take the fifth element and heap it: + +![HeapSort 5](./pictures/Heap/HeapSort 5.png) + +Take the sixth element and heap it: + +![HeapSort 6](./pictures/Heap/HeapSort 6.png) + +Heap sort completed! + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/linear-data-structure.en.md b/docs_en/cs-basics/data-structure/linear-data-structure.en.md new file mode 100644 index 00000000000..47a247665d0 --- /dev/null +++ b/docs_en/cs-basics/data-structure/linear-data-structure.en.md @@ -0,0 +1,342 @@ +--- +title: 线性数据结构 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 数组,链表,栈,队列,双端队列,复杂度分析,随机访问,插入删除 + - - meta + - name: description + content: 总结数组/链表/栈/队列的特性与操作,配合复杂度分析与典型应用,掌握线性结构的选型与实现。 +--- + +## 1. 数组 + +**数组(Array)** 是一种很常见的数据结构。它由相同类型的元素(element)组成,并且是使用一块连续的内存来存储。 + +我们直接可以利用元素的索引(index)可以计算出该元素对应的存储地址。 + +数组的特点是:**提供随机访问** 并且容量有限。 + +```java +假如数组的长度为 n。 +访问:O(1)//访问特定位置的元素 +插入:O(n )//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时 +删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时 +``` + +![数组](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/array.png) + +## 2. 链表 + +### 2.1. 链表简介 + +**链表(LinkedList)** 虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。 + +链表的插入和删除操作的复杂度为 O(1) ,只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。 + +使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点。 + +### 2.2. 链表分类 + +**常见链表分类:** + +1. 单链表 +2. 双向链表 +3. 循环链表 +4. 双向循环链表 + +```java +假如链表中有n个元素。 +访问:O(n)//访问特定位置的元素 +插入删除:O(1)//必须要要知道插入元素的位置 +``` + +#### 2.2.1. 单链表 + +**单链表** 单向链表只有一个方向,结点只有一个后继指针 next 指向后面的节点。因此,链表这种数据结构通常在物理内存上是不连续的。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向 null。 + +![单链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/single-linkedlist.png) + +#### 2.2.2. 循环链表 + +**循环链表** 其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向 null,而是指向链表的头结点。 + +![循环链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/circular-linkedlist.png) + +#### 2.2.3. 双向链表 + +**双向链表** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。 + +![双向链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) + +#### 2.2.4. 双向循环链表 + +**双向循环链表** 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。 + +![双向循环链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-circular-linkedlist.png) + +### 2.3. 应用场景 + +- 如果需要支持随机访问的话,链表没办法做到。 +- 如果需要存储的数据元素的个数不确定,并且需要经常添加和删除数据的话,使用链表比较合适。 +- 如果需要存储的数据元素的个数确定,并且不需要经常添加和删除数据的话,使用数组比较合适。 + +### 2.4. 数组 vs 链表 + +- 数组支持随机访问,而链表不支持。 +- 数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。 +- 数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的! + +## 3. 栈 + +### 3.1. 栈简介 + +**栈 (Stack)** 只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。** + +栈常用一维数组或链表来实现,用数组实现的栈叫作 **顺序栈** ,用链表实现的栈叫作 **链式栈** 。 + +```java +假设堆栈中有n个元素。 +访问:O(n)//最坏情况 +插入删除:O(1)//顶端插入和删除元素 +``` + +![栈](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88.png) + +### 3.2. 栈的常见应用场景 + +当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出(LIFO, Last In First Out)** 的特性时,我们就可以使用栈这个数据结构。 + +#### 3.2.1. 实现浏览器的回退和前进功能 + +我们只需要使用两个栈(Stack1 和 Stack2)和就能实现这个功能。比如你按顺序查看了 1,2,3,4 这四个页面,我们依次把 1,2,3,4 这四个页面压入 Stack1 中。当你想回头看 2 这个页面的时候,你点击回退按钮,我们依次把 4,3 这两个页面从 Stack1 弹出,然后压入 Stack2 中。假如你又想回到页面 3,你点击前进按钮,我们将 3 页面从 Stack2 弹出,然后压入到 Stack1 中。示例图如下: + +![栈实现浏览器倒退和前进](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88%E5%AE%9E%E7%8E%B0%E6%B5%8F%E8%A7%88%E5%99%A8%E5%80%92%E9%80%80%E5%92%8C%E5%89%8D%E8%BF%9B.png) + +#### 3.2.2. 检查符号是否成对出现 + +> 给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串,判断该字符串是否有效。 +> +> 有效字符串需满足: +> +> 1. 左括号必须用相同类型的右括号闭合。 +> 2. 左括号必须以正确的顺序闭合。 +> +> 比如 "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]"、"([)]" 则不是。 + +这个问题实际是 Leetcode 的一道题目,我们可以利用栈 `Stack` 来解决这个问题。 + +1. 首先我们将括号间的对应规则存放在 `Map` 中,这一点应该毋容置疑; +2. 创建一个栈。遍历字符串,如果字符是左括号就直接加入`stack`中,否则将`stack` 的栈顶元素与这个括号做比较,如果不相等就直接返回 false。遍历结束,如果`stack`为空,返回 `true`。 + +```java +public boolean isValid(String s){ + // 括号之间的对应规则 + HashMap mappings = new HashMap(); + mappings.put(')', '('); + mappings.put('}', '{'); + mappings.put(']', '['); + Stack stack = new Stack(); + char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (mappings.containsKey(chars[i])) { + char topElement = stack.empty() ? '#' : stack.pop(); + if (topElement != mappings.get(chars[i])) { + return false; + } + } else { + stack.push(chars[i]); + } + } + return stack.isEmpty(); +} +``` + +#### 3.2.3. Reverse string + +Just push each character in the string onto the stack and then pop it out. + +#### 3.2.4. Maintenance function calls + +The last called function must complete execution first, conforming to the stack's **Last In First Out (LIFO, Last In First Out)** characteristics. +For example, recursive function calls can be implemented through the stack. Each recursive call will push the parameters and return address onto the stack. + +#### 3.2.5 Depth-first traversal (DFS) + +During a depth-first search, the stack is used to save the search path for backtracking to the previous level. + +### 3.3. Stack implementation + +The stack can be implemented either as an array or as a linked list. Regardless of whether it is based on an array or a linked list, the time complexity of pushing and popping onto the stack is O(1). + +Below we use an array to implement a stack, and this stack has basic methods such as `push()`, `pop()` (return the top element of the stack and pop it off the stack), `peek()` (return the top element of the stack without popping it), `isEmpty()`, and `size()`. + +> Tip: Before each push to the stack, first determine whether the stack capacity is sufficient. If not, use `Arrays.copyOf()` to expand the capacity; + +```java +public class MyStack { + private int[] storage;//array to store elements in the stack + private int capacity;//Stack capacity + private int count;//The number of elements in the stack + private static final int GROW_FACTOR = 2; + + //Constructor without initial capacity. The default capacity is 8 + public MyStack() { + this.capacity = 8; + this.storage=new int[8]; + this.count = 0; + } + + //Construction method with initial capacity + public MyStack(int initialCapacity) { + if (initialCapacity < 1) + throw new IllegalArgumentException("Capacity too small."); + + this.capacity = initialCapacity; + this.storage = new int[initialCapacity]; + this.count = 0; + } + + //Push to stack + public void push(int value) { + if (count == capacity) { + ensureCapacity(); + } + storage[count++] = value; + } + + //Ensure capacity + private void ensureCapacity() { + int newCapacity = capacity * GROW_FACTOR; + storage = Arrays.copyOf(storage, newCapacity); + capacity = newCapacity; + } + + //Return the top element of the stack and pop it off the stack + private int pop() { + if (count == 0) + throw new IllegalArgumentException("Stack is empty."); + count--; + return storage[count]; + } + + //Return the top element of the stack without popping it + private int peek() { + if (count == 0){ + throw new IllegalArgumentException("Stack is empty."); + }else { + return storage[count-1]; + } + } + + //Determine whether the stack is empty + private boolean isEmpty() { + return count == 0; + } + + //Return the number of elements in the stack + private int size() { + return count; + } + +} +``` + +Verify + +```java +MyStack myStack = new MyStack(3); +myStack.push(1); +myStack.push(2); +myStack.push(3); +myStack.push(4); +myStack.push(5); +myStack.push(6); +myStack.push(7); +myStack.push(8); +System.out.println(myStack.peek());//8 +System.out.println(myStack.size());//8 +for (int i = 0; i < 8; i++) { + System.out.println(myStack.pop()); +} +System.out.println(myStack.isEmpty());//true +myStack.pop();//Error report: java.lang.IllegalArgumentException: Stack is empty. +``` + +## 4. Queue + +### 4.1. Queue Introduction + +**Queue** is a linear table of **First In, First Out (FIFO, First In, First Out)**. In specific applications, it is usually implemented using linked lists or arrays. The queue implemented using arrays is called **sequential queue**, and the queue implemented using linked lists is called **chained queue**. **The queue only allows insertion operations at the back end (rear), which is enqueue, and deletion operations at the front end (front), which is dequeue** + +Queues operate similarly to stacks, the only difference being that queues only allow new data to be added on the backend. + +```java +Suppose there are n elements in the queue. +Access: O(n) // worst case +Insertion and deletion: O(1) //Backend inserts and frontend deletes elements +``` + +![Queue](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/queue.png) + +### 4.2. Queue classification + +#### 4.2.1. Single queue + +A single queue is a common queue. Every time an element is added, it is added to the end of the queue. Single queue is divided into **sequential queue (array implementation)** and **chain queue (linked list implementation)**. + +**The sequential queue has the problem of "false overflow", which is the situation where there is a position but cannot be added. ** + +Assume that the figure below is a sequential queue. We dequeue the first two elements 1,2 and merge the two elements 7,8 into the queue. When performing enqueue and dequeue operations, front and rear will continue to move backward. When rear moves to the end, we can no longer add data to the queue, even if there is free space in the array, this phenomenon is **"false overflow"**. In addition to the false overflow problem, as shown in the figure below, when element 8 is added, the rear pointer moves outside the array (out of bounds). + +> In order to avoid the overlapping of the head and tail of the queue when there is only one element, which makes processing cumbersome, two pointers are introduced. The front pointer points to the opposite element, and the rear pointer points to the next position of the last element in the queue. In this way, when front is equal to rear, the queue does not have one element left, but an empty queue. ——From "Dahua Data Structure" + +![Sequential queue false overflow](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/seq-queue-false-overflow.png) + +#### 4.2.2. Circular Queue + +Circular queues can solve the problem of false overflow and out-of-bounds problems of sequential queues. The solution is: start from the beginning, which will form a loop that connects end to end, which is where the name of the circular queue comes from. + +Still using the picture above, if we point the rear pointer to the position where the array index is 0, there will be no out-of-bounds problem. When we add another element to the queue, rear moves backward. + +![Circular queue](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/circular-queue.png) + +In the sequential queue, when we say `front==rear`, the queue is empty, but in the circular queue, it is different and may be full, as shown in the figure above. There are two solutions: + +1. You can set a flag variable `flag`. When `front==rear` and `flag=0`, the queue is empty. When `front==rear` and `flag=1`, the queue is full. +2. When the queue is empty, it is `front==rear`. When the queue is full, we ensure that there is still a free position in the array, and rear points to this free position, as shown in the figure below. So now the condition for judging whether the queue is full is: `(rear+1) % QueueSize==front`. + +#### 4.2.3 Double-ended queue + +**Double-ended queue (Deque)** is a queue that can perform insertion and deletion operations at both ends of the queue. It is more flexible than a single queue.Generally speaking, we can perform `addFirst`, `addLast`, `removeFirst` and `removeLast` operations on a deque. + +#### 4.2.4 Priority queue + +**Priority Queue** is not a linear data structure in terms of underlying structure. It is generally implemented by a heap. + +1. As each element is enqueued, the priority queue inserts the new element into the heap and adjusts the heap. +2. When the head of the queue is dequeued, the priority queue will return the top element of the heap and adjust the heap. + +For the specific implementation of the heap, please see the section [Heap](https://javaguide.cn/cs-basics/data-structure/heap.html). + +All in all, no matter what operations we perform, the priority queue can perform a series of heap-related operations according to a certain sorting method, thereby ensuring the orderliness of the entire collection. + +Although the underlying structure of the priority queue is not strictly linear, we cannot perceive the heap when we use it. From the user's perspective, the priority queue can be considered a linear data structure: a linear queue that is automatically sorted. + +### 4.3. Common application scenarios of queues + +When we need to process data in a certain order, we can consider using the queue data structure. + +- **Blocking queue:** A blocking queue can be regarded as a queue with blocking operations added to the queue. When the queue is empty, the dequeue operation blocks, and when the queue is full, the enqueue operation blocks. Using blocking queues we can easily implement the "producer-consumer" model. +- **Request/Task Queue in Thread Pool:** When there are no idle threads in the thread pool, how will new task request thread resources be processed? The answer is that these tasks will be put into the task queue and wait for the threads in the thread pool to become idle before taking the tasks out of the queue for execution. Task queues are divided into unbounded queues (implemented based on linked lists) and bounded queues (implemented based on arrays). The characteristic of unbounded queue is that there is no theoretical limit on queue capacity, and tasks can continue to be queued until system resources are exhausted. For example: the blocking queue `LinkedBlockingQueue` used by `FixedThreadPool` has a default capacity of `Integer.MAX_VALUE`, so it can be regarded as an "unbounded queue". The bounded queue is different. When the queue is full, if new tasks are submitted, because the queue cannot continue to accommodate tasks, the thread pool will reject these tasks and throw a `java.util.concurrent.RejectedExecutionException` exception. +- **Stack**: The double-ended queue can inherently implement all the functions of the stack (`push`, `pop` and `peek`), and the relevant methods have been implemented in the Deque interface. The Stack class has been abandoned like Vector, and double-ended queues (Deques) are now commonly used in Java to implement stacks. +- **Breadth-First Search (BFS)**: During the breadth-first search process of the graph, the queue is used to store the nodes to be accessed, ensuring that the nodes of the graph are traversed in hierarchical order. +- Linux kernel process queue (queued by priority) +- Real life parties, playlists on the player; +- Message queue +- Wait... + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/red-black-tree.en.md b/docs_en/cs-basics/data-structure/red-black-tree.en.md new file mode 100644 index 00000000000..36981bafaa3 --- /dev/null +++ b/docs_en/cs-basics/data-structure/red-black-tree.en.md @@ -0,0 +1,100 @@ +--- +title: red-black tree +category: Computer Basics +tag: + - data structure +head: + - - meta + - name: keywords + content: red-black tree, self-balancing, rotation, insertion and deletion, properties, black height, time complexity + - - meta + - name: description + content: In-depth explanation of the five properties of red-black trees and the rotation adjustment process, understanding of the self-balancing mechanism and its application in standard libraries and index structures. +--- + +## Introduction to red-black trees + +Red Black Tree is a self-balancing binary search tree. It was invented by Rudolf Bayer in 1972 and was known as symmetric binary B-trees. It was later modified in 1978 by Leo J. Guibas and Robert Sedgewick to its current "red-black tree". + +Due to its self-balancing characteristics, it ensures that operations such as search, addition, and deletion can be completed within O(logn) time complexity in the worst case, with stable performance. + +In the JDK, `TreeMap`, `TreeSet` and JDK1.8's `HashMap` all use red-black trees at the bottom layer. + +## Why do we need red-black trees? + +The red-black tree was born to solve the shortcomings of the binary search tree. + +Binary search tree is a data structure based on comparison. Each node of it has a key value, and the key value of the left child node is less than the key value of the parent node, and the key value of the right child node is greater than the key value of the parent node. Such a structure can facilitate search, insertion, and deletion operations, because only the key values ​​of the nodes need to be compared to determine the location of the target node. However, a big problem with a binary search tree is that its shape depends on the order in which nodes are inserted. If nodes are inserted in ascending or descending order, the binary search tree will degenerate into a linear structure, that is, a linked list. In this case, the performance of the binary search tree will be greatly reduced, and the time complexity will change from O(logn) to O(n). + +The red-black tree was born to solve the shortcomings of the binary search tree, because the binary search tree will degenerate into a linear structure in some cases. + +## **Features of red-black trees** + +1. Each node is either red or black. Black determines balance, red does not. This corresponds to 1~2 nodes that can be stored in one node in the 2-3 tree. +2. The root node is always black. +3. Each leaf node is a black empty node (NIL node). What this means is that the red-black tree will have an empty leaf node, which is the rule of the red-black tree. +4. If a node is red, its child nodes must be black (not necessarily vice versa). Usually this rule is also called no consecutive red nodes. A node can temporarily have up to 3 child nodes, with a black node in the middle and red nodes on the left and right. +5. Each path from any node to its leaf node or empty child node must contain the same number of black nodes (that is, the same black height). Each layer has only one node that contributes to the tree height to determine the balance, which corresponds to the black node in the red-black tree. + +It is these characteristics that ensure the balance of the red-black tree, so that the height of the red-black tree will not exceed 2log(n+1). + +## Red-black tree data structure + +Based on the BST binary search tree, AVL, 2-3 trees, and red-black trees are all self-balancing binary trees (collectively referred to as B-trees). However, compared to the time complexity caused by high-level balancing in AVL trees, red-black trees have looser control over balance. Red-black trees only need to ensure the balance of black nodes. + +## Red-black tree structure implementation + +```java +public class Node { + + public Class clazz; + public Integer value; + public Node parent; + public Node left; + public Node right; + + // Required properties for AVL tree + public int height; + // Attributes required for red-black tree + public Color color = Color.RED; + +} +``` + +### 1. Left leaning dyeing + +![Slide 1](./pictures/red-black tree/red-black tree 1.png) + +- When dyeing, find the uncle node of the current node based on the grandfather node of the current node. +- Then dye the parent node black, the uncle node black, and the grandpa node red. However, the grandfather node's red coloring is temporary, and the root node will be dyed black after the balancing tree height operation. + +### 2. Right leaning dyeing + +![Slide 2](./pictures/red-black tree/red-black tree 2.png) + +### 3. Left-hand balance adjustment + +#### 3.1 One left rotation + +![Slide 3](./pictures/red-black tree/red-black tree 3.png) + +#### 3.2 Right-hand rotation + Left-hand rotation + +![Slide 4](./pictures/red-black tree/red-black tree 4.png) + +### 4. Right-hand adjustment + +#### 4.1 One right turn + +![Slide 5](./pictures/red-black tree/red-black tree 5.png) + +#### 4.2 Left-hand + right-hand rotation + +![Slide 6](./pictures/red-black tree/red-black tree 6.png) + +## Article recommendation + +- ["In-depth analysis of red-black trees and Java implementation" - Meituan Dianping technical team](https://zhuanlan.zhihu.com/p/24367771) +- [Comics: What is a red-black tree? - Programmer Xiao Hui](https://juejin.im/post/5a27c6946fb9a04509096248#comment) (Also introduced the binary search tree, highly recommended) + + \ No newline at end of file diff --git a/docs_en/cs-basics/data-structure/tree.en.md b/docs_en/cs-basics/data-structure/tree.en.md new file mode 100644 index 00000000000..b6bcf6d7831 --- /dev/null +++ b/docs_en/cs-basics/data-structure/tree.en.md @@ -0,0 +1,192 @@ +--- +title: 树 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 树,二叉树,二叉搜索树,平衡树,遍历,前序,中序,后序,层序,高度,深度 + - - meta + - name: description + content: 系统讲解树与二叉树的核心概念与遍历方法,结合高度/深度等指标,夯实数据结构基础与算法思维。 +--- + +树就是一种类似现实生活中的树的数据结构(倒置的树)。任何一颗非空树只有一个根节点。 + +一棵树具有以下特点: + +1. 一棵树中的任意两个结点有且仅有唯一的一条路径连通。 +2. 一棵树如果有 n 个结点,那么它一定恰好有 n-1 条边。 +3. 一棵树不包含回路。 + +下图就是一颗树,并且是一颗二叉树。 + +![二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E4%BA%8C%E5%8F%89%E6%A0%91-2.png) + +如上图所示,通过上面这张图说明一下树中的常用概念: + +- **节点**:树中的每个元素都可以统称为节点。 +- **根节点**:顶层节点或者说没有父节点的节点。上图中 A 节点就是根节点。 +- **父节点**:若一个节点含有子节点,则这个节点称为其子节点的父节点。上图中的 B 节点是 D 节点、E 节点的父节点。 +- **子节点**:一个节点含有的子树的根节点称为该节点的子节点。上图中 D 节点、E 节点是 B 节点的子节点。 +- **兄弟节点**:具有相同父节点的节点互称为兄弟节点。上图中 D 节点、E 节点的共同父节点是 B 节点,故 D 和 E 为兄弟节点。 +- **叶子节点**:没有子节点的节点。上图中的 D、F、H、I 都是叶子节点。 +- **节点的高度**:该节点到叶子节点的最长路径所包含的边数。 +- **节点的深度**:根节点到该节点的路径所包含的边数 +- **节点的层数**:节点的深度+1。 +- **树的高度**:根节点的高度。 + +> 关于树的深度和高度的定义可以看 stackoverflow 上的这个问题:[What is the difference between tree depth and height?](https://stackoverflow.com/questions/2603692/what-is-the-difference-between-tree-depth-and-height) 。 + +## 二叉树的分类 + +**二叉树**(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。 + +**二叉树** 的分支通常被称作“**左子树**”或“**右子树**”。并且,**二叉树** 的分支具有左右次序,不能随意颠倒。 + +**二叉树** 的第 i 层至多拥有 `2^(i-1)` 个节点,深度为 k 的二叉树至多总共有 `2^(k+1)-1` 个节点(满二叉树的情况),至少有 2^(k) 个节点(关于节点的深度的定义国内争议比较多,我个人比较认可维基百科对[节点深度的定义]())。 + +![危机百科对节点深度的定义](https://oss.javaguide.cn/github/javaguide/image-20220119112736158.png) + +### 满二叉树 + +一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是 **满二叉树**。也就是说,如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 **满二叉树**。如下图所示: + +![满二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/full-binary-tree.png) + +### 完全二叉树 + +除最后一层外,若其余层都是满的,并且最后一层是满的或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。 + +大家可以想象为一棵树从根结点开始扩展,扩展完左子节点才能开始扩展右子节点,每扩展完一层,才能继续扩展下一层。如下图所示: + +![完全二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/complete-binary-tree.png) + +完全二叉树有一个很好的性质:**父结点和子节点的序号有着对应关系。** + +细心的小伙伴可能发现了,当根节点的值为 1 的情况下,若父结点的序号是 i,那么左子节点的序号就是 2i,右子节点的序号是 2i+1。这个性质使得完全二叉树利用数组存储时可以极大地节省空间,以及利用序号找到某个节点的父结点和子节点,后续二叉树的存储会详细介绍。 + +### 平衡二叉树 + +**平衡二叉树** 是一棵二叉排序树,且具有以下性质: + +1. 可以是一棵空树 +2. 如果不是空树,它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。 + +平衡二叉树的常用实现方法有 **红黑树**、**AVL 树**、**替罪羊树**、**加权平衡树**、**伸展树** 等。 + +在给大家展示平衡二叉树之前,先给大家看一棵树: + +![斜树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/oblique-tree.png) + +**你管这玩意儿叫树???** + +没错,这玩意儿还真叫树,只不过这棵树已经退化为一个链表了,我们管它叫 **斜树**。 + +**如果这样,那我为啥不直接用链表呢?** + +谁说不是呢? + +二叉树相比于链表,由于父子节点以及兄弟节点之间往往具有某种特殊的关系,这种关系使得我们在树中对数据进行**搜索**和**修改**时,相对于链表更加快捷便利。 + +但是,如果二叉树退化为一个链表了,那么那么树所具有的优秀性质就难以表现出来,效率也会大打折,为了避免这样的情况,我们希望每个做 “家长”(父结点) 的,都 **一碗水端平**,分给左儿子和分给右儿子的尽可能一样多,相差最多不超过一层,如下图所示: + +![平衡二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/balanced-binary-tree.png) + +## 二叉树的存储 + +二叉树的存储主要分为 **链式存储** 和 **顺序存储** 两种: + +### 链式存储 + +和链表类似,二叉树的链式存储依靠指针将各个节点串联起来,不需要连续的存储空间。 + +每个节点包括三个属性: + +- 数据 data。data 不一定是单一的数据,根据不同情况,可以是多个具有不同类型的数据。 +- 左节点指针 left +- 右节点指针 right。 + +可是 JAVA 没有指针啊! + +那就直接引用对象呗(别问我对象哪里找) + +![链式存储二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/chain-store-binary-tree.png) + +### 顺序存储 + +顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。根结点的序号为 1,对于每个节点 Node,假设它存储在数组中下标为 i 的位置,那么它的左子节点就存储在 2i 的位置,它的右子节点存储在下标为 2i+1 的位置。 + +一棵完全二叉树的数组顺序存储如下图所示: + +![完全二叉树的数组顺序存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/sequential-storage.png) + +大家可以试着填写一下存储如下二叉树的数组,比较一下和完全二叉树的顺序存储有何区别: + +![非完全二叉树的数组顺序存储](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/sequential-storage2.png) + +可以看到,如果我们要存储的二叉树不是完全二叉树,在数组中就会出现空隙,导致内存利用率降低 + +## 二叉树的遍历 + +### 先序遍历 + +![先序遍历](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/preorder-traversal.png) + +二叉树的先序遍历,就是先输出根结点,再遍历左子树,最后遍历右子树,遍历左子树和右子树的时候,同样遵循先序遍历的规则,也就是说,我们可以递归实现先序遍历。 + +代码如下: + +```java +public void preOrder(TreeNode root){ + if(root == null){ + return; + } + system.out.println(root.data); + preOrder(root.left); + preOrder(root.right); +} +``` + +### In-order traversal + +![Inorder traversal](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/inorder-traversal.png) + +In-order traversal of a binary tree is to first traverse the left subtree recursively in order, then output the value of the root node, and then traverse the right subtree recursively in order. You can imagine that the tree is flattened with a slap, and the parent node is photographed between the left child node and the right child node, as shown in the following figure: + +![Inorder traversal](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/inorder-traversal2.png) + +The code is as follows: + +```java +public void inOrder(TreeNode root){ + if(root==null){ + return; + } + inOrder(root.left); + system.out.println(root.data); + inOrder(root.right); +} +``` + +### Postorder traversal + +![Postorder traversal](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/postorder-traversal.png) + +Post-order traversal of a binary tree means first recursively traversing the left subtree in post-order, then recursively traversing the right subtree in post-order, and finally outputting the value of the root node. + +The code is as follows: + +```java +public void postOrder(TreeNode root){ + if(root==null){ + return; + } + postOrder(root.left); + postOrder(root.right); + system.out.println(root.data); +} +``` + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/application-layer-protocol.en.md b/docs_en/cs-basics/network/application-layer-protocol.en.md new file mode 100644 index 00000000000..506094d7341 --- /dev/null +++ b/docs_en/cs-basics/network/application-layer-protocol.en.md @@ -0,0 +1,152 @@ +--- +title: 应用层常见协议总结(应用层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 应用层协议,HTTP,WebSocket,DNS,SMTP,FTP,特性,场景 + - - meta + - name: description + content: 汇总应用层常见协议的核心概念与典型场景,重点对比 HTTP 与 WebSocket 的通信模型与能力边界。 +--- + +## HTTP:超文本传输协议 + +**超文本传输协议(HTTP,HyperText Transfer Protocol)** 是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。 + +HTTP 使用客户端-服务器模型,客户端向服务器发送 HTTP Request(请求),服务器响应请求并返回 HTTP Response(响应),整个过程如下图所示。 + +![](https://oss.javaguide.cn/github/javaguide/450px-HTTP-Header.png) + +HTTP 协议基于 TCP 协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。 + +另外, HTTP 协议是“无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。 + +## Websocket:全双工通信协议 + +WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。 + +WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。 + +WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + +下面是 WebSocket 的常见应用场景: + +- 视频弹幕 +- 实时消息推送,详见[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)这篇文章 +- 实时游戏对战 +- 多用户协同编辑 +- 社交聊天 +- …… + +WebSocket 的工作过程可以分为以下几个步骤: + +1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket; +2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。 +3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。 +4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。 + +另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 + +## SMTP:简单邮件传输(发送)协议 + +**简单邮件传输(发送)协议(SMTP,Simple Mail Transfer Protocol)** 基于 TCP 协议,是一种用于发送电子邮件的协议 + +![SMTP 协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/what-is-smtp.png) + +注意 ⚠️:**接受邮件的协议不是 SMTP 而是 POP3 协议。** + +SMTP 协议这块涉及的内容比较多,下面这两个问题比较重要: + +1. 电子邮件的发送过程 +2. 如何判断邮箱是真正存在的? + +**电子邮件的发送过程?** + +比如我的邮箱是“”,我要向“”发送邮件,整个过程可以简单分为下面几步: + +1. 通过 **SMTP** 协议,我将我写好的邮件交给 163 邮箱服务器(邮局)。 +2. 163 邮箱服务器发现我发送的邮箱是 qq 邮箱,然后它使用 SMTP 协议将我的邮件转发到 qq 邮箱服务器。 +3. qq 邮箱服务器接收邮件之后就通知邮箱为“”的用户来收邮件,然后用户就通过 **POP3/IMAP** 协议将邮件取出。 + +**如何判断邮箱是真正存在的?** + +很多场景(比如邮件营销)下面我们需要判断我们要发送的邮箱地址是否真的存在,这个时候我们可以利用 SMTP 协议来检测: + +1. 查找邮箱域名对应的 SMTP 服务器地址 +2. 尝试与服务器建立连接 +3. 连接成功后尝试向需要验证的邮箱发送邮件 +4. 根据返回结果判定邮箱地址的真实性 + +推荐几个在线邮箱是否有效检测工具: + +1. +2. +3. + +## POP3/IMAP:邮件接收的协议 + +这两个协议没必要多做阐述,只需要了解 **POP3 和 IMAP 两者都是负责邮件接收的协议** 即可(二者也是基于 TCP 协议)。另外,需要注意不要将这两者和 SMTP 协议搞混淆了。**SMTP 协议只负责邮件的发送,真正负责接收的协议是 POP3/IMAP。** + +IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。 + +## FTP:文件传输协议 + +**FTP 协议** 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。 + +FTP 是基于客户—服务器(C/S)模型而设计的,在客户端与 FTP 服务器之间建立两个连接。如果我们要基于 FTP 协议开发一个文件传输的软件的话,首先需要搞清楚 FTP 的原理。关于 FTP 的原理,很多书籍上已经描述的非常详细了: + +> FTP 的独特的优势同时也是与其它客户服务器程序最大的不同点就在于它在两台通信的主机之间使用了两条 TCP 连接(其它客户服务器应用程序一般只有一条 TCP 连接): +> +> 1. 控制连接:用于传送控制信息(命令和响应) +> 2. 数据连接:用于数据传送; +> +> 这种将命令和数据分开传送的思想大大提高了 FTP 的效率。 + +![FTP工作过程](https://oss.javaguide.cn/github/javaguide/cs-basics/network/ftp.png) + +注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。因此,FTP 传输的文件可能会被窃听或篡改。建议在传输敏感数据时使用更安全的协议,如 SFTP(SSH File Transfer Protocol,一种基于 SSH 协议的安全文件传输协议,用于在网络上安全地传输文件)。 + +## Telnet:远程登陆协议 + +**Telnet 协议** 基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。 + +![Telnet:远程登陆协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/Telnet_is_vulnerable_to_eavesdropping-2.png) + +## SSH:安全的网络传输协议 + +**SSH(Secure Shell)** 基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务。 + +The classic use of SSH is to log into a remote computer to execute commands. In addition to this, SSH also supports tunneling protocols, port mapping, and X11 connections (allowing users to run graphical applications locally on remote servers). SSH can also transfer files securely with the help of SFTP (SSH File Transfer Protocol) or SCP (Secure Copy Protocol) protocols. + +SSH uses a client-server model and the default port is 22. SSH is a daemon process that is responsible for monitoring client requests in real time and processing them. Most modern operating systems provide SSH. + +As shown in the figure below, SSH Client and SSH Server generate a shared symmetric encryption key through public key exchange for subsequent encrypted communication. + +![SSH: Secure Network Transfer Protocol](https://oss.javaguide.cn/github/javaguide/cs-basics/network/ssh-client-server.png) + +## RTP: Real-Time Transport Protocol + +RTP (Real-time Transport Protocol) is usually based on UDP protocol, but also supports TCP protocol. It provides end-to-end real-time data transmission function, but does not include resource reservation and does not guarantee real-time transmission quality. These functions are implemented by WebRTC. + +The RTP protocol is divided into two sub-protocols: + +- **RTP (Real-time Transport Protocol, Real-time Transport Protocol)**: Transmits data with real-time characteristics. +- **RTCP (RTP Control Protocol, RTP Control Protocol)**: Provides statistical information (such as network delay, packet loss rate, etc.) during real-time transmission. WebRTC handles packet loss based on this information + +## DNS: Domain Name System + +DNS (Domain Name System) is based on the UDP protocol and is used to solve the mapping problem between domain names and IP addresses. + +![DNS: Domain Name System](https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png) + +## Reference + +- "Top-Down Method of Computer Networks" (Seventh Edition) +- Introduction to RTP protocol: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/arp.en.md b/docs_en/cs-basics/network/arp.en.md new file mode 100644 index 00000000000..86c16fce88f --- /dev/null +++ b/docs_en/cs-basics/network/arp.en.md @@ -0,0 +1,112 @@ +--- +title: ARP 协议详解(网络层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: ARP,地址解析,IP到MAC,广播问询,单播响应,ARP表,欺骗 + - - meta + - name: description + content: 讲解 ARP 的地址解析机制与报文流程,结合 ARP 表与广播/单播详解常见攻击与防御策略。 +--- + +每当我们学习一个新的网络协议的时候,都要把他结合到 OSI 七层模型中,或者是 TCP/IP 协议栈中来学习,一是要学习该协议在整个网络协议栈中的位置,二是要学习该协议解决了什么问题,地位如何?三是要学习该协议的工作原理,以及一些更深入的细节。 + +**ARP 协议**,可以说是在协议栈中属于一个**偏底层的、非常重要的、又非常简单的**通信协议。 + +开始阅读这篇文章之前,你可以先看看下面几个问题: + +1. **ARP 协议在协议栈中的位置?** ARP 协议在协议栈中的位置非常重要,在理解了它的工作原理之后,也很难说它到底是网络层协议,还是链路层协议,因为它恰恰串联起了网络层和链路层。国外的大部分教程通常将 ARP 协议放在网络层。 +2. **ARP 协议解决了什么问题,地位如何?** ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 +3. **ARP 工作原理?** 只希望大家记住几个关键词:**ARP 表、广播问询、单播响应**。 + +## MAC 地址 + +在介绍 ARP 协议之前,有必要介绍一下 MAC 地址。 + +MAC 地址的全称是 **媒体访问控制地址(Media Access Control Address)**。如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。 + +![路由器的背面就会注明 MAC 位址](https://oss.javaguide.cn/github/javaguide/cs-basics/network/router-back-will-indicate-mac-address.png) + +可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。 + +> 还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。 + +MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。 + +MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。 + +最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。 + +## ARP 协议工作原理 + +ARP 协议工作时有一个大前提,那就是 **ARP 表**。 + +在一个局域网内,每个网络设备都自己维护了一个 ARP 表,ARP 表记录了某些其他网络设备的 IP 地址-MAC 地址映射关系,该映射关系以 `` 三元组的形式存储。其中,TTL 为该映射关系的生存周期,典型值为 20 分钟,超过该时间,该条目将被丢弃。 + +ARP 的工作原理将分两种场景讨论: + +1. **同一局域网内的 MAC 寻址**; +2. **从一个局域网到另一个局域网中的网络设备的寻址**。 + +### 同一局域网内的 MAC 寻址 + +假设当前有如下场景:IP 地址为`137.196.7.23`的主机 A,想要给同一局域网内的 IP 地址为`137.196.7.14`主机 B,发送 IP 数据报文。 + +> 再次强调,当主机发送 IP 数据报文时(网络层),仅知道目的地的 IP 地址,并不清楚目的地的 MAC 地址,而 ARP 协议就是解决这一问题的。 + +为了达成这一目标,主机 A 将不得不通过 ARP 协议来获取主机 B 的 MAC 地址,并将 IP 报文封装成链路层帧,发送到下一跳上。在该局域网内,关于此将按照时间顺序,依次发生如下事件: + +1. 主机 A 检索自己的 ARP 表,发现 ARP 表中并无主机 B 的 IP 地址对应的映射条目,也就无从知道主机 B 的 MAC 地址。 + +2. 主机 A 将构造一个 ARP 查询分组,并将其广播到所在的局域网中。 + + ARP 分组是一种特殊报文,ARP 分组有两类,一种是查询分组,另一种是响应分组,它们具有相同的格式,均包含了发送和接收的 IP 地址、发送和接收的 MAC 地址。当然了,查询分组中,发送的 IP 地址,即为主机 A 的 IP 地址,接收的 IP 地址即为主机 B 的 IP 地址,发送的 MAC 地址也是主机 A 的 MAC 地址,但接收的 MAC 地址绝不会是主机 B 的 MAC 地址(因为这正是我们要问询的!),而是一个特殊值——`FF-FF-FF-FF-FF-FF`,之前说过,该 MAC 地址是广播地址,也就是说,查询分组将广播给该局域网内的所有设备。 + +3. 主机 A 构造的查询分组将在该局域网内广播,理论上,每一个设备都会收到该分组,并检查查询分组的接收 IP 地址是否为自己的 IP 地址,如果是,说明查询分组已经到达了主机 B,否则,该查询分组对当前设备无效,丢弃之。 + +4. 主机 B 收到了查询分组之后,验证是对自己的问询,接着构造一个 ARP 响应分组,该分组的目的地只有一个——主机 A,发送给主机 A。同时,主机 B 提取查询分组中的 IP 地址和 MAC 地址信息,在自己的 ARP 表中构造一条主机 A 的 IP-MAC 映射记录。 + + ARP 响应分组具有和 ARP 查询分组相同的构造,不同的是,发送和接受的 IP 地址恰恰相反,发送的 MAC 地址为发送者本身,目标 MAC 地址为查询分组的发送者,也就是说,ARP 响应分组只有一个目的地,而非广播。 + +5. 主机 A 终将收到主机 B 的响应分组,提取出该分组中的 IP 地址和 MAC 地址后,构造映射信息,加入到自己的 ARP 表中。 + +![](./images/arp/arp_same_lan.png) + +在整个过程中,有几点需要补充说明的是: + +1. 主机 A 想要给主机 B 发送 IP 数据报,如果主机 B 的 IP-MAC 映射信息已经存在于主机 A 的 ARP 表中,那么主机 A 无需广播,只需提取 MAC 地址并构造链路层帧发送即可。 +2. ARP 表中的映射信息是有生存周期的,典型值为 20 分钟。 +3. 目标主机接收到了问询主机构造的问询报文后,将先把问询主机的 IP-MAC 映射存进自己的 ARP 表中,这样才能获取到响应的目标 MAC 地址,顺利的发送响应分组。 + +总结来说,ARP 协议是一个**广播问询,单播响应**协议。 + +### 不同局域网内的 MAC 寻址 + +更复杂的情况是,发送主机 A 和接收主机 B 不在同一个子网中,假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是,一般情况下,我们说网络设备都有一个 IP 地址和一个 MAC 地址,这里说的网络设备,更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 IP 地址和 MAC 地址。因此,在讨论 ARP 表时,路由器的多个接口都各自维护一个 ARP 表,而非一个路由器只维护一个 ARP 表。 + +接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在的子网内所有设备(接口)都将会捕获该分组,因为该分组的目的 IP 与发送主机 A 的 IP 在同一个子网中。但是当目的 IP 与 A 不在同一子网时,A 所在子网内将不会有设备成功接收该分组。那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下: + +1. 主机 A 查询 ARP 表,期望寻找到目标路由器的本子网接口的 MAC 地址。 + +The destination router refers to the router that can analyze the subnet where B is located based on the IP address of destination host B and can forward the packet to the subnet where B is located. + +2. Host A cannot find the MAC address of the subnet interface of the target router. It will use the ARP protocol to query the MAC address. Since the target interface and host A are in the same subnet, the process is the same as MAC addressing in the same LAN. + +3. Host A obtains the MAC address of the target interface, first constructs an IP datagram, where the source IP is A's IP address, and the destination IP address is B's IP address, and then constructs a link layer frame, where the source MAC address is A's MAC address, and the destination MAC address is the MAC address of the interface connected to the router in this subnet. Host A will send this link layer frame to the target interface in unicast mode. + +4. The target interface receives the link layer frame sent from host A, parses it, queries the forwarding table based on the destination IP address, and forwards the IP datagram to the interface connected to the subnet where host B is located. + + At this point, the frame has been transferred from the subnet where host A is located to the subnet where host B is located. + +5. The router interface queries the ARP table, hoping to find the MAC address of host B. + +6. If the router interface fails to find the MAC address of host B, it will use the ARP protocol, broadcast inquiry, and unicast response to obtain the MAC address of host B. + +7. The router interface will re-encapsulate the IP datagram into a link layer frame, with the destination MAC address being the MAC address of host B, and send it in unicast until the destination. + +![](./images/arp/arp_different_lan.png) + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/computer-network-xiexiren-summary.en.md b/docs_en/cs-basics/network/computer-network-xiexiren-summary.en.md new file mode 100644 index 00000000000..0d217320c4e --- /dev/null +++ b/docs_en/cs-basics/network/computer-network-xiexiren-summary.en.md @@ -0,0 +1,328 @@ +--- +title: 《计算机网络》(谢希仁)内容总结 +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 计算机网络,谢希仁,术语,分层模型,链路,主机,教材总结 + - - meta + - name: description + content: 基于《计算机网络》教材的学习笔记,梳理术语与分层模型等核心知识点,便于期末复习与面试巩固。 +--- + +本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的[《计算机网络》第七版](https://www.elias.ltd/usr/local/etc/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%EF%BC%88%E7%AC%AC7%E7%89%88%EF%BC%89%E8%B0%A2%E5%B8%8C%E4%BB%81.pdf)这本书。为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。 + +![](https://oss.javaguide.cn/p3-juejin/fb5d8645cd55484ab0177f25a13e97db~tplv-k3u1fbpfcp-zoom-1.png) + +相关问题:[如何评价谢希仁的计算机网络(第七版)? - 知乎](https://www.zhihu.com/question/327872966) 。 + +## 1. 计算机网络概述 + +### 1.1. 基本术语 + +1. **结点 (node)**:网络中的结点可以是计算机,集线器,交换机或路由器等。 +2. **链路(link )** : 从一个结点到另一个结点的一段物理线路。中间没有任何其他交点。 +3. **主机(host)**:连接在因特网上的计算机。 +4. **ISP(Internet Service Provider)**:因特网服务提供者(提供商)。 + + ![ISP (Internet Service Provider) Definition](https://oss.javaguide.cn/p3-juejin/e77e26123d404d438d0c5943e3c65893~tplv-k3u1fbpfcp-zoom-1.png) + +5. **IXP(Internet eXchange Point)**:互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。 + + ![IXP Traffic Levels During the Stratos Skydive — RIPE Labs](https://oss.javaguide.cn/p3-juejin/7f9a6ddaa09441ceac11cb77f7a69d8f~tplv-k3u1fbpfcp-zoom-1.png) + +

https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive

+ +6. **RFC(Request For Comments)**:意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。 +7. **广域网 WAN(Wide Area Network)**:任务是通过长距离运送主机发送的数据。 +8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。 +9. **局域网 LAN(Local Area Network)**:学校或企业大多拥有多个互连的局域网。 + + ![MAN & WMAN | Red de área metropolitana, Redes informaticas, Par trenzado](https://oss.javaguide.cn/p3-juejin/eb48d21b2e984a63a26250010d7adac4~tplv-k3u1fbpfcp-zoom-1.png) + +

http://conexionesmanwman.blogspot.com/

+ +10. **个人区域网 PAN(Personal Area Network)**:在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。 + + ![Advantages and disadvantages of personal area network (PAN) - IT Release](https://oss.javaguide.cn/p3-juejin/54bd7b420388494fbe917e3c9c13f1a7~tplv-k3u1fbpfcp-zoom-1.png) + +

https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/

+ +11. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。 +12. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。 + + ![](https://oss.javaguide.cn/p3-juejin/addb6b2211444a4da9e0ffc129dd444f~tplv-k3u1fbpfcp-zoom-1.gif) + +13. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。 +14. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 + +### 1.2. 重要知识点总结 + +1. **计算机网络(简称网络)把许多计算机连接在一起,而互联网把许多网络连接在一起,是网络的网络。** +2. 小写字母 i 开头的 internet(互联网)是通用名词,它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议(即通信规则)可以是任意的。大写字母 I 开头的 Internet(互联网)是专用名词,它指全球最大的,开放的,由众多网络相互连接而成的特定的互联网,并采用 TCP/IP 协议作为通信规则,其前身为 ARPANET。Internet 的推荐译名为因特网,现在一般流行称为互联网。 +3. 路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据段的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 +4. 互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 +5. 计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S 方式)和对等连接方式(P2P 方式)。 +6. 客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 +7. 按照作用范围的不同,计算机网络分为广域网 WAN,城域网 MAN,局域网 LAN,个人区域网 PAN。 +8. **计算机网络最常用的性能指标是:速率,带宽,吞吐量,时延(发送时延,处理时延,排队时延),时延带宽积,往返时间和信道利用率。** +9. 网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 +10. **五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是 TCP 和 UDP 协议,网络层最重要的协议是 IP 协议。** + +![](https://oss.javaguide.cn/p3-juejin/acec0fa44041449b8088872dcd7c0b3a~tplv-k3u1fbpfcp-zoom-1.gif) + +下面的内容会介绍计算机网络的五层体系结构:**物理层+数据链路层+网络层(网际层)+运输层+应用层**。 + +## 2. 物理层(Physical Layer) + +![物理层](https://oss.javaguide.cn/p3-juejin/cf1bfdd36e5f4bde94aea44bbe7a6f8a~tplv-k3u1fbpfcp-zoom-1.png) + +### 2.1. 基本术语 + +1. **数据(data)**:运送消息的实体。 +2. **信号(signal)**:数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。 +3. **码元( code)**:在使用时间域(或简称为时域)的波形来表示数字信号时,代表不同离散数值的基本波形。 +4. **单工(simplex )**:只能有一个方向的通信而没有反方向的交互。 +5. **半双工(half duplex )**:通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。 +6. **全双工(full duplex)**:通信的双方可以同时发送和接收信息。 + + ![](https://oss.javaguide.cn/p3-juejin/b1f02095b7c34eafb3c255ee81f58c2a~tplv-k3u1fbpfcp-zoom-1.png) + +7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量 + + ![](https://oss.javaguide.cn/p3-juejin/f939342f543046459ffabdc476f7bca4~tplv-k3u1fbpfcp-zoom-1.png) + +8. **奈氏准则**:在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 +9. **香农定理**:在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。 +10. **基带信号(baseband signal)**:来自信源的信号。指没有经过调制的数字信号或模拟信号。 +11. **带通(频带)信号(bandpass signal)**:把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道),这里调制过后的信号就是带通信号。 +12. **调制(modulation )**:对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 +13. **信噪比(signal-to-noise ratio )**:指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。 +14. **信道复用(channel multiplexing )**:指多个用户共享同一个信道。(并不一定是同时)。 + + ![信道复用技术](https://oss.javaguide.cn/p3-juejin/5d9bf7b3db324ae7a88fcedcbace45d8~tplv-k3u1fbpfcp-zoom-1.png) + +15. **比特率(bit rate )**:单位时间(每秒)内传送的比特数。 +16. **波特率(baud rate)**:单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。 +17. **复用(multiplexing)**:共享信道的方法。 +18. **ADSL(Asymmetric Digital Subscriber Line )**:非对称数字用户线。 +19. **光纤同轴混合网(HFC 网)**:在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 + +### 2.2. 重要知识点总结 + +1. **物理层的主要任务就是确定与传输媒体接口有关的一些特性,如机械特性,电气特性,功能特性,过程特性。** +2. 一个数据通信系统可划分为三大部分,即源系统,传输系统,目的系统。源系统包括源点(或源站,信源)和发送器,目的系统包括接收器和终点。 +3. **通信的目的是传送消息。如话音,文字,图像等都是消息,数据是运送消息的实体。信号则是数据的电气或电磁的表现。** +4. 根据信号中代表消息的参数的取值方式不同,信号可分为模拟信号(或连续信号)和数字信号(或离散信号)。在使用时间域(简称时域)的波形表示数字信号时,代表不同离散数值的基本波形称为码元。 +5. 根据双方信息交互的方式,通信可划分为单向通信(或单工通信),双向交替通信(或半双工通信),双向同时通信(全双工通信)。 +6. 来自信源的信号称为基带信号。信号要在信道上传输就要经过调制。调制有基带调制和带通调制之分。最基本的带通调制方法有调幅,调频和调相。还有更复杂的调制方法,如正交振幅调制。 +7. 要提高数据在信道上的传递速率,可以使用更好的传输媒体,或使用先进的调制技术。但数据传输速率不可能任意被提高。 +8. 传输媒体可分为两大类,即导引型传输媒体(双绞线,同轴电缆,光纤)和非导引型传输媒体(无线,红外,大气激光)。 +9. 为了有效利用光纤资源,在光纤干线和用户之间广泛使用无源光网络 PON。无源光网络无需配备电源,其长期运营成本和管理成本都很低。最流行的无源光网络是以太网无源光网络 EPON 和吉比特无源光网络 GPON。 + +### 2.3. 补充 + +#### 2.3.1. 物理层主要做啥? + +物理层主要做的事情就是 **透明地传送比特流**。也可以将物理层的主要任务描述为确定与传输媒体的接口的一些特性,即:机械特性(接口所用接线器的一些物理属性如形状和尺寸),电气特性(接口电缆的各条线上出现的电压的范围),功能特性(某条线上出现的某一电平的电压的意义),过程特性(对于不同功能的各种可能事件的出现顺序)。 + +**物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。** 现有的计算机网络中的硬件设备和传输媒体的种类非常繁多,而且通信手段也有许多不同的方式。物理层的作用正是尽可能地屏蔽掉这些传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可以使数据链路层只考虑完成本层的协议和服务,而不必考虑网络的具体传输媒体和通信手段是什么。 + +#### 2.3.2. 几种常用的信道复用技术 + +1. **频分复用(FDM)**:所有用户在同样的时间占用不同的带宽资源。 +2. **时分复用(TDM)**:所有用户在不同的时间占用同样的频带宽度(分时不分频)。 +3. **统计时分复用 (Statistic TDM)**:改进的时分复用,能够明显提高信道的利用率。 +4. **码分复用(CDM)**:用户使用经过特殊挑选的不同码型,因此各用户之间不会造成干扰。这种系统发送的信号有很强的抗干扰能力,其频谱类似于白噪声,不易被敌人发现。 +5. **波分复用( WDM)**:波分复用就是光的频分复用。 + +#### 2.3.3. 几种常用的宽带接入技术,主要是 ADSL 和 FTTx + +用户到互联网的宽带接入方法有非对称数字用户线 ADSL(用数字技术对现有的模拟电话线进行改造,而不需要重新布线。ADSL 的快速版本是甚高速数字用户线 VDSL。),光纤同轴混合网 HFC(是在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网)和 FTTx(即光纤到······)。 + +## 3. 数据链路层(Data Link Layer) + +![数据链路层](https://oss.javaguide.cn/p3-juejin/83ec6dafc8c14ca185bafb656d86f0b2~tplv-k3u1fbpfcp-zoom-1.png) + +### 3.1. 基本术语 + +1. **链路(link)**:一个结点到相邻结点的一段物理链路。 +2. **数据链路(data link)**:把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路。 +3. **循环冗余检验 CRC(Cyclic Redundancy Check)**:为了保证数据传输的可靠性,CRC 是数据链路层广泛使用的一种检错技术。 +4. **帧(frame)**:一个数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元。 +5. **MTU(Maximum Transfer Uint )**:最大传送单元。帧的数据部分的的长度上限。 +6. **误码率 BER(Bit Error Rate )**:在一段时间内,传输错误的比特占所传输比特总数的比率。 +7. **PPP(Point-to-Point Protocol )**:点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意图: + ![PPP](https://oss.javaguide.cn/p3-juejin/6b0310d3103c4149a725a28aaf001899~tplv-k3u1fbpfcp-zoom-1.jpeg) +8. **MAC 地址(Media Access Control 或者 Medium Access Control)**:意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。” + + ![ARP (Address Resolution Protocol) explained](https://oss.javaguide.cn/p3-juejin/057b83e7ec5b4c149e56255a3be89141~tplv-k3u1fbpfcp-zoom-1.png) + +9. **网桥(bridge)**:一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 +10. **交换机(switch )**:广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 + +### 3.2. 重要知识点总结 + +1. 链路是从一个结点到相邻结点的一段物理链路,数据链路则在链路的基础上增加了一些必要的硬件(如网络适配器)和软件(如协议的实现) +2. 数据链路层使用的主要是**点对点信道**和**广播信道**两种。 +3. 数据链路层传输的协议数据单元是帧。数据链路层的三个基本问题是:**封装成帧**,**透明传输**和**差错检测** +4. **循环冗余检验 CRC** 是一种检错方法,而帧检验序列 FCS 是添加在数据后面的冗余码 +5. **点对点协议 PPP** 是数据链路层使用最多的一种协议,它的特点是:简单,只检测差错而不去纠正差错,不使用序号,也不进行流量控制,可同时支持多种网络层协议 +6. PPPoE 是为宽带上网的主机使用的链路层协议 +7. **局域网的优点是:具有广播功能,从一个站点可方便地访问全网;便于系统的扩展和逐渐演变;提高了系统的可靠性,可用性和生存性。** +8. 计算机与外接局域网通信需要通过通信适配器(或网络适配器),它又称为网络接口卡或网卡。**计算器的硬件地址就在适配器的 ROM 中**。 +9. 以太网采用的无连接的工作方式,对发送的数据帧不进行编号,也不要求对方发回确认。目的站收到有差错帧就把它丢掉,其他什么也不做 +10. 以太网采用的协议是具有冲突检测的**载波监听多点接入 CSMA/CD**。协议的特点是:**发送前先监听,边发送边监听,一旦发现总线上出现了碰撞,就立即停止发送。然后按照退避算法等待一段随机时间后再次发送。** 因此,每一个站点在自己发送数据之后的一小段时间内,存在着遭遇碰撞的可能性。以太网上的各站点平等地争用以太网信道 +11. 以太网的适配器具有过滤功能,它只接收单播帧,广播帧和多播帧。 +12. 使用集线器可以在物理层扩展以太网(扩展后的以太网仍然是一个网络) + +### 3.3. 补充 + +1. 数据链路层的点对点信道和广播信道的特点,以及这两种信道所使用的协议(PPP 协议以及 CSMA/CD 协议)的特点 +2. 数据链路层的三个基本问题:**封装成帧**,**透明传输**,**差错检测** +3. 以太网的 MAC 层硬件地址 +4. 适配器,转发器,集线器,网桥,以太网交换机的作用以及适用场合 + +## 4. 网络层(Network Layer) + +![网络层](https://oss.javaguide.cn/p3-juejin/775dc8136bec486aad4f1182c68f24cd~tplv-k3u1fbpfcp-zoom-1.png) + +### 4.1. 基本术语 + +1. **虚电路(Virtual Circuit)** : 在两个终端设备的逻辑或物理端口之间,通过建立的双向的透明传输通道。虚电路表示这只是一条逻辑上的连接,分组都沿着这条逻辑连接按照存储转发方式传送,而并不是真正建立了一条物理连接。 +2. **IP(Internet Protocol )** : 网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一,是 TCP/IP 体系结构网际层的核心。配套的有 ARP,RARP,ICMP,IGMP。 +3. **ARP(Address Resolution Protocol)** : 地址解析协议。地址解析协议 ARP 把 IP 地址解析为硬件地址。 +4. **ICMP(Internet Control Message Protocol )**:网际控制报文协议 (ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告)。 +5. **子网掩码(subnet mask )**:它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。 +6. **CIDR( Classless Inter-Domain Routing )**:无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号)。 +7. **默认路由(default route)**:当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。 +8. **路由选择算法(Routing Algorithm)**:路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。 + +### 4.2. 重要知识点总结 + +1. **TCP/IP 协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限,所传送的分组可能出错、丢失、重复和失序。进程之间通信的可靠性由运输层负责** +2. 在互联网的交付有两种,一是在本网络直接交付不用经过路由器,另一种是和其他网络的间接交付,至少经过一个路由器,但最后一次一定是直接交付 +3. 分类的 IP 地址由网络号字段(指明网络)和主机号字段(指明主机)组成。网络号字段最前面的类别指明 IP 地址的类别。IP 地址是一种分等级的地址结构。IP 地址管理机构分配 IP 地址时只分配网络号,主机号由得到该网络号的单位自行分配。路由器根据目的主机所连接的网络号来转发分组。一个路由器至少连接到两个网络,所以一个路由器至少应当有两个不同的 IP 地址 +4. IP 数据报分为首部和数据两部分。首部的前一部分是固定长度,共 20 字节,是所有 IP 数据包必须具有的(源地址,目的地址,总长度等重要地段都固定在首部)。一些长度可变的可选字段固定在首部的后面。IP 首部中的生存时间给出了 IP 数据报在互联网中所能经过的最大路由器数。可防止 IP 数据报在互联网中无限制的兜圈子。 +5. **地址解析协议 ARP 把 IP 地址解析为硬件地址。ARP 的高速缓存可以大大减少网络上的通信量。因为这样可以使主机下次再与同样地址的主机通信时,可以直接从高速缓存中找到所需要的硬件地址而不需要再去以广播方式发送 ARP 请求分组** +6. 无分类域间路由选择 CIDR 是解决目前 IP 地址紧缺的一个好办法。CIDR 记法在 IP 地址后面加上斜线“/”,然后写上前缀所占的位数。前缀(或网络前缀)用来指明网络,前缀后面的部分是后缀,用来指明主机。CIDR 把前缀都相同的连续的 IP 地址组成一个“CIDR 地址块”,IP 地址分配都以 CIDR 地址块为单位。 +7. 网际控制报文协议是 IP 层的协议。ICMP 报文作为 IP 数据报的数据,加上首部后组成 IP 数据报发送出去。使用 ICMP 数据报并不是为了实现可靠传输。ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 报文的种类有两种,即 ICMP 差错报告报文和 ICMP 询问报文。 +8. **要解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议-IPv6。** IPv6 所带来的变化有 ① 更大的地址空间(采用 128 位地址)② 灵活的首部格式 ③ 改进的选项 ④ 支持即插即用 ⑤ 支持资源的预分配 ⑥IPv6 的首部改为 8 字节对齐。 +9. **虚拟专用网络 VPN 利用公用的互联网作为本机构专用网之间的通信载体。VPN 内使用互联网的专用地址。一个 VPN 至少要有一个路由器具有合法的全球 IP 地址,这样才能和本系统的另一个 VPN 通过互联网进行通信。所有通过互联网传送的数据都需要加密。** +10. MPLS 的特点是:① 支持面向连接的服务质量 ② 支持流量工程,平衡网络负载 ③ 有效的支持虚拟专用网 VPN。MPLS 在入口节点给每一个 IP 数据报打上固定长度的“标记”,然后根据标记在第二层(链路层)用硬件进行转发(在标记交换路由器中进行标记交换),因而转发速率大大加快。 + +## 5. 传输层(Transport Layer) + +![传输层](https://oss.javaguide.cn/p3-juejin/9fe85e137e7f4f03a580512200a59609~tplv-k3u1fbpfcp-zoom-1.png) + +### 5.1. 基本术语 + +1. **进程(process)**:指计算机中正在运行的程序实体。 +2. **应用进程互相通信**:一台主机的进程和另一台主机中的一个进程交换数据的过程(另外注意通信真正的端点不是主机而是主机中的进程,也就是说端到端的通信是应用进程之间的通信)。 +3. **传输层的复用与分用**:复用指发送方不同的进程都可以通过同一个运输层协议传送数据。分用指接收方的运输层在剥去报文的首部后能把这些数据正确的交付到目的应用进程。 +4. **TCP(Transmission Control Protocol)**:传输控制协议。 +5. **UDP(User Datagram Protocol)**:用户数据报协议。 + + ![TCP 和 UDP](https://oss.javaguide.cn/p3-juejin/b136e69e0b9b426782f77623dcf098bd~tplv-k3u1fbpfcp-zoom-1.png) + +6. **端口(port)**:端口的目的是为了确认对方机器的哪个进程在与自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。 +7. **停止等待协议(stop-and-wait)**:指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。 +8. **流量控制** : 就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。 +9. **拥塞控制**:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。 + +### 5.2. 重要知识点总结 + +1. **运输层提供应用进程之间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节(如网络拓补,所采用的路由选择协议等),它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。** +2. **网络层为主机提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。** +3. 运输层的两个重要协议是用户数据报协议 UDP 和传输控制协议 TCP。按照 OSI 的术语,两个对等运输实体在通信时传送的数据单位叫做运输协议数据单元 TPDU(Transport Protocol Data Unit)。但在 TCP/IP 体系中,则根据所使用的协议是 TCP 或 UDP,分别称之为 TCP 报文段或 UDP 用户数据报。 +4. **UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式。 TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务,难以避免地增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。** +5. 硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层各种协议进程与运输实体进行层间交互的一种地址。UDP 和 TCP 的首部格式中都有源端口和目的端口这两个重要字段。当运输层收到 IP 层交上来的运输层报文时,就能够根据其首部中的目的端口号把数据交付应用层的目的应用层。(两个进程之间进行通信不光要知道对方 IP 地址而且要知道对方的端口号(为了找到对方计算机中的应用进程)) +6. 运输层用一个 16 位端口号标志一个端口。端口号只有本地意义,它只是为了标志计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。协议端口号简称端口。虽然通信的终点是应用进程,但只要把所发送的报文交到目的主机的某个合适端口,剩下的工作(最后交付目的进程)就由 TCP 和 UDP 来完成。 +7. 运输层的端口号分为服务器端使用的端口号(0˜1023 指派给熟知端口,1024˜49151 是登记端口号)和客户端暂时使用的端口号(49152˜65535) +8. **UDP 的主要特点是 ① 无连接 ② 尽最大努力交付 ③ 面向报文 ④ 无拥塞控制 ⑤ 支持一对一,一对多,多对一和多对多的交互通信 ⑥ 首部开销小(只有四个字段:源端口,目的端口,长度和检验和)** +9. **TCP 的主要特点是 ① 面向连接 ② 每一条 TCP 连接只能是一对一的 ③ 提供可靠交付 ④ 提供全双工通信 ⑤ 面向字节流** +10. **TCP 用主机的 IP 地址加上主机上的端口号作为 TCP 连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP 地址:端口号)来表示。每一条 TCP 连接唯一地被通信两端的两个端点所确定。** +11. 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 +12. 为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。 +13. 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 +14. TCP 报文段的前 20 个字节是固定的,其后有 40 字节长度的可选字段。如果加入可选字段后首部长度不是 4 的整数倍字节,需要在再在之后用 0 填充。因此,TCP 首部的长度取值为 20+4n 字节,最长为 60 字节。 +15. **TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口的前沿通常是不断向前移动的。一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。** +16. 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 +17. **为了进行拥塞控制,TCP 发送方要维持一个拥塞窗口 cwnd 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。** +18. **TCP 的拥塞控制采用了四种算法,即慢开始,拥塞避免,快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。** +19. 运输连接的三个阶段,即:连接建立,数据传送和连接释放。 +20. **主动发起 TCP 连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫做服务器。TCP 连接采用三报文握手机制。服务器要确认用户的连接请求,然后客户要对服务器的确认进行确认。** +21. TCP 的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了 TCP 连接 + +### 5.3. 补充(重要) + +以下知识点需要重点关注: + +1. 端口和套接字的意义 +2. UDP 和 TCP 的区别以及两者的应用场景 +3. 在不可靠的网络上实现可靠传输的工作原理,停止等待协议和 ARQ 协议 +4. TCP 的滑动窗口,流量控制,拥塞控制和连接管理 +5. TCP 的三次握手,四次挥手机制 + +## 6. 应用层(Application Layer) + +![应用层](https://oss.javaguide.cn/p3-juejin/0f13f0ee13b24af7bdddf56162eb6602~tplv-k3u1fbpfcp-zoom-1.png) + +### 6.1. 基本术语 + +1. **域名系统(DNS)**:域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。 + + ![](https://oss.javaguide.cn/p3-juejin/e7da4b07947f4c0094d46dc96a067df0~tplv-k3u1fbpfcp-zoom-1.png) + +

https://www.seobility.net/en/wiki/HTTP_headers

+ +2. **文件传输协议(FTP)**:FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 + + ![FTP工作过程](https://oss.javaguide.cn/p3-juejin/f3f2caaa361045a38fb89bb9fee15bd3~tplv-k3u1fbpfcp-zoom-1.png) + +3. **简单文件传输协议(TFTP)**:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。 +4. **远程终端协议(TELNET)**:Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。 +5. **万维网(WWW)**:WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。 +6. **万维网的大致工作工程:** + + ![万维网的大致工作工程](https://oss.javaguide.cn/p3-juejin/ba628fd37fdc4ba59c1a74eae32e03b1~tplv-k3u1fbpfcp-zoom-1.jpeg) + +7. **统一资源定位符(URL)**:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 +8. **超文本传输协议(HTTP)**:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。 + + HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示: + + ![](https://oss.javaguide.cn/p3-juejin/8e3efca026654874bde8be88c96e1783~tplv-k3u1fbpfcp-zoom-1.jpeg) + +9. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。 +10. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。 + + ![一个电子邮件被发送的过程](https://oss.javaguide.cn/p3-juejin/2bdccb760474435aae52559f2ef9652f~tplv-k3u1fbpfcp-zoom-1.png) + +

https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/

+ +11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。 + +12. **垂直搜索引擎**:垂直搜索引擎是针对某一个行业的专业搜索引擎,是搜索引擎的细分和延伸,是对网页库中的某类专门的信息进行一次整合,定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式,通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。其特点就是“专、精、深”,且具有行业色彩,相比较通用搜索引擎的海量信息无序化,垂直搜索引擎则显得更加专注、具体和深入。 +13. **全文索引** :全文索引技术是目前搜索引擎的关键技术。试想在 1M 大小的文件中搜索一个词,可能需要几秒,在 100M 的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。 +14. **目录索引**:目录索引( search index/directory),顾名思义就是将网站分门别类地存放在相应的目录中,因此用户在查询信息时,可选择关键词搜索,也可按分类目录逐层查找。 + +### 6.2. 重要知识点总结 + +1. 文件传输协议(FTP)使用 TCP 可靠的运输服务。FTP 使用客户服务器方式。一个 FTP 服务器进程可以同时为多个用户提供服务。在进行文件传输时,FTP 的客户和服务器之间要先建立两个并行的 TCP 连接:控制连接和数据连接。实际用于传输文件的是数据连接。 +2. 万维网客户程序与服务器之间进行交互使用的协议是超文本传输协议 HTTP。HTTP 使用 TCP 连接进行可靠传输。但 HTTP 本身是无连接、无状态的。HTTP/1.1 协议使用了持续连接(分为非流水线方式和流水线方式) +3. 电子邮件把邮件发送到收件人使用的邮件服务器,并放在其中的收件人邮箱中,收件人可随时上网到自己使用的邮件服务器读取,相当于电子邮箱。 +4. An email system has three important components: user agent, mail server, and email protocol (including email sending protocols, such as SMTP, and email reading protocols, such as POP3 and IMAP). Both the user agent and the mail server must run these protocols. + +### 6.3. Supplement (Important) + +The following knowledge points need to be focused on: + +1. Common protocols at the application layer (focusing on the HTTP protocol) +2. Domain name system-resolve IP address from domain name +3. The general process of visiting a website +4. System call and application programming interface concepts + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/dns.en.md b/docs_en/cs-basics/network/dns.en.md new file mode 100644 index 00000000000..08af0ddb54d --- /dev/null +++ b/docs_en/cs-basics/network/dns.en.md @@ -0,0 +1,115 @@ +--- +title: DNS 域名系统详解(应用层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: DNS,域名解析,递归查询,迭代查询,缓存,权威DNS,端口53,UDP + - - meta + - name: description + content: 详解 DNS 的层次结构与解析流程,覆盖递归/迭代、缓存与权威服务器,明确应用层端口与性能优化要点。 +--- + +DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是**域名和 IP 地址的映射问题**。 + +![DNS:域名系统](https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png) + +在实际使用中,有一种情况下,浏览器是可以不必动用 DNS 就可以获知域名和 IP 地址的映射的。浏览器在本地会维护一个`hosts`列表,一般来说浏览器要先查看要访问的域名是否在`hosts`列表中,如果有的话,直接提取对应的 IP 地址记录,就好了。如果本地`hosts`列表内没有域名-IP 对应记录的话,那么 DNS 就闪亮登场了。 + +目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,基于 UDP 协议之上,端口为 53** 。 + +![TCP/IP 各层协议概览](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png) + +## DNS 服务器 + +DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一): + +- 根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。 +- 顶级域 DNS 服务器(TLD 服务器)。顶级域是指域名的后缀,如`com`、`org`、`net`和`edu`等。国家也有自己的顶级域,如`uk`、`fr`和`ca`。TLD 服务器提供了权威 DNS 服务器的 IP 地址。 +- 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。 +- 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构。 + +世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 600 多台,未来还会继续增加。 + +## DNS 工作流程 + +以下图为例,介绍 DNS 的查询解析过程。DNS 的查询解析过程分为两种模式: + +- **迭代** +- **递归** + +下图是实践中常采用的方式,从请求主机到本地 DNS 服务器的查询是递归的,其余的查询时迭代的。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/DNS-process.png) + +现在,主机`cis.poly.edu`想知道`gaia.cs.umass.edu`的 IP 地址。假设主机`cis.poly.edu`的本地 DNS 服务器为`dns.poly.edu`,并且`gaia.cs.umass.edu`的权威 DNS 服务器为`dns.cs.umass.edu`。 + +1. 首先,主机`cis.poly.edu`向本地 DNS 服务器`dns.poly.edu`发送一个 DNS 请求,该查询报文包含被转换的域名`gaia.cs.umass.edu`。 +2. 本地 DNS 服务器`dns.poly.edu`检查本机缓存,发现并无记录,也不知道`gaia.cs.umass.edu`的 IP 地址该在何处,不得不向根服务器发送请求。 +3. 根服务器注意到请求报文中含有`edu`顶级域,因此告诉本地 DNS,你可以向`edu`的 TLD DNS 发送请求,因为目标域名的 IP 地址很可能在那里。 +4. 本地 DNS 获取到了`edu`的 TLD DNS 服务器地址,向其发送请求,询问`gaia.cs.umass.edu`的 IP 地址。 +5. `edu`的 TLD DNS 服务器仍不清楚请求域名的 IP 地址,但是它注意到该域名有`umass.edu`前缀,因此返回告知本地 DNS,`umass.edu`的权威服务器可能记录了目标域名的 IP 地址。 +6. 这一次,本地 DNS 将请求发送给权威 DNS 服务器`dns.cs.umass.edu`。 +7. 终于,由于`gaia.cs.umass.edu`向权威 DNS 服务器备案过,在这里有它的 IP 地址记录,权威 DNS 成功地将 IP 地址返回给本地 DNS。 +8. 最后,本地 DNS 获取到了目标域名的 IP 地址,将其返回给请求主机。 + +除了迭代式查询,还有一种递归式查询如下图,具体过程和上述类似,只是顺序有所不同。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/DNS-process2.png) + +另外,DNS 的缓存位于本地 DNS 服务器。由于全世界的根服务器甚少,只有 600 多台,分为 13 组,且顶级域的数量也在一个可数的范围内,因此本地 DNS 通常已经缓存了很多 TLD DNS 服务器,所以在实际查找过程中,无需访问根服务器。根服务器通常是被跳过的,不请求的。这样可以提高 DNS 查询的效率和速度,减少对根服务器和 TLD 服务器的负担。 + +## DNS 报文格式 + +DNS 的报文格式如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/DNS-packet.png) + +DNS 报文分为查询和回答报文,两种形式的报文结构相同。 + +- 标识符。16 比特,用于标识该查询。这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接收到的回答。 +- 标志。1 比特的”查询/回答“标识位,`0`表示查询报文,`1`表示回答报文;1 比特的”权威的“标志位(当某 DNS 服务器是所请求名字的权威 DNS 服务器时,且是回答报文,使用”权威的“标志);1 比特的”希望递归“标志位,显式地要求执行递归查询;1 比特的”递归可用“标志位,用于回答报文中,表示 DNS 服务器支持递归查询。 +- 问题数、回答 RR 数、权威 RR 数、附加 RR 数。分别指示了后面 4 类数据区域出现的数量。 +- 问题区域。包含正在被查询的主机名字,以及正被询问的问题类型。 +- 回答区域。包含了对最初请求的名字的资源记录。**在回答报文的回答区域中可以包含多条 RR,因此一个主机名能够有多个 IP 地址。** +- 权威区域。包含了其他权威服务器的记录。 +- 附加区域。包含了其他有帮助的记录。 + +## DNS 记录 + +DNS 服务器在响应查询时,需要查询自己的数据库,数据库中的条目被称为 **资源记录(Resource Record,RR)** 。RR 提供了主机名到 IP 地址的映射。RR 是一个包含了`Name`, `Value`, `Type`, `TTL`四个字段的四元组。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/20210506174303797.png) + +`TTL`是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间。 + +`Name`和`Value`字段的取值取决于`Type`: + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/20210506170307897.png) + +- 如果`Type=A`,则`Name`是主机名信息,`Value` 是该主机名对应的 IP 地址。这样的 RR 记录了一条主机名到 IP 地址的映射。 +- 如果 `Type=AAAA` (与 `A` 记录非常相似),唯一的区别是 A 记录使用的是 IPv4,而 `AAAA` 记录使用的是 IPv6。 +- If `Type=CNAME` (Canonical Name Record, real name record), then `Value` is the canonical host name corresponding to the host with the alias `Name`. The `Value` value is the canonical host name. `CNAME` records map one hostname to another hostname. A `CNAME` record is used to create an alias for an existing `A` record. Examples below. +- If `Type=NS`, then `Name` is a domain and `Value` is the hostname of an authoritative DNS server that knows how to obtain IP addresses for hosts in that domain. Typically such RRs are issued by the TLD server. +- If `Type=MX`, then `Value` is the canonical hostname of the individual mail server named `Name`. Now that there are `MX` records, the mail server can use the same alias as other servers. To obtain the canonical hostname of a mail server, request an MX record; to obtain the canonical hostname of another server, request a CNAME record. + +`CNAME` records always point to another domain name, not an IP address. Assume the following DNS zone: + +```plain +NAME TYPE VALUE +-------------------------------------------------- +bar.example.com. CNAME foo.example.com. +foo.example.com.A 192.0.2.23 +``` + +When the user queries `bar.example.com`, the DNS Server actually returns the IP address of `foo.example.com`. + +## Reference + +- DNS server type: +- DNS Message Resource Record Field Formats: +- Understanding Different Types of Record in DNS Server: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/http-status-codes.en.md b/docs_en/cs-basics/network/http-status-codes.en.md new file mode 100644 index 00000000000..09a0ddd7248 --- /dev/null +++ b/docs_en/cs-basics/network/http-status-codes.en.md @@ -0,0 +1,83 @@ +--- +title: Summary of common HTTP status codes (application layer) +category: Computer Basics +tag: + - computer network +head: + - - meta + - name: keywords + content: HTTP status code, 2xx, 3xx, 4xx, 5xx, redirection, error code, 201 Created, 204 No Content + - - meta + - name: description + content: Summarizes the meanings and usage scenarios of common HTTP status codes, emphasizing confusion points such as 201/204 to improve interface design and debugging efficiency. +--- + +HTTP status codes are used to describe the results of HTTP requests. For example, 2xx means that the request was successfully processed. + +![Common HTTP status codes](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-status-code.png) + +### 1xx Informational (informational status code) + +Compared with other types of status codes, you will most likely not encounter 1xx, so you can skip it here. + +### 2xx Success (success status code) + +- **200 OK**: The request was successfully processed. For example, send an HTTP request to query user data to the server, and the server returns the user data correctly. This is the most common HTTP status code we usually have. +- **201 Created**: The request was successfully processed and a new resource was created on the server. For example, create a new user via a POST request. +- **202 Accepted**: The server has received the request, but has not yet processed it. For example, if you send a request that takes a long time to be processed by the server (such as report generation, Excel export), the server receives the request but has not yet completed the processing. +- **204 No Content**: The server has successfully processed the request, but did not return any content. For example, a request is sent to delete a user, and the server successfully handles the deletion but returns nothing. + +🐛 Correction (see: [issue#2458](https://github.com/Snailclimb/JavaGuide/issues/2458)): The 201 Created status code is more precisely the creation of one or more new resources, please refer to: . + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/rfc9110-201-created.png) + +Here is a special mention of the 204 status code, which is not seen many times in study/work. + +[HTTP RFC 2616 description of 204 status code](https://tools.ietf.org/html/rfc2616#section-10.2.5) is as follows: + +> The server has fulfilled the request but does not need to return an +> entity-body, and might want to return updated metainformation. The +> response MAY include new or updated metainformation in the form of +> entity-headers, which if present SHOULD be associated with the +> requested variant. +> +> If the client is a user agent, it SHOULD NOT change its document view +> from that which caused the request to be sent. This response is +> primarily intended to allow input for actions to take place without +> causing a change to the user agent's active document view, although +> any new or updated metainformation SHOULD be applied to the document +> currently in the user agent's active view. +> +> The 204 response MUST NOT include a message-body, and thus is always +> terminated by the first empty line after the header fields. + +Simply put, the 204 status code describes a scenario where after we send an HTTP request to the server, we only focus on whether the processing result is successful. In other words, what we need is a result: true/false. + +For example: you want to chase a girl, you ask the girl: "Can I chase you?", the girl replies: "Okay!". We can easily understand the 204 status code by treating this girl as a server. + +### 3xx Redirection (redirect status code) + +- **301 Moved Permanently**: The resource has been permanently redirected. For example, the URL of your website has been changed. +- **302 Found**: The resource was temporarily redirected. For example, some resources on your website are temporarily transferred to another URL. + +### 4xx Client Error (client error status code) + +- **400 Bad Request**: There was a problem with the HTTP request sent. For example, the request parameters are illegal and the request method is wrong. +- **401 Unauthorized**: Unauthenticated but requesting resources that require authentication before accessing. +- **403 Forbidden**: Directly reject the HTTP request and do not process it. Generally used for illegal requests. +- **404 Not Found**: The resource you requested was not found on the server. For example, if you request information about a certain user, the server does not find the specified user. +- **409 Conflict**: Indicates that the requested resource conflicts with the current status of the server and the request cannot be processed. + +### 5xx Server Error (server error status code) + +- **500 Internal Server Error**: There is a problem on the server side (usually there is a bug on the server side). For example, your server suddenly throws an exception when processing a request, but the exception is not handled correctly on the server. +- **502 Bad Gateway**: Our gateway forwards the request to the server, but the server returns an error response. + +### Reference + +- +- +- +- + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/http-vs-https.en.md b/docs_en/cs-basics/network/http-vs-https.en.md new file mode 100644 index 00000000000..65f7b24e05d --- /dev/null +++ b/docs_en/cs-basics/network/http-vs-https.en.md @@ -0,0 +1,149 @@ +--- +title: HTTP vs HTTPS(应用层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: HTTP,HTTPS,SSL,TLS,加密,认证,端口,安全性,握手流程 + - - meta + - name: description + content: 对比 HTTP 与 HTTPS 的协议与安全机制,解析 SSL/TLS 工作原理与握手流程,明确应用层安全落地细节。 +--- + +## HTTP 协议 + +### HTTP 协议介绍 + +HTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。顾名思义,HTTP 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。 + +并且,HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。 + +### HTTP 协议通信过程 + +HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80. 通信过程主要如下: + +1. 服务器在 80 端口等待客户的请求。 +2. 浏览器发起到服务器的 TCP 连接(创建套接字 Socket)。 +3. 服务器接收来自浏览器的 TCP 连接。 +4. 浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。 +5. 关闭 TCP 连接。 + +### HTTP 协议优点 + +扩展性强、速度快、跨平台支持性好。 + +## HTTPS 协议 + +### HTTPS 协议介绍 + +HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443. + +HTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。 + +### HTTPS 协议优点 + +保密性好、信任度高。 + +## HTTPS 的核心—SSL/TLS 协议 + +HTTPS 之所以能达到较高的安全性要求,就是结合了 SSL/TLS 和 TCP 协议,对通信数据进行加密,解决了 HTTP 数据透明的问题。接下来重点介绍一下 SSL/TLS 的工作原理。 + +### SSL 和 TLS 的区别? + +**SSL 和 TLS 没有太大的区别。** + +SSL 指安全套接字协议(Secure Sockets Layer),首次发布与 1996 年。SSL 的首次发布其实已经是他的 3.0 版本,SSL 1.0 从未面世,SSL 2.0 则具有较大的缺陷(DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption)。很快,在 1999 年,SSL 3.0 进一步升级,**新版本被命名为 TLS 1.0**。因此,TLS 是基于 SSL 之上的,但由于习惯叫法,通常把 HTTPS 中的核心加密协议混称为 SSL/TLS。 + +### SSL/TLS 的工作原理 + +#### 非对称加密 + +SSL/TLS 的核心要素是**非对称加密**。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。可以设想一个场景, + +> 在某个自助邮局,每个通信信道都是一个邮箱,每一个邮箱所有者都在旁边立了一个牌子,上面挂着一把钥匙:这是我的公钥,发送者请将信件放入我的邮箱,并用公钥锁好。 +> +> 但是公钥只能加锁,并不能解锁。解锁只能由邮箱的所有者——因为只有他保存着私钥。 +> +> 这样,通信信息就不会被其他人截获了,这依赖于私钥的保密性。 + +![](./images/http-vs-https/public-key-cryptography.png) + +非对称加密的公钥和私钥需要采用一种复杂的数学机制生成(密码学认为,为了较高的安全性,尽量不要自己创造加密方案)。公私钥对的生成算法依赖于单向陷门函数。 + +> 单向函数:已知单向函数 f,给定任意一个输入 x,易计算输出 y=f(x);而给定一个输出 y,假设存在 f(x)=y,很难根据 f 来计算出 x。 +> +> 单向陷门函数:一个较弱的单向函数。已知单向陷门函数 f,陷门 h,给定任意一个输入 x,易计算出输出 y=f(x;h);而给定一个输出 y,假设存在 f(x;h)=y,很难根据 f 来计算出 x,但可以根据 f 和 h 来推导出 x。 + +![单向函数](./images/http-vs-https/OWF.png) + +上图就是一个单向函数(不是单项陷门函数),假设有一个绝世秘籍,任何知道了这个秘籍的人都可以把苹果汁榨成苹果,那么这个秘籍就是“陷门”了吧。 + +在这里,函数 f 的计算方法相当于公钥,陷门 h 相当于私钥。公钥 f 是公开的,任何人对已有输入,都可以用 f 加密,而要想根据加密信息还原出原信息,必须要有私钥才行。 + +#### 对称加密 + +使用 SSL/TLS 进行通信的双方需要使用非对称加密方案来通信,但是非对称加密设计了较为复杂的数学算法,在实际通信过程中,计算的代价较高,效率太低,因此,SSL/TLS 实际对消息的加密使用的是对称加密。 + +> 对称加密:通信双方共享唯一密钥 k,加解密算法已知,加密方利用密钥 k 加密,解密方利用密钥 k 解密,保密性依赖于密钥 k 的保密性。 + +![](./images/http-vs-https/symmetric-encryption.png) + +对称加密的密钥生成代价比公私钥对的生成代价低得多,那么有的人会问了,为什么 SSL/TLS 还需要使用非对称加密呢?因为对称加密的保密性完全依赖于密钥的保密性。在双方通信之前,需要商量一个用于对称加密的密钥。我们知道网络通信的信道是不安全的,传输报文对任何人是可见的,密钥的交换肯定不能直接在网络信道中传输。因此,使用非对称加密,对对称加密的密钥进行加密,保护该密钥不在网络信道中被窃听。这样,通信双方只需要一次非对称加密,交换对称加密的密钥,在之后的信息通信中,使用绝对安全的密钥,对信息进行对称加密,即可保证传输消息的保密性。 + +#### 公钥传输的信赖性 + +SSL/TLS 介绍到这里,了解信息安全的朋友又会想到一个安全隐患,设想一个下面的场景: + +> 客户端 C 和服务器 S 想要使用 SSL/TLS 通信,由上述 SSL/TLS 通信原理,C 需要先知道 S 的公钥,而 S 公钥的唯一获取途径,就是把 S 公钥在网络信道中传输。要注意网络信道通信中有几个前提: +> +> 1. 任何人都可以捕获通信包 +> 2. 通信包的保密性由发送者设计 +> 3. 保密算法设计方案默认为公开,而(解密)密钥默认是安全的 +> +> 因此,假设 S 公钥不做加密,在信道中传输,那么很有可能存在一个攻击者 A,发送给 C 一个诈包,假装是 S 公钥,其实是诱饵服务器 AS 的公钥。当 C 收获了 AS 的公钥(却以为是 S 的公钥),C 后续就会使用 AS 公钥对数据进行加密,并在公开信道传输,那么 A 将捕获这些加密包,用 AS 的私钥解密,就截获了 C 本要给 S 发送的内容,而 C 和 S 二人全然不知。 +> +> 同样的,S 公钥即使做加密,也难以避免这种信任性问题,C 被 AS 拐跑了! + +![](./images/http-vs-https/attack1.png) + +为了公钥传输的信赖性问题,第三方机构应运而生——证书颁发机构(CA,Certificate Authority)。CA 默认是受信任的第三方。CA 会给各个服务器颁发证书,证书存储在服务器上,并附有 CA 的**电子签名**(见下节)。 + +当客户端(浏览器)向服务器发送 HTTPS 请求时,一定要先获取目标服务器的证书,并根据证书上的信息,检验证书的合法性。一旦客户端检测到证书非法,就会发生错误。客户端获取了服务器的证书后,由于证书的信任性是由第三方信赖机构认证的,而证书上又包含着服务器的公钥信息,客户端就可以放心的信任证书上的公钥就是目标服务器的公钥。 + +#### 数字签名 + +好,到这一小节,已经是 SSL/TLS 的尾声了。上一小节提到了数字签名,数字签名要解决的问题,是防止证书被伪造。第三方信赖机构 CA 之所以能被信赖,就是 **靠数字签名技术** 。 + +数字签名,是 CA 在给服务器颁发证书时,使用散列+加密的组合技术,在证书上盖个章,以此来提供验伪的功能。具体行为如下: + +> CA 知道服务器的公钥,对证书采用散列技术生成一个摘要。CA 使用 CA 私钥对该摘要进行加密,并附在证书下方,发送给服务器。 +> +> 现在服务器将该证书发送给客户端,客户端需要验证该证书的身份。客户端找到第三方机构 CA,获知 CA 的公钥,并用 CA 公钥对证书的签名进行解密,获得了 CA 生成的摘要。 +> +> The client performs the same hashing process on the certificate data (including the server's public key) to obtain a digest, and compares the digest with the digest previously decoded from the signature. If they are the same, the authentication succeeds; otherwise, the verification fails. + +![](./images/http-vs-https/digital-signature.png) + +In summary, the public key transfer mechanism with certificates is as follows: + +1. There is a server S, a client C, and a third-party trust authority CA. +2. S trusts the CA. The CA knows S’s public key, and the CA issues a certificate to S. And attach a cryptographic signature of the message digest with the CA's private key. +3. S obtains a certificate issued by the CA and passes the certificate to C. +4. C obtains S’s certificate, trusts the CA and knows the CA public key, uses the CA public key to decrypt the signature on S’s certificate, and hashes the message to obtain the digest. Compare summaries to verify the authenticity of the S-certificate. +5. Trust S's public key (in S's certificate) if C verifies that S's certificate is authentic. + +![](./images/http-vs-https/public-key-transmission.png) + +For digital signatures, what I’m talking about here is relatively simple. If you don’t understand it, I strongly recommend you to watch the video [Principles of Digital Signatures and Digital Certificates] (https://www.bilibili.com/video/BV18N411X7ty/). This is the clearest explanation I have ever seen. + +![](https://oss.javaguide.cn/github/javaguide/image-20220321121814946.png) + +## Summary + +- **Port number**: The default for HTTP is 80, and the default for HTTPS is 443. +- **URL Prefix**: The URL prefix of HTTP is `http://`, and the URL prefix of HTTPS is `https://`. +- **Security and Resource Consumption**: The HTTP protocol runs on top of TCP, all transmitted content is clear text, and neither the client nor the server can verify the identity of the other party. HTTPS is an HTTP protocol that runs on top of SSL/TLS, which runs on top of TCP. All transmitted content is encrypted using symmetric encryption, but the symmetric encryption key is asymmetrically encrypted using a server-side certificate. Therefore, HTTP is not as secure as HTTPS, but HTTPS consumes more server resources than HTTP. + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/http1.0-vs-http1.1.en.md b/docs_en/cs-basics/network/http1.0-vs-http1.1.en.md new file mode 100644 index 00000000000..affb03ca907 --- /dev/null +++ b/docs_en/cs-basics/network/http1.0-vs-http1.1.en.md @@ -0,0 +1,175 @@ +--- +title: HTTP 1.0 vs HTTP 1.1(应用层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: HTTP/1.0,HTTP/1.1,长连接,管道化,缓存,状态码,Host,带宽优化 + - - meta + - name: description + content: 细致对比 HTTP/1.0 与 HTTP/1.1 的协议差异,涵盖长连接、管道化、缓存与状态码增强等关键变更与实践影响。 +--- + +这篇文章会从下面几个维度来对比 HTTP 1.0 和 HTTP 1.1: + +- 响应状态码 +- 缓存处理 +- 连接方式 +- Host 头处理 +- 带宽优化 + +## 响应状态码 + +HTTP/1.0 仅定义了 16 种状态码。HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 + +## 缓存处理 + +缓存技术通过避免用户与源服务器的频繁交互,节约了大量的网络带宽,降低了用户接收信息的延迟。 + +### HTTP/1.0 + +HTTP/1.0 提供的缓存机制非常简单。服务器端使用`Expires`标签来标志(时间)一个响应体,在`Expires`标志时间内的请求,都会获得该响应体缓存。服务器端在初次返回给客户端的响应体中,有一个`Last-Modified`标签,该标签标记了被请求资源在服务器端的最后一次修改。在请求头中,使用`If-Modified-Since`标签,该标签标志一个时间,意为客户端向服务器进行问询:“该时间之后,我要请求的资源是否有被修改过?”通常情况下,请求头中的`If-Modified-Since`的值即为上一次获得该资源时,响应体中的`Last-Modified`的值。 + +如果服务器接收到了请求头,并判断`If-Modified-Since`时间后,资源确实没有修改过,则返回给客户端一个`304 not modified`响应头,表示”缓冲可用,你从浏览器里拿吧!”。 + +如果服务器判断`If-Modified-Since`时间后,资源被修改过,则返回给客户端一个`200 OK`的响应体,并附带全新的资源内容,表示”你要的我已经改过的,给你一份新的”。 + +![HTTP1.0cache1](./images/http-vs-https/HTTP1.0cache1.png) + +![HTTP1.0cache2](./images/http-vs-https/HTTP1.0cache2.png) + +### HTTP/1.1 + +HTTP/1.1 的缓存机制在 HTTP/1.0 的基础上,大大增加了灵活性和扩展性。基本工作原理和 HTTP/1.0 保持不变,而是增加了更多细致的特性。其中,请求头中最常见的特性就是`Cache-Control`,详见 MDN Web 文档 [Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control). + +## 连接方式 + +**HTTP/1.0 默认使用短连接** ,也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如 JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 TCP 连接,这样就会导致有大量的“握手报文”和“挥手报文”占用了带宽。 + +**为了解决 HTTP/1.0 存在的资源浪费的问题, HTTP/1.1 优化为默认长连接模式 。** 采用长连接模式的请求报文会通知服务端:“我向你请求连接,并且连接成功建立后,请不要关闭”。因此,该 TCP 连接将持续打开,为后续的客户端-服务端的数据交互服务。也就是说在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。 + +如果 TCP 连接一直保持的话也是对资源的浪费,因此,一些服务器软件(如 Apache)还会支持超时时间的时间。在超时时间之内没有新的请求达到,TCP 连接才会被关闭。 + +有必要说明的是,HTTP/1.0 仍提供了长连接选项,即在请求头中加入`Connection: Keep-alive`。同样的,在 HTTP/1.1 中,如果不希望使用长连接选项,也可以在请求头中加入`Connection: close`,这样会通知服务器端:“我不需要长连接,连接成功后即可关闭”。 + +**HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。** + +**实现长连接需要客户端和服务端都支持长连接。** + +## Host 头处理 + +域名系统(DNS)允许多个主机名绑定到同一个 IP 地址上,但是 HTTP/1.0 并没有考虑这个问题,假设我们有一个资源 URL 是 的请求报文中,将会请求的是`GET /home.html HTTP/1.0`.也就是不会加入主机名。这样的报文送到服务器端,服务器是理解不了客户端想请求的真正网址。 + +因此,HTTP/1.1 在请求头中加入了`Host`字段。加入`Host`字段的报文头部将会是: + +```plain +GET /home.html HTTP/1.1 +Host: example1.org +``` + +这样,服务器端就可以确定客户端想要请求的真正的网址了。 + +## 带宽优化 + +### 范围请求 + +HTTP/1.1 引入了范围请求(range request)机制,以避免带宽的浪费。当客户端想请求一个文件的一部分,或者需要继续下载一个已经下载了部分但被终止的文件,HTTP/1.1 可以在请求中加入`Range`头部,以请求(并只能请求字节型数据)数据的一部分。服务器端可以忽略`Range`头部,也可以返回若干`Range`响应。 + +`206 (Partial Content)` 状态码的主要作用是确保客户端和代理服务器能正确识别部分内容响应,避免将其误认为完整资源并错误地缓存。这对于正确处理范围请求和缓存管理非常重要。 + +一个典型的 HTTP/1.1 范围请求示例: + +```bash +# 获取一个文件的前 1024 个字节 +GET /z4d4kWk.jpg HTTP/1.1 +Host: i.imgur.com +Range: bytes=0-1023 +``` + +`206 Partial Content` 响应: + +```bash + +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-1023/146515 +Content-Length: 1024 +… +(二进制内容) +``` + +简单解释一下 HTTP 范围响应头部中的字段: + +- **`Content-Range` 头部**:指示返回数据在整个资源中的位置,包括起始和结束字节以及资源的总长度。例如,`Content-Range: bytes 0-1023/146515` 表示服务器端返回了第 0 到 1023 字节的数据(共 1024 字节),而整个资源的总长度是 146,515 字节。 +- **`Content-Length` 头部**:指示此次响应中实际传输的字节数。例如,`Content-Length: 1024` 表示服务器端传输了 1024 字节的数据。 + +`Range` 请求头不仅可以请求单个字节范围,还可以一次性请求多个范围。这种方式被称为“多重范围请求”(multiple range requests)。 + +客户端想要获取资源的第 0 到 499 字节以及第 1000 到 1499 字节: + +```bash +GET /path/to/resource HTTP/1.1 +Host: example.com +Range: bytes=0-499,1000-1499 +``` + +服务器端返回多个字节范围,每个范围的内容以分隔符分开: + +```bash +HTTP/1.1 206 Partial Content +Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5 +Content-Length: 376 + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 0-99/2000 + +(第 0 到 99 字节的数据块) + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 500-599/2000 + +(第 500 到 599 字节的数据块) + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 1000-1099/2000 + +(第 1000 到 1099 字节的数据块) + +--3d6b6a416f9b5-- +``` + +### Status code 100 + +A new status code `100` was added to HTTP/1.1. The usage scenario of this status code is that there are some large file requests, and the server may not be willing to respond to such requests. At this time, the status code `100` can be used to indicate whether the request will be responded to normally. The process is as follows: + +![HTTP1.1continue1](./images/http-vs-https/HTTP1.1continue1.png) + +![HTTP1.1continue2](./images/http-vs-https/HTTP1.1continue2.png) + +However, in HTTP/1.0, there is no `100 (Continue)` status code. To trigger this mechanism, you can send an `Expect` header containing a `100-continue` value. + +### Compression + +Data in many formats are pre-compressed during transmission. Compression of data can significantly optimize bandwidth utilization. However, HTTP/1.0 does not provide many options for data compression, does not support the selection of compression details, and cannot distinguish between end-to-end compression or hop-by-hop compression. + +HTTP/1.1 makes a distinction between content-codings and transfer-codings. Content encoding is always end-to-end and transport encoding is always hop-by-hop. + +HTTP/1.0 includes the `Content-Encoding` header to encode messages end-to-end. HTTP/1.1 added the `Transfer-Encoding` header, which can perform hop-by-hop transfer encoding of messages. HTTP/1.1 also added the `Accept-Encoding` header, which is used by the client to indicate what kind of content encoding it can handle. + +## Summary + +1. **Connection method**: HTTP 1.0 is a short connection, and HTTP 1.1 supports long connections. +1. **Status response codes**: A large number of new status codes have been added to HTTP/1.1, including 24 new error response status codes alone. For example, `100 (Continue)` - a warm-up request before requesting a large resource, `206 (Partial Content)` - the identification code of the range request, `409 (Conflict)` - the request conflicts with the specifications of the current resource, `410 (Gone)` - the resource has been permanently transferred and does not have any known forwarding address. +1. **Cache processing**: In HTTP1.0, If-Modified-Since, Expires in the header are mainly used as the standard for cache judgment. HTTP1.1 introduces more cache control strategies such as Entity tag, If-Unmodified-Since, If-Match, If-None-Match and more optional cache headers to control the cache strategy. +1. **Bandwidth optimization and use of network connections**: In HTTP1.0, there are some phenomena of wasting bandwidth. For example, the client only needs a part of an object, but the server sends the entire object, and does not support the resumption function. HTTP1.1 introduces the range header field in the request header, which allows only a certain part of the resource to be requested, that is, the return code is 206 (Partial Content). This facilitates developers to freely choose to make full use of bandwidth and connections. +1. **Host header processing**: HTTP/1.1 adds the `Host` field to the request header. + +## References + +[Key differences between HTTP/1.0 and HTTP/1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/nat.en.md b/docs_en/cs-basics/network/nat.en.md new file mode 100644 index 00000000000..561386f91d1 --- /dev/null +++ b/docs_en/cs-basics/network/nat.en.md @@ -0,0 +1,68 @@ +--- +title: NAT 协议详解(网络层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: NAT,地址转换,端口映射,LAN,WAN,连接跟踪,DHCP + - - meta + - name: description + content: 解析 NAT 的地址转换与端口映射机制,结合 LAN/WAN 通信与转换表,理解家庭与企业网络的实践细节。 +--- + +## 应用场景 + +**NAT 协议(Network Address Translation)** 的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,Local Area Network,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(Wide Area Network,WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。 + +这个场景其实不难理解。随着一个个小型办公室、家庭办公室(Small Office, Home Office, SOHO)的出现,为了管理这些 SOHO,一个个子网被设计出来,从而在整个 Internet 中的主机数量将非常庞大。如果每个主机都有一个“绝对唯一”的 IP 地址,那么 IPv4 地址的表达能力可能很快达到上限($2^{32}$)。因此,实际上,SOHO 子网中的 IP 地址是“相对的”,这在一定程度上也缓解了 IPv4 地址的分配压力。 + +SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器扮演。路由器的 LAN 一侧管理着一个小子网,而它的 WAN 接口才是真正参与到 Internet 中的接口,也就有一个“绝对唯一的地址”。NAT 协议,正是在 LAN 中的主机在与 LAN 外界通信时,起到了地址转换的关键作用。 + +## 细节 + +![NAT 协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/nat-demo.png) + +假设当前场景如上图。中间是一个路由器,它的右侧组织了一个 LAN,网络号为`10.0.0/24`。LAN 侧接口的 IP 地址为`10.0.0.4`,并且该子网内有至少三台主机,分别是`10.0.0.1`,`10.0.0.2`和`10.0.0.3`。路由器的左侧连接的是 WAN,WAN 侧接口的 IP 地址为`138.76.29.7`。 + +首先,针对以上信息,我们有如下事实需要说明: + +1. 路由器的右侧子网的网络号为`10.0.0/24`,主机号为`10.0.0/8`,三台主机地址,以及路由器的 LAN 侧接口地址,均由 DHCP 协议规定。而且,该 DHCP 运行在路由器内部(路由器自维护一个小 DHCP 服务器),从而为子网内提供 DHCP 服务。 +2. 路由器的 WAN 侧接口地址同样由 DHCP 协议规定,但该地址是路由器从 ISP(网络服务提供商)处获得,也就是该 DHCP 通常运行在路由器所在区域的 DHCP 服务器上。 + +现在,路由器内部还运行着 NAT 协议,从而为 LAN-WAN 间通信提供地址转换服务。为此,一个很重要的结构是 **NAT 转换表**。为了说明 NAT 的运行细节,假设有以下请求发生: + +1. 主机`10.0.0.1`向 IP 地址为`128.119.40.186`的 Web 服务器(端口 80)发送了 HTTP 请求(如请求页面)。此时,主机`10.0.0.1`将随机指派一个端口,如`3345`,作为本次请求的源端口号,将该请求发送到路由器中(目的地址将是`128.119.40.186`,但会先到达`10.0.0.4`)。 +2. `10.0.0.4`即路由器的 LAN 接口收到`10.0.0.1`的请求。路由器将为该请求指派一个新的源端口号,如`5001`,并将请求报文发送给 WAN 接口`138.76.29.7`。同时,在 NAT 转换表中记录一条转换记录**138.76.29.7:5001——10.0.0.1:3345**。 +3. 请求报文到达 WAN 接口,继续向目的主机`128.119.40.186`发送。 + +之后,将会有如下响应发生: + +1. 主机`128.119.40.186`收到请求,构造响应报文,并将其发送给目的地`138.76.29.7:5001`。 +2. 响应报文到达路由器的 WAN 接口。路由器查询 NAT 转换表,发现`138.76.29.7:5001`在转换表中有记录,从而将其目的地址和目的端口转换成为`10.0.0.1:3345`,再发送到`10.0.0.4`上。 +3. 被转换的响应报文到达路由器的 LAN 接口,继而被转发至目的地`10.0.0.1`。 + +![LAN-WAN 间通信提供地址转换](https://oss.javaguide.cn/github/javaguide/cs-basics/network/nat-demo2.png) + +🐛 修正(参见:[issue#2009](https://github.com/Snailclimb/JavaGuide/issues/2009)):上图第四步的 Dest 值应该为 `10.0.0.1:3345` 而不是~~`138.76.29.7:5001`~~,这里笔误了。 + +## 划重点 + +针对以上过程,有以下几个重点需要强调: + +1. 当请求报文到达路由器,并被指定了新端口号时,由于端口号有 16 位,因此,通常来说,一个路由器管理的 LAN 中的最大主机数 $≈65500$($2^{16}$ 的地址空间),但通常 SOHO 子网内不会有如此多的主机数量。 +2. 对于目的服务器来说,从来不知道“到底是哪个主机给我发送的请求”,它只知道是来自`138.76.29.7:5001`的路由器转发的请求。因此,可以说,**路由器在 WAN 和 LAN 之间起到了屏蔽作用**,所有内部主机发送到外部的报文,都具有同一个 IP 地址(不同的端口号),所有外部发送到内部的报文,也都只有一个目的地(不同端口号),是经过了 NAT 转换后,外部报文才得以正确地送达内部主机。 +3. 在报文穿过路由器,发生 NAT 转换时,如果 LAN 主机 IP 已经在 NAT 转换表中注册过了,则不需要路由器新指派端口,而是直接按照转换记录穿过路由器。同理,外部报文发送至内部时也如此。 + +总结 NAT 协议的特点,有以下几点: + +1. NAT 协议通过对 WAN 屏蔽 LAN,有效地缓解了 IPv4 地址分配压力。 +2. LAN 主机 IP 地址的变更,无需通告 WAN。 +3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。 +4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。 + +然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的**。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 + + + diff --git a/docs_en/cs-basics/network/network-attack-means.en.md b/docs_en/cs-basics/network/network-attack-means.en.md new file mode 100644 index 00000000000..917641e1496 --- /dev/null +++ b/docs_en/cs-basics/network/network-attack-means.en.md @@ -0,0 +1,478 @@ +--- +title: 网络攻击常见手段总结 +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 网络攻击,DDoS,IP 欺骗,ARP 欺骗,中间人攻击,扫描,防护 + - - meta + - name: description + content: 总结常见 TCP/IP 攻击与防护思路,覆盖 DDoS、IP/ARP 欺骗、中间人等手段,强调工程防护实践。 +--- + +> 本文整理完善自[TCP/IP 常见攻击手段 - 暖蓝笔记 - 2021](https://mp.weixin.qq.com/s/AZwWrOlLxRSSi-ywBgZ0fA)这篇文章。 + +这篇文章的内容主要是介绍 TCP/IP 常见攻击手段,尤其是 DDoS 攻击,也会补充一些其他的常见网络攻击手段。 + +## IP 欺骗 + +### IP 是什么? + +在网络中,所有的设备都会分配一个地址。这个地址就仿佛小蓝的家地址「**多少号多少室**」,这个号就是分配给整个子网的,「**室**」对应的号码即分配给子网中计算机的,这就是网络中的地址。「号」对应的号码为网络号,「**室**」对应的号码为主机号,这个地址的整体就是 **IP 地址**。 + +### 通过 IP 地址我们能知道什么? + +通过 IP 地址,我们就可以知道判断访问对象服务器的位置,从而将消息发送到服务器。一般发送者发出的消息首先经过子网的集线器,转发到最近的路由器,然后根据路由位置访问下一个路由器的位置,直到终点 + +**IP 头部格式** : + +![](https://oss.javaguide.cn/p3-juejin/843fd07074874ee0b695eca659411b42~tplv-k3u1fbpfcp-zoom-1.png) + +### IP 欺骗技术是什么? + +骗呗,拐骗,诱骗! + +IP 欺骗技术就是**伪造**某台主机的 IP 地址的技术。通过 IP 地址的伪装使得某台主机能够**伪装**另外的一台主机,而这台主机往往具有某种特权或者被另外的主机所信任。 + +假设现在有一个合法用户 **(1.1.1.1)** 已经同服务器建立正常的连接,攻击者构造攻击的 TCP 数据,伪装自己的 IP 为 **1.1.1.1**,并向服务器发送一个带有 RST 位的 TCP 数据段。服务器接收到这样的数据后,认为从 **1.1.1.1** 发送的连接有错误,就会清空缓冲区中建立好的连接。 + +这时,如果合法用户 **1.1.1.1** 再发送合法数据,服务器就已经没有这样的连接了,该用户就必须从新开始建立连接。攻击时,伪造大量的 IP 地址,向目标发送 RST 数据,使服务器不对合法用户服务。虽然 IP 地址欺骗攻击有着相当难度,但我们应该清醒地意识到,这种攻击非常广泛,入侵往往从这种攻击开始。 + +![IP 欺骗 DDoS 攻击](https://oss.javaguide.cn/p3-juejin/7547a145adf9404aa3a05f01f5ca2e32~tplv-k3u1fbpfcp-zoom-1.png) + +### 如何缓解 IP 欺骗? + +虽然无法预防 IP 欺骗,但可以采取措施来阻止伪造数据包渗透网络。**入口过滤** 是防范欺骗的一种极为常见的防御措施,如 BCP38(通用最佳实践文档)所示。入口过滤是一种数据包过滤形式,通常在[网络边缘](https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/)设备上实施,用于检查传入的 IP 数据包并确定其源标头。如果这些数据包的源标头与其来源不匹配或者看上去很可疑,则拒绝这些数据包。一些网络还实施出口过滤,检查退出网络的 IP 数据包,确保这些数据包具有合法源标头,以防止网络内部用户使用 IP 欺骗技术发起出站恶意攻击。 + +## SYN Flood(洪水) + +### SYN Flood 是什么? + +SYN Flood 是互联网上最原始、最经典的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击之一,旨在耗尽可用服务器资源,致使服务器无法传输合法流量 + +SYN Flood 利用了 TCP 协议的三次握手机制,攻击者通常利用工具或者控制僵尸主机向服务器发送海量的变源 IP 地址或变源端口的 TCP SYN 报文,服务器响应了这些报文后就会生成大量的半连接,当系统资源被耗尽后,服务器将无法提供正常的服务。 +增加服务器性能,提供更多的连接能力对于 SYN Flood 的海量报文来说杯水车薪,防御 SYN Flood 的关键在于判断哪些连接请求来自于真实源,屏蔽非真实源的请求以保障正常的业务请求能得到服务。 + +![](https://oss.javaguide.cn/p3-juejin/2b3d2d4dc8f24890b5957df1c7d6feb8~tplv-k3u1fbpfcp-zoom-1.png) + +### TCP SYN Flood 攻击原理是什么? + +**TCP SYN Flood** 攻击利用的是 **TCP** 的三次握手(**SYN -> SYN/ACK -> ACK**),假设连接发起方是 A,连接接受方是 B,即 B 在某个端口(**Port**)上监听 A 发出的连接请求,过程如下图所示,左边是 A,右边是 B。 + +![](https://oss.javaguide.cn/p3-juejin/a39355a1ea404323a11ca6644e009183~tplv-k3u1fbpfcp-zoom-1.png) + +A 首先发送 **SYN**(Synchronization)消息给 B,要求 B 做好接收数据的准备;B 收到后反馈 **SYN-ACK**(Synchronization-Acknowledgement) 消息给 A,这个消息的目的有两个: + +- 向 A 确认已做好接收数据的准备, +- 同时要求 A 也做好接收数据的准备,此时 B 已向 A 确认好接收状态,并等待 A 的确认,连接处于**半开状态(Half-Open)**,顾名思义只开了一半;A 收到后再次发送 **ACK** (Acknowledgement) 消息给 B,向 B 确认也做好了接收数据的准备,至此三次握手完成,「**连接**」就建立了, + +大家注意到没有,最关键的一点在于双方是否都按对方的要求进入了**可以接收消息**的状态。而这个状态的确认主要是双方将要使用的**消息序号(**SequenceNum),**TCP** 为保证消息按发送顺序抵达接收方的上层应用,需要用**消息序号**来标记消息的发送先后顺序的。 + +**TCP**是「**双工**」(Duplex)连接,同时支持双向通信,也就是双方同时可向对方发送消息,其中 **SYN** 和 **SYN-ACK** 消息开启了 A→B 的单向通信通道(B 获知了 A 的消息序号);**SYN-ACK** 和 **ACK** 消息开启了 B→A 单向通信通道(A 获知了 B 的消息序号)。 + +上面讨论的是双方在诚实守信,正常情况下的通信。 + +但实际情况是,网络可能不稳定会丢包,使握手消息不能抵达对方,也可能是对方故意不按规矩来,故意延迟或不发送握手确认消息。 + +假设 B 通过某 **TCP** 端口提供服务,B 在收到 A 的 **SYN** 消息时,积极的反馈了 **SYN-ACK** 消息,使连接进入**半开状态**,因为 B 不确定自己发给 A 的 **SYN-ACK** 消息或 A 反馈的 ACK 消息是否会丢在半路,所以会给每个待完成的半开连接都设一个**Timer**,如果超过时间还没有收到 A 的 **ACK** 消息,则重新发送一次 **SYN-ACK** 消息给 A,直到重试超过一定次数时才会放弃。 + +![图片](https://oss.javaguide.cn/p3-juejin/7ff1daddcec44d61994f254e664987b4~tplv-k3u1fbpfcp-zoom-1.png) + +B 为帮助 A 能顺利连接,需要**分配内核资源**维护半开连接,那么当 B 面临海量的连接 A 时,如上图所示,**SYN Flood** 攻击就形成了。攻击方 A 可以控制肉鸡向 B 发送大量 SYN 消息但不响应 ACK 消息,或者干脆伪造 SYN 消息中的 **Source IP**,使 B 反馈的 **SYN-ACK** 消息石沉大海,导致 B 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。 + +### SYN Flood 的常见形式有哪些? + +**恶意用户可通过三种不同方式发起 SYN Flood 攻击**: + +1. **直接攻击:** 不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。在此类攻击中,攻击者完全不屏蔽其 IP 地址。由于攻击者使用具有真实 IP 地址的单一源设备发起攻击,因此很容易发现并清理攻击者。为使目标机器呈现半开状态,黑客将阻止个人机器对服务器的 SYN-ACK 数据包做出响应。为此,通常采用以下两种方式实现:部署防火墙规则,阻止除 SYN 数据包以外的各类传出数据包;或者,对传入的所有 SYN-ACK 数据包进行过滤,防止其到达恶意用户机器。实际上,这种方法很少使用(即便使用过也不多见),因为此类攻击相当容易缓解 – 只需阻止每个恶意系统的 IP 地址。哪怕攻击者使用僵尸网络(如 [Mirai 僵尸网络](https://www.cloudflare.com/learning/ddos/glossary/mirai-botnet/)),通常也不会刻意屏蔽受感染设备的 IP。 +2. **欺骗攻击:** 恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。虽然数据包可能经过伪装,但还是可以通过这些数据包追根溯源。此类检测工作很难开展,但并非不可实现;特别是,如果 Internet 服务提供商 (ISP) 愿意提供帮助,则更容易实现。 +3. **分布式攻击(DDoS):** 如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。随着混淆级别的攀升,攻击者可能还会命令每台分布式设备伪造其发送数据包的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。 + +### 如何缓解 SYN Flood? + +#### 扩展积压工作队列 + +目标设备安装的每个操作系统都允许具有一定数量的半开连接。若要响应大量 SYN 数据包,一种方法是增加操作系统允许的最大半开连接数目。为成功扩展最大积压工作,系统必须额外预留内存资源以处理各类新请求。如果系统没有足够的内存,无法应对增加的积压工作队列规模,将对系统性能产生负面影响,但仍然好过拒绝服务。 + +#### 回收最先创建的 TCP 半开连接 + +另一种缓解策略是在填充积压工作后覆盖最先创建的半开连接。这项策略要求完全建立合法连接的时间低于恶意 SYN 数据包填充积压工作的时间。当攻击量增加或积压工作规模小于实际需求时,这项特定的防御措施将不奏效。 + +#### SYN Cookie + +此策略要求服务器创建 Cookie。为避免在填充积压工作时断开连接,服务器使用 SYN-ACK 数据包响应每一项连接请求,而后从积压工作中删除 SYN 请求,同时从内存中删除请求,保证端口保持打开状态并做好重新建立连接的准备。如果连接是合法请求并且已将最后一个 ACK 数据包从客户端机器发回服务器,服务器将重建(存在一些限制)SYN 积压工作队列条目。虽然这项缓解措施势必会丢失一些 TCP 连接信息,但好过因此导致对合法用户发起拒绝服务攻击。 + +## UDP Flood(洪水) + +### UDP Flood 是什么? + +**UDP Flood** 也是一种拒绝服务攻击,将大量的用户数据报协议(**UDP**)数据包发送到目标服务器,目的是压倒该设备的处理和响应能力。防火墙保护目标服务器也可能因 **UDP** 泛滥而耗尽,从而导致对合法流量的拒绝服务。 + +### UDP Flood 攻击原理是什么? + +**UDP Flood** 主要通过利用服务器响应发送到其中一个端口的 **UDP** 数据包所采取的步骤。在正常情况下,当服务器在特定端口接收到 **UDP** 数据包时,会经过两个步骤: + +- 服务器首先检查是否正在运行正在侦听指定端口的请求的程序。 +- 如果没有程序在该端口接收数据包,则服务器使用 **ICMP**(ping)数据包进行响应,以通知发送方目的地不可达。 + +举个例子。假设今天要联系酒店的小蓝,酒店客服接到电话后先查看房间的列表来确保小蓝在客房内,随后转接给小蓝。 + +首先,接待员接收到呼叫者要求连接到特定房间的电话。接待员然后需要查看所有房间的清单,以确保客人在房间中可用,并愿意接听电话。碰巧的是,此时如果突然间所有的电话线同时亮起来,那么他们就会很快就变得不堪重负了。 + +当服务器接收到每个新的 **UDP** 数据包时,它将通过步骤来处理请求,并利用该过程中的服务器资源。发送 **UDP** 报文时,每个报文将包含源设备的 **IP** 地址。在这种类型的 **DDoS** 攻击期间,攻击者通常不会使用自己的真实 **IP** 地址,而是会欺骗 **UDP** 数据包的源 **IP** 地址,从而阻止攻击者的真实位置被暴露并潜在地饱和来自目标的响应数据包服务器。 + +由于目标服务器利用资源检查并响应每个接收到的 **UDP** 数据包的结果,当接收到大量 **UDP** 数据包时,目标的资源可能会迅速耗尽,导致对正常流量的拒绝服务。 + +![](https://oss.javaguide.cn/p3-juejin/23dbbc8243a84ed181e088e38bffb37a~tplv-k3u1fbpfcp-zoom-1.png) + +### 如何缓解 UDP Flooding? + +大多数操作系统部分限制了 **ICMP** 报文的响应速率,以中断需要 ICMP 响应的 **DDoS** 攻击。这种缓解的一个缺点是在攻击过程中,合法的数据包也可能被过滤。如果 **UDP Flood** 的容量足够高以使目标服务器的防火墙的状态表饱和,则在服务器级别发生的任何缓解都将不足以应对目标设备上游的瓶颈。 + +## HTTP Flood(洪水) + +### HTTP Flood 是什么? + +HTTP Flood 是一种大规模的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击,旨在利用 HTTP 请求使目标服务器不堪重负。目标因请求而达到饱和,且无法响应正常流量后,将出现拒绝服务,拒绝来自实际用户的其他请求。 + +![HTTP 洪水攻击](https://oss.javaguide.cn/p3-juejin/aa64869551d94c8d89fa80eaf4395bfa~tplv-k3u1fbpfcp-zoom-1.png) + +### HTTP Flood 的攻击原理是什么? + +HTTP 洪水攻击是“第 7 层”DDoS 攻击的一种。第 7 层是 OSI 模型的应用程序层,指的是 HTTP 等互联网协议。HTTP 是基于浏览器的互联网请求的基础,通常用于加载网页或通过互联网发送表单内容。缓解应用程序层攻击特别复杂,因为恶意流量和正常流量很难区分。 + +为了获得最大效率,恶意行为者通常会利用或创建僵尸网络,以最大程度地扩大攻击的影响。通过利用感染了恶意软件的多台设备,攻击者可以发起大量攻击流量来进行攻击。 + +HTTP 洪水攻击有两种: + +- **HTTP GET 攻击**:在这种攻击形式下,多台计算机或其他设备相互协调,向目标服务器发送对图像、文件或其他资产的多个请求。当目标被传入的请求和响应所淹没时,来自正常流量源的其他请求将被拒绝服务。 +- **HTTP POST 攻击**:一般而言,在网站上提交表单时,服务器必须处理传入的请求并将数据推送到持久层(通常是数据库)。与发送 POST 请求所需的处理能力和带宽相比,处理表单数据和运行必要数据库命令的过程相对密集。这种攻击利用相对资源消耗的差异,直接向目标服务器发送许多 POST 请求,直到目标服务器的容量饱和并拒绝服务为止。 + +### 如何防护 HTTP Flood? + +如前所述,缓解第 7 层攻击非常复杂,而且通常要从多方面进行。一种方法是对发出请求的设备实施质询,以测试它是否是机器人,这与在线创建帐户时常用的 CAPTCHA 测试非常相似。通过提出 JavaScript 计算挑战之类的要求,可以缓解许多攻击。 + +其他阻止 HTTP 洪水攻击的途径包括使用 Web 应用程序防火墙 (WAF)、管理 IP 信誉数据库以跟踪和有选择地阻止恶意流量,以及由工程师进行动态分析。Cloudflare 具有超过 2000 万个互联网设备的规模优势,能够分析来自各种来源的流量并通过快速更新的 WAF 规则和其他防护策略来缓解潜在的攻击,从而消除应用程序层 DDoS 流量。 + +## DNS Flood(洪水) + +### DNS Flood 是什么? + +域名系统(DNS)服务器是互联网的“电话簿“;互联网设备通过这些服务器来查找特定 Web 服务器以便访问互联网内容。DNS Flood 攻击是一种分布式拒绝服务(DDoS)攻击,攻击者用大量流量淹没某个域的 DNS 服务器,以尝试中断该域的 DNS 解析。如果用户无法找到电话簿,就无法查找到用于调用特定资源的地址。通过中断 DNS 解析,DNS Flood 攻击将破坏网站、API 或 Web 应用程序响应合法流量的能力。很难将 DNS Flood 攻击与正常的大流量区分开来,因为这些大规模流量往往来自多个唯一地址,查询该域的真实记录,模仿合法流量。 + +### DNS Flood 的攻击原理是什么? + +![](https://oss.javaguide.cn/p3-juejin/97ea11a212924900b10d159226783887~tplv-k3u1fbpfcp-zoom-1.png) + +域名系统的功能是将易于记忆的名称(例如 example.com)转换成难以记住的网站服务器地址(例如 192.168.0.1),因此成功攻击 DNS 基础设施将导致大多数人无法使用互联网。DNS Flood 攻击是一种相对较新的基于 DNS 的攻击,这种攻击是在高带宽[物联网(IoT)](https://www.cloudflare.com/learning/ddos/glossary/internet-of-things-iot/)[僵尸网络](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-botnet/)(如 [Mirai](https://www.cloudflare.com/learning/ddos/glossary/mirai-botnet/))兴起后激增的。DNS Flood 攻击使用 IP 摄像头、DVR 盒和其他 IoT 设备的高带宽连接直接淹没主要提供商的 DNS 服务器。来自 IoT 设备的大量请求淹没 DNS 提供商的服务,阻止合法用户访问提供商的 DNS 服务器。 + +DNS Flood 攻击不同于 [DNS 放大攻击](https://www.cloudflare.com/zh-cn/learning/ddos/dns-amplification-ddos-attack/)。与 DNS Flood 攻击不同,DNS 放大攻击反射并放大不安全 DNS 服务器的流量,以便隐藏攻击的源头并提高攻击的有效性。DNS 放大攻击使用连接带宽较小的设备向不安全的 DNS 服务器发送无数请求。这些设备对非常大的 DNS 记录发出小型请求,但在发出请求时,攻击者伪造返回地址为目标受害者。这种放大效果让攻击者能借助有限的攻击资源来破坏较大的目标。 + +### 如何防护 DNS Flood? + +DNS Flood 对传统上基于放大的攻击方法做出了改变。借助轻易获得的高带宽僵尸网络,攻击者现能针对大型组织发动攻击。除非被破坏的 IoT 设备得以更新或替换,否则抵御这些攻击的唯一方法是使用一个超大型、高度分布式的 DNS 系统,以便实时监测、吸收和阻止攻击流量。 + +## TCP 重置攻击 + +在 **TCP** 重置攻击中,攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。正常情况下,如果客户端收发现到达的报文段对于相关连接而言是不正确的,**TCP** 就会发送一个重置报文段,从而导致 **TCP** 连接的快速拆卸。 + +**TCP** 重置攻击利用这一机制,通过向通信方发送伪造的重置报文段,欺骗通信双方提前关闭 TCP 连接。如果伪造的重置报文段完全逼真,接收者就会认为它有效,并关闭 **TCP** 连接,防止连接被用来进一步交换信息。服务端可以创建一个新的 **TCP** 连接来恢复通信,但仍然可能会被攻击者重置连接。万幸的是,攻击者需要一定的时间来组装和发送伪造的报文,所以一般情况下这种攻击只对长连接有杀伤力,对于短连接而言,你还没攻击呢,人家已经完成了信息交换。 + +从某种意义上来说,伪造 **TCP** 报文段是很容易的,因为 **TCP/IP** 都没有任何内置的方法来验证服务端的身份。有些特殊的 IP 扩展协议(例如 `IPSec`)确实可以验证身份,但并没有被广泛使用。客户端只能接收报文段,并在可能的情况下使用更高级别的协议(如 `TLS`)来验证服务端的身份。但这个方法对 **TCP** 重置包并不适用,因为 **TCP** 重置包是 **TCP** 协议本身的一部分,无法使用更高级别的协议进行验证。 + +## 模拟攻击 + +> 以下实验是在 `OSX` 系统中完成的,其他系统请自行测试。 + +现在来总结一下伪造一个 **TCP** 重置报文要做哪些事情: + +- 嗅探通信双方的交换信息。 +- 截获一个 `ACK` 标志位置位 1 的报文段,并读取其 `ACK` 号。 +- 伪造一个 TCP 重置报文段(`RST` 标志位置为 1),其序列号等于上面截获的报文的 `ACK` 号。这只是理想情况下的方案,假设信息交换的速度不是很快。大多数情况下为了增加成功率,可以连续发送序列号不同的重置报文。 +- 将伪造的重置报文发送给通信的一方或双方,时其中断连接。 + +为了实验简单,我们可以使用本地计算机通过 `localhost` 与自己通信,然后对自己进行 TCP 重置攻击。需要以下几个步骤: + +- 在两个终端之间建立一个 TCP 连接。 +- 编写一个能嗅探通信双方数据的攻击程序。 +- 修改攻击程序,伪造并发送重置报文。 + +下面正式开始实验。 + +> 建立 TCP 连接 + +可以使用 netcat 工具来建立 TCP 连接,这个工具很多操作系统都预装了。打开第一个终端窗口,运行以下命令: + +```bash +nc -nvl 8000 +``` + +这个命令会启动一个 TCP 服务,监听端口为 `8000`。接着再打开第二个终端窗口,运行以下命令: + +```bash +nc 127.0.0.1 8000 +``` + +该命令会尝试与上面的服务建立连接,在其中一个窗口输入一些字符,就会通过 TCP 连接发送给另一个窗口并打印出来。 + +![](https://oss.javaguide.cn/p3-juejin/df0508cbf26446708cf98f8ad514dbea~tplv-k3u1fbpfcp-zoom-1.gif) + +> 嗅探流量 + +编写一个攻击程序,使用 Python 网络库 `scapy` 来读取两个终端窗口之间交换的数据,并将其打印到终端上。代码比较长,下面为一部份,完整代码后台回复 TCP 攻击,代码的核心是调用 `scapy` 的嗅探方法: + +![](https://oss.javaguide.cn/p3-juejin/27feb834aa9d4b629fd938611ac9972e~tplv-k3u1fbpfcp-zoom-1.png) + +这段代码告诉 `scapy` 在 `lo0` 网络接口上嗅探数据包,并记录所有 TCP 连接的详细信息。 + +- **iface** : 告诉 scapy 在 `lo0`(localhost)网络接口上进行监听。 +- **lfilter** : 这是个过滤器,告诉 scapy 忽略所有不属于指定的 TCP 连接(通信双方皆为 `localhost`,且端口号为 `8000`)的数据包。 +- **prn** : scapy 通过这个函数来操作所有符合 `lfilter` 规则的数据包。上面的例子只是将数据包打印到终端,下文将会修改函数来伪造重置报文。 +- **count** : scapy 函数返回之前需要嗅探的数据包数量。 + +> 发送伪造的重置报文 + +下面开始修改程序,发送伪造的 TCP 重置报文来进行 TCP 重置攻击。根据上面的解读,只需要修改 prn 函数就行了,让其检查数据包,提取必要参数,并利用这些参数来伪造 TCP 重置报文并发送。 + +例如,假设该程序截获了一个从(`src_ip`, `src_port`)发往 (`dst_ip`, `dst_port`)的报文段,该报文段的 ACK 标志位已置为 1,ACK 号为 `100,000`。攻击程序接下来要做的是: + +- 由于伪造的数据包是对截获的数据包的响应,所以伪造数据包的源 `IP/Port` 应该是截获数据包的目的 `IP/Port`,反之亦然。 +- 将伪造数据包的 `RST` 标志位置为 1,以表示这是一个重置报文。 +- 将伪造数据包的序列号设置为截获数据包的 ACK 号,因为这是发送方期望收到的下一个序列号。 +- 调用 `scapy` 的 `send` 方法,将伪造的数据包发送给截获数据包的发送方。 + +对于我的程序而言,只需将这一行取消注释,并注释这一行的上面一行,就可以全面攻击了。按照步骤 1 的方法设置 TCP 连接,打开第三个窗口运行攻击程序,然后在 TCP 连接的其中一个终端输入一些字符串,你会发现 TCP 连接被中断了! + +> 进一步实验 + +1. 可以继续使用攻击程序进行实验,将伪造数据包的序列号加减 1 看看会发生什么,是不是确实需要和截获数据包的 `ACK` 号完全相同。 +2. 打开 `Wireshark`,监听 lo0 网络接口,并使用过滤器 `ip.src == 127.0.0.1 && ip.dst == 127.0.0.1 && tcp.port == 8000` 来过滤无关数据。你可以看到 TCP 连接的所有细节。 +3. 在连接上更快速地发送数据流,使攻击更难执行。 + +## 中间人攻击 + +猪八戒要向小蓝表白,于是写了一封信给小蓝,结果第三者小黑拦截到了这封信,把这封信进行了篡改,于是乎在他们之间进行搞破坏行动。这个马文才就是中间人,实施的就是中间人攻击。好我们继续聊聊什么是中间人攻击。 + +### 什么是中间人? + +攻击中间人攻击英文名叫 Man-in-the-MiddleAttack,简称「MITM 攻击」。指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。我们画一张图: + +![图片](https://oss.javaguide.cn/p3-juejin/d69b74e63981472b852797f2fa08976f~tplv-k3u1fbpfcp-zoom-1.png) + +从这张图可以看到,中间人其实就是攻击者。通过这种原理,有很多实现的用途,比如说,你在手机上浏览不健康网站的时候,手机就会提示你,此网站可能含有病毒,是否继续访问还是做其他的操作等等。 + +### 中间人攻击的原理是什么? + +举个例子,我和公司签了一个一份劳动合同,一人一份合同。不晓得哪个可能改了合同内容,不知道真假了,怎么搞?只好找专业的机构来鉴定,自然就要花钱。 + +在安全领域有句话:**我们没有办法杜绝网络犯罪,只好想办法提高网络犯罪的成本**。既然没法杜绝这种情况,那我们就想办法提高作案的成本,今天我们就简单了解下基本的网络安全知识,也是面试中的高频面试题了。 + +为了避免双方说活不算数的情况,双方引入第三家机构,将合同原文给可信任的第三方机构,只要这个机构不监守自盗,合同就相对安全。 + +**如果第三方机构内部不严格或容易出现纰漏?** + +虽然我们将合同原文给第三方机构了,为了防止内部人员的更改,需要采取什么措施呢 + +一种可行的办法是引入 **摘要算法** 。即合同和摘要一起,为了简单的理解摘要。大家可以想象这个摘要为一个函数,这个函数对原文进行了加密,会产生一个唯一的散列值,一旦原文发生一点点变化,那么这个散列值将会变化。 + +#### 有哪些常用的摘要算法呢? + +目前比较常用的加密算法有消息摘要算法和安全散列算法(**SHA**)。**MD5** 是将任意长度的文章转化为一个 128 位的散列值,可是在 2004 年,**MD5** 被证实了容易发生碰撞,即两篇原文产生相同的摘要。这样的话相当于直接给黑客一个后门,轻松伪造摘要。 + +所以在大部分的情况下都会选择 **SHA 算法** 。 + +**出现内鬼了怎么办?** + +看似很安全的场面了,理论上来说杜绝了篡改合同的做法。主要某个员工同时具有修改合同和摘要的权利,那搞事儿就是时间的问题了,毕竟没哪个系统可以完全的杜绝员工接触敏感信息,除非敏感信息都不存在。所以能不能考虑将合同和摘要分开存储呢 + +**那如何确保员工不会修改合同呢?** + +这确实蛮难的,不过办法总比困难多。我们将合同放在双方手中,摘要放在第三方机构,篡改难度进一步加大 + +**那么员工万一和某个用户串通好了呢?** + +看来放在第三方的机构还是不好使,同样存在不小风险。所以还需要寻找新的方案,这就出现了 **数字签名和证书**。 + +#### 数字证书和签名有什么用? + +同样的,举个例子。Sum 和 Mike 两个人签合同。Sum 首先用 **SHA** 算法计算合同的摘要,然后用自己私钥将摘要加密,得到数字签名。Sum 将合同原文、签名,以及公钥三者都交给 Mike + +![](https://oss.javaguide.cn/p3-juejin/e4b7d6fca78b45c8840c12411b717f2f~tplv-k3u1fbpfcp-zoom-1.png) + +如果 Sum 想要证明合同是 Mike 的,那么就要使用 Mike 的公钥,将这个签名解密得到摘要 x,然后 Mike 计算原文的 sha 摘要 Y,随后对比 x 和 y,如果两者相等,就认为数据没有被篡改 + +在这样的过程中,Mike 是不能更改 Sum 的合同,因为要修改合同不仅仅要修改原文还要修改摘要,修改摘要需要提供 Mike 的私钥,私钥即 Sum 独有的密码,公钥即 Sum 公布给他人使用的密码 + +总之,公钥加密的数据只能私钥可以解密。私钥加密的数据只有公钥可以解密,这就是 **非对称加密** 。 + +隐私保护?不是吓唬大家,信息是透明的兄 die,不过尽量去维护个人的隐私吧,今天学习对称加密和非对称加密。 + +大家先读读这个字"钥",是读"yao",我以前也是,其实读"yue" + +#### 什么是对称加密? + +对称加密,顾名思义,加密方与解密方使用同一钥匙(秘钥)。具体一些就是,发送方通过使用相应的加密算法和秘钥,对将要发送的信息进行加密;对于接收方而言,使用解密算法和相同的秘钥解锁信息,从而有能力阅读信息。 + +![图片](https://oss.javaguide.cn/p3-juejin/ef81cb5e2f0a4d3d9ac5a44ecf97e3cc~tplv-k3u1fbpfcp-zoom-1.png) + +#### 常见的对称加密算法有哪些? + +**DES** + +DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实际用于算法,其余 8 位可以被用于奇偶校验,并在算法中被丢弃。因此,**DES** 的有效密钥长度为 56 位,通常称 **DES** 的密钥长度为 56 位。假设秘钥为 56 位,采用暴力破 Jie 的方式,其秘钥个数为 2 的 56 次方,那么每纳秒执行一次解密所需要的时间差不多 1 年的样子。当然,没人这么干。**DES** 现在已经不是一种安全的加密方法,主要因为它使用的 56 位密钥过短。 + +![](https://oss.javaguide.cn/p3-juejin/9eb3a2bf6cf14132a890bc3447480eeb~tplv-k3u1fbpfcp-zoom-1.jpeg) + +**IDEA** + +国际数据加密算法(International Data Encryption Algorithm)。秘钥长度 128 位,优点没有专利的限制。 + +**AES** + +当 DES 被破解以后,没过多久推出了 **AES** 算法,提供了三种长度供选择,128 位、192 位和 256,为了保证性能不受太大的影响,选择 128 即可。 + +**SM1 和 SM4** + +之前几种都是国外的,我们国内自行研究了国密 **SM1**和 **SM4**。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可 + +**总结**: + +![](https://oss.javaguide.cn/p3-juejin/578961e3175540e081e1432c409b075a~tplv-k3u1fbpfcp-zoom-1.png) + +#### 常见的非对称加密算法有哪些? + +在对称加密中,发送方与接收方使用相同的秘钥。那么在非对称加密中则是发送方与接收方使用的不同的秘钥。其主要解决的问题是防止在秘钥协商的过程中发生泄漏。比如在对称加密中,小蓝将需要发送的消息加密,然后告诉你密码是 123balala,ok,对于其他人而言,很容易就能劫持到密码是 123balala。那么在非对称的情况下,小蓝告诉所有人密码是 123balala,对于中间人而言,拿到也没用,因为没有私钥。所以,非对称密钥其实主要解决了密钥分发的难题。如下图 + +![](https://oss.javaguide.cn/p3-juejin/153cf04a0ecc43c38003f3a1ab198cc0~tplv-k3u1fbpfcp-zoom-1.png) + +其实我们经常都在使用非对称加密,比如使用多台服务器搭建大数据平台 hadoop,为了方便多台机器设置免密登录,是不是就会涉及到秘钥分发。再比如搭建 docker 集群也会使用相关非对称加密算法。 + +常见的非对称加密算法: + +- RSA(RSA 加密算法,RSA Algorithm):优势是性能比较快,如果想要较高的加密难度,需要很长的秘钥。 + +- ECC:基于椭圆曲线提出。是目前加密强度最高的非对称加密算法 +- SM2:同样基于椭圆曲线问题设计。最大优势就是国家认可和大力支持。 + +总结: + +![](https://oss.javaguide.cn/p3-juejin/28b96fb797904d4b818ee237cdc7614c~tplv-k3u1fbpfcp-zoom-1.png) + +#### 常见的散列算法有哪些? + +这个大家应该更加熟悉了,比如我们平常使用的 MD5 校验,在很多时候,我并不是拿来进行加密,而是用来获得唯一性 ID。在做系统的过程中,存储用户的各种密码信息,通常都会通过散列算法,最终存储其散列值。 + +**MD5**(不推荐) + +MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较普遍的散列算法,具体的应用场景你可以自行  参阅。虽然,因为算法的缺陷,它的唯一性已经被破解了,但是大部分场景下,这并不会构成安全问题。但是,如果不是长度受限(32 个字符),我还是不推荐你继续使用 **MD5** 的。 + +**SHA** + +安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。 + +**SM3** + +国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。 + +**总结**: + +![图片](https://oss.javaguide.cn/p3-juejin/79c3c2f72d2f44c7abf2d73a49024495~tplv-k3u1fbpfcp-zoom-1.png) + +**大部分情况下使用对称加密,具有比较不错的安全性。如果需要分布式进行秘钥分发,考虑非对称。如果不需要可逆计算则散列算法。** 因为这段时间有这方面需求,就看了一些这方面的资料,入坑信息安全,就怕以后洗发水都不用买。谢谢大家查看! + +#### 第三方机构和证书机制有什么用? + +问题还有,此时如果 Sum 否认给过 Mike 的公钥和合同,不久 gg 了 + +所以需要 Sum 过的话做过的事儿需要足够的信誉,这就引入了 **第三方机构和证书机制** 。 + +证书之所以会有信用,是因为证书的签发方拥有信用。所以如果 Sum 想让 Mike 承认自己的公钥,Sum 不会直接将公钥给 Mike ,而是提供由第三方机构,含有公钥的证书。如果 Mike 也信任这个机构,法律都认可,那 ik,信任关系成立 + +![](https://oss.javaguide.cn/p3-juejin/b1a3dbf87e3e41ff894f39512a10f66d~tplv-k3u1fbpfcp-zoom-1.png) + +如上图所示,Sum 将自己的申请提交给机构,产生证书的原文。机构用自己的私钥签名 Sum 的申请原文(先根据原文内容计算摘要,再用私钥加密),得到带有签名信息的证书。Mike 拿到带签名信息的证书,通过第三方机构的公钥进行解密,获得 Sum 证书的摘要、证书的原文。有了 Sum 证书的摘要和原文,Mike 就可以进行验签。验签通过,Mike 就可以确认 Sum 的证书的确是第三方机构签发的。 + +用上面这样一个机制,合同的双方都无法否认合同。这个解决方案的核心在于需要第三方信用服务机构提供信用背书。这里产生了一个最基础的信任链,如果第三方机构的信任崩溃,比如被黑客攻破,那整条信任链条也就断裂了 + +为了让这个信任条更加稳固,就需要环环相扣,打造更长的信任链,避免单点信任风险 + +![](https://oss.javaguide.cn/p3-juejin/1481f0409da94ba6bb0fee69bf0996f8~tplv-k3u1fbpfcp-zoom-1.png) + +上图中,由信誉最好的根证书机构提供根证书,然后根证书机构去签发二级机构的证书;二级机构去签发三级机构的证书;最后有由三级机构去签发 Sum 证书。 + +如果要验证 Sum 证书的合法性,就需要用三级机构证书中的公钥去解密 Sum 证书的数字签名。 + +如果要验证三级机构证书的合法性,就需要用二级机构的证书去解密三级机构证书的数字签名。 + +如果要验证二级结构证书的合法性,就需要用根证书去解密。 + +以上,就构成了一个相对长一些的信任链。如果其中一方想要作弊是非常困难的,除非链条中的所有机构同时联合起来,进行欺诈。 + +### 中间人攻击如何避免? + +既然知道了中间人攻击的原理也知道了他的危险,现在我们看看如何避免。相信我们都遇到过下面这种状况: + +![](https://oss.javaguide.cn/p3-juejin/0dde4b76be6240699312d822a3fe1ed3~tplv-k3u1fbpfcp-zoom-1.png) + +出现这个界面的很多情况下,都是遇到了中间人攻击的现象,需要对安全证书进行及时地监测。而且大名鼎鼎的 github 网站,也曾遭遇过中间人攻击: + +想要避免中间人攻击的方法目前主要有两个: + +- 客户端不要轻易相信证书:因为这些证书极有可能是中间人。 +- App 可以提前预埋证书在本地:意思是我们本地提前有一些证书,这样其他证书就不能再起作用了。 + +## DDOS + +通过上面的描述,总之即好多种攻击都是 **DDOS** 攻击,所以简单总结下这个攻击相关内容。 + +其实,像全球互联网各大公司,均遭受过大量的 **DDoS**。 + +2018 年,GitHub 在一瞬间遭到高达 1.35Tbps 的带宽攻击。这次 DDoS 攻击几乎可以堪称是互联网有史以来规模最大、威力最大的 DDoS 攻击了。在 GitHub 遭到攻击后,仅仅一周后,DDoS 攻击又开始对 Google、亚马逊甚至 Pornhub 等网站进行了 DDoS 攻击。后续的 DDoS 攻击带宽最高也达到了 1Tbps。 + +### DDoS 攻击究竟是什么? + +DDos 全名 Distributed Denial of Service,翻译成中文就是**分布式拒绝服务**。指的是处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。单一的 DoS 攻击一般是采用一对一方式的,它利用网络协议和操作系统的一些缺陷,采用**欺骗和伪装**的策略来进行网络攻击,使网站服务器充斥大量要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷以至于瘫痪而停止提供正常的网络服务。 + +> 举个例子 + +我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。 + +上面这个例子讲的就是典型的 DDoS 攻击,一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。 + +攻击方式很多,比如 **ICMP Flood**、**UDP Flood**、**NTP Flood**、**SYN Flood**、**CC 攻击**、**DNS Query Flood**等等。 + +### 如何应对 DDoS 攻击? + +#### 高防服务器 + +还是拿开的重庆火锅店举例,高防服务器就是我给重庆火锅店增加了两名保安,这两名保安可以让保护店铺不受流氓骚扰,并且还会定期在店铺周围巡逻防止流氓骚扰。 + +高防服务器主要是指能独立硬防御 50Gbps 以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点等,这东西是不错,就是贵~ + +#### 黑名单 + +面对火锅店里面的流氓,我一怒之下将他们拍照入档,并禁止他们踏入店铺,但是有的时候遇到长得像的人也会禁止他进入店铺。这个就是设置黑名单,此方法秉承的就是“错杀一千,也不放一百”的原则,会封锁正常流量,影响到正常业务。 + +#### DDoS 清洗 + +**DDos** 清洗,就是我发现客人进店几分钟以后,但是一直不点餐,我就把他踢出店里。 + +**DDoS** 清洗会对用户请求数据进行实时监控,及时发现 **DOS** 攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。 + +#### CDN 加速 + +CDN 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将火锅店开到了线上,承接外卖服务,这样流氓找不到店在哪里,也耍不来流氓了。 + +在现实中,CDN 服务将网站访问流量分配到了各个节点中,这样一方面隐藏网站的真实 IP,另一方面即使遭遇 **DDoS** 攻击,也可以将流量分散到各个节点中,防止源站崩溃。 + +## 参考 + +- HTTP 洪水攻击 - CloudFlare: +- SYN 洪水攻击: +- 什么是 IP 欺骗?: +- 什么是 DNS 洪水?| DNS 洪水 DDoS 攻击: + + + diff --git a/docs_en/cs-basics/network/osi-and-tcp-ip-model.en.md b/docs_en/cs-basics/network/osi-and-tcp-ip-model.en.md new file mode 100644 index 00000000000..aaf16393d80 --- /dev/null +++ b/docs_en/cs-basics/network/osi-and-tcp-ip-model.en.md @@ -0,0 +1,203 @@ +--- +title: OSI 和 TCP/IP 网络分层模型详解(基础) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: OSI 七层,TCP/IP 四层,分层模型,职责划分,协议栈,对比 + - - meta + - name: description + content: 详解 OSI 与 TCP/IP 的分层模型与职责划分,结合历史与实践对比两者差异与工程取舍。 +--- + +## OSI 七层模型 + +**OSI 七层模型** 是国际标准化组织提出一个网络分层模型,其大体结构以及每一层提供的功能如下图所示: + +![OSI 七层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png) + +每一层都专注做一件事情,并且每一层都需要使用下一层提供的功能比如传输层需要使用网络层提供的路由和寻址功能,这样传输层才知道把数据传输到哪里去。 + +**OSI 的七层体系结构概念清楚,理论也很完整,但是它比较复杂而且不实用,而且有些功能在多个层中重复出现。** + +上面这种图可能比较抽象,再来一个比较生动的图片。下面这个图片是我在国外的一个网站上看到的,非常赞! + +![osi七层模型2](https://oss.javaguide.cn/github/javaguide/osi七层模型2.png) + +**既然 OSI 七层模型这么厉害,为什么干不过 TCP/IP 四 层模型呢?** + +的确,OSI 七层模型当时一直被一些大公司甚至一些国家政府支持。这样的背景下,为什么会失败呢?我觉得主要有下面几方面原因: + +1. OSI 的专家缺乏实际经验,他们在完成 OSI 标准时缺乏商业驱动力 +2. OSI 的协议实现起来过分复杂,而且运行效率很低 +3. OSI 制定标准的周期太长,因而使得按 OSI 标准生产的设备无法及时进入市场(20 世纪 90 年代初期,虽然整套的 OSI 国际标准都已经制定出来,但基于 TCP/IP 的互联网已经抢先在全球相当大的范围成功运行了) +4. OSI 的层次划分不太合理,有些功能在多个层次中重复出现。 + +OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础。为了更好地去了解网络分层,OSI 七层模型还是非常有必要学习的。 + +最后再分享一个关于 OSI 七层模型非常不错的总结图片! + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-model-detail.png) + +## TCP/IP 四层模型 + +**TCP/IP 四层模型** 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成: + +1. 应用层 +2. 传输层 +3. 网络层 +4. 网络接口层 + +需要注意的是,我们并不能将 TCP/IP 四层模型 和 OSI 七层模型完全精确地匹配起来,不过可以简单将两者对应起来,如下图所示: + +![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) + +### 应用层(Application layer) + +**应用层位于传输层之上,主要提供两个终端设备上的应用程序之间信息交换的服务,它定义了信息交换的格式,消息会交给下一层传输层来传输。** 我们把应用层交互的数据单元称为报文。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-five-layer-sample-diagram.png) + +应用层协议定义了网络通信规则,对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如支持 Web 应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。 + +**应用层常见协议**: + +![应用层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/application-layer-protocol.png) + +- **HTTP(Hypertext Transfer Protocol,超文本传输协议)**:基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。 +- **SMTP(Simple Mail Transfer Protocol,简单邮件发送协议)**:基于 TCP 协议,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。 +- **POP3/IMAP(邮件接收协议)**:基于 TCP 协议,两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。 +- **FTP(File Transfer Protocol,文件传输协议)** : 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。 +- **Telnet(远程登陆协议)**:基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。 +- **SSH(Secure Shell Protocol,安全的网络传输协议)**:基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务 +- **RTP(Real-time Transport Protocol,实时传输协议)**:通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。 +- **DNS(Domain Name System,域名管理系统)**: 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。 + +关于这些协议的详细介绍请看 [应用层常见协议总结(应用层)](./application-layer-protocol.md) 这篇文章。 + +### 传输层(Transport layer) + +**传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。** 应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。 + +**传输层常见协议**: + +![传输层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/transport-layer-protocol.png) + +- **TCP(Transmission Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。 +- **UDP(User Datagram Protocol,用户数据协议)**:提供 **无连接** 的,**尽最大努力** 的数据传输服务(不保证数据传输的可靠性),简单高效。 + +### 网络层(Network layer) + +**网络层负责为分组交换网上的不同主机提供通信服务。** 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报,简称数据报。 + +⚠️ 注意:**不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混**。 + +**网络层的还有一个任务就是选择合适的路由,使源主机运输层所传下来的分组,能通过网络层中的路由器找到目的主机。** + +这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称。 + +互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Internet Protocol)和许多路由选择协议,因此互联网的网络层也叫做 **网际层** 或 **IP 层**。 + +**网络层常见协议**: + +![网络层常见协议](images/network-model/nerwork-layer-protocol.png) + +- **IP(Internet Protocol,网际协议)**:TCP/IP 协议中最重要的协议之一,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 +- **ARP(Address Resolution Protocol,地址解析协议)**:ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 +- **ICMP(Internet Control Message Protocol,互联网控制报文协议)**:一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。例如,Ping 工具就使用了 ICMP 协议来测试网络连通性。 +- **NAT(Network Address Translation,网络地址转换协议)**:NAT 协议的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。 +- **OSPF(Open Shortest Path First,开放式最短路径优先)** ):一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。 +- **RIP(Routing Information Protocol,路由信息协议)**:一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议,基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。 +- **BGP(Border Gateway Protocol,边界网关协议)**:一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议,具有高度的灵活性和可扩展性。 + +### 网络接口层(Network interface layer) + +我们可以把网络接口层看作是数据链路层和物理层的合体。 + +1. 数据链路层(data link layer)通常简称为链路层( 两台主机之间的数据传输,总是在一段一段的链路上传送的)。**数据链路层的作用是将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。** +2. **物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异** + +网络接口层重要功能和协议如下图所示: + +![网络接口层重要功能和协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-interface-layer-protocol.png) + +### 总结 + +简单总结一下每一层包含的协议和核心技术: + +![TCP/IP 各层协议概览](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png) + +**应用层协议** : + +- HTTP(Hypertext Transfer Protocol,超文本传输协议) +- SMTP(Simple Mail Transfer Protocol,简单邮件发送协议) +- POP3/IMAP(邮件接收协议) +- FTP(File Transfer Protocol,文件传输协议) +- Telnet(远程登陆协议) +- SSH(Secure Shell Protocol,安全的网络传输协议) +- RTP(Real-time Transport Protocol,实时传输协议) +- DNS(Domain Name System,域名管理系统) +- …… + +**传输层协议** : + +- TCP 协议 + - 报文段结构 + - 可靠数据传输 + - 流量控制 + - 拥塞控制 +- UDP 协议 + - 报文段结构 + - RDT(可靠数据传输协议) + +**网络层协议** : + +- IP(Internet Protocol,网际协议) +- ARP(Address Resolution Protocol,地址解析协议) +- ICMP 协议(控制报文协议,用于发送控制消息) +- NAT(Network Address Translation,网络地址转换协议) +- OSPF(Open Shortest Path First,开放式最短路径优先) +- RIP(Routing Information Protocol,路由信息协议) +- BGP(Border Gateway Protocol,边界网关协议) +- …… + +**网络接口层** : + +- 差错检测技术 +- 多路访问协议(信道复用技术) +- CSMA/CD 协议 +- MAC 协议 +- 以太网技术 +- …… + +## 网络分层的原因 + +在这篇文章的最后,我想聊聊:“为什么网络要分层?”。 + +说到分层,我们先从我们平时使用框架开发一个后台程序来说,我们往往会按照每一层做不同的事情的原则将系统分为三层(复杂的系统分层会更多): + +1. Repository(数据库操作) +2. Service(业务操作) +3. Controller(前后端数据交互) + +**复杂的系统需要分层,因为每一层都需要专注于一类事情。网络分层的原因也是一样,每一层只专注于做一类事情。** + +好了,再来说回:“为什么网络要分层?”。我觉得主要有 3 方面的原因: + +1. **各层之间相互独立**:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)**。这个和我们对开发时系统进行分层是一个道理。** +2. **提高了整体灵活性**:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。**这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。** +3. **大问题化小**:分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 **这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。** + +我想到了计算机世界非常非常有名的一句话,这里分享一下: + +> 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,计算机整个体系从上到下都是按照严格的层次结构设计的。 + +## 参考 + +- TCP/IP model vs OSI model: +- Data Encapsulation and the TCP/IP Protocol Stack: + + + diff --git a/docs_en/cs-basics/network/other-network-questions.en.md b/docs_en/cs-basics/network/other-network-questions.en.md new file mode 100644 index 00000000000..3db3be0802d --- /dev/null +++ b/docs_en/cs-basics/network/other-network-questions.en.md @@ -0,0 +1,544 @@ +--- +title: 计算机网络常见面试题总结(上) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 计算机网络面试题,TCP/IP四层模型,HTTP面试,HTTPS vs HTTP,HTTP/1.1 vs HTTP/2,HTTP/3 QUIC,TCP三次握手,UDP区别,DNS解析,WebSocket vs SSE,GET vs POST,应用层协议,网络分层,队头阻塞,PING命令,ARP协议 + - - meta + - name: description + content: 最新计算机网络高频面试题总结(上):TCP/IP四层模型、HTTP全版本对比、TCP三次握手、DNS解析、WebSocket/SSE实时推送等,附图解+⭐️重点标注,一文搞定应用层&传输层&网络层核心考点,快速备战后端面试! +--- + + + +上篇主要是计算机网络基础和应用层相关的内容。 + +## 计算机网络基础 + +### 网络分层模型 + +#### OSI 七层模型是什么?每一层的作用是什么? + +**OSI 七层模型** 是国际标准化组织提出的一个网络分层模型,其大体结构以及每一层提供的功能如下图所示: + +![OSI 七层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png) + +每一层都专注做一件事情,并且每一层都需要使用下一层提供的功能比如传输层需要使用网络层提供的路由和寻址功能,这样传输层才知道把数据传输到哪里去。 + +**OSI 的七层体系结构概念清楚,理论也很完整,但是它比较复杂而且不实用,而且有些功能在多个层中重复出现。** + +上面这种图可能比较抽象,再来一个比较生动的图片。下面这个图片是我在国外的一个网站上看到的,非常赞! + +![osi七层模型2](https://oss.javaguide.cn/github/javaguide/osi七层模型2.png) + +#### ⭐️TCP/IP 四层模型是什么?每一层的作用是什么? + +**TCP/IP 四层模型** 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成: + +1. 应用层 +2. 传输层 +3. 网络层 +4. 网络接口层 + +需要注意的是,我们并不能将 TCP/IP 四层模型 和 OSI 七层模型完全精确地匹配起来,不过可以简单将两者对应起来,如下图所示: + +![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) + +关于每一层作用的详细介绍,请看 [OSI 和 TCP/IP 网络分层模型详解(基础)](https://javaguide.cn/cs-basics/network/osi-and-tcp-ip-model.html) 这篇文章。 + +#### 为什么网络要分层? + +说到分层,我们先从我们平时使用框架开发一个后台程序来说,我们往往会按照每一层做不同的事情的原则将系统分为三层(复杂的系统分层会更多): + +1. Repository(数据库操作) +2. Service(业务操作) +3. Controller(前后端数据交互) + +**复杂的系统需要分层,因为每一层都需要专注于一类事情。网络分层的原因也是一样,每一层只专注于做一类事情。** + +好了,再来说回:“为什么网络要分层?”。我觉得主要有 3 方面的原因: + +1. **各层之间相互独立**:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)**。这个和我们对开发时系统进行分层是一个道理。** +2. **提高了灵活性和可替换性**:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。并且,每一层都可以根据需要进行修改或替换,而不会影响到整个网络的结构。**这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。** +3. **大问题化小**:分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 **这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。** + +我想到了计算机世界非常非常有名的一句话,这里分享一下: + +> 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,计算机整个体系从上到下都是按照严格的层次结构设计的。 + +### 常见网络协议 + +#### ⭐️应用层有哪些常见的协议? + +![应用层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/application-layer-protocol.png) + +- **HTTP(Hypertext Transfer Protocol,超文本传输协议)**:基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。 +- **SMTP(Simple Mail Transfer Protocol,简单邮件发送协议)**:基于 TCP 协议,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。 +- **POP3/IMAP(邮件接收协议)**:基于 TCP 协议,两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。 +- **FTP(File Transfer Protocol,文件传输协议)** : 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。 +- **Telnet(远程登陆协议)**:基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。 +- **SSH(Secure Shell Protocol,安全的网络传输协议)**:基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务 +- **RTP(Real-time Transport Protocol,实时传输协议)**:通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。 +- **DNS(Domain Name System,域名管理系统)**: 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。 + +关于这些协议的详细介绍请看 [应用层常见协议总结(应用层)](./application-layer-protocol.md) 这篇文章。 + +#### 传输层有哪些常见的协议? + +![传输层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/transport-layer-protocol.png) + +- **TCP(Transmission Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。 +- **UDP(User Datagram Protocol,用户数据协议)**:提供 **无连接** 的,**尽最大努力** 的数据传输服务(不保证数据传输的可靠性),简单高效。 + +#### 网络层有哪些常见的协议? + +![网络层常见协议](images/network-model/nerwork-layer-protocol.png) + +- **IP(Internet Protocol,网际协议)**:TCP/IP 协议中最重要的协议之一,属于网络层的协议,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 +- **ARP (Address Resolution Protocol, Address Resolution Protocol)**: The ARP protocol solves the problem of conversion between network layer addresses and link layer addresses. Because during the physical transmission of an IP datagram, you always need to know where the next hop (physical next destination) should go, but the IP address is a logical address, and the MAC address is the physical address. The ARP protocol solves some problems of converting IP addresses to MAC addresses. +- **ICMP (Internet Control Message Protocol)**: A protocol used to transmit network status and error messages, often used for network diagnosis and troubleshooting. For example, the Ping tool uses the ICMP protocol to test network connectivity. +- **NAT (Network Address Translation, Network Address Translation Protocol)**: The application scenario of the NAT protocol is just like its name - Network Address Translation, which is used in the address translation process from the internal network to the external network. Specifically, within a small subnet (local area network, LAN), each host uses the IP address under the same LAN, but outside the LAN, in the wide area network (WAN), a unified IP address is needed to identify the location of the LAN on the entire Internet. +- **OSPF (Open Shortest Path First, Open Shortest Path First)**: An Interior Gateway Protocol (IGP), which is also a widely used dynamic routing protocol. Based on the link state algorithm, it takes into account factors such as link bandwidth and delay to select the best path. +- **RIP (Routing Information Protocol, Routing Information Protocol)**: An Interior Gateway Protocol (IGP), which is also a dynamic routing protocol. It is based on the distance vector algorithm, uses a fixed number of hops as a metric, and selects the path with the least number of hops as the best path. +- **BGP (Border Gateway Protocol, Border Gateway Protocol)**: A routing protocol used to exchange Network Layer Reachability Information (NLRI) between routing domains, with a high degree of flexibility and scalability. + +## HTTP + +### ⭐️What exactly happens from entering the URL to page display? (very important) + +> Similar question: When opening a web page, what protocols will be used in the entire process? + +Let’s look at a picture first (from "Illustrating HTTP"): + + + +There is an error to note in the above picture: it is OSPF, not OPSF. OSPF (Open Shortest Path First, ospf) Open Shortest Path First Protocol is a routing protocol developed by the Internet Engineering Task Force + +Generally speaking, it is divided into the following steps: + +1. Enter the URL of the specified web page in the browser. +2. The browser obtains the IP address corresponding to the domain name through the DNS protocol. +3. The browser initiates a TCP connection request to the target server based on the IP address and port number. +4. The browser sends an HTTP request message to the server on the TCP connection to request the content of the web page. +5. After receiving the HTTP request message, the server processes the request and returns an HTTP response message to the browser. +6. After the browser receives the HTTP response message, it parses the HTML code in the response body, renders the structure and style of the web page, and at the same time initiates an HTTP request again based on the URLs of other resources in the HTML (such as images, CSS, JS, etc.) to obtain the contents of these resources until the web page is fully loaded and displayed. +7. When the browser does not need to communicate with the server, it can actively close the TCP connection, or wait for the server's closing request. + +For a detailed introduction, you can view this article: [The whole process of accessing web pages (knowledge connection)](https://javaguide.cn/cs-basics/network/the-whole-process-of-accessing-web-pages.html) (strongly recommended). + +### ⭐️What are the HTTP status codes? + +HTTP status codes are used to describe the results of HTTP requests. For example, 2xx means that the request was successfully processed. + +![Common HTTP status codes](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-status-code.png) + +For a more detailed summary of HTTP status codes, you can read this article I wrote: [Summary of common HTTP status codes (application layer)](https://javaguide.cn/cs-basics/network/http-status-codes.html). + +### What are the common fields in HTTP Header? + +| Request header field name | Description | Example | +| :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | +| Accept | Acceptable response content types (Content-Types). | Accept: text/plain | +| Accept-Charset | Acceptable character set | Accept-Charset: utf-8 | +| Accept-Datetime | Acceptable version expressed in time | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT | +| Accept-Encoding | A list of acceptable encodings. See HTTP compression. | Accept-Encoding: gzip, deflate | +| Accept-Language | A list of natural languages that are acceptable for response content. | Accept-Language: en-US || Authorization | Authentication information used for Hypertext Transfer Protocol authentication | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Cache-Control | Used to specify directives that all caching mechanisms in this request/response chain must comply with | Cache-Control: no-cache | +| Connection | The type of connection this browser prefers to use | Connection: keep-alive | +| Content-Length | The length of the request body as an octet array (8-bit bytes) | Content-Length: 348 | +| Content-MD5 | The binary MD5 hash value of the request body's content, encoded in Base64 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== | +| Content-Type | The multimedia type of the request body (used in POST and PUT requests) | Content-Type: application/x-www-form-urlencoded | +| Cookie | A Hypertext Transfer Protocol Cookie previously sent by the server via Set-Cookie (described below) | Cookie: $Version=1; Skin=new; | +| Date | The date and time the message was sent (sent in the "Hypertext Transfer Protocol Date" format defined in RFC 7231) | Date: Tue, 15 Nov 1994 08:12:31 GMT | +| Expect | Indicates that the client requires the server to perform specific behavior | Expect: 100-continue | +| From | The email address of the user who initiated this request | From: `user@example.com` | +| Host | The domain name of the server (used for virtual hosts), and the Transmission Control Protocol port number that the server listens on. The port number may be omitted if the requested port is the standard port of the corresponding service. | Host: en.wikipedia.org | +| If-Match | The corresponding operation is performed only if the entity provided by the client matches the corresponding entity on the server. The main purpose is to be used in methods like PUT to update a resource only if the resource has not been modified since the user last updated the resource. | If-Match: "737060cd8c284d8af7ad3082f209582d" | +| If-Modified-Since | Allows the server to return a `304 Not Modified` status code if the requested resource has not been modified since the specified date | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT | +| If-None-Match | Allows the server to return a `304 Not Modified` status code if the ETag of the requested resource has not changed | If-None-Match: "737060cd8c284d8af7ad3082f209582d" | +| If-Range | If the entity has not been modified, send me the part or parts I am missing; otherwise, send the entire new entity | If-Range: "737060cd8c284d8af7ad3082f209582d" || If-Unmodified-Since | Send a response only if the entity has not been modified since a certain time. | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT | +| Max-Forwards | Limits the number of times this message can be forwarded by proxies and gateways. | Max-Forwards: 10 | +| Origin | Initiates a request for cross-origin resource sharing. | `Origin: http://www.example-social-network.com` | +| Pragma | Implementation dependent, these fields may have multiple effects at any time in the request/response chain. | Pragma: no-cache | +| Proxy-Authorization | Authentication information used to authenticate to the proxy. | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Range | Requests only a portion of an entity. Byte offsets start with 0. See byte service. | Range: bytes=500-999 | +| Referer | indicates the previous page visited by the browser. It is a link on that page that brings the browser to the currently requested page. | `Referer: http://en.wikipedia.org/wiki/Main_Page` | +| TE | The transfer encoding that the browser expects to accept: you can use the value in the Transfer-Encoding field of the response protocol header; | TE: trailers, deflate | +| Upgrade | Requests the server to be upgraded to another protocol. | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | +| User-Agent | The browser's browser identification string | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 | +| Via | Informs the server which proxy this request was issued by. | Via: 1.0 fred, 1.1 example.com (Apache/1.1) | +| Warning | A general warning that errors may exist in the entity content body. | Warning: 199 Miscellaneous warning | + +### ⭐️What is the difference between HTTP and HTTPS? (Important) + +![HTTP and HTTPS comparison](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-vs-https.png) + +- **Port number**: The default for HTTP is 80, and the default for HTTPS is 443. +- **URL Prefix**: The URL prefix of HTTP is `http://`, and the URL prefix of HTTPS is `https://`. +- **Security and Resource Consumption**: The HTTP protocol runs on top of TCP, all transmitted content is clear text, and neither the client nor the server can verify the identity of the other party. HTTPS is an HTTP protocol that runs on top of SSL/TLS, which runs on top of TCP. All transmitted content is encrypted using symmetric encryption, but the symmetric encryption key is asymmetrically encrypted using a server-side certificate. Therefore, HTTP is not as secure as HTTPS, but HTTPS consumes more server resources than HTTP. +- **SEO (Search Engine Optimization)**: Search engines generally prefer websites that use the HTTPS protocol because HTTPS provides greater security and user privacy protection. Websites that use the HTTPS protocol may be prioritized in search results, impacting SEO. + +For a more detailed comparison summary of HTTP and HTTPS, you can read this article I wrote: [HTTP vs HTTPS (application layer)](https://javaguide.cn/cs-basics/network/http-vs-https.html). + +### What is the difference between HTTP/1.0 and HTTP/1.1? + +![Comparison between HTTP/1.0 and HTTP/1.1](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.0-vs-http1.1.png)- **连接方式** : HTTP/1.0 为短连接,HTTP/1.1 支持长连接。HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。 +- **状态响应码** : HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 +- **缓存机制** : 在 HTTP/1.0 中主要使用 Header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP/1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 +- **带宽**:HTTP/1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP/1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 +- **Host 头(Host Header)处理** :HTTP/1.1 引入了 Host 头字段,允许在同一 IP 地址上托管多个域名,从而支持虚拟主机的功能。而 HTTP/1.0 没有 Host 头字段,无法实现虚拟主机。 + +关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:[HTTP/1.0 vs HTTP/1.1(应用层)](https://javaguide.cn/cs-basics/network/http1.0-vs-http1.1.html) 。 + +### ⭐️HTTP/1.1 和 HTTP/2.0 有什么区别? + +![HTTP/1.0 和 HTTP/1.1 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.1-vs-http2.0.png) + +- **多路复用(Multiplexing)**:HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本),互不干扰。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接,而浏览器为了控制资源会有 6-8 个 TCP 连接的限制。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。 +- **二进制帧(Binary Frames)**:HTTP/2.0 使用二进制帧进行数据传输,而 HTTP/1.1 则使用文本格式的报文。二进制帧更加紧凑和高效,减少了传输的数据量和带宽消耗。 +- **队头阻塞**:HTTP/2 引入了多路复用技术,允许多个请求和响应在单个 TCP 连接上并行交错传输,解决了 HTTP/1.1 应用层的队头阻塞问题,但 HTTP/2 依然受到 TCP 层队头阻塞 的影响。 +- **头部压缩(Header Compression)**:HTTP/1.1 支持`Body`压缩,`Header`不支持压缩。HTTP/2.0 支持对`Header`压缩,使用了专门为`Header`压缩而设计的 HPACK 算法,减少了网络开销。 +- **服务器推送(Server Push)**:HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数和延迟。而 HTTP/1.1 需要客户端自己发送请求来获取相关资源。 + +HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://blog.cloudflare.com/http-2-for-web-developers/)): + +![HTTP/2 Multiplexing](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-multiplexing.png) + +可以看到,HTTP/2 的多路复用机制允许多个请求和响应共享一个 TCP 连接,从而避免了 HTTP/1.1 在应对并发请求时需要建立多个并行连接的情况,减少了重复连接建立和维护的额外开销。而在 HTTP/1.1 中,尽管支持持久连接,但为了缓解队头阻塞问题,浏览器通常会为同一域名建立多个并行连接。 + +### HTTP/2.0 和 HTTP/3.0 有什么区别? + +![HTTP/2.0 和 HTTP/3.0 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-vs-http3.0.png) + +- **传输协议**:HTTP/2.0 是基于 TCP 协议实现的,HTTP/3.0 新增了 QUIC(Quick UDP Internet Connections) 协议来实现可靠的传输,提供与 TLS/SSL 相当的安全性,具有较低的连接和传输延迟。你可以将 QUIC 看作是 UDP 的升级版本,在其基础上新增了很多功能比如加密、重传等等。HTTP/3.0 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。 +- **连接建立**:HTTP/2.0 需要经过经典的 TCP 三次握手过程(由于安全的 HTTPS 连接建立还需要 TLS 握手,共需要大约 3 个 RTT)。由于 QUIC 协议的特性(TLS 1.3,TLS 1.3 除了支持 1 个 RTT 的握手,还支持 0 个 RTT 的握手)连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。 +- **头部压缩**:HTTP/2.0 使用 HPACK 算法进行头部压缩,而 HTTP/3.0 使用更高效的 QPACK 头压缩算法。 +- **队头阻塞**:HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 +- **连接迁移**:HTTP/3.0 支持连接迁移,因为 QUIC 使用 64 位 ID 标识连接,只要 ID 不变就不会中断,网络环境改变时(如从 Wi-Fi 切换到移动数据)也能保持连接。而 TCP 连接是由(源 IP,源端口,目的 IP,目的端口)组成,这个四元组中一旦有一项值发生改变,这个连接也就不能用了。 +- **错误恢复**:HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。 +- **安全性**:在 HTTP/2.0 中,TLS 用于加密和认证整个 HTTP 会话,包括所有的 HTTP 头部和数据负载。TLS 的工作是在 TCP 层之上,它加密的是在 TCP 连接中传输的应用层的数据,并不会对 TCP 头部以及 TLS 记录层头部进行加密,所以在传输的过程中 TCP 头部可能会被攻击者篡改来干扰通信。而 HTTP/3.0 的 QUIC 对整个数据包(包括报文头和报文体)进行了加密与认证处理,保障安全性。 + +HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较: + +![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png) + +下图是一个更详细的 HTTP/2.0 和 HTTP/3.0 对比图: + +![HTTP/2.0 和 HTTP/3.0 详细对比图](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2-and-http3-stacks-comparison.png) + +从上图可以看出: + +- **HTTP/2.0**:使用 TCP 作为传输协议、使用 HPACK 进行头部压缩、依赖 TLS 进行加密。 +- **HTTP/3.0**:使用基于 UDP 的 QUIC 协议、使用更高效的 QPACK 进行头部压缩、在 QUIC 中直接集成了 TLS。QUIC 协议具备连接迁移、拥塞控制与避免、流量控制等特性。 + +关于 HTTP/1.0 -> HTTP/3.0 更详细的演进介绍,推荐阅读[HTTP1 到 HTTP3 的工程优化](https://dbwu.tech/posts/http_evolution/)。 + +### HTTP/1.1 和 HTTP/2.0 的队头阻塞有什么不同? + +HTTP/1.1 队头阻塞的主要原因是无法多路复用: + +- 在一个 TCP 连接中,资源的请求和响应是按顺序处理的。如果一个大的资源(如一个大文件)正在传输,后续的小资源(如较小的 CSS 文件)需要等待前面的资源传输完成后才能被发送。 +- 如果浏览器需要同时加载多个资源(如多个 CSS、JS 文件等),它通常会开启多个并行的 TCP 连接(一般限制为 6 个)。但每个连接仍然受限于顺序的请求-响应机制,因此仍然会发生 **应用层的队头阻塞**。 + +虽然 HTTP/2.0 引入了多路复用技术,允许多个请求和响应在单个 TCP 连接上并行交错传输,解决了 **HTTP/1.1 应用层的队头阻塞问题**,但 HTTP/2.0 依然受到 **TCP 层队头阻塞** 的影响: + +- HTTP/2.0 通过帧(frame)机制将每个资源分割成小块,并为每个资源分配唯一的流 ID,这样多个资源的数据可以在同一 TCP 连接中交错传输。 +- TCP 作为传输层协议,要求数据按顺序交付。如果某个数据包在传输过程中丢失,即使后续的数据包已经到达,也必须等待丢失的数据包重传后才能继续处理。这种传输层的顺序性导致了 **TCP 层的队头阻塞**。 +- 举例来说,如果 HTTP/2 的一个 TCP 数据包中携带了多个资源的数据(例如 JS 和 CSS),而该数据包丢失了,那么后续数据包中的所有资源数据都需要等待丢失的数据包重传回来,导致所有流(streams)都被阻塞。 + +最后,来一张表格总结补充一下: + +| **方面** | **HTTP/1.1 的队头阻塞** | **HTTP/2.0 的队头阻塞** | +| -------------- | ---------------------------------------- | ---------------------------------------------------------------- | +| **层级** | 应用层(HTTP 协议本身的限制) | 传输层(TCP 协议的限制) | +| **根本原因** | 无法多路复用,请求和响应必须按顺序传输 | TCP 要求数据包按顺序交付,丢包时阻塞整个连接 | +| **受影响范围** | 单个 HTTP 请求/响应会阻塞后续请求/响应。 | 单个 TCP 包丢失会影响所有 HTTP/2.0 流(依赖于同一个底层 TCP 连接) | +| **缓解方法** | 开启多个并行的 TCP 连接 | 减少网络掉包或者使用基于 UDP 的 QUIC 协议 | +| **影响场景** | 每次都会发生,尤其是大文件阻塞小文件时。 | 丢包率较高的网络环境下更容易发生。 | + +### ⭐️HTTP 是不保存状态的协议, 如何保存用户状态? + +HTTP 协议本身是 **无状态的 (stateless)** 。这意味着服务器默认情况下无法区分两个连续的请求是否来自同一个用户,或者同一个用户之前的操作是什么。这就像一个“健忘”的服务员,每次你跟他说话,他都不知道你是谁,也不知道你之前点过什么菜。 + +但在实际的 Web 应用中,比如网上购物、用户登录等场景,我们显然需要记住用户的状态(例如购物车里的商品、用户的登录信息)。为了解决这个问题,主要有以下几种常用机制: + +**方案一:Session (会话) 配合 Cookie (主流方式):** + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/session-cookie-authentication-process.png) + +这可以说是最经典也是最常用的方法了。基本流程是这样的: + +1. 用户向服务器发送用户名、密码、验证码用于登陆系统。 +2. 服务器验证通过后,会为这个用户创建一个专属的 Session 对象(可以理解为服务器上的一块内存,存放该用户的状态数据,如购物车、登录信息等)存储起来,并给这个 Session 分配一个唯一的 `SessionID`。 +3. 服务器通过 HTTP 响应头中的 `Set-Cookie` 指令,把这个 `SessionID` 发送给用户的浏览器。 +4. 浏览器接收到 `SessionID` 后,会将其以 Cookie 的形式保存在本地。当用户保持登录状态时,每次向该服务器发请求,浏览器都会自动带上这个存有 `SessionID` 的 Cookie。 +5. 服务器收到请求后,从 Cookie 中拿出 `SessionID`,就能找到之前保存的那个 Session 对象,从而知道这是哪个用户以及他之前的状态了。 + +使用 Session 的时候需要注意下面几个点: + +- **客户端 Cookie 支持**:依赖 Session 的核心功能要确保用户浏览器开启了 Cookie。 +- **Session 过期管理**:合理设置 Session 的过期时间,平衡安全性和用户体验。 +- **Session ID 安全**:为包含 `SessionID` 的 Cookie 设置 `HttpOnly` 标志可以防止客户端脚本(如 JavaScript)窃取,设置 Secure 标志可以保证 `SessionID` 只在 HTTPS 连接下传输,增加安全性。 + +Session 数据本身存储在服务器端。常见的存储方式有: + +- **服务器内存**:实现简单,访问速度快,但服务器重启数据会丢失,且不利于多服务器间的负载均衡。这种方式适合简单且用户量不大的业务场景。 +- **数据库 (如 MySQL, PostgreSQL)**:数据持久化,但读写性能相对较低,一般不会使用这种方式。 +- **分布式缓存 (如 Redis)**:性能高,支持分布式部署,是目前大规模应用中非常主流的方案。 + +**方案二:当 Cookie 被禁用时:URL 重写 (URL Rewriting)** + +如果用户的浏览器禁用了 Cookie,或者某些情况下不便使用 Cookie,还有一种备选方案是 URL 重写。这种方式会将 `SessionID` 直接附加到 URL 的末尾,作为参数传递。例如:。服务器端会解析 URL 中的 `sessionid` 参数来获取 `SessionID`,进而找到对应的 Session 数据。 + +这种方法一般不会使用,存在以下缺点: + +- URL 会变长且不美观; +- `SessionID` 暴露在 URL 中,安全性较低(容易被复制、分享或记录在日志中); +- 对搜索引擎优化 (SEO) 可能不友好。 + +**方案三:Token-based 认证 (如 JWT - JSON Web Tokens)** + +这是一种越来越流行的无状态认证方式,尤其适用于前后端分离的架构和微服务。 + +![ JWT 身份验证示意图](https://oss.javaguide.cn/github/javaguide/system-design/jwt/jwt-authentication%20process.png) + +以 JWT 为例(普通 Token 方案也可以),简化后的步骤如下 + +1. 用户向服务器发送用户名、密码以及验证码用于登陆系统; +2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT; +3. 客户端收到 Token 后自己保存起来(比如浏览器的 `localStorage` ); +4. 用户以后每次向后端发请求都在 Header 中带上这个 JWT ; +5. 服务端检查 JWT 并从中获取用户相关信息。 + +JWT 详细介绍可以查看这两篇文章: + +- [JWT 基础概念详解](https://javaguide.cn/system-design/security/jwt-intro.html) +- [JWT 身份认证优缺点分析](https://javaguide.cn/system-design/security/advantages-and-disadvantages-of-jwt.html) + +总结来说,虽然 HTTP 本身是无状态的,但通过 Cookie + Session、URL 重写或 Token 等机制,我们能够有效地在 Web 应用中跟踪和管理用户状态。其中,**Cookie + Session 是最传统也最广泛使用的方式,而 Token-based 认证则在现代 Web 应用中越来越受欢迎。** + +### URI 和 URL 的区别是什么? + +- URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。 +- URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。 + +URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。 + +### Cookie 和 Session 有什么区别? + +准确点来说,这个问题属于认证授权的范畴,你可以在 [认证授权基础概念详解](https://javaguide.cn/system-design/security/basis-of-authority-certification.html) 这篇文章中找到详细的答案。 + +### ⭐️GET 和 POST 的区别 + +这个问题在知乎上被讨论的挺火热的,地址: 。 + +![](https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png) + +GET 和 POST 是 HTTP 协议中两种常用的请求方法,它们在不同的场景和目的下有不同的特点和用法。一般来说,可以从以下几个方面来区分二者(重点搞清两者在语义上的区别即可): + +- 语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。 +- 幂等:GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。 +- 格式:GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。不过,实际上 GET 请求也可以用 body 传输数据,只是并不推荐这样做,因为这样可能会导致一些兼容性或者语义上的问题。 +- 缓存:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。 +- 安全性:GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中。 + +再次提示,重点搞清两者在语义上的区别即可,实际使用过程中,也是通过语义来区分使用 GET 还是 POST。不过,也有一些项目所有的请求都用 POST,这个并不是固定的,项目组达成共识即可。 + +## WebSocket + +### 什么是 WebSocket? + +WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。 + +WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。 + +WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + +下面是 WebSocket 的常见应用场景: + +- 视频弹幕 +- 实时消息推送,详见[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)这篇文章 +- 实时游戏对战 +- 多用户协同编辑 +- 社交聊天 +- …… + +### ⭐️WebSocket 和 HTTP 有什么区别? + +WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。 + +下面是二者的主要区别: + +- WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。 +- WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。 +- WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。 +- WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)。 + +### WebSocket 的工作过程是什么样的? + +WebSocket 的工作过程可以分为以下几个步骤: + +1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket; +2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。 +3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。 +4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。 + +另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 + +### ⭐️WebSocket 与短轮询、长轮询的区别 + +这三种方式,都是为了解决“**客户端如何及时获取服务器最新数据,实现实时更新**”的问题。它们的实现方式和效率、实时性差异较大。 + +**1.短轮询(Short Polling)** + +- **原理**:客户端每隔固定时间(如 5 秒)发起一次 HTTP 请求,询问服务器是否有新数据。服务器收到请求后立即响应。 +- **优点**:实现简单,兼容性好,直接用常规 HTTP 请求即可。 +- **缺点**: + - **实时性一般**:消息可能在两次轮询间到达,用户需等到下次请求才知晓。 + - **资源浪费大**:反复建立/关闭连接,且大多数请求收到的都是“无新消息”,极大增加服务器和网络压力。 + +**2.长轮询(Long Polling)** + +- **原理**:客户端发起请求后,若服务器暂时无新数据,则会保持连接,直到有新数据或超时才响应。客户端收到响应后立即发起下一次请求,实现“伪实时”。 +- **优点**: + - **实时性较好**:一旦有新数据可立即推送,无需等待下次定时请求。 + - **空响应减少**:减少了无效的空响应,提升了效率。 +- **缺点**: + - **服务器资源占用高**:需长时间维护大量连接,消耗服务器线程/连接数。 + - **资源浪费大**:每次响应后仍需重新建立连接,且依然基于 HTTP 单向请求-响应机制。 + +**3. WebSocket** + +- **原理**:客户端与服务器通过一次 HTTP Upgrade 握手后,建立一条持久的 TCP 连接。之后,双方可以随时、主动地发送数据,实现真正的全双工、低延迟通信。 +- **优点**: + - **实时性强**:数据可即时双向收发,延迟极低。 + - **资源效率高**:连接持续,无需反复建立/关闭,减少资源消耗。 + - **功能强大**:支持服务端主动推送消息、客户端主动发起通信。 +- **缺点**: + - **使用限制**:需要服务器和客户端都支持 WebSocket 协议。对连接管理有一定要求(如心跳保活、断线重连等)。 + - **实现麻烦**:实现起来比短轮询和长轮询要更麻烦一些。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + +### ⭐️SSE 与 WebSocket 有什么区别? + +SSE (Server-Sent Events) 和 WebSocket 都是用来实现服务器向浏览器实时推送消息的技术,让网页内容能自动更新,而不需要用户手动刷新。虽然目标相似,但它们在工作方式和适用场景上有几个关键区别: + +1. **通信方式:** + - **SSE:** **单向通信**。只有服务器能向客户端(浏览器)发送数据。客户端不能通过同一个连接向服务器发送数据(需要发起新的 HTTP 请求)。 + - **WebSocket:** **双向通信 (全双工)**。客户端和服务器可以随时互相发送消息,实现真正的实时交互。 +2. **底层协议:** + - **SSE:** 基于**标准的 HTTP/HTTPS 协议**。它本质上是一个“长连接”的 HTTP 请求,服务器保持连接打开并持续发送事件流。不需要特殊的服务器或协议支持,现有的 HTTP 基础设施就能用。 + - **WebSocket:** 使用**独立的 ws:// 或 wss:// 协议**。它需要通过一个特定的 HTTP "Upgrade" 请求来建立连接,并且服务器需要明确支持 WebSocket 协议来处理连接和消息帧。 +3. **实现复杂度和成本:** + - **SSE:** **实现相对简单**,主要在服务器端处理。浏览器端有标准的 EventSource API,使用方便。开发和维护成本较低。 + - **WebSocket:** **稍微复杂一些**。需要服务器端专门处理 WebSocket 连接和协议,客户端也需要使用 WebSocket API。如果需要考虑兼容性、心跳、重连等,开发成本会更高。 +4. **断线重连:** + - **SSE:** **浏览器原生支持**。EventSource API 提供了自动断线重连的机制。 + - **WebSocket:** **需要手动实现**。开发者需要自己编写逻辑来检测断线并进行重连尝试。 +5. **数据类型:** + - **SSE:** **主要设计用来传输文本** (UTF-8 编码)。如果需要传输二进制数据,需要先进行 Base64 等编码转换成文本。 + - **WebSocket:** **原生支持传输文本和二进制数据**,无需额外编码。 + +为了提供更好的用户体验和利用其简单、高效、基于标准 HTTP 的特性,**Server-Sent Events (SSE) 是目前大型语言模型 API(如 OpenAI、DeepSeek 等)实现流式响应的常用甚至可以说是标准的技术选择**。 + +这里以 DeepSeek 为例,我们发送一个请求并打开浏览器控制台验证一下: + +![DeepSeek 响应标头](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse.png) + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse-eventstream.png) + +可以看到,响应头应里包含了 `text/event-stream`,说明使用的确实是 SSE。并且,响应数据也确实是持续分块传输。 + +## PING + +### PING 命令的作用是什么? + +PING 命令是一种常用的网络诊断工具,经常用来测试网络中主机之间的连通性和网络延迟。 + +这里简单举一个例子,我们来 PING 一下百度。 + +```bash +# 发送4个PING请求数据包到 www.baidu.com +❯ ping -c 4 www.baidu.com + +PING www.a.shifen.com (14.119.104.189): 56 data bytes +64 bytes from 14.119.104.189: icmp_seq=0 ttl=54 time=27.867 ms +64 bytes from 14.119.104.189: icmp_seq=1 ttl=54 time=28.732 ms +64 bytes from 14.119.104.189: icmp_seq=2 ttl=54 time=27.571 ms +64 bytes from 14.119.104.189: icmp_seq=3 ttl=54 time=27.581 ms + +--- www.a.shifen.com ping statistics --- +4 packets transmitted, 4 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = 27.571/27.938/28.732/0.474 ms +``` + +PING 命令的输出结果通常包括以下几部分信息: + +1. **ICMP Echo Request(请求报文)信息**:序列号、TTL(Time to Live)值。 +2. **目标主机的域名或 IP 地址**:输出结果的第一行。 +3. **往返时间(RTT,Round-Trip Time)**:从发送 ICMP Echo Request(请求报文)到接收到 ICMP Echo Reply(响应报文)的总时间,用来衡量网络连接的延迟。 +4. **统计结果(Statistics)**:包括发送的 ICMP 请求数据包数量、接收到的 ICMP 响应数据包数量、丢包率、往返时间(RTT)的最小、平均、最大和标准偏差值。 + +如果 PING 对应的目标主机无法得到正确的响应,则表明这两个主机之间的连通性存在问题(有些主机或网络管理员可能禁用了对 ICMP 请求的回复,这样也会导致无法得到正确的响应)。如果往返时间(RTT)过高,则表明网络延迟过高。 + +### PING 命令的工作原理是什么? + +PING 基于网络层的 **ICMP(Internet Control Message Protocol,互联网控制报文协议)**,其主要原理就是通过在网络上发送和接收 ICMP 报文实现的。 + +ICMP 报文中包含了类型字段,用于标识 ICMP 报文类型。ICMP 报文的类型有很多种,但大致可以分为两类: + +- **查询报文类型**:向目标主机发送请求并期望得到响应。 +- **差错报文类型**:向源主机发送错误信息,用于报告网络中的错误情况。 + +PING 用到的 ICMP Echo Request(类型为 8 ) 和 ICMP Echo Reply(类型为 0) 属于查询报文类型 。 + +- PING 命令会向目标主机发送 ICMP Echo Request。 +- 如果两个主机的连通性正常,目标主机会返回一个对应的 ICMP Echo Reply。 + +## DNS + +### DNS 的作用是什么? + +DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是**域名和 IP 地址的映射问题**。 + +![DNS:域名系统](https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png) + +在一台电脑上,可能存在浏览器 DNS 缓存,操作系统 DNS 缓存,路由器 DNS 缓存。如果以上缓存都查询不到,那么 DNS 就闪亮登场了。 + +目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,它可以在 UDP 或 TCP 协议之上运行,端口为 53** 。 + +### DNS 服务器有哪些?根服务器有多少个? + +DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一): + +- 根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。 +- 顶级域 DNS 服务器(TLD 服务器)。顶级域是指域名的后缀,如`com`、`org`、`net`和`edu`等。国家也有自己的顶级域,如`uk`、`fr`和`ca`。TLD 服务器提供了权威 DNS 服务器的 IP 地址。 +- 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。 +- 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构 + +世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 1700 多台,未来还会继续增加。 + +### ⭐️DNS 解析的过程是什么样的? + +There are many steps in the whole process. I wrote a separate article to introduce it in detail: [DNS Domain Name System Detailed Explanation (Application Layer)](https://javaguide.cn/cs-basics/network/dns.html). + +### Do you know about DNS hijacking? How to deal with it? + +DNS hijacking is a network attack that modifies the resolution results of the DNS server so that the domain name visited by the user points to the wrong IP address, resulting in the user being unable to access normal websites or being directed to malicious websites. DNS hijacking is sometimes also called DNS redirection, DNS spoofing, or DNS pollution. + +## Reference + +- "HTTP Illustrated" +- "Top-Down Method of Computer Networks" (Seventh Edition) +- Detailed explanation of HTTP/2.0 and HTTPS protocols: +- Complete list of HTTP request header fields | HTTP Request Headers: +- HTTP1, HTTP2, HTTP3: +- What do you think of HTTP/3? - Che Xiaopang's answer - Zhihu: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/other-network-questions2.en.md b/docs_en/cs-basics/network/other-network-questions2.en.md new file mode 100644 index 00000000000..122a25ff93e --- /dev/null +++ b/docs_en/cs-basics/network/other-network-questions2.en.md @@ -0,0 +1,273 @@ +--- +title: 计算机网络常见面试题总结(下) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 计算机网络面试题,TCP vs UDP,TCP三次握手,HTTP/3 QUIC,IPv4 vs IPv6,TCP可靠性,IP地址,NAT协议,ARP协议,传输层面试,网络层高频题,基于TCP协议,基于UDP协议,队头阻塞,四次挥手 + - - meta + - name: description + content: 最新计算机网络高频面试题总结(下):TCP/UDP深度对比、三次握手四次挥手、HTTP/3 QUIC优化、IPv6优势、NAT/ARP详解,附表格+⭐️重点标注,一文掌握传输层&网络层核心考点,快速通关后端技术面试! +--- + +下篇主要是传输层和网络层相关的内容。 + +## TCP 与 UDP + +### ⭐️TCP 与 UDP 的区别(重要) + +1. **是否面向连接**: + - TCP 是面向连接的。在传输数据之前,必须先通过“三次握手”建立连接;数据传输完成后,还需要通过“四次挥手”来释放连接。这保证了双方都准备好通信。 + - UDP 是无连接的。发送数据前不需要建立任何连接,直接把数据包(数据报)扔出去。 +2. **是否是可靠传输**: + - TCP 提供可靠的数据传输服务。它通过序列号、确认应答 (ACK)、超时重传、流量控制、拥塞控制等一系列机制,来确保数据能够无差错、不丢失、不重复且按顺序地到达目的地。 + - UDP 提供不可靠的传输。它尽最大努力交付 (best-effort delivery),但不保证数据一定能到达,也不保证到达的顺序,更不会自动重传。收到报文后,接收方也不会主动发确认。 +3. **是否有状态**: + - TCP 是有状态的。因为要保证可靠性,TCP 需要在连接的两端维护连接状态信息,比如序列号、窗口大小、哪些数据发出去了、哪些收到了确认等。 + - UDP 是无状态的。它不维护连接状态,发送方发出数据后就不再关心它是否到达以及如何到达,因此开销更小(**这很“渣男”!**)。 +4. **传输效率**: + - TCP 因为需要建立连接、发送确认、处理重传等,其开销较大,传输效率相对较低。 + - UDP 结构简单,没有复杂的控制机制,开销小,传输效率更高,速度更快。 +5. **传输形式**: + - TCP 是面向字节流 (Byte Stream) 的。它将应用程序交付的数据视为一连串无结构的字节流,可能会对数据进行拆分或合并。 + - UDP 是面向报文 (Message Oriented) 的。应用程序交给 UDP 多大的数据块,UDP 就照样发送,既不拆分也不合并,保留了应用程序消息的边界。 +6. **首部开销**: + - TCP 的头部至少需要 20 字节,如果包含选项字段,最多可达 60 字节。 + - UDP 的头部非常简单,固定只有 8 字节。 +7. **是否提供广播或多播服务**: + - TCP 只支持点对点 (Point-to-Point) 的单播通信。 + - UDP 支持一对一 (单播)、一对多 (多播/Multicast) 和一对所有 (广播/Broadcast) 的通信方式。 +8. …… + +为了更直观地对比,可以看下面这个表格: + +| 特性 | TCP | UDP | +| ------------ | -------------------------- | ----------------------------------- | +| **连接性** | 面向连接 | 无连接 | +| **可靠性** | 可靠 | 不可靠 (尽力而为) | +| **状态维护** | 有状态 | 无状态 | +| **传输效率** | 较低 | 较高 | +| **传输形式** | 面向字节流 | 面向数据报 (报文) | +| **头部开销** | 20 - 60 字节 | 8 字节 | +| **通信模式** | 点对点 (单播) | 单播、多播、广播 | +| **常见应用** | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 | + +### ⭐️什么时候选择 TCP,什么时候选 UDP? + +选择 TCP 还是 UDP,主要取决于你的应用**对数据传输的可靠性要求有多高,以及对实时性和效率的要求有多高**。 + +当**数据准确性和完整性至关重要,一点都不能出错**时,通常选择 TCP。因为 TCP 提供了一整套机制(三次握手、确认应答、重传、流量控制等)来保证数据能够可靠、有序地送达。典型应用场景如下: + +- **Web 浏览 (HTTP/HTTPS):** 网页内容、图片、脚本必须完整加载才能正确显示。 +- **文件传输 (FTP, SCP):** 文件内容不允许有任何字节丢失或错序。 +- **邮件收发 (SMTP, POP3, IMAP):** 邮件内容需要完整无误地送达。 +- **远程登录 (SSH, Telnet):** 命令和响应需要准确传输。 +- …… + +当**实时性、速度和效率优先,并且应用能容忍少量数据丢失或乱序**时,通常选择 UDP。UDP 开销小、传输快,没有建立连接和保证可靠性的复杂过程。典型应用场景如下: + +- **实时音视频通信 (VoIP, 视频会议, 直播):** 偶尔丢失一两个数据包(可能导致画面或声音短暂卡顿)通常比因为等待重传(TCP 机制)导致长时间延迟更可接受。应用层可能会有自己的补偿机制。 +- **在线游戏:** 需要快速传输玩家位置、状态等信息,对实时性要求极高,旧的数据很快就没用了,丢失少量数据影响通常不大。 +- **DHCP (动态主机配置协议):** 客户端在请求 IP 时自身没有 IP 地址,无法满足 TCP 建立连接的前提条件,并且 DHCP 有广播需求、交互模式简单以及自带可靠性机制。 +- **物联网 (IoT) 数据上报:** 某些场景下,传感器定期上报数据,丢失个别数据点可能不影响整体趋势分析。 +- …… + +### HTTP 基于 TCP 还是 UDP? + +~~**HTTP 协议是基于 TCP 协议的**,所以发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。~~ + +🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)): + +HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** : + +- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。 +- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。 + +![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png) + +**为什么 HTTP/3 要做这个改变呢?主要有两大原因:** + +1. 解决队头阻塞 (Head-of-Line Blocking,简写:HOL blocking) 问题。 +2. 减少连接建立的延迟。 + +下面我们来详细介绍这两大优化。 + +在 HTTP/2 中,虽然可以在一个 TCP 连接上并发传输多个请求/响应流(多路复用),但 TCP 本身的特性(保证有序、可靠)意味着如果其中一个流的某个 TCP 报文丢失或延迟,整个 TCP 连接都会被阻塞,等待该报文重传。这会导致所有在这个 TCP 连接上的 HTTP/2 流都受到影响,即使其他流的数据包已经到达。**QUIC (运行在 UDP 上) 解决了这个问题**。QUIC 内部实现了自己的多路复用和流控制机制。不同的 HTTP 请求/响应流在 QUIC 层面是真正独立的。如果一个流的数据包丢失,它只会阻塞该流,而不会影响同一 QUIC 连接上的其他流(本质上是多路复用+轮询),大大提高了并发传输的效率。 + +In addition to solving the head-of-line blocking problem, HTTP/3.0 can also reduce the delay in the handshake process. In HTTP/2.0, if you want to establish a secure HTTPS connection, you need to go through the TCP three-way handshake and TLS handshake: + +1. TCP three-way handshake: The client and server exchange SYN and ACK packets to establish a TCP connection. This process requires 1.5 RTT (round-trip time), which is the time from sending to receiving a data packet. +2. TLS handshake: The client and server exchange keys and certificates, establishing a TLS encryption layer. This process requires at least 1 RTT (TLS 1.3) or 2 RTT (TLS 1.2). + +Therefore, HTTP/2.0 connection establishment requires at least 2.5 RTT (TLS 1.3) or 3.5 RTT (TLS 1.2). In HTTP/3.0, the QUIC protocol used (TLS 1.3, TLS 1.3 not only supports 1 RTT handshake, but also supports 0 RTT handshake) connection establishment requires only 0-RTT or 1-RTT. This means that QUIC, in the best case scenario, does not require any additional round trip time to establish a new connection. + +For relevant proofs, please refer to the following two links: + +- +- + +### What TCP/UDP based protocols do you know? + +TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) are the two core protocols of the Internet transport layer. They provide basic communication services for various application layer protocols. Here are some common application layer protocols built on top of TCP and UDP: + +**Protocol running on top of TCP protocol (emphasis on reliable, orderly transmission):** + +| Full Chinese name (abbreviation) | Full English name | Main uses | Description and characteristics | +| -------------------------- | ---------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Hypertext Transfer Protocol (HTTP) | HyperText Transfer Protocol | Transfers web pages, hypertext, multimedia content | **HTTP/1.x and HTTP/2 are based on TCP**. Early versions were unencrypted and were the basis for web communications. | +| HyperText Transfer Protocol Secure (HTTPS) | HyperText Transfer Protocol Secure | Encrypted web page transfer | Adds an SSL/TLS encryption layer between HTTP and TCP to ensure confidentiality and integrity of data transmission. | +| File Transfer Protocol (FTP) | File Transfer Protocol | File transfer | Traditional FTP **clear text transfer**, unsafe. It is recommended to use its secure version **SFTP (SSH File Transfer Protocol)** or **FTPS (FTP over SSL/TLS)**. | +| Simple Mail Transfer Protocol (SMTP) | Simple Mail Transfer Protocol | **Send** email | Responsible for sending emails from the client to the server, or between mail servers. Can be upgraded to encrypted transmission via **STARTTLS**. | +| Post Office Protocol version 3 (POP3) | **Receive** email | Typically delete the server copy** after downloading the message from the server** to the local device** (configurable retention). **POP3S** is its SSL/TLS encrypted version. | +| Internet Message Access Protocol (IMAP) | Internet Message Access Protocol | **Receive and manage** emails | Emails are retained on the server and support multi-device synchronization of email status, folder management, online search, etc. **IMAPS** is its SSL/TLS encrypted version. The modern email service of choice. | +| Remote Terminal Protocol (Telnet) | Teletype Network | Remote Terminal Login | **Clear text transmission** All data (including passwords), security is extremely poor, and has basically been completely replaced by SSH. | +| Secure Shell Protocol (SSH) | Secure Shell | Secure remote management, encrypted data transmission | Provides functions such as encrypted remote login and command execution, as well as secure file transfer (SFTP), and is a secure alternative to Telnet. | + +**Protocol running on top of UDP protocol (emphasis on fast, low-overhead transmission):** + +| Full Chinese name (abbreviation) | Full English name | Main uses | Description and characteristics | +| ----------------------- | -------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Hypertext Transfer Protocol (HTTP/3) | HyperText Transfer Protocol version 3 | A new generation of web page transmission | Based on the **QUIC** protocol (QUIC itself is built on UDP), designed to reduce latency, solve the TCP head-of-queue blocking problem, and support 0-RTT connection establishment. | +| Dynamic Host Configuration Protocol (DHCP) | Dynamic Host Configuration Protocol | Dynamic allocation of IP addresses and network configuration | The client automatically obtains IP address, subnet mask, gateway, DNS server and other information from the server. | +| Domain Name System (DNS) | Domain Name System | Domain name to IP address resolution | **Usually UDP** is used for fast queries. When the response packet is too large or does a zone transfer (AXFR), **switches to TCP** to ensure data integrity. | +| Real-time Transport Protocol (RTP) | Real-time Transport Protocol | Real-time audio and video data stream transmission | Commonly used in VoIP, video conferencing, live broadcast, etc. Pursue low latency and allow for a small amount of packet loss. Typically used with RTCP. | +| RTP Control Protocol (RTCP) | RTP Control Protocol | Quality monitoring and control information of RTP streams | Works with RTP to provide statistical information such as packet loss, delay, jitter, etc. to assist flow control and congestion management. | +| Trivial File Transfer Protocol (TFTP) | Trivial File Transfer Protocol | Simplified file transfer | Simple function, often used in small file transfer scenarios such as diskless workstation startup in LAN, network device firmware upgrade, etc. || 简单网络管理协议 (SNMP) | Simple Network Management Protocol | 网络设备的监控与管理 | 允许网络管理员查询和修改网络设备的状态信息。 | +| 网络时间协议 (NTP) | Network Time Protocol | 同步计算机时钟 | 用于在网络中的计算机之间同步时间,确保时间的一致性。 | + +**总结一下:** + +- **TCP** 更适合那些对数据**可靠性、完整性和顺序性**要求高的应用,如网页浏览 (HTTP/HTTPS)、文件传输 (FTP/SFTP)、邮件收发 (SMTP/POP3/IMAP)。 +- **UDP** 则更适用于那些对**实时性要求高、能容忍少量数据丢失**的应用,如域名解析 (DNS)、实时音视频 (RTP)、在线游戏、网络管理 (SNMP) 等。 + +### ⭐️TCP 三次握手和四次挥手(非常重要) + +**相关面试题**: + +- 为什么要三次握手? +- 第 2 次握手传回了 ACK,为什么还要传回 SYN? +- 为什么要四次挥手? +- 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? +- 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? +- 为什么第四次挥手客户端需要等待 2\*MSL(报文段最长寿命)时间后才进入 CLOSED 状态? + +**参考答案**:[TCP 三次握手和四次挥手(传输层)](https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html) 。 + +### ⭐️TCP 如何保证传输的可靠性?(重要) + +[TCP 传输可靠性保障(传输层)](https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html) + +## IP + +### IP 协议的作用是什么? + +**IP(Internet Protocol,网际协议)** 是 TCP/IP 协议中最重要的协议之一,属于网络层的协议,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。 + +目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 + +### 什么是 IP 地址?IP 寻址如何工作? + +每个连入互联网的设备或域(如计算机、服务器、路由器等)都被分配一个 **IP 地址(Internet Protocol address)**,作为唯一标识符。每个 IP 地址都是一个字符序列,如 192.168.1.1(IPv4)、2001:0db8:85a3:0000:0000:8a2e:0370:7334(IPv6) 。 + +当网络设备发送 IP 数据包时,数据包中包含了 **源 IP 地址** 和 **目的 IP 地址** 。源 IP 地址用于标识数据包的发送方设备或域,而目的 IP 地址则用于标识数据包的接收方设备或域。这类似于一封邮件中同时包含了目的地地址和回邮地址。 + +网络设备根据目的 IP 地址来判断数据包的目的地,并将数据包转发到正确的目的地网络或子网络,从而实现了设备间的通信。 + +这种基于 IP 地址的寻址方式是互联网通信的基础,它允许数据包在不同的网络之间传递,从而实现了全球范围内的网络互联互通。IP 地址的唯一性和全局性保证了网络中的每个设备都可以通过其独特的 IP 地址进行标识和寻址。 + +![IP 地址使数据包到达其目的地](https://oss.javaguide.cn/github/javaguide/cs-basics/network/internet_protocol_ip_address_diagram.png) + +### 什么是 IP 地址过滤? + +**IP 地址过滤(IP Address Filtering)** 简单来说就是限制或阻止特定 IP 地址或 IP 地址范围的访问。例如,你有一个图片服务突然被某一个 IP 地址攻击,那我们就可以禁止这个 IP 地址访问图片服务。 + +IP 地址过滤是一种简单的网络安全措施,实际应用中一般会结合其他网络安全措施,如认证、授权、加密等一起使用。单独使用 IP 地址过滤并不能完全保证网络的安全。 + +### ⭐️IPv4 和 IPv6 有什么区别? + +**IPv4(Internet Protocol version 4)** 是目前广泛使用的 IP 地址版本,其格式是四组由点分隔的数字,例如:123.89.46.72。IPv4 使用 32 位地址作为其 Internet 地址,这意味着共有约 42 亿( 2^32)个可用 IP 地址。 + +![IPv4](https://oss.javaguide.cn/github/javaguide/cs-basics/network/Figure-1-IPv4Addressformatwithdotteddecimalnotation-29c824f6a451d48d8c27759799f0c995.png) + +这么少当然不够用啦!为了解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议 - **IPv6(Internet Protocol version 6)**。IPv6 地址使用更复杂的格式,该格式使用由单或双冒号分隔的一组数字和字母,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334 。IPv6 使用 128 位互联网地址,这意味着越有 2^128(3 开头的 39 位数字,恐怖如斯) 个可用 IP 地址。 + +![IPv6](https://oss.javaguide.cn/github/javaguide/cs-basics/network/Figure-2-IPv6Addressformatwithhexadecimalnotation-7da3a419bd81627a9b2cef3b0efb4940.png) + +除了更大的地址空间之外,IPv6 的优势还包括: + +- **无状态地址自动配置(Stateless Address Autoconfiguration,简称 SLAAC)**:主机可以直接通过根据接口标识和网络前缀生成全局唯一的 IPv6 地址,而无需依赖 DHCP(Dynamic Host Configuration Protocol)服务器,简化了网络配置和管理。 +- **NAT(Network Address Translation,网络地址转换) 成为可选项**:IPv6 地址资源充足,可以给全球每个设备一个独立的地址。 +- **对标头结构进行了改进**:IPv6 标头结构相较于 IPv4 更加简化和高效,减少了处理开销,提高了网络性能。 +- **可选的扩展头**:允许在 IPv6 标头中添加不同的扩展头(Extension Headers),用于实现不同类型的功能和选项。 +- **ICMPv6(Internet Control Message Protocol for IPv6)**:IPv6 中的 ICMPv6 相较于 IPv4 中的 ICMP 有了一些改进,如邻居发现、路径 MTU 发现等功能的改进,从而提升了网络的可靠性和性能。 +- …… + +### 如何获取客户端真实 IP? + +获取客户端真实 IP 的方法有多种,主要分为应用层方法、传输层方法和网络层方法。 + +**应用层方法** : + +通过 [X-Forwarded-For](https://en.wikipedia.org/wiki/X-Forwarded-For) 请求头获取,简单方便。不过,这种方法无法保证获取到的是真实 IP,这是因为 X-Forwarded-For 字段可能会被伪造。如果经过多个代理服务器,X-Forwarded-For 字段可能会有多个值(附带了整个请求链中的所有代理服务器 IP 地址)。并且,这种方法只适用于 HTTP 和 SMTP 协议。 + +**传输层方法**: + +利用 TCP Options 字段承载真实源 IP 信息。这种方法适用于任何基于 TCP 的协议,不受应用层的限制。不过,这并非是 TCP 标准所支持的,所以需要通信双方都进行改造。也就是:对于发送方来说,需要有能力把真实源 IP 插入到 TCP Options 里面。对于接收方来说,需要有能力把 TCP Options 里面的 IP 地址读取出来。 + +也可以通过 Proxy Protocol 协议来传递客户端 IP 和 Port 信息。这种方法可以利用 Nginx 或者其他支持该协议的反向代理服务器来获取真实 IP 或者在业务服务器解析真实 IP。 + +**网络层方法**: + +隧道 +DSR 模式。这种方法可以适用于任何协议,就是实施起来会比较麻烦,也存在一定限制,实际应用中一般不会使用这种方法。 + +### NAT 的作用是什么? + +**NAT (Network Address Translation)** is mainly used to translate IP addresses between different networks. It allows mapping of private IP addresses (such as the IP addresses used on the LAN) to public IP addresses (IP addresses used on the Internet) or vice versa, allowing multiple devices within the LAN to access the Internet through a single public IP address. + +NAT can not only alleviate the shortage of IPv4 address resources, but also hide the actual topology of the internal network, making it impossible for external networks to directly access devices in the internal network, thereby improving the security of the internal network. + +![NAT implements IP address translation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-address-translation.png) + +Related reading: [Detailed explanation of NAT protocol (network layer)](https://javaguide.cn/cs-basics/network/nat.html). + +## ARP + +### What is a Mac address? + +The full name of MAC address is **Media Access Control Address**. If every resource on the Internet is uniquely identified by an IP address (IP protocol content), then all network devices are uniquely identified by a MAC address. + +![The MAC address will be indicated on the back of the router](https://oss.javaguide.cn/github/javaguide/cs-basics/network/router-back-will-indicate-mac-address.png) + +It can be understood that the MAC address is the real ID number of a network device, and the IP address is just a non-duplicate positioning method (for example, for Zhang San who lives in a certain street in a certain city in a certain province, this logical positioning is the IP address, and his ID number is his MAC address). It can also be understood that the MAC address is the ID number and the IP address is the postal address. The MAC address also has some other names, such as LAN address, physical address, Ethernet address, etc. + +> Another thing to know is that not only network resources have IP addresses, but network devices also have IP addresses, such as routers. But structurally speaking, the role of network devices such as routers is to form a network, and it is usually an intranet, so the IP addresses they use are usually intranet IPs. When intranet devices communicate with devices outside the intranet, they need to use the NAT protocol. + +The length of the MAC address is 6 bytes (48 bits), and the address space size is as much as 280 trillion ($2^{48}$). The MAC address is managed and allocated by the IEEE. In theory, the MAC address on the network card in a network device is permanent. Different network card manufacturers purchase their own MAC address space (the first 24 bits of the MAC) from the IEEE, that is, the first 24 bits are managed uniformly by the IEEE to ensure no duplication. The next 24 bits are managed by each manufacturer themselves to ensure that the MAC addresses of the two network cards produced will not be repeated. + +The MAC address is portable and permanent, and the ID number permanently identifies a person's identity and will not change no matter where he goes. IP addresses do not have these properties. When a device changes networks, its IP address may change, which means its positioning on the Internet changes. + +Finally, remember that MAC addresses have a special address: FF-FF-FF-FF-FF-FF (all ones), which represents the broadcast address. + +### ⭐️What problems does the ARP protocol solve? + +ARP protocol, the full name is **Address Resolution Protocol**, which solves the problem of conversion between network layer addresses and link layer addresses. Because during the physical transmission of an IP datagram, you always need to know where the next hop (physical next destination) should go, but the IP address is a logical address, and the MAC address is the physical address. The ARP protocol solves some problems of converting IP addresses to MAC addresses. + +### How does the ARP protocol work? + +[Detailed explanation of ARP protocol (network layer)](https://javaguide.cn/cs-basics/network/arp.html) + +## Review suggestions + +I highly recommend everyone to read the book "HTTP Illustrated". This book does not have many pages, but the content is very substantial. It is very helpful whether it is used to systematically master some knowledge about the network or simply to cope with interviews. Some of the articles below are for reference only. When I was studying this course in my sophomore year, the textbook we used was "Computer Networks 7th Edition" (edited by Xie Xiren). I don't recommend everyone to read this textbook. The book is very thick and the knowledge is theoretical. I'm not sure if you can finish it calmly. + +## Reference + +- "HTTP Illustrated" +- "Top-Down Method of Computer Networks" (Seventh Edition) +- What is Internet Protocol (IP)? : +- Various methods to transparently transmit the real source IP - Geek Time: +- What Is NAT and What Are the Benefits of NAT Firewalls?: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/tcp-connection-and-disconnection.en.md b/docs_en/cs-basics/network/tcp-connection-and-disconnection.en.md new file mode 100644 index 00000000000..65c705e5243 --- /dev/null +++ b/docs_en/cs-basics/network/tcp-connection-and-disconnection.en.md @@ -0,0 +1,134 @@ +--- +title: TCP 三次握手和四次挥手(传输层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: TCP,三次握手,四次挥手,状态机,SYN,ACK,FIN,半连接队列,全连接队列 + - - meta + - name: description + content: 详解 TCP 建连与断连过程,结合状态迁移与队列机制解析可靠通信保障与高并发连接处理。 +--- + +TCP 是一种面向连接的、可靠的传输层协议。为了在两个不可靠的端点之间建立一个可靠的连接,TCP 采用了三次握手(Three-way Handshake)的策略。 + +## 建立连接-TCP 三次握手 + +![TCP 三次握手图解](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-shakes-hands-three-times.png) + +建立一个 TCP 连接需要“三次握手”,缺一不可: + +1. **第一次握手 (SYN)**: 客户端向服务端发送一个 SYN(Synchronize Sequence Numbers)报文段,其中包含一个由客户端随机生成的初始序列号(Initial Sequence Number, ISN),例如 seq=x。发送后,客户端进入 **SYN_SEND** 状态,等待服务端的确认。 +2. **第二次握手 (SYN+ACK)**: 服务端收到 SYN 报文段后,如果同意建立连接,会向客户端回复一个确认报文段。该报文段包含两个关键信息: + - **SYN**:服务端也需要同步自己的初始序列号,因此报文段中也包含一个由服务端随机生成的初始序列号,例如 seq=y。 + - **ACK** (Acknowledgement):用于确认收到了客户端的请求。其确认号被设置为客户端初始序列号加一,即 ack=x+1。 + - 发送该报文段后,服务端进入 **SYN_RECV** 状态。 +3. **第三次握手 (ACK)**: 客户端收到服务端的 SYN+ACK 报文段后,会向服务端发送一个最终的确认报文段。该报文段包含确认号 ack=y+1。发送后,客户端进入 **ESTABLISHED** 状态。服务端收到这个 ACK 报文段后,也进入 **ESTABLISHED** 状态。 + +至此,双方都确认了连接的建立,TCP 连接成功创建,可以开始进行双向数据传输。 + +### 什么是半连接队列和全连接队列? + +在 TCP 三次握手过程中,服务端内核会使用两个队列来管理连接请求: + +1. **半连接队列**(也称 SYN Queue):当服务端收到客户端的 SYN 请求并回复 SYN+ACK 后,连接会处于 SYN_RECV 状态。此时,这个连接信息会被放入半连接队列。这个队列存储的是尚未完成三次握手的连接。 +2. **全连接队列**(也称 Accept Queue):当服务端收到客户端对 ACK 响应时,意味着三次握手成功完成,服务端会将该连接从半连接队列移动到全连接队列。如果未收到客户端的 ACK 响应,会进行重传,重传的等待时间通常是指数增长的。如果重传次数超过系统规定的最大重传次数,系统将从半连接队列中删除该连接信息。 + +这两个队列的存在是为了处理并发连接请求,确保服务端能够有效地管理新的连接请求。 + +如果全连接队列满了,新的已完成握手的连接可能会被丢弃,或者触发其他策略。这两个队列的大小都受系统参数控制,它们的容量限制是影响服务器处理高并发连接能力的重要因素,也是 SYN 泛洪攻击(SYN Flood)所针对的目标。 + +### 为什么要三次握手? + +TCP 三次握手的核心目的是为了在客户端和服务器之间建立一个**可靠的**、**全双工的**通信信道。这需要实现两个主要目标: + +**1. 确认双方的收发能力,并同步初始序列号 (ISN)** + +TCP 通信依赖序列号来保证数据的有序和可靠。三次握手是双方交换和确认彼此初始序列号(ISN)的过程,通过这个过程,双方也间接验证了各自的收发能力。 + +- **第一次握手 (客户端 → 服务器)** :客户端发送 SYN 包。 + - 服务器:能确认客户端的发送能力正常,自己的接收能力正常。 + - 客户端:无法确认任何事。 +- **第二次握手 (服务器 → 客户端)** :服务器回复 SYN+ACK 包。 + - 客户端:能确认自己的发送和接收能力正常,服务器的接收和发送能力正常。 + - 服务端:能确认对方发送能力正常,自己接收能力正常 +- **第三次握手 (客户端 → 服务器)** :客户端发送 ACK 包。 + - 客户端:能确认双方发送和接收能力正常。 + - 服务端:能确认双方发送和接收能力正常。 + +经过这三次交互,双方都确认了彼此的收发功能完好,并完成了初始序列号的同步,为后续可靠的数据传输奠定了基础。 + +**2. 防止已失效的连接请求被错误地建立** + +这是“为什么不能是两次握手”的关键原因。 + +设想一个场景:客户端发送的第一个连接请求(SYN1)因网络延迟而滞留,于是客户端重发了第二个请求(SYN2)并成功建立了连接,数据传输完毕后连接被释放。此时,延迟的 SYN1 才到达服务端。 + +- **如果是两次握手**:服务端收到这个失效的 SYN1 后,会误认为是一个新的连接请求,并立即分配资源、建立连接。但这将导致服务端单方面维持一个无效连接,白白浪费系统资源,因为客户端并不会有任何响应。 +- **有了第三次握手**:服务端收到失效的 SYN1 并回复 SYN+ACK 后,会等待客户端的最终确认(ACK)。由于客户端当前并没有发起连接的意图,它会忽略这个 SYN+ACK 或者发送一个 RST (Reset) 报文。这样,服务端就无法收到第三次握手的 ACK,最终会超时关闭这个错误的连接,从而避免了资源浪费。 + +因此,三次握手是确保 TCP 连接可靠性的**最小且必需**的步骤。它不仅确认了双方的通信能力,更重要的是增加了一个最终确认环节,以防止网络中延迟、重复的历史请求对连接建立造成干扰。 + +### 第 2 次握手传回了 ACK,为什么还要传回 SYN? + +服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。 + +> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务端之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务端使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务端之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务端之间传递。 + +### 三次握手过程中可以携带数据吗? + +在 TCP 三次握手过程中,第三次握手是可以携带数据的(客户端发送完 ACK 确认包之后就进入 ESTABLISHED 状态了),这一点在 RFC 793 文档中有提到。也就是说,一旦完成了前两次握手,TCP 协议允许数据在第三次握手时开始传输。 + +如果第三次握手的 ACK 确认包丢失,但是客户端已经开始发送携带数据的包,那么服务端在收到这个携带数据的包时,如果该包中包含了 ACK 标记,服务端会将其视为有效的第三次握手确认。这样,连接就被认为是建立的,服务端会处理该数据包,并继续正常的数据传输流程。 + +## 断开连接-TCP 四次挥手 + +![TCP 四次挥手图解](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-waves-four-times.png) + +断开一个 TCP 连接则需要“四次挥手”,缺一不可: + +1. **第一次挥手 (FIN)**:当客户端(或任何一方)决定关闭连接时,它会向服务端发送一个 **FIN**(Finish)标志的报文段,表示自己已经没有数据要发送了。该报文段包含一个序列号 seq=u。发送后,客户端进入 **FIN-WAIT-1** 状态。 +2. **第二次挥手 (ACK)**:服务端收到 FIN 报文段后,会立即回复一个 **ACK** 确认报文段。其确认号为 ack=u+1。发送后,服务端进入 **CLOSE-WAIT** 状态。客户端收到这个 ACK 后,进入 **FIN-WAIT-2** 状态。此时,TCP 连接处于**半关闭(Half-Close)**状态:客户端到服务端的发送通道已关闭,但服务端到客户端的发送通道仍然可以传输数据。 +3. **第三次挥手 (FIN)**:当服务端确认所有待发送的数据都已发送完毕后,它也会向客户端发送一个 **FIN** 报文段,表示自己也准备关闭连接。该报文段同样包含一个序列号 seq=y。发送后,服务端进入 **LAST-ACK** 状态,等待客户端的最终确认。 +4. **The fourth wave**: After receiving the FIN message segment from the server, the client will reply with a final **ACK** confirmation message segment, the confirmation number is ack=y+1. After sending, the client enters the **TIME-WAIT** state. After receiving this ACK, the server immediately enters the **CLOSED** state to complete the connection closure. The client will wait for **2MSL** (Maximum Segment Lifetime, the maximum survival time of the message segment) in the **TIME-WAIT** state before finally entering the **CLOSED** state. + +**As long as the four waves are not over, the client and server can continue to transmit data! ** + +### Why wave four times? + +TCP is full-duplex communication and can transmit data in both directions. Either party can issue a connection release notification after the data transmission is completed, and enter a semi-closed state after the other party confirms. When the other party has no more data to send, a connection release notification is issued. After the other party confirms, the TCP connection is completely closed. + +For example: A and B are on the phone and the call is about to end. + +1. **First wave**: A said "I have nothing more to say" +2. **Second wave**: B answers "I understand", but B may have something else to say, and A cannot ask B to end the call at his own pace. +3. **Waving for the third time**: Then B may have talked for a while, and finally B said "I'm done" +4. **The fourth wave**: Person A answers "I know", and the call ends. + +### Why can't the ACK and FIN sent by the server be combined into three waves? + +Because when the server receives the client's request to disconnect, there may still be some data that has not been sent. At this time, it responds with ACK first, indicating that it has received the disconnect request. Wait until the data is sent before sending FIN to disconnect the data transmission from the server to the client. + +### What will happen if the ACK from the server is not delivered to the client when waving for the second time? + +The client starts a retransmission timer after sending the FIN. If no ACK from the server is received before the timer expires, the client will consider the FIN message lost and resend the FIN message. + +### Why does the client need to wait for 2\*MSL (maximum segment life) time before entering the CLOSED state after the fourth wave? + +When waving for the fourth time, the ACK sent by the client to the server may be lost. If the server does not receive the ACK for some reason, the server will resend the FIN. If the client receives the FIN within 2\*MSL, it will resend the ACK and wait for 2MSL again to prevent the server from continuously resending FIN without receiving the ACK. + +> **MSL (Maximum Segment Lifetime)**: The maximum survival time of a segment in the network. 2MSL is the maximum time required for a send and a reply. If the Client does not receive the FIN again until 2MSL, the Client concludes that the ACK has been successfully received and ends the TCP connection. + +## Reference + +- "Computer Networks (7th Edition)" + +- "HTTP Illustrated" + +- TCP and UDP Tutorial: + +- Starting from an online problem, a detailed explanation of TCP semi-connection queue and full connection queue: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/tcp-reliability-guarantee.en.md b/docs_en/cs-basics/network/tcp-reliability-guarantee.en.md new file mode 100644 index 00000000000..f1ef37bd3d3 --- /dev/null +++ b/docs_en/cs-basics/network/tcp-reliability-guarantee.en.md @@ -0,0 +1,135 @@ +--- +title: TCP 传输可靠性保障(传输层) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: TCP,可靠性,重传,SACK,流量控制,拥塞控制,滑动窗口,校验和 + - - meta + - name: description + content: 系统梳理 TCP 的可靠性保障机制,覆盖重传/选择确认、流量与拥塞控制,明确端到端可靠传输的实现要点。 +--- + +## TCP 如何保证传输的可靠性? + +1. **基于数据块传输**:应用数据被分割成 TCP 认为最适合发送的数据块,再传输给网络层,数据块被称为报文段或段。 +2. **对失序数据包重新排序以及去重**:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。 +3. **校验和** : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 +4. **重传机制** : 在数据包丢失或延迟的情况下,重新发送数据包,直到收到对方的确认应答(ACK)。TCP 重传机制主要有:基于计时器的重传(也就是超时重传)、快速重传(基于接收端的反馈信息来引发重传)、SACK(在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了)、D-SACK(重复 SACK,在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了)。关于重传机制的详细介绍,可以查看[详解 TCP 超时与重传机制](https://zhuanlan.zhihu.com/p/101702312)这篇文章。 +5. **流量控制** : TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。 +6. **拥塞控制** : 当网络拥塞时,减少数据的发送。TCP 在发送数据的时候,需要考虑两个因素:一是接收方的接收能力,二是网络的拥塞程度。接收方的接收能力由滑动窗口表示,表示接收方还有多少缓冲区可以用来接收数据。网络的拥塞程度由拥塞窗口表示,它是发送方根据网络状况自己维护的一个值,表示发送方认为可以在网络中传输的数据量。发送方发送数据的大小是滑动窗口和拥塞窗口的最小值,这样可以保证发送方既不会超过接收方的接收能力,也不会造成网络的过度拥塞。 + +## TCP 如何实现流量控制? + +**TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。** 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 + +**为什么需要流量控制?** 这是因为双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来。如果接收方处理不过来的话,就只能把处理不过来的数据存在 **接收缓冲区(Receiving Buffers)** 里(失序的数据包也会被存放在缓存区里)。如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。 + +这里需要注意的是(常见误区): + +- 发送端不等同于客户端 +- 接收端不等同于服务端 + +TCP 为全双工(Full-Duplex, FDX)通信,双方可以进行双向通信,客户端和服务端既可能是发送端又可能是服务端。因此,两端各有一个发送缓冲区与接收缓冲区,两端都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制(TCP 传输速率不能大于应用的数据处理速率)。通信双方的发送窗口和接收窗口的要求相同 + +**TCP 发送窗口可以划分成四个部分**: + +1. 已经发送并且确认的 TCP 段(已经发送并确认); +2. 已经发送但是没有确认的 TCP 段(已经发送未确认); +3. 未发送但是接收方准备接收的 TCP 段(可以发送); +4. 未发送并且接收方也并未准备接受的 TCP 段(不可发送)。 + +**TCP 发送窗口结构图示**: + +![TCP发送窗口结构](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-send-window.png) + +- **SND.WND**:发送窗口。 +- **SND.UNA**:Send Unacknowledged 指针,指向发送窗口的第一个字节。 +- **SND.NXT**:Send Next 指针,指向可用窗口的第一个字节。 + +**可用窗口大小** = `SND.UNA + SND.WND - SND.NXT` 。 + +**TCP 接收窗口可以划分成三个部分**: + +1. 已经接收并且已经确认的 TCP 段(已经接收并确认); +2. 等待接收且允许发送方发送 TCP 段(可以接收未确认); +3. 不可接收且不允许发送方发送 TCP 段(不可接收)。 + +**TCP 接收窗口结构图示**: + +![TCP接收窗口结构](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-receive-window.png) + +**接收窗口的大小是根据接收端处理数据的速度动态调整的。** 如果接收端读取数据快,接收窗口可能会扩大。 否则,它可能会缩小。 + +另外,这里的滑动窗口大小只是为了演示使用,实际窗口大小通常会远远大于这个值。 + +## TCP 的拥塞控制是怎么实现的? + +在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 + +![TCP的拥塞控制](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-congestion-control.png) + +为了进行拥塞控制,TCP 发送方要维持一个 **拥塞窗口(cwnd)** 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。 + +TCP 的拥塞控制采用了四种算法,即 **慢开始**、 **拥塞避免**、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 + +- **慢开始:** 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。 +- **拥塞避免:** 拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1. +- **快重传与快恢复:** 在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。 + +## ARQ 协议了解吗? + +**自动重传请求**(Automatic Repeat-reQuest,ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认信息(Acknowledgements,就是我们常说的 ACK),它通常会重新发送,直到收到确认或者重试超过一定的次数。 + +ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。 + +### 停止等待 ARQ 协议 + +The stop-and-wait protocol is to achieve reliable transmission. Its basic principle is to stop sending every time a packet is sent and wait for the other party to confirm (reply ACK). If after a period of time (after the timeout period), the ACK confirmation is still not received, it means that the transmission was not successful and needs to be resent until the confirmation is received before sending the next packet; + +In the stop-and-wait protocol, if the receiver receives a duplicate packet, it discards the packet, but also sends an acknowledgment at the same time. + +**1) No error situation:** + +The sender sends the packet, the receiver receives it within the specified time, and replies with confirmation. The sender sends again. + +**2) Error occurs (timeout retransmission):** + +Timeout retransmission in the stop-and-wait protocol means that as long as no confirmation is received after a period of time, the previously sent packet will be retransmitted (the packet just sent is considered to be lost). Therefore, a timeout timer needs to be set after each packet is sent, and its retransmission time should be longer than the average round-trip time of data in packet transmission. This automatic retransmission method is often called **Automatic Repeat Request ARQ**. In addition, in the stop-and-wait protocol, if a duplicate packet is received, the packet is discarded, but an acknowledgment is also sent at the same time. + +**3) Confirmation lost and confirmation late** + +- **Confirmation Lost**: The confirmation message is lost during transmission. When A sends an M1 message and B receives it, B sends an M1 confirmation message to A, but it is lost during transmission. However, A does not know that after the timeout, A retransmits the M1 message, and B takes the following two measures after receiving the message again: 1. Discard the duplicate M1 message and do not deliver it to the upper layer. 2. Send a confirmation message to A. (It will not be considered that it has been sent, so it will not be sent again. If A can retransmit, it proves that B's confirmation message is lost). +- **ACKLATE**: The acknowledgment message was late during transmission. A sends M1 message, B receives and sends confirmation. If no acknowledgment message is received within the timeout period, A retransmits the M1 message, and B still receives and continues to send the acknowledgment message (B received 2 copies of M1). At this time, A receives the confirmation message sent by B for the second time. Then send other data. After a while, A received the first confirmation message for M1 sent by B (A also received 2 confirmation messages). The processing is as follows: 1. After A receives the duplicate confirmation, it discards it directly. 2. After B receives the duplicate M1, it also discards the duplicate M1 directly. + +### Continuous ARQ protocol + +The continuous ARQ protocol improves channel utilization. The sender maintains a sending window, and packets within the sending window can be sent continuously without waiting for confirmation from the other party. The receiver generally uses cumulative acknowledgment and sends an acknowledgment to the last packet that arrives in sequence, indicating that all packets up to this packet have been received correctly. + +- **Advantages:** The channel utilization rate is high and easy to implement. Even if the confirmation is lost, there is no need to retransmit. +- **Disadvantage:** It cannot reflect to the sender the information of all packets that the receiver has correctly received. For example: the sender sends 5 messages, and the third one is lost (No. 3). At this time, the receiver can only send confirmations to the first two. The sender has no way of knowing the whereabouts of the last three packets, and has to retransmit them all once. This is also called Go-Back-N, which means that you need to go back and retransmit the N messages that have been sent. + +## How to implement timeout retransmission? How to determine the timeout and retransmission time? + +After the sender sends the data, it starts a timer and waits for the destination to confirm receipt of the segment. The receiving entity sends back a corresponding acknowledgment message (ACK) for the successfully received packet. If the sending entity does not receive the acknowledgment message within a reasonable round-trip delay (RTT), the corresponding data packet is assumed to be [lost](https://zh.wikipedia.org/wiki/PacketLost) and retransmitted. + +- RTT (Round Trip Time): round trip time, that is, the time from when a data packet is sent to when the corresponding ACK is received. +- RTO (Retransmission Time Out): Retransmission timeout, which is calculated from the time when the data is sent. Retransmission will be performed after this time. + +The determination of RTO is a key issue because it directly affects the performance and efficiency of TCP. If the RTO is set too small, it will cause unnecessary retransmissions and increase network burden; if the RTO is set too large, it will cause delays in data transmission and reduce throughput. Therefore, RTO should be dynamically adjusted based on the actual network conditions. + +The value of RTT will change with network fluctuations, so TCP cannot directly use RTT as RTO. In order to dynamically adjust RTO, the TCP protocol uses some algorithms, such as the weighted moving average (EWMA) algorithm, Karn algorithm, Jacobson algorithm, etc. These algorithms all estimate the value of RTO based on the measurement and changes of the round-trip delay (RTT). + +## Reference + +1. "Computer Networks (7th Edition)" +2. "HTTP Illustrated" +3. [https://www.9tut.com/tcp-and-udp-tutorial](https://www.9tut.com/tcp-and-udp-tutorial) +4. [https://github.com/wolverinn/Waking-Up/blob/master/Computer%20Network.md](https://github.com/wolverinn/Waking-Up/blob/master/Computer%20Network.md) +5. TCP Flow Control—[https://www.brianstorti.com/tcp-flow-control/](https://www.brianstorti.com/tcp-flow-control/) +6. TCP flow control: +7. TCP sliding window principle: + + \ No newline at end of file diff --git a/docs_en/cs-basics/network/the-whole-process-of-accessing-web-pages.en.md b/docs_en/cs-basics/network/the-whole-process-of-accessing-web-pages.en.md new file mode 100644 index 00000000000..697228a32a9 --- /dev/null +++ b/docs_en/cs-basics/network/the-whole-process-of-accessing-web-pages.en.md @@ -0,0 +1,87 @@ +--- +title: 访问网页的全过程(知识串联) +category: 计算机基础 +tag: + - 计算机网络 +head: + - - meta + - name: keywords + content: 访问网页流程,DNS,TCP 建连,HTTP 请求,资源加载,渲染,关闭连接 + - - meta + - name: description + content: 串联从输入 URL 到页面渲染的完整链路,涵盖 DNS、TCP、HTTP 与静态资源加载,助力面试与实践理解。 +--- + +开发岗中总是会考很多计算机网络的知识点,但如果让面试官只考一道题,便涵盖最多的计网知识点,那可能就是 **网页浏览的全过程** 了。本篇文章将带大家从头到尾过一遍这道被考烂的面试题,必会!!! + +总的来说,网络通信模型可以用下图来表示,也就是大家只要熟记网络结构五层模型,按照这个体系,很多知识点都能顺出来了。访问网页的过程也是如此。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/five-layers.png) + +开始之前,我们先简单过一遍完整流程: + +1. 在浏览器中输入指定网页的 URL。 +2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。 +3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。 +4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。 +5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。 +6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。 +7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。 + +## 应用层 + +一切的开始——打开浏览器,在地址栏输入 URL,回车确认。那么,什么是 URL?访问 URL 有什么用? + +### URL + +URL(Uniform Resource Locators),即统一资源定位器。网络上的所有资源都靠 URL 来定位,每一个文件就对应着一个 URL,就像是路径地址。理论上,文件资源和 URL 一一对应。实际上也有例外,比如某些 URL 指向的文件已经被重定位到另一个位置,这样就有多个 URL 指向同一个文件。 + +### URL 的组成结构 + +![URL的组成结构](https://oss.javaguide.cn/github/javaguide/cs-basics/network/URL-parts.png) + +1. 协议。URL 的前缀通常表示了该网址采用了何种应用层协议,通常有两种——HTTP 和 HTTPS。当然也有一些不太常见的前缀头,比如文件传输时用到的`ftp:`。 +2. 域名。域名便是访问网址的通用名,这里也有可能是网址的 IP 地址,域名可以理解为 IP 地址的可读版本,毕竟绝大部分人都不会选择记住一个网址的 IP 地址。 +3. 端口。如果指明了访问网址的端口的话,端口会紧跟在域名后面,并用一个冒号隔开。 +4. 资源路径。域名(端口)后紧跟的就是资源路径,从第一个`/`开始,表示从服务器上根目录开始进行索引到的文件路径,上图中要访问的文件就是服务器根目录下`/path/to/myfile.html`。早先的设计是该文件通常物理存储于服务器主机上,但现在随着网络技术的进步,该文件不一定会物理存储在服务器主机上,有可能存放在云上,而文件路径也有可能是虚拟的(遵循某种规则)。 +5. 参数。参数是浏览器在向服务器提交请求时,在 URL 中附带的参数。服务器解析请求时,会提取这些参数。参数采用键值对的形式`key=value`,每一个键值对使用`&`隔开。参数的具体含义和请求操作的具体方法有关。 +6. 锚点。锚点顾名思义,是在要访问的页面上的一个锚。要访问的页面大部分都多于一页,如果指定了锚点,那么在客户端显示该网页是就会定位到锚点处,相当于一个小书签。值得一提的是,在 URL 中,锚点以`#`开头,并且**不会**作为请求的一部分发送给服务端。 + +### DNS + +键入了 URL 之后,第一个重头戏登场——DNS 服务器解析。DNS(Domain Name System)域名系统,要解决的是 **域名和 IP 地址的映射问题** 。毕竟,域名只是一个网址便于记住的名字,而网址真正存在的地址其实是 IP 地址。 + +传送门:[DNS 域名系统详解(应用层)](https://javaguide.cn/cs-basics/network/dns.html) + +### HTTP/HTTPS + +利用 DNS 拿到了目标主机的 IP 地址之后,浏览器便可以向目标 IP 地址发送 HTTP 报文,请求需要的资源了。在这里,根据目标网站的不同,请求报文可能是 HTTP 协议或安全性增强的 HTTPS 协议。 + +传送门: + +- [HTTP vs HTTPS(应用层)](https://javaguide.cn/cs-basics/network/http-vs-https.html) +- [HTTP 1.0 vs HTTP 1.1(应用层)](https://javaguide.cn/cs-basics/network/http1.0-vs-http1.1.html) +- [HTTP 常见状态码总结(应用层)](https://javaguide.cn/cs-basics/network/http-status-codes.html) + +## 传输层 + +由于 HTTP 协议是基于 TCP 协议的,在应用层的数据封装好以后,要交给传输层,经 TCP 协议继续封装。 + +TCP 协议保证了数据传输的可靠性,是数据包传输的主力协议。 + +传送门: + +- [TCP 三次握手和四次挥手(传输层)](https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html) +- [TCP 传输可靠性保障(传输层)](https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html) + +## 网络层 + +终于,来到网络层,此时我们的主机不再是和另一台主机进行交互了,而是在和中间系统进行交互。也就是说,应用层和传输层都是端到端的协议,而网络层及以下都是中间件的协议了。 + +**网络层的的核心功能——转发与路由**,必会!!!如果面试官问到了网络层,而你恰好又什么都不会的话,最最起码要说出这五个字——**转发与路由**。 + +- 转发:将分组从路由器的输入端口转移到合适的输出端口。 +- 路由:确定分组从源到目的经过的路径。 + +所以到目前为止,我们的数据包经过了应用层、传输层的封装,来到了网络层,终于开始准备在物理层面传输了,第一个要解决的问题就是——**往哪里传输?或者说,要把数据包发到哪个路由器上?** 这便是 BGP 协议要解决的问题。 + diff --git a/docs_en/cs-basics/operating-system/linux-intro.en.md b/docs_en/cs-basics/operating-system/linux-intro.en.md new file mode 100644 index 00000000000..12d4dd29dd5 --- /dev/null +++ b/docs_en/cs-basics/operating-system/linux-intro.en.md @@ -0,0 +1,437 @@ +--- +title: Linux 基础知识总结 +category: 计算机基础 +tag: + - 操作系统 + - Linux +head: + - - meta + - name: description + content: 简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。 + - - meta + - name: keywords + content: Linux,基础命令,发行版,文件系统,权限,进程,网络 +--- + + + +简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。 + +## 初探 Linux + +### Linux 简介 + +通过以下三点可以概括 Linux 到底是什么: + +- **类 Unix 系统**:Linux 是一种自由、开放源码的类似 Unix 的操作系统 +- **Linux 本质是指 Linux 内核**:严格来讲,Linux 这个词本身只表示 Linux 内核,单独的 Linux 内核并不能成为一个可以正常工作的操作系统。所以,就有了各种 Linux 发行版。 +- **Linux 之父(林纳斯·本纳第克特·托瓦兹 Linus Benedict Torvalds)**:一个编程领域的传奇式人物,真大佬!我辈崇拜敬仰之楷模。他是 **Linux 内核** 的最早作者,随后发起了这个开源项目,担任 Linux 内核的首要架构师。他还发起了 Git 这个开源项目,并为主要的开发者。 + +![Linux 之父](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/linux-father.png) + +### Linux 诞生 + +1989 年,Linus Torvalds 进入芬兰陆军新地区旅,服 11 个月的国家义务兵役,军衔为少尉,主要服务于计算机部门,任务是弹道计算。服役期间,购买了安德鲁·斯图尔特·塔能鲍姆所著的教科书及 minix 源代码,开始研究操作系统。1990 年,他退伍后回到大学,开始接触 Unix。 + +> **Minix** 是一个迷你版本的类 Unix 操作系统,由塔能鲍姆教授为了教学之用而创作,采用微核心设计。它启发了 Linux 内核的创作。 + +1991 年,Linus Torvalds 开源了 Linux 内核。Linux 以一只可爱的企鹅作为标志,象征着敢作敢为、热爱生活。 + +![OPINION: Make the switch to a Linux operating system | Opinion ...](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/Linux-Logo.png) + +### 常见的 Linux 发行版本 + +![Linux 操作系统](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/linux.png) + +Linus Torvalds 开源的只是 Linux 内核,我们上面也提到了操作系统内核的作用。一些组织或厂商将 Linux 内核与各种软件和文档包装起来,并提供系统安装界面和系统配置、设定与管理工具,就构成了 Linux 的发行版本。 + +> 内核主要负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 + +Linux 的发行版本可以大体分为两类: + +- **商业公司维护的发行版本**:比如 Red Hat 公司维护支持的 Red Hat Enterprise Linux (RHEL)。 +- **社区组织维护的发行版本**:比如基于 Red Hat Enterprise Linux(RHEL)的 CentOS、基于 Debian 的 Ubuntu。 + +对于初学者学习 Linux ,推荐选择 CentOS,原因如下: + +- CentOS 免费且开放源代码; +- CentOS 基于 RHEL,功能与 RHEL 高度一致,安全稳定、性能优秀。 + +## Linux 文件系统 + +### Linux 文件系统简介 + +在 Linux 操作系统中,一切被操作系统管理的资源,如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或目录等,都被视为文件。这是 Linux 系统中一个重要的概念,即"一切都是文件"。 + +这种概念源自 UNIX 哲学,即将所有资源都抽象为文件的方式来进行管理和访问。Linux 的文件系统也借鉴了 UNIX 文件系统的设计理念。这种设计使得 Linux 系统可以通过统一的文件接口来管理和操作不同类型的资源,从而实现了一种统一的文件操作方式。例如,可以使用类似于读写文件的方式来对待网络接口、磁盘驱动器、设备文件等,使得操作和管理这些资源更加统一和简便。 + +这种文件为中心的设计理念为 Linux 系统带来了灵活性和可扩展性,使得 Linux 成为一种强大的操作系统。同时,这也是 Linux 系统的一大特点,深受广大用户和开发者的喜欢和推崇。 + +### inode 介绍 + +inode 是 Linux/Unix 文件系统的基础。那 inode 到是什么?有什么作用呢? + +通过以下五点可以概括 inode 到底是什么: + +1. 硬盘以扇区 (Sector) 为最小物理存储单位,而操作系统和文件系统以块 (Block) 为单位进行读写,块由多个扇区组成。文件数据存储在这些块中。现代硬盘扇区通常为 4KB,与一些常见块大小相同,但操作系统也支持更大的块大小,以提升大文件读写性能。文件元信息(例如权限、大小、修改时间以及数据块位置)存储在 inode(索引节点)中。每个文件都有唯一的 inode。inode 本身不存储文件数据,而是存储指向数据块的指针,操作系统通过这些指针找到并读取文件数据。 固态硬盘 (SSD) 虽然没有物理扇区,但使用逻辑块,其概念与传统硬盘的块类似。 +2. inode 是一种固定大小的数据结构,其大小在文件系统创建时就确定了,并且在文件的生命周期内保持不变。 +3. inode 的访问速度非常快,因为系统可以直接通过 inode 号码定位到文件的元数据信息,无需遍历整个文件系统。 +4. inode 的数量是有限的,每个文件系统只能包含固定数量的 inode。这意味着当文件系统中的 inode 用完时,无法再创建新的文件或目录,即使磁盘上还有可用空间。因此,在创建文件系统时,需要根据文件和目录的预期数量来合理分配 inode 的数量。 +5. 可以使用 `stat` 命令可以查看文件的 inode 信息,包括文件的 inode 号、文件类型、权限、所有者、文件大小、修改时间。 + +简单来说:inode 就是用来维护某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等信息。 + +再总结一下 inode 和 block: + +- **inode**:记录文件的属性信息,可以使用 `stat` 命令查看 inode 信息。 +- **block**:实际文件的内容,如果一个文件大于一个块时候,那么将占用多个 block,但是一个块只能存放一个文件。(因为数据是由 inode 指向的,如果有两个文件的数据存放在同一个块中,就会乱套了) + +![文件inode信息](./images/文件inode信息.png) + +可以看出,Linux/Unix 操作系统使用 inode 区分不同的文件。这样做的好处是,即使文件名被修改或删除,文件的 inode 号码不会改变,从而可以避免一些因文件重命名、移动或删除导致的错误。同时,inode 也可以提供更高的文件系统性能,因为 inode 的访问速度非常快,可以直接通过 inode 号码定位到文件的元数据信息,无需遍历整个文件系统。 + +不过,使用 inode 号码也使得文件系统在用户和应用程序层面更加抽象和复杂,需要通过系统命令或文件系统接口来访问和管理文件的 inode 信息。 + +### 硬链接和软链接 + +在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种: + +**1、硬链接(Hard Link)** + +- 在 Linux/类 Unix 文件系统中,每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。硬链接通过 inode 节点号建立连接,硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的(可以看作是互为硬链接,源头是同一份文件),删除其中任何一个对另外一个没有影响,可以通过给文件设置硬链接文件来防止重要文件被误删。 +- 只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。 +- 硬链接具有一些限制,不能对目录以及不存在的文件创建硬链接,并且,硬链接也不能跨越文件系统。 +- `ln` 命令用于创建硬链接。 + +**2、软链接(Symbolic Link 或 Symlink)** + +- 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。 +- 源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。 +- 软连接类似于 Windows 系统中的快捷方式。 +- 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。 +- `ln -s` 命令用于创建软链接。 + +**硬链接为什么不能跨文件系统?** + +我们之前提到过,硬链接是通过 inode 节点号建立连接的,而硬链接和源文件共享相同的 inode 节点号。 + +然而,每个文件系统都有自己的独立 inode 表,且每个 inode 表只维护该文件系统内的 inode。如果在不同的文件系统之间创建硬链接,可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。 + +### Linux 文件类型 + +Linux 支持很多文件类型,其中非常重要的文件类型有: **普通文件**,**目录文件**,**链接文件**,**设备文件**,**管道文件**,**Socket 套接字文件** 等。 + +- **普通文件(-)**:用于存储信息和数据, Linux 用户可以根据访问权限对普通文件进行查看、更改和删除。比如:图片、声音、PDF、text、视频、源代码等等。 +- **目录文件(d,directory file)**:目录也是文件的一种,用于表示和管理系统中的文件,目录文件中包含一些文件名和子目录名。打开目录事实上就是打开目录文件。 +- **符号链接文件(l,symbolic link)**:保留了指向文件的地址而不是文件本身。 +- **字符设备(c,char)**:用来访问字符设备比如键盘。 +- **设备文件(b,block)**:用来访问块设备比如硬盘、软盘。 +- **管道文件(p,pipe)** : 一种特殊类型的文件,用于进程之间的通信。 +- **套接字文件(s,socket)**:用于进程间的网络通信,也可以用于本机之间的非网络通信。 + +每种文件类型都有不同的用途和属性,可以通过命令如`ls`、`file`等来查看文件的类型信息。 + +```bash +# 普通文件(-) +-rw-r--r-- 1 user group 1024 Apr 14 10:00 file.txt + +# 目录文件(d,directory file)* +drwxr-xr-x 2 user group 4096 Apr 14 10:00 directory/ + +# 套接字文件(s,socket) +srwxrwxrwx 1 user group 0 Apr 14 10:00 socket +``` + +### Linux 目录树 + +Linux 使用一种称为目录树的层次结构来组织文件和目录。目录树由根目录(/)作为起始点,向下延伸,形成一系列的目录和子目录。每个目录可以包含文件和其他子目录。结构层次鲜明,就像一棵倒立的树。 +![Linux的目录结构](./images/Linux目录树.png) + +**常见目录说明:** + +- **/bin:** 存放二进制可执行文件(ls、cat、mkdir 等),常用命令一般都在这里; +- **/etc:** 存放系统管理和配置文件; +- **/home:** 存放所有用户文件的根目录,是用户主目录的基点,比如用户 user 的主目录就是/home/user,可以用~user 表示; +- **/usr:** 用于存放系统应用程序; +- **/opt:** 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把 tomcat 等都安装到这里; +- **/proc:** 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; +- **/root:** 超级用户(系统管理员)的主目录(特权阶级^o^); +- **/sbin:** 存放二进制可执行文件,只有 root 才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如 ifconfig 等; +- **/dev:** 用于存放设备文件; +- **/mnt:** 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; +- **/boot:** 存放用于系统引导时使用的各种文件; +- **/lib 和/lib64:** 存放着和系统运行相关的库文件 ; +- **/tmp:** 用于存放各种临时文件,是公用的临时文件存储点; +- **/var:** 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; +- **/lost+found:** 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows 下叫什么.chk)就在这里。 + +## Linux 常用命令 + +下面只是给出了一些比较常用的命令。 + +推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。Linux 命令在线速查手册: 。 + +![ Linux 命令快查](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/linux-command-search.png) + +另外,[shell.how](https://www.shell.how/) 这个网站可以用来解释常见命令的意思,对你学习 Linux 基本命令以及其他常用命令(如 Git、NPM)。 + +![shell.how 使用示例](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/shell-now.png) + +### 目录切换 + +- `cd usr`:切换到该目录下 usr 目录 +- `cd ..(或cd../)`:切换到上一层目录 +- `cd /`:切换到系统根目录 +- `cd ~`:切换到用户主目录 +- **`cd -`:** 切换到上一个操作所在目录 + +### 目录操作 + +- `ls`:显示目录中的文件和子目录的列表。例如:`ls /home`,显示 `/home` 目录下的文件和子目录列表。 +- `ll`:`ll` 是 `ls -l` 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息。 +- `mkdir [选项] 目录名`:创建新目录(增)。例如:`mkdir -m 755 my_directory`,创建一个名为 `my_directory` 的新目录,并将其权限设置为 755,其中所有者拥有读、写、执行权限,所属组和其他用户只有读、执行权限,无法修改目录内容(如创建或删除文件)。如果希望所有用户(包括所属组和其他用户)对目录都拥有读、写、执行权限,则应设置权限为 `777`,即:`mkdir -m 777 my_directory`。 +- `find [路径] [表达式]`:在指定目录及其子目录中搜索文件或目录(查),非常强大灵活。例如:① 列出当前目录及子目录下所有文件和文件夹: `find .`;② 在`/home`目录下查找以 `.txt` 结尾的文件名:`find /home -name "*.txt"` ,忽略大小写: `find /home -i name "*.txt"` ;③ 当前目录及子目录下查找所有以 `.txt` 和 `.pdf` 结尾的文件:`find . \( -name "*.txt" -o -name "*.pdf" \)`或`find . -name "*.txt" -o -name "*.pdf"`。 +- `pwd`:显示当前工作目录的路径。 +- `rmdir [选项] 目录名`:删除空目录(删)。例如:`rmdir -p my_directory`,删除名为 `my_directory` 的空目录,并且会递归删除`my_directory`的空父目录,直到遇到非空目录或根目录。 +- `rm [选项] 文件或目录名`:删除文件/目录(删)。例如:`rm -r my_directory`,删除名为 `my_directory` 的目录,`-r`(recursive,递归) 表示会递归删除指定目录及其所有子目录和文件。 +- `cp [选项] 源文件/目录 目标文件/目录`:复制文件或目录(移)。例如:`cp file.txt /home/file.txt`,将 `file.txt` 文件复制到 `/home` 目录下,并重命名为 `file.txt`。`cp -r source destination`,将 `source` 目录及其下的所有子目录和文件复制到 `destination` 目录下,并保留源文件的属性和目录结构。 +- `mv [选项] 源文件/目录 目标文件/目录`:移动文件或目录(移),也可以用于重命名文件或目录。例如:`mv file.txt /home/file.txt`,将 `file.txt` 文件移动到 `/home` 目录下,并重命名为 `file.txt`。`mv` 与 `cp` 的结果不同,`mv` 好像文件“搬家”,文件个数并未增加。而 `cp` 对文件进行复制,文件个数增加了。 + +### 文件操作 + +像 `mv`、`cp`、`rm` 等文件和目录都适用的命令,这里就不重复列举了。 + +- `touch [选项] 文件名..`:创建新文件或更新已存在文件(增)。例如:`touch file1.txt file2.txt file3.txt` ,创建 3 个文件。 +- `ln [选项] <源文件> <硬链接/软链接文件>`:创建硬链接/软链接。例如:`ln -s file.txt file_link`,创建名为 `file_link` 的软链接,指向 `file.txt` 文件。`-s` 选项代表的就是创建软链接,s 即 symbolic(软链接又名符号链接) 。 +- `cat/more/less/tail 文件名`:文件的查看(查) 。命令 `tail -f 文件` 可以对某个文件进行动态监控,例如 Tomcat 的日志文件, 会随着程序的运行,日志会变化,可以使用 `tail -f catalina-2016-11-11.log` 监控 文 件的变化 。 +- `vim 文件名`:修改文件的内容(改)。vim 编辑器是 Linux 中的强大组件,是 vi 编辑器的加强版,vim 编辑器的命令和快捷方式有很多,但此处不一一阐述,大家也无需研究的很透彻,使用 vim 编辑修改文件的方式基本会使用就可以了。在实际开发中,使用 vim 编辑器主要作用就是修改配置文件,下面是一般步骤:`vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输入:wq/q!` (输入 wq 代表写入内容并退出,即保存;输入 q!代表强制退出不保存)。 + +### 文件压缩 + +**1)打包并压缩文件:** + +Linux 中的打包文件一般是以 `.tar` 结尾的,压缩的命令一般是以 `.gz` 结尾的。而一般情况下打包和压缩是一起进行的,打包并压缩后的文件的后缀名一般 `.tar.gz`。 + +命令:`tar -zcvf 打包压缩后的文件名 要打包压缩的文件` ,其中: + +- z:调用 gzip 压缩命令进行压缩 +- c:打包文件 +- v:显示运行过程 +- f:指定文件名 + +比如:假如 test 目录下有三个文件分别是:`aaa.txt`、 `bbb.txt`、`ccc.txt`,如果我们要打包 `test` 目录并指定压缩后的压缩包名称为 `test.tar.gz` 可以使用命令:`tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt` 或 `tar -zcvf test.tar.gz /test/` 。 + +**2)解压压缩包:** + +命令:`tar [-xvf] 压缩文件` + +其中 x 代表解压 + +示例: + +- 将 `/test` 下的 `test.tar.gz` 解压到当前目录下可以使用命令:`tar -xvf test.tar.gz` +- 将 /test 下的 test.tar.gz 解压到根目录/usr 下:`tar -xvf test.tar.gz -C /usr`(`-C` 代表指定解压的位置) + +### 文件传输 + +- `scp [选项] 源文件 远程文件` (scp 即 secure copy,安全复制):用于通过 SSH 协议进行安全的文件传输,可以实现从本地到远程主机的上传和从远程主机到本地的下载。例如:`scp -r my_directory user@remote:/home/user` ,将本地目录`my_directory`上传到远程服务器 `/home/user` 目录下。`scp -r user@remote:/home/user/my_directory` ,将远程服务器的 `/home/user` 目录下的`my_directory`目录下载到本地。需要注意的是,`scp` 命令需要在本地和远程系统之间建立 SSH 连接进行文件传输,因此需要确保远程服务器已经配置了 SSH 服务,并且具有正确的权限和认证方式。 +- `rsync [选项] 源文件 远程文件` : 可以在本地和远程系统之间高效地进行文件复制,并且能够智能地处理增量复制,节省带宽和时间。例如:`rsync -r my_directory user@remote:/home/user`,将本地目录`my_directory`上传到远程服务器 `/home/user` 目录下。 +- `ftp` (File Transfer Protocol):提供了一种简单的方式来连接到远程 FTP 服务器并进行文件上传、下载、删除等操作。使用之前需要先连接登录远程 FTP 服务器,进入 FTP 命令行界面后,可以使用 `put` 命令将本地文件上传到远程主机,可以使用`get`命令将远程主机的文件下载到本地,可以使用 `delete` 命令删除远程主机的文件。这里就不进行演示了。 + +### 文件权限 + +操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(executable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。 + +通过 **`ls -l`** 命令我们可以 查看某个目录下的文件或目录的权限 + +示例:在随意某个目录下`ls -l` + +![](./images/Linux权限命令.png) + +第一列的内容的信息解释如下: + +![](./images/Linux权限解读.png) + +> 下面将详细讲解文件的类型、Linux 中权限以及文件有所有者、所在组、其它组具体是什么? + +**文件的类型:** + +- d:代表目录 +- -:代表文件 +- l:代表软链接(可以认为是 window 中的快捷方式) + +**Linux 中权限分为以下几种:** + +- r:代表权限是可读,r 也可以用数字 4 表示 +- w:代表权限是可写,w 也可以用数字 2 表示 +- x:代表权限是可执行,x 也可以用数字 1 表示 + +**文件和目录权限的区别:** + +对文件和目录而言,读写执行表示不同的意义。 + +对于文件: + +| 权限名称 | 可执行操作 | +| :------- | --------------------------: | +| r | 可以使用 cat 查看文件的内容 | +| w | 可以修改文件的内容 | +| x | 可以将其运行为二进制文件 | + +对于目录: + +| 权限名称 | 可执行操作 | +| :------- | -----------------------: | +| r | 可以查看目录下列表 | +| w | 可以创建和删除目录下文件 | +| x | 可以使用 cd 进入目录 | + +需要注意的是:**超级用户可以无视普通用户的权限,即使文件目录权限是 000,依旧可以访问。** + +**在 Linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。** + +- **所有者(u)** :一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。 +- **文件所在组(g)** :当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。 +- **其它组(o)** :除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。 + +> 我们再来看看如何修改文件/目录的权限。 + +**修改文件/目录的权限的命令:`chmod`** + +示例:修改/test 下的 aaa.txt 的权限为文件所有者有全部权限,文件所有者所在的组有读写权限,其他用户只有读的权限。 + +**`chmod u=rwx,g=rw,o=r aaa.txt`** 或者 **`chmod 764 aaa.txt`** + +![](./images/修改文件权限.png) + +**补充一个比较常用的东西:** + +假如我们装了一个 zookeeper,我们每次开机到要求其自动启动该怎么办? + +1. 新建一个脚本 zookeeper +2. 为新建的脚本 zookeeper 添加可执行权限,命令是:`chmod +x zookeeper` +3. 把 zookeeper 这个脚本添加到开机启动项里面,命令是:`chkconfig --add zookeeper` +4. 如果想看看是否添加成功,命令是:`chkconfig --list` + +### 用户管理 + +Linux 系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。 + +用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪,并控制他们对系统资源的访问;另一方面也可以帮助用户组织文件,并为用户提供安全性保护。 + +**Linux 用户管理相关命令:** + +- `useradd [选项] 用户名`:创建用户账号。使用`useradd`指令所建立的帐号,实际上是保存在 `/etc/passwd`文本文件中。 +- `userdel [选项] 用户名`:删除用户帐号。 +- `usermod [选项] 用户名`:修改用户账号的属性和配置比如用户名、用户 ID、家目录。 +- `passwd [选项] 用户名`: 设置用户的认证信息,包括用户密码、密码过期时间等。。例如:`passwd -S 用户名` ,显示用户账号密码信息。`passwd -d 用户名`: 清除用户密码,会导致用户无法登录。`passwd 用户名`,修改用户密码,随后系统会提示输入新密码并确认密码。 +- `su [选项] 用户名`(su 即 Switch User,切换用户):在当前登录的用户和其他用户之间切换身份。 + +### 用户组管理 + +每个用户都有一个用户组,系统可以对一个用户组中的所有用户进行集中管理。不同 Linux 系统对用户组的规定有所不同,如 Linux 下的用户属于与它同名的用户组,这个用户组在创建用户时同时创建。 + +用户组的管理涉及用户组的添加、删除和修改。组的增加、删除和修改实际上就是对`/etc/group`文件的更新。 + +**Linux 系统用户组的管理相关命令:** + +- `groupadd [选项] 用户组` :增加一个新的用户组。 +- `groupdel 用户组`:要删除一个已有的用户组。 +- `groupmod [选项] 用户组` : 修改用户组的属性。 + +### 系统状态 + +- `top [选项]`:用于实时查看系统的 CPU 使用率、内存使用率、进程信息等。 +- `htop [选项]`:类似于 `top`,但提供了更加交互式和友好的界面,可让用户交互式操作,支持颜色主题,可横向或纵向滚动浏览进程列表,并支持鼠标操作。 +- `uptime [选项]`:用于查看系统总共运行了多长时间、系统的平均负载等信息。 +- `vmstat [间隔时间] [重复次数]`:vmstat (Virtual Memory Statistics) 的含义为显示虚拟内存状态,但是它可以报告关于进程、内存、I/O 等系统整体运行状态。 +- `free [选项]`:用于查看系统的内存使用情况,包括已用内存、可用内存、缓冲区和缓存等。 +- `df [选项] [文件系统]`:用于查看系统的磁盘空间使用情况,包括磁盘空间的总量、已使用量和可用量等,可以指定文件系统上。例如:`df -a`,查看全部文件系统。 +- `du [选项] [文件]`:用于查看指定目录或文件的磁盘空间使用情况,可以指定不同的选项来控制输出格式和单位。 +- `sar [选项] [时间间隔] [重复次数]`:用于收集、报告和分析系统的性能统计信息,包括系统的 CPU 使用、内存使用、磁盘 I/O、网络活动等详细信息。它的特点是可以连续对系统取样,获得大量的取样数据。取样数据和分析的结果都可以存入文件,使用它时消耗的系统资源很小。 +- `ps [选项]`:用于查看系统中的进程信息,包括进程的 ID、状态、资源使用情况等。`ps -ef`/`ps -aux`:这两个命令都是查看当前系统正在运行进程,两者的区别是展示格式不同。如果想要查看特定的进程可以使用这样的格式:`ps aux|grep redis` (查看包括 redis 字符串的进程),也可使用 `pgrep redis -a`。 +- `systemctl [命令] [服务名称]`:用于管理系统的服务和单元,可以查看系统服务的状态、启动、停止、重启等。 + +### 网络通信 + +- `ping [选项] 目标主机`:测试与目标主机的网络连接。 +- `ifconfig` 或 `ip`:用于查看系统的网络接口信息,包括网络接口的 IP 地址、MAC 地址、状态等。 +- `netstat [选项]`:用于查看系统的网络连接状态和网络统计信息,可以查看当前的网络连接情况、监听端口、网络协议等。 +- `ss [选项]`:比 `netstat` 更好用,提供了更快速、更详细的网络连接信息。 +- `nload`:`sar` 和 `nload` 都可以监控网络流量,但`sar` 的输出是文本形式的数据,不够直观。`nload` 则是一个专门用于实时监控网络流量的工具,提供图形化的终端界面,更加直观。不过,`nload` 不保存历史数据,所以它不适合用于长期趋势分析。并且,系统并没有默认安装它,需要手动安装。 +- `sudo hostnamectl set-hostname 新主机名`:更改主机名,并且重启后依然有效。`sudo hostname 新主机名`也可以更改主机名。不过需要注意的是,使用 `hostname` 命令直接更改主机名只是临时生效,系统重启后会恢复为原来的主机名。 + +### 其他 + +- `sudo + 其他命令`:以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。 +- `grep [选项] "搜索内容" 文件路径`:非常强大且常用的文本搜索命令,它可以根据指定的字符串或正则表达式,在文件或命令输出中进行匹配查找,适用于日志分析、文本过滤、快速定位等多种场景。示例:忽略大小写搜索 syslog 中所有包含 error 的行:`grep -i "error" /var/log/syslog`,查找所有与 java 相关的进程:`ps -ef | grep "java"`。 +- `kill -9 进程的pid`:杀死进程(-9 表示强制终止)先用 ps 查找进程,然后用 kill 杀掉。 +- `shutdown`:`shutdown -h now`:指定现在立即关机;`shutdown +5 "System will shutdown after 5 minutes"`:指定 5 分钟后关机,同时送出警告信息给登入用户。 +- `reboot`:`reboot`:重开机。`reboot -w`:做个重开机的模拟(只有纪录并不会真的重开机)。 + +## Linux 环境变量 + +在 Linux 系统中,环境变量是用来定义系统运行环境的一些参数,比如每个用户不同的主目录(HOME)。 + +### 环境变量分类 + +按照作用域来分,环境变量可以简单的分成: + +- 用户级别环境变量 : `~/.bashrc`、`~/.bash_profile`。 +- 系统级别环境变量 : `/etc/bashrc`、`/etc/environment`、`/etc/profile`、`/etc/profile.d`。 + +上述配置文件执行先后顺序为:`/etc/environment` –> `/etc/profile` –> `/etc/profile.d` –> `~/.bash_profile` –> `/etc/bashrc` –> `~/.bashrc` + +如果要修改系统级别环境变量文件,需要管理员具备对该文件的写入权限。 + +建议用户级别环境变量在 `~/.bash_profile`中配置,系统级别环境变量在 `/etc/profile.d` 中配置。 + +按照生命周期来分,环境变量可以简单的分成: + +- 永久的:需要用户修改相关的配置文件,变量永久生效。 +- 临时的:用户利用 `export` 命令,在当前终端下声明环境变量,关闭 shell 终端失效。 + +### 读取环境变量 + +通过 `export` 命令可以输出当前系统定义的所有环境变量。 + +```bash +# 列出当前的环境变量值 +export -p +``` + +除了 `export` 命令之外, `env` 命令也可以列出所有环境变量。 + +`echo` 命令可以输出指定环境变量的值。 + +```bash +# 输出当前的PATH环境变量的值 +echo $PATH +# 输出当前的HOME环境变量的值 +echo $HOME +``` + +### 环境变量修改 + +通过 `export`命令可以修改指定的环境变量。不过,这种方式修改环境变量仅仅对当前 shell 终端生效,关闭 shell 终端就会失效。修改完成之后,立即生效。 + +```bash +export CLASSPATH=./JAVA_HOME/lib;$JAVA_HOME/jre/lib +``` + +通过 `vim` 命令修改环境变量配置文件。这种方式修改环境变量永久有效。 + +```bash +vim ~/.bash_profile +``` + +如果修改的是系统级别环境变量则对所有用户生效,如果修改的是用户级别环境变量则仅对当前用户生效。 + +修改完成之后,需要 `source` 命令让其生效或者关闭 shell 终端重新登录。 + +```bash +source /etc/profile +``` + + + diff --git a/docs_en/cs-basics/operating-system/operating-system-basic-questions-01.en.md b/docs_en/cs-basics/operating-system/operating-system-basic-questions-01.en.md new file mode 100644 index 00000000000..4006ddfae2a --- /dev/null +++ b/docs_en/cs-basics/operating-system/operating-system-basic-questions-01.en.md @@ -0,0 +1,468 @@ +--- +title: 操作系统常见面试题总结(上) +category: 计算机基础 +tag: + - 操作系统 +head: + - - meta + - name: keywords + content: 操作系统面试题,用户态 vs 内核态,进程 vs 线程,死锁必要条件,系统调用过程,进程调度算法,PCB进程控制块,进程间通信IPC,死锁预防避免,操作系统基础高频题,虚拟内存管理 + - - meta + - name: description + content: 最新操作系统高频面试题总结(上):用户态/内核态切换、进程线程区别、死锁四条件、系统调用详解、调度算法对比,附图表+⭐️重点标注,一文掌握OS核心考点,快速通关后端技术面试! +--- + + + +很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如 **用户态和内核态、系统调用、进程和线程、死锁、内存管理、虚拟内存、文件系统**等等。 + +这篇文章只是对一些操作系统比较重要概念的一个概览,深入学习的话,建议大家还是老老实实地去看书。另外, 这篇文章的很多内容参考了《现代操作系统》第三版这本书,非常感谢。 + +开始本文的内容之前,我们先聊聊为什么要学习操作系统。 + +- **从对个人能力方面提升来说**:操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。比如说我们开发的系统使用的缓存(比如 Redis)和操作系统的高速缓存就很像。CPU 中的高速缓存有很多种,不过大部分都是为了解决 CPU 处理速度和内存处理速度不对等的问题。我们还可以把内存看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。同样地,我们使用的 Redis 缓存就是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。高速缓存一般会按照局部性原理(2-8 原则)根据相应的淘汰算法保证缓存中的数据是经常会被访问的。我们平常使用的 Redis 缓存很多时候也会按照 2-8 原则去做,很多淘汰算法都和操作系统中的类似。既说了 2-8 原则,那就不得不提命中率了,这是所有缓存概念都通用的。简单来说也就是你要访问的数据有多少能直接在缓存中直接找到。命中率高的话,一般表明你的缓存设计比较合理,系统处理速度也相对较快。 +- **从面试角度来说**:尤其是校招,对于操作系统方面知识的考察是非常非常多的。 + +**简单来说,学习操作系统能够提高自己思考的深度以及对技术的理解力,并且,操作系统方面的知识也是面试必备。** + +## 操作系统基础 + +![](https://oss.javaguide.cn/2020-8/image-20200807161118901.png) + +### 什么是操作系统? + +通过以下四点可以概括操作系统到底是什么: + +1. 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。 +2. 操作系统本质上是一个运行在计算机上的软件程序 ,主要用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。 +3. 操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使用的负责人,统筹着各种相关事项。 +4. 操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。 + +很多人容易把操作系统的内核(Kernel)和中央处理器(CPU,Central Processing Unit)弄混。你可以简单从下面两点来区别: + +1. 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。 +2. CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。 + +下图清晰说明了应用程序、内核、CPU 这三者的关系。 + +![Kernel_Layout](https://oss.javaguide.cn/2020-8/Kernel_Layout.png) + +### 操作系统主要有哪些功能? + +从资源管理的角度来看,操作系统有 6 大功能: + +1. **进程和线程的管理**:进程的创建、撤销、阻塞、唤醒,进程间的通信等。 +2. **存储管理**:内存的分配和管理、外存(磁盘等)的分配和管理等。 +3. **文件管理**:文件的读、写、创建及删除等。 +4. **设备管理**:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。 +5. **网络管理**:操作系统负责管理计算机网络的使用。网络是计算机系统中连接不同计算机的方式,操作系统需要管理计算机网络的配置、连接、通信和安全等,以提供高效可靠的网络服务。 +6. **安全管理**:用户的身份认证、访问控制、文件加密等,以防止非法用户对系统资源的访问和操作。 + +### 常见的操作系统有哪些? + +#### Windows + +目前最流行的个人桌面操作系统 ,不做多的介绍,大家都清楚。界面简单易操作,软件生态非常好。 + +_玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Windows 用于玩游戏,一台 Mac 用于平时日常开发和学习使用。_ + +![windows](./images/windows.png) + +#### Unix + +最早的多用户、多任务操作系统 。后面崛起的 Linux 在很多方面都参考了 Unix。 + +目前这款操作系统已经逐渐逐渐退出操作系统的舞台。 + +![unix](./images/unix.png) + +#### Linux + +**Linux 是一套免费使用、开源的类 Unix 操作系统。** Linux 存在着许多不同的发行版本,但它们都使用了 **Linux 内核** 。 + +> 严格来讲,Linux 这个词本身只表示 Linux 内核,在 GNU/Linux 系统中,Linux 实际就是 Linux 内核,而该系统的其余部分主要是由 GNU 工程编写和提供的程序组成。单独的 Linux 内核并不能成为一个可以正常工作的操作系统。 +> +> **很多人更倾向使用 “GNU/Linux” 一词来表达人们通常所说的 “Linux”。** + +![Linux 操作系统](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/linux.png) + +#### Mac OS + +苹果自家的操作系统,编程体验和 Linux 相当,但是界面、软件生态以及用户体验各方面都要比 Linux 操作系统更好。 + +![macos](./images/macos.png) + +### 用户态和内核态 + +#### 什么是用户态和内核态? + +根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别: + +![用户态和内核态](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/usermode-and-kernelmode.png) + +- **用户态(User Mode)** : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。 +- **内核态(Kernel Mode)** :内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。 + +内核态相比用户态拥有更高的特权级别,因此能够执行更底层、更敏感的操作。不过,由于进入内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查),应该尽量减少进入内核态的次数,以提高系统的性能和稳定性。 + +#### 为什么要有用户态和内核态?只有一个内核态不行么? + +这样设计主要是为了**安全**和**稳定**。 + +- 在 CPU 的所有指令中,有一些指令是比较危险的比如内存分配、设置时钟、IO 处理等,如果所有的程序都能使用这些指令的话,会对系统的正常运行造成灾难性地影响。因此,我们需要限制这些危险指令只能内核态运行。这些只能由操作系统内核态执行的指令也被叫做 **特权指令** 。 +- 如果计算机系统中只有一个内核态,那么所有程序或进程都必须共享系统资源,例如内存、CPU、硬盘等,这将导致系统资源的竞争和冲突,从而影响系统性能和效率。并且,这样也会让系统的安全性降低,毕竟所有程序或进程都具有相同的特权级别和访问权限。 + +因此,同时具有用户态和内核态主要是为了保证计算机系统的安全性、稳定性和性能。 + +#### 用户态和内核态是如何切换的? + +![用户态切换到内核态的 3 种方式](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/the-way-switch-between-user-mode-and-kernel-mode.drawio.png) + +用户态切换到内核态的 3 种方式: + +1. **系统调用(Trap)**:这是最主要的方式,是应用程序**主动**发起的。比如,当我们的程序需要读取一个文件或者发送网络数据时,它无法直接操作磁盘或网卡,就必须调用操作系统提供的接口(如 `read()`,`send()`), 这会触发一次从用户态到内核态的切换。 +2. **中断(Interrupt)**:这是**被动**的,由外部硬件设备触发。比如,当硬盘完成了数据读取,会向 CPU 发送一个中断信号,CPU 会暂停当前用户态的程序,切换到内核态去处理这个中断。 +3. **异常(Exception)**:这也是**被动**的,由程序自身错误引起。比如,我们的代码执行了一个除以零的操作,或者访问了一个非法的内存地址(缺页异常),CPU 会捕获这个异常,并切换到内核态去处理它。 + +在系统的处理上,中断和异常类似,都是通过中断向量表来找到相应的处理程序进行处理。区别在于,中断来自处理器外部,不是由任何一条专门的指令造成,而异常是执行当前指令的结果。 + +最后,需要强调的是,这种**状态切换是有性能开销的**。因为它涉及到保存用户态的上下文(寄存器等)、切换到内核态执行、再恢复用户态的上下文。因此,在高性能编程中,我们常常需要考虑如何减少这种切换次数,比如通过缓冲 I/O 来批量读写文件,就是一个典型的例子。 + +### 系统调用 + +#### 什么是系统调用? + +我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的内核态级别的子功能咋办呢?那就需要系统调用了! + +也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。 + +![系统调用](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/system-call.png) + +这些系统调用按功能大致可分为如下几类: + +- 设备管理:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。 +- 文件管理:完成文件的读、写、创建及删除等功能。 +- 进程管理:进程的创建、撤销、阻塞、唤醒,进程间的通信等功能。 +- 内存管理:完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。 + +系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核态,而普通的库函数调用由函数库或用户自己提供,运行于用户态。 + +总结:系统调用是应用程序与操作系统之间进行交互的一种方式,通过系统调用,应用程序可以访问操作系统底层资源例如文件、设备、网络等。 + +#### 系统调用的过程了解吗? + +系统调用的过程可以简单分为以下几个步骤: + +1. 用户态的程序发起系统调用,因为系统调用中涉及一些特权指令(只能由操作系统内核态执行的指令),用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。 +2. 发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调用。 +3. 当系统调用处理完成后,操作系统使用特权指令(如 `iret`、`sysret` 或 `eret`)切换回用户态,恢复用户态的上下文,继续执行用户程序。 + +![系统调用的过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/system-call-procedure.png) + +## 进程和线程 + +### 进程和线程的区别是什么? + +进程和线程是操作系统中并发执行的两个核心概念,它们的关系可以理解为 **工厂和工人** 的关系。 + +**进程(Process)就像一个工厂**。操作系统在分配资源时,是以进程为基本单位的。比如,当我启动一个微信,操作系统就为它建立了一个独立的工厂,分配给它专属的内存空间、文件句柄等资源。这个工厂与其他工厂(比如我打开的浏览器进程)是严格隔离的。 + +**线程(Thread)则像是工厂里的工人**。一个工厂里可以有很多工人,他们共享这个工厂的资源,但每个工人有自己的工具箱和任务清单,让他们可以独立地执行不同的任务。比如微信这个工厂里,可以有一个工人(线程)负责接收消息,一个工人负责渲染界面。 + +这是我用 AI 绘制的一张图片,可以说是非常形象了: + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/process-and-thread-difference-wechat-factory-as-an-example.png) + +下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧! + +![Java 运行时数据区域(JDK1.8 之后)](https://oss.javaguide.cn/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png) + +从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 + +这里从 3 个角度总结下线程和进程的核心区别: + +1. **资源所有权:** 进程是资源分配的基本单位,拥有独立的地址空间;而线程是 CPU 调度的基本单位,几乎不拥有系统资源,只保留少量私有数据(PC、栈、寄存器),主要共享其所属进程的资源。 +2. **开销:** 创建或销毁一个工厂(进程)的开销很大,需要分配独立的资源。而雇佣或解雇一个工人(线程)的开销就小得多。同理,进程间的上下文切换开销远大于线程间的切换。 +3. **健壮性:** 工厂之间是隔离的,一个工厂倒闭(进程崩溃)不会影响其他工厂。但一个工厂内的工人之间是共享资源的,一个工人操作失误(比如一个线程访问了非法内存)可能会导致整个工厂停工(整个进程崩溃)。 + +### 有了进程为什么还需要线程? + +核心原因就是**为了在单个应用内实现低开销、高效率的并发**。如果我想让微信同时接收消息和发送文件,如果用两个进程来实现,不仅资源开销巨大,它们之间通信还非常麻烦(需要 IPC)。而使用两个线程,它们不仅切换成本低,还能直接通过共享内存高效通信,从而能更好地利用多核 CPU,提升应用的响应速度和吞吐量。 + +再那我们上面举的工厂和工人为例:线程=同一屋檐下的轻量级工人,切换成本低、共享内存零拷贝;若换成两个独立进程,就得各建一座工厂(独立地址空间),既费砖又费电(资源与 IPC 开销)。 + +### 为什么要使用多线程? + +先从总体上来说: + +- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + +再深入到计算机底层来探讨: + +- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 +- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 + +### 线程间的同步的方式有哪些? + +线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。 + +下面是几种常见的线程同步的方式: + +1. **互斥锁(Mutex)** :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。 +2. **读写锁(Read-Write Lock)** :允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。 +3. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 +4. **屏障(Barrier)** :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。 +5. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。 + +### PCB 是什么?包含哪些信息? + +**PCB(Process Control Block)** 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。 + +当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。 + +PCB 主要包含下面几部分的内容: + +- 进程的描述信息,包括进程的名称、标识符等等; +- 进程的调度信息,包括进程阻塞原因、进程状态(就绪、运行、阻塞等)、进程优先级(标识进程的重要程度)等等; +- 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。 +- 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。 +- 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。 +- …… + +### 进程有哪几种状态? + +我们一般把进程大致分为 5 种状态,这一点和线程很像! + +- **创建状态(new)**:进程正在被创建,尚未到就绪状态。 +- **就绪状态(ready)**:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。 +- **运行状态(running)**:进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。 +- **阻塞状态(waiting)**:又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。 +- **结束状态(terminated)**:进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。 + +![进程状态图转换图](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/state-transition-of-process.png) + +### 进程间的通信方式有哪些? + +> 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74) 这篇文章,推荐阅读,总结的非常不错。 + +1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 +2. **有名管道(Named Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 **先进先出(First In First Out)** 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 +3. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; +4. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。 +5. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。 +6. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。 +7. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。 + +### 进程的调度算法有哪些? + +![常见进程调度算法](https://oss.javaguide.cn/github/javaguide/cs-basics/network/scheduling-algorithms-of-process.png) + +进程调度算法的核心目标是决定就绪队列中的哪个进程应该获得 CPU 资源,其设计目标通常是在**吞吐量、周转时间、响应时间**和**公平性**之间做权衡。 + +我习惯将这些算法分为两大类:**非抢占式**和**抢占式**。 + +**第一类:非抢占式调度 (Non-Preemptive)** + +这种方式下,一旦 CPU 分配给一个进程,它就会一直运行下去,直到任务完成或主动放弃(比如等待 I/O)。 + +1. **先到先服务调度算法(FCFS,First Come, First Served)** : 这是最简单的,就像排队,谁先来谁先用。优点是公平、实现简单。但缺点很明显,如果一个很长的任务先到了,后面无数个短任务都得等着,这会导致平均等待时间很长,我们称之为“护航效应”。 +2. **短作业优先的调度算法(SJF,Shortest Job First)** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源。理论上,它的平均等待时间是最短的,吞吐量很高。但缺点是,它需要预测运行时间,这很难做到,而且可能会导致长作业“饿死”,永远得不到执行。 + +**第二类:抢占式调度 (Preemptive)** + +操作系统可以强制剥夺当前进程的 CPU 使用权,分配给其他更重要的进程。现代操作系统基本都采用这种方式。 + +- **时间片轮转调度算法(RR,Round-Robin)** : 这是最经典、最公平的抢占式算法。它给每个进程分配一个固定的时间片,用完了就把它放到队尾,切换到下一个进程。它非常适合分时系统,保证了每个进程都能得到响应,但时间片的设置很关键:太长了退化成 FCFS,太短了则会导致过于频繁的上下文切换,增加系统开销。 +- **优先级调度算法(Priority)**:每个进程都有一个优先级,进程调度器总是选择优先级最高的进程,具有相同优先级的进程以 FCFS 方式执行。这很灵活,可以根据内存要求,时间要求或任何其他资源要求来确定优先级,但同样可能导致低优先级进程“饿死”。 + +前面介绍的几种进程调度的算法都有一定的局限性,如:**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。那有没有一种结合了上面这些进程调度算法优点的呢? + +**多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)** 是现实世界中最常用的一种算法,比如早期的 UNIX。它非常聪明,结合了 RR 和优先级调度。它设置了多个不同优先级的队列,每个队列使用 RR 调度,时间片大小也不同。新进程先进入最高优先级队列;如果在一个时间片内没执行完,就会被降级到下一个队列。这样既照顾了短作业(在高优先级队列中快速完成),也保证了长作业不会饿死(最终会在低优先级队列中得到执行),是一种非常均衡的方案。 + +### 那究竟是谁来调度这个进程呢? + +负责进程调度的核心是操作系统内核中的两个紧密协作的组件:**调度程序(Scheduler)** 和 **分派程序(Dispatcher)**。我们可以把它们理解成一个团队: + +- **调度程序 (Scheduler):** 可以看作是决策者。当需要进行调度时,调度程序会被激活,它会根据预设的调度算法(比如我们前面聊到的多级反馈队列),从就绪队列中挑选出下一个应该占用 CPU 的进程。 +- **分派程序 (Dispatcher):** 可以看作是执行者。它负责完成具体的“交接”工作,也就是**上下文切换**。这个过程非常底层,主要包括: + - 保存当前进程的上下文(CPU 寄存器状态、程序计数器等)到其进程控制块(PCB)中。 + - 加载下一个被选中进程的上下文,从其 PCB 中读取状态,恢复到 CPU 寄存器。 + - 将 CPU 的控制权正式移交给新进程,让它开始运行。 + +## 死锁 + +### 什么是死锁? + +死锁(Deadlock)描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。 + +一个最经典的例子就是**“交叉持锁”**。想象有两个线程和两个锁: + +- 线程 1 先拿到了锁 A,然后尝试去获取锁 B。 +- 几乎同时,线程 2 拿到了锁 B,然后尝试去获取锁 A。 + +这时,线程 1 等着线程 2 释放锁 B,而线程 2 等着线程 1 释放锁 A,双方都持有对方需要的资源,并等待对方释放,就形成了一个“死结”。 + + + +### 产生死锁的四个必要条件是什么? + +死锁的发生并不是偶然的,它需要同时满足**四个必要条件**: + +1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。 +2. **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。 +3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。 +4. **循环等待**:有一组等待进程 {P0, P1,..., Pn}, P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,……,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。 + +**注意 ⚠️**:这四个条件是产生死锁的 **必要条件** ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。 + +下面是百度百科对必要条件的解释: + +> 如果没有事物情况 A,则必然没有事物情况 B,也就是说如果有事物情况 B 则一定有事物情况 A,那么 A 就是 B 的必要条件。从逻辑学上看,B 能推导出 A,A 就是 B 的必要条件,等价于 B 是 A 的充分条件。 + +### 能写一个模拟产生死锁的代码吗? + +下面通过一个实际的例子来模拟下图展示的线程死锁: + +![线程死锁示意图 ](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811-20230814005444749.png) + +```java +public class DeadLockDemo { + private static Object resource1 = new Object();//资源 1 + private static Object resource2 = new Object();//资源 2 + + public static void main(String[] args) { + new Thread(() -> { + synchronized (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"); + synchronized (resource2) { + System.out.println(Thread.currentThread() + "get resource2"); + } + } + }, "线程 1").start(); + + new Thread(() -> { + synchronized (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"); + synchronized (resource1) { + System.out.println(Thread.currentThread() + "get resource1"); + } + } + }, "线程 2").start(); + } +} +``` + +Output + +```text +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 +``` + +线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 `resource2` 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 + +### 解决死锁的方法 + +解决死锁的方法可以从多个角度去分析,一般的情况下,有**预防,避免,检测和解除四种**。 + +- **死锁预防:** 这是我们程序员最常用的方法。通过编码规范来破坏条件。最经典的就是**破坏循环等待**,比如规定所有线程都必须**按相同的顺序**来获取锁(比如先 A 后 B),这样就不会形成环路。 +- **死锁避免:** 这是一种更动态的方法,比如操作系统的**银行家算法**。它会在分配资源前进行预测,如果这次分配可能导致未来发生死锁,就拒绝分配。但这种方法开销很大,在通用系统中用得比较少。 +- **死锁检测与解除:** 这是一种“事后补救”的策略,就像乐观锁。系统允许死锁发生,但会有一个后台线程(或机制)定期检测是否存在死锁环路(比如通过分析线程等待图)。一旦发现,就会采取措施解除,比如**强制剥夺某个线程的资源或直接终止它**。数据库系统中的死锁处理就常常采用这种方式。 + +#### 死锁的预防 + +死锁四大必要条件上面都已经列出来了,很显然,只要破坏四个必要条件中的任何一个就能够预防死锁的发生。 + +破坏第一个条件 **互斥条件**:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源 **往往是不能同时访问的** ,所以这种做法在大多数的场合是行不通的。 + +破坏第三个条件 **非抢占**:也就是说可以采用 **剥夺式调度算法**,但剥夺式调度方法目前一般仅适用于 **主存资源** 和 **处理器资源** 的分配,并不适用于所有的资源,会导致 **资源利用率下降**。 + +所以一般比较实用的 **预防死锁的方法**,是通过考虑破坏第二个条件和第四个条件。 + +**1、静态分配策略** + +静态分配策略可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源,不会出现占有一些资源等待一些资源的情况。 + +静态分配策略逻辑简单,实现也很容易,但这种策略 **严重地降低了资源利用率**,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才使用的,这样就可能造成一个进程占有了一些 **几乎不用的资源而使其他需要该资源的进程产生等待** 的情况。 + +**2、层次分配策略** + +层次分配策略破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略,证明略。 + +#### 死锁的避免 + +上面提到的 **破坏** 死锁产生的四个必要条件之一就可以成功 **预防系统发生死锁** ,但是会导致 **低效的进程运行** 和 **资源使用率** 。而死锁的避免相反,它的角度是允许系统中**同时存在四个必要条件** ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 **明智和合理的选择** ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。 + +我们将系统的状态分为 **安全状态** 和 **不安全状态** ,每当在为申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。 + +> 如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。 + +那么如何保证系统保持在安全状态呢?通过算法,其中最具有代表性的 **避免死锁算法** 就是 Dijkstra 的银行家算法,银行家算法用一句话表达就是:当一个进程申请使用资源的时候,**银行家算法** 通过先 **试探** 分配给该进程资源,然后通过 **安全性算法** 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 **真的分配资源给该进程**。 + +银行家算法详情可见:[《一句话+一张图说清楚——银行家算法》](https://blog.csdn.net/qq_33414271/article/details/80245715) 。 + +操作系统教程书中讲述的银行家算法也比较清晰,可以一看. + +死锁的避免(银行家算法)改善了 **资源使用率低的问题** ,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 **安全性检查** ,需要花费较多的时间。 + +#### 死锁的检测 + +对资源的分配加以限制可以 **预防和避免** 死锁的发生,但是都不利于各进程对系统资源的**充分共享**。解决死锁问题的另一条途径是 **死锁检测和解除** (这里突然联想到了乐观锁和悲观锁,感觉死锁的检测和解除就像是 **乐观锁** ,分配资源时不去提前管会不会发生死锁了,等到真的死锁出现了再来解决嘛,而 **死锁的预防和避免** 更像是悲观锁,总是觉得死锁会出现,所以在分配资源的时候就很谨慎)。 + +这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统 **定时地运行一个 “死锁检测”** 的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它。 + +##### 进程-资源分配图 + +操作系统中的每一刻时刻的**系统状态**都可以用**进程-资源分配图**来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于**检测系统是否处于死锁状态**。 + +用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,用一个圆圈表示每一个进程,用 **有向边** 来表示**进程申请资源和资源被分配的情况**。 + +图中 2-21 是**进程-资源分配图**的一个例子,其中共有三个资源类,每个进程的资源占有和申请情况已清楚地表示在图中。在这个例子中,由于存在 **占有和等待资源的环路** ,导致一组进程永远处于等待资源的状态,发生了 **死锁**。 + +![进程-资源分配图](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/process-resource-allocation-diagram.jpg) + +进程-资源分配图中存在环路并不一定是发生了死锁。因为循环等待资源仅仅是死锁发生的必要条件,而不是充分条件。图 2-22 便是一个有环路而无死锁的例子。虽然进程 P1 和进程 P3 分别占用了一个资源 R1 和一个资源 R2,并且因为等待另一个资源 R2 和另一个资源 R1 形成了环路,但进程 P2 和进程 P4 分别占有了一个资源 R1 和一个资源 R2,它们申请的资源得到了满足,在有限的时间里会归还资源,于是进程 P1 或 P3 都能获得另一个所需的资源,环路自动解除,系统也就不存在死锁状态了。 + +##### 死锁检测步骤 + +知道了死锁检测的原理,我们可以利用下列步骤编写一个 **死锁检测** 程序,检测系统是否产生了死锁。 + +1. 如果进程-资源分配图中无环路,则此时系统没有发生死锁 +2. 如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁。 +3. 如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 **既不阻塞又非独立的进程** ,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内 **消除所有的边** ,则不会发生死锁,否则会发生死锁。(消除边的过程类似于 **拓扑排序**) + +#### 死锁的解除 + +当死锁检测程序检测到存在死锁发生时,应设法让其解除,让系统从死锁状态中恢复过来,常用的解除死锁的方法有以下四种: + +1. **立即结束所有进程的执行,重新启动操作系统**:这种方法简单,但以前所在的工作全部作废,损失很大。 +2. **撤销涉及死锁的所有进程,解除死锁后继续运行**:这种方法能彻底打破**死锁的循环等待**条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算。 +3. **逐个撤销涉及死锁的进程,回收其资源直至死锁解除。** +4. **抢占资源**:从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。 + +## 参考 + +- 《计算机操作系统—汤小丹》第四版 +- 《深入理解计算机系统》 +- 《重学操作系统》 +- 操作系统为什么要分用户态和内核态: +- 从根上理解用户态与内核态: +- 什么是僵尸进程与孤儿进程: + + + diff --git a/docs_en/cs-basics/operating-system/operating-system-basic-questions-02.en.md b/docs_en/cs-basics/operating-system/operating-system-basic-questions-02.en.md new file mode 100644 index 00000000000..bc26a6b4731 --- /dev/null +++ b/docs_en/cs-basics/operating-system/operating-system-basic-questions-02.en.md @@ -0,0 +1,418 @@ +--- +title: 操作系统常见面试题总结(下) +category: 计算机基础 +tag: + - 操作系统 +head: + - - meta + - name: keywords + content: 操作系统面试题,虚拟内存详解,分页 vs 分段,页面置换算法,内存碎片,伙伴系统,TLB快表,页缺失,文件系统基础,磁盘调度算法,硬链接 vs 软链接 + - - meta + - name: description + content: 最新操作系统高频面试题总结(下):虚拟内存映射、内存碎片/伙伴系统、TLB+页缺失处理、分页分段对比、页面置换算法详解、文件系统&磁盘调度,附图表+⭐️重点标注,一文掌握OS内存/文件考点,快速通关后端面试! +--- + +## 内存管理 + +### 内存管理主要做了什么? + +![内存管理主要做的事情](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/memory-management-roles.png) + +操作系统的内存管理非常重要,主要负责下面这些事情: + +- **内存的分配与回收**:对进程所需的内存进行分配和释放,malloc 函数:申请内存,free 函数:释放内存。 +- **地址转换**:将程序中的虚拟地址转换成内存中的物理地址。 +- **内存扩充**:当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。 +- **内存映射**:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。 +- **内存优化**:通过调整内存分配策略和回收算法来优化内存使用效率。 +- **内存安全**:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。 +- …… + +### 什么是内存碎片? + +内存碎片是由内存的申请和释放产生的,通常分为下面两种: + +- **内部内存碎片(Internal Memory Fragmentation,简称为内存碎片)**:已经分配给进程使用但未被使用的内存。导致内部内存碎片的主要原因是,当采用固定比例比如 2 的幂次方进行内存分配时,进程所分配的内存可能会比其实际所需要的大。举个例子,一个进程只需要 65 字节的内存,但为其分配了 128(2^7) 大小的内存,那 63 字节的内存就成为了内部内存碎片。 +- **外部内存碎片(External Memory Fragmentation,简称为外部碎片)**:由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并未分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。 + +![内存碎片](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/internal-and-external-fragmentation.png) + +内存碎片会导致内存利用率下降,如何减少内存碎片是内存管理要非常重视的一件事情。 + +### 常见的内存管理方式有哪些? + +内存管理方式可以简单分为下面两种: + +- **连续内存管理**:为一个用户程序分配一个连续的内存空间,内存利用率一般不高。 +- **非连续内存管理**:允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。 + +#### 连续内存管理 + +**块式管理** 是早期计算机操作系统的一种连续内存管理方式,存在严重的内存碎片问题。块式管理会将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为内部内存碎片。除了内部内存碎片之外,由于两个内存块之间可能还会有外部内存碎片,这些不连续的外部内存碎片由于太小了无法再进行分配。 + +在 Linux 系统中,连续内存管理采用了 **伙伴系统(Buddy System)算法** 来实现,这是一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。伙伴系统的主要思想是将内存按 2 的幂次划分(每一块内存大小都是 2 的幂次比如 2^6=64 KB),并将相邻的内存块组合成一对伙伴(注意:**必须是相邻的才是伙伴**)。 + +当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就将其一分为二,分成两个大小相等的伙伴块。如果还是大的话,就继续切分,直到到达合适的大小为止。 + +假设两块相邻的内存块都被释放,系统会将这两个内存块合并,进而形成一个更大的内存块,以便后续的内存分配。这样就可以减少内存碎片的问题,提高内存利用率。 + +![伙伴系统(Buddy System)内存管理](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux-buddy-system.png) + +虽然解决了外部内存碎片的问题,但伙伴系统仍然存在内存利用率不高的问题(内部内存碎片)。这主要是因为伙伴系统只能分配大小为 2^n 的内存块,因此当需要分配的内存大小不是 2^n 的整数倍时,会浪费一定的内存空间。举个例子:如果要分配 65 大小的内存快,依然需要分配 2^7=128 大小的内存块。 + +![伙伴系统内存浪费问题](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/buddy-system-memory-waste.png) + +对于内部内存碎片的问题,Linux 采用 **SLAB** 进行解决。由于这部分内容不是本篇文章的重点,这里就不详细介绍了。 + +#### 非连续内存管理 + +非连续内存管理存在下面 3 种方式: + +- **段式管理**:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 +- **页式管理**:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。 +- **段页式管理机制**:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。 + +### 虚拟内存 + +#### 什么是虚拟内存?有什么用? + +**虚拟内存(Virtual Memory)** 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。 + +![虚拟内存作为进程访问主存的桥梁](https://oss.javaguide.cn/xingqiu/virtual-memory.png) + +总结来说,虚拟内存主要提供了下面这些能力: + +- **隔离进程**:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。 +- **提升物理内存利用率**:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。 +- **简化内存管理**:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。 +- **多个进程共享物理内存**:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。 +- **提高内存使用安全性**:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。 +- **提供更大的可使用内存空间**:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。 + +#### 没有虚拟内存有什么问题? + +如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。 + +**具体有什么问题呢?** 这里举几个例子说明(参考虚拟内存提供的能力回答这个问题): + +1. 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。 +2. 同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。 +3. 程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。 +4. …… + +#### 什么是虚拟地址和物理地址? + +**物理地址(Physical Address)** 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是 **虚拟地址(Virtual Address)** 。 + +也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。 + +操作系统一般通过 CPU 芯片中的一个重要组件 **MMU(Memory Management Unit,内存管理单元)** 将虚拟地址转换为物理地址,这个过程被称为 **地址翻译/地址转换(Address Translation)** 。 + +![地址翻译过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/physical-virtual-address-translation.png) + +通过 MMU 将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。 + +MMU 将虚拟地址翻译为物理地址的主要机制有两种: **分段机制** 和 **分页机制** 。 + +#### 什么是虚拟地址空间和物理地址空间? + +- 虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。 +- 物理地址空间是物理地址的集合,是物理内存的范围。 + +#### 虚拟地址与物理内存地址是如何映射的? + +MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: + +1. 分段机制 +2. 分页机制 +3. 段页机制 + +其中,现代操作系统广泛采用分页机制,需要重点关注! + +### 分段机制 + +**分段机制(Segmentation)** 以段(一段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 + +#### 段表有什么用?地址翻译过程是怎样的? + +分段管理通过 **段表(Segment Table)** 映射虚拟地址和物理地址。 + +分段机制下的虚拟地址由两部分组成: + +- **段号**:标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。 +- **段内偏移量**:相对于该段起始地址的偏移量。 + +具体的地址翻译过程如下: + +1. MMU 首先解析得到虚拟地址中的段号; +2. 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项); +3. 从段信息中取出该段的起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址。 + +![分段机制下的地址翻译过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/segment-virtual-address-composition.png) + +段表中还存有诸如段长(可用于检查虚拟地址是否超出合法范围)、段类型(该段的类型,例如代码段、数据段等)等信息。 + +**通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?** + +不一定。段表项可能并不存在: + +- **段表项被删除**:软件错误、软件恶意行为等情况可能会导致段表项被删除。 +- **段表项还未创建**:如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。 + +#### 分段机制为什么会导致内存外部碎片? + +分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。从而造成物理内存资源利用率的降低。 + +举个例子:假设可用物理内存为 5G 的系统使用分段机制分配内存。现在有 4 个进程,每个进程的内存占用情况如下: + +- 进程 1:0~1G(第 1 段) +- 进程 2:1~3G(第 2 段) +- 进程 3:3~4.5G(第 3 段) +- 进程 4:4.5~5G(第 4 段) + +此时,我们关闭了进程 1 和进程 4,则第 1 段和第 4 段的内存会被释放,空闲物理内存还有 1.5G。由于这 1.5G 物理内存并不是连续的,导致没办法将空闲的物理内存分配给一个需要 1.5G 物理内存的进程。 + +![分段机制导致外部内存碎片](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/segment-external-memory-fragmentation.png) + +### 分页机制 + +**分页机制(Paging)** 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。 + +**注意:这里的页是连续等长的,不同于分段机制下不同长度的段。** + +在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。 + +#### 页表有什么用?地址翻译过程是怎样的? + +分页管理通过 **页表(Page Table)** 映射虚拟地址和物理地址。我这里画了一张基于单级页表进行地址翻译的示意图。 + +![单级页表](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/page-table.png) + +在分页机制下,每个进程都会有一个对应的页表。 + +分页机制下的虚拟地址由两部分组成: + +- **页号**:通过虚拟页号可以从页表中取出对应的物理页号; +- **页内偏移量**:物理页起始地址+页内偏移量=物理内存地址。 + +具体的地址翻译过程如下: + +1. MMU 首先解析得到虚拟地址中的虚拟页号; +2. 通过虚拟页号去该应用程序的页表中取出对应的物理页号(找到对应的页表项); +3. 用该物理页号对应的物理页起始地址(物理地址)加上虚拟地址中的页内偏移量得到最终的物理地址。 + +![分页机制下的地址翻译过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/paging-virtual-address-composition.png) + +页表中还存有诸如访问标志(标识该页面有没有被访问过)、脏数据标识位等信息。 + +**通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?** + +不一定!可能会存在 **页缺失** 。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。关于页缺失的内容,后面会详细介绍到。 + +#### 单级页表有什么问题?为什么需要多级页表? + +以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,`2^20 * 2^2 / 1024 * 1024= 4MB`。也就是说一个程序啥都不干,页表大小就得占用 4M。 + +系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。 + +为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。 + +这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。 + +假设只需要 2 个二级页表,那两级页表的内存占用情况为: 4KB(一级页表占用) + 4KB \* 2(二级页表占用) = 12 KB。 + +![多级页表](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/multilevel-page-table.png) + +多级页表属于时间换空间的典型场景,利用增加页表查询的次数减少页表占用的空间。 + +#### TLB 有什么用?使用 TLB 之后的地址翻译流程是怎样的? + +为了提高虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表)** 。 + +![加入 TLB 之后的地址翻译](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/physical-virtual-address-translation-mmu.png) + +在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 (Memory Management Unit,内存管理单元) 内部的单元,本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页号)对的哈希表。 + +使用 TLB 之后的地址翻译流程是这样的: + +1. 用虚拟地址中的虚拟页号作为 key 去 TLB 中查询; +2. 如果能查到对应的物理页的话,就不用再查询页表了,这种情况称为 TLB 命中(TLB hit)。 +3. 如果不能查到对应的物理页的话,还是需要去查询主存中的页表,同时将页表中的该映射表项添加到 TLB 中,这种情况称为 TLB 未命中(TLB miss)。 +4. 当 TLB 填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。 + +![使用 TLB 之后的地址翻译流程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/page-table-tlb.png) + +由于页表也在主存中,因此在没有 TLB 之前,每次读写内存数据时 CPU 要访问两次主存。有了 TLB 之后,对于存在于 TLB 中的页表数据只需要访问一次主存即可。 + +TLB 的设计思想非常简单,但命中率往往非常高,效果很好。这就是因为被频繁访问的页就是其中的很小一部分。 + +看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。 + +#### 换页机制有什么用? + +换页机制的思想是当物理内存不够用的时候,操作系统选择将一些物理页的内容放到磁盘上去,等要用到的时候再将它们读取到物理内存中。也就是说,换页机制利用磁盘这种较低廉的存储设备扩展的物理内存。 + +这也就解释了一个日常使用电脑常见的问题:为什么操作系统中所有进程运行所需的物理内存即使比真实的物理内存要大一些,这些进程也是可以正常运行的,只是运行速度会变慢。 + +这同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的物理内存空间来支持程序的运行。 + +#### 什么是页缺失? + +根据维基百科: + +> 页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。 + +常见的页缺失有下面这两种: + +- **硬性页缺失(Hard Page Fault)**:物理内存中没有对应的物理页。于是,Page Fault Handler 会指示 CPU 从已经打开的磁盘文件中读取相应的内容到物理内存,而后交由 MMU 建立相应的虚拟页和物理页的映射关系。 +- **软性页缺失(Soft Page Fault)**:物理内存中有对应的物理页,但虚拟页还未和物理页建立映射。于是,Page Fault Handler 会指示 MMU 建立相应的虚拟页和物理页的映射关系。 + +发生上面这两种缺页错误的时候,应用程序访问的是有效的物理内存,只是出现了物理页缺失或者虚拟页和物理页的映射关系未建立的问题。如果应用程序访问的是无效的物理内存的话,还会出现 **无效缺页错误(Invalid Page Fault)** 。 + +#### 常见的页面置换算法有哪些? + +当发生硬性页缺失时,如果物理内存中没有空闲的物理页面可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面了。 + +用来选择淘汰哪一个物理页的规则叫做 **页面置换算法** ,我们可以把页面置换算法看成是淘汰物物理页的规则。 + +页缺失太频繁的发生会非常影响性能,一个好的页面置换算法应该是可以减少页缺失出现的次数。 + +常见的页面置换算法有下面这 5 种(其他还有很多页面置换算法都是基于这些算法改进得来的): + +![常见的页面置换算法](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/image-20230409113009139.png) + +1. **最佳页面置换算法(OPT,Optimal)**:优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。 +2. **先进先出页面置换算法(FIFO,First In First Out)** : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可满足需求。不过,它的性能并不是很好。 +3. **最近最久未使用页面置换算法(LRU ,Least Recently Used)**:LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。LRU 算法是根据各页之前的访问情况来实现,因此是易于实现的。OPT 算法是根据各页未来的访问情况来实现,因此是不可实现的。 +4. **最少使用页面置换算法(LFU,Least Frequently Used)** : 和 LRU 算法比较像,不过该置换算法选择的是之前一段时间内使用最少的页面作为淘汰页。 +5. **时钟页面置换算法(Clock)**:可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。 + +**FIFO 页面置换算法性能为何不好?** + +主要原因主要有二: + +1. **经常访问或者需要长期存在的页面会被频繁调入调出**:较早调入的页往往是经常被访问或者需要长期存在的页,这些页会被反复调入和调出。 +2. **存在 Belady 现象**:被置换的页面并不是进程不会访问的,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。出现该异常的原因是因为 FIFO 算法只考虑了页面进入内存的顺序,而没有考虑页面访问的频率和紧迫性。 + +**哪一种页面置换算法实际用的比较多?** + +LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT 的页面置换算法。 + +不过,需要注意的是,实际应用中这些算法会被做一些改进,就比如 InnoDB Buffer Pool( InnoDB 缓冲池,MySQL 数据库中用于管理缓存页面的机制)就改进了传统的 LRU 算法,使用了一种称为"Adaptive LRU"的算法(同时结合了 LRU 和 LFU 算法的思想)。 + +### 分页机制和分段机制有哪些共同点和区别? + +**共同点**: + +- 都是非连续内存管理的方式。 +- 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。 + +**区别**: + +- 分页机制以页面为单位进行内存管理,而分段机制以段为单位进行内存管理。页的大小是固定的,由操作系统决定,通常为 2 的幂次方。而段的大小不固定,取决于我们当前运行的程序。 +- 页是物理单位,即操作系统将物理内存划分成固定大小的页面,每个页面的大小通常是 2 的幂次方,例如 4KB、8KB 等等。而段则是逻辑单位,是为了满足程序对内存空间的逻辑需求而设计的,通常根据程序中数据和代码的逻辑结构来划分。 +- 分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。分页机制解决了外部内存碎片的问题,但仍然可能会出现内部内存碎片。 +- 分页机制采用了页表来完成虚拟地址到物理地址的映射,页表通过一级页表和二级页表来实现多级映射;而分段机制则采用了段表来完成虚拟地址到物理地址的映射,每个段表项中记录了该段的起始地址和长度信息。 +- 分页机制对程序没有任何要求,程序只需要按照虚拟地址进行访问即可;而分段机制需要程序员将程序分为多个段,并且显式地使用段寄存器来访问不同的段。 + +### 段页机制 + +结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。 + +在段页式机制下,地址翻译的过程分为两个步骤: + +1. **段式地址映射(虚拟地址 → 线性地址):** + - 虚拟地址 = 段选择符(段号)+ 段内偏移。 + - 根据段号查段表,找到段基址,加上段内偏移得到线性地址。 +2. **页式地址映射(线性地址 → 物理地址):** + - 线性地址 = 页号 + 页内偏移。 + - 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。 + +### 局部性原理 + +要想更好地理解虚拟内存技术,必须要知道计算机中著名的 **局部性原理(Locality Principle)**。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。 + +局部性原理是指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。其中,时间局部性是指一个数据项或指令在一段时间内被反复使用的特点,空间局部性是指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。 + +在分页机制中,页表的作用是将虚拟地址转换为物理地址,从而完成内存访问。在这个过程中,局部性原理的作用体现在两个方面: + +- **时间局部性**:由于程序中存在一定的循环或者重复操作,因此会反复访问同一个页或一些特定的页,这就体现了时间局部性的特点。为了利用时间局部性,分页机制中通常采用缓存机制来提高页面的命中率,即将最近访问过的一些页放入缓存中,如果下一次访问的页已经在缓存中,就不需要再次访问内存,而是直接从缓存中读取。 +- **空间局部性**:由于程序中数据和指令的访问通常是具有一定的空间连续性的,因此当访问某个页时,往往会顺带访问其相邻的一些页。为了利用空间局部性,分页机制中通常采用预取技术来预先将相邻的一些页读入内存缓存中,以便在未来访问时能够直接使用,从而提高访问速度。 + +总之,局部性原理是计算机体系结构设计的重要原则之一,也是许多优化算法的基础。在分页机制中,利用时间局部性和空间局部性,采用缓存和预取技术,可以提高页面的命中率,从而提高内存访问效率 + +## 文件系统 + +### 文件系统主要做了什么? + +文件系统主要负责管理和组织计算机存储设备上的文件和目录,其功能包括以下几个方面: + +1. **存储管理**:将文件数据存储到物理存储介质中,并且管理空间分配,以确保每个文件都有足够的空间存储,并避免文件之间发生冲突。 +2. **文件管理**:文件的创建、删除、移动、重命名、压缩、加密、共享等等。 +3. **目录管理**:目录的创建、删除、移动、重命名等等。 +4. **文件访问控制**:管理不同用户或进程对文件的访问权限,以确保用户只能访问其被授权访问的文件,以保证文件的安全性和保密性。 + +### 硬链接和软链接有什么区别? + +在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种: + +**1、硬链接(Hard Link)** + +- 在 Linux/类 Unix 文件系统中,每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。硬链接通过 inode 节点号建立连接,硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的(可以看作是互为硬链接,源头是同一份文件),删除其中任何一个对另外一个没有影响,可以通过给文件设置硬链接文件来防止重要文件被误删。 +- 只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。 +- 硬链接具有一些限制,不能对目录以及不存在的文件创建硬链接,并且,硬链接也不能跨越文件系统。 +- `ln` 命令用于创建硬链接。 + +**2、软链接(Symbolic Link 或 Symlink)** + +- 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。 +- 源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。 +- 软连接类似于 Windows 系统中的快捷方式。 +- 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。 +- `ln -s` 命令用于创建软链接。 + +### 硬链接为什么不能跨文件系统? + +我们之前提到过,硬链接是通过 inode 节点号建立连接的,而硬链接和源文件共享相同的 inode 节点号。 + +然而,每个文件系统都有自己的独立 inode 表,且每个 inode 表只维护该文件系统内的 inode。如果在不同的文件系统之间创建硬链接,可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。 + +### 提高文件系统性能的方式有哪些? + +- **优化硬件**:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Independent Disks)等技术提高磁盘性能。 +- **选择合适的文件系统选型**:不同的文件系统具有不同的特性,对于不同的应用场景选择合适的文件系统可以提高系统性能。 +- **运用缓存**:访问磁盘的效率比较低,可以运用缓存来减少磁盘的访问次数。不过,需要注意缓存命中率,缓存命中率过低的话,效果太差。 +- **避免磁盘过度使用**:注意磁盘的使用率,避免将磁盘用满,尽量留一些剩余空间,以免对文件系统的性能产生负面影响。 +- **对磁盘进行合理的分区**:合理的磁盘分区方案,能够使文件系统在不同的区域存储文件,从而减少文件碎片,提高文件读写性能。 + +### 常见的磁盘调度算法有哪些? + +磁盘调度算法是操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。 + +一次磁盘读写操作的时间由磁盘寻道/寻找时间、延迟时间和传输时间决定。磁盘调度算法可以通过改变到达磁盘请求的处理顺序,减少磁盘寻道时间和延迟时间。 + +常见的磁盘调度算法有下面这 6 种(其他还有很多磁盘调度算法都是基于这些算法改进得来的): + +![常见的磁盘调度算法](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/disk-scheduling-algorithms.png) + +1. **先来先服务算法(First-Come First-Served,FCFS)**:按照请求到达磁盘调度器的顺序进行处理,先到达的请求的先被服务。FCFS 算法实现起来比较简单,不存在算法开销。不过,由于没有考虑磁头移动的路径和方向,平均寻道时间较长。同时,该算法容易出现饥饿问题,即一些后到的磁盘请求可能需要等待很长时间才能得到服务。 +2. **最短寻道时间优先算法(Shortest Seek Time First,SSTF)**:也被称为最佳服务优先(Shortest Service Time First,SSTF)算法,优先选择距离当前磁头位置最近的请求进行服务。SSTF 算法能够最小化磁头的寻道时间,但容易出现饥饿问题,即磁头附近的请求不断被服务,远离磁头的请求长时间得不到响应。实际应用中,需要优化一下该算法的实现,避免出现饥饿问题。 +3. **扫描算法(SCAN)**:也被称为电梯(Elevator)算法,基本思想和电梯非常类似。磁头沿着一个方向扫描磁盘,如果经过的磁道有请求就处理,直到到达磁盘的边界,然后改变移动方向,依此往复。SCAN 算法能够保证所有的请求得到服务,解决了饥饿问题。但是,如果磁头从一个方向刚扫描完,请求才到的话。这个请求就需要等到磁头从相反方向过来之后才能得到处理。 +4. **循环扫描算法(Circular Scan,C-SCAN)**:SCAN 算法的变体,只在磁盘的一侧进行扫描,并且只按照一个方向扫描,直到到达磁盘边界,然后回到磁盘起点,重新开始循环。 +5. **边扫描边观察算法(LOOK)**:SCAN 算法中磁头到了磁盘的边界才改变移动方向,这样可能会做很多无用功,因为磁头移动方向上可能已经没有请求需要处理了。LOOK 算法对 SCAN 算法进行了改进,如果磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,依此往复。也就是边扫描边观察指定方向上还有无请求,因此叫 LOOK。 +6. **均衡循环扫描算法(C-LOOK)**:C-SCAN 只有到达磁盘边界时才能改变磁头移动方向,并且磁头返回时也需要返回到磁盘起点,这样可能会做很多无用功。C-LOOK 算法对 C-SCAN 算法进行了改进,如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。 + +## 参考 + +- 《计算机操作系统—汤小丹》第四版 +- 《深入理解计算机系统》 +- 《重学操作系统》 +- 《现代操作系统原理与实现》 +- 王道考研操作系统知识点整理: +- 内存管理之伙伴系统与 SLAB: +- 为什么 Linux 需要虚拟内存: +- 程序员的自我修养(七):内存缺页错误: +- 虚拟内存的那点事儿: + + + diff --git a/docs_en/cs-basics/operating-system/shell-intro.en.md b/docs_en/cs-basics/operating-system/shell-intro.en.md new file mode 100644 index 00000000000..f915c0f4c77 --- /dev/null +++ b/docs_en/cs-basics/operating-system/shell-intro.en.md @@ -0,0 +1,537 @@ +--- +title: Shell 编程基础知识总结 +category: 计算机基础 +tag: + - 操作系统 + - Linux +head: + - - meta + - name: description + content: Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。这篇文章我会简单总结一下 Shell 编程基础知识,带你入门 Shell 编程! + - - meta + - name: keywords + content: Shell,脚本,命令,自动化,运维,Linux,基础语法 +--- + +Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。 + +这篇文章我会简单总结一下 Shell 编程基础知识,带你入门 Shell 编程! + +## 走进 Shell 编程的大门 + +### 为什么要学 Shell? + +学一个东西,我们大部分情况都是往实用性方向着想。从工作角度来讲,学习 Shell 是为了提高我们自己工作效率,提高产出,让我们在更少的时间完成更多的事情。 + +很多人会说 Shell 编程属于运维方面的知识了,应该是运维人员来做,我们做后端开发的没必要学。我觉得这种说法大错特错,相比于专门做 Linux 运维的人员来说,我们对 Shell 编程掌握程度的要求要比他们低,但是 Shell 编程也是我们必须要掌握的! + +目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。 + +两者之间,Shell 几乎是 IT 企业必须使用的运维自动化编程语言,特别是在运维工作中的服务监控、业务快速部署、服务启动停止、数据备份及处理、日志分析等环节里,shell 是不可缺的。Python 更适合处理复杂的业务逻辑,以及开发复杂的运维软件工具,实现通过 web 访问等。Shell 是一个命令解释器,解释执行用户所输入的命令和程序。一输入命令,就立即回应的交互的对话方式。 + +另外,了解 shell 编程也是大部分互联网公司招聘后端开发人员的要求。下图是我截取的一些知名互联网公司对于 Shell 编程的要求。 + +![大型互联网公司对于shell编程技能的要求](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60190220.jpg) + +### 什么是 Shell? + +简单来说“Shell 编程就是对一堆 Linux 命令的逻辑化处理”。 + +W3Cschool 上的一篇文章是这样介绍 Shell 的,如下图所示。 +![什么是 Shell?](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/19456505.jpg) + +### Shell 编程的 Hello World + +学习任何一门编程语言第一件事就是输出 HelloWorld 了!下面我会从新建文件到 shell 代码编写来说下 Shell 编程如何输出 Hello World。 + +(1)新建一个文件 helloworld.sh :`touch helloworld.sh`,扩展名为 sh(sh 代表 Shell)(扩展名并不影响脚本执行,见名知意就好,如果你用 php 写 shell 脚本,扩展名就用 php 好了) + +(2) 使脚本具有执行权限:`chmod +x helloworld.sh` + +(3) 使用 vim 命令修改 helloworld.sh 文件:`vim helloworld.sh`(vim 文件------>进入文件----->命令模式------>按 i 进入编辑模式----->编辑文件 ------->按 Esc 进入底行模式----->输入:wq/q! (输入 wq 代表写入内容并退出,即保存;输入 q!代表强制退出不保存。)) + +helloworld.sh 内容如下: + +```shell +#!/bin/bash +#第一个shell小程序,echo 是linux中的输出命令。 +echo "helloworld!" +``` + +shell 中 # 符号表示注释。**shell 的第一行比较特殊,一般都会以#!开始来指定使用的 shell 类型。在 linux 中,除了 bash shell 以外,还有很多版本的 shell, 例如 zsh、dash 等等...不过 bash shell 还是我们使用最多的。** + +(4) 运行脚本:`./helloworld.sh` 。(注意,一定要写成 `./helloworld.sh` ,而不是 `helloworld.sh` ,运行其它二进制的程序也一样,直接写 `helloworld.sh` ,linux 系统会去 PATH 里寻找有没有叫 helloworld.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 `helloworld.sh` 是会找不到命令的,要用`./helloworld.sh` 告诉系统说,就在当前目录找。) + +![shell 编程Hello World](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/55296212.jpg) + +## Shell 变量 + +### Shell 编程中的变量介绍 + +**Shell 编程中一般分为三种变量:** + +1. **我们自己定义的变量(自定义变量):** 仅在当前 Shell 实例中有效,其他 Shell 启动的程序不能访问局部变量。 +2. **Linux 已定义的环境变量**(环境变量, 例如:`PATH`, ​`HOME` 等..., 这类变量我们可以直接使用),使用 `env` 命令可以查看所有的环境变量,而 set 命令既可以查看环境变量也可以查看自定义变量。 +3. **Shell 变量**:Shell 变量是由 Shell 程序设置的特殊变量。Shell 变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了 Shell 的正常运行 + +**常用的环境变量:** + +> PATH 决定了 shell 将到哪些目录中寻找命令或程序 +> HOME 当前用户主目录 +> HISTSIZE  历史记录数 +> LOGNAME 当前用户的登录名 +> HOSTNAME  指主机的名称 +> SHELL 当前用户 Shell 类型 +> LANGUAGE  语言相关的环境变量,多语言可以修改此环境变量 +> MAIL  当前用户的邮件存放目录 +> PS1  基本提示符,对于 root 用户是#,对于普通用户是\$ + +**使用 Linux 已定义的环境变量:** + +比如我们要看当前用户目录可以使用:`echo $HOME`命令;如果我们要看当前用户 Shell 类型 可以使用`echo $SHELL`命令。可以看出,使用方法非常简单。 + +**使用自己定义的变量:** + +```shell +#!/bin/bash +#自定义变量hello +hello="hello world" +echo $hello +echo "helloworld!" +``` + +![使用自己定义的变量](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/19835037.jpg) + +**Shell 编程中的变量名的命名的注意事项:** + +- 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头,但是可以使用下划线(\_)开头。 +- 中间不能有空格,可以使用下划线(\_)。 +- 不能使用标点符号。 +- 不能使用 bash 里的关键字(可用 help 命令查看保留关键字)。 + +### Shell 字符串入门 + +字符串是 shell 编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号。这点和 Java 中有所不同。 + +在单引号中所有的特殊符号,如$和反引号都没有特殊含义。在双引号中,除了"$"、"\\"、反引号和感叹号(需开启 `history expansion`),其他的字符没有特殊含义。 + +**单引号字符串:** + +```shell +#!/bin/bash +name='SnailClimb' +hello='Hello, I am $name!' +echo $hello +``` + +输出内容: + +```plain +Hello, I am $name! +``` + +**双引号字符串:** + +```shell +#!/bin/bash +name='SnailClimb' +hello="Hello, I am $name!" +echo $hello +``` + +输出内容: + +```plain +Hello, I am SnailClimb! +``` + +### Shell 字符串常见操作 + +**拼接字符串:** + +```shell +#!/bin/bash +name="SnailClimb" +# 使用双引号拼接 +greeting="hello, "$name" !" +greeting_1="hello, ${name} !" +echo $greeting $greeting_1 +# 使用单引号拼接 +greeting_2='hello, '$name' !' +greeting_3='hello, ${name} !' +echo $greeting_2 $greeting_3 +``` + +Output result: + +![Output results](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/51148933.jpg) + +**Get string length:** + +```shell +#!/bin/bash +#Get the string length +name="SnailClimb" +# The first way +echo ${#name} #output 10 +# The second way +expr length "$name"; +``` + +Output result: + +```plain +10 +10 +``` + +When using the expr command, spaces must be included around the operator in the expression. If no spaces are included, the expression itself will be output: + +```shell +expr 5+6 // Directly output 5+6 +expr 5 + 6 //output 11 +``` + +For some operators, we also need to use the symbol `\` to escape, otherwise a syntax error will be prompted. + +```shell +expr 5 * 6 // Output error +expr 5 \* 6 // Output 30 +``` + +**Intercept substring:** + +Simple string interception: + +```shell +#Truncate 10 characters starting from the first character of the string +str="SnailClimb is a great man" +echo ${str:0:10} #Output:SnailClimb +``` + +Intercept based on expression: + +```shell +#!bin/bash +#author:amau + +var="https://www.runoob.com/linux/linux-shell-variable.html" +# % means delete the next match, the shortest result +# %% means delete the matching from the end, the longest matching result +# #Indicates deleting matches from the beginning, the shortest result +# ## indicates deleting the match from the beginning and the longest matching result +# Note: * is a wildcard character, meaning to match any number of any characters +s1=${var%%t*} #h +s2=${var%t*} #https://www.runoob.com/linux/linux-shell-variable.h +s3=${var%%.*} #https://www +s4=${var#*/} #/www.runoob.com/linux/linux-shell-variable.html +s5=${var##*/} #linux-shell-variable.html +``` + +### Shell array + +bash supports one-dimensional arrays (not multi-dimensional arrays) and does not limit the size of the array. I give you a shell code example about array operations below. Through this example, you can know how to create an array, get the length of the array, get/delete the array element at a specific position, delete the entire array, and traverse the array. + +```shell +#!/bin/bash +array=(1 2 3 4 5); +# Get the length of the array +length=${#array[@]} +# or +length2=${#array[*]} +#Output array length +echo $length #Output: 5 +echo $length2 #Output: 5 +# Output the third element of the array +echo ${array[2]} #Output: 3 +unset array[1]# Deleting the element with subscript 1 means deleting the second element +for i in ${array[@]};do echo $i ;done # Traverse the array, output: 1 3 4 5 +unset array; #Delete all elements in the array +for i in ${array[@]};do echo $i ;done # Traverse the array, the array elements are empty and there is no output content +``` + +## Shell basic operators + +> Description: The picture comes from "Rookie Tutorial" + +Shell programming supports the following operators + +- Arithmetic operators +- Relational operators +- Boolean operators +- String operators +- File test operator + +### Arithmetic operators + +![Arithmetic operator](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/4937342.jpg) + +I use the addition operator to make a simple example (note: not single quotes, but backticks): + +```shell +#!/bin/bash +a=3;b=3; +val=`expr $a + $b` +#Output: Total value: 6 +echo "Total value : $val" +``` + +### Relational operators + +Relational operators only support numbers, not strings, unless the value of the string is a number. + +![shell relational operator](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/64391380.jpg) + +A simple example demonstrates the use of relational operators. The function of the following shell program is to output A when score=100, otherwise output B. + +```shell +#!/bin/bash +score=90; +maxscore=100; +if [ $score -eq $maxscore ] +then + echo "A" +else + echo "B" +fi +``` + +Output result: + +```plain +B +``` + +### Logical operators + +![Logical operator](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60545848.jpg) + +Example: + +```shell +#!/bin/bash +a=$(( 1 && 0)) +# Output: 0; for logical AND operations, only if both sides of the AND are 1, the result of the AND is 1; otherwise, the result of the AND is 0 +echo $a; +``` + +### Boolean operators + +![Boolean operator](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/93961425.jpg) + +I won’t do a demonstration here, it should be pretty simple. + +### String operators + +![String operator](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/309094.jpg) + +Simple example: + +```shell +#!/bin/bash +a="abc"; +b="efg"; +if [ $a = $b ] +then + echo "a equals b" +else + echo "a is not equal to b" +fi +``` + +Output: + +```plain +a is not equal to b +``` + +### File related operators + +![File related operators](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60359774.jpg) + +The usage is very simple. For example, we have defined a file path `file="/usr/learnshell/test.sh"`. If we want to determine whether the file is readable, we can do this `if [ -r $file ]`. If we want to determine whether the file is writable, we can do this `-w $file`. Isn’t it very simple? + +## Shell process control + +### if conditional statement + +Simple if else-if else conditional statement example + +```shell +#!/bin/bash +a=3; +b=9; +if [ $a -eq $b ] +then + echo "a equals b" +elif [ $a -gt $b ] +then + echo "a is greater than b" +else + echo "a is less than b" +fi +``` + +Output result: + +```plain +a is less than b +``` + +I believe that through the above examples, you have mastered the if conditional statement in shell programming. However, one thing to mention is that unlike our common if conditional statements in Java and PHP, shell if conditional statements cannot contain empty statements, which are statements that do nothing. + +### for loop statement + +Learn the most basic use of the for loop statement through the following three simple examples. In fact, the function of the for loop statement is much greater than the examples you see below. + +**Output the data in the current list:** + +```shell +for loop in 1 2 3 4 5 +do + echo "The value is: $loop" +done +``` + +**Generate 10 random numbers:** + +```shell +#!/bin/bash +for i in {0..9}; +do + echo $RANDOM; +done +``` + +**Output 1 to 5:** + +Normally, you need to add \$ when calling shell variables, but it is not required in (()) of for. Let’s look at an example: + +```shell +#!/bin/bash +length=5 +for((i=1;i<=length;i++));do + echo $i; +done; +``` + +### while statement + +**Basic while loop statement:** + +```shell +#!/bin/bash +int=1 +while(( $int<=5 )) +do + echo $int + let "int++" +done +``` + +**while loop can be used to read keyboard information:** + +```shell +echo 'Press to exit' +echo -n 'Enter your favorite movie: ' +while read FILM +do + echo "Yes! $FILM is a good movie" +done +``` + +Output content: + +```plain +Press to exit +Enter your favorite movie: Transformers +Yes! Transformers is a good movie +```**Infinite Loop:** + +```shell +while true +do + command +done +``` + +## Shell function + +### Function with no parameters and no return value + +```shell +#!/bin/bash +hello(){ + echo "This is my first shell function!" +} +echo "-----Function starts execution-----" +hello +echo "-----Function execution completed-----" +``` + +Output result: + +```plain +-----Function starts execution----- +This is my first shell function! +-----Function execution completed----- +``` + +### Functions with return values + +**Add two numbers after inputting them and return the result: ** + +```shell +#!/bin/bash +funWithReturn(){ + echo "Enter the first number: " + read aNum + echo "Enter the second number: " + read anotherNum + echo "The two numbers are $aNum and $anotherNum!" + return $(($aNum+$anotherNum)) +} +funWithReturn +echo "The sum of the two numbers entered is $?" +``` + +Output result: + +```plain +Enter the first number: +1 +Enter the second number: +2 +The two numbers are 1 and 2! +The sum of the two numbers entered is 3 +``` + +### Function with parameters + +```shell +#!/bin/bash +funWithParam(){ + echo "The first parameter is $1 !" + echo "The second parameter is $2 !" + echo "The tenth parameter is $10!" + echo "The tenth parameter is ${10} !" + echo "The eleventh parameter is ${11} !" + echo "The total number of parameters is $#!" + echo "Output all parameters as a string $* !" +} +funWithParam 1 2 3 4 5 6 7 8 9 34 73 +``` + +Output result: + +```plain +The first parameter is 1 ! +The second parameter is 2! +The tenth parameter is 10! +The tenth parameter is 34! +The eleventh parameter is 73! +There are 11 parameters in total! +Output all parameters as a string 1 2 3 4 5 6 7 8 9 34 73 ! +``` + + \ No newline at end of file diff --git a/docs_en/database/basis.en.md b/docs_en/database/basis.en.md new file mode 100644 index 00000000000..c47e63af29f --- /dev/null +++ b/docs_en/database/basis.en.md @@ -0,0 +1,238 @@ +--- +title: 数据库基础知识总结 +category: 数据库 +tag: + - 数据库基础 +--- + + + +数据库知识基础,这部分内容一定要理解记忆。虽然这部分内容只是理论知识,但是非常重要,这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容,所以参考了维基百科和百度百科相应的介绍。 + +## 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员? + +这四个概念描述了从数据本身到管理整个体系的不同层次,我们常用一个图书馆的例子来把它们串联起来理解。 + +- **数据库 (Database - DB):** 它就像是图书馆里,书架上存放的所有书籍和资料。从技术上讲,数据库就是按照一定数据模型组织、描述和储存起来的、可以被各种用户共享的结构化数据的集合。它就是我们最终要存取的核心——信息本身。 +- **数据库管理系统 (Database Management System - DBMS):** 它就像是整个图书馆的管理系统,包括图书的分类编目规则、借阅归还流程、安全检查系统等等。从技术上讲,DBMS 是一种大型软件,比如我们常用的 MySQL、Oracle、PostgreSQL 软件。它的核心职责是科学地组织和存储数据、高效地获取和维护数据;为我们屏蔽了底层文件操作的复杂性,提供了一套标准接口(如 SQL)来操纵数据,并负责并发控制、事务管理、权限控制等复杂问题。 +- **数据库系统 (Database System - DBS):** 它就是整个正常运转的图书馆。这是一个更大的概念,不仅包括书(DB)和管理系统(DBMS),还包括了硬件、应用和使用的人。 +- **数据库管理员 (Database Administrator - DBA ):** 他就是图书馆的馆长,负责整个数据库系统正常运行。他的职责非常广泛,包括数据库的设计、安装、监控、性能调优、备份与恢复、安全管理等等,确保整个系统的稳定、高效和安全。 + +DB 和 DBMS 我们通常会搞混,这里再简单提一下:**通常我们说“用 MySQL 数据库”,其实是用 MySQL(DBMS)来管理一个或多个数据库(DB)。** + +## DBMS 有哪些主要的功能 + +DBMS 通常提供四大核心功能: + +1. **数据定义:** 这是 DBMS 的基础。它提供了一套数据定义语言(Data Definition Language - DDL),让我们能够创建、修改和删除数据库中的各种对象。这不仅仅是定义表的结构(比如字段名、数据类型),还包括定义视图、索引、触发器、存储过程等。 +2. **数据操作:** 这是我们作为开发者日常使用最多的功能。它提供了一套数据操作语言(Data Manipulation Language - DML),核心就是我们熟悉的增、删、改、查(CRUD)操作。它让我们能够方便地对数据库中的数据进行操作和检索。 +3. **数据控制:** 这是保证数据正确、安全、可靠的关键。通常包含并发控制、事务管理、完整性约束、权限控制、安全性限制等功能。 +4. **数据库维护:** 这部分功能是为了保障数据库系统的长期稳定运行。它包括了数据的导入导出、数据库的备份与恢复、性能监控与分析、以及系统日志管理等。 + +## 你知道哪些类型的 DBMS? + +### 关系型数据库 + +除了我们最常用的关系型数据库(RDBMS),比如 MySQL(开源首选)、PostgreSQL(功能最全)、Oracle(企业级),它们基于严格的表结构和 SQL,非常适合结构化数据和需要事务保证的场景,例如银行交易、订单系统。 + +近年来,为了应对互联网应用带来的海量数据、高并发和多样化数据结构的需求,涌现出了一大批 NoSQL 和 NewSQL 数据库。 + +### NoSQL 数据库 + +它们的共同特点是为了极致的性能和水平扩展能力,在某些方面(通常是事务)做了妥协。 + +**1. 键值数据库,代表是 Redis。** + +- **特点:** 数据模型极其简单,就是一个巨大的 Map,通过 Key 来存取 Value。内存操作,性能极高。 +- **适用场景:** 非常适合做缓存、会话存储、计数器等对读写性能要求极高的场景。 + +**2. 文档数据库,代表是 MongoDB。** + +- **特点:** 它存储的是半结构化的文档(比如 JSON/BSON),结构灵活,不需要预先定义表结构。 +- **适用场景:** 特别适合那些数据结构多变、快速迭代的业务,比如用户画像、内容管理系统、日志存储等。 + +**3. 列式数据库,代表是 HBase, Cassandra。** + +- **特点:** 数据是按列族而不是按行来存储的。这使得它在对大量行进行少量列的读取时,性能极高。 +- **适用场景:** 专为海量数据存储和分析设计,非常适合做大数据分析、监控数据存储、推荐系统等需要高吞吐量写入和范围扫描的场景。 + +**4. 图形数据库,代表是 Neo4j。** + +- **特点:** 数据模型是节点(Nodes)和边(Edges),专门用来存储和查询实体之间的复杂关系。 +- **适用场景:** 在社交网络(好友关系)、推荐引擎(用户-商品关系)、知识图谱、欺诈检测(资金流动关系)等场景下,表现远超关系型数据库。 + +### NewSQL 数据库 + +由于 NoSQL 不支持事务,很多对于数据安全要去非常高的系统(比如财务系统、订单系统、交易系统)就不太适合使用了。不过,这类系统往往有存储大量数据的需求。 + +这些系统往往只能通过购买性能更强大的计算机,或者通过数据库中间件来提高存储能力。不过,前者的金钱成本太高,后者的开发成本太高。 + +于是,**NewSQL** 就来了! + +简单来说,NewSQL 就是:**分布式存储+SQL+事务** 。NewSQL 不仅具有 NoSQL 对海量数据的存储管理能力,还保持了传统数据库支持 ACID 和 SQL 等特性。因此,NewSQL 也可以称为 **分布式关系型数据库**。 + +NewSQL 数据库设计的一些目标: + +1. 横向扩展(Scale Out) : 通过增加机器的方式来提高系统的负载能力。与之类似的是 Scale Up(纵向扩展),升级硬件设备的方式来提高系统的负载能力。 +2. 强一致性(Strict Consistency):在任意时刻,所有节点中的数据是一样的。 +3. 高可用(High Availability):系统几乎可以一直提供服务。 +4. 支持标准 SQL(Structured Query Language) :PostgreSQL、MySQL、Oracle 等关系型数据库都支持 SQL 。 +5. 事务(ACID) : 原子性(Atomicity)、一致性(Consistency)、 隔离性(Isolation); 持久性(Durability)。 +6. 兼容主流关系型数据库 : 兼容 MySQL、Oracle、PostgreSQL 等常用关系型数据库。 +7. 云原生 (Cloud Native):可在公有云、私有云、混合云中实现部署工具化、自动化。 +8. HTAP(Hybrid Transactional/Analytical Processing) :支持 OLTP 和 OLAP 混合处理。 + +NewSQL 数据库代表:Google 的 F1/Spanner、阿里的 [OceanBase](https://open.oceanbase.com/)、PingCAP 的 [TiDB](https://pingcap.com/zh/product-community/) 。 + +## 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性? + +- **元组**:元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。 +- **码**:码就是能唯一标识实体的属性,对应表中的列。 +- **候选码**:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 +- **主码** : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。 +- **外码** : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。 +- **主属性**:候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。 +- **非主属性:** 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。 + +## 什么是 ER 图? + +我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计,这个也是面试官问你项目的时候经常会被问到的。 + +**ER 图** 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。 + +ER 图由下面 3 个要素组成: + +- **实体**:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。 +- **属性**:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。 +- **联系**:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。 + +下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。 + +![学生与课程之间联系的E-R图](https://oss.javaguide.cn/github/javaguide/csdn/c745c87f6eda9a439e0eea52012c7f4a.png) + +## 数据库范式了解吗? + +数据库范式有 3 种: + +- 1NF(第一范式):属性不可再分。 +- 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。 +- 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。 + +### 1NF(第一范式) + +属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。**1NF 是所有关系型数据库的最基本要求** ,也就是说关系型数据库中创建的表一定满足第一范式。 + +### 2NF(第二范式) + +2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。 + +![第二范式](https://oss.javaguide.cn/github/javaguide/csdn/bd1d31be3779342427fc9e462bf7f05c.png) + +一些重要的概念: + +- **函数依赖(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(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。 + +### 3NF(第三范式) + +3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,**基本**上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。 + +## 主键和外键有什么区别? + +从定义和属性上看,它们的区别是: + +- **主键 (Primary Key):** 它的核心作用是唯一标识表中的每一行数据。因此,主键列的值必须是唯一的 (Unique) 且不能为空 (Not Null)。一张表只能有一个主键。主键保证了实体完整性。 +- **外键 (Foreign Key):** 它的核心作用是建立并强制两张表之间的关联关系。一张表中的外键列,其值必须对应另一张表中某行的主键值(或者是一个 NULL 值)。因此,外键的值可以重复,也可以为空。一张表可以有多个外键,分别关联到不同的表。外键保证了引用完整性。 + +用一个简单的电商例子来说明:假设我们有两张表:`users` (用户表) 和 `orders` (订单表)。 + +- 在 `users` 表中,`user_id` 列是**主键**。每个用户的 `user_id` 都是独一无二的,我们用它来区分张三和李四。 +- 在 `orders` 表中,`order_id` 是它自己的**主键**。同时,它会有一个 `user_id` 列,这个列就是一个**外键**,它引用了 `users` 表的 `user_id` 主键。 + +这个外键约束就保证了: + +1. 你不能创建一个不属于任何已知用户的订单( `user_id` 在 `users` 表中不存在)。 +2. 你不能删除一个已经下了订单的用户(除非设置了级联删除等特殊规则)。 + +## 为什么不推荐使用外键与级联? + +对于外键和级联,阿里巴巴开发手册这样说到: + +> 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 +> +> 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度 + +为什么不要用外键呢?大部分人可能会这样回答: + +1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 +2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力; +3. **对分库分表不友好**:因为分库分表下外键是无法生效的。 +4. …… + +我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: + +1. 保证了数据库数据的一致性和完整性; +2. 级联操作方便,减轻了程序代码量; +3. …… + +所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。 + +## 什么是存储过程? + +我们可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程在业务比较复杂的时候是非常实用的,比如很多时候我们完成一个操作可能需要写一大串 SQL 语句,这时候我们就可以写有一个存储过程,这样也方便了我们下一次的调用。存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯 SQL 语句执行要快,因为存储过程是预编译过的。 + +存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。 + +阿里巴巴 Java 开发手册里要求禁止使用存储过程。 + +![阿里巴巴Java开发手册: 禁止存储过程](https://oss.javaguide.cn/github/javaguide/csdn/0fa082bc4d4f919065767476a41b2156.png) + +## drop、delete 与 truncate 区别? + +### 用法不同 + +- `drop`(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。 +- `truncate` (清空数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。 +- `delete`(删除数据) : `delete from 表名 where 列名=值`,删除某一行的数据,如果不加 `where` 子句和`truncate table 表名`作用类似。 + +`truncate`, `delete` without `where` clause, and `drop` will delete the data in the table, but **`truncate` and `delete` only delete the data without deleting the structure (definition) of the table. When the `drop` statement is executed, the structure of the table will also be deleted, that is, the corresponding table no longer exists after executing `drop`. ** + +### Belong to different database languages + +`truncate` and `drop` belong to DDL (data definition language) statements, and the operation takes effect immediately. The original data is not placed in the rollback segment, cannot be rolled back, and the operation does not trigger the trigger. The `delete` statement is a DML (database operation language) statement. This operation will be placed in the rollback segment and will only take effect after the transaction is submitted. + +**Differences between DML statements and DDL statements:** + +- DML is the abbreviation of Database Manipulation Language (Data Manipulation Language). It refers to the operation of table records in the database, mainly including the insertion, update, deletion and query of table records. It is the most frequently used operation by developers in daily life. +- DDL (Data Definition Language) is the abbreviation of data definition language. Simply put, it is an operating language for creating, deleting, and modifying objects within the database. The biggest difference between it and the DML language is that DML only operates on the internal data of the table, and does not involve the definition of the table, modification of the structure, nor other objects. DDL statements are more commonly used by database administrators (DBAs) and are rarely used by general developers. + +In addition, since `select` will not destroy the table, some places also distinguish `select` separately and call it database query language DQL (Data Query Language). + +### Different execution speeds + +Generally speaking: `drop` > `truncate` > `delete` (I have not actually tested this). + +- When the `delete` command is executed, the `binlog` log of the database will be generated. Logging takes time, but it also has the advantage of facilitating data rollback and recovery. +- The `truncate` command does not generate database logs when executed, so it is faster than `delete`. In addition, the auto-increment value of the table and the index will be restored to the initial size, etc. +- The `drop` command will release all the space occupied by the table. + +Tips: You should focus more on usage scenarios rather than execution efficiency. + +## What are the usual steps of database design? + +1. **Requirements Analysis**: Analyze user needs, including data, functional and performance requirements. +2. **Conceptual Structure Design**: Mainly use E-R model for design, including drawing E-R diagram. +3. **Logical structure design**: Realize the conversion from E-R model to relational model by converting E-R diagram into table. +4. **Physical structure design**: Mainly to select the appropriate storage structure and access path for the designed database. +5. **Database Implementation**: including programming, testing and trial operation +6. **Database Operation and Maintenance**: System operation and daily maintenance of the database. + +## Reference + +- +- +- + + \ No newline at end of file diff --git a/docs_en/database/character-set.en.md b/docs_en/database/character-set.en.md new file mode 100644 index 00000000000..439f2ae0dd4 --- /dev/null +++ b/docs_en/database/character-set.en.md @@ -0,0 +1,338 @@ +--- +title: 字符集详解 +category: 数据库 +tag: + - 数据库基础 +head: + - - meta + - name: keywords + content: 字符集,编码,UTF-8,UTF-16,GBK,utf8mb4,emoji,存储与传输 + - - meta + - name: description + content: 从编码与字符集原理入手,解释 utf8 与 utf8mb4 差异与 emoji 存储问题,指导数据库与应用的正确配置。 +--- + +MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4`**。 + +如果使用 **`utf8`** 的话,存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。 + +为什么会这样呢?这篇文章可以从源头给你解答。 + +## 字符集是什么? + +字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 **字符集** 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。 + +**计算机只能存储二进制的数据,那英文、汉字、表情等字符应该如何存储呢?** + +我们要将这些字符和二进制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"**字符编码**",反之,二进制数据解析成字符的过程称为“**字符解码**”。 + +## 字符编码是什么? + +字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法,可以看作是一种映射规则。也就是说,字符编码的目的是为了让计算机能够存储和传输各种文字信息。 + +每种字符集都有自己的字符编码规则,常用的字符集编码规则有 ASCII 编码、 GB2312 编码、GBK 编码、GB18030 编码、Big5 编码、UTF-8 编码、UTF-16 编码等。 + +## 有哪些常见的字符集? + +常见的字符集有:ASCII、GB2312、GB18030、GBK、Unicode……。 + +不同的字符集的主要区别在于: + +- 可以表示的字符范围 +- 编码方式 + +### ASCII + +**ASCII** (**A**merican **S**tandard **C**ode for **I**nformation **I**nterchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)。 + +**为什么 ASCII 字符集没有考虑到中文等其他字符呢?** 因为计算机是美国人发明的,当时,计算机的发展还处于比较雏形的时代,还未在其他国家大规模使用。因此,美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言。 + +ASCII 字符集至今为止共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。 + +一个 ASCII 码长度是一个字节也就是 8 个 bit,比如“a”对应的 ASCII 码是“01100001”。不过,最高位是 0 仅仅作为校验位,其余 7 位使用 0 和 1 进行组合,所以,ASCII 字符集可以定义 128(2^7)个字符。 + +由于,ASCII 码可以表示的字符实在是太少了。后来,人们对其进行了扩展得到了 **ASCII 扩展字符集** 。ASCII 扩展字符集使用 8 位(bits)表示一个字符,所以,ASCII 扩展字符集可以定义 256(2^8)个字符。 + +![ASCII字符编码](https://oss.javaguide.cn/github/javaguide/csdn/c1c6375d08ca268690cef2b13591a5b4.png) + +### GB2312 + +我们上面说了,ASCII 字符集是一种现代美国英语适用的字符集。因此,很多国家都捣鼓了一个适合自己国家语言的字符集。 + +GB2312 字符集是一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。不过,GB2312 字符集不支持绝大部分的生僻字和繁体字。 + +对于英语字符,GB2312 编码和 ASCII 码是相同的,1 字节编码即可。对于非英字符,需要 2 字节编码。 + +### GBK + +GBK 字符集可以看作是 GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。 + +GBK 中 K 是汉语拼音 Kuo Zhan(扩展)中的“Kuo”的首字母。 + +### GB18030 + +GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,是目前为止最全面的汉字字符集,共收录汉字 70000 多个。 + +### BIG5 + +BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。 + +### Unicode & UTF-8 + +为了更加适合本国语言,诞生了很多种字符集。 + +我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题:**使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。** + +就比如说你使用 UTF-8 编码方式打开 GB2312 编码格式的文件就会出现乱码。示例:“牛”这个汉字 GB2312 编码后的十六进制数值为 “C5A3”,而 “C5A3” 用 UTF-8 解码之后得到的却是 “ţ”。 + +你可以通过这个网站在线进行编码和解码: + +![](https://oss.javaguide.cn/github/javaguide/csdn/836c49b117ee4408871b0020b74c991d.png) + +这样我们就搞懂了乱码的本质:**编码和解码时用了不同或者不兼容的字符集** 。 + +![](https://oss.javaguide.cn/javaguide/a8808cbabeea49caa3af27d314fa3c02-1.jpg) + +为了解决这个问题,人们就想:“如果我们能够有一种字符集将世界上所有的字符都纳入其中就好了!”。 + +然后,**Unicode** 带着这个使命诞生了。 + +Unicode 字符集中包含了世界上几乎所有已知的字符。不过,Unicode 字符集并没有规定如何存储这些字符(也就是如何使用二进制数据表示这些字符)。 + +然后,就有了 **UTF-8**(**8**-bit **U**nicode **T**ransformation **F**ormat)。类似的还有 UTF-16、 UTF-32。 + +UTF-8 使用 1 到 4 个字节为每个字符编码, UTF-16 使用 2 或 4 个字节为每个字符编码,UTF-32 固定位 4 个字节为每个字符编码。 + +UTF-8 可以根据不同的符号自动选择编码的长短,像英文字符只需要 1 个字节就够了,这一点 ASCII 字符集一样 。因此,对于英语字符,UTF-8 编码和 ASCII 码是相同的。 + +UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。 + +**UTF-8** 是目前使用最广的一种字符编码。 + +![](https://oss.javaguide.cn/javaguide/1280px-Utf8webgrowth.svg.png) + +## MySQL 字符集 + +MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。 + +### 查看支持的字符集 + +你可以通过 `SHOW CHARSET` 命令来查看,支持 like 和 where 子句。 + +![](https://oss.javaguide.cn/javaguide/image-20211008164229671.png) + +### 默认字符集 + +在 MySQL5.7 中,默认字符集是 `latin1` ;在 MySQL8.0 中,默认字符集是 `utf8mb4` + +### 字符集的层次级别 + +MySQL 中的字符集有以下的层次级别: + +- `server`(MySQL 实例级别) +- `database`(库级别) +- `table`(表级别) +- `column`(字段级别) + +它们的优先级可以简单的认为是从上往下依次增大,也即 `column` 的优先级会大于 `table` 等其余层次的。如指定 MySQL 实例级别字符集是`utf8mb4`,指定某个表字符集是`latin1`,那么这个表的所有字段如果不指定的话,编码就是`latin1`。 + +#### server + +不同版本的 MySQL 其 `server` 级别的字符集默认值不同,在 MySQL5.7 中,其默认值是 `latin1` ;在 MySQL8.0 中,其默认值是 `utf8mb4` 。 + +当然也可以通过在启动 `mysqld` 时指定 `--character-set-server` 来设置 `server` 级别的字符集。 + +```bash +mysqld +mysqld --character-set-server=utf8mb4 +mysqld --character-set-server=utf8mb4 \ + --collation-server=utf8mb4_0900_ai_ci +``` + +Or if you are starting MySQL from source, you can specify options in the `cmake` command: + +```sh +cmake .-DDEFAULT_CHARSET=latin1 +or +cmake . -DDEFAULT_CHARSET=latin1 \ + -DDEFAULT_COLLATION=latin1_german1_ci +``` + +In addition, you can also change the value of `character_set_server` at runtime to modify the `server` level character set. + +The `server` level character set is a global setting of the MySQL server. It will not only serve as the default character set when creating or modifying the database (if no other character set is specified), but also affects the connection character set between the client and the server. For 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 + +The `database` level character set is specified when we create and modify the database: + +```sql +CREATE DATABASE db_name + [[DEFAULT] CHARACTER SET charset_name] + [[DEFAULT] COLLATE collation_name] + +ALTER DATABASE db_name + [[DEFAULT] CHARACTER SET charset_name] + [[DEFAULT] COLLATE collation_name] +``` + +As mentioned earlier, if no character set is specified when executing the above statement, MySQL will use the `server` level character set. + +You can check the character set of a database in the following way: + +```sql +USE db_name; +SELECT @@character_set_database, @@collation_database; +``` + +```sql +SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME +FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db_name'; +``` + +#### table + +The `table` level character set is specified when creating and modifying the table: + +```sql +CREATE TABLE tbl_name (column_list) + [[DEFAULT] CHARACTER SET charset_name] + [COLLATE collation_name]] + +ALTER TABLE tbl_name + [[DEFAULT] CHARACTER SET charset_name] + [COLLATE collation_name] +``` + +If no character set is specified when creating and modifying tables, the `database` level character set will be used. + +#### column + +The `column` level character set is also specified when creating and modifying the table, but it is defined in the column. Here is an example: + +```sql +CREATE TABLE t1 +( + col1 VARCHAR(5) + CHARACTER SET latin1 + COLLATE latin1_german1_ci +); +``` + +If a column-level character set is not specified, the table-level character set will be used. + +### Connection character set + +As mentioned earlier, the hierarchical levels of character sets 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`: Describes the character set used by the SQL statement sent by the client to the server. +- `character_set_connection`: Describes what character set the server uses for translation when it receives a SQL statement. +- `character_set_results`: Describes what character set is used in the results returned by the server to the client. + +Their values can be queried through the following SQL statement: + +```sql +SELECT * FROM performance_schema.session_variables +WHERE VARIABLE_NAME IN ( +'character_set_client', 'character_set_connection', +'character_set_results', 'collation_connection' +) ORDER BY VARIABLE_NAME; +``` + +```sql +SHOW SESSION VARIABLES LIKE 'character\_set\_%'; +``` + +If you want to modify the values of the variables mentioned earlier, you have the following methods: + +1. Modify the configuration file + +```properties +[mysql] +# Only for MySQL client program +default-character-set=utf8mb4 +``` + +2. Use SQL statements + +```sql +set names utf8mb4 +# Or modify them one by one +# SET character_set_client = utf8mb4; +# SET character_set_results = utf8mb4; +# SET collation_connection = utf8mb4; +``` + +### Impact of JDBC on connection character sets + +I don’t know if you have ever encountered a situation where emoji expressions are stored normally, but when you use software like Navicat to query, you find that the emoji expressions turn into question marks. This problem is most likely caused by the JDBC driver. + +According to the previous content, we know that the connection character set will also affect the data we store, and the JDBC driver will affect the connection character set. + +`mysql-connector-java` (JDBC driver) mainly affects the connection character set through these properties: + +- `characterEncoding` +- `characterSetResults` + +Taking `DataGrip 2023.1.2` as an example, in its advanced dialog box for configuring data sources, you can see that the default value of `characterSetResults` is `utf8`. When using `mysql-connector-java 8.0.25`, the connection character set will finally be set to `utf8mb3`. In this case, the emoji expression will be displayed as a question mark, and the current version of the driver does not support setting `characterSetResults` to `utf8mb4`, but it is allowed to change to `mysql-connector-java driver 8.0.29`. + +For details, please take a look at StackOverflow's 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 usage + +Normally, we recommend using UTF-8 as the default character encoding. + +However, there is a small pit here. + +There are two sets of UTF-8 encoding implementations in the MySQL character encoding set: + +- **`utf8`**: `utf8` encoding only supports `1-3` bytes. In `utf8` encoding, Chinese characters occupy 3 bytes, and other numbers, English, and symbols occupy one byte. However, emoji symbols occupy 4 bytes, and some more complex text and traditional Chinese characters also occupy 4 bytes. +- **`utf8mb4`**: Complete implementation of UTF-8, genuine! Supports up to 4 bytes for character representation, so it can be used to store emoji symbols. + +**Why are there two sets of UTF-8 encoding implementations? **The reasons are as follows: + +![](https://oss.javaguide.cn/javaguide/image-20211008164542347.png) + +Therefore, if you need to store `emoji` type data or some more complex text or traditional Chinese characters into the MySQL database, the database encoding must be specified as `utf8mb4` instead of `utf8`, otherwise an error will be reported when storing. + +Demonstrate it! (Environment: MySQL 5.7+) + +The table creation statement is as follows. We specify the database CHARSET as `utf8`. + +```sql +CREATE TABLE `user` ( + `id` varchar(66) CHARACTER SET utf8mb3 NOT NULL, + `name` varchar(33) CHARACTER SET utf8mb3 NOT NULL, + `phone` varchar(33) CHARACTER SET utf8mb3 DEFAULT NULL, + `password` varchar(100) CHARACTER SET utf8mb3 DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +When we execute the following insert statement to insert data into the database, an error is reported! + +```sql +INSERT INTO `user` (`id`, `name`, `phone`, `password`) +VALUES + ('A00003', 'guide brother😘😘😘', '181631312312', '123456');``` + +The error message is as follows: + +```plain +Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1 +``` + +## Reference + +-Charset & Encoding: +- Understand the character set and character encoding in ten minutes: +- Unicode-Wikipedia: +- GB2312-Wikipedia: +- UTF-8-Wikipedia: +- GB18030-Wikipedia: +- MySQL8 documentation: +- MySQL5.7 documentation: +- MySQL Connector/J documentation: + + \ No newline at end of file diff --git a/docs_en/database/elasticsearch/elasticsearch-questions-01.en.md b/docs_en/database/elasticsearch/elasticsearch-questions-01.en.md new file mode 100644 index 00000000000..a801052566e --- /dev/null +++ b/docs_en/database/elasticsearch/elasticsearch-questions-01.en.md @@ -0,0 +1,22 @@ +--- +title: Summary of common Elasticsearch interview questions (paid) +category: database +tag: + - NoSQL + - Elasticsearch +head: + - - meta + - name: keywords + content: Elasticsearch interviews, indexing, sharding, inversion, query, aggregation, tuning + - - meta + - name: description + content: Contains Elasticsearch high-frequency interview questions and practical points, focusing on indexing/sharding/inversion and aggregation queries to form a system review list. +--- + +**Elasticsearch** The related interview questions are exclusive to my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link to view the detailed introduction and how to join), which has been compiled into ["Java Interview Guide North"](../../zhuanlan/java-mian-shi-zhi-bei.md). + +![](https://oss.javaguide.cn/javamianshizhibei/elasticsearch-questions.png) + + + + \ No newline at end of file diff --git a/docs_en/database/mongodb/mongodb-questions-01.en.md b/docs_en/database/mongodb/mongodb-questions-01.en.md new file mode 100644 index 00000000000..dfed4bd9e29 --- /dev/null +++ b/docs_en/database/mongodb/mongodb-questions-01.en.md @@ -0,0 +1,350 @@ +--- +title: MongoDB常见面试题总结(上) +category: 数据库 +tag: + - NoSQL + - MongoDB +head: + - - meta + - name: keywords + content: MongoDB 面试,文档存储,无模式,副本集,分片,索引,一致性 + - - meta + - name: description + content: 汇总 MongoDB 基础与架构高频题,涵盖文档模型、索引、副本集与分片,强调高可用与一致性实践。 +--- + +> 少部分内容参考了 MongoDB 官方文档的描述,在此说明一下。 + +## MongoDB 基础 + +### MongoDB 是什么? + +MongoDB 是一个基于 **分布式文件存储** 的开源 NoSQL 数据库系统,由 **C++** 编写的。MongoDB 提供了 **面向文档** 的存储方式,操作起来比较简单和容易,支持“**无模式**”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 **文档类型数据库** 。 + +在高负载的情况下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。在许多场景下,MongoDB 可以用于代替传统的关系型数据库或键/值存储方式,皆在为 Web 应用提供可扩展的高可用高性能数据存储解决方案。 + +### MongoDB 的存储结构是什么? + +MongoDB 的存储结构区别于传统的关系型数据库,主要由如下三个单元组成: + +- **文档(Document)**:MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。 +- **集合(Collection)**:一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。 +- **数据库(Database)**:一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。 + +也就是说,MongoDB 将数据记录存储为文档 (更具体来说是[BSON 文档](https://www.mongodb.com/docs/manual/core/document/#std-label-bson-document-format)),这些文档在集合中聚集在一起,数据库中存储一个或多个文档集合。 + +**SQL 与 MongoDB 常见术语对比**: + +| SQL | MongoDB | +| ------------------------ | ------------------------------- | +| 表(Table) | 集合(Collection) | +| 行(Row) | 文档(Document) | +| 列(Col) | 字段(Field) | +| 主键(Primary Key) | 对象 ID(Objectid) | +| 索引(Index) | 索引(Index) | +| 嵌套表(Embedded Table) | 嵌入式文档(Embedded Document) | +| 数组(Array) | 数组(Array) | + +#### 文档 + +MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。字段的值可能包括其他文档、数组和文档数组。 + +![MongoDB 文档](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-document..png) + +文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。 + +- 键不能含有 `\0`(空字符)。这个字符用来表示键的结尾。 +- `.` 和 `$` 有特别的意义,只有在特定环境下才能使用。 +- 以下划线`_`开头的键是保留的(不是严格要求的)。 + +**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/)。 + +根据维基百科对 BJSON 的介绍,BJSON 的遍历速度优于 JSON,这也是 MongoDB 选择 BSON 的主要原因,但 BJSON 需要更多的存储空间。 + +> 与 JSON 相比,BSON 着眼于提高存储和扫描效率。BSON 文档中的大型元素以长度字段为前缀以便于扫描。在某些情况下,由于长度前缀和显式数组索引的存在,BSON 使用的空间会多于 JSON。 + +![BSON 官网首页](https://oss.javaguide.cn/github/javaguide/database/mongodb/bsonspec.org.png) + +#### 集合 + +MongoDB 集合存在于数据库中,**没有固定的结构**,也就是 **无模式** 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况下,插入集合中的数据都会有一定的关联性。 + +![MongoDB 集合](https://oss.javaguide.cn/github/javaguide/database/mongodb/crud-annotated-collection.png) + +集合不需要事先创建,当第一个文档插入或者第一个索引创建时,如果该集合不存在,则会创建一个新的集合。 + +集合名可以是满足下列条件的任意 UTF-8 字符串: + +- 集合名不能是空字符串`""`。 +- 集合名不能含有 `\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 为代价最大限度地减少存储使用。 + +By default, WiredTiger uses the [Snappy](https://github.com/google/snappy) compression algorithm (open sourced by Google, designed to achieve very high speed and reasonable compression, with a compression ratio of 3 to 5 times) using block compression for all collections and prefix compression for all indexes. + +In addition to Snappy, there are the following compression algorithms for collections: + +- [zlib](https://github.com/madler/zlib): Highly compressed algorithm, compression ratio 5 to 7 times +- [Zstandard](https://github.com/facebook/zstd) (zstd for short): A fast lossless compression algorithm open sourced by Facebook. It provides higher compression rate and lower CPU usage for zlib-level real-time compression scenarios and better compression ratio. It is available in MongoDB 4.2. + +WiredTiger logs are also compressed, using the Snappy compression algorithm by default. If a log record is less than or equal to 128 bytes, WiredTiger does not compress the record. + +## Differences between Amazon Document and MongoDB + +Amazon DocumentDB (compatible with MongoDB) is a fast, reliable, fully managed database service. Amazon DocumentDB makes it easy to set up, operate, and scale MongoDB-compatible databases in the cloud. + +### `$vectorSearch` operator + +Amazon DocumentDB does not support `$vectorSearch` as a standalone operator. Instead, we support `vectorSearch` internally in the `$search` operator. For more information, see Vector Search Amazon DocumentDB(https://docs.aws.amazon.com/zh_cn/documentdb/latest/developerguide/vector-search.html). + +### `OpCountersCommand` + +The behavior of Amazon DocumentDB's `OpCountersCommand` deviates from MongoDB's `opcounters.command` as follows: + +- MongoDB's `opcounters.command` counts all commands except inserts, updates, and deletes, while Amazon DocumentDB's `OpCountersCommand` also excludes `find` commands. +- Amazon DocumentDB counts internal commands (such as `getCloudWatchMetricsV2`) against `OpCountersCommand`. + +### Manage databases and collections + +Amazon DocumentDB does not support managed or local databases, nor the MongoDB `system.*` or `startup_log` collections. + +### `cursormaxTimeMS` + +In Amazon DocumentDB, `cursor.maxTimeMS` resets the counter for each request. `getMore` So if `maxTimeMS` of 3000MS is specified and the query takes 2800MS and each subsequent `getMore` request takes 300MS, the cursor will not time out. The cursor will timeout `maxTimeMS` only if a single operation (either a query or a single `getMore` request) takes longer than the specified value. In addition, the scanner that checks cursor execution time runs at a five (5) minute interval size. + +### explain() + +Amazon DocumentDB emulates the MongoDB 4.0 API on a purpose-built database engine that leverages a distributed, fault-tolerant, self-healing storage system. Therefore, query plans and the output of explain() may differ between Amazon DocumentDB and MongoDB. Customers wishing to control their query plans can use the `$hint` operator to force a preferred index. + +### Field name restrictions + +Amazon DocumentDB does not support dot "." for example, `db.foo.insert({‘x.1':1})` in document field names. + +Amazon DocumentDB also does not support the $ prefix in field names. + +For example, try the following commands in Amazon DocumentDB or MongoDB: + +```shell +rs0:PRIMARY< db.foo.insert({"a":{"$a":1}}) +``` + +MongoDB will return the following: + +```shell +WriteResult({ "nInserted" : 1 }) +``` + +Amazon DocumentDB will return an error: + +```shell +WriteResult({ + "nInserted" : 0, + "writeError" : { + "code": 2, + "errmsg" : "Document can't have $ prefix field names: $a" + } +}) +``` + +## Reference + +- MongoDB official documents (main reference materials, subject to official documents): +- "The Definitive Guide to MongoDB" +- Technical information | MongoDB transaction principles - MongoDB Chinese community: +- Transactions - MongoDB official documentation: +- WiredTiger Storage Engine - MongoDB official documentation: +- One of WiredTiger storage engines: basic data structure analysis: + + \ No newline at end of file diff --git a/docs_en/database/mongodb/mongodb-questions-02.en.md b/docs_en/database/mongodb/mongodb-questions-02.en.md new file mode 100644 index 00000000000..cecb3ae48df --- /dev/null +++ b/docs_en/database/mongodb/mongodb-questions-02.en.md @@ -0,0 +1,282 @@ +--- +title: MongoDB常见面试题总结(下) +category: 数据库 +tag: + - NoSQL + - MongoDB +head: + - - meta + - name: keywords + content: MongoDB 索引,复合索引,多键索引,文本索引,地理索引,查询优化 + - - meta + - name: description + content: 讲解 MongoDB 常见索引类型与适用场景,结合查询优化与写入开销权衡,提升检索性能与稳定性。 +--- + +## MongoDB 索引 + +### MongoDB 索引有什么用? + +和关系型数据库类似,MongoDB 中也有索引。索引的目的主要是用来提高查询效率,如果没有索引的话,MongoDB 必须执行 **集合扫描** ,即扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在合适的索引,MongoDB 可以使用该索引来限制它必须检查的文档数量。并且,MongoDB 可以使用索引中的排序返回排序后的结果。 + +虽然索引可以显著缩短查询时间,但是使用索引、维护索引是有代价的。在执行写入操作时,除了要更新文档之外,还必须更新索引,这必然会影响写入的性能。因此,当有大量写操作而读操作少时,或者不考虑读操作的性能时,都不推荐建立索引。 + +### MongoDB 支持哪些类型的索引? + +**MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。** + +- **单字段索引:** 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。 +- **复合索引:** 建立在多个字段上的索引,也可以称之为组合索引、联合索引。 +- **多键索引**:MongoDB 的一个字段可能是数组,在对这种字段创建索引时,就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。 +- **哈希索引**:按数据的哈希值索引,用在哈希分片集群上。 +- **文本索引:** 支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。 +- **地理位置索引:** 基于经纬度的索引,适合 2D 和 3D 的位置查询。 +- **唯一索引**:确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。 +- **TTL 索引**:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。 +- …… + +### 复合索引中字段的顺序有影响吗? + +复合索引中字段的顺序非常重要,例如下图中的复合索引由`{userid:1, score:-1}`组成,则该复合索引首先按照`userid`升序排序;然后再每个`userid`的值内,再按照`score`降序排序。 + +![复合索引](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongodb-composite-index.png) + +在复合索引中,按照何种方式排序,决定了该索引在查询中是否能被应用到。 + +走复合索引的排序: + +```sql +db.s2.find().sort({"userid": 1, "score": -1}) +db.s2.find().sort({"userid": -1, "score": 1}) +``` + +不走复合索引的排序: + +```sql +db.s2.find().sort({"userid": 1, "score": 1}) +db.s2.find().sort({"userid": -1, "score": -1}) +db.s2.find().sort({"score": 1, "userid": -1}) +db.s2.find().sort({"score": 1, "userid": 1}) +db.s2.find().sort({"score": -1, "userid": -1}) +db.s2.find().sort({"score": -1, "userid": 1}) +``` + +我们可以通过 explain 进行分析: + +```sql +db.s2.find().sort({"score": -1, "userid": 1}).explain() +``` + +### 复合索引遵循左前缀原则吗? + +**MongoDB 的复合索引遵循左前缀原则**:拥有多个键的索引,可以同时得到所有这些键的前缀组成的索引,但不包括除左前缀之外的其他子集。比如说,有一个类似 `{a: 1, b: 1, c: 1, ..., z: 1}` 这样的索引,那么实际上也等于有了 `{a: 1}`、`{a: 1, b: 1}`、`{a: 1, b: 1, c: 1}` 等一系列索引,但是不会有 `{b: 1}` 这样的非左前缀的索引。 + +### 什么是 TTL 索引? + +TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间 `expireAfterSeconds` ,当一个文档达到预设的过期时间之后就会被删除。TTL 索引除了有 `expireAfterSeconds` 属性外,和普通索引一样。 + +数据过期对于某些类型的信息很有用,比如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保存有限的时间。 + +**TTL 索引运行原理**: + +- MongoDB 会开启一个后台线程读取该 TTL 索引的值来判断文档是否过期,但不会保证已过期的数据会立马被删除,因后台线程每 60 秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间 60 秒以上的现象。 +- 对于副本集而言,TTL 索引的后台进程只会在 Primary 节点开启,在从节点会始终处于空闲状态,从节点的数据删除是由主库删除后产生的 oplog 来做同步。 + +**TTL 索引限制**: + +- TTL 索引是单字段索引。复合索引不支持 TTL +- `_id`字段不支持 TTL 索引。 +- 无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。 +- 如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。 + +### 什么是覆盖索引查询? + +根据官方文档介绍,覆盖查询是以下的查询: + +- 所有的查询字段是索引的一部分。 +- 结果中返回的所有字段都在同一索引中。 +- 查询中没有字段等于`null`。 + +由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于内存中,从索引中获取数据比通过扫描文档读取数据要快得多。 + +举个例子:我们有如下 `users` 集合: + +```json +{ + "_id": ObjectId("53402597d852426020000002"), + "contact": "987654321", + "dob": "01-01-1991", + "gender": "M", + "name": "Tom Benzamin", + "user_name": "tombenzamin" +} +``` + +我们在 `users` 集合中创建联合索引,字段为 `gender` 和 `user_name` : + +```sql +db.users.ensureIndex({gender:1,user_name:1}) +``` + +现在,该索引会覆盖以下查询: + +```sql +db.users.find({gender:"M"},{user_name:1,_id:0}) +``` + +为了让指定的索引覆盖查询,必须显式地指定 `_id: 0` 来从结果中排除 `_id` 字段,因为索引不包括 `_id` 字段。 + +## MongoDB 高可用 + +### 复制集群 + +#### 什么是复制集群? + +MongoDB 的复制集群又称为副本集群,是一组维护相同数据集合的 mongod 进程。 + +客户端连接到整个 Mongodb 复制集群,主节点机负责整个复制集群的写,从节点可以进行读操作,但默认还是主节点负责整个复制集群的读。主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。 + +通常来说,一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。 + +- **主节点**:整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。 +- **从节点**:从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。 +- **仲裁节点**:这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。 + +下图是一个典型的三成员副本集群: + +![](https://oss.javaguide.cn/github/javaguide/database/mongodb/replica-set-read-write-operations-primary.png) + +主节点与备节点之间是通过 **oplog(操作日志)** 来同步数据的。oplog 是 local 库下的一个特殊的 **上限集合(Capped Collection)** ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。 + +> 上限集合类似于定长的循环队列,数据顺序追加到集合的尾部,当集合空间达到上限时,它会覆盖集合中最旧的文档。上限集合的数据将会被顺序写入到磁盘的固定空间内,所以,I/O 速度非常快,如果不建立索引,性能更好。 + +![](https://oss.javaguide.cn/github/javaguide/database/mongodb/replica-set-primary-with-two-secondaries.png) + +当主节点上的一个写操作完成后,会向 oplog 集合写入一条对应的日志,而从节点则通过这个 oplog 不断拉取到新的日志,在本地进行回放以达到数据同步的目的。 + +副本集最多有一个主节点。 如果当前主节点不可用,一个选举会抉择出新的主节点。MongoDB 的节点选举规则能够保证在 Primary 挂掉之后选取的新节点一定是集群中数据最全的一个。 + +#### 为什么要用复制集群? + +- **实现 failover**:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。 +- **实现读写分离**:我们可以设置从节点上可以读取数据,主节点负责写入数据,这样的话就实现了读写分离,减轻了主节点读写压力过大的问题。MongoDB 4.0 之前版本如果主库压力不大,不建议读写分离,因为写会阻塞读,除非业务对响应时间不是非常关注以及读取历史数据接受一定时间延迟。 + +### 分片集群 + +#### 什么是分片集群? + +分片集群是 MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中, 不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。 + +MongoDB 的分片集群由如下三个部分组成(下图来源于[官方文档对分片集群的介绍](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)架构 + +#### 为什么要用分片集群? + +随着系统数据量以及吞吐量的增长,常见的解决办法有两种:垂直扩展和水平扩展。 + +垂直扩展通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等;水平扩展则通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。 + +类似于 Redis Cluster,MongoDB 也可以通过分片实现 **水平扩展** 。水平扩展这种方式更灵活,可以满足更大数据量的存储需求,支持更高吞吐量。并且,水平扩展所需的整体成本更低,仅仅需要相对较低配置的单机服务器即可,代价是增加了部署的基础设施和维护的复杂性。 + +也就是说当你遇到如下问题时,可以使用分片集群解决: + +- 存储容量受单机限制,即磁盘资源遭遇瓶颈。 +- 读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。 + +#### 什么是分片键? + +**分片键(Shard Key)** 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。也就是说,分片键决定了集合内的文档如何在集群的多个分片间的分布状况。 + +分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求: + +- 它必须在所有文档中都出现。 +- 它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。 +- MongoDB 4.2 之前的版本,文档的分片键字段值不可变。MongoDB 4.2 版本开始,除非分片键字段是不可变的 `_id` 字段,否则您可以更新文档的分片键值。MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。 +- 它的大小不能超过 512 字节。 + +#### 如何选择分片键? + +选择合适的片键对 sharding 效率影响很大,主要基于如下四个因素(摘自[分片集群使用注意事项 - - 腾讯云文档](https://cloud.tencent.com/document/product/240/44611)): + +- **取值基数** 取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。 例如:选择年龄做一个基数,范围最多只有 100 个,随着数据量增多,同一个值分布过多时,导致 chunck 的增长超出 chuncksize 的范围,引起 jumbo chunk,从而无法迁移,导致数据分布不均匀,性能瓶颈。 +- **取值分布** 取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。 +- **查询带分片** 查询时建议带上分片,使用分片键进行条件查询时,mongos 可以直接定位到具体分片,否则 mongos 需要将查询分发到所有分片,再等待响应返回。 +- **避免单调递增或递减** 单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。 + +综上,在选择片键时要考虑以上 4 个条件,尽可能满足更多的条件,才能降低 MoveChunks 对性能的影响,从而获得最优的性能体验。 + +#### 分片策略有哪些? + +MongoDB 支持两种分片算法来满足不同的查询需求(摘自[MongoDB 分片集群介绍 - 阿里云文档](https://help.aliyun.com/document_detail/64561.html?spm=a2c4g.11186623.0.0.3121565eQhUGGB#h2--shard-key-3)): + +**1、基于范围的分片**: + +![](https://oss.javaguide.cn/github/javaguide/database/mongodb/example-of-scope-based-sharding.png) + +MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时,范围分片更高效。 + +- 优点:Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。 +- 缺点:可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。 +- 适用场景:分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。 + +**2、基于 Hash 值的分片** + +![](https://oss.javaguide.cn/github/javaguide/database/mongodb/example-of-hash-based-sharding.png) + +MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。 + +- 优点:可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。 +- 缺点:不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。 +- 适用场景:分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。 + +除了上述两种分片策略,您还可以配置 **复合片键** ,例如由一个低基数的键和一个单调递增的键组成。 + +#### 分片数据如何存储? + +**Chunk(块)** 是 MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中**划分**的概念。 + +分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。 + +By default, the maximum size of a Chunk is 64MB (adjustable, the value range is 1~1024 MB. If there are no special requirements, it is recommended to keep the default value). When inserting, updating, or deleting data, if Mongos senses the size of the target Chunk or the amount of data in it exceeds the upper limit, **Chunk split** will be triggered. + +![Chunk splitting](https://oss.javaguide.cn/github/javaguide/database/mongodb/chunk-splitting-shard-a.png) + +The growth of data will cause more and more chunks to be split. At this time, the number of Chunks on each shard may be unbalanced. The **Balancer** component in Mongos will perform automatic balancing and try to balance the number of Chunks on each Shard. This process is **Rebalance**. By default, Rebalance for databases and collections is turned on. + +As shown in the figure below, as data is inserted, the chunks are split, so that the two shards AB have three chunks and the C shard has only one. At this time, one allocated by B will be migrated to the C shard to achieve cluster data balance. + +![Chunk Migration](https://oss.javaguide.cn/github/javaguide/database/mongodb/mongo-reblance-three-shards.png) + +> Balancer is a background process of MongoDB that runs on the Primary node of Config Server (since MongoDB version 3.4). It monitors the number of Chunks on each shard and migrates them when the number of Chunks on a certain shard reaches a threshold. + +Chunks will only be split, not merged, even if the value of chunkSize becomes larger. + +The Rebalance operation consumes system resources. We can reduce its impact on the normal use of MongoDB by executing it during low business peak periods, pre-sharding, or setting the Rebalance time window. + +#### What is the principle of Chunk migration? + +For a detailed introduction to the principles of chunk migration, it is recommended to read the article [Understanding MongoDB chunk migration in one article] (https://mongoing.com/archives/77479) from the MongoDB Chinese community. + +## Recommended learning materials + +- [MongoDB Chinese Manual|Official Document Chinese Version](https://docs.mongoing.com/) (recommended): Based on version 4.2, it is constantly synchronized with the latest official version. +- [MongoDB Beginner Tutorial - 7 Days to Learn MongoDB](https://mongoing.com/archives/docs/mongodb%e5%88%9d%e5%ad%a6%e8%80%85%e6%95%99%e7%a8%8b/mon godb%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): Quick start. +- [SpringBoot integrates MongoDB in practice - 2022](https://www.cnblogs.com/dxflqm/p/16643981.html): A very good introductory article to MongoDB, which mainly focuses on the use of MongoDB's Java client to introduce basic addition, deletion, modification and query operations. + +## Reference + +- MongoDB official documents (main reference materials, subject to official documents): +- "The Definitive Guide to MongoDB" +- Indexes - MongoDB official documentation: +- MongoDB - Index Knowledge - Programmer Xiang Zai - 2022: +- MongoDB - Index: +- Sharding - MongoDB official documentation: +- Introduction to MongoDB sharded cluster - Alibaba Cloud Document: +- Precautions for using sharded clusters - - Tencent Cloud Document: + + \ No newline at end of file diff --git a/docs_en/database/mysql/a-thousand-lines-of-mysql-study-notes.en.md b/docs_en/database/mysql/a-thousand-lines-of-mysql-study-notes.en.md new file mode 100644 index 00000000000..44b18758932 --- /dev/null +++ b/docs_en/database/mysql/a-thousand-lines-of-mysql-study-notes.en.md @@ -0,0 +1,962 @@ +--- +title: One Thousand Lines of MySQL Study Notes +category: database +tag: + -MySQL +head: + - - meta + - name: keywords + content: MySQL notes, tuning, indexing, transactions, tools, experience summary, practice + - - meta + - name: description + content: Organizes thousands of lines of notes on MySQL learning and practice, condensing tuning ideas, indexing and transaction key points, and tool usage for quick reference and review. +--- + +> Original address: , JavaGuide has simplified the layout of this article and added a new table of contents. + +A very good summary, I strongly recommend saving it and reading it when needed. + +### Basic operations + +```sql +/* Windows service */ +-- Start MySQL + net start mysql +-- Create a 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 +-- Display system variable information + SHOW VARIABLES +``` + +### Database operations + +```sql +/* Database operations */ +-- View the current database + SELECT DATABASE(); +-- Display the current time, user name, and database version + SELECT now(), user(), version(); +--Create library + CREATE DATABASE[IF NOT EXISTS] database name database options + Database options: + CHARACTER SET charset_name + COLLATE collation_name +-- View existing libraries + SHOW DATABASES[ LIKE 'PATTERN'] +-- View current library information + SHOW CREATE DATABASE database name +-- Modify library option information + ALTER DATABASE library name option information +-- Delete library + DROP DATABASE[IF EXISTS] database name + Delete the directory related to the database and its directory contents at the same time +``` + +### Table operations + +```sql +/* Table operations */ +--Create table + CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [Library name.] Table name (structure definition of the table) [Table options] + Each field must have a data type + There cannot be a comma after the last field + TEMPORARY temporary table, the table automatically disappears when the session ends + For field definition: + 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 + Tables use different data structures when managing data. Different structures will lead to different processing methods and provided feature operations. + Common engines: InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive + Different engines use different ways to save the structure and data of tables + MyISAM table file meaning: .frm table definition, .MYD table data, .MYI table index + InnoDB table file meaning: .frm table definition, table space data and log files + SHOW ENGINES -- displays storage engine status information + SHOW ENGINE engine name {LOGS|STATUS} -- displays the log or status information of the storage engine + --increment starting number + AUTO_INCREMENT = number of rows + --Data file directory + DATA DIRECTORY = 'Directory' + -- Index file directory + INDEX DIRECTORY = 'Directory' + -- table comments + COMMENT = 'string' + --Partition options + PARTITION BY ... (see manual for details) +-- View all tables + SHOW TABLES[ LIKE 'pattern'] + SHOW TABLES FROM library 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'] +--Modify table + --Modify options for the table itself + ALTER TABLE table name table options + eg: ALTER TABLE table name ENGINE=MYISAM; + -- Rename the table + RENAME TABLE original table name TO new table name + RENAME TABLE original table name TO database name.table name (the table can be moved to another database) + -- RENAME can exchange two table names + -- Modify the field structure of the table (13.1.2. ALTER TABLE syntax) + ALTER TABLE table name operation name + -- operation name + ADD[COLUMN] field definition -- add field + AFTER field name -- means adding after the field name + FIRST -- means adding in the first + ADD PRIMARY KEY (field name) -- Create a primary key + ADD UNIQUE [index name] (field name)--Create a unique index + ADD INDEX [index name] (field name) -- Create a normal index + DROP[COLUMN] field name -- delete field + MODIFY[COLUMN] Field name Field attribute -- Supports modification of field attributes, field name cannot be modified (all original attributes must also be written) + CHANGE[COLUMN] Original field name New field name Field attributes -- Supports modification of field names + DROP PRIMARY KEY -- Delete the primary key (you need to delete its AUTO_INCREMENT attribute before deleting the primary key) + DROP INDEX index name -- delete index + DROP FOREIGN KEY foreign key -- delete foreign key +-- Delete table + DROP TABLE[IF EXISTS] table name ... +--Clear table data + TRUNCATE [TABLE] table name +--Copy table structure + CREATE TABLE table name LIKE table name to copy +--Copy table structure and data + CREATE TABLE table name [AS] SELECT * FROM table name to be copied +-- Check the table for errors + CHECK TABLE tbl_name [, tbl_name] ... [option] ... +-- Optimize table + OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... +-- Repair table + REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... [QUICK] [EXTENDED] [USE_FRM] +-- analysis table + ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... +``` + +### Data operations + +```sql +/* Data operations */ ------------------ +-- increase + INSERT [INTO] table name [(field list)] VALUES (value list)[, (value list), ...] + -- The field list can be omitted if the list of values to be inserted contains all fields and is in consistent order. + -- Multiple data records can be inserted at the same time! + REPLACE is similar to INSERT, the only difference is that for matching rows, the data of the existing row (compared with the primary key/unique key) is replaced, and if there is no existing row, a new row is inserted. + INSERT [INTO] table name SET field name=value[, field name=value, ...] +-- Check + SELECT field list FROM table name [other clauses] + -- Multiple fields that can come from multiple tables + --Other clauses may not be used + -- The field list can be replaced by * to indicate all fields +-- Delete + DELETE FROM table name [delete condition clause] + Without a conditional clause, all will be deleted +-- change + UPDATE table name SET field name=new value[, field name=new value] [update condition]``` + +### 字符集编码 + +```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 Programming + +```sql +/* SQL programming */ ------------------ +--//Local variables ---------- +-- variable declaration + declare var_name[,...] type [default value] + This statement is used to declare local variables. To provide a default value for a variable, include a default clause. The value can be specified as an expression and does not need to be a constant. If there is no default clause, the initial value is null. +-- assignment + Use set and select into statements to assign values to variables. + - Note: Global variables (user-defined variables) can be used within functions +--//Global variables ---------- +-- Definition, assignment +The set statement can define and assign values to variables. +set @var = value; +You can also use the select into statement to initialize and assign values to variables. This requires that the select statement can only return one row, but it can be multiple fields, which means that multiple variables are assigned values ​​at the same time. The number of variables needs to be consistent with the number of columns in the query. +You can also think of the assignment statement as an expression, which is executed through select. At this time, in order to avoid = being treated as a relational operator, use := instead. (The set statement can use = and :=). +select @var:=20; +select @v1:=id, @v2=name from t1 limit 1; +select * from tbl_name where @var:=30; +Select into can assign the data obtained from the query in the table to a variable. + -| select max(height) into @max_height from tb; +-- Custom variable name +In order to avoid conflicts between user-defined variables and system identifiers (usually field names) in select statements, user-defined variables use @ as the starting symbol before the variable name. +@var=10; + - Once a variable is defined, it is valid throughout the session (login to logout) +--//Control structure ---------- +--if statement +if search_condition then + statement_list +[elseif search_condition then + statement_list] +... +[else + statement_list] +end if; +--case statement +CASE value WHEN [compare-value] THEN result +[WHEN [compare-value] THEN result ...] +[ELSE result] +END +-- while loop +[begin_label:] while search_condition do + statement_list +end while [end_label]; +- If you need to terminate the while loop early within the loop, you need to use labels; labels need to appear in pairs. + --Exit the loop + Exit the entire loop leave + Exit the current loop iterate + Determine which loop to exit through the exit label +--//Built-in function ---------- +-- Numerical functions +abs(x) -- absolute value abs(-10.9) = 10 +format(x, d) -- format the thousandth value format(1234567.456, 2) = 1,234,567.46 +ceil(x) -- round up ceil(10.1) = 11 +floor(x) -- round down floor (10.1) = 10 +round(x) -- round to integer +mod(m, n) -- m%n m mod n Find remainder 10%3=1 +pi() -- get pi +pow(m, n) -- m^n +sqrt(x) -- arithmetic square root +rand() -- random number +truncate(x, d) -- truncate to d decimal places +-- Time and date function +now(), current_timestamp(); -- current date and time +current_date(); -- current date +current_time(); -- current time +date('yyyy-mm-dd hh:ii:ss'); -- Get the date part +time('yyyy-mm-dd hh:ii:ss'); -- Get the time part +date_format('yyyy-mm-dd hh:ii:ss', '%d %y %a %d %m %b %j'); -- formatting time +unix_timestamp(); -- Get unix timestamp +from_unixtime(); -- Get time from timestamp +-- String functions +length(string) – string length, bytes +char_length(string) – the number of characters in string +substring(str, position [,length]) -- Starting from the position of str, take length characters +replace(str,search_str,replace_str) -- replace search_str with replace_str in str +instr(string,substring) -- returns the position where substring first appears in string +concat(string [,...]) -- Concatenate strings +charset(str) -- Returns the string character set +lcase(string) -- Convert to lowercase +left(string, length) -- Take length characters from the left in string2 +load_file(file_name) -- read content from file +locate(substring, string [,start_position]) -- same as instr, but can specify the starting position +lpad(string, length, pad) -- Repeat adding pad to the beginning of string until the length of the string is length +ltrim(string) -- remove leading spaces +repeat(string, count) -- repeat count times +rpad(string, length, pad) --Add pad after str until the length is length +rtrim(string) -- remove trailing spaces +strcmp(string1,string2) -- compare the size of two strings character by character +-- Process function +case when [condition] then result [when [condition] then result ...] [else result] end multi-branch +if(expr1,expr2,expr3) double branch. +-- Aggregation function +count() +sum(); +max(); +min(); +avg(); +group_concat() +--Other commonly used functions +md5(); +default(); +--//Storage function, custom function ---------- +-- New + CREATE FUNCTION function_name (parameter list) RETURNS return value type + function body + - The function name should be a legal identifier and should not conflict with existing keywords. + - A function should belong to a certain database. You can use the form of db_name.function_name to execute the database to which the current function belongs, otherwise it is the current database. + - The parameter part consists of "parameter name" and "parameter type". Multiple parameters are separated by commas. + - The function body consists of multiple available mysql statements, process control, variable declaration and other statements. + - Multiple statements should be enclosed using begin...end statement blocks. + - There must be a return return value statement. +-- Delete + DROP FUNCTION [IF EXISTS] function_name; +-- View + SHOW FUNCTION STATUS LIKE 'partten' + SHOW CREATE FUNCTION function_name; +-- Modify + ALTER FUNCTION function_name function option +--// Stored procedure, custom function ---------- +-- Definition +Stored stored procedure is a piece of code (procedure) consisting of SQL stored in the database. +A stored procedure is usually used to complete a piece of business logic, such as registration, shift payment, order warehousing, etc. +A function usually focuses on a certain function and is regarded as a service for other programs. It needs to call the function in other statements. However, a stored procedure cannot be called by other people and is executed by itself through call. +-- create +CREATE PROCEDURE sp_name (parameter list) + process body +Parameter list: Different from the parameter list of a function, the parameter type needs to be specified +IN, indicating input type +OUT, indicating output type +INOUT, indicating mixed type +Note that there is no return value. +``` + +### Stored procedure + +```sql +/* Stored procedure */ ------------------ +A stored procedure is a collection of executable code. It prefers business logic to functions. +Call: CALL procedure name +-- Note +- No return value. +- Can only be called alone and cannot be mixed with other statements +-- Parameters +IN|OUT|INOUT parameter name data type +IN input: During the calling process, data is input into the parameters inside the procedure body. +OUT output: During the calling process, the result of processing the process body is returned to the client. +INOUT input and output: both input and output +-- Grammar +CREATE PROCEDURE procedure name (parameter list) +BEGIN + process body +END``` + +### User and permission management + +```sql +/* User and permission management */ ------------------ +-- root password reset +1. Stop the MySQL service +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("password") WHERE `user` = "root"; +5. FLUSH PRIVILEGES; +User information table: mysql.user +-- Refresh permissions +FLUSH PRIVILEGES; +-- Add user +CREATE USER username IDENTIFIED BY [PASSWORD] password (string) + - Must have the global CREATE USER permission of the mysql database, or have the INSERT permission. + - Users can only be created, but permissions cannot be granted. + - User name, pay attention to the quotation marks: such as 'user_name'@'192.168.1.1' + - Passwords also need quotation marks, and pure numeric passwords also need quotation marks. + - To specify a password in plain text, ignore the PASSWORD keyword. To specify a password as the hashed value returned by the PASSWORD() function, include the keyword PASSWORD +-- Rename user +RENAME USER old_user TO new_user +-- Set password +SET PASSWORD = PASSWORD('password') -- Set a password for the current user +SET PASSWORD FOR username = PASSWORD('password') -- Set a password for the specified user +-- Delete user +DROP USER username +-- Assign permissions/Add users +GRANT permission list ON table name TO user name [IDENTIFIED BY [PASSWORD] 'password'] + - all privileges means all permissions + - *.* represents all tables in all libraries + - Library name.Table name indicates a table under a certain library + GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817'; +-- View permissions +SHOW GRANTS FOR username + -- View current user permissions + SHOW GRANTS; or SHOW GRANTS FOR CURRENT_USER; or SHOW GRANTS FOR CURRENT_USER(); +-- revoke permission +REVOKE permission list ON table name FROM user name +REVOKE ALL PRIVILEGES, GRANT OPTION FROM username -- revoke all privileges +--Permission level +-- To use GRANT or REVOKE, you must have the GRANT OPTION permission, and you must be using the permission you are granting or revoking. +Global level: Global permissions apply to all databases in a given server, mysql.user + GRANT ALL ON *.* and REVOKE ALL ON *.* only grant and revoke global permissions. +Database level: Database permissions apply to all targets in a given database, mysql.db, mysql.host + GRANT ALL ON db_name.* and REVOKE ALL ON db_name.* only grant and revoke database permissions. +Table level: Table permissions apply to all columns in a given table, mysql.talbes_priv + GRANT ALL ON db_name.tbl_name and REVOKE ALL ON db_name.tbl_name only grant and revoke table permissions. +Column level: Column permissions apply to a single column in a given table, mysql.columns_priv + When using REVOKE, you must specify the same columns as the authorized columns. +-- Permission list +ALL [PRIVILEGES] -- Set all simple permissions except GRANT OPTION +ALTER -- enable ALTER TABLE +ALTER ROUTINE -- alter or cancel a stored subroutine +CREATE -- enables the use of CREATE TABLE +CREATE ROUTINE -- Create a stored subroutine +CREATE TEMPORARY TABLES -- enables the use of CREATE TEMPORARY TABLE +CREATE USER -- Allows CREATE USER, DROP USER, RENAME USER and REVOKE ALL PRIVILEGES. +CREATE VIEW -- enables the use of CREATE VIEW +DELETE -- allows DELETE to be used +DROP -- enables the use of DROP TABLE +EXECUTE -- allows the user to run a stored subroutine +FILE -- allows SELECT...INTO OUTFILE and LOAD DATA INFILE +INDEX -- allows CREATE INDEX and DROP INDEX +INSERT -- Allow the use of INSERT +LOCK TABLES -- Allows LOCK TABLES on tables for which you have SELECT permissions +PROCESS -- Allows use of SHOW FULL PROCESSLIST +REFERENCES -- not implemented +RELOAD -- Allow the use of FLUSH +REPLICATION CLIENT -- allows the user to ask for the address of a slave or master server +REPLICATION SLAVE -- for replication slave servers (read binary log events from the master server) +SELECT -- Allow the use of SELECT +SHOW DATABASES -- show all databases +SHOW VIEW -- enables the use of SHOW CREATE VIEW +SHUTDOWN -- Allow mysqladmin shutdown +SUPER -- Allows the use of CHANGE MASTER, KILL, PURGE MASTER LOGS and SET GLOBAL statements, the mysqladmin debug command; allows you to connect (once) even if max_connections have been reached. +UPDATE -- Allow UPDATE +USAGE -- synonym for "no permissions" +GRANT OPTION -- Allow permission to be granted +``` + +### Table maintenance + +```sql +/* Table maintenance */ +--Analyze and store keyword distribution of tables +ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE table name ... +-- Check one or more tables for errors +CHECK TABLE tbl_name [, tbl_name] ... [option] ... +option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED} +-- Defragment data files +OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... +``` + +### Miscellaneous + +```sql +/* Miscellaneous */ ------------------ +1. You can use backticks (`) to wrap identifiers (library names, table names, field names, indexes, aliases) to avoid duplication of keywords! Chinese can also be used as an identifier! +2. Each library directory contains an option file db.opt that saves the current database. +3. Notes: + Single line comment #Comment content + Multi-line comments /* Comment content */ + Single line comment -- comment content (standard SQL comment style, requiring a space after double dashes (space, TAB, newline, etc.)) +4. Pattern wildcard: + _ any single character + % any number of characters, even zero characters + Single quotes need to be escaped \' +5. The statement terminator in the CMD command line can be ";", "\G", "\g", which only affects the display results. Elsewhere, end with a semicolon. delimiter can modify the statement terminator of the current conversation. +6. SQL is not case sensitive +7. Clear existing statements: \c +``` + + \ No newline at end of file diff --git a/docs_en/database/mysql/how-sql-executed-in-mysql.en.md b/docs_en/database/mysql/how-sql-executed-in-mysql.en.md new file mode 100644 index 00000000000..d43f6e16a78 --- /dev/null +++ b/docs_en/database/mysql/how-sql-executed-in-mysql.en.md @@ -0,0 +1,144 @@ +--- +title: SQL语句在MySQL中的执行过程 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL 执行流程,解析器,优化器,执行器,缓冲池,日志,架构 + - - meta + - name: description + content: 拆解 SQL 在 MySQL 的执行路径,从解析优化到执行与缓存,结合存储引擎交互,构建完整的运行时视角。 +--- + +> 本文来自[木木匠](https://github.com/kinglaw1204)投稿。 + +本篇文章会分析下一个 SQL 语句在 MySQL 中的执行流程,包括 SQL 的查询在 MySQL 内部会怎么流转,SQL 语句的更新是怎么完成的。 + +在分析之前我会先带着你看看 MySQL 的基础架构,知道了 MySQL 由那些组件组成以及这些组件的作用是什么,可以帮助我们理解和解决这些问题。 + +## 一 MySQL 基础架构分析 + +### 1.1 MySQL 基本架构概览 + +下图是 MySQL 的一个简要架构图,从下图你可以很清晰的看到用户的 SQL 语句在 MySQL 内部是如何执行的。 + +先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。 + +- **连接器:** 身份认证和权限相关(登录 MySQL 的时候)。 +- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 +- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 +- **优化器:** 按照 MySQL 认为最优的方案去执行。 +- **执行器:** 执行语句,然后从存储引擎返回数据。 - + +![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) + +简单来说 MySQL 主要分为 Server 层和存储引擎层: + +- **Server 层**:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binlog 日志模块。 +- **存储引擎**:主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。**现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5 版本开始就被当做默认存储引擎了。** + +### 1.2 Server 层基本组件介绍 + +#### 1) 连接器 + +连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。 + +主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即使管理员修改了该用户的权限,该用户也是不受影响的。 + +#### 2) 查询缓存(MySQL 8.0 版本后移除) + +查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。 + +连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询语句,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 + +MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。 + +所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。 + +MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。 + +#### 3) 分析器 + +MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步: + +**第一步,词法分析**,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。 + +**第二步,语法分析**,主要就是判断你输入的 SQL 是否正确,是否符合 MySQL 的语法。 + +完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。 + +#### 4) 优化器 + +优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。 + +可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。 + +#### 5) 执行器 + +当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。 + +## 二 语句分析 + +### 2.1 查询语句 + +说了以上这么多,那么究竟一条 SQL 语句是如何执行的呢?其实我们的 SQL 可以分为两种,一种是查询,一种是更新(增加,修改,删除)。我们先分析下查询语句,语句如下: + +```sql +select * from tb_student A where A.age='18' and A.name=' 张三 '; +``` + +结合上面的说明,我们分析下这个语句的执行流程: + +- 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 +- 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 +- 接下来就是优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案:a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 + +- 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 + +### 2.2 更新语句 + +以上就是一条查询 SQL 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?SQL 语句如下: + +```plain +update tb_student A set A.age='19' where A.name=' 张三 '; +``` + +我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块是 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: + +- 先查询到张三这一条数据,不会走查询缓存,因为更新语句会导致与该表相关的查询缓存失效。 +- 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 +- 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。 +- 更新完成。 + +**这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?** + +这是因为最开始 MySQL 并没有 InnoDB 引擎(InnoDB 引擎是其他公司以插件形式插入 MySQL 的),MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。 + +It's not that it's impossible to use only one log module, but the InnoDB engine supports transactions through redo log. Then, some students will ask, can I use two log modules, but not so complicated? Why does redo log introduce prepare pre-commit state? Here we use proof by contradiction to explain why we do this? + +- **Write the redo log first and submit it directly, then write the binlog**. Suppose that after writing the redo log, the machine hangs up and the binlog log is not written. Then after the machine is restarted, the machine will restore the data through the redo log, but the binlog does not record the data at this time. When the machine is backed up later, this piece of data will be lost, and the master-slave synchronization will also lose this piece of data. +- **Write the binlog first, and then write the redo log**. Suppose that after writing the binlog, the machine restarts abnormally. Since there is no redo log, the machine cannot restore this record, but there is a record in the binlog. The same reason as above will cause data inconsistency. + +If the redo log two-stage submission method is used, it will be different. After writing the binlog, and then submitting the redo log will prevent the above problems and ensure the consistency of the data. So the question is, is there an extreme situation? Assume that the redo log is in the pre-commit state and the binlog has been written. What will happen if an abnormal restart occurs at this time? +This depends on the processing mechanism of MySQL. The processing process of MySQL is as follows: + +- Determine whether the redo log is complete. If it is complete, submit it immediately. +- If the redo log is only pre-committed but not in commit status, it will be judged at this time whether the binlog is complete. If it is complete, the redo log will be submitted. If it is incomplete, the transaction will be rolled back. + +This solves the problem of data consistency. + +## Three Summary + +- MySQL is mainly divided into the Server layer and the Engine layer. The Server layer mainly includes connectors, query caches, analyzers, optimizers, executors, and a log module (binlog). This log module can be shared by all execution engines. Redolog is only available in InnoDB. +- The engine layer is plug-in type and currently mainly includes MyISAM, InnoDB, Memory, etc. +- The execution flow of the query statement is as follows: Permission verification (if the cache is hit)--->Query cache--->Analyzer--->Optimizer--->Permission verification--->Executor--->Engine +- The update statement execution process is as follows: Analyzer---->Permission Verification---->Executor--->Engine---redo log (prepare status)--->binlog--->redo log (commit status) + +## Four References + +- "MySQL Practical Lectures 45" +- MySQL 5.6 Reference Manual: + + \ No newline at end of file diff --git a/docs_en/database/mysql/index-invalidation-caused-by-implicit-conversion.en.md b/docs_en/database/mysql/index-invalidation-caused-by-implicit-conversion.en.md new file mode 100644 index 00000000000..a78c6866c48 --- /dev/null +++ b/docs_en/database/mysql/index-invalidation-caused-by-implicit-conversion.en.md @@ -0,0 +1,169 @@ +--- +title: MySQL隐式转换造成索引失效 +category: 数据库 +tag: + - MySQL + - 性能优化 +head: + - - meta + - name: keywords + content: 隐式转换,索引失效,类型不匹配,函数计算,优化器,性能退化 + - - meta + - name: description + content: 解析隐式转换导致的索引失效与性能退化,给出类型规范、语句改写与参数配置建议,避免查询退化。 +--- + +> 本次测试使用的 MySQL 版本是 `5.7.26`,随着 MySQL 版本的更新某些特性可能会发生改变,本文不代表所述观点和结论于 MySQL 所有版本均准确无误,版本差异请自行甄别。 +> +> 原文: + +## 前言 + +数据库优化是一个任重而道远的任务,想要做优化必须深入理解数据库的各种特性。在开发过程中我们经常会遇到一些原因很简单但造成的后果却很严重的疑难杂症,这类问题往往还不容易定位,排查费时费力最后发现是一个很小的疏忽造成的,又或者是因为不了解某个技术特性产生的。 + +于数据库层面,最常见的恐怕就是索引失效了,且一开始因为数据量小还不易被发现。但随着业务的拓展数据量的提升,性能问题慢慢的就体现出来了,处理不及时还很容易造成雪球效应,最终导致数据库卡死甚至瘫痪。造成索引失效的原因可能有很多种,相关技术博客已经有太多了,今天我要记录的是**隐式转换造成的索引失效**。 + +## 数据准备 + +首先使用存储过程生成 1000 万条测试数据, +测试表一共建立了 7 个字段(包括主键),`num1`和`num2`保存的是和`ID`一样的顺序数字,其中`num2`是字符串类型。 +`type1`和`type2`保存的都是主键对 5 的取模,目的是模拟实际应用中常用类似 type 类型的数据,但是`type2`是没有建立索引的。 +`str1`和`str2`都是保存了一个 20 位长度的随机字符串,`str1`不能为`NULL`,`str2`允许为`NULL`,相应的生成测试数据的时候我也会在`str2`字段生产少量`NULL`值(每 100 条数据产生一个`NULL`值)。 + +```sql +-- 创建测试数据表 +DROP TABLE IF EXISTS test1; +CREATE TABLE `test1` ( + `id` int(11) NOT NULL, + `num1` int(11) NOT NULL DEFAULT '0', + `num2` varchar(11) NOT NULL DEFAULT '', + `type1` int(4) NOT NULL DEFAULT '0', + `type2` int(4) NOT NULL DEFAULT '0', + `str1` varchar(100) NOT NULL DEFAULT '', + `str2` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `num1` (`num1`), + KEY `num2` (`num2`), + KEY `type1` (`type1`), + KEY `str1` (`str1`), + KEY `str2` (`str2`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- 创建存储过程 +DROP PROCEDURE IF EXISTS pre_test1; +DELIMITER // +CREATE PROCEDURE `pre_test1`() +BEGIN + DECLARE i INT DEFAULT 0; + SET autocommit = 0; + WHILE i < 10000000 DO + SET i = i + 1; + SET @str1 = SUBSTRING(MD5(RAND()),1,20); + -- 每100条数据str2产生一个null值 + IF i % 100 = 0 THEN + SET @str2 = NULL; + ELSE + SET @str2 = @str1; + END IF; + INSERT INTO test1 (`id`, `num1`, `num2`, + `type1`, `type2`, `str1`, `str2`) + VALUES (CONCAT('', i), CONCAT('', i), + CONCAT('', i), i%5, i%5, @str1, @str2); + -- 事务优化,每一万条数据提交一次事务 + IF i % 10000 = 0 THEN + COMMIT; + END IF; + END WHILE; +END; +// DELIMITER ; +-- 执行存储过程 +CALL pre_test1(); +``` + +数据量比较大,还涉及使用`MD5`生成随机字符串,所以速度有点慢,稍安勿躁,耐心等待即可。 + +1000 万条数据,我用了 33 分钟才跑完(实际时间跟你电脑硬件配置有关)。这里贴几条生成的数据,大致长这样。 + +![](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. When comparing hexadecimal values with non-numeric values, they will be treated as binary strings. +> 5. If one parameter is `TIMESTAMP` or `DATETIME`, and the other parameter is a constant, the constant will be converted to `timestamp` +> 6. One parameter is of type `decimal`. If the other parameter is `decimal` or an integer, the integer will be converted to `decimal` for comparison. If the other parameter is a floating point number, `decimal` will be converted to a floating point number for comparison. +> 7. **In all other cases, both parameters will be converted to floating point numbers before comparison** + +According to the description of the official document, implicit conversion has occurred in our 23rd SQL. The query condition of the 2nd SQL is `num1 = '10000'`. The left side is an `int` type and the right is a string. The 3rd SQL is the opposite. According to the official conversion rule 7, both the left and right sides will be converted to floating point numbers before comparison. + +Let’s look at the second SQL first: ``SELECT * FROM `test1` WHERE num1 = '10000';`` **The left side is int type**`10000`, which is still `10000` when converted to a floating point number, and the string type on the right side is `'10000``, which is also `10000` when converted into a floating point number. The conversion results on both sides are unique and certain, so the use of indexes is not affected. + +Item 3 SQL: ``SELECT * FROM `test1` WHERE num2 = 10000;`` **The left side is string type**`'10000'', and the conversion result to floating point number 10000 is unique, and the conversion result of `int` type `10000` on the right side is also unique. However, because the left side is the search condition, although it is unique to convert `'10000'` to `10000`, other strings can also be converted to `10000`, such as `'10000a'`, `'010000'', `'10000'', etc. can be converted to floating point number `10000`. In this case, the index cannot be used. + +Regarding this **implicit conversion**, we can verify it through query testing. First insert a few pieces of data, including `num2='10000a'`, `'010000'' and `'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'); +``` + +Then use the third SQL statement ``SELECT * FROM `test1` WHERE num2 = 10000;`` to query: + +![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-03.png) + +As you can see from the results, the three pieces of data inserted later also match. So what are the rules for implicit conversion of this string? Why can `num2='10000a'`, `'010000'' and `'10000'' be matched? After consulting relevant information, we found the following rules: + +1. Strings **not starting with a number** will be converted to `0`. For example, `'abc'`, `'a123bc'`, `'abc123''` will be converted to `0`; +2. **Strings starting with a number** will be intercepted during conversion, from the first character to the first non-numeric content. For example, `'123abc'` will be converted to `123`, `'012abc'` will be converted to `012` which is `12`, `'5.3a66b78c'` will be converted to `5.3`, and the others are the same. + +Now test and verify the above rules as follows: + +![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-04.png) + +This also confirms the previous query results. + +Write a SQL query str1 field again: ``SELECT * FROM `test1` WHERE str1 = 1234;`` + +![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-05.png) + +## Analysis and summary + +Through the above test, we discovered some features of MySQL operators: + +1. When the data types on the left and right sides of the operator are inconsistent, **implicit conversion** will occur. +2. When the left side of the where query operator is a numeric type, implicit conversion occurs, which has little impact on efficiency, but it is still not recommended. +3. When the left side of the where query operator is a character type, implicit conversion occurs, which will cause the index to fail and cause the full table scan to be extremely inefficient. +4. When a string is converted to a numeric type, the string starting with a non-number will be converted to `0`, and the string starting with a number will intercept the value from the first character to the first non-number content as the conversion result. + +Therefore, we must develop good habits when writing SQL. Whatever type of field is being queried, the condition on the right side of the equal sign should be written as the corresponding type. Especially when the query field is a string, the condition on the right side of the equal sign must be enclosed in quotation marks to indicate that it is a string. Otherwise, the index will fail and trigger a full table scan. + + \ No newline at end of file diff --git a/docs_en/database/mysql/innodb-implementation-of-mvcc.en.md b/docs_en/database/mysql/innodb-implementation-of-mvcc.en.md new file mode 100644 index 00000000000..805629c180e --- /dev/null +++ b/docs_en/database/mysql/innodb-implementation-of-mvcc.en.md @@ -0,0 +1,266 @@ +--- +title: InnoDB存储引擎对MVCC的实现 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: InnoDB,MVCC,快照读,当前读,一致性视图,隐藏列,事务版本,间隙锁 + - - meta + - name: description + content: 深入解析 InnoDB 的 MVCC 实现细节与读写隔离,覆盖一致性视图、快照/当前读与隐藏列、间隙锁的配合。 +--- + +## 多版本并发控制 (Multi-Version Concurrency Control) + +MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。 + +1、读操作(SELECT): + +当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下: + +- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。 +- 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。 +- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。 + +2、写操作(INSERT、UPDATE、DELETE): + +当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下: + +- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。 +- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。 +- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。 + +3、事务提交和回滚: + +- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。 +- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。 + +4、版本的回收: + +为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。 + +MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。 + +## 一致性非锁定读和锁定读 + +### 一致性非锁定读 + +对于 [**一致性非锁定读(Consistent Nonlocking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见 + +在 `InnoDB` 存储引擎中,[多版本控制 (multi versioning)](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html) 就是对非锁定读的实现。如果读取的行正在执行 `DELETE` 或 `UPDATE` 操作,这时读取操作不会去等待行上锁的释放。相反地,`InnoDB` 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read) + +在 `Repeatable Read` 和 `Read Committed` 两个隔离级别下,如果是执行普通的 `select` 语句(不包括 `select ... lock in share mode` ,`select ... for update`)则会使用 `一致性非锁定读(MVCC)`。并且在 `Repeatable Read` 下 `MVCC` 实现了可重复读和防止部分幻读 + +### 锁定读 + +如果执行的是下列语句,就是 [**锁定读(Locking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html) + +- `select ... lock in share mode` +- `select ... for update` +- `insert`、`update`、`delete` 操作 + +在锁定读下,读取的是数据的最新版本,这种读也被称为 `当前读(current 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) + +Regenerate `Read View`. At this time, transactions 101 and 102 have been submitted, so **m_ids** is empty, then m_up_limit_id = m_low_limit_id = 104, and the latest version transaction ID is 102, which satisfies 102 < m_low_limit_id. It can be seen that the query result is `name = Zhao Liu` + +> **Summary:** **Under the RC isolation level, the transaction will generate and set up a new Read View at the beginning of each query, resulting in non-repeatable reads** + +### ReadView generation under RR + +At repeatable read level, a Read View (m_ids list) is generated only the first time data is read after the transaction starts. + +**1. The version chain in the case of T4 is: ** + +![](./images/mvvc/0e906b95-c916-4f30-beda-9cb3e49746bf.png) + +A `Read View` is generated when the `select` statement is currently executed. At this time, **`m_ids`: [101,102]**, `m_low_limit_id` is: 104, `m_up_limit_id` is: 101, `m_creator_trx_id` is: 103 + +This is the same as at RC level: + +- The `DB_TRX_ID` of the latest record is 101, m_up_limit_id <= 101 < m_low_limit_id, so you need to search in the `m_ids` list and find that `DB_TRX_ID` exists in the list, then this record is not visible +- Find the previous version record in the `undo log` based on `DB_ROLL_PTR`. The `DB_TRX_ID` of the previous record is still 101 and is invisible. +- Continue to find the previous `DB_TRX_ID` as 1, which satisfies 1 < m_up_limit_id and is visible, so transaction 103 queries the data as `name = cauliflower` + +**2. In the case of time point T6: ** + +![](./images/mvvc/79ed6142-7664-4e0b-9023-cf546586aa39.png) + +`Read View` will only be generated once at the RR level, so **`m_ids`: [101,102]** is still used at this time, `m_low_limit_id` is: 104, `m_up_limit_id` is: 101, `m_creator_trx_id` is: 103 + +- The `DB_TRX_ID` of the latest record is 102, m_up_limit_id <= 102 < m_low_limit_id, so you need to search in the `m_ids` list and find that `DB_TRX_ID` exists in the list, then this record is not visible + +- Find the previous version record in the `undo log` based on `DB_ROLL_PTR`. The `DB_TRX_ID` of the previous record is 101 and is invisible. + +- Continue to find the previous version record in the `undo log` based on `DB_ROLL_PTR`. The `DB_TRX_ID` of the previous record is still 101 and is invisible. + +- Continue to find the previous `DB_TRX_ID` as 1, which satisfies 1 < m_up_limit_id and is visible, so transaction 103 queries the data as `name = cauliflower` + +**3. In the case of time point T9: ** + +![](./images/mvvc/cbbedbc5-0e3c-4711-aafd-7f3d68a4ed4e.png) + +The situation at this time is exactly the same as T6. Since `Read View` has been generated, **`m_ids`: [101,102]** is still used at this time, so the query result is still `name = cauliflower` + +## MVCC➕Next-key-Lock prevents phantom reads + +The `InnoDB` storage engine uses `MVCC` and `Next-key Lock` to solve the phantom read problem at the RR level: + +**1. Execute ordinary `select`, and the data will be read in the way of `MVCC` snapshot reading** + +In the case of snapshot read, the RR isolation level will only generate `Read View` for the first query after the transaction is started, and will be used until the transaction is committed. Therefore, after generating `Read View`, the updated and inserted record versions made by other transactions are not visible to the current transaction, achieving repeatable reading and preventing "phantom reading" under snapshot reading. + +**2. Execute select...for update/lock in share mode, insert, update, delete and other current reads** + +Under the current read, all the latest data is read. If other transactions insert new records and they happen to be within the query range of the current transaction, phantom reads will occur! `InnoDB` uses [Next-key Lock](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks) to prevent this. When executing the current read, the read records will be locked and their gaps will be locked to prevent other transactions from inserting data within the query range. As long as I don't let you insert, phantom reading won't happen + +## Reference + +- **"MySQL Technology Insider InnoDB Storage Engine 2nd Edition"** +- [Relationship between transaction isolation level and locks in Innodb](https://tech.meituan.com/2014/08/20/innodb-lock.html) +- [MySQL transactions and how MVCC implements isolation levels](https://blog.csdn.net/qq_35190492/article/details/109044141) +- [InnoDB Transaction Analysis-MVCC](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/) + + \ No newline at end of file diff --git a/docs_en/database/mysql/mysql-auto-increment-primary-key-continuous.en.md b/docs_en/database/mysql/mysql-auto-increment-primary-key-continuous.en.md new file mode 100644 index 00000000000..cb8095ca1c6 --- /dev/null +++ b/docs_en/database/mysql/mysql-auto-increment-primary-key-continuous.en.md @@ -0,0 +1,228 @@ +--- +title: MySQL自增主键一定是连续的吗 +category: 数据库 +tag: + - MySQL + - 大厂面试 +head: + - - meta + - name: keywords + content: 自增主键,不连续,事务回滚,并发插入,计数器,聚簇索引 + - - meta + - name: description + content: 解析自增主键不连续的根因与触发场景,结合事务回滚与并发插入,说明 InnoDB 计数器与聚簇索引的行为。 +--- + +> 作者:飞天小牛肉 +> +> 原文: + +众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。 + +但实际上,MySQL 的自增主键并不能保证一定是连续递增的。 + +下面举个例子来看下,如下所示创建一张表: + +![](https://oss.javaguide.cn/p3-juejin/3e6b80ba50cb425386b80924e3da0d23~tplv-k3u1fbpfcp-zoom-1.png) + +## 自增值保存在哪里? + +使用 `insert into test_pk values(null, 1, 1)` 插入一行数据,再执行 `show create table` 命令来看一下表的结构定义: + +![](https://oss.javaguide.cn/p3-juejin/c17e46230bd34150966f0d86b2ad5e91~tplv-k3u1fbpfcp-zoom-1.png) + +上述表的结构定义存放在后缀名为 `.frm` 的本地文件中,在 MySQL 安装目录下的 data 文件夹下可以找到这个 `.frm` 文件: + +![](https://oss.javaguide.cn/p3-juejin/3ec0514dd7be423d80b9e7f2d52f5902~tplv-k3u1fbpfcp-zoom-1.png) + +从上述表结构可以看到,表定义里面出现了一个 `AUTO_INCREMENT=2`,表示下一次插入数据时,如果需要自动生成自增值,会生成 id = 2。 + +但需要注意的是,自增值并不会保存在这个表结构也就是 `.frm` 文件中,不同的引擎对于自增值的保存策略不同: + +1)MyISAM 引擎的自增值保存在数据文件中 + +2)InnoDB 引擎的自增值,其实是保存在了内存里,并没有持久化。第一次打开表的时候,都会去找自增值的最大值 `max(id)`,然后将 `max(id)+1` 作为这个表当前的自增值。 + +举个例子:我们现在表里当前数据行里最大的 id 是 1,AUTO_INCREMENT=2,对吧。这时候,我们删除 id=1 的行,AUTO_INCREMENT 还是 2。 + +![](https://oss.javaguide.cn/p3-juejin/61b8dc9155624044a86d91c368b20059~tplv-k3u1fbpfcp-zoom-1.png) + +但如果马上重启 MySQL 实例,重启后这个表的 AUTO_INCREMENT 就会变成 1。 也就是说,MySQL 重启可能会修改一个表的 AUTO_INCREMENT 的值。 + +![](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 重启前的值” + +也就是说对于上面这个例子来说,重启实例后这个表的 AUTO_INCREMENT 仍然是 2。 + +理解了 MySQL 自增值到底保存在哪里以后,我们再来看看自增值的修改机制,并以此引出第一种自增值不连续的场景。 + +## 自增值不连续的场景 + +### 自增值不连续场景 1 + +在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下: + +- 如果插入数据时 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) + +Although we inserted a (null, 3, 3) record, it was rolled back using rollback, so there is no such record in the database: + +![](https://oss.javaguide.cn/p3-juejin/6cb4c02722674dd399939d3d03a431c1~tplv-k3u1fbpfcp-zoom-1.png) + +In the case of this transaction rollback, the auto-increment value does not also roll back! As shown in the figure below, the self-increment value still stubbornly increases from 4 to 5: + +![](https://oss.javaguide.cn/p3-juejin/e6eea1c927424ac7bda34a511ca521ae~tplv-k3u1fbpfcp-zoom-1.png) + +So when we insert a piece of data (null, 3, 3) at this time, the primary key id will be automatically assigned to `5`: + +![](https://oss.javaguide.cn/p3-juejin/80da69dd13b543c4a32d6ed832a3c568~tplv-k3u1fbpfcp-zoom-1.png) + +So, why doesn't MySQL change the table's auto-increment value back when a unique key conflict or rollback occurs? If we go back, won't the self-increasing IDs become discontinuous? + +In fact, the main reason for doing this is to improve performance. + +We directly use proof by contradiction to verify: Assume that MySQL will change the self-increment value back when the transaction is rolled back, what will happen? + +Now there are two transactions A and B that are executed in parallel. When applying for auto-increment value, in order to prevent the two transactions from applying for the same auto-increment ID, they must lock and then apply sequentially, right? + +1. Assume that transaction A applies for id = 1, and transaction B applies for id = 2. Then the auto-increment value of table t is 3 at this time, and execution continues thereafter. +2. Transaction B is submitted correctly, but transaction A has a unique key conflict, that is, the insertion of the row record with id = 1 fails. If transaction A is allowed to roll back the auto-incremented id, that is, change the current auto-increment value of the table back to 1, then there will be a situation like this: there is already a row with id = 2 in the table, and the current auto-increment id value is 1. +3. Next, other transactions that continue to be executed will apply for id=2. At this time, an insert statement error "primary key conflict" will appear. + +![](https://oss.javaguide.cn/p3-juejin/5f26f02e60f643c9a7cab88a9f1bdce9~tplv-k3u1fbpfcp-zoom-1.png) + +In order to resolve this primary key conflict, there are two methods: + +1. Before each application for an id, first determine whether the id already exists in the table. If it exists, skip the id. +2. Expand the lock scope of the self-increasing ID. You must wait until a transaction is completed and submitted before the next transaction can apply for the self-increasing ID. + +Obviously, the costs of the above two methods are relatively high and will cause performance problems. The reason is the "allowing auto-increment id fallback" we assumed. + +Therefore, InnoDB abandoned this design and will not fall back to incrementing the ID if the statement fails to execute. It is precisely because of this that it is only guaranteed that the auto-incrementing id is increasing, but it is not guaranteed to be continuous. + +In summary, three scenarios of discontinuous self-increasing value have been analyzed, and there is also a fourth scenario: batch insertion of data. + +### Self-increasing value discontinuous scenario 4 + +For statements that insert data in batches, MySQL has a strategy to apply for auto-increment IDs in batches: + +1. During statement execution, if you apply for an auto-increment ID for the first time, 1 will be allocated; +2. After 1 is used up, this statement will apply for an auto-increment ID for the second time, and 2 will be allocated; +3. After 2 are used up, the same statement remains. If you apply for an auto-increment ID for the third time, 4 will be allocated; +4. By analogy, if you use the same statement to apply for auto-increment IDs, the number of auto-increment IDs applied each time is twice that of the previous time. + +Note that the batch insertion of data mentioned here does not include multiple value values ​​in the ordinary insert statement! ! ! , because this type of statement can accurately calculate how many IDs are needed when applying for auto-incrementing IDs, and then apply for them all at once. After the application is completed, the lock can be released. + +For statements such as `insert ... select`, replace ... select and load data, MySQL does not know how many IDs need to be applied for, so it adopts this batch application strategy. After all, it is too slow to apply one by one. + +For example, assume that our current table has the following data: + +![](https://oss.javaguide.cn/p3-juejin/6453cfc107f94e3bb86c95072d443472~tplv-k3u1fbpfcp-zoom-1.png) + +We create a table `test_pk2` with the same structure definition as the current table `test_pk`: + +![](https://oss.javaguide.cn/p3-juejin/45248a6dc34f431bba14d434bee2c79e~tplv-k3u1fbpfcp-zoom-1.png) + +Then use `insert...select` to batch insert data into the `teset_pk2` table: + +![](https://oss.javaguide.cn/p3-juejin/c1b061e86bae484694d15ceb703b10ca~tplv-k3u1fbpfcp-zoom-1.png) + +As you can see, the data was successfully imported. + +Let’s take a look at the auto-increment value of `test_pk2`: + +![](https://oss.javaguide.cn/p3-juejin/0ff9039366154c738331d64ebaf88d3b~tplv-k3u1fbpfcp-zoom-1.png) + +As analyzed above, it is 8 instead of 6 + +Specifically, insert...select actually inserts 5 rows of data (1 1) (2 2) (3 3) (4 4) (5 5) into the table. However, these five lines of data are self-increasing IDs applied for three times. Combined with the batch application strategy, the number of self-increasing IDs applied for each time is twice that of the previous time, so: + +- I applied for an id for the first time: id=1 +- Two ids were assigned for the second time: id=2 and id=3 +- For the third time, 4 ids were assigned: id=4, id = 5, id = 6, id=7 + +Since this statement actually uses only 5 ids, id=6 and id=7 are wasted. After that, execute `insert into test_pk2 values(null,6,6)`, and the actually inserted data is (8,6,6): + +![](https://oss.javaguide.cn/p3-juejin/51612fbac3804cff8c5157df21d6e355~tplv-k3u1fbpfcp-zoom-1.png) + +## Summary + +This article summarizes four scenarios of discontinuous self-increasing value: + +1. The auto-increment initial value and auto-increment step size are not set to 1. +2. Unique key conflict +3. Transaction rollback +4. Batch insertion (such as `insert...select` statement) + + \ No newline at end of file diff --git a/docs_en/database/mysql/mysql-high-performance-optimization-specification-recommendations.en.md b/docs_en/database/mysql/mysql-high-performance-optimization-specification-recommendations.en.md new file mode 100644 index 00000000000..29ab4112114 --- /dev/null +++ b/docs_en/database/mysql/mysql-high-performance-optimization-specification-recommendations.en.md @@ -0,0 +1,396 @@ +--- +title: MySQL高性能优化规范建议总结 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL 优化,索引设计,SQL 规范,表结构,慢查询,参数调优,实践清单 + - - meta + - name: description + content: 提炼 MySQL 高性能优化规范,涵盖索引与 SQL、表结构与慢查询、参数与实用清单,提升线上稳定与效率。 +--- + +> 作者: 听风 原文地址: 。 +> +> JavaGuide 已获得作者授权,并对原文内容进行了完善补充。 + +## 数据库命名规范 + +- 所有数据库对象名称必须使用小写字母并用下划线分割。 +- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。 +- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。 +- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。 +- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 + +## 数据库基本设计规范 + +### 所有表必须使用 InnoDB 存储引擎 + +没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 + +InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 + +### 数据库和表的字符集统一使用 UTF8 + +兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 + +推荐阅读一下我写的这篇文章:[MySQL 字符集详解](../character-set.md) 。 + +### 所有表和字段都需要添加注释 + +使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。 + +### 尽量控制单表数据量的大小,建议控制在 500 万以内 + +500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。 + +可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。 + +### 谨慎使用 MySQL 分区表 + +分区表在物理上表现为多个文件,在逻辑上表现为一个表。 + +谨慎选择分区键,跨分区查询效率可能更低。 + +建议采用物理分表的方式管理大数据。 + +### 经常一起使用的列放到一个表中 + +避免更多的关联操作。 + +### 禁止在表中建立预留字段 + +- 预留字段的命名很难做到见名识义。 +- 预留字段无法确认存储的数据类型,所以无法选择合适的类型。 +- 对预留字段类型的修改,会对表进行锁定。 + +### 禁止在数据库中存储文件(比如图片)这类大的二进制数据 + +在数据库中存储文件会严重影响数据库性能,消耗过多存储空间。 + +文件(比如图片)这类大的二进制数据通常存储于文件服务器,数据库只存储文件地址信息。 + +### 不要被数据库范式所束缚 + +一般来说,设计关系数据库时需要满足第三范式,但为了满足第三范式,我们可能会拆分出多张表。而在进行查询时需要对多张表进行关联查询,有时为了提高查询效率,会降低范式的要求,在表中保存一定的冗余信息,也叫做反范式。但要注意反范式一定要适度。 + +### 禁止在线上做数据库压力测试 + +### 禁止从开发环境、测试环境直接连接生产环境数据库 + +安全隐患极大,要对生产环境抱有敬畏之心! + +## 数据库字段设计规范 + +### 优先选择符合存储需要的最小的数据类型 + +存储字节越小,占用空间也就越小,性能也越好。 + +**a.某些字符串可以转换成数字类型存储,比如可以将 IP 地址转换成整型数据。** + +数字是连续的,性能更好,占用空间也更小。 + +MySQL 提供了两个方法来处理 ip 地址: + +- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位); +- `INET_NTOA()`:把整型的 ip 转为地址。 + +插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型;显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。 + +**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 个。 + +### 减少同数据库的交互次数 + +The database is more suitable for processing batch operations. Merging multiple identical operations together can improve processing efficiency. + +### When performing or judgment corresponding to the same column, use in instead of or + +Do not exceed 500 values for in. The in operation can make more efficient use of indexes, or indexes can rarely be used in most cases. + +### Disable the use of order by rand() for random sorting + +order by rand() will load all qualified data in the table into memory, then sort all data in memory according to randomly generated values, and may generate a random value for each row. If the data set that meets the conditions is very large, it will consume a lot of CPU, IO and memory resources. + +It is recommended to get a random value in the program and then get the data from the database. + +### Disable function transformations and calculations on columns in the WHERE clause + +Performing function transformations or calculations on columns results in the index not being able to be used. + +**Not recommended**: + +```sql +where date(create_time)='20190101' +``` + +**Recommended**: + +```sql +where create_time >= '20190101' and create_time < '20190102' +``` + +### Use UNION ALL instead of UNION when it is obvious that there will be no duplicate values + +- UNION will put all the data of the two result sets into the temporary table and then perform the deduplication operation. +- UNION ALL will no longer deduplicate the result set. + +### Split a complex large SQL into multiple small SQLs + +- Large SQL is logically complex and requires a large amount of CPU for calculation. +- In MySQL, one SQL can only use one CPU for calculation. +- After SQL is split, it can be executed in parallel to improve processing efficiency. + +### The program connects to different databases using different accounts, and cross-database queries are prohibited. + +- Leave room for database migration and sharding of databases and tables. +- Reduce business coupling. +- Avoid security risks caused by excessive permissions. + +## Database operation code of conduct + +### Batch write (UPDATE, DELETE, INSERT) operations of more than 1 million rows must be performed in batches multiple times. + +**Large batch operations may cause severe master-slave delays** + +In a master-slave environment, large batch operations may cause serious master-slave delays. Large-batch write operations generally take a long time to execute, and only after the execution on the master database is completed, they will be executed on other slave databases, so it will cause a long delay between the master database and the slave database. + +**When the binlog log is in row format, a large number of logs will be generated** + +Large batch write operations will generate a large number of logs, especially for row format binary data. Since the modification of each row of data is recorded in the row format, the more data we modify at one time, the more logs will be generated, and the longer the log transmission and recovery will take, which is also a reason for the master-slave delay. + +**Avoid large transaction operations** + +Modifying data in large batches must be done in a transaction, which will cause a large amount of data in the table to be locked, resulting in a large amount of blocking. Blocking will have a great impact on the performance of MySQL. + +In particular, long-term blocking will occupy all available connections to the database, which will prevent other applications in the production environment from connecting to the database. Therefore, it is important to pay attention to batching large write operations. + +### For large tables, use pt-online-schema-change to modify the table structure. + +- Avoid master-slave delays caused by large table modifications. +- Avoid table locking when modifying table fields. + +You must be careful when modifying the data structure of a large table, as it will cause serious table lock operations, which is intolerable especially in a production environment. + +pt-online-schema-change will first create a new table with the same structure as the original table, modify the table structure on the new table, then copy the data in the original table to the new table, and add some triggers to the original table. Copy the newly added data in the original table to the new table. After all the row data is copied, name the new table the original table and delete the original table. Break the original DDL operation into multiple small batches. + +### It is forbidden to grant super permissions to the account used by the program. + +- When the maximum number of connections is reached, 1 user connection with super privileges is also run. +- Super permissions can only be reserved for the account used by the DBA to handle problems. + +### For the program to connect to the database account, follow the principle of least privileges + +- The database account used by the program can only be used under one DB, and cross-databases are not allowed. +- In principle, the account used by the program is not allowed to have drop permissions. + +## Recommended reading + +- [MySQL design conventions that technical students must learn are painful lessons - Alibaba Developers](https://mp.weixin.qq.com/s/XC8e5iuQtfsrEOERffEZ-Q) +- [Let’s talk about 15 tips for creating database tables](https://mp.weixin.qq.com/s/NM-aHaW6TXrnO6la6Jfl5A) + + \ No newline at end of file diff --git a/docs_en/database/mysql/mysql-index.en.md b/docs_en/database/mysql/mysql-index.en.md new file mode 100644 index 00000000000..f3f9553522b --- /dev/null +++ b/docs_en/database/mysql/mysql-index.en.md @@ -0,0 +1,564 @@ +--- +title: MySQL索引详解 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL 索引,B+树,覆盖索引,联合索引,选择性,回表,索引下推 + - - meta + - name: description + content: 深入解析 MySQL 索引结构与选型,覆盖 B+ 树、联合与覆盖索引、选择性与回表等关键优化点与实践。 +--- + +> 感谢[WT-AHA](https://github.com/WT-AHA)对本文的完善,相关 PR: 。 + +但凡经历过几场面试的小伙伴,应该都清楚,数据库索引这个知识点在面试中出现的频率高到离谱。 + +除了对于准备面试来说非常重要之外,善用索引对 SQL 的性能提升非常明显,是一个性价比较高的 SQL 优化手段。 + +## 索引介绍 + +**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** + +索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 + +索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。 + +## 索引的优缺点 + +**索引的优点:** + +1. **查询速度起飞 (主要目的)**:通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。 +2. **保证数据唯一性**:通过创建**唯一索引 (Unique Index)**,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户 ID、邮箱等。主键本身就是一种唯一索引。 +3. **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。 + +**索引的缺点:** + +1. **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML 操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。 +2. **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。 +3. **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。 + +**那么,用了索引就一定能提高查询性能吗?** + +**不一定。** 大多数情况下,合理使用索引确实比全表扫描快得多。但也有例外: + +- **数据量太小**:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。 +- **查询结果集占比过大**:如果要查询的数据占了整张表的大部分(比如超过 20%-30%),优化器可能会认为全表扫描更划算,因为通过索引多次回表(随机 I/O)的成本可能高于一次顺序的全表扫描。 +- **索引维护不当或统计信息过时**:导致优化器做出错误判断。 + +## 索引底层数据结构选型 + +### Hash 表 + +哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 + +**为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。 + +```java +hash = hashfunc(key) +index = hash % array_size +``` + +![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092328171.png) + +但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了提高链表过长时的搜索效率,引入了红黑树。 + +![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092224836.png) + +为了减少 Hash 冲突的发生,一个好的哈希函数应该“均匀地”将数据分布在整个可能的哈希值集合中。 + +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) 这篇文章。 + +既然哈希表这么快,**为什么 MySQL 没有使用其作为索引的数据结构呢?** 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。 + +试想一种情况: + +```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` 呢? `b = 1 AND a = 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`:和第二种一样的情况,整个索引都不会使用。 +4. 查询 `b=1 AND a=1 AND c=1`:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 `b=1` 和 `a=1` 的条件进行重排序,变成 `a=1 AND 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)这篇文章。 + + + diff --git a/docs_en/database/mysql/mysql-logs.en.md b/docs_en/database/mysql/mysql-logs.en.md new file mode 100644 index 00000000000..a87b372773d --- /dev/null +++ b/docs_en/database/mysql/mysql-logs.en.md @@ -0,0 +1,353 @@ +--- +title: MySQL三大日志(binlog、redo log和undo log)详解 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL 日志,binlog,redo log,undo log,两阶段提交,崩溃恢复,复制 + - - meta + - name: description + content: 系统解析 MySQL 的 binlog/redo/undo 三大日志与两阶段提交,理解崩溃恢复与主从复制的实现原理与取舍。 +--- + +> 本文来自公号程序猿阿星投稿,JavaGuide 对其做了补充完善。 + +## 前言 + +MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。 + +![](https://oss.javaguide.cn/github/javaguide/01.png) + +今天就来聊聊 redo log(重做日志)、binlog(归档日志)、两阶段提交、undo log(回滚日志)。 + +## redo log + +redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。 + +比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。 + +![](https://oss.javaguide.cn/github/javaguide/02.png) + +MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。 + +后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。 + +更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。 + +然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 redo log 文件里。 + +![](https://oss.javaguide.cn/github/javaguide/03.png) + +> 图片笔误提示:第 4 步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。 + +理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。 + +> 小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成 + +### 刷盘时机 + +在 InnoDB 存储引擎中,**redo log buffer**(重做日志缓冲区)是一块用于暂存 redo log 的内存区域。为了确保事务的持久性和数据的一致性,InnoDB 会在特定时机将这块缓冲区中的日志数据刷新到磁盘上的 redo log 文件中。这些时机可以归纳为以下六种: + +1. **事务提交时(最核心)**:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。 +2. **redo log buffer 空间不足时**:这是 InnoDB 的一种主动容量管理策略,旨在避免因缓冲区写满而导致用户线程阻塞。 + - 当 redo log buffer 的已用空间超过其总容量的**一半 (50%)** 时,后台线程会**主动**将这部分日志刷新到磁盘,为后续的日志写入腾出空间,这是一种“未雨绸缪”的优化。 + - 如果因为大事务或 I/O 繁忙导致 buffer 被**完全写满**,那么所有试图写入新日志的用户线程都会被**阻塞**,并强制进行一次同步刷盘,直到有可用空间为止。这种情况会影响数据库性能,应尽量避免。 +3. **触发检查点 (Checkpoint) 时**:Checkpoint 是 InnoDB 为了缩短崩溃恢复时间而设计的核心机制。当 Checkpoint 被触发时,InnoDB 需要将在此检查点之前的所有脏页刷写到磁盘。根据 **Write-Ahead Logging (WAL)** 原则,数据页写入磁盘前,其对应的 redo log 必须先落盘。因此,执行 Checkpoint 操作必然会确保相关的 redo log 也已经被刷新到了磁盘。 +4. **后台线程周期性刷新**:InnoDB 有一个后台的 master thread,它会大约每秒执行一次例行任务,其中就包括将 redo log buffer 中的日志刷新到磁盘。这个机制是 `innodb_flush_log_at_trx_commit` 设置为 0 或 2 时的主要持久化保障。 +5. **正常关闭服务器**:在 MySQL 服务器正常关闭的过程中,为了确保所有已提交事务的数据都被完整保存,InnoDB 会执行一次最终的刷盘操作,将 redo log buffer 中剩余的全部日志都清空并写入磁盘文件。 +6. **binlog 切换时**:当开启 binlog 后,在 MySQL 采用 `innodb_flush_log_at_trx_commit=1` 和 `sync_binlog=1` 的 双一配置下,为了保证 redo log 和 binlog 之间状态的一致性(用于崩溃恢复或主从复制),在 binlog 文件写满或者手动执行 flush logs 进行切换时,会触发 redo log 的刷盘动作。 + +总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。 + +我们要注意设置正确的刷盘策略`innodb_flush_log_at_trx_commit` 。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。 + +`innodb_flush_log_at_trx_commit` 的值有 3 种,也就是共有 3 种刷盘策略: + +- **0**:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。 +- **1**:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。 +- **2**:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。 + +刷盘策略`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) + +When it is `2`, as long as the transaction is submitted successfully, the contents in `redo log buffer` are only written to the file system cache (`page cache`). + +If MySQL is just down, there will be no data loss, but the downtime may result in `1` second of data loss. + +### Log file group + +There is more than one redo log file stored on the hard disk, but it appears in the form of a **log file group**. The size of each `redo` log file is the same. + +For example, it can be configured as a set of `4` files, the size of each file is `1GB`, and the entire redo log log file group can record the contents of `4G`. + +It adopts the form of a circular array, writing from the beginning, writing to the end and then returning to the beginning to write in a loop, as shown in the figure below. + +![](https://oss.javaguide.cn/github/javaguide/10.png) + +There are two important attributes in this **log file group**, namely `write pos, checkpoint` + +- **write pos** is the position of the current record, moving backward while writing +- **checkpoint** is the current position to be erased, and it is also moved to the future. + +Every time the redo log is flushed and recorded in the **log file group**, the `write pos` position will be moved back and updated. + +Each time MySQL loads the **log file group** to recover data, the loaded redo log records will be cleared, and the `checkpoint` will be moved back and updated. + +The empty part between `write pos` and `checkpoint` can be used to write new redo log records. + +![](https://oss.javaguide.cn/github/javaguide/11.png) + +If `write pos` catches up with `checkpoint`, it means that the **log file group** is full. At this time, new redo log records cannot be written. MySQL has to stop, clear some records, and advance `checkpoint`. + +![](https://oss.javaguide.cn/github/javaguide/12.png) + +Note that starting from MySQL 8.0.30, the log file group has changed slightly: + +> 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. 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. + +This means that before MySQL 8.0.30, you can configure the number and file size of the log file group through `innodb_log_files_in_group` and `innodb_log_file_size`, but in MySQL 8.0.30 and later versions, these two variables have been abandoned, and even if they are specified, they are used to calculate the value of `innodb_redo_log_capacity`. The number of files in the log file group is fixed at 32, and the file size is `innodb_redo_log_capacity / 32`. + +Regarding this change, we can verify it. + +First create a configuration file and configure the values ​​of `innodb_log_files_in_group` and `innodb_log_file_size`: + +```properties +[mysqld] +innodb_log_file_size = 10485760 +innodb_log_files_in_group = 64 +``` + +Docker starts a MySQL 8.0.32 container: + +```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 +``` + +Now let's take a look at the startup log: + +```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. +``` + +It is also shown here that the two variables `innodb_log_files_in_group` and `innodb_log_file_size` are used to calculate `innodb_redo_log_capacity` and have been deprecated. + +Let’s take a look at the number of files in the log file group: + +![](images/redo-log.png) + +You can see that there are exactly 32, and the size of each log file is `671088640 / 32 = 20971520` + +Therefore, when using MySQL 8.0.30 and later versions, it is recommended to use the `innodb_redo_log_capacity` variable to configure the log file group + +### redo log summary + +I believe everyone knows the function of redo log, its flushing time and storage mode. + +Now let's think about a question: **As long as the modified data page is flushed directly to the disk every time, what's the point of the redo log? ** + +Aren't they all brushes? What's the difference? + +```java +1 Byte=8bit +1 KB = 1024 Byte +1 MB = 1024 KB +1 GB = 1024 MB +1 TB = 1024 GB +``` + +In fact, the data page size is `16KB`, and flushing is time-consuming. It may only modify a few `Byte` data in the data page. Is it necessary to flush the complete data page? + +Moreover, data page flushing is written randomly, because the corresponding location of a data page may be at a random location in the hard disk file, so the performance is very poor. + +If you are writing redo log, one row of records may occupy dozens of `Byte` and only include the table space number, data page number, and disk file offset. +Amount, update value, plus sequential writing, so the disk flushing speed is very fast. + +Therefore, the performance of recording modifications in the form of redo log will far exceed that of refreshing the data page, which also makes the database's concurrency capability stronger. + +> In fact, the data pages of the memory will also be flushed at certain times. We call this page merging. We will explain this in detail when we talk about `Buffer Pool` + +## binlog + +redo log is a physical log that records "what modifications were made on a certain data page" and belongs to the InnoDB storage engine. + +Binlog is a logical log, and the record content is the original logic of the statement, similar to "add 1 to the c field of the row with ID=2", and belongs to the `MySQL Server` layer.不管用什么存储引擎,只要发生了表数据更新,都会产生 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,这有助于管理多个并发事务的回滚需求。 + +Normally, the **rollback segment header** (usually the first page of the rollback segment) is responsible for managing the rollback segment. The rollback segment header is part of the rollback segment, usually on the first page of the rollback segment. **history list** is part of the rollback segment header. Its main function is to record the undo log of all transactions that have been submitted but have not yet been purged. This list enables the purge thread to find and clean up undo log records that are no longer needed. + +In addition, the implementation of `MVCC` depends on: **Hidden fields, Read View, and undo log**. In the internal implementation, InnoDB determines the visibility of the data through the `DB_TRX_ID` and `Read View` of the data row. If it is not visible, it uses the `DB_ROLL_PTR` of the data row to find the historical version in the undo log. The data version read by each transaction may be different. In the same transaction, the user can only see the modifications committed before the transaction created `Read View` and the modifications made by the transaction itself. + +## Summary + +> This part is a supplement to JavaGuide: + +The MySQL InnoDB engine uses **redo log (redo log)** to ensure the **durability** of transactions, and uses **undo log (rollback log)** to ensure the **atomicity** of transactions. + +The **data backup, primary and secondary, primary-master, and master-slave** of the MySQL database are all inseparable from binlog. Binlog is required to synchronize data and ensure data consistency. + +## Reference + +- "MySQL Practical Lectures 45" +- "Take you to become a MySQL practical optimization master from scratch" +- "How MySQL works: Understanding MySQL from the fundamentals" +- "MySQL Technology Innodb Storage Engine" + + \ No newline at end of file diff --git a/docs_en/database/mysql/mysql-query-cache.en.md b/docs_en/database/mysql/mysql-query-cache.en.md new file mode 100644 index 00000000000..9590b7f4f26 --- /dev/null +++ b/docs_en/database/mysql/mysql-query-cache.en.md @@ -0,0 +1,209 @@ +--- +title: MySQL查询缓存详解 +category: 数据库 +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 中的查询缓存虽然能够提升数据库的查询性能,但是查询同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 +--- + +缓存是一个有效且实用的系统性能优化的手段,不论是操作系统还是各种软件和网站或多或少都用到了缓存。 + +然而,有经验的 DBA 都建议生产环境中把 MySQL 自带的 Query Cache(查询缓存)给关掉。而且,从 MySQL 5.7.20 开始,就已经默认弃用查询缓存了。在 MySQL 8.0 及之后,更是直接删除了查询缓存的功能。 + +这又是为什么呢?查询缓存真就这么鸡肋么? + +带着如下几个问题,我们正式进入本文。 + +- MySQL 查询缓存是什么?适用范围? +- MySQL 缓存规则是什么? +- MySQL 缓存的优缺点是什么? +- MySQL 缓存对性能有什么影响? + +## MySQL 查询缓存介绍 + +MySQL 体系架构如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/mysql/mysql-architecture.png) + +为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。 + +- 如果匹配(命中),则将查询的结果集直接返回给客户端,不必再解析、执行查询。 +- 如果没有匹配(未命中),则将 Hash 值和结果集保存在查询缓存中,以便以后使用。 + +也就是说,**一个查询语句(select)到了 MySQL Server 之后,会先到查询缓存看看,如果曾经执行过的话,就直接返回结果集给客户端。** + +![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) + +## MySQL 查询缓存管理和配置 + +通过 `show variables like '%query_cache%'`命令可以查看查询缓存相关的信息。 + +8.0 版本之前的话,打印的信息可能是下面这样的: + +```bash +mysql> show variables like '%query_cache%'; ++------------------------------+---------+ +| Variable_name | Value | ++------------------------------+---------+ +| have_query_cache | YES | +| query_cache_limit | 1048576 | +| query_cache_min_res_unit | 4096 | +| query_cache_size | 599040 | +| query_cache_type | ON | +| query_cache_wlock_invalidate | OFF | ++------------------------------+---------+ +6 rows in set (0.02 sec) +``` + +8.0 以及之后版本之后,打印的信息是下面这样的: + +```bash +mysql> show variables like '%query_cache%'; ++------------------+-------+ +| Variable_name | Value | ++------------------+-------+ +| have_query_cache | NO | ++------------------+-------+ +1 row in set (0.01 sec) +``` + +我们这里对 8.0 版本之前`show variables like '%query_cache%';`命令打印出来的信息进行解释。 + +- **`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 官方文档: + + + diff --git a/docs_en/database/mysql/mysql-query-execution-plan.en.md b/docs_en/database/mysql/mysql-query-execution-plan.en.md new file mode 100644 index 00000000000..3c35c989514 --- /dev/null +++ b/docs_en/database/mysql/mysql-query-execution-plan.en.md @@ -0,0 +1,145 @@ +--- +title: MySQL execution plan analysis +category: database +tag: + -MySQL +head: + - - meta + - name: keywords + content: MySQL basics, MySQL execution plan, EXPLAIN, query optimizer + - - meta + - name: description + content: Execution plan refers to the specific execution method of a SQL statement after being optimized by the MySQL query optimizer. The first step in optimizing SQL should be to understand the execution plan of SQL. +--- + +> This article comes from the official MySQL technology, and JavaGuide has supplemented and improved it. Original address: + +The first step in optimizing SQL should be to understand the execution plan of SQL. In this article, let’s learn about MySQL `EXPLAIN` execution plan. + +## What is an execution plan? + +**Execution plan** refers to the specific execution method of a SQL statement after being optimized by **MySQL Query Optimizer**. + +Execution plans are usually used in SQL performance analysis, optimization and other scenarios. Through the results of `EXPLAIN`, you can learn information such as the query sequence of the data table, the operation type of the data query operation, which indexes can be hit, which indexes will actually be hit, how many rows of records in each data table are queried, etc. + +## How to get the execution plan? + +MySQL provides us with the `EXPLAIN` command to obtain information about the execution plan. + +It should be noted that the `EXPLAIN` statement does not actually execute the relevant statements. Instead, it analyzes the statements through the query optimizer to find the optimal query plan and displays the corresponding information. + +The `EXPLAIN` execution plan supports `SELECT`, `DELETE`, `INSERT`, `REPLACE` and `UPDATE` statements. We usually use it to analyze `SELECT` query statements. It is very simple to use. The syntax is as follows: + +```sql +EXPLAIN + SELECT query statement; +``` + +Let’s briefly look at the execution plan of the next 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); ++----+-------------+----------+------------+-------+------------------+----------+----------+------+--------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+----------+------------+-------+------------------+----------+----------+------+--------+----------+-------------+ +| 1 | PRIMARY | dept_emp | NULL | ALL | NULL | NULL | NULL | NULL | 331143 | 100.00 | Using where | +| 2 | SUBQUERY | dept_emp | NULL | index | PRIMARY,dept_no | PRIMARY | 16 | NULL | 331143 | 100.00 | Using index | ++----+-------------+----------+------------+-------+------------------+----------+----------+------+--------+----------+-------------+ +``` + +As you can see, there are 12 columns in the execution plan results. The meaning of each column is summarized in the following table: + +| **Column Name** | **Meaning** | +|------------- | ----------------------------------------------- | +| id | The sequence identifier of the SELECT query | +| select_type | The query type corresponding to the SELECT keyword | +| table | table name used | +| partitions | Matching partitions, or NULL for unpartitioned tables | +| type | table access method | +| possible_keys | Possible indexes | +| key | the actual index used | +| key_len | The length of the selected index | +| ref | When using an index equivalent query, the column or constant to compare with the index | +| rows | The expected number of rows to read | +| filtered | Percentage of retained records after filtering by table conditions | +| Extra | Additional Information | + +## How to analyze EXPLAIN results? + +In order to analyze the execution results of the `EXPLAIN` statement, we need to understand the important fields in the execution plan. + +###id + +`SELECT` identifier, used to identify the execution order of each `SELECT` statement. + +If the ids are the same, execute them in order from top to bottom. The id is different. The larger the id value, the higher the execution priority. If the row refers to the union result of other rows, the value can be NULL. + +### select_type + +The type of query is mainly used to distinguish between ordinary queries, joint queries, subqueries and other complex queries. Common values ​​are: + +- **SIMPLE**: Simple query, does not contain UNION or subquery. +- **PRIMARY**: If the query contains subqueries or other parts, the outer SELECT will be marked as PRIMARY. +- **SUBQUERY**: The first SELECT in a subquery. +- **UNION**: In the UNION statement, SELECT that appears after UNION. +- **DERIVED**: Subqueries appearing in FROM will be marked as DERIVED. +- **UNION RESULT**: The result of UNION query. + +### table + +Each row has a corresponding table name for the table name used in the query. In addition to the normal table, the table name may also be the following values: + +- **``**: This line refers to the UNION result of the lines with id M and N; +- **``**: This line refers to the derived table result generated by the table with id N. Derived tables may be generated from subqueries in the FROM statement. +- **``**: This line refers to the materialized subquery results generated by the table with id N. + +### type (important) + +The type of query execution, describes how the query is executed. The order of all values from best to worst is: + +system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL + +The specific meanings of several common types are as follows: + +- **system**: If the engine used by the table is accurate for table row count statistics (such as MyISAM), and there is only one row of records in the table, the access method is system, which is a special case of const. +- **const**: There is at most one row of matching records in the table, which can be found in one query. It is often used to use all fields of the primary key or unique index as query conditions. +- **eq_ref**: When querying connected tables, the rows of the previous table have only one corresponding row in the current table. It is the best join method besides system and const. It is often used to use all fields of the primary key or unique index as the join condition. +- **ref**: Use ordinary indexes as query conditions, and the query results may find multiple rows that meet the conditions. +- **index_merge**: When the query condition uses multiple indexes, it means that Index Merge optimization is turned on. At this time, the key column in the execution plan lists the indexes used. +- **range**: Perform a range query on the index column. The key column in the execution plan indicates which index is used.- **index**: The query traverses the entire index tree, similar to ALL, except that it scans the index, which is generally in memory and is faster. +- **ALL**: Full table scan. + +### possible_keys + +The possible_keys column represents the indexes that MySQL may use when executing the query. If this column is NULL, it means that there is no index that may be used; in this case, you need to check the columns used in the WHERE statement to see if query performance can be improved by adding an index to one or more of these columns. + +### key (important) + +The key column represents the index actually used by MySQL. If NULL, the index is not used. + +### key_len + +The key_len column represents the maximum length of the index actually used by MySQL; when a joint index is used, it may be the sum of the lengths of multiple columns. The shorter it is, the better if it meets your needs. If the key column displays NULL , the key_len column also displays NULL . + +### rows + +The rows column indicates a rough estimate of the number of rows that need to be found or read based on table statistics and selection. The smaller the value, the better. + +### Extra (Important) + +This column contains additional information about MySQL parsing the query. Through this information, you can more accurately understand how MySQL executes the query. Common values are as follows: + +- **Using filesort**: External index sorting is used during sorting, and in-table index is not used for sorting. +- **Using temporary**: MySQL needs to create a temporary table to store the results of the query, which is common in ORDER BY and GROUP BY. +- **Using index**: Indicates that the query uses a covering index, without returning the table, and the query efficiency is very high. +- **Using index condition**: Indicates that the query optimizer chooses to use the index condition push-down feature. +- **Using where**: Indicates that the query uses the WHERE clause for conditional filtering. This usually occurs when the index is not used. +- **Using join buffer (Block Nested Loop)**: The method of joining table query, which means that when the driven table does not use an index, MySQL will first read out the driving table and put it into the join buffer, and then traverse the driven table and driving table for query. + +As a reminder, when the Extra column contains Using filesort or Using temporary, MySQL performance may have problems and needs to be avoided as much as possible. + +## Reference + +- +- + + \ No newline at end of file diff --git a/docs_en/database/mysql/mysql-questions-01.en.md b/docs_en/database/mysql/mysql-questions-01.en.md new file mode 100644 index 00000000000..f79a25704b8 --- /dev/null +++ b/docs_en/database/mysql/mysql-questions-01.en.md @@ -0,0 +1,1095 @@ +--- +title: MySQL常见面试题总结 +category: 数据库 +tag: + - MySQL + - 大厂面试 +head: + - - meta + - name: keywords + content: MySQL面试题,MySQL基础架构,InnoDB存储引擎,MySQL索引,B+树索引,事务隔离级别,redo log,undo log,binlog,MVCC,行级锁,慢查询优化 + - - meta + - name: description + content: MySQL高频面试题精讲:基础架构、InnoDB引擎、索引原理、B+树、事务ACID、MVCC、redo/undo/binlog日志、行锁/表锁、慢查询优化,一文速通大厂必考点! +--- + + + +## MySQL 基础 + +### 什么是关系型数据库? + +顾名思义,关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 + +关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。 + +![关系型数据库表关系](https://oss.javaguide.cn/java-guide-blog/5e3c1a71724a38245aa43b02_99bf70d46cc247be878de9d3a88f0c44.png) + +大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。 + +**有哪些常见的关系型数据库呢?** + +MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。 + +### 什么是 SQL? + +SQL 是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。 + +几乎所有的主流关系数据库都支持 SQL ,适用性非常强。并且,一些非关系型数据库也兼容 SQL 或者使用的是类似于 SQL 的查询语言。 + +SQL 可以帮助我们: + +- 新建数据库、数据表、字段; +- 在数据库中增加,删除,修改,查询数据; +- 新建视图、函数、存储过程; +- 对数据库中的数据进行简单的数据分析; +- 搭配 Hive,Spark SQL 做大数据; +- 搭配 SQLFlow 做机器学习; +- …… + +### 什么是 MySQL? + +![](https://oss.javaguide.cn/github/javaguide/csdn/20210327143351823.png) + +**MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。** + +由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是**3306**。 + +### ⭐️MySQL 有什么优点? + +这个问题本质上是在问 MySQL 如此流行的原因。 + +MySQL 成功可以归功于在**生态、功能和运维**这三个层面上的综合优势。 + +**第一,从生态和成本角度看,它的护城河非常深。** + +- **开源免费:** 这是它得以广泛普及的基石。任何公司和个人都可以免费使用,极大地降低了技术门槛和初期成本。 +- **社区庞大,生态完善:** 经过几十年的发展,MySQL 拥有极其活跃的社区和丰富的生态系统。这意味着无论你遇到什么问题,几乎都能在网上找到解决方案;同时,市面上所有的主流编程语言、框架、ORM 工具、监控系统都对 MySQL 有完美的支持。它的文档也非常丰富,学习资源唾手可得。 + +**第二,从核心技术功能上看,它非常强大且均衡。** + +- **强大的事务支持:** 这是它作为关系型数据库的立身之本。值得一提的是,InnoDB 默认的可重复读(REPEATABLE-READ)隔离级别,通过 MVCC 和 Next-Key Lock 机制,很大程度上避免了幻读问题,这在很多其他数据库中都需要更高的隔离级别才能做到,兼顾了性能和一致性。详细介绍可以阅读笔者写的这篇文章:[MySQL 事务隔离级别详解](https://javaguide.cn/database/mysql/transaction-isolation-level.html)。 +- **优秀的性能和可扩展性:** MySQL 本身经过了海量互联网业务的严酷考验,单机性能非常出色。更重要的是,它围绕着水平扩展,形成了一套非常成熟的架构方案,比如主从复制、读写分离、以及通过中间件实现的分库分表。这让它能够支撑从初创公司到大型互联网平台的各种规模的业务。 + +**第三,从运维和使用角度看,它非常‘亲民’。** + +- **开箱即用,上手简单:** 相比于 Oracle 等大型商业数据库,MySQL 的安装、配置和日常使用都非常简单直观,学习曲线平缓,对于开发者和初级 DBA 非常友好。 +- **维护成本低:** 由于其简单性和庞大的社区,找到相关的运维人才和解决方案都相对容易,整体的维护成本也更低。 + +值得一提的是最近几年,PostgreSQL 的势头很猛,甚至压过了 MySQL。网上出现了很多抨击诋毁 MySQL 的文章,笔者认为任何无脑抨击其中一方或者吹捧另外一方的行为都是不可取的。 + +笔者也写过一篇文章分享对这两个关系型数据库代表的看法,感兴趣的可以看看:[MySQL 被干成老二了?](https://mp.weixin.qq.com/s/APWD-PzTcTqGUuibAw7GGw)。 + +## MySQL 字段类型 + +MySQL 字段类型可以简单分为三大类: + +- **数值类型**:整型(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 等。 + +下面这张图不是我画的,忘记是从哪里保存下来的了,总结的还蛮不错的。 + +![MySQL 常见字段类型总结](https://oss.javaguide.cn/github/javaguide/mysql/summary-of-mysql-field-types.png) + +MySQL 字段类型比较多,我这里会挑选一些日常开发使用很频繁且面试常问的字段类型,以面试问题的形式来详细介绍。如无特殊说明,针对的都是 InnoDB 存储引擎。 + +另外,推荐阅读一下《高性能 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 + +`TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。 + +如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。 + +关于两者的详细对比以及日期存储类型选择建议,请参考我写的这篇文章: [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。 + +### ⭐️手机号存储用 INT 还是 VARCHAR? + +存储手机号,**强烈推荐使用 VARCHAR 类型**,而不是 INT 或 BIGINT。主要原因如下: + +1. **格式兼容性与完整性:** + - 手机号可能包含前导零(如某些地区的固话区号)、国家代码前缀('+'),甚至可能带有分隔符('-' 或空格)。INT 或 BIGINT 这种数字类型会自动丢失这些重要的格式信息(比如前导零会被去掉,'+' 和 '-' 无法存储)。 + - VARCHAR 可以原样存储各种格式的号码,无论是国内的 11 位手机号,还是带有国家代码的国际号码,都能完美兼容。 +2. **非算术性:**手机号虽然看起来是数字,但我们从不对它进行数学运算(比如求和、平均值)。它本质上是一个标识符,更像是一个字符串。用 VARCHAR 更符合其数据性质。 +3. **查询灵活性:** + - 业务中常常需要根据号段(前缀)进行查询,例如查找所有 "138" 开头的用户。使用 VARCHAR 类型配合 `LIKE '138%'` 这样的 SQL 查询既直观又高效。 + - 如果使用数字类型,进行类似的前缀匹配通常需要复杂的函数转换(如 CAST 或 SUBSTRING),或者使用范围查询(如 `WHERE phone >= 13800000000 AND phone < 13900000000`),这不仅写法繁琐,而且可能无法有效利用索引,导致性能下降。 +4. **加密存储的要求(非常关键):** + - 出于数据安全和隐私合规的要求,手机号这类敏感个人信息通常必须加密存储在数据库中。 +- The encrypted data (ciphertext) is a long string of characters (usually composed of letters, numbers, symbols, or Base64/Hex encoded), and the INT or BIGINT types cannot store this ciphertext at all. Only types such as VARCHAR, TEXT, or BLOB will do. + +**About VARCHAR length selection:** + +- **If stored without encryption (strongly not recommended!):** Considering international numbers and possible format characters, VARCHAR(20) to VARCHAR(32) is usually a relatively safe range, enough to cover the vast majority of mobile phone number formats around the world. VARCHAR(15) may not be sufficient for some numbers with country codes and formatting characters. +- **If storing encrypted (recommended standard practice):** The length must be accurately calculated and set based on the maximum length of the ciphertext produced by the chosen encryption algorithm, and possible encoding methods (e.g. Base64 will increase the length by about 1/3). Often longer VARCHAR lengths are required, such as VARCHAR(128), VARCHAR(256) or even longer. + +Finally, here is a table to summarize: + +| Comparing dimensions | VARCHAR type (recommended) | INT/BIGINT type (not recommended) | Description/Remarks | +|---------------- | ---------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------- | +| **Format Compatibility** | ✔ Can store leading zeros, "+", "-", spaces, etc. | ✘ Leading zeros are automatically lost and symbols cannot be stored | VARCHAR can store various mobile phone number formats as they are, INT/BIGINT only supports simple numbers, and leading zeros will disappear | +| **Integrity** | ✔ No format information is lost | ✘ Format information is lost | For example, "013800012345" stored in INT will become 13800012345, and "+" cannot be stored | +| **Non-arithmetic** | ✔ Suitable for storing "identifiers" | ✘ Only suitable for numerical operations | Mobile phone numbers are essentially string identifiers and do not perform mathematical operations. VARCHAR is more suitable for practical purposes | +| **Query flexibility** | ✔ Supports `LIKE '138%'`, etc. | ✘ Querying prefixes is inconvenient or has poor performance | Use VARCHAR to efficiently query by number segment/prefix, but the numeric type needs to be converted to a string or other complex processing | +| **Encrypted storage support** | ✔ Can store encrypted ciphertext (letters, symbols, etc.) | ✘ Unable to store ciphertext | After encrypting the mobile phone number, the ciphertext is a string/binary, only VARCHAR, TEXT, BLOB, etc. are compatible | +| **Length setting recommendations** | 15~20 (unencrypted), encryption depends on the situation | Meaningless | When not encrypted, VARCHAR(15~20) is universal, the encrypted length depends on the algorithm and encoding method | + +## MySQL Infrastructure + +> It is recommended to read this article [Execution Process of SQL Statements in MySQL](./how-sql-executed-in-mysql.md) to understand the MySQL infrastructure. In addition, "the execution flow of a SQL statement in MySQL" is also a frequently asked question in interviews. + +The following figure is a brief architecture diagram of MySQL. From the figure below, you can clearly see how a SQL statement on the client is executed inside MySQL. + +![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) + +As can be seen from the above figure, MySQL mainly consists of the following parts: + +- **Connector:** Identity authentication and permission related (when logging in to MySQL). +- **Query cache:** When executing a query statement, the cache will be queried first (removed after MySQL 8.0 version, because this function is not very practical). +- **Analyzer:** If the cache is not hit, the SQL statement will go through the analyzer. To put it bluntly, the analyzer needs to first see what your SQL statement is doing, and then check whether the syntax of your SQL statement is correct. +- **Optimizer:** Execute according to the solution considered optimal by MySQL. +- **Executor:** Execute the statement and return data from the storage engine. Before executing the statement, it will be judged whether there is permission. If there is no permission, an error will be reported. +- **Plug-in storage engine**: Mainly responsible for data storage and reading. It adopts a plug-in architecture and supports various storage engines such as InnoDB, MyISAM, and Memory. InnoDB is the default storage engine of MySQL. In most scenarios, using InnoDB is the best choice. + +## MySQL storage engine + +The core of MySQL lies in the storage engine. If you want to learn MySQL in depth, you must study the MySQL storage engine in depth. + +### What storage engines does MySQL support? Which one is used by default? + +MySQL supports multiple storage engines. You can view all storage engines supported by MySQL through the `SHOW ENGINES` command. + +![View all storage engines provided by MySQL](https://oss.javaguide.cn/github/javaguide/mysql/image-20220510105408703.png) + +From the picture above, we can see that MySQL's current default storage engine is InnoDB. Moreover, among all storage engines, only InnoDB is a transactional storage engine, which means that only InnoDB supports transactions. + +The MySQL version I use here is 8.x, and there may be differences between different MySQL versions. + +Prior to MySQL 5.5.5, MyISAM was the default storage engine for MySQL. After version 5.5.5, InnoDB is the default storage engine for MySQL. + +You can check your MySQL version with the `SELECT VERSION()` command. + +```bash +mysql> SELECT VERSION(); ++-----------+ +| VERSION() | ++-----------+ +| 8.0.27 | ++-----------+ +1 row in set (0.00 sec) +``` + +You can also directly view MySQL's current default storage engine through the `SHOW VARIABLES LIKE '%storage_engine%'` command. + +```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) +``` + +If you want to have an in-depth understanding of each storage engine and the differences between them, it is recommended that you read the following introduction in the official MySQL document (the interview will not ask such detailed questions, just understand it): + +- Detailed introduction to InnoDB storage engine: . +- Detailed introduction to other storage engines: . + +![](https://oss.javaguide.cn/github/javaguide/mysql/image-20220510155143458.png) + +### Do you understand the MySQL storage engine architecture? + +The MySQL storage engine adopts a **plug-in architecture** and supports multiple storage engines. We can even set up different storage engines for different database tables to meet the needs of different scenarios. **The storage engine is based on tables, not databases. ** + +The following diagram shows the MySQL architecture with pluggable storage engines: + +![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-index.md) 。 + +### 索引是什么? + +**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** + +索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 + +索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。 + +**索引的优点:** + +1. **查询速度起飞 (主要目的)**:通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。 +2. **保证数据唯一性**:通过创建**唯一索引 (Unique Index)**,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户 ID、邮箱等。主键本身就是一种唯一索引。 +3. **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。 + +**索引的缺点:** + +1. **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML 操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。 +2. **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。 +3. **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。 + +**那么,用了索引就一定能提高查询性能吗?** + +**不一定。** 大多数情况下,合理使用索引确实比全表扫描快得多。但也有例外: + +- **数据量太小**:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。 +- **查询结果集占比过大**:如果要查询的数据占了整张表的大部分(比如超过 20%-30%),优化器可能会认为全表扫描更划算,因为通过索引多次回表(随机 I/O)的成本可能高于一次顺序的全表扫描。 +- **索引维护不当或统计信息过时**:导致优化器做出错误判断。 + +### 索引为什么快? + +索引之所以快,核心原因是它**大大减少了磁盘 I/O 的次数**。 + +它的本质是一种**排好序的数据结构**,就像书的目录,让我们不用一页一页地翻(全表扫描)。 + +在 MySQL 中,这个数据结构是**B+树**。B+树结构主要从两方面做了优化: + +1. B+树的特点是“矮胖”,一个千万数据的表,索引树的高度可能只有 3-4 层。这意味着,最多只需要**3-4 次磁盘 I/O**,就能精确定位到我想要的数据,而全表扫描可能需要成千上万次,所以速度极快。 +2. B+树的叶子节点是**用链表连起来的**。找到开头后,就能顺着链表**顺序读**下去,这对磁盘非常友好,还能触发预读。 + +### MySQL 索引底层数据结构是什么? + +在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,详细介绍可以参考笔者写的这篇文章:[MySQL 索引详解](https://javaguide.cn/database/mysql/mysql-index.html)。 + +### 为什么 InnoDB 没有使用哈希作为索引的数据结构? + +> 我发现很多求职者甚至是面试官对这个问题都有误解,他们相当然的认为 MySQL 底层并没有使用哈希或者 B 树作为索引的数据结构。 +> +> 实际上,不论是提问还是回答这个问题都要区分好存储引擎。像 MEMORY 引擎就同时支持哈希和 B 树。 + +哈希索引的底层是哈希表。它的优点是,在进行**精确的等值查询**时,理论上时间复杂度是 **O(1)** ,速度极快。比如 `WHERE id = 123`。 + +但是,它有几个对于通用数据库来说是致命的缺点: + +1. **不支持范围查询:** 这是最主要的原因。哈希函数的一个特点是它会把相邻的输入值(比如 `id=100` 和 `id=101`)映射到哈希表中完全不相邻的位置。这种顺序的破坏,使得我们无法处理像 `WHERE age > 30` 或 `BETWEEN 100 AND 200`这样的范围查询。要完成这种查询,哈希索引只能退化为全表扫描。 +2. **不支持排序:** 同理,因为哈希值是无序的,所以我们无法利用哈希索引来优化 `ORDER BY` 子句。 +3. **不支持部分索引键查询:** 对于联合索引,比如`(col1, col2)`,哈希索引必须使用所有索引列进行查询,它无法单独利用 `col1` 来加速查询。 +4. **哈希冲突问题:** 当不同的键产生相同的哈希值时,需要额外的链表或开放寻址来解决,这会降低性能。 + +鉴于数据库查询中范围查询和排序是极其常见的操作,一个不支持这些功能的索引结构,显然不能作为默认的、通用的索引类型。 + +### 为什么 InnoDB 没有使用 B 树作为索引的数据结构? + +B 树和 B+树都是优秀的多路平衡搜索树,非常适合磁盘存储,因为它们都很“矮胖”,能最大化地利用每一次磁盘 I/O。 + +但 B+树是 B 树的一个增强版,它针对数据库场景做了几个关键优化: + +1. **I/O 效率更高:** 在 B+树中,只有叶子节点才存储数据(或数据指针),而非叶子节点只存储索引键。因为非叶子节点不存数据,所以它们可以容纳更多的索引键。这意味着 B+树的“扇出”更大,在同样的数据量下,B+树通常会比 B 树更矮,也就意味着查找数据所需的磁盘 I/O 次数更少。 +2. **查询性能更稳定:** 在 B+树中,任何一次查询都必须从根节点走到叶子节点才能找到数据,所以查询路径的长度是固定的。而在 B 树中,如果运气好,可能在非叶子节点就找到了数据,但运气不好也得走到叶子,这导致查询性能不稳定。 +3. **对范围查询极其友好:** 这是 B+树最核心的优势。它的所有叶子节点之间通过一个双向链表连接。当我们执行一个范围查询(比如 `WHERE id > 100`)时,只需要通过树形结构找到 `id=100` 的叶子节点,然后就可以沿着链表向后顺序扫描,而无需再回溯到上层节点。这使得范围查询的效率大大提高。 + +### 什么是覆盖索引? + +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。 + +在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。 + +**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。** + +### 请解释一下 MySQL 的联合索引及其最左前缀原则 + +使用表中的多个字段创建索引,就是 **联合索引**,也叫 **组合索引** 或 **复合索引**。 + +以 `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` 呢? `b = 1 AND a = 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`:和第二种一样的情况,整个索引都不会使用。 +4. 查询 `b=1 AND a=1 AND c=1`:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 `b=1` 和 `a=1` 的条件进行重排序,变成 `a=1 AND 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)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。 + +### SELECT \* 会导致索引失效吗? + +`SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖。 + +### 哪些字段适合创建索引? + +- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 +- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。 +- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。 +- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 +- **被经常频繁用于连接的字段**:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 + +### 索引失效的原因有哪些? + +1. 创建了组合索引,但查询条件未遵守最左匹配原则; +2. 在索引列上进行计算、函数、类型转换等操作; +3. 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`; +4. 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到; +5. IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同); +6. 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html "隐式转换"); + +## 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 日志 + +上诉问题的答案可以在[《Java 面试指北》(付费,点击链接领取优惠卷)](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 **「技术面试题篇」** 中找到。 + +![《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。 + +![脏读](https://oss.javaguide.cn/github/javaguide/database/mysql/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 的修改被丢失。 + +![丢失修改](https://oss.javaguide.cn/github/javaguide/database/mysql/concurrency-consistency-issues-missing-modifications.png) + +#### 不可重复读(Unrepeatable read) + +指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 + +例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。 + +![不可重复读](https://oss.javaguide.cn/github/javaguide/database/mysql/concurrency-consistency-issues-unrepeatable-read.png) + +#### 幻读(Phantom read) + +幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 + +例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。 + +![幻读](https://oss.javaguide.cn/github/javaguide/database/mysql/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 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是: + +- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。 +- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。 +- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。 +- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 + +| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | +| ---------------- | ----------------- | -------------------------------- | ---------------------- | +| READ UNCOMMITTED | √ | √ | √ | +| READ COMMITTED | × | √ | √ | +| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) | +| SERIALIZABLE | × | × | × | + +### MySQL 的默认隔离级别是什么? + +MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看: + +- MySQL 8.0 之前:`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 的隔离级别是基于锁实现的吗? + +MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 + +SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 + +## 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,即仅锁住索引本身,而不是范围。** + +### 共享锁和排他锁呢? + +不论是表级锁还是行级锁,都存在共享锁(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(推荐) 等开源项目就可以实现分布式文件服务。 + +**数据库只存储文件地址信息,文件由文件存储服务负责存储。** + +### MySQL 如何存储 IP 地址? + +可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。 + +MySQL 提供了两个方法来处理 ip 地址 + +- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位) +- `INET_NTOA()` :把整型的 ip 转为地址 + +插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。 + +### 有哪些常见的 SQL 优化手段? + +[《Java 面试指北》(付费)](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 **「技术面试题篇」** 有一篇文章详细介绍了常见的 SQL 优化手段,非常全面,清晰易懂! + +![常见的 SQL 优化手段](https://oss.javaguide.cn/javamianshizhibei/javamianshizhibei-sql-optimization.png) + +文章地址:https://www.yuque.com/snailclimb/mf2z3k/abc2sv (密码获取:)。 + +### 如何分析 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)的区别: +- Technology sharing | Isolation level: Correct understanding of phantom reading: +- MySQL Server Logs - MySQL 5.7 Reference Manual: +- Redo Log - MySQL 5.7 Reference Manual: +- Locking Reads - MySQL 5.7 Reference Manual: +- In-depth understanding of database row locks and table locks +- Detailed explanation of the role of intention locks in MySQL InnoDB: +- In-depth analysis of MySQL self-increasing lock: +- How should non-repeatable reads and phantom reads be distinguished in the database? : + + \ No newline at end of file diff --git a/docs_en/database/mysql/some-thoughts-on-database-storage-time.en.md b/docs_en/database/mysql/some-thoughts-on-database-storage-time.en.md new file mode 100644 index 00000000000..b748f03a788 --- /dev/null +++ b/docs_en/database/mysql/some-thoughts-on-database-storage-time.en.md @@ -0,0 +1,201 @@ +--- +title: MySQL日期类型选择建议 +category: 数据库 +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 日期类型性能分析, 数据库时间存储优化 +--- + +在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。 + +本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。 + +## 不要用字符串存储日期 + +和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。 + +但是,这是不正确的做法,主要会有下面两个问题: + +1. **空间效率**:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。 +2. **查询与计算效率低下**: + - **比较操作复杂且低效**:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。 + - **计算功能受限**:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。 + - **索引性能不佳**:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。 + +## DATETIME 和 TIMESTAMP 选择 + +`DATETIME` 和 `TIMESTAMP` 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢? + +下面我们从几个关键维度对它们进行对比: + +### 时区信息 + +`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。 + +**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。 + +**`TIMESTAMP` 和时区有关**。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 `TIMESTAMP` 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。 + +这意味着,对于同一条记录的 `TIMESTAMP` 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。 + +下面实际演示一下! + +建表 SQL 语句: + +```sql +CREATE TABLE `time_zone_test` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `date_time` datetime DEFAULT NULL, + `time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) 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` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。 + +In order to obtain predictable behavior and possibly reduce the conversion overhead of `TIMESTAMP`, it is recommended to manage time zones uniformly at the application level, or to explicitly set the `time_zone` parameter at the database connection/session level, rather than relying on the server's default or operating system time zone. + +## Are numeric timestamps a better choice? + +In addition to the above two types, integer types (`INT` or `BIGINT`) are also commonly used in practice to store the so-called "Unix timestamp" (that is, the total number of seconds, or milliseconds, from January 1, 1970 00:00:00 UTC to the target time). + +This storage method has some advantages of the `TIMESTAMP` type, and using it to perform date sorting and comparison operations will be more efficient, and it is also very convenient across systems. After all, it is just a stored value. The disadvantage is also obvious, that is, the readability of the data is too poor, and you cannot intuitively see the specific time. + +The timestamp is defined as follows: + +> The definition of timestamp is to count from a base time. This base time is "1970-1-1 00:00:00 +0:00". Starting from this time, it is expressed as an integer and measured in seconds. As time goes by, this time integer continues to increase. In this way, I only need one value to perfectly represent time, and this value is an absolute value, that is, no matter where you are in any corner of the earth, the timestamp representing time is the same, and the generated value is the same, and there is no concept of time zone. Therefore, no additional conversion is required during the time transmission in the system. It is converted to local time in string format only when displayed to the user. + +Actual operations in the database: + +```sql +-- Convert datetime string to Unix timestamp (seconds) +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) + +-- Convert Unix timestamp (seconds) to datetime format +mysql> SELECT FROM_UNIXTIME(1578707612); ++--------------------------+ +| FROM_UNIXTIME(1578707612) | ++--------------------------+ +| 2020-01-11 09:53:32 | ++--------------------------+ +1 row in set (0.01 sec) +``` + +## There is no DATETIME in PostgreSQL + +Since some readers mentioned the time type of PostgreSQL (PG), I will expand and add it here. The PG official document describes the time type at: . + +![PostgreSQL time type summary](https://oss.javaguide.cn/github/javaguide/mysql/pg-datetime-types.png) + +As you can see, PG does not have a type named `DATETIME`: + +- PG's `TIMESTAMP WITHOUT TIME ZONE` is functionally closest to MySQL's `DATETIME`. It stores the date and time, but does not contain any time zone information and stores literal values. +- PG's `TIMESTAMP WITH TIME ZONE` (or `TIMESTAMPTZ`) is equivalent to MySQL's `TIMESTAMP`. It converts the input value to UTC when stored and converted based on the current session's time zone when retrieved for display. + +For most application scenarios that require recording precise time points, `TIMESTAMPTZ` is the most recommended and robust choice in PostgreSQL because it can best handle time zone complexities. + +## Summary + +How to store time in MySQL? `DATETIME`? `TIMESTAMP`? Or a numerical timestamp? + +There is no silver bullet. Many programmers think that numeric timestamps are really good, efficient and compatible. However, many people think that it is not intuitive enough. + +The author of the magic book "High-Performance MySQL" recommends TIMESTAMP because numerical representation of time is not intuitive enough. The following is the original text: + + + +Each method has its own advantages, and the best way to choose is based on the actual scenario. Let's make a simple comparison of these three methods to help you choose the correct data type for storing time in actual development: + +| Type | Storage space | Date format | Date range | Whether to include time zone information | +| -------------------------- | -------- | ----------------------------- | --------------------------------------------------------------- | ------------- | +| DATETIME | 5~8 bytes | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | No | +| TIMESTAMP | 4~7 bytes | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | Yes | +| Numeric timestamp | 4 bytes | Full number such as 1578707612 | Time after 1970-01-01 00:00:01 | No | + +**Summary of selection suggestions:** + +- The core strength of `TIMESTAMP` is its built-in time zone handling capabilities. The database takes care of UTC storage and automatic conversion based on the session time zone, simplifying the development of applications that need to handle multiple time zones. If your application needs to handle multiple time zones, or you want the database to automatically manage time zone conversions, `TIMESTAMP` is a natural choice (note its time range limit, aka the 2038 problem). +- If the application scenario does not involve time zone conversion, or you want the application to have full control over the time zone logic, and need to represent time after 2038, `DATETIME` is a safer choice. +- Numeric timestamps are a powerful option if comparison performance is of paramount concern, or if time data needs to be passed frequently across systems and the sacrifice of readability (or always conversion at the application layer) is acceptable. + + \ No newline at end of file diff --git a/docs_en/database/mysql/transaction-isolation-level.en.md b/docs_en/database/mysql/transaction-isolation-level.en.md new file mode 100644 index 00000000000..e0dce97f95d --- /dev/null +++ b/docs_en/database/mysql/transaction-isolation-level.en.md @@ -0,0 +1,123 @@ +--- +title: Detailed explanation of MySQL transaction isolation level +category: database +tag: + -MySQL +head: + - - meta + - name: keywords + content: transaction, isolation level, read uncommitted, read committed, repeatable read, serializable, MVCC, lock + - - meta + - name: description + Content: Sort out the four major transaction isolation levels and concurrency phenomena, combine InnoDB's MVCC and lock mechanism, and clarify the response strategy for phantom reads/non-repeatable reads. +--- + +> This article was jointly written by [SnailClimb](https://github.com/Snailclimb) and [guang19](https://github.com/guang19). + +For an introduction to the basic overview of transactions, please read the introduction of this article: [MySQL common knowledge points & summary of interview questions] (./mysql-questions-01.md#MySQL-Transactions) + +## Transaction isolation level summary + +The SQL standard defines four transaction isolation levels to balance transaction isolation and concurrency performance. The higher the level, the better the data consistency, but the concurrency performance may be lower. The four levels are: + +- **READ-UNCOMMITTED (read uncommitted)**: The lowest isolation level, allowing reading of uncommitted data changes, which may lead to dirty reads, phantom reads, or non-repeatable reads. This level is rarely used in practical applications because its guarantee of data consistency is too weak. +- **READ-COMMITTED (read committed)**: Allows reading of data that has been committed by concurrent transactions, which can prevent dirty reads, but phantom reads or non-repeatable reads may still occur. This is the default isolation level for most databases (such as Oracle, SQL Server). +- **REPEATABLE-READ (repeatable read)**: The results of multiple reads of the same field are consistent, unless the data is modified by the own transaction itself. Dirty reads and non-repeatable reads can be prevented, but phantom reads may still occur. The default isolation level of the MySQL InnoDB storage engine is REPEATABLE READ. Moreover, InnoDB solves the phantom read problem to a large extent through the MVCC (Multi-version Concurrency Control) and Next-Key Locks (gap lock + row lock) mechanisms at this level. +- **SERIALIZABLE**: The highest isolation level, fully compliant with ACID isolation level. All transactions are executed one after another in order, so that there is no possibility of interference between transactions. In other words, this level can prevent dirty reads, non-repeatable reads and phantom reads. + +| Isolation level | Dirty Read | Non-Repeatable Read | Phantom Read | +|---------------- | ------------------ | ---------------------------------- | ----------------------- | +| READ UNCOMMITTED | √ | √ | √ | +| READ COMMITTED | × | √ | √ | +| REPEATABLE READ | × | × | √ (Standard) / ≈× (InnoDB) | +| SERIALIZABLE | × | × | × | + +**Default level query:** + +The default isolation level for the MySQL InnoDB storage engine is **REPEATABLE READ**. You can view it with the following command: + +- Before MySQL 8.0: `SELECT @@tx_isolation;` +- MySQL 8.0 and later: `SELECT @@transaction_isolation;` + +```bash +mysql> SELECT @@transaction_isolation; ++------------------------+ +| @@transaction_isolation | ++------------------------+ +| REPEATABLE-READ | ++------------------------+ +``` + +**InnoDB's REPEATABLE READ handles phantom reads:** + +In the standard SQL isolation level definition, REPEATABLE READ cannot prevent phantom reads. However, the implementation of InnoDB largely avoids phantom reads through the following mechanisms: + +- **Snapshot Read**: Ordinary SELECT statement, implemented through the **MVCC** mechanism. A data snapshot is created when a transaction starts, and subsequent snapshot reads read this version of the data, thereby avoiding seeing newly inserted rows (phantom reads) or modified rows (non-repeatable reads) by other transactions. +- **Current Read**: Operations like `SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `INSERT`, `UPDATE`, `DELETE`. InnoDB uses **Next-Key Lock** to lock the scanned index records and the range (gap) between them to prevent other transactions from inserting new records in this range, thereby avoiding phantom reads. Next-Key Lock is a combination of Record Lock and Gap Lock. + +It is worth noting that although it is generally believed that the higher the isolation level, the worse the concurrency, but the InnoDB storage engine optimizes the REPEATABLE READ level through the MVCC mechanism. For many common read-only or read-more-write-less scenarios, there may not be a significant performance difference compared to READ COMMITTED. However, in write-intensive scenarios with high concurrency conflicts, RR's gap lock mechanism may cause more lock waits than RC. + +In addition, in some specific scenarios, such as distributed transactions that require strict consistency (XA Transactions), InnoDB may require or recommend the use of SERIALIZABLE isolation level to ensure the consistency of global data. + +Chapter 7.7 of "MySQL Technology Insider: InnoDB Storage Engine (2nd Edition)" reads: + +> The InnoDB storage engine provides support for XA transactions and supports the implementation of distributed transactions through XA transactions. Distributed transactions refer to allowing multiple independent transactional resources to participate in a global transaction. Transactional resources are typically relational database systems, but can be other types of resources. Global transactions require all participating transactions to either commit or roll back, which improves the original ACID requirements for transactions. In addition, when using distributed transactions, the transaction isolation level of the InnoDB storage engine must be set to SERIALIZABLE. + +## Actual situation demonstration + +Below I will use two command lines MySQL to simulate the problem of dirty reading of the same data by multiple threads (multiple transactions). + +In the default configuration of the MySQL command line, transactions are automatically committed, that is, the COMMIT operation will be performed immediately after executing the SQL statement. If you want to explicitly start a transaction, you need to use the command: `START TRANSACTION`. + +We can set the isolation level with the following command. + +```sql +SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE] +``` + +Let’s take a look at some of the concurrency control statements we use in the following actual operations: + +- `START TRANSACTION` |`BEGIN`: Explicitly start a transaction. +- `COMMIT`: Commits the transaction, making all modifications to the database permanent. +- `ROLLBACK`: Rollback ends the user's transaction and undoes all uncommitted modifications in progress. + +### Dirty read (read uncommitted) + +![]() + +### Avoid dirty reads (read committed) + +![](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) + +### Non-repeatable read + +It is still the same as the read committed picture above. Although it avoids reading uncommitted, the non-repeatable read problem occurs before a transaction is completed. + +![](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) + +### Repeatable read![](https://oss.javaguide.cn/github/javaguide/2019-33-2%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB.jpg) + +### Phantom reading + +#### Demonstrate the occurrence of phantom reading + +![](https://oss.javaguide.cn/github/javaguide/phantom_read.png) + +When SQL Script 1 queries for the first time, there is only one record with a salary of 500. SQL Script 2 inserts a record with a salary of 500. After submission, SQL Script 1 uses the current read query again in the same transaction and finds that there are two records with a salary of 500. This is a phantom read. + +#### How to solve phantom reading + +There are many ways to solve phantom reads, but their core idea is that when one transaction is operating data in a certain table, another transaction is not allowed to add or delete data in this table. The main ways to solve phantom reading are as follows: + +1. Adjust the transaction isolation level to `SERIALIZABLE`. +2. At the repeatable read transaction level, add a table lock to the table in the transaction operation. +3. Under the transaction level of repeatable read, add `Next-key Lock (Record Lock+Gap Lock)` to the table of transaction operation. + +### Reference + +- "MySQL Technology Insider: InnoDB Storage Engine" +- +- [Mysql Lock: Seven Questions of the Soul](https://tech.youzan.com/seven-questions-about-the-lock-of-MySQL/) +- [Relationship between transaction isolation level and locks in Innodb](https://tech.meituan.com/2014/08/20/innodb-lock.html) + + \ No newline at end of file diff --git a/docs_en/database/nosql.en.md b/docs_en/database/nosql.en.md new file mode 100644 index 00000000000..5428c5d9152 --- /dev/null +++ b/docs_en/database/nosql.en.md @@ -0,0 +1,68 @@ +--- +title: Summary of NoSQL basic knowledge +category: database +tag: + - NoSQL + - MongoDB + - Redis +head: + - - meta + - name: keywords + content: NoSQL, key-value, document, column family, graph database, distributed, scalability, data model + - - meta + - name: description + content: Summarizes the classification and characteristics of NoSQL, compares relational databases, combines distributed and scalable scenarios, and guides model and selection. +--- + +## What is NoSQL? + +NoSQL (abbreviation for Not Only SQL) generally refers to non-relational databases, mainly targeting key-value, document and graph type data storage. Moreover, NoSQL databases inherently support features such as distribution, data redundancy and data sharding, aiming to provide scalable, highly available and high-performance data storage solutions. + +A common misconception is that NoSQL databases, or non-relational databases, do not store relational data well. NoSQL databases can store relational data—they store it differently than relational databases do. + +NoSQL database representatives: HBase, Cassandra, MongoDB, Redis. + +![](https://oss.javaguide.cn/github/javaguide/database/mongodb/sql-nosql-tushi.png) + +## What is the difference between SQL and NoSQL? + +| | SQL database | NoSQL database | +| :---------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Data storage model | Structured storage, tables with fixed rows and columns | Unstructured storage. Document: JSON document, Key-value: key-value pairs, Wide-column: Table with rows and dynamic columns, Graph: Nodes and Edges | +| Development History | Developed in the 1970s with a focus on reducing data duplication | Developed in the late 2000s with a focus on improving scalability and reducing storage costs for large-scale data | +| Examples | Oracle, MySQL, Microsoft SQL Server, PostgreSQL | Documents: MongoDB, CouchDB, Keys: Redis, DynamoDB, Wide columns: Cassandra, HBase, Charts: Neo4j, Amazon Neptune, Giraph | +| ACID properties | Provide atomicity, consistency, isolation, and durability (ACID) properties | Generally do not support ACID transactions, trade-offs are made for scalability and high performance, and a few support such as MongoDB. However, MongoDB's support for ACID transactions is still different from MySQL. | +| Performance | Performance often depends on the disk subsystem. For optimal performance, queries, indexes, and table structures often need to be optimized. | Performance is typically determined by the size of the underlying hardware cluster, network latency, and the calling application. | +| Expansion | Vertical (use more powerful servers for expansion), read-write separation, database and table sharding | Horizontal (horizontal expansion by adding servers, usually based on sharding mechanism) | +| Purpose | Data storage for common enterprise-level projects | Wide range of uses, such as graph databases that support analysis and traversal of relationships between connected data, and key-value databases that can handle large amounts of data expansion and extremely high state changes | +| Query syntax | Structured Query Language (SQL) | Data access syntax may vary from database to database | + +## What are the advantages of NoSQL databases? + +NoSQL databases are ideal for many modern applications, such as mobile, web, and gaming applications, which require flexible, scalable, high-performance, and powerful databases to provide a superior user experience. + +- **Flexibility:** NoSQL databases often offer flexible architectures to enable faster, more iterative development. Flexible data models make NoSQL databases ideal for semi-structured and unstructured data. +- **Scalability:** NoSQL databases are typically designed to scale horizontally through the use of distributed hardware clusters, rather than vertically by adding expensive and powerful servers. +- **High Performance:** NoSQL databases are optimized for specific data models and access patterns, which allows for higher performance than trying to accomplish similar functions with relational databases. +- **Powerful Features:** NoSQL databases provide powerful APIs and data types specifically built for their respective data models. + +## What types of NoSQL databases are there? + +NoSQL databases can be mainly divided into the following four types: + +- **Key-value**: A key-value database is a simpler database where each item contains a key and a value. This is an extremely flexible NoSQL database type because the application has complete control over what is stored in the value field without any restrictions. Redis and DynanoDB are two very popular key-value databases. +- **Document**: Data in the document database is stored in documents similar to JSON (JavaScript Object Notation) objects, which is very clear and intuitive. Each document contains pairs of fields and values. These values ​​can typically be of various types, including strings, numbers, Boolean values, arrays, or objects, and their structure is usually consistent with the objects developers use in their code. MongoDB is a very popular document database. +- **Graph**: Graph databases are designed to easily build and run applications that work with highly connected data sets. Typical use cases for graph databases include social networks, recommendation engines, fraud detection, and knowledge graphs. Neo4j and Giraph are two very popular graph databases. +- **Wide Column**: Wide column storage database is very suitable for storing large amounts of data. Cassandra and HBase are two very popular wide column storage databases. + +The picture below comes from [Microsoft's official documentation | Relational data and NoSQL data](https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/relational-vs-nosql-data). + +![NoSQL data model](https://oss.javaguide.cn/github/javaguide/database/mongodb/types-of-nosql-datastores.png) + +## Reference + +- What is NoSQL? - MongoDB official documentation: +- What is NoSQL? - AWS: +- NoSQL vs. SQL Databases - MongoDB official documentation: + + \ No newline at end of file diff --git a/docs_en/database/redis/3-commonly-used-cache-read-and-write-strategies.en.md b/docs_en/database/redis/3-commonly-used-cache-read-and-write-strategies.en.md new file mode 100644 index 00000000000..83e894ff4f1 --- /dev/null +++ b/docs_en/database/redis/3-commonly-used-cache-read-and-write-strategies.en.md @@ -0,0 +1,126 @@ +--- +title: 3种常用的缓存读写策略详解 +category: 数据库 +tag: + - Redis +head: + - - meta + - name: keywords + content: 缓存读写策略,Cache Aside,Read Through,Write Through,一致性,失效 + - - meta + - name: description + content: 总结三种常见缓存读写策略及适用场景,分析一致性与失效处理,指导业务选型与问题规避。 +--- + +看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的 3 种读写策略**”的时候却一脸懵逼。 + +在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单写了一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 + +但是,搞懂 3 种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的! + +**下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。** + +### Cache Aside Pattern(旁路缓存模式) + +**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** + +Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。 + +下面我们来看一下这个策略模式下的缓存读写步骤。 + +**写**: + +- 先更新 db +- 然后直接删除 cache 。 + +简单画了一张图帮助大家理解写的步骤。 + +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png) + +**读** : + +- 从 cache 中读取数据,读取到就直接返回 +- cache 中读取不到的话,就从 db 中读取数据返回 +- 再把数据放到 cache 中。 + +简单画了一张图帮助大家理解读的步骤。 + +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png) + +你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。 + +比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 cache ,后更新 db 么?**” + +**答案:** 那肯定是不行的!因为这样可能会造成 **数据库(db)和缓存(Cache)数据不一致**的问题。 + +举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。 + +这个过程可以简单描述为: + +> 请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新 + +当你这样回答之后,面试官可能会紧接着就追问:“**在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?**” + +**答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。 + +举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。 + +这个过程可以简单描述为: + +> 请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache + +现在我们再来分析一下 **Cache Aside Pattern 的缺陷**。 + +**缺陷 1:首次请求数据一定不在 cache 的问题** + +解决办法:可以将热点数据可以提前放入 cache 中。 + +**缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。** + +解决办法: + +- 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。 +- 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。 + +### Read/Write Through Pattern(读写穿透) + +Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。 + +这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。 + +**写(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 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。 + + + diff --git a/docs_en/database/redis/cache-basics.en.md b/docs_en/database/redis/cache-basics.en.md new file mode 100644 index 00000000000..7e77ced0782 --- /dev/null +++ b/docs_en/database/redis/cache-basics.en.md @@ -0,0 +1,21 @@ +--- +title: Summary of common interview questions on caching basics (paid) +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: cache interview, consistency, elimination strategy, penetration, avalanche, hotspot, architecture + - - meta + - name: description + content: Collects high-frequency questions on caching foundation and architecture, covering consistency and elimination strategies, penetration/avalanche and other issues and management solutions, and builds a system review list. +--- + +**Caching Basics** The relevant interview questions are exclusive to my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link to view the detailed introduction and how to join), which has been compiled into ["Java Interview Guide"](../../zhuanlan/java-mian-shi-zhi-bei.md). + +![](https://oss.javaguide.cn/javamianshizhibei/database-questions.png) + + + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-cluster.en.md b/docs_en/database/redis/redis-cluster.en.md new file mode 100644 index 00000000000..327f4e03831 --- /dev/null +++ b/docs_en/database/redis/redis-cluster.en.md @@ -0,0 +1,14 @@ +--- +title: Detailed explanation of Redis cluster (paid) +category: database +tag: + - Redis +--- + +**Redis Cluster** The relevant interview questions are my [Knowledge Planet](../../about-the-author/zhishixingqiu-two-years.md) (click the link to view the detailed introduction and how to join) exclusive content, which has been compiled into ["Java Interview Guide North"](../../zhuanlan/java-mian-shi-zhi-bei.md). + +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-database.png) + + + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-common-blocking-problems-summary.en.md b/docs_en/database/redis/redis-common-blocking-problems-summary.en.md new file mode 100644 index 00000000000..4cf4c445b74 --- /dev/null +++ b/docs_en/database/redis/redis-common-blocking-problems-summary.en.md @@ -0,0 +1,178 @@ +--- +title: Redis常见阻塞原因总结 +category: 数据库 +tag: + - Redis +--- + +> 本文整理完善自: ,作者:阿 Q 说代码 + +这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意! + +## O(n) 命令 + +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) 的时间复杂度更小。 +- …… + +## SAVE 创建 RDB 快照 + +Redis 提供了两个命令来生成 RDB 快照文件: + +- `save` : 同步保存操作,会阻塞 Redis 主线程; +- `bgsave` : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。 + +默认情况下,Redis 默认配置会使用 `bgsave` 命令。如果手动使用 `save` 命令生成 RDB 快照文件的话,就会阻塞主线程。 + +## AOF + +### AOF 日志记录阻塞 + +Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同。 + +![AOF 记录日志过程](https://oss.javaguide.cn/github/javaguide/database/redis/redis-aof-write-log-disc.png) + +**为什么是在执行完命令之后记录日志呢?** + +- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查; +- 在命令执行完之后再记录,不会阻塞当前的命令执行。 + +这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过): + +- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失; +- **可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)**。 + +### AOF 刷盘阻塞 + +开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 `server.aof_buf` 中,然后再根据 `appendfsync` 配置来决定何时将其同步到硬盘中的 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` 的时机由操作系统决定)。 + +当后台线程( `aof_fsync` 线程)调用 `fsync` 函数同步 AOF 文件时,需要等待,直到写入完成。当磁盘压力太大的时候,会导致 `fsync` 操作发生阻塞,主线程调用 `write` 函数时也会被阻塞。`fsync` 完成后,主线程执行 `write` 才能成功返回。 + +关于 AOF 工作流程的详细介绍可以查看:[Redis 持久化机制详解](./redis-persistence.md),有助于理解 AOF 刷盘阻塞。 + +### AOF 重写阻塞 + +1. fork 出一条子线程来将文件重写,在执行 `BGREWRITEAOF` 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。 +2. 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。 +3. 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 + +阻塞就是出现在第 2 步的过程中,将缓冲区中新数据写到新文件的过程中会产生**阻塞**。 + +相关阅读:[Redis AOF 重写阻塞问题分析](https://cloud.tencent.com/developer/article/1633077)。 + +## 大 Key + +如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准: + +- string 类型的 value 超过 1MB +- 复合类型(列表、哈希、集合、有序集合等)的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 + +大 key 造成的阻塞问题如下: + +- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 +- 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 +- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 + +### 查找大 key + +当我们在使用 Redis 自带的 `--bigkeys` 参数查找大 key 时,最好选择在从节点上执行该命令,因为主节点上执行时,会**阻塞**主节点。 + +- 我们还可以使用 SCAN 命令来查找大 key; + +- 通过分析 RDB 文件来找出 big key,这种方案的前提是 Redis 采用的是 RDB 持久化。网上有现成的工具: + +- - redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具 + - rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。 + +### 删除大 key + +删除操作的本质是要释放键值对占用的内存空间。 + +释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,**操作系统需要把释放掉的内存块插入一个空闲内存块的链表**,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会**阻塞**当前释放内存的应用程序。 + +所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。 + +删除大 key 时建议采用分批次删除和异步删除的方式进行。 + +## 清空数据库 + +清空数据库和上面 bigkey 删除也是同样道理,`flushdb`、`flushall` 也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点。 + +## 集群扩容 + +Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。 + +在扩缩容的时候,需要进行数据迁移。而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。 + +When performing migration, Redis at both ends will enter a blocking state of varying lengths. For small keys, this time can be ignored. However, if the memory usage of the key is too large, in severe cases, it will trigger a failover within the cluster, causing unnecessary switching. + +## Swap (memory swap) + +**What is Swap? ** Swap literally means exchange. Swap in Linux is often called memory swap or swap partition. Similar to virtual memory in Windows, when memory is insufficient, part of the hard disk space is virtualized into memory to solve the problem of insufficient memory capacity. Therefore, the role of the Swap partition is to sacrifice the hard disk and increase the memory to solve the problem of insufficient or full VPS memory. + +Swap is very fatal for Redis. An important prerequisite for Redis to ensure high performance is that all data is in memory. If the operating system swaps part of the memory used by Redis out of the hard disk, the performance of Redis will drop sharply after the swap because the read and write speeds of the memory and the hard disk are several orders of magnitude different. + +The check method to identify Swap in Redis is as follows: + +1. Query the Redis process number + +```bash +redis-cli -p 6383 info server | grep process_id +process_id: 4476 +``` + +2. Query memory swap information based on process number + +```bash +cat /proc/4476/smaps | grep Swap +Swap: 0kB +Swap: 0kB +Swap: 4kB +Swap: 0kB +Swap: 0kB +..... +``` + +If the swap volume is all 0KB or some are 4KB, it is normal. + +Methods to prevent memory swapping: + +- Ensure that the machine has sufficient available memory +- Ensure that all Redis instances are set to the maximum available memory (maxmemory) to prevent uncontrollable growth of Redis memory in extreme situations +- Reduce the system's swap priority, such as `echo 10 > /proc/sys/vm/swappiness` + +## CPU competition + +Redis is a typical CPU-intensive application and is not recommended to be deployed together with other multi-core CPU-intensive services. When other processes consume the CPU excessively, it will seriously affect the throughput of Redis. + +You can get the current Redis usage through `redis-cli --stat`. Use the `top` command to obtain information such as the CPU utilization of the process. Use the `info commandstats` statistical information to analyze the command's unreasonable overhead time and check whether it is due to high algorithm complexity or excessive memory optimization. + +## Network problem + +Network problems such as connection rejection, network delay, and network card soft interruption may also cause Redis to block. + +## Reference + +- Analysis and summary of 6 major scenarios of Redis blocking: +- Redis development and operation notes - Redis nightmare - blocking: + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-data-structures-01.en.md b/docs_en/database/redis/redis-data-structures-01.en.md new file mode 100644 index 00000000000..735de4294cb --- /dev/null +++ b/docs_en/database/redis/redis-data-structures-01.en.md @@ -0,0 +1,499 @@ +--- +title: Detailed explanation of the 5 basic data types of Redis +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis common data types + - - meta + - name: description + content: Summary of Redis basic data types: String (string), List (list), Set (set), Hash (hash), Zset (ordered set) +--- + +Redis has 5 basic data types: String (string), List (list), Set (collection), Hash (hash), and Zset (ordered set). + +These five data types are directly provided to users and are the storage form of data. Their underlying implementation mainly relies on these eight data structures: simple dynamic string (SDS), LinkedList (double linked list), Dict (hash table/dictionary), SkipList (skip list), Intset (integer collection), ZipList (compressed list), QuickList (quick list). + +The underlying data structure corresponding to Redis's five basic data types is implemented as shown in the following table: + +| String | List | Hash | Set | Zset | +| :----- | :--------------------------- | :------------ | :----------- | :---------------- | +| SDS | LinkedList/ZipList/QuickList | Dict, ZipList | Dict, Intset | ZipList, SkipList | + +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 became QuickList. Starting from Redis 7.0, ZipList is replaced by ListPack. + +You can find a very detailed introduction to Redis data types/structures on the Redis official 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/) + +In the future, with the release of new versions of Redis, new data structures may appear. By consulting the corresponding introduction on the Redis official website, you can always get the most reliable information. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720181630203.png) + +## String + +### Introduction + +String is the simplest and most commonly used data type in Redis. + +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 the path of images), and serialized objects. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124403897.png) + +Although Redis is written in C language, Redis does not use C's string representation. Instead, it builds its own **Simple Dynamic String** (**SDS**). Compared with C's native strings, Redis's SDS can save not only text data but also binary data, and the complexity of obtaining the string length is O(1) (C string is O(N)). In addition, Redis's SDS API is safe and will not cause buffer overflow. + +### Common commands + +| Commands | Introduction | +| ---------------------------------- | ---------------------------------- | +| SET key value | Set the value of the specified key | +| SETNX key value | Set the value of key only if the key does not exist | +| GET key | Get the value of the specified key | +| MSET key1 value1 key2 value2 …… | Set the value of one or more specified keys | +| MGET key1 key2 ... | Get the value of one or more specified keys | +| STRLEN key | Returns the length of the string value stored in key | +| INCR key | Increment the numeric value stored in key by one | +| DECR key | Decrement the numeric value stored in key by one | +| EXISTS key | Determine whether the specified key exists | +| DEL key (general) | Delete the specified key | +| EXPIRE key seconds (general) | Set the expiration time for the specified key | + +For more Redis String commands and detailed usage guides, please view the corresponding introduction on the Redis official website: . + +**Basic Operation**: + +```bash +> SET key value +OK +> GET key +"value" +> EXISTS key +(integer) 1 +> STRLEN key +(integer) 5 +> DEL key +(integer) 1 +> GET key +(nil) +``` + +**Batch Settings**: + +```bash +> MSET key1 value1 key2 value2 +OK +> MGET key1 key2 # Get the values corresponding to multiple keys in batches +1) "value1" +2) "value2" +``` + +**Counter (can be used when the content of the string is an integer):** + +```bash +>SET number 1 +OK +> INCR number # Increase the number value stored in key by one +(integer) 2 +> GET number +"2" +> DECR number # Decrement the numeric value stored in key by one +(integer) 1 +> GET number +"1" +``` + +**Set expiration time (default is never expires)**: + +```bash +> EXPIRE key 60 +(integer) 1 +> SETEX key 60 value # Set value and set expiration time +OK +> TTL key +(integer) 56 +``` + +### Application scenarios + +**Scenarios where regular data needs to be stored** + +- Example: Cache Session, Token, image address, serialized object (more memory-saving than Hash storage). +- Related commands: `SET`, `GET`. + +**Scenarios that require counting** + +- Examples: the number of user requests per unit time (simple current limiting can be used), the number of page visits per unit time. +- Related commands: `SET`, `GET`, `INCR`, `DECR`. + +**Distributed Lock** + +The simplest distributed lock can be implemented using the `SETNX key value` command (there are some flaws, and it is generally not recommended to implement distributed locks this way). + +## List + +### Introduction + +List in Redis is actually the implementation of the linked list data structure. I introduced the linked list data structure in detail in this article [Linear Data Structure: Array, Linked List, Stack, Queue](https://javaguide.cn/cs-basics/data-structure/linear-data-structure.html), so I won’t go into details here. + +Many high-level programming languages ​​have built-in implementations of linked lists, such as `LinkedList` in Java, but the C language does not implement linked lists, so Redis implements its own linked list data structure. The implementation of Redis's List is a **doubly linked list**, which can support reverse search and traversal, making it more convenient to operate, but it brings some additional memory overhead. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124413287.png) + +### Common commands + +| Commands | Introduction ||-------------------------------- |------------------------------------------------ | +| RPUSH key value1 value2 ... | Add one or more elements to the tail (right) of the specified list | +| LPUSH key value1 value2 ... | Add one or more elements to the head (left) of the specified list | +| LSET key index value | Set the value at the specified list index position to value | +| LPOP key | Remove and get the first element (leftmost) of the specified list | +| RPOP key | Remove and get the last element (rightmost) of the specified list | +| LLEN key | Get the number of list elements | +| LRANGE key start end | Get the elements between start and end of the list | + +For more Redis List commands and detailed usage guides, please view the corresponding introduction on the Redis official website: . + +**Queue implementation via `RPUSH/LPOP` or `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" +``` + +**Implementing the stack via `RPUSH/RPOP` or `LPUSH/LPOP`**: + +```bash +>RPUSH myList2 value1 value2 value3 +(integer) 3 +> RPOP myList2 # Take out the rightmost element of the list +"value3" +``` + +I specially drew a picture to help everyone understand the `RPUSH`, `LPOP`, `LPUSH`, `RPOP` commands: + +![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-list.png) + +**View the list elements corresponding to the subscript range through `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" +``` + +Through the `LRANGE` command, you can implement paging query based on List, and the performance is very high! + +**View the length of the linked list through `LLEN`: + +```bash +> LLEN myList +(integer) 3 +``` + +### Application scenarios + +**Information flow display** + +- Examples: latest articles, latest developments. +- Related commands: `LPUSH`, `LRANGE`. + +**Message Queue** + +`List` can be used as a message queue, but its function is too simple and has many flaws, so it is not recommended. + +Relatively speaking, `Stream`, a newly added data structure in Redis 5.0, is more suitable for message queues, but its function is still very crude. Compared with professional message queues, there are still many shortcomings, such as message loss and accumulation problems that are difficult to solve. + +## Hash + +### Introduction + +Hash in Redis is a String type field-value (key-value pair) mapping table, which is particularly suitable for storing objects. During subsequent operations, you can directly modify the values ​​of certain fields in this object. + +Hash is similar to `HashMap` before JDK1.8, and the internal implementation is similar (array + linked list). However, Redis's Hash has been optimized more. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124421703.png) + +### Common commands + +| Commands | Introduction | +|------------------------------------------------ |---------------------------------------------------------------- | +| HSET key field value | Set the value of the specified field in the specified hash table | +| HSETNX key field value | Set the value of the specified field only if the specified field does not exist | +| HMSET key field1 value1 field2 value2 ... | Set one or more field-value (field-value) pairs into the specified hash table at the same time | +| HGET key field | Get the value of the specified field in the specified hash table | +| HMGET key field1 field2 ... | Get the value of one or more specified fields in the specified hash table | +| HGETALL key | Get all key-value pairs in the specified hash table | +| HEXISTS key field | Check whether the specified field exists in the specified hash table | +| HDEL key field1 field2 ... | Delete one or more hash table fields | +| HLEN key | Get the number of fields in the specified hash table | +| HINCRBY key field increment | Perform operations on the specified field in the specified hash (positive numbers are added, negative numbers are subtracted) | + +For more Redis Hash commands and detailed usage guides, please view the corresponding introduction on the Redis official website: . + +**Mock object data storage**: + +```bash +> HMSET userInfoKey name "guide" description "dev" age 24 +OK +> HEXISTS userInfoKey name # Check whether the field specified in the value corresponding to key exists. +(integer) 1 +> HGET userInfoKey name # Get the value of the specified field stored in the hash table. +"guide" +> HGET userInfoKey age +"24" +> HGETALL userInfoKey # Get all fields and values of the specified key in the hash table +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 +``` + +### Application scenarios + +**Object data storage scenario** + +- Examples: user information, product information, article information, shopping cart information. +- Related commands: `HSET` (set the value of a single field), `HMSET` (set the value of multiple fields), `HGET` (get the value of a single field), `HMGET` (get the value of multiple fields). + +## Set + +### Introduction + +The Set type in Redis is an unordered collection. The elements in the collection are not in order but are unique, somewhat similar to `HashSet` in Java. Set is a good choice when you need to store a list of data and do not want duplicate data, and Set provides an important interface for determining whether an element is in a Set collection, which List cannot provide. + +You can easily implement intersection, union, and difference operations based on Set. For example, you can store all the followers of a user in one set and all their fans in one set. In this case, Set can very conveniently implement functions such as common following, common fans, and common preferences. This process is also the process of finding intersection.![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124430264.png) + +### Common commands + +| Commands | Introduction | +|---------------------------------------- | ------------------------------------------ | +| SADD key member1 member2 ... | Add one or more elements to the specified collection | +| SMEMBERS key | Get all elements in the specified collection | +| SCARD key | Get the number of elements of the specified collection | +| SISMEMBER key member | Determine whether the specified element is in the specified set | +| SINTER key1 key2 ... | Get the intersection of all given sets | +| SINTERSTORE destination key1 key2 ... | Stores the intersection of all the given sets in destination | +| SUNION key1 key2 ... | Get the union of all given sets | +| SUNIONSTORE destination key1 key2 ... | Stores the union of all the given sets in destination | +| SDIFF key1 key2 ... | Get the difference set of all given sets | +| SDIFFSTORE destination key1 key2 ... | Stores the difference of all given sets in destination | +| SPOP key count | Randomly remove and obtain one or more elements in the specified collection | +| SRANDMEMBER key count | Randomly obtain the specified number of elements in the specified collection | + +For more Redis Set commands and detailed usage guides, please view the corresponding introduction on the Redis official website: . + +**Basic Operation**: + +```bash +> SADD mySet value1 value2 +(integer) 2 +> SADD mySet value1 # Duplicate elements are not allowed, so the addition fails +(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`. + +**Find intersection**: + +```bash +> SINTERSTORE mySet3 mySet mySet2 +(integer) 1 +> SMEMBERS mySet3 +1) "value2" +``` + +**Find union**: + +```bash +> SUNION mySet mySet2 +1) "value3" +2) "value2" +3) "value1" +``` + +**Find the difference set**: + +```bash +> SDIFF mySet mySet2 # The difference set is a set composed of all elements that belong to mySet but do not belong to A +1) "value1" +``` + +### Application scenarios + +**Scenarios where the data that needs to be stored cannot be repeated** + +- Examples: Website UV statistics (`HyperLogLog` is more suitable for scenarios with a huge amount of data), article likes, dynamic likes and other scenarios. +- Related commands: `SCARD` (get the number of sets). + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719073733851.png) + +**Scenarios where the intersection, union and difference of multiple data sources need to be obtained** + +- Examples: common friends (intersection), common fans (intersection), common following (intersection), friend recommendation (difference set), music recommendation (difference set), subscription account recommendation (difference set + intersection) and other scenarios. +- Related commands: `SINTER` (intersection), `SINTERSTORE` (intersection), `SUNION` (union), `SUNIONSTORE` (union), `SDIFF` (difference), `SDIFFSTORE` (difference). + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719074543513.png) + +**Scenarios where elements in the data source need to be randomly obtained** + +- Examples: Lottery system, random roll call and other scenarios. +- Related commands: `SPOP` (randomly obtain elements from the set and remove them, suitable for scenarios that do not allow repeated winnings), `SRANDMEMBER` (randomly obtain elements from the set, suitable for scenarios that allow repeated winnings). + +## Sorted Set (ordered set) + +### Introduction + +Sorted Set is similar to Set, but compared with Set, Sorted Set adds a weight parameter `score`, so that the elements in the set can be ordered by `score`, and the list of elements can also be obtained through the range of `score`. It's a bit like a combination of `HashMap` and `TreeSet` in Java. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124437791.png) + +### Common commands + +| Commands | Introduction | +|------------------------------------------------ |---------------------------------------------------------------------------------------------------------------- | +| ZADD key score1 member1 score2 member2 ... | Add one or more elements to the specified sorted set | +| ZCARD KEY | Get the number of elements of the specified ordered set | +| ZSCORE key member | Get the score value of the specified element in the specified ordered set | +| ZINTERSTORE destination numkeys key1 key2 ... | Store the intersection of all given ordered sets in destination, perform a SUM aggregation operation on the score values corresponding to the same elements, numkeys is the number of sets | +| ZUNIONSTORE destination numkeys key1 key2 ... | Find the union, others are similar to ZINTERSTORE | +| ZDIFFSTORE destination numkeys key1 key2 ... | Find the difference set, others are similar to ZINTERSTORE || ZRANGE key start end | Get the elements between start and end of the specified ordered set (score from low to high) | +| ZREVRANGE key start end | Get the elements between start and end of the specified ordered set (score from high to bottom) | +| ZREVRANK key member | Get the ranking of the specified element in the specified ordered set (score sorted from large to small) | + +For more Redis Sorted Set commands and detailed usage guides, please view the corresponding introduction on the Redis official website: . + +**Basic Operation**: + +```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). + +**Get the ranking of the specified element**: + +```bash +> ZREVRANK myZset value1 +0 +> ZREVRANK myZset value2 +1 +``` + +**Find intersection**: + +```bash +> ZINTERSTORE myZset3 2 myZset myZset2 +1 +> ZRANGE myZset3 0 1 WITHSCORES +value2 +5 +``` + +**Find union**: + +```bash +> ZUNIONSTORE myZset4 2 myZset myZset2 +3 +> ZRANGE myZset4 0 2 WITHSCORES +value1 +2 +value3 +3 +value2 +5 +``` + +**Find the difference set**: + +```bash +> ZDIFF 2 myZset myZset2 WITHSCORES +value1 +2 +``` + +### Application scenarios + +**Scenarios where elements in the data source need to be randomly obtained and sorted according to a certain weight** + +- Examples: various rankings, such as the ranking of gifts in the live broadcast room, the WeChat step ranking in the circle of friends, the rank ranking in Honor of Kings, the topic popularity ranking, etc. +- Related commands: `ZRANGE` (sort from small to large), `ZREVRANGE` (sort from large to small), `ZREVRANK` (specify element ranking). + +![](https://oss.javaguide.cn/github/javaguide/database/redis/2021060714195385.png) + +There is an article in the "Technical Interview Questions" of ["Java Interview Guide"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) that details how to use Sorted Set to design and create a ranking list. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) + +**Scenarios where the data that needs to be stored has priority or importance** such as priority task queue. + +- Example: Priority task queue. +- Related commands: `ZRANGE` (sort from small to large), `ZREVRANGE` (sort from large to small), `ZREVRANK` (specify element ranking). + +## Summary + +| Data type | Description | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| String | 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 the path of images), and serialized objects. | +| List | The implementation of Redis's List is a doubly linked list, which can support reverse search and traversal, making it more convenient to operate, but it brings some additional memory overhead. | +| Hash | A mapping table of String type field-value (key-value pairs), which is particularly suitable for storing objects. During subsequent operations, you can directly modify the values of certain fields in this object. | +| Set | An unordered set. The elements in the set are not in order but are unique, somewhat similar to `HashSet` in Java. | +| Zset | Compared with Set, Sorted Set adds a weight parameter `score`, so that the elements in the set can be ordered by `score`, and the list of elements can also be obtained through the range of `score`. It's a bit like a combination of `HashMap` and `TreeSet` in Java. | + +## Reference + +- Redis Data Structures: . +- Redis Commands: . +- Redis Data types tutorial: . +- Whether Redis uses Hash or String to store object information: + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-data-structures-02.en.md b/docs_en/database/redis/redis-data-structures-02.en.md new file mode 100644 index 00000000000..a55a96334ab --- /dev/null +++ b/docs_en/database/redis/redis-data-structures-02.en.md @@ -0,0 +1,223 @@ +--- +title: Detailed explanation of three special data types of Redis +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis common data types + - - meta + - name: description + content: Summary of Redis special data types: HyperLogLogs (cardinality statistics), Bitmap (bit storage), Geospatial (geographic location). +--- + +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 is not an actual data type in Redis, but a set of bit-oriented operations defined on the String type, treating it as a bit vector. Since strings are binary-safe blocks and have a maximum length of 512 MB, they are suitable for setting up to 2^32 different bits. + +Bitmap stores continuous binary numbers (0 and 1). Through Bitmap, only one bit is needed to represent the value or status corresponding to a certain element, and the key is the corresponding element itself. We know that 8 bits can form a byte, so Bitmap itself will greatly save storage space. + +You can think of a Bitmap as an array that stores binary numbers (0s and 1s). The index of each element in the array is called offset. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) + +### Common commands + +| Commands | Introduction | +|------------------------------------------------ |---------------------------------------------------------------- | +| 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 ... | Operate on one or more Bitmaps. Available operators are AND, OR, XOR and NOT | + +**Bitmap basic operation demonstration**: + +```bash +# SETBIT will return the value of the previous bit (default is 0) here will generate 7 bits +> SETBIT mykey 7 1 +(integer) 0 +> SETBIT mykey 7 0 +(integer) 1 +> GETBIT mykey 7 +(integer) 0 +> SETBIT mykey 6 1 +(integer) 0 +> SETBIT mykey 8 1 +(integer) 0 +# Count the number of bits set to 1 through bitcount. +> BITCOUNT mykey +(integer) 2 +``` + +### Application scenarios + +**Scenarios where status information needs to be saved (represented by 0/1)** + +- Examples: user check-in status, active user status, user behavior statistics (such as whether a video has been liked). +- Related commands: `SETBIT`, `GETBIT`, `BITCOUNT`, `BITOP`. + +## HyperLogLog (cardinality statistics) + +### Introduction + +HyperLogLog is a well-known cardinality counting probability algorithm, which is optimized and improved based on LogLog Counting (LLC). It is not unique to Redis. Redis only implements this algorithm and provides some out-of-the-box APIs. + +The HyperLogLog provided by Redis takes up very little space, requiring only 12k of space to store close to `2^64` different elements. This is really amazing. Is this the charm of mathematics? Moreover, Redis has optimized the storage structure of HyperLogLog and uses two counting methods: + +- **Sparse Matrix**: When the count is small, it takes up very little space. +- **Dense Matrix**: When the count reaches a certain threshold, it occupies 12k space. + +There are corresponding detailed instructions in the official Redis documentation: + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220721091424563.png) + +In order to save memory, the cardinality counting probability algorithm does not directly store metadata, but estimates the cardinality value (the number of elements in the set) through a certain probability and statistical method. Therefore, the counting result of HyperLogLog is not an exact value, and there is a certain error (standard error is `0.81%`). + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) + +The use of HyperLogLog is very simple, but the principle is very complex. For the principle of HyperLogLog and its implementation in Redis, you can read this article: [Explanation of the principle of HyperLogLog algorithm and how Redis applies it](https://juejin.cn/post/6844903785744056333). + +I recommend another tool that can help understand the principles of HyperLogLog: [Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure](http://content.research.neustar.biz/blog/hll.html). + +In addition to HyperLogLog, Redis also provides other probabilistic data structures, corresponding official document address: . + +### Common commands + +There are very few commands related to HyperLogLog, and the most commonly used are only 3. + +| Commands | Introduction | +|------------------------------------------------ |-------------------------------------------------------------------------------- | +| PFADD key element1 element2 ... | Add one or more elements to HyperLogLog | +| PFCOUNT key1 key2 | Get the unique count of one or more HyperLogLogs. | +| PFMERGE destkey sourcekey1 sourcekey2 ... | Merge multiple HyperLogLogs into destkey. destkey will combine multiple sources to calculate the corresponding unique count. | + +**HyperLogLog basic operation demonstration**: + +```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" +>PFCOUNTdesthll +(integer) 6``` + +### Application scenarios + +**Huge number of counting scenarios (millions, tens of millions or more)** + +- Example: Daily/weekly/monthly access IP statistics of popular websites, UV statistics of popular posts. +- Related commands: `PFADD`, `PFCOUNT`. + +## Geospatial (geographic location) + +### Introduction + +Geospatial index (GEO for short) is mainly used to store geographical location information and is implemented based on Sorted Set. + +Through GEO, we can easily implement functions such as calculating the distance between two locations and obtaining elements near a specified location. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194359494.png) + +### Common commands + +| Commands | Introduction | +|------------------------------------------------ |------------------------------------------------------------------------------------------------------------- | +| GEOADD key longitude1 latitude1 member1 ... | Add the latitude and longitude information corresponding to one or more elements to GEO | +| GEOPOS key member1 member2 ... | Returns the latitude and longitude information of the given element | +| GEODIST key member1 member2 M/KM/FT/MI | Returns the distance between two given elements | +| GEORADIUS key longitude latitude radius distance | Get other elements within the distance range near the specified position, supporting ASC (from near to far), DESC (from far to near), Count (number) and other parameters | +| GEORADIUSBYMEMBER key member radius distance | Similar to the GEORADIUS command, except that the reference center point is an element in GEO | + +**Basic Operation**: + +```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 +``` + +View `personLocation` through the Redis visualization tool. As expected, the bottom layer is a Sorted Set. + +The longitude and latitude data of the geographical location information stored in GEO is converted into an integer through the GeoHash algorithm, and this integer is used as the score (weight parameter) of the Sorted Set. + +![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220721201545147.png) + +**Get other elements within the specified position range**: + +```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 +``` + +For an analysis of the underlying principles of the `GEORADIUS` command, you can read this article by Ali: [How does Redis implement the "people nearby" function? ](https://juejin.cn/post/6844903966061363207). + +**Remove element**: + +The bottom layer of GEO is Sorted Set, and you can use Sorted Set related commands on GEO. + +```bash +> ZREM personLocation user1 +1 +> ZRANGE personLocation 0 -1 +user3 +user2 +> ZSCORE personLocation user2 +4069879562983946 +``` + +### Application scenarios + +**Scenarios that require management of geospatial data** + +- Example: People nearby. +- Related commands: `GEOADD`, `GEORADIUS`, `GEORADIUSBYMEMBER`. + +## Summary + +| Data type | Description | +|---------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bitmap | You can think of a Bitmap as an array that stores binary numbers (0 and 1). The subscript of each element in the array is called offset. Through Bitmap, only one bit is needed to represent the value or status corresponding to an element, and the key is the corresponding element itself. We know that 8 bits can form a byte, so Bitmap itself will greatly save storage space. | +| HyperLogLog | The HyperLogLog provided by Redis takes up very little space, requiring only 12k of space to store close to `2^64` different elements. However, the count result of HyperLogLog is not an exact value and has a certain error (standard error is `0.81%`). | +| Geospatial index | Geospatial index (GEO) is mainly used to store geographical location information and is implemented based on Sorted Set. | + +## Reference + +- Redis Data Structures: . +- "Redis Deep Adventure: Core Principles and Application Practices" 1.6 Four ounces make a huge difference - HyperLogLog +- Bloom filter, bitmap, HyperLogLog: \ No newline at end of file diff --git a/docs_en/database/redis/redis-delayed-task.en.md b/docs_en/database/redis/redis-delayed-task.en.md new file mode 100644 index 00000000000..a9a392add2a --- /dev/null +++ b/docs_en/database/redis/redis-delayed-task.en.md @@ -0,0 +1,89 @@ +--- +title: How to implement delayed tasks based on Redis +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis, delayed tasks, expiration events, Redisson, DelayedQueue, reliability, consistency + - - meta + - name: description + content: Compare the two solutions of Redis expiration event and Redisson delay queue, analyze the trade-off between reliability and consistency, and give project selection suggestions. +--- + +The function of implementing delayed tasks based on Redis is nothing more than the following two options: + +1. Redis expiration event monitoring +2. Redisson’s built-in delay queue + +During the interview, you can first say that you have considered these two solutions, but in the end you found that there are many problems with the Redis expiration event monitoring solution, so you finally chose the DelayedQueue solution built into Redisson. + +At this time, the interviewer may ask you some relevant questions, which we will mention later, so just prepare in advance. + +In addition, in addition to the questions introduced below, it is recommended that you review all the common questions related to Redis. It is not ruled out that the interviewer will ask you some other questions about Redis. + +### How does Redis expiration event monitoring implement the delayed task function? + +Redis 2.0 introduces publish and subscribe (pub/sub) functionality. In pub/sub, a concept called channel is introduced, which is somewhat similar to topic in message queue. + +Pub/sub involves two roles: publisher (publisher) and subscriber (subscriber, also called consumer): + +- The publisher delivers messages to the specified channel through `PUBLISH`. +- Subscribers subscribe to the channels they care about through `SUBSCRIBE`. Moreover, subscribers can subscribe to one or multiple channels. + +![Redis publish and subscribe (pub/sub) function](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) + +In the pub/sub mode, the producer needs to specify which channel to send the message to, and the consumer subscribes to the corresponding channel to obtain the message. + +There are many default channels in Redis, and these channels are sent to them by Redis itself, not by the code we write ourselves. Among them, `__keyevent@0__:expired` is a default channel, responsible for monitoring key expiration events. That is to say, when a key expires, Redis will publish a key expiration event to the channel `__keyevent@__:expired`. + +We only need to listen to this channel to get the information about the expired key, thereby realizing the delayed task function. + +This function is officially called **keyspace notifications** by Redis, and its function is to monitor changes in Redis keys and values ​​in real time. + +### What are the shortcomings of Redis expiration event monitoring to implement the delayed task function? + +**1. Poor timeliness** + +An introduction in the official document explains the reason for poor timeliness, address: . + +![Redis expiration events](https://oss.javaguide.cn/github/javaguide/database/redis/redis-timing-of-expired-events.png) + +The core of this paragraph is: the expiration event message is published when the Redis server deletes the key, instead of being published directly after a key expires. + +We know that there are two commonly used deletion strategies for expired data: + +1. **Lazy deletion**: The data will only be checked for expiration when the key is taken out. This is the most CPU-friendly, but may cause too many expired keys to be deleted. +2. **Periodic deletion**: Extract a batch of keys at regular intervals to delete expired keys. Moreover, the Redis underlying layer will reduce the impact of deletion operations on CPU time by limiting the duration and frequency of deletion operations. + +Periodic deletion is more memory friendly, lazy deletion is more CPU friendly. Both have their own merits, so Redis uses **regular deletion + lazy/lazy deletion**. + +Therefore, there will be situations where I set the expiration time of the key, but the key has not been deleted by the specified time, and no expiration event is released. + +**2. Lost message** + +Messages in Redis's pub/sub mode do not support persistence, unlike message queues. In the pub/sub mode of Redis, the publisher sends the message to the specified channel, and the subscriber listens to the corresponding channel to receive the message. When there are no subscribers, the message will be discarded directly and will not be stored in Redis. + +**3. Repeated consumption of messages under multiple service instances** + +Redis's pub/sub mode currently only has a broadcast mode, which means that when a producer publishes a message to a specific channel, all consumers subscribed to the relevant channel can receive the message. + +At this time, we need to pay attention to the problem of multiple service instances repeatedly processing messages, which will increase the amount of code development and maintenance difficulty. + +### What is the principle of Redisson delay queue? What are the advantages? + +Redisson is an open source Java language Redis client that provides many out-of-the-box features, such as the implementation of multiple distributed locks and delay queues. + +We can use Redisson's built-in delay queue RDelayedQueue to implement the delayed task function. + +Redisson's delay queue RDelayedQueue is implemented based on Redis' SortedSet. SortedSet is an ordered set in which each element can be set with a score, representing the weight of the element. Redisson takes advantage of this feature to insert tasks that need to be delayed into a SortedSet and set corresponding expiration times as scores for them. + +Redisson periodically scans the SortedSet for expired elements using the `zrangebyscore` command, then removes these expired elements from the SortedSet and adds them to the ready message list. The ready message list is a blocking queue, and when a message comes in, it will be monitored by the consumer. This avoids the consumer polling the entire SortedSet and improves execution efficiency. + +Compared with Redis expiration event monitoring to implement delayed task function, this method has the following advantages: + +1. **Reduces the possibility of message loss**: Messages in DelayedQueue will be persisted. Even if Redis is down, only a few messages may be lost according to the persistence mechanism, which will have little impact. Of course, you can also use database scanning as a compensation mechanism. +2. **There is no problem of repeated consumption of messages**: Each client obtains tasks from the same target queue, and there is no problem of repeated consumption. + +Compared with Redisson's built-in delay queue, the message queue can achieve higher throughput and stronger reliability by ensuring the reliability of message consumption and controlling the number of message producers and consumers. In actual projects, the delayed message solution of the message queue is preferred. \ No newline at end of file diff --git a/docs_en/database/redis/redis-memory-fragmentation.en.md b/docs_en/database/redis/redis-memory-fragmentation.en.md new file mode 100644 index 00000000000..3953a477ed4 --- /dev/null +++ b/docs_en/database/redis/redis-memory-fragmentation.en.md @@ -0,0 +1,131 @@ +--- +title: Detailed explanation of Redis memory fragmentation +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis, memory fragmentation, allocator, memory management, memory usage, optimization + - - meta + - name: description + content: Analyze the causes and effects of Redis memory fragmentation, combine allocator and memory management strategies, provide observation and optimization directions, and reduce resource waste. +--- + +## What is memory fragmentation? + +You can think of memory fragmentation simply as free memory that is not available. + +For example: the operating system allocates 32 bytes of continuous memory space for you, but you actually only need to use 24 bytes of memory space to store data. If the extra 8 bytes of memory space cannot be allocated to store other data later, it can be called memory fragmentation. + +![Memory fragmentation](https://oss.javaguide.cn/github/javaguide/memory-fragmentation.png) + +Although Redis memory fragmentation will not affect Redis performance, it will increase memory consumption. + +## Why is there Redis memory fragmentation? + +There are two common reasons for Redis memory fragmentation: + +**1. When Redis stores data, the memory space applied to the operating system may be larger than the actual storage space required for the data. ** + +The following is the original words of Redis official: + +> To store user keys, Redis allocates at most as much memory as the `maxmemory` setting enables (however there are small extra allocations possible). + +When Redis uses the `zmalloc` method (the memory allocation method implemented by Redis itself) for memory allocation, in addition to allocating memory of the size `size`, it will also allocate more memory of the size `PREFIX_SIZE`. + +The source code of the `zmalloc` method is as follows (source code address: + +```java +void *zmalloc(size_t size) { + //Allocate memory of specified size + void *ptr = malloc(size+PREFIX_SIZE); + if (!ptr) zmalloc_oom_handler(size); +#ifdef HAVE_MALLOC_SIZE + update_zmalloc_stat_alloc(zmalloc_size(ptr)); + return ptr; +#else + *((size_t*)ptr) = size; + update_zmalloc_stat_alloc(size+PREFIX_SIZE); + return (char*)ptr+PREFIX_SIZE; +#endif +} +``` + +In addition, Redis can use a variety of memory allocators to allocate memory (libc, jemalloc, tcmalloc). The default is [jemalloc](https://github.com/jemalloc/jemalloc), and jemalloc allocates memory according to a series of fixed sizes (8 bytes, 16 bytes, 32 bytes...). The memory units divided by jemalloc are as shown in the figure below: + +![jemalloc memory unit diagram](https://oss.javaguide.cn/github/javaguide/database/redis/6803d3929e3e46c1b1c9d0bb9ee8e717.png) + +When the memory requested by the program is closest to a certain fixed value, jemalloc will allocate a corresponding size of space to it. For example, if the program needs to apply for 17 bytes of memory, jemalloc will directly allocate 32 bytes of memory to it, which will result in a waste of 15 bytes of memory. However, jemalloc is specially optimized for memory fragmentation problems and generally does not cause excessive fragmentation. + +**2. Frequent modification of data in Redis will also cause memory fragmentation. ** + +When some data in Redis is deleted, Redis usually does not easily release memory to the operating system. + +This also has the corresponding original words in the official Redis documentation: + +![](https://oss.javaguide.cn/github/javaguide/redis-docs-memory-optimization.png) + +Document address: . + +## How to view Redis memory fragmentation information? + +Use the `info memory` command to view Redis memory-related information. The specific meaning of each parameter in the figure below is detailed in the Redis official document: . + +![](https://oss.javaguide.cn/github/javaguide/redis-info-memory.png) + +The calculation formula of Redis memory fragmentation rate: `mem_fragmentation_ratio` (memory fragmentation rate) = `used_memory_rss` (the size of physical memory space actually allocated to Redis by the operating system)/ `used_memory` (the size of memory space actually applied for by the Redis memory allocator to store data) + +In other words, the larger the value of `mem_fragmentation_ratio` (memory fragmentation rate), the more serious the memory fragmentation rate. + +Be sure not to mistake `used_memory_rss` minus `used_memory` value as the size of the memory fragment! ! ! This includes not only memory fragmentation, but also other process overhead, as well as overhead for shared libraries, stacks, etc. + +Many friends may ask: "What is the memory fragmentation rate that needs to be cleaned up?". + +Normally, we think that memory fragmentation needs to be cleaned up when `mem_fragmentation_ratio > 1.5`. `mem_fragmentation_ratio > 1.5` means that if you use Redis to store data with an actual size of 2G, you will need to use more than 3G of memory. + +If you want to quickly check the memory fragmentation rate, you can also use the following command: + +```bash +> redis-cli -p 6379 info | grep mem_fragmentation_ratio +``` + +In addition, the memory fragmentation rate may be less than 1. I have never encountered this situation in daily use. Interested friends can read this article [Fault Analysis | What should I do if the memory fragmentation rate of Redis is too low? - Weixin Open Source Community](https://mp.weixin.qq.com/s/drlDvp7bfq5jt2M5pTqJCw). + +## How to clean up Redis memory fragments? + +Redis4.0-RC3 version comes with memory defragmentation, which can avoid the problem of excessive memory fragmentation rate. + +Just set the `activedefrag` configuration item to `yes` directly through the `config set` command. + +```bash +config set activedefrag yes +``` + +The specific time to clean up needs to be controlled by the following two parameters: + +```bash +#Start cleaning up when the space occupied by memory fragmentation reaches 500mb +config set active-defrag-ignore-bytes 500mb +# Start cleaning when the memory fragmentation rate is greater than 1.5 +config set active-defrag-threshold-lower 50 +``` + +The Redis automatic memory fragmentation cleaning mechanism may have an impact on Redis performance. We can reduce the impact on Redis performance through the following two parameters: + +```bash +# The proportion of CPU time occupied by memory fragmentation cleaning should not be less than 20% +config set active-defrag-cycle-min 20 +# The proportion of CPU time occupied by memory fragmentation cleaning is no more than 50% +config set active-defrag-cycle-max 50 +``` + +In addition, restarting the node can defragment the memory. If you are using a Redis cluster with a high-availability architecture, you can convert the master node with an excessive fragmentation rate into a slave node for safe restart. + +## Reference + +- Redis official documentation: +- Redis core technology and practice - Geek Time - Why is the memory usage still very high after deleting data? : +- Redis source code analysis - memory allocation: < Source code analysis - memory management> + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-persistence.en.md b/docs_en/database/redis/redis-persistence.en.md new file mode 100644 index 00000000000..1557daf0b8f --- /dev/null +++ b/docs_en/database/redis/redis-persistence.en.md @@ -0,0 +1,217 @@ +--- +title: Redis持久化机制详解 +category: 数据库 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis持久化机制详解 + - - meta + - name: description + content: Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)、RDB 和 AOF 的混合持久化(Redis 4.0 新增)。 +--- + +使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。 + +Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式: + +- 快照(snapshotting,RDB) +- 只追加文件(append-only file, AOF) +- RDB 和 AOF 的混合持久化(Redis 4.0 新增) + +官方文档地址: 。 + +![](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-persitence.png) + +## RDB 持久化 + +### 什么是 RDB 持久化? + +Redis 可以通过创建快照来获得存储在内存里面的数据在 **某个时间点** 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 + +快照持久化是 Redis 默认采用的持久化方式,在 `redis.conf` 配置文件中默认有此下配置: + +```clojure +save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。 + +save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。 + +save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。 +``` + +### RDB 创建快照时会阻塞主线程吗? + +Redis 提供了两个命令来生成 RDB 快照文件: + +- `save` : 同步保存操作,会阻塞 Redis 主线程; +- `bgsave` : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。 + +> 这里说 Redis 主线程而不是主进程的主要是因为 Redis 启动之后主要是通过单线程的方式完成主要的工作。如果你想将其描述为 Redis 主进程,也没毛病。 + +## AOF 持久化 + +### 什么是 AOF 持久化? + +与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 `appendonly` 参数开启: + +```bash +appendonly yes +``` + +开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 `server.aof_buf` 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( `fsync`策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。 + +只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。 + +AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 `dir` 参数设置的,默认的文件名是 `appendonly.aof`。 + +### AOF 工作基本流程是怎样的? + +AOF 持久化功能的实现可以简单分为 5 步: + +1. **命令追加(append)**:所有的写命令会追加到 AOF 缓冲区中。 +2. **文件写入(write)**:将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用`write`函数(系统调用),`write`将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。 +3. **文件同步(fsync)**:这一步才是持久化的核心!根据你在 `redis.conf` 文件里 `appendfsync` 配置的策略,Redis 会在不同的时机,调用 `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` 执行写操作后,会立刻调用 `fsync` 函数同步 AOF 文件(刷盘)。主线程会阻塞,直到 `fsync` 将数据完全刷到磁盘后才会返回。这种方式数据最安全,理论上不会有任何数据丢失。但因为每个写操作都会同步阻塞主线程,所以性能极差。 +2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒)。这种方式主线程的性能基本不受影响。在性能和数据安全之间做出了绝佳的平衡。不过,在 Redis 异常宕机时,最多可能丢失最近 1 秒内的数据。 +3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`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 优秀的地方**: + +- The content stored in the RDB file is compressed binary data, which saves the data set at a certain point in time. The file is small and suitable for data backup and disaster recovery. AOF files store each write command, similar to MySQL's binlog log, and are usually much larger than RDB files. When the AOF becomes too large, Redis can automatically rewrite the AOF in the background. The new AOF file saves the same database state as the original AOF file, but is smaller in size. However, before Redis 7.0, AOF might use a lot of memory if there were write commands during rewrite, and all write commands arriving during rewrite would be written to disk twice. +- Use RDB files to restore data and directly parse and restore the data. There is no need to execute commands one by one, and the speed is very fast. AOF needs to execute each write command in sequence, which is very slow. In other words, RDB is faster when restoring large data sets compared to AOF. + +**AOF is better than RDB**: + +- The data security of RDB is not as good as AOF, and there is no way to persist data in real time or within seconds. The process of generating RDB files is relatively arduous. Although the BGSAVE sub-process writing the RDB files will not block the main thread, it will have an impact on the CPU resources and memory resources of the machine. In severe cases, it may even directly shut down the Redis service. AOF supports second-level data loss (depending on the fsync policy, if it is everysec, up to 1 second of data is lost). It only requires appending commands to the AOF file, and the operation is lightweight. +- RDB files are saved in a specific binary format, and there are multiple versions of RDB in the evolution of Redis versions, so there is a problem that the old version of the Redis service is not compatible with the new version of the RDB format. +- AOF contains a log of all operations in a format that is easy to understand and parse. You can easily export AOF files for analysis, and you can also directly manipulate AOF files to solve some problems. For example, if you execute the `FLUSHALL` command and accidentally refresh all the contents, as long as the AOF file has not been overwritten, you can restore the previous state by deleting the latest command and restarting. + +**In summary**: + +- If there is no impact if some data saved by Redis is lost, you can choose to use RDB. +- Using AOF alone is not recommended as creating an RDB snapshot from time to time allows for database backups, faster restarts, and troubleshooting AOF engine errors. +- If the saved data requires relatively high security, it is recommended to enable RDB and AOF persistence at the same time or enable RDB and AOF mixed persistence. + +## Reference + +- "Redis Design and Implementation" +- Redis persistence - Redis official documentation: +- The difference between AOF and RDB persistence: +- Detailed explanation of Redis AOF persistence - Programmer Li Xiaobing: +- Redis RDB and AOF persistence · Analyze: + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-questions-01.en.md b/docs_en/database/redis/redis-questions-01.en.md new file mode 100644 index 00000000000..421aa9d10bf --- /dev/null +++ b/docs_en/database/redis/redis-questions-01.en.md @@ -0,0 +1,905 @@ +--- +title: Redis常见面试题总结(上) +category: 数据库 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis面试题,Redis基础,Redis数据结构,Redis线程模型,Redis持久化,Redis内存管理,Redis性能优化,Redis分布式锁,Redis消息队列,Redis延时队列,Redis缓存策略,Redis单线程,Redis多线程,Redis过期策略,Redis淘汰策略 + - - meta + - name: description + content: 最新Redis面试题总结(上):深入讲解Redis基础、五大常用数据结构、单线程模型原理、持久化机制、内存淘汰与过期策略、分布式锁与消息队列实现。适合准备后端面试的开发者! +--- + + + +## Redis 基础 + +### 什么是 Redis? + +[Redis](https://redis.io/) (**RE**mote **DI**ctionary **S**erver)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。 + +为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。 + +![Redis 数据类型概览](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 或者通过 Redis 官网提供的[在线 Redis 环境](https://try.redis.io/)(少部分命令无法使用)来实际体验 Redis。 + +![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),感兴趣的话可以看看。 + +### ⭐️Redis 为什么这么快? + +Redis 内部做了非常多的性能优化,比较重要的有下面 4 点: + +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) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。 + +> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。 + +![why-redis-so-fast](https://oss.javaguide.cn/github/javaguide/database/redis/why-redis-so-fast.png) + +那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。 + +### 除了 Redis,你还知道其他分布式缓存方案吗? + +如果面试中被问到这个问题的话,面试官主要想看看: + +1. 你在选择 Redis 作为分布式缓存方案时,是否是经过严谨的调研和思考,还是只是因为 Redis 是当前的“热门”技术。 +2. 你在分布式缓存方向的技术广度。 + +如果你了解其他方案,并且能解释为什么最终选择了 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 为什么用跳表实现有序集合](https://javaguide.cn/database/redis/redis-skiplist.html)。 + +### 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 适合什么场景? + +HyperLogLog (HLL) 是一种非常巧妙的概率性数据结构,它专门解决一类非常棘手的大数据问题:在海量数据中,用极小的内存,估算一个集合中不重复元素的数量,也就是我们常说的基数(Cardinality) + +HLL 做的最核心的权衡,就是用一点点精确度的损失,来换取巨大的内存空间节省。它给出的不是一个 100%精确的数字,而是一个带有很小标准误差(Redis 中默认是 0.81%)的近似值。 + +**基于这个核心权衡,HyperLogLog 最适合以下特征的场景:** + +1. **数据量巨大,内存敏感:** 这是 HLL 的主战场。比如,要统计一个亿级日活 App 的每日独立访客数。如果用传统的 Set 来存储用户 ID,一个 ID 占几十个字节,上亿个 ID 可能需要几个 GB 甚至几十 GB 的内存,这在很多场景下是不可接受的。而 HLL,在 Redis 中只需要固定的 12KB 内存,就能处理天文数字级别的基数,这是一个颠覆性的优势。 +2. **对结果的精确度要求不是 100%:** 这是使用 HLL 的前提。比如,产品经理想知道一个热门帖子的 UV(独立访客数)是大约 1000 万还是 1010 万,这个细微的差别通常不影响商业决策。但如果场景是统计一个交易系统的准确交易笔数,那 HLL 就完全不适用,因为金融场景要求 100%的精确。 + +**所以,HyperLogLog 具体的应用场景就非常清晰了:** + +- **网站/App 的 UV(Unique Visitor)统计:** 比如统计首页每天有多少个不同的 IP 或用户 ID 访问过。 +- **搜索引擎关键词统计:** 统计每天有多少个不同的用户搜索了某个关键词。 +- **社交网络互动统计:** 比如统计一条微博被多少个不同的用户转发过。 + +在这些场景下,我们关心的是数量级和趋势,而不是个位数的差异。 + +最后,Redis 的实现还非常智能,它内部会根据基数的大小,在**稀疏矩阵**(占用空间更小)和**稠密矩阵**(固定的 12KB)之间自动切换,进一步优化了内存使用。总而言之,当您需要对海量数据进行去重计数,并且可以接受微小误差时,HyperLogLog 就是不二之选。 + +### 使用 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 +``` + +### 如果我想判断一个元素是否不在海量元素集合中,用什么数据类型? + +这是布隆过滤器的经典应用场景。布隆过滤器可以告诉你一个元素一定不存在或者可能存在,它也有极高的空间效率和一定的误判率,但绝不会漏报。也就是说,布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。 + +Bloom Filter 的简单原理图如下: + +![Bloom Filter 的简单原理示意图](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-simple-schematic-diagram.png) + +当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 + +如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +## ⭐️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) + +### 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 comments for hz](https://oss.javaguide.cn/github/javaguide/database/redis/redis.conf-hz.png) + +There is also a similar parameter **dynamic-hz**. After this parameter is turned on, Redis will dynamically calculate a value based on hz. Redis provides and enables by default the ability to use adaptive hz values, + +Both parameters are in the Redis configuration file `redis.conf`: + +```properties +# Default is 10 +hz 10 +# Enabled by default +dynamic-hz yes +``` + +One more thing to mention, in addition to the periodic task of regularly deleting expired keys, there are also other periodic tasks such as closing timed-out client connections and updating statistics. The execution frequency of these periodic tasks is also determined by the hz parameter. + +**Why doesn’t regular deletion delete all expired keys? ** + +This will have too big an impact on performance. If we have a very large number of keys, traversing the checks one by one is very time-consuming and will seriously affect performance. Redis designed this strategy to balance memory and performance. + +**Why not delete the key immediately after it expires? Wouldn't this waste a lot of memory space? ** + +Because it is not easy to do, or the cost of this deletion method is too high. If we use the delay queue as the deletion strategy, there are the following problems: + +1. The overhead of the queue itself may be large: when there are many keys, a delay queue may not be able to accommodate them. +2. Maintaining the delay queue is too troublesome: modifying the expiration time of a key requires adjusting its position in the delay queue, and concurrency control also needs to be introduced. + +### What should I do if a large number of keys expire together? + +When a large number of keys in Redis expire at the same point in time, the following problems may occur: + +- **Increased request latency**: Redis needs to consume CPU resources when processing expired keys. If the number of expired keys is large, it will cause the CPU usage of the Redis instance to increase, which will affect the processing speed of other requests, causing increased latency. +- **Memory usage is too high**: Although expired keys have expired, they will still occupy memory space before Redis actually deletes them. If expired keys are not cleaned up in time, it may cause excessive memory usage or even cause a memory overflow. + +To avoid these problems, you can take the following options: + +1. **Try to avoid centralized expiration of keys**: Try to be as random as possible when setting the expiration time of keys. +2. **Enable lazy free mechanism**: Modify the `redis.conf` configuration file and set the `lazyfree-lazy-expire` parameter to `yes` to enable the lazy free mechanism. After the lazy free mechanism is enabled, Redis will asynchronously delete expired keys in the background without blocking the main thread, thereby reducing the impact on Redis performance. + +### Do you understand the Redis memory elimination strategy? + +> Related questions: There are 20 million data in MySQL, but only 20 million data are stored in Redis. How to ensure that the data in Redis are hot data? + +Redis's memory elimination policy will only be triggered when the running memory reaches the configured maximum memory threshold, which is defined through the `maxmemory` parameter of `redis.conf`. Under 64-bit operating systems, `maxmemory` defaults to 0, which means there is no limit on the memory size. Under 32-bit operating systems, the default maximum memory value is 3GB. + +You can use the command `config get maxmemory` to view the value of `maxmemory`. + +```bash +> config get maxmemory +maxmemory +0 +``` + +Redis provides 6 memory elimination strategies: + +1. **volatile-lru (least recently used)**: Select the least recently used data from the data set (`server.db[i].expires`) with an expiration time set for elimination. +2. **volatile-ttl**: Select the data to be expired from the data set (`server.db[i].expires`) with an expiration time set for elimination. +3. **volatile-random**: Randomly select data for elimination from the data set (`server.db[i].expires`) with expiration time set. +4. **allkeys-lru (least recently used)**: Remove the least recently used data from the data set (`server.db[i].dict`). +5. **allkeys-random**: Randomly select data for elimination from the data set (`server.db[i].dict`). +6. **no-eviction** (default memory eviction policy): It is forbidden to evict data. When the memory is not enough to accommodate newly written data, an error will be reported for the new write operation. + +After version 4.0, the following two types are added: + +7. **volatile-lfu (least frequently used)**: Select the least frequently used data from the data set (`server.db[i].expires`) with an expiration time set for elimination. +8. **allkeys-lfu (least frequently used)**: Remove the least frequently used data from the data set (`server.db[i].dict`). + +`allkeys-xxx` means to eliminate data from all key values, while `volatile-xxx` means to eliminate data from key values ​​with an expiration time set. + +The enumeration array of memory elimination strategies is defined in `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} +}; +``` + +You can use the `config get maxmemory-policy` command to view the current Redis memory elimination policy. + +```bash +> config get maxmemory-policy +maxmemory-policy +noeviction +``` + +You can modify the memory elimination policy through the `config set maxmemory-policy memory elimination policy` command, which will take effect immediately. However, it will become invalid after restarting Redis in this way. Modifying the `maxmemory-policy` parameter in `redis.conf` will not be invalidated by restarting. However, it needs to be restarted before the modification can take effect. + +```properties +maxmemory-policy noeviction +``` + +For detailed instructions on the elimination strategy, please refer to the official Redis documentation: . + +## Reference + +- "Redis Development and Operation and Maintenance" +- "Redis Design and Implementation" +- "Redis Core Principles and Practical Combat" +- Redis command manual: +- RedisSearch Ultimate User Guide, you deserve it! : +- 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) + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-questions-02.en.md b/docs_en/database/redis/redis-questions-02.en.md new file mode 100644 index 00000000000..2a520856220 --- /dev/null +++ b/docs_en/database/redis/redis-questions-02.en.md @@ -0,0 +1,820 @@ +--- +title: Summary of common Redis interview questions (Part 2) +category: database +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis interview questions, Redis transactions, Redis performance optimization, Redis cache penetration, Redis cache breakdown, Redis cache avalanche, Redis bigkey, Redis hotkey, Redis slow query, Redis memory fragmentation, Redis cluster, Redis Sentinel, Redis Cluster, Redis pipeline, Redis Lua script + - - meta + - name: description + content: Summary of the latest Redis interview questions (Part 2): In-depth analysis of Redis transaction principles, performance optimization (pipeline/Lua/bigkey/hotkey), cache penetration/breakdown/avalanche solutions, slow queries and memory fragmentation, and detailed explanations of Redis Sentinel and Cluster clusters. Help you easily cope with back-end technical interviews! +--- + + + +## Redis transactions + +### What is a Redis transaction? + +You can understand transactions in Redis as: **Redis transactions provide a function to package multiple command requests. Then, execute all the packaged commands in sequence without being interrupted midway. ** + +Redis transactions are rarely used in actual development, and their functions are relatively useless. Do not confuse them with the transactions of relational databases that we usually understand. + +In addition to not meeting atomicity and durability, each command in the transaction will interact with the Redis server through the network, which is a waste of resources. Obviously it is enough to execute multiple commands in batches at one time, but this kind of operation is really incomprehensible. + +Therefore, Redis transactions are not recommended for daily development. + +### How to use Redis transactions? + +Redis can implement transaction functions through commands such as **`MULTI`, `EXEC`, `DISCARD` and `WATCH`**. + +```bash +> MULTI +OK +> SET PROJECT "JavaGuide" +QUEUED +>GET PROJECT +QUEUED +> EXEC +1) OK +2) "JavaGuide" +``` + +You can enter multiple commands after the [`MULTI`](https://redis.io/commands/multi) command. Redis will not execute these commands immediately, but will put them in the queue. When the [`EXEC`](https://redis.io/commands/exec) command is called, all commands will be executed. + +The process goes like this: + +1. Start transaction (`MULTI`); +2. Command enqueue (batch operation of Redis commands, executed in first-in, first-out (FIFO) order); +3. Execute transaction (`EXEC`). + +You can also cancel a transaction through the [`DISCARD`](https://redis.io/commands/discard) command, which will clear all commands saved in the transaction queue. + +```bash +> MULTI +OK +> SET PROJECT "JavaGuide" +QUEUED +>GET PROJECT +QUEUED +> DISCARD +OK +``` + +You can monitor the specified Key through the [`WATCH`](https://redis.io/commands/watch) command. When calling the `EXEC` command to execute a transaction, if a Key monitored by the `WATCH` command is modified by **other clients/Session**, the entire transaction will not be executed. + +```bash +# client 1 +> SET PROJECT "RustGuide" +OK +> WATCH PROJECT +OK +> MULTI +OK +> SET PROJECT "JavaGuide" +QUEUED + +# client 2 +# Modify the value of PROJECT before client 1 executes the EXEC command to commit the transaction +> SET PROJECT "GoGuide" + +# client 1 +# The modification failed because the value of PROJECT was modified by client 2 +> EXEC +(nil) +>GET PROJECT +"GoGuide" +``` + +However, if **WATCH** and **Transaction** are in the same Session, and the modification of the Key monitored by **WATCH** occurs within the transaction, the transaction can be executed successfully (related issue: [Different effects when the WATCH command encounters the MULTI command](https://github.com/Snailclimb/JavaGuide/issues/1714)). + +Modify the Key monitored by WATCH within the transaction: + +```bash +> SET PROJECT "JavaGuide" +OK +> WATCH PROJECT +OK +> MULTI +OK +> SET PROJECT "JavaGuide1" +QUEUED +> SET PROJECT "JavaGuide2" +QUEUED +> SET PROJECT "JavaGuide3" +QUEUED +> EXEC +1) OK +2) OK +3) OK +127.0.0.1:6379> GET PROJECT +"JavaGuide3" +``` + +Modify the Key monitored by WATCH outside the transaction: + +```bash +> SET PROJECT "JavaGuide" +OK +> WATCH PROJECT +OK +> SET PROJECT "JavaGuide2" +OK +> MULTI +OK +> GET USER +QUEUED +> EXEC +(nil) +``` + +The relevant introduction to the Redis official website [https://redis.io/topics/transactions](https://redis.io/topics/transactions) is as follows: + +![Redis transactions](https://oss.javaguide.cn/github/javaguide/database/redis/redis-transactions.png) + +### Do Redis transactions support atomicity? + +Redis transactions are different from the relational database transactions we usually understand. We know that transactions have four major characteristics: **1. Atomicity**, **2. Isolation**, **3. Durability**, **4. Consistency**. + +1. **Atomicity**: A transaction is the smallest execution unit and does not allow division. The atomicity of transactions ensures that actions either complete completely or have no effect at all; +2. **Isolation**: When accessing the database concurrently, a user's transaction will not be interfered by other transactions, and the database is independent between concurrent transactions; +3. **Durability**: after a transaction is committed. Its changes to the data in the database are durable and should not have any impact even if the database fails; +4. **Consistency**: The data remains consistent before and after executing a transaction, and the results of multiple transactions reading the same data are the same. + +In the case of a Redis transaction running error, except for the command with an error during execution, other commands can be executed normally. Moreover, Redis transactions do not support rollback operations. Therefore, Redis transactions do not actually satisfy atomicity. + +The Redis official website also explains why it does not support rollback. To put it simply, Redis developers feel that there is no need to support rollback, which is simpler, more convenient and has better performance. Redis developers feel that even command execution errors should be discovered during the development process rather than during the production process. + +![Why does Redis not support rollback](https://oss.javaguide.cn/github/javaguide/database/redis/redis-rollback.png) + +**Related issues**: + +- [issue#452: Regarding the issue that Redis transactions do not satisfy atomicity](https://github.com/Snailclimb/JavaGuide/issues/452). +- [Issue#491: About Redis without transaction rollback? ](https://github.com/Snailclimb/JavaGuide/issues/491). + +### Do Redis transactions support persistence? + +One important thing that makes Redis different from Memcached is that Redis supports persistence and supports 3 persistence methods: + +- Snapshotting (RDB); +- Append-only file (AOF); +- Hybrid persistence of RDB and AOF (new in Redis 4.0). + +Compared with RDB persistence, AOF persistence has better real-time performance. There are three different AOF persistence methods (`fsync` strategy) in the Redis configuration file. They are: + +```bash +appendfsync always #Every time a data modification occurs, the fsync function will be called to synchronize the AOF file. After fsync is completed, the thread returns, which will seriously reduce the speed of Redis. +appendfsync everysec #Call the fsync function to synchronize the AOF file once every second +appendfsync no #Let the operating system decide when to synchronize, usually every 30 seconds +``` + +Data loss will occur when the `fsync` policy of AOF persistence is no and everysec. Always can basically meet the persistence requirements, but the performance is too poor and will not be used in the actual development process. + +Therefore, the durability of Redis transactions cannot be guaranteed.### 如何解决 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/latest/develop/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 +- The value of a composite type (List, Hash, Set, Sorted Set, etc.) contains more than 5000 elements (however, for a value of a composite type, the more elements it contains, the more memory it takes up). + +![bigkey criterion](https://oss.javaguide.cn/github/javaguide/database/redis/bigkey-criterion.png) + +#### How did bigkey come about? What's the harm? + +Bigkey is usually generated for the following reasons: + +- Improper program design, such as directly using the String type to store binary data corresponding to larger files. +- Inadequate consideration of the data scale of the business. For example, when using collection types, the rapid growth of data volume was not considered. +- Failure to clean up junk data in time, such as a large number of redundant useless key-value pairs in the hash. + +In addition to consuming more memory space and bandwidth, bigkey will also have a relatively large impact on performance. + +In this article [Summary of Common Redis Blocking Causes](./redis-common-blocking-problems-summary.md) we mentioned that large keys can also cause blocking problems. Specifically, it is mainly reflected in the following three aspects: + +1. Client timeout blocking: Since Redis executes commands in a single thread, and it takes a long time to operate large keys, Redis will be blocked. From the perspective of the client, there will be no response for a long time. +2. Network blocking: Each time a large key is obtained, the network traffic is large. If the size of a key is 1 MB and the number of visits per second is 1,000, then 1,000 MB of traffic will be generated per second, which is catastrophic for servers with ordinary Gigabit network cards. +3. Working thread blocking: If you use del to delete a large key, the working thread will be blocked, making it impossible to process subsequent commands. + +The blocking problem caused by large keys will further affect master-slave synchronization and cluster expansion. + +In summary, there are many potential problems caused by big keys, and we should try to avoid the existence of big keys in Redis. + +#### How to discover bigkey? + +**1. Use the `--bigkeys` parameter that comes with Redis to search. ** + +```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 hashes 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 +``` + +From the running results of this command, we can see that this command will scan (Scan) all keys in Redis, which will have a slight impact on the performance of Redis. Moreover, this method can only find the top 1 bigkey of each data structure (the String data type that takes up the largest memory, and the composite data type that contains the most elements). However, having many elements in a key does not mean that it takes up more memory. We need to make further judgments based on specific business conditions. + +When executing this command online, in order to reduce the impact on Redis, you need to specify the `-i` parameter to control the frequency of scanning. `redis-cli -p 6379 --bigkeys -i 3` means that the rest interval after each scan during the scanning process is 3 seconds. + +**2. Use the SCAN command that comes with Redis** + +The `SCAN` command can return matching keys according to a certain pattern and number. After obtaining the key, you can use `STRLEN`, `HLEN`, `LLEN` and other commands to return its length or number of members. + +| Data structure | Command | Complexity | Result (corresponding to key) | +| ---------- | ------ | ------ | ------------------ | +| String | STRLEN | O(1) | The length of the string value | +| Hash | HLEN | O(1) | Number of fields in the hash table | +| List | LLEN | O(1) | Number of elements in the list | +| Set | SCARD | O(1) | Number of set elements | +| Sorted Set | ZCARD | O(1) | Number of elements in a sorted set | + +For collection types, you can also use the `MEMORY USAGE` command (Redis 4.0+). This command will return the memory space occupied by key-value pairs. + +**3. Use open source tools to analyze RDB files. ** + +Find big keys by analyzing RDB files. The premise of this solution is that your Redis uses RDB persistence. + +There are ready-made codes/tools available online that can be used directly: + +- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools): A tool written in Python language to analyze Redis RDB snapshot files. +- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys): A tool written in Go language to analyze Redis's RDB snapshot file, with better performance. + +**4. Use the Redis analysis service of the public cloud. ** + +If you are using the public cloud Redis service, you can see if it provides key analysis function (usually it does). + +Here we take Alibaba Cloud Redis as an example. It supports bigkey real-time analysis and discovery. The document address is: . + +![Alibaba Cloud Key Analysis](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) + +#### How to deal with bigkey? + +Common processing and optimization methods for bigkey are as follows (these methods can be used in conjunction): + +- **Split bigkey**: Split a bigkey into multiple small keys. For example, a Hash containing tens of thousands of fields is split into multiple Hash according to a certain strategy (such as secondary hashing). +- **Manual Cleanup**: Redis 4.0+ can use the `UNLINK` command to asynchronously delete one or more specified keys. For Redis 4.0 and below, you can consider using the `SCAN` command in combination with the `DEL` command to delete in batches. +- **Adopt appropriate data structures**: For example, do not use String to save file binary data, use HyperLogLog to count page UV, and Bitmap to save status information (0/1). +- **Turn on lazy-free (lazy deletion/delayed release)**: The lazy-free feature was introduced in Redis 4.0. It means that Redis uses an asynchronous method to delay the release of the memory used by the key, and hands the operation to a separate sub-thread to avoid blocking the main thread. + +### Redis hotkey (hot key) + +#### What is a hotkey? + +If a key is accessed more often and significantly more than other keys, then this key can be regarded as a hotkey. For example, if a Redis instance processes 5,000 requests per second, and a certain key has 2,000 visits per second, then this key can be regarded as a hotkey. + +The main reason for the emergence of hotkey is the sudden increase in the number of visits to a certain hot data, such as major hot search events and products participating in flash sales.#### What are the dangers of hotkey? + +Processing hotkeys consumes a lot of CPU and bandwidth and may affect the normal processing of other requests by the Redis instance. In addition, if the sudden request to access the hotkey exceeds the processing capacity of Redis, Redis will directly crash. In this case, a large number of requests will fall on the later database, possibly causing the database to crash. + +Therefore, hotkey is likely to become the bottleneck of system performance and needs to be optimized separately to ensure high availability and stability of the system. + +#### How to find hotkey? + +**1. Use the `--hotkeys` parameter that comes with Redis to search. ** + +The `hotkeys` parameter has been added to Redis version 4.0.3, which can return the number of times all keys have been accessed. + +The prerequisite for using this solution is that the `maxmemory-policy` parameter of Redis Server is set to the LFU algorithm, otherwise the error shown below will occur. + +```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. +``` + +There are two LFU algorithms in Redis: + +1. **volatile-lfu (least frequently used)**: Select the least frequently used data from the data set (`server.db[i].expires`) with an expiration time set for elimination. +2. **allkeys-lfu (least frequently used)**: When the memory is not enough to accommodate newly written data, in the key space, remove the least frequently used key. + +The following is an example from the configuration file `redis.conf`: + +```properties +# Use volatile-lfu strategy +maxmemory-policy volatile-lfu + +# Or use allkeys-lfu strategy +maxmemory-policy allkeys-lfu +``` + +It should be noted that the `hotkeys` parameter command will also increase the CPU and memory consumption (global scan) of the Redis instance, so it needs to be used with caution. + +**2. Use the `MONITOR` command. ** + +The `MONITOR` command is a way provided by Redis to view all operations of Redis in real time. It can be used to temporarily monitor the operation of a Redis instance, including read, write, delete and other operations. + +Since this command has a large impact on Redis performance, it is prohibited to open `MONITOR` for a long time (it is recommended to use this command with caution in a production environment). + +```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" +``` + +In the event of an emergency, we can choose to briefly execute the `MONITOR` command at an appropriate time and redirect the output to a file. After closing the `MONITOR` command, we can find out the hotkey during this period by classifying and analyzing the requests in the file. + +**3. Use open source projects. ** + +JD Retail's [hotkey](https://gitee.com/jd-platform-opensource/hotkey) project not only supports hotkey discovery, but also supports hotkey processing. + +![JD retail open source hotkey](https://oss.javaguide.cn/github/javaguide/database/redis/jd-hotkey.png) + +**4. Estimate in advance based on business conditions. ** + +Some hotkeys can be estimated based on business conditions, such as product data participating in flash sale activities, etc. However, we cannot predict the emergence of all hotkeys, such as breaking hot news events. + +**5. Record analysis in business code. ** + +Add corresponding logic to the business code to record and analyze key access conditions. However, this method will increase the complexity of the business code and is generally not used. + +**6. Use the Redis analysis service of the public cloud. ** + +If you are using the public cloud Redis service, you can see if it provides key analysis function (usually it does). + +Here we take Alibaba Cloud Redis as an example. It supports hotkey real-time analysis and discovery. The document address is: . + +![Alibaba Cloud Key Analysis](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) + +#### How to solve hotkey? + +Common processing and optimization methods for hotkey are as follows (these methods can be used in conjunction): + +- **Read and write separation**: The master node handles write requests, and the slave node handles read requests. +- **Use Redis Cluster**: Store hotspot data distributedly on multiple Redis nodes. +- **Level 2 Cache**: Hotkey is processed using Level 2 cache, and a copy of the hotkey is stored in the JVM local memory (Caffeine can be used). + +In addition to these methods, if you use the public cloud Redis service, you can also pay attention to the out-of-the-box solutions it provides. + +Here we take Alibaba Cloud Redis as an example. It supports optimizing hot key issues through the proxy query cache function (Proxy Query Cache). + +![Optimizing hot key issues through Alibaba Cloud’s Proxy Query Cache](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-hotkey-proxy-query-cache.png) + +### Slow query command + +#### Why are there slow query commands? + +We know that the execution of a Redis command can be simplified to the following 4 steps: + +1. Send a command; +2. Command queuing; +3. Command execution; +4. Return the result. + +Redis slow query counts the time it takes to execute this step of the command. Slow query commands are those commands that take a long time to execute. + +Why does Redis have slow query commands? + +Most commands in Redis have O(1) time complexity, but there are also a few commands with O(n) time complexity, such as: + +- `KEYS *`: will return all keys that match the rules. +- `HGETALL`: will return all key-value pairs in a Hash. +- `LRANGE`: Returns elements within the specified range in List. +- `SMEMBERS`: Returns all elements in Set. +- `SINTER`/`SUNION`/`SDIFF`: Calculate the intersection/union/difference of multiple Sets. +-… + +Since the time complexity of these commands is O(n), sometimes the entire table will be scanned. As n increases, the execution time will become longer. However, these commands do not necessarily cannot be used, but the value of N needs to be specified. In addition, if you have traversal requirements, you can use `HSCAN`, `SSCAN`, `ZSCAN` instead. + +In addition to these O(n) time complexity commands that may cause slow queries, there are also some commands that may have a time complexity above O(N), such as: + +- `ZRANGE`/`ZREVRANGE`: Returns all elements within the specified ranking range in the specified Sorted Set. The time complexity is O(log(n)+m), where n is the number of all elements and m is the number of elements returned. When m and n are quite large, the time complexity of O(n) is smaller.- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- …… + +#### 如何找到慢查询命令? + +Redis 提供了一个内置的**慢查询日志 (Slow Log)** 功能,专门用来记录执行时间超过指定阈值的命令。这对于排查性能瓶颈、找出导致 Redis 阻塞的“慢”操作非常有帮助,原理和 MySQL 的慢查询日志类似。 + +在 `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. **唯一 ID**: 日志条目的唯一标识符。 +2. **时间戳 (Timestamp)**: 命令执行完成时的 Unix 时间戳。 +3. **耗时 (Duration)**: 命令执行所花费的时间,单位是**微秒**。 +4. **命令及参数 (Command)**: 执行的具体命令及其参数数组。 +5. **客户端信息 (Client IP:Port)**: 执行命令的客户端地址和端口。 +6. **客户端名称 (Client Name)**: 如果客户端设置了名称 (CLIENT SETNAME)。 + +`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 查询数据库并更新缓存。 + +#### 缓存雪崩和缓存击穿有什么区别? + +缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。 + +### 如何保证缓存和数据库数据的一致性? + +缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的: + +- **读操作**: + 1. 先尝试从缓存读取数据。 + 2. 如果缓存命中,直接返回数据。 + 3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。 +- **写操作**: + 1. 先更新数据库。 + 2. 再直接删除缓存中对应的数据。 + +图解如下: + +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png) + +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png) + +如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案: + +1. **缓存失效时间(TTL - Time To Live)变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +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 阻塞原因有: + +- `O(n)` 复杂度命令执行(如 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS` 等),随着数据量增大导致执行时间过长。 +- 执行 `SAVE` 命令生成 RDB 快照时同步阻塞主线程,而 `BGSAVE` 通过 `fork` 子进程避免阻塞。 +- AOF 记录日志在主线程中进行,可能因命令执行后写日志而阻塞后续命令。 +- AOF 刷盘(fsync)时后台线程同步到磁盘,磁盘压力大导致 `fsync` 阻塞,进而阻塞主线程 `write` 操作,尤其在 `appendfsync always` 或 `everysec` 配置下明显。 +- AOF 重写过程中将重写缓冲区内容追加到新 AOF 文件时产生阻塞。 +- Operating large keys (string > 1MB or composite type elements > 5000) causes client timeouts, network blocking, and worker thread blocking. +- Using `flushdb` or `flushall` to clear the database involves deleting a large number of key-value pairs and releasing memory, causing the main thread to block. +- Data migration is a synchronous operation when the cluster is expanded or reduced. Migration of large keys causes nodes at both ends to be blocked for a long time, which may trigger failover. +- Insufficient memory triggers Swap, and the operating system swaps out the Redis memory to the hard disk, causing a sharp decline in read and write performance. +- Other processes excessively occupy the CPU, causing Redis throughput to decrease. +- Network problems such as connection rejection, high latency, network card soft interrupt, etc. cause Redis to block. + +For a detailed introduction, you can read this article: [Summary of common blocking causes in Redis](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html). + +## Redis cluster + +**Redis Sentinel**: + +1. What is Sentinel? What's the use? +2. How does Sentinel detect whether a node is offline? What is the difference between subjective offline and objective offline? +3. How does Sentinel implement failover? +4. Why is it recommended to deploy multiple sentinel nodes (sentinel cluster)? +5. How does Sentinel choose a new master (election mechanism)? +6. How to select Leader from Sentinel cluster? +7. Can Sentinel prevent split-brain? + +**Redis Cluster**: + +1. Why do you need Redis Cluster? What problem was solved? What are the advantages? +2. How is Redis Cluster sharded? +3. Why are the hash slots of Redis Cluster 16384? +4. How to determine which hash slot a given key should be distributed to? +5. Does Redis Cluster support redistribution of hash slots? +6. Can Redis Cluster provide services during expansion and contraction? +7. How do nodes in Redis Cluster communicate? + +**Reference answer**: [Detailed explanation of Redis cluster (paid)](https://javaguide.cn/database/redis/redis-cluster.html). + +## Redis usage specifications + +In the actual use of Redis, we try to adhere to some common specifications, such as: + +1. Use a connection pool: avoid frequently creating and closing client connections. +2. Try not to use O(n) commands. When using O(n) commands, pay attention to the number of n: O(n) commands such as `KEYS *`, `HGETALL`, `LRANGE`, `SMEMBERS`, `SINTER`/`SUNION`/`SDIFF` are not unusable, but the value of n needs to be clear. In addition, if you have traversal requirements, you can use `HSCAN`, `SSCAN`, `ZSCAN` instead. +3. Use batch operations to reduce network transmission: native batch operation commands (such as `MGET`, `MSET`, etc.), pipeline, Lua scripts. +4. Try not to use Redis transactions: The functions implemented by Redis transactions are relatively useless, and you can use Lua scripts instead. +5. It is forbidden to turn on the monitor for a long time: it will have a great impact on performance. +6. Control the life cycle of the key: Avoid storing too much data that is not frequently accessed in Redis. +7.… + +## Reference + +- "Redis Development and Operation and Maintenance" +- "Redis Design and Implementation" +- Redis Transactions: +- What is Redis Pipeline: +- A detailed explanation of the discovery and processing of BigKey and HotKey in Redis: +- Exploration of ideas and methods for solving Bigkey problems: +- Comprehensive troubleshooting guide for Redis latency issues: + + \ No newline at end of file diff --git a/docs_en/database/redis/redis-skiplist.en.md b/docs_en/database/redis/redis-skiplist.en.md new file mode 100644 index 00000000000..2ba31c10200 --- /dev/null +++ b/docs_en/database/redis/redis-skiplist.en.md @@ -0,0 +1,727 @@ +--- +title: Redis为什么用跳表实现有序集合 +category: 数据库 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis,跳表,有序集合,ZSet,时间复杂度,平衡树对比,实现原理 + - - meta + - name: description + content: 深入讲解 Redis 有序集合为何选择跳表实现,结合时间复杂度与平衡树对比,理解工程权衡与源码细节。 +--- + +## 前言 + +近几年针对 Redis 面试时会涉及常见数据结构的底层设计,其中就有这么一道比较有意思的面试题:“Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。 + +本文就以这道大厂常问的面试题为切入点,带大家详细了解一下跳表这个数据结构。 + +本文整体脉络如下图所示,笔者会从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005468.png) + +## 跳表在 Redis 中的运用 + +这里我们需要先了解一下 Redis 用到跳表的数据结构有序集合的使用,Redis 有个比较常用的数据结构叫**有序集合(sorted set,简称 zset)**,正如其名它是一个可以保证有序且元素唯一的集合,所以它经常用于排行榜等需要进行统计排列的场景。 + +这里我们通过命令行的形式演示一下排行榜的实现,可以看到笔者分别输入 3 名用户:**xiaoming**、**xiaohong**、**xiaowang**,它们的**score**分别是 60、80、60,最终按照成绩升级降序排列。 + +```bash + +127.0.0.1:6379> zadd rankList 60 xiaoming +(integer) 1 +127.0.0.1:6379> zadd rankList 80 xiaohong +(integer) 1 +127.0.0.1:6379> zadd rankList 60 xiaowang +(integer) 1 + +# 返回有序集中指定区间内的成员,通过索引,分数从高到低 +127.0.0.1:6379> ZREVRANGE rankList 0 100 WITHSCORES +1) "xiaohong" +2) "80" +3) "xiaowang" +4) "60" +5) "xiaoming" +6) "60" +``` + +此时我们通过 `object` 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是**ziplist(压缩列表)**。 + +```bash +127.0.0.1:6379> object encoding rankList +"ziplist" +``` + +因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间,在有序集合元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。 + +```bash +zset-max-ziplist-value 64 +zset-max-ziplist-entries 128 +``` + +一旦有序集合中的某个元素超出这两个其中的一个阈值它就会转为 **skiplist**(实际是 dict+skiplist,还会借用字典来提高获取指定元素的效率)。 + +我们不妨在添加一个大于 64 字节的元素,可以看到有序集合的底层存储转为 skiplist。 + +```bash +127.0.0.1:6379> zadd rankList 90 yigemingzihuichaoguo64zijiedeyonghumingchengyongyuceshitiaobiaodeshijiyunyong +(integer) 1 + +# 超过阈值,转为跳表 +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) + +Finally, let’s talk about deletion. Suppose we want to delete element 10 here. We must locate the maximum value of the current jump table **each layer** element that is less than 10. The index execution steps are: + +1. The successor node of level 2 index 4 is 8, and the pointer advances. +2. Index 8 has no successor node, there is no element to be deleted in this layer, and the pointer points directly downward. +3. The successor node of level 1 index 8 is 10, which means that when deleting level 1 index 8, it needs to disconnect its pointer from level 1 index 10 and delete 10. +4. After the level 1 index is positioned, the pointer moves downward, the subsequent node is 9, and the pointer advances. +5. The successor node of 9 is 10. Similarly, it needs to point to null and delete 10. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005503.png) + +### Template definition + +After having the overall idea, we can start to implement a skip list. First, define the node **Node** in the skip list. From the above demonstration, we can see that each **Node** contains the following elements: + +1. The stored **value** value. +2. The address of the successor node. +3. Multi-level index. + +In order to more conveniently and uniformly manage the **Node** successor node addresses and the element addresses pointed to by the multi-level index, the author set up a **forwards** array in **Node** to record the successor nodes of the original linked list node and the successor node points of the multi-level index. + +Take the following figure as an example. The length of our **forwards** array is 5, in which **index 0** records the successor node address of the original linked list node, while the rest from bottom to top represent the successor node points from the 1st level index to the 4th level index. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005347.png) + +So we have such a code definition. It can be seen that the author sets the length of the array to a fixed 16** (the maximum height calculated above is recommended to be 16)**. The default **data** is -1, and the maximum height of the node **maxLevel** is initialized to 1. Note that the value of **maxLevel** represents the total height of the original linked list plus the index. + +```java +/** + * The maximum height of the skip table index is 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; + +} +``` + +### Element addition + +After defining the node, we first add the following elements. When adding elements, the first step is to set **data**. We can directly set the incoming **value** to **data**. + +Then there is the setting of the height **maxLevel**. We have also given the idea above. The default height is 1, that is, there is only one original linked list node. Through the random algorithm, the index height is increased by 1 every time it is greater than 0.5. From this, we derive the height calculation algorithm `randomLevel()`: + +```java +/** + * Theoretically, the number of elements in the primary index should account for 50% of the original data, the number of elements in the secondary index should account for 25%, and the third-level index should account for 12.5%, all the way to the top. + * Because the promotion probability of each level here is 50%. For each newly inserted node, randomLevel needs to be called to generate a reasonable number of levels. + * The randomLevel method will randomly generate a number between 1~MAX_LEVEL, and: + * 50% probability of returning 1 + * 25% probability of returning 2 + * 12.5% probability of returning 3... + * @return + */ +private int randomLevel() { + int level = 1; + while (Math.random() > PROB && level < MAX_LEVEL) { + ++level; + } + return level; +} +``` + +Then set the successor node address of the **Node** and **Node** index currently to be inserted. This step is a little more complicated. We assume that the height of the current node is 4, that is, 1 node plus 3 indexes, so we create an array **maxOfMinArr** with a length of 4, and traverse the maximum value of the index nodes at all levels that is smaller than the current **value**. + +Assume that the **value** we want to insert is 5. Our array search result shows that the predecessor node of the current node and the predecessor nodes of the first-level index and the second-level index are all 4, and the third-level index is empty. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005299.png) + +Then we locate the successor nodes at all levels based on this array **maxOfMinArr**, so that the inserted element 5 points to these successor nodes, and **maxOfMinArr** points to 5. The result is as follows: + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005369.png) + +The conversion into code is in the following form. Isn't it very simple? Let's continue: + +```java +/** + * The default height is 1, that is, there is only one node of its own. + */ +private int levelCount = 1; + +/** + * The node at the bottom of the jump list, that is, the head node + */ +private Node h = new Node(); + +public void add(int value) { + int level = randomLevel(); // Random height of new node + + Node newNode = new Node(); + newNode.data = value; + newNode.maxLevel = level; + + //An array used to record the predecessor nodes of each layer + Node[] update = new Node[level]; + for (int i = 0; i < level; i++) { + update[i] = h; + } + + Node p = h; + // Key correction: Start searching from the current highest level of the jump list + for (int i = levelCount - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + //Only record the predecessor nodes of the layer that needs to be updated + if (i < level) { + update[i] = p; + } + } + + //Insert new node + for (int i = 0; i < level; i++) { + newNode.forwards[i] = update[i].forwards[i]; + update[i].forwards[i] = newNode; + } + + //Update the total height of the jump table + if (levelCount < level) { + levelCount = level; + } +} +``` + +### Element query + +The query logic is relatively simple. Start from the highest-level index of the jump table and locate the maximum value that is less than the value to be queried. Take the following figure as an example. We hope to find node 8: + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005323.png) + +- **Start from the highest level (level 3 index)**: The search pointer `p` starts from the head node. On a level 3 index, `p`'s successor `forwards[2]` (assuming level 3 up, indexing starts at 0) points to node `5`. Since `5 < 8`, pointer `p` moves right to node `5`. The successor `forwards[2]` of node `5` at the level 3 index is `null` (or points to a node greater than `8`, not shown in the figure). The search to the right of the current level ends, pointer `p` remains at node `5`, and moves down to the level 2 index**. +- **Indexed at Level 2**: Current pointer `p` is node `5`. The successor `forwards[1]` of `p` points to node `8`. Since `8` is not less than `8` (i.e. `8 < 8` is `false`), the current level search to the right ends (`p` will not move to node `8`). Pointer `p` remains at node `5` and moves down to index level 1**.- **在 1 级索引** :当前指针 `p` 为节点 `5`。`p` 的后继 `forwards[0]` 指向最底层的节点 `5`。由于 `5 < 8`,指针 `p` 向右移动到最底层的节点 `5`。此时,当前指针 `p` 为最底层的节点 `5`。其后继 `forwards[0]` 指向最底层的节点 `6`。由于 `6 < 8`,指针 `p` 向右移动到最底层的节点 `6`。当前指针 `p` 为最底层的节点 `6`。其后继 `forwards[0]` 指向最底层的节点 `7`。由于 `7 < 8`,指针 `p` 向右移动到最底层的节点 `7`。当前指针 `p` 为最底层的节点 `7`。其后继 `forwards[0]` 指向最底层的节点 `8`。由于 `8` 不小于 `8`(即 `8 < 8` 为 `false`),当前层级向右查找结束。此时,已经遍历完所有层级,`for` 循环结束。 +- **最终定位与检查** :经过所有层级的查找,指针 `p` 最终停留在最底层(0 级索引)的节点 `7`。这个节点是整个跳表中值小于目标值 `8` 的那个最大的节点。检查节点 `7` 的**后继节点**(即 `p.forwards[0]`):`p.forwards[0]` 指向节点 `8`。判断 `p.forwards[0].data`(即节点 `8` 的值)是否等于目标值 `8`。条件满足(`8 == 8`),**查找成功,找到节点 `8`**。 + +所以我们的代码实现也很上述步骤差不多,从最高级索引开始向前查找,如果不为空且小于要查找的值,则继续向前搜寻,遇到不小于的节点则继续向下,如此往复,直到得到当前跳表中小于查找值的最大节点,查看其前驱是否等于要查找的值: + +```java +public Node get(int value) { + Node p = h; // 从头节点开始 + + // 从最高层级索引开始,逐层向下 + for (int i = levelCount - 1; i >= 0; i--) { + // 在当前层级向右查找,直到 p.forwards[i] 为 null + // 或者 p.forwards[i].data 大于等于目标值 value + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; // 向右移动 + } + // 此时 p.forwards[i] 为 null,或者 p.forwards[i].data >= value + // 或者 p 是当前层级中小于 value 的最大节点(如果存在这样的节点) + } + + // 经过所有层级的查找,p 现在是原始链表(0级索引)中 + // 小于目标值 value 的最大节点(或者头节点,如果所有元素都大于等于 value) + + // 检查 p 在原始链表中的下一个节点是否是目标值 + 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.5f; + + /** + * 默认情况下的高度为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[] update = new Node[level]; + for (int i = 0; i < level; i++) { + update[i] = h; + } + + Node p = h; + // 关键修正:从跳表的当前最高层开始查找 + for (int i = levelCount - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + // 只记录需要更新的层的前驱节点 + if (i < level) { + update[i] = p; + } + } + + // 插入新节点 + for (int i = 0; i < level; i++) { + newNode.forwards[i] = update[i].forwards[i]; + update[i].forwards[i] = newNode; + } + + // 更新跳表的总高度 + 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]; + } + + } +} + +``` + +Test code: + +```java +public static void main(String[] args) { + SkipList skipList = new SkipList(); + for (int i = 0; i < 24; i++) { + skipList.add(i); + } + + System.out.println("************Output added results**********"); + skipList.printAll(); + + SkipList.Node node = skipList.get(22); + System.out.println("************query results:" + node+" ************"); + + skipList.delete(22); + System.out.println("************Delete result**********"); + skipList.printAll(); + + + } +``` + +**Features of Redis skip table**: + +1. Using **doubly linked list**, different from the above example, there is a rollback pointer. It is mainly used to simplify operations. For example, when deleting an element, you also need to find the predecessor node of the element. It is very convenient to use the rollback pointer. +2. The `score` value can be repeated. If the `score` value is the same, it will be sorted according to the ele (the value stored in the node, which is sds) dictionary. +3. The default maximum number of levels allowed by the Redis jump table is 32, which is defined by `ZSKIPLIST_MAXLEVEL` in the source code. + +## Comparison with other three data structures + +Finally, let’s answer the interview question at the beginning of the article: “Why does the bottom layer of Redis’s ordered set use a jump table instead of a balanced tree, a red-black tree, or a B+ tree?” + +### Balanced tree vs skip list + +Let’s first talk about its comparison with the balanced tree. The balanced tree is also called the AVL tree. It is a strictly balanced binary tree. The balance condition must be met (the height difference between the left and right subtrees of all nodes does not exceed 1, that is, the balance factor is in the range `[-1,1]`). The time complexity of insertion, deletion and query of the balanced tree is the same as that of the skip table **O(log n)**. + +For range queries, it can also achieve the same effect as skipping tables through in-order traversal. However, every insertion or deletion operation needs to ensure the absolute balance of the left and right nodes of the entire tree. As long as it is unbalanced, it must be maintained through rotation operations. This process is relatively time-consuming. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005312.png) + +The original intention of the birth of skip lists is to overcome some shortcomings of balanced trees. The inventor of skip lists mentioned in detail in the paper ["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. +> +> A skip list is a data structure that can be used to replace a balanced tree. Skip lists use probabilistic balancing rather than strictly enforced balancing, so the insertion and deletion algorithms in skip lists are much simpler and faster than the equivalent algorithms for balanced trees. + +The author has also posted the core code of the AVL tree insertion operation here. It can be seen that each addition operation requires a recursive positioning to locate the insertion position, and then it is necessary to trace back to the root node to check whether the nodes at each layer along the way are unbalanced, and then adjust the nodes by rotating them. + +```java +//Add new elements (key, value) to the binary search tree +public void add(K key, V value) { + root = add(root, key, value); +} + +// Insert elements (key, value) into the binary search tree rooted at node, recursive algorithm +// Return the root of the binary search tree after inserting the new node +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 type requires right-hand rotation + if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) { + return rightRotate(node); + } + + //RR type imbalance requires left rotation + if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) { + return leftRotate(node); + } + + //LR needs to be rotated left first into LL shape, and then rotated right + 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; +} +``` + +### Red-black tree vs jump list + +Red Black Tree is also a self-balancing binary search tree. Its query performance is slightly inferior to AVL tree, but insertion and deletion are more efficient. The time complexity of insertion, deletion and query of red-black tree is the same as that of skip table **O(log n)**. + +A red-black tree is a **black balanced tree**, that is, from any node to another leaf node, the black nodes it passes through are the same. When inserting it, it needs to be rotated and dyed (red-to-black conversion) to ensure black balance. However, the overhead of maintaining balance is smaller than that of an AVL tree. For a detailed introduction to red-black trees, you can view this article: [Red-Black Tree](https://javaguide.cn/cs-basics/data-structure/red-black-tree.html). + +Compared with red-black trees, the implementation of skip tables is also simpler. Moreover, when searching for data according to intervals, the efficiency of red-black trees is not as high as that of jump tables. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005709.png) + +The core code added corresponding to the red-black tree is as follows, readers can refer to and understand by themselves: + +```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; + } + + //The left node is not red, the right node is red, left rotation + if (isRed(node.right) && !isRed(node.left)) { + node = leftRotate(node); + } + + //Left chain right-hand rotation + if (isRed(node.left) && isRed(node.left.left)) { + node = rightRotate(node); + } + + //color flip + if (isRed(node.left) && isRed(node.right)) { + flipColors(node); + } + + return node; +}``` + +### B+ tree vs skip list + +Readers who use MySQL must know the data structure of B+ tree. B+ tree is a commonly used data structure with the following characteristics: + +1. **Multi-fork tree structure**: It is a multi-fork tree. Each node can contain multiple child nodes, which reduces the height of the tree and has high query efficiency. +2. **High storage efficiency**: Non-leaf nodes store multiple keys, and leaf nodes store values, so that each node can store more keys, and the query efficiency is higher when performing range queries based on the index. - +3. **Balance**: It is absolutely balanced, that is, the height of each branch of the tree is not much different, ensuring that the query and insertion time complexity is **O(log n)**. +4. **Sequential access**: Leaf nodes are connected through linked list pointers, and range queries perform well. +5. **Uniform distribution of data**: B+ tree insertion may cause data to be redistributed, making the data more evenly distributed throughout the tree and ensuring range query and deletion efficiency. + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005649.png) + +Therefore, B+ tree is more suitable as one of the commonly used index structures in databases and file systems. Its core idea is to obtain query data by locating as many indexes as possible with as little IO as possible. For in-memory databases such as Redis, it is not interested in these, because Redis, as an in-memory database, cannot store a large amount of data, so the index does not need to be maintained through B+ trees. It only needs to be randomly maintained according to probability, saving memory. Moreover, using skip tables to implement zset is simpler than the former. When inserting, you only need to insert the data into the appropriate position in the linked list through the index and then randomly maintain an index of a certain height. There is no need to split and merge nodes when an imbalance is found during insertion like the B+ tree. + +### Reasons given by the author of Redis + +Of course, we can also use the reasons given by the author of Redis himself: + +> 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 requires little changes to the code. + +Translated it means: + +> There are several reasons: +> +> 1. They don’t take up much memory. It's mostly up to you. Changing the parameters of a node's probability of having a given number of levels makes them more memory efficient than B-trees. +> +> 2. Sorted sets are often the target of many ZRANGE or ZREVRANGE operations, that is, traversing the jump list in a linked list. With this operation, the cache locality of skip lists is at least as good as other types of balanced trees. +> +> 3. They are easier to implement, debug, etc. For example, due to the simplicity of skip tables, I received a patch (already in the Redis master branch) to achieve O(log(N)) ZRANK with enhanced skip tables. It requires only minimal modifications to the code. + +## Summary + +This article introduces the working principle and implementation of skip tables through a large amount of space to help readers become more familiar with the advantages and disadvantages of the skip table data structure. Finally, it compares the characteristics of each data structure operation to help readers better understand this interview question. It is recommended that when readers understand skip tables, they should cooperate with writing simulations as much as possible to understand the detailed process of adding, deleting, modifying and checking skip tables. + +## Reference + +- Why does redis use skiplist instead of red-black? : +- Skip List--Skip List (one of the most detailed skip list articles on the entire Internet): +- Detailed explanation of Redis objects and underlying data structures: +- Redis ordered set (sorted set): +- Comparison of red-black trees and skip tables: +- Why does redis's zset use jump tables instead of b+ trees? : \ No newline at end of file diff --git a/docs_en/database/sql/sql-questions-01.en.md b/docs_en/database/sql/sql-questions-01.en.md new file mode 100644 index 00000000000..36be8817d1c --- /dev/null +++ b/docs_en/database/sql/sql-questions-01.en.md @@ -0,0 +1,1822 @@ +--- +title: Summary of common SQL interview questions (1) +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL interview questions, query, grouping, sorting, connection, subquery, aggregation + - - meta + - name: description + content: Contains basic SQL high-frequency questions and solutions, covering typical scenarios such as query/grouping/sorting/joining, etc., emphasizing the balance between readability and performance. +--- + +> The question comes from: [Niuke Question Ba - SQL must be known and mastered](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=298) + +## Retrieve data + +`SELECT` is used to query data from the database. + +### Retrieve all IDs from the Customers table + +The existing table `Customers` is as follows: + +| cust_id | +| ------- | +| A | +| B | +| C | + +Write SQL statement to retrieve all `cust_id` from `Customers` table. + +Answer: + +```sql +SELECT cust_id +FROM Customers +``` + +### Retrieve and list a list of ordered products + +The table `OrderItems` contains a non-empty column `prod_id`, which represents the item id and contains all ordered items (some have been ordered multiple times). + +| prod_id | +| ------- | +|a1| +|a2| +| a3 | +| a4 | +| a5 | +| a6 | +| a7 | + +Write a SQL statement to retrieve and list the deduplicated list of all ordered items (`prod_id`). + +Answer: + +```sql +SELECT DISTINCT prod_id +FROM OrderItems +``` + +Knowledge point: `DISTINCT` is used to return the unique distinct value in a column. + +### Retrieve all columns + +Now there is a `Customers` table (the table contains columns `cust_id` represents the customer id and `cust_name` represents the customer name) + +| cust_id | cust_name | +| ------- | --------- | +| a1 | andy | +| a2 | ben | +| a3 | tony | +| a4 | tom | +| a5 | an | +| a6 | lee | +| a7 | hex | + +You need to write SQL statements to retrieve all columns. + +Answer: + +```sql +SELECT cust_id, cust_name +FROM Customers +``` + +## Sort retrieval data + +`ORDER BY` is used to sort the result set by one column or multiple columns. By default, records are sorted in ascending order. If you need to sort records in descending order, you can use the `DESC` keyword. + +### Retrieve customer names and sort them + +There is a table `Customers`, `cust_id` represents the customer id, `cust_name` represents the customer name. + +| cust_id | cust_name | +| ------- | --------- | +| a1 | andy | +| a2 | ben | +| a3 | tony | +| a4 | tom | +| a5 | an | +| a6 | lee | +| a7 | hex | + +Retrieve all customer names (`cust_name`) from `Customers` and display the results in order from Z to A. + +Answer: + +```sql +SELECT cust_name +FROM Customers +ORDER BY cust_name DESC +``` + +### Sort by customer ID and date + +There is `Orders` table: + +| cust_id | order_num | order_date | +| ------- | --------- | ------------------- | +| andy | aaaa | 2021-01-01 00:00:00 | +| andy | bbbb | 2021-01-01 12:00:00 | +| bob | cccc | 2021-01-10 12:00:00 | +| dick | dddd | 2021-01-11 00:00:00 | + +Write a SQL statement to retrieve the customer ID (`cust_id`) and order number (`order_num`) from the `Orders` table, and sort the results first by customer ID, and then in reverse order by order date. + +Answer: + +```sql +# Sort by column name +# Note: order_date descending order, not order_num +SELECT cust_id, order_num +FROM Orders +ORDER BY cust_id,order_date DESC +``` + +Knowledge point: `order by` When sorting multiple columns, the columns that are sorted first are placed in front, and the columns that are sorted later are placed in the back. Also, different columns can have different sorting rules. + +### Sort by quantity and price + +Suppose there is an `OrderItems` table: + +| quantity | item_price | +| -------- | ---------- | +| 1 | 100 | +| 10 | 1003 | +| 2 | 500 | + +Write a SQL statement to display the quantity (`quantity`) and price (`item_price`) in the `OrderItems` table, and sort them by quantity from high to low and price from high to low. + +Answer: + +```sql +SELECT quantity, item_price +FROM OrderItems +ORDER BY quantity DESC,item_price DESC +``` + +### Check SQL statement + +There is a `Vendors` table: + +| vend_name | +| --------- | +| Haidilao | +| Xiaolongkan | +| Dalongyi | + +Is there something wrong with the following SQL statement? Try to correct it so that it runs correctly and returns the results in reverse order according to `vend_name`. + +```sql +SELECT vend_name, +FROM Vendors +ORDER vend_name DESC +``` + +After correction: + +```sql +SELECT vend_name +FROM Vendors +ORDER BY vend_name DESC +``` + +Knowledge points: + +- Commas are used to separate columns. +- ORDER BY contains BY, which needs to be written completely and in the correct position. + +## Filter data + +`WHERE` can filter the returned data. + +The following operators can be used in the `WHERE` clause: + +| Operator | Description | +| :------ | :--------------------------------------------------------------- | +| = | equals | +| <> | is not equal to. **Note:** In some versions of SQL, this operator can be written as != | +| > | greater than | +| < | less than | +| >= | Greater than or equal to | +| <= | Less than or equal to | +| BETWEEN | Within a range | +| LIKE | Search for a pattern | +| IN | Specifies multiple possible values for a column | + +### Return fixed price products + +There is table `Products`: + +| prod_id | prod_name | prod_price | +| ------- | --------------- | ---------- || a0018 | sockets | 9.49 | +| a0019 | iphone13 | 600 | +| b0018 | gucci t-shirts | 1000 | + +[Problem] Retrieving the product ID (`prod_id`) and product name (`prod_name`) from the `Products` table only returns products with a price of $9.49. + +Answer: + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_price = 9.49 +``` + +### Return higher priced products + +There is table `Products`: + +| prod_id | prod_name | prod_price | +| ------- | --------------- | ---------- | +| a0018 | sockets | 9.49 | +| a0019 | iphone13 | 600 | +| b0019 | gucci t-shirts | 1000 | + +[Problem] Write a SQL statement to retrieve the product ID (`prod_id`) and product name (`prod_name`) from the `Products` table, returning only products with a price of $9 or more. + +Answer: + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_price >= 9 +``` + +### Return products and sort them by price + +There is table `Products`: + +| prod_id | prod_name | prod_price | +| ------- | --------- | ---------- | +| a0011 | egg | 3 | +| a0019 | sockets | 4 | +| b0019 | coffee | 15 | + +[Question] Write a SQL statement to return the name (`prod_name`) and price (`prod_price`) of all products in the `Products` table with a price between $3 and $6, and then sort the results by price. + +Answer: + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price BETWEEN 3 AND 6 +ORDER BY prod_price + +# or +SELECT prod_name, prod_price +FROM Products +WHERE prod_price >= 3 AND prod_price <= 6 +ORDER BY prod_price +``` + +### Return to more products + +The `OrderItems` table contains: order number `order_num`, `quantity` product quantity + +| order_num | quantity | +| --------- | -------- | +| a1 | 105 | +| a2 | 1100 | +| a2 | 200 | +| a4 | 1121 | +| a5 | 10 | +| a2 | 19 | +| a7 | 5 | + +[Problem] Retrieve all distinct and unique order numbers (`order_num`) from the `OrderItems` table, where each order must contain 100 or more products. + +Answer: + +```sql +SELECT order_num +FROM OrderItems +GROUP BY order_num +HAVING SUM(quantity) >= 100 +``` + +## Advanced data filtering + +The `AND` and `OR` operators are used to filter records based on more than one condition and can be used in combination. `AND` requires both conditions to be true, and `OR` only needs one of the 2 conditions to be true. + +### Retrieve supplier name + +The `Vendors` table has fields vendor name (`vend_name`), vendor country (`vend_country`), vendor state (`vend_state`) + +| vend_name | vend_country | vend_state | +| --------- | ------------ | ---------- | +| apple | USA | CA | +| vivo | CNA | shenzhen | +| huawei | CNA | xian | + +[Problem] Write a SQL statement to retrieve the vendor name (`vend_name`) from the `Vendors` table and return only suppliers in California (this needs to be filtered by country [USA] and state [CA], maybe there is a CA in other countries) + +Answer: + +```sql +SELECT vend_name +FROM Vendors +WHERE vend_country = 'USA' AND vend_state = 'CA' +``` + +### Retrieve and list a list of ordered products + +The `OrderItems` table contains all ordered products (some have been ordered multiple times). + +| 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 | + +[Question] Write a SQL statement to find all orders for `BR01`, `BR02` or `BR03` with a quantity of at least 100 units. You need to return the order number (`order_num`), product ID (`prod_id`) and quantity (`quantity`) of the `OrderItems` table and filter by product ID and quantity. + +Answer: + +```sql +SELECT order_num, prod_id, quantity +FROM OrderItems +WHERE prod_id IN ('BR01', 'BR02', 'BR03') AND quantity >= 100 +``` + +### Returns the name and price of all products priced between $3 and $6 + +There is table `Products`: + +| prod_id | prod_name | prod_price | +| ------- | --------- | ---------- | +| a0011 | egg | 3 | +| a0019 | sockets | 4 | +| b0019 | coffee | 15 | + +[Question] Write a SQL statement to return the name (`prod_name`) and price (`prod_price`) of all products with a price between $3 and $6, use the AND operator, and then sort the results in ascending order by price. + +Answer: + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price >= 3 and prod_price <= 6 +ORDER BY prod_price +``` + +### Check SQL statement + +The suppliers table `Vendors` has fields supplier name `vend_name`, supplier country `vend_country`, supplier province `vend_state` + +| vend_name | vend_country | vend_state | +| --------- | ------------ | ---------- | +| apple | USA | CA | +| vivo | CNA | shenzhen | +| huawei | CNA | xian | + +[Problem] Modify the following sql correctly so that it can be returned correctly. + +```sql +SELECT vend_name +FROM Vendors +ORDER BY vend_name +WHERE vend_country = 'USA' AND vend_state = 'CA'; +``` + +After modification: + +```sql +SELECT vend_name +FROM Vendors +WHERE vend_country = 'USA' AND vend_state = 'CA' +ORDER BY vend_name +``` + +The `ORDER BY` statement must be placed after `WHERE`. + +## Use wildcards to filterSQL wildcards must be used with the `LIKE` operator + +In SQL, the following wildcard characters can be used: + +| wildcard | description | +| :---------------------------------- | :---------------------------------- | +| `%` | represents zero or more characters | +| `_` | Replaces only one character | +| `[charlist]` | Any single character in a character list | +| `[^charlist]` or `[!charlist]` | Any single character not in the character list | + +### Retrieve product name and description (1) + +The `Products` table is as follows: + +| prod_name | prod_desc | +| ---------- | --------------- | +| a0011 | usb | +| a0019 | iphone13 | +| b0019 | gucci t-shirts | +| c0019 | gucci toy | +| d0019 | lego toy | + +[Problem] Write a SQL statement to retrieve the product name (`prod_name`) and description (`prod_desc`) from the `Products` table and return only the product names that contain the word `toy` in the description. + +Answer: + +```sql +SELECT prod_name, prod_desc +FROM Products +WHERE prod_desc LIKE '%toy%' +``` + +### Retrieve product name and description (2) + +The `Products` table is as follows: + +| prod_name | prod_desc | +| ---------- | --------------- | +| a0011 | usb | +| a0019 | iphone13 | +| b0019 | gucci t-shirts | +| c0019 | gucci toy | +| d0019 | lego toy | + +[Problem] Write a SQL statement to retrieve the product name (`prod_name`) and description (`prod_desc`) from the `Products` table, return only the products where the word `toy` does not appear in the description, and finally sort the results by "product name". + +Answer: + +```sql +SELECT prod_name, prod_desc +FROM Products +WHERE prod_desc NOT LIKE '%toy%' +ORDER BY prod_name +``` + +### Retrieve product name and description (3) + +The `Products` table is as follows: + +| prod_name | prod_desc | +| --------- | ---------------- | +| a0011 | usb | +| a0019 | iphone13 | +| b0019 | gucci t-shirts | +| c0019 | gucci toy | +| d0019 | lego carrots toy | + +[Problem] Write a SQL statement to retrieve the product name (`prod_name`) and description (`prod_desc`) from the `Products` table, and only return products where `toy` and `carrots` appear in the description. There are several ways to do this, but for this challenge, use `AND` and two `LIKE` comparisons. + +Answer: + +```sql +SELECT prod_name, prod_desc +FROM Products +WHERE prod_desc LIKE '%toy%' AND prod_desc LIKE "%carrots%" +``` + +### Retrieve product name and description (4) + +The `Products` table is as follows: + +| prod_name | prod_desc | +| --------- | ---------------- | +| a0011 | usb | +| a0019 | iphone13 | +| b0019 | gucci t-shirts | +| c0019 | gucci toy | +| d0019 | lego toy carrots | + +[Problem] Write a SQL statement to retrieve the product name (prod_name) and description (prod_desc) from the Products table, and only return products in which toys and carrots appear in **sequence** in the description. Tip: Just use `LIKE` with three `%` symbols. + +Answer: + +```sql +SELECT prod_name, prod_desc +FROM Products +WHERE prod_desc LIKE '%toy%carrots%' +``` + +## Create a calculated field + +### Alias + +A common use of aliases is to rename table column fields in retrieved results (to meet specific reporting requirements or customer needs). There is a table `Vendors` representing supplier information, `vend_id` supplier id, `vend_name` supplier name, `vend_address` supplier address, `vend_city` supplier 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 | + +[Problem] Write a SQL statement to retrieve `vend_id`, `vend_name`, `vend_address` and `vend_city` from the `Vendors` table, rename `vend_name` to `vname`, rename `vend_city` to `vcity`, rename `vend_address` to `vaddress`, and sort the results in ascending order by vendor name. + +Answer: + +```sql +SELECT vend_id, vend_name AS vname, vend_address AS vaddress, vend_city AS vcity +FROM Vendors +ORDER BY vname +# as can be omitted +SELECT vend_id, vend_name vname, vend_address vaddress, vend_city vcity +FROM Vendors +ORDER BY vname +``` + +### Discount + +Our example store is running a sale with 10% off all products. The `Products` table contains `prod_id` product id, `prod_price` product price. + +[Question] Write SQL statements to return `prod_id`, `prod_price` and `sale_price` from the `Products` table. `sale_price` is a calculated field containing the promotional price. Tip: You can multiply by 0.9 to get 90% of the original price (i.e. 10% discount). + +Answer: + +```sql +SELECT prod_id, prod_price, prod_price * 0.9 AS sale_price +FROM Products +``` + +Note: `sale_price` is the name of the calculation result, not the original column name. + +## Use functions to process data + +### Customer login name + +Our store is online and customer accounts are being created. All users require a login name, and the default login name is a combination of their name and city. + +The `Customers` table is given as follows: + +| 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 | + +[Question] Write a SQL statement to return the customer ID (`cust_id`), customer name (`cust_name`) and login name (`user_login`), where the login name is all uppercase letters and consists of the first two characters of the customer's contact person (`cust_contact`) and the first three characters of the city (`cust_city`). Tip: Requires the use of functions, concatenation and aliases. + +Answer: + +```sql +SELECT cust_id, cust_name, UPPER(CONCAT(SUBSTRING(cust_contact, 1, 2), SUBSTRING(cust_city, 1, 3))) AS user_login +FROM Customers +``` + +Knowledge points: + +- Interception function `SUBSTRING()`: intercepts a string, `substring(str,n,m)` (n represents the starting interception position, m represents the number of characters to be intercepted) means that the returned string str intercepts m characters starting from the nth character; +- Concatenation function `CONCAT()`: concatenates two or more strings into one string, select concat(A,B): concatenates strings A and B. + +- Uppercase function `UPPER()`: Converts the specified string to uppercase. + +### Return the order number and order date of all orders in January 2020 + +`Orders` order table is as follows: + +| 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 | + +[Question] Write a SQL statement to return the order number (`order_num`) and order date (`order_date`) of all orders in January 2020, and sort them in ascending order by order date + +Answer: + +```sql +SELECT order_num, order_date +FROM Orders +WHERE month(order_date) = '01' AND YEAR(order_date) = '2020' +ORDER BY order_date +``` + +You can also use wildcards to do this: + +```sql +SELECT order_num, order_date +FROM Orders +WHERE order_date LIKE '2020-01%' +ORDER BY order_date +``` + +Knowledge points: + +- Date format: `YYYY-MM-DD` +- Time format: `HH:MM:SS` + +Commonly used functions related to date and time processing: + +| function | description | +| --------------- | ------------------------------- | +| `ADDDATE()` | Add a date (day, week, etc.) | +| `ADDTIME()` | Add a time (hour, minute, etc.) | +| `CURDATE()` | Returns the current date | +| `CURTIME()` | Return the current time | +| `DATE()` | Returns the date part of a datetime | +| `DATEDIFF` | Calculate the difference between two dates | +| `DATE_FORMAT()` | Returns a formatted date or time string | +| `DAY()` | Returns the day part of a date | +| `DAYOFWEEK()` | For a date, return the corresponding day of the week | +| `HOUR()` | Returns the hour part of a time | +| `MINUTE()` | Returns the minute part of a time | +| `MONTH()` | Returns the month part of a date | +| `NOW()` | Returns the current date and time | +| `SECOND()` | Returns the seconds part of a time | +| `TIME()` | Returns the time part of a datetime | +| `YEAR()` | Returns the year part of a date | + +## Summary data + +Functions related to summary data: + +| function | description | +| --------- | ---------------- | +| `AVG()` | Returns the average value of a column | +| `COUNT()` | Returns the number of rows in a column | +| `MAX()` | Returns the maximum value of a column | +| `MIN()` | Returns the minimum value of a column | +| `SUM()` | Returns the sum of values in a column | + +### Determine the total number of products sold + +The `OrderItems` table represents the products sold, and `quantity` represents the number of items sold. + +| quantity | +|--------| +| 10 | +| 100 | +| 1000 | +| 10001 | +| 2 | +| 15 | + +[Question] Write a SQL statement to determine the total number of products sold. + +Answer: + +```sql +SELECT Sum(quantity) AS items_ordered +FROM OrderItems +``` + +### Determine the total number of product items BR01 sold + +The `OrderItems` table represents the products sold, `quantity` represents the quantity of items sold, and the product item is `prod_id`. + +| quantity | prod_id | +| -------- | ------- | +| 10 | AR01 | +| 100 | AR10 | +| 1000 | BR01 | +| 10001 | BR010 | + +[Problem] Modify the created statement to determine the total number of product items (`prod_id`) sold as "BR01". + +Answer: + +```sql +SELECT Sum(quantity) AS items_ordered +FROM OrderItems +WHERE prod_id = 'BR01' +``` + +### Determine the price of the most expensive product in the Products table that is $10 or less + +The `Products` table is as follows, `prod_price` represents the price of the product. + +| prod_price | +| ---------- | +| 9.49 | +| 600 | +| 1000 | + +[Question] Write a SQL statement to determine the price (`prod_price`) of the most expensive product in the `Products` table whose price does not exceed $10. Name the calculated field `max_price`. + +Answer: + +```sql +SELECT Max(prod_price) AS max_price +FROM Products +WHERE prod_price <= 10 +``` + +## Grouped data + +`GROUP BY`: + +- The `GROUP BY` clause groups records into summary rows. +- `GROUP BY` returns one record for each group. +- `GROUP BY` usually also involves aggregating `COUNT`, `MAX`, `SUM`, `AVG`, etc. +- `GROUP BY` can group by one or more columns. +- `GROUP BY` After sorting by grouping fields, `ORDER BY` can sort by summary fields. + +`HAVING`: + +- `HAVING` is used to filter aggregated `GROUP BY` results. +- `HAVING` must be used with `GROUP BY`. +- `WHERE` and `HAVING` can be in the same query. + +`HAVING` vs `WHERE`: + +- `WHERE`: Filter the specified rows, and aggregate functions (grouping functions) cannot be added later. +- `HAVING`: Filter grouping, must be used together with `GROUP BY`, cannot be used alone. + +### Return the number of rows for each order number + +The `OrderItems` table contains every product for every order + +| order_num | +| --------- | +| a002 | +| a002 | +| a002 | +| a004 | +| a007 | + +[Question] Write a SQL statement to return the number of rows (`order_lines`) for each order number (`order_num`), and sort the results in ascending order by `order_lines`. + +Answer: + +```sql +SELECT order_num, Count(order_num) AS order_lines +FROM OrderItems +GROUP BY order_num +ORDER BY order_lines``` + +Knowledge points: + +1. Both `count(*)` and `count(column name)` are acceptable. The difference is that `count(column name)` counts the number of non-NULL rows; +2. `order by` is executed last, so column aliases can be used; +3. Don’t forget to add `group by` when performing group aggregation, otherwise there will only be one row of results. + +### Lowest cost product per supplier + +There is a `Products` table with fields `prod_price` representing the product price and `vend_id` representing the supplier id. + +| vend_id | prod_price | +| ------- | ---------- | +| a0011 | 100 | +| a0019 | 0.1 | +| b0019 | 1000 | +| b0019 | 6980 | +| b0019 | 20 | + +[Question] Write a SQL statement that returns a field named `cheapest_item` that contains the lowest cost product from each supplier (using `prod_price` from the `Products` table), and then sorts the results in ascending order from lowest cost to highest cost. + +Answer: + +```sql +SELECT vend_id, Min(prod_price) AS cheapest_item +FROM Products +GROUP BY vend_id +ORDER BY cheapest_item +``` + +### Return the order numbers of all orders whose total order quantity is not less than 100 + +`OrderItems` represents the order item table, including: order number `order_num` and order quantity `quantity`. + +| order_num | quantity | +| --------- | -------- | +| a1 | 105 | +| a2 | 1100 | +| a2 | 200 | +| a4 | 1121 | +| a5 | 10 | +| a2 | 19 | +| a7 | 5 | + +[Question] Please write a SQL statement to return all order numbers whose total order quantity is not less than 100. The final results are sorted in ascending order by order number. + +Answer: + +```sql +# Direct aggregation +SELECT order_num +FROM OrderItems +GROUP BY order_num +HAVING Sum(quantity) >= 100 +ORDER BY order_num + +# Subquery +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 +``` + +Knowledge points: + +- `where`: Filter the specified rows. Aggregation functions (grouping functions) cannot be added later. +- `having`: filter grouping, used together with `group by`, cannot be used alone. + +### Calculate the sum + +The `OrderItems` table represents order information, including fields: order number `order_num` and `item_price` the selling price of the product, `quantity` the quantity of the product. + +| 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 | + +[Question] Write a SQL statement to aggregate based on order numbers and return all order numbers with a total order price of not less than 1,000. The final results are sorted in ascending order by order number. + +Tip: total price = item_price times quantity + +Answer: + +```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 +``` + +### Check SQL statement + +`OrderItems` table contains `order_num` order numbers + +| order_num | +| --------- | +| a002 | +| a002 | +| a002 | +| a004 | +| a007 | + +[Question] Modify the following code correctly and execute it + +```sql +SELECT order_num, COUNT(*) AS items +FROM OrderItems +GROUP BY items +HAVING COUNT(*) >= 3 +ORDER BY items, order_num; +``` + +After modification: + +```sql +SELECT order_num, COUNT(*) AS items +FROM OrderItems +GROUP BY order_num +HAVING items >= 3 +ORDER BY items, order_num; +``` + +## Use subquery + +A subquery is a SQL query nested within a larger query, also called an inner query or inner select, and the statement containing the subquery is also called an outer query or outer select. Simply put, a subquery refers to using the result of a `SELECT` query (subquery) as the data source or judgment condition of another SQL statement (main query). + +Subqueries can be embedded in `SELECT`, `INSERT`, `UPDATE` and `DELETE` statements, and can also be used with operators such as `=`, `<`, `>`, `IN`, `BETWEEN`, `EXISTS` and other operators. + +Subqueries are often used after the `WHERE` clause and the `FROM` clause: + +- When used in the `WHERE` clause, depending on different operators, the subquery can return a single row and a single column, multiple rows and a single column, or a single row and multiple columns of data. A subquery is to return a value that can be used as a WHERE clause query condition. +- When used in the `FROM` clause, multi-row and multi-column data is generally returned, which is equivalent to returning a temporary table, so as to comply with the rule that `FROM` is followed by a table. This approach can implement joint queries on multiple tables. + +> Note: MySQL database only supports subqueries from version 4.1, and earlier versions do not support it. + +The basic syntax of a subquery for a `WHERE` clause is as follows: + +```sql +SELECT column_name [, column_name ] +FROM table1 [, table2 ] +WHERE column_name operator +(SELECT column_name [, column_name ] +FROM table1 [, table2 ] +[WHERE]) +``` + +- Subqueries need to be placed within brackets `( )`. +- `operator` represents the operator used for the `WHERE` clause, which can be a comparison operator (such as `=`, `<`, `>`, `<>`, etc.) or a logical operator (such as `IN`, `NOT IN`, `EXISTS`, `NOT EXISTS`, etc.), which is determined according to the requirements. + +The basic syntax of a subquery for a `FROM` clause is as follows: + +```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; +``` + +- The result returned by the subquery for `FROM` is equivalent to a temporary table, so you need to use the AS keyword to give the temporary table a name. +- Subqueries need to be placed within brackets `( )`. +- You can specify multiple temporary table names and join these tables using the `JOIN` statement. + +### Returns a list of customers who purchased products priced at $10 or more + +`OrderItems` represents the order item table, containing fields order number: `order_num`, order price: `item_price`; `Orders` table represents the order information table, containing customer `id: cust_id` and order number: `order_num` + +`OrderItems` table: + +| order_num | item_price | +| --------- | ---------- | +| a1 | 10 | +| a2 | 1 | +| a2 | 1 | +| a4 | 2 | +| a5 | 5 || a2 | 1 | +| a7 | 7 | + +`Orders` table: + +| order_num | cust_id | +| --------- | ------- | +| a1 | cust10 | +| a2 | cust1 | +| a2 | cust1 | +| a4 | cust2 | +| a5 | cust5 | +| a2 | cust1 | +| a7 | cust7 | + +[Problem] Use a subquery to return a list of customers who purchased products with a price of $10 or more. The results do not need to be sorted. + +Answer: + +```sql +SELECT cust_id +FROM Orders +WHERE order_num IN (SELECT DISTINCT order_num + FROM OrderItems + where item_price >= 10) +``` + +### Determine which orders purchased the product with prod_id BR01 (1) + +The table `OrderItems` represents the order product information table, `prod_id` is the product id; the `Orders` table represents the order table, `cust_id` represents the customer id and the order date `order_date` + +`OrderItems` table: + +| prod_id | order_num | +| ------- | --------- | +| BR01 | a0001 | +| BR01 | a0002 | +| BR02 | a0003 | +| BR02 | a0013 | + +`Orders` table: + +| 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 | + +【Question】 + +Write a SQL statement that uses a subquery to determine which orders (in `OrderItems`) purchased the product with `prod_id` as "BR01", then return the customer ID (`cust_id`) and order date (`order_date`) for each product from the `Orders` table, sorting the results in ascending order by order date. + +Answer: + +```sql +# Writing method 1: subquery +SELECT cust_id,order_date +FROM Orders +WHERE order_num IN + (SELECT order_num + FROM OrderItems + WHERE prod_id = 'BR01' ) +ORDER BY order_date; + +# Writing method 2: Join table +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 +``` + +### Return the emails of all customers who purchased the product with prod_id BR01 (1) + +You want to know the date of ordering BR01 product. The table `OrderItems` represents the order product information table, `prod_id` is the product id; the `Orders` table represents the order table, which has `cust_id` which represents the customer id and the order date `order_date`; the `Customers` table contains `cust_email` customer email and `cust_id` customer id + +`OrderItems` table: + +| prod_id | order_num | +| ------- | --------- | +| BR01 | a0001 | +| BR01 | a0002 | +| BR02 | a0003 | +| BR02 | a0013 | + +`Orders` table: + +| 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 | + +The `Customers` table represents customer information, `cust_id` is the customer id, `cust_email` is the customer email + +| cust_id | cust_email | +| ------- | ------------------ | +| cust10 | | +| cust1 | | +| cust2 | | + +[Problem] Return the emails of all customers who purchased the product with `prod_id` as `BR01` (`cust_email` in the `Customers` table) without sorting the results. + +Tip: This involves `SELECT` statements, the innermost one returning `order_num` from the `OrderItems` table, and the middle one returning `cust_id` from the `Customers` table. + +Answer: + +```sql +# Writing method 1: subquery +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')) + +# Writing method 2: Join table (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' + +#Writing 3: Join table (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' +``` + +### Return the total amount of different orders for each customer + +We need a list of customer IDs with the total amount they have ordered. + +The `OrderItems` table represents order information. The `OrderItems` table has order number: `order_num`, product selling price: `item_price`, and product quantity: `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` table order number: `order_num`, customer id: `cust_id` + +| order_num | cust_id | +| --------- | ------- | +| a0001 | cust10 | +| a0002 | cust1 | +| a0003 | cust1 | +| a0013 | cust2 | + +【Question】 + +Write a SQL statement that returns the customer ID (`cust_id` in the `Orders` table) and use a subquery to return `total_ordered` to return the total number of orders for each customer, sorting the results by amount from largest to smallest. + +Answer: + +```sql +# Writing method 1: subquery +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; + +# Writing method 2: Join table +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``` + +For a detailed introduction to the writing method, please refer to: [issue#2402: Errors in writing method 1 and how to modify it](https://github.com/Snailclimb/JavaGuide/issues/2402). + +### Retrieve all product names and corresponding sales totals from the Products table + +Retrieve all product names: `prod_name`, product ids: `prod_id` in the `Products` table + +| prod_id | prod_name | +| ------- | --------- | +| a0001 | egg | +| a0002 | sockets | +| a0013 | coffee | +| a0003 | cola | + +`OrderItems` represents the order item table, order product: `prod_id`, sold quantity: `quantity` + +| prod_id | quantity | +| ------- | -------- | +| a0001 | 105 | +| a0002 | 1100 | +| a0002 | 200 | +| a0013 | 1121 | +| a0003 | 10 | +| a0003 | 19 | +| a0003 | 5 | + +【Question】 + +Write a SQL statement to retrieve all product names (`prod_name`) from the `Products` table, and a calculated column named `quant_sold` that contains the total number of products sold (retrieved using a subquery and `SUM(quantity)` on the `OrderItems` table). + +Answer: + +```sql +# Writing method 1: subquery +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 + +# Writing method 2: Join table +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 cannot be used here, an error will be reported) +``` + +## Join table + +JOIN means "connection". As the name suggests, the SQL JOIN clause is used to join two or more tables for query. + +When connecting tables, you need to select a field in each table and compare the values ​​of these fields. Two records with the same value will be merged into one. **The essence of joining tables is to merge records from different tables to form a new table. Of course, this new table is only temporary, it only exists for the duration of this query**. + +The basic syntax for joining two tables using `JOIN` is as follows: + +```sql +SELECT table1.column1, table2.column2... +FROM table1 +JOIN table2 +ON table1.common_column1 = table2.common_column2; +``` + +`table1.common_column1 = table2.common_column2` is a join condition. Only records that meet this condition will be merged into one row. You can join tables using several operators, such as =, >, <, <>, <=, >=, !=, `between`, `like`, or `not`, but the most common is to use =. + +When there are fields with the same name in two tables, in order to help the database engine distinguish the fields of which table, the table name needs to be added when writing the field names with the same name. Of course, if the written field name is unique in the two tables, you can not use the above format and just write the field name. + +In addition, if the related field names of the two tables are the same, you can also use the `USING` clause instead of `ON`, for example: + +```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 + +# If the associated field names of the two tables are the same, you can also use the USING clause: JOIN....USING() +SELECT c.cust_name, o.order_num +FROM Customers c +INNER JOIN Orders o +USING(cust_id) +ORDER BY c.cust_name +``` + +**The difference between `ON` and `WHERE`**: + +- When joining tables, SQL will generate a new temporary table based on the join conditions. `ON` is the connection condition, which determines the generation of temporary tables. +- `WHERE` is to filter the data in the temporary table after the temporary table is generated to generate the final result set. At this time, there is no JOIN-ON. + +So in summary: **SQL first generates a temporary table based on ON, and then filters the temporary table based on WHERE**. + +SQL allows some modifying keywords to be added to the left of `JOIN` to form different types of connections, as shown in the following table: + +| Connection type | Description | +|------------------------------------------------ |------------------------------------------------------------------------------------------------ | +| INNER JOIN inner join | (default connection method) Rows will be returned only when there are records that meet the conditions in both tables. | +| LEFT JOIN / LEFT OUTER JOIN Left (outer) join | Returns all rows in the left table, even if there are no rows in the right table that meet the condition. | +| RIGHT JOIN / RIGHT OUTER JOIN Right (outer) join | Returns all rows in the right table, even if there are no rows in the left table that meet the condition. | +| FULL JOIN / FULL OUTER JOIN Full (outer) join | As long as one of the tables has records that meet the conditions, rows will be returned. | +| SELF JOIN | Joins a table to itself as if the table were two tables. To differentiate between two tables, at least one table needs to be renamed in the SQL statement. | +| CROSS JOIN | Cross join returns the Cartesian product of record sets from two or more joined tables. | + +The figure below shows 7 usages related to LEFT JOIN, RIGHT JOIN, INNER JOIN, and OUTER JOIN. + +![](https://oss.javaguide.cn/github/javaguide/csdn/d1794312b448516831369f869814ab39.png) + +If you just write `JOIN` without adding any modifiers, the default is `INNER JOIN` + +For `INNER JOIN`, there is also an implicit way of writing, called "**Implicit inner join**", that is, there is no `INNER JOIN` keyword, and the `WHERE` statement is used to implement the inner join function. + +```sql +#Implicit inner join +SELECT c.cust_name, o.order_num +FROM Customers c,Orders o +WHERE c.cust_id = o.cust_id +ORDER BY c.cust_name + +#Explicit inner join +SELECT c.cust_name, o.order_num +FROM Customers c +INNER JOIN Orders o +USING(cust_id) +ORDER BY c.cust_name; +``` + +### Return customer name and related order number + +The `Customers` table has fields customer name `cust_name` and customer id `cust_id` + +| cust_id | cust_name | +|--------|---------| +| cust10 | andy | +| cust1 | ben | +| cust2 | tony | +| cust22 | tom | +| cust221 | an | +| cust2217 | hex |`Orders` order information table, containing fields `order_num` order number, `cust_id` customer id + +| order_num | cust_id | +| --------- | -------- | +| a1 | cust10 | +| a2 | cust1 | +| a3 | cust2 | +| a4 | cust22 | +| a5 | cust221 | +| a7 | cust2217 | + +[Question] Write a SQL statement to return the customer name (`cust_name`) in the `Customers` table and the related order number (`order_num`) in the `Orders` table, and sort the results by customer name and order number in ascending order. You can try two different writing methods, one using simple equal join syntax, and the other using INNER JOIN. + +Answer: + +```sql +#Implicit inner join +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 + +#Explicit inner join +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; +``` + +### Return the customer name and related order number and the total price of each order + +The `Customers` table has fields, customer name: `cust_name`, customer id: `cust_id` + +| cust_id | cust_name | +|--------|---------| +| cust10 | andy | +| cust1 | ben | +| cust2 | tony | +| cust22 | tom | +| cust221 | an | +| cust2217 | hex | + +`Orders` order information table, contains fields, order number: `order_num`, customer id: `cust_id` + +| order_num | cust_id | +| --------- | -------- | +| a1 | cust10 | +| a2 | cust1 | +| a3 | cust2 | +| a4 | cust22 | +| a5 | cust221 | +| a7 | cust2217 | + +The `OrderItems` table has fields, product order number: `order_num`, product quantity: `quantity`, product price: `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 | + +[Problem] In addition to returning the customer name and order number, return the customer name (`cust_name`) in the `Customers` table and the related order number (`order_num`) in the `Orders` table, add a third column `OrderTotal`, which contains the total price of each order, and sort the results by customer name and then by order number in ascending order. + +```sql +# Simple equal connection syntax +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 +``` + +Note, some friends may write like this: + +```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 +``` + +This is wrong! Clustering only `cust_name` does meet the meaning of the question, but it does not comply with the syntax of `GROUP BY`. + +In the select statement, if there is no `GROUP BY` statement, then `cust_name` and `order_num` will return several values, while `sum(quantity * item_price)` will only return one value. Through `group by` `cust_name` can make `cust_name` and `sum(quantity * item_price)` correspond one to one, or **clustering**, so the same must be done. `order_num` performs clustering. + +> **In a word, the fields in select are either all clustered or none** + +### Determine which orders purchased the product with prod_id BR01 (2) + +The table `OrderItems` represents the order product information table, `prod_id` is the product id; the `Orders` table represents the order table, `cust_id` represents the customer id and the order date `order_date` + +`OrderItems` table: + +| prod_id | order_num | +| ------- | --------- | +| BR01 | a0001 | +| BR01 | a0002 | +| BR02 | a0003 | +| BR02 | a0013 | + +`Orders` table: + +| 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 | + +【Question】 + +Write a SQL statement that uses a subquery to determine which orders (in `OrderItems`) purchased the product with `prod_id` as "BR01", then return the customer ID (`cust_id`) and order date (`order_date`) for each product from the `Orders` table, sorting the results in ascending order by order date. + +Tip: This time use joins and simple equijoin syntax. + +```sql +# Writing method 1: subquery +SELECT cust_id, order_date +FROM Orders +WHERE order_num IN (SELECT order_num + FROM OrderItems + WHERE prod_id = 'BR01') +ORDER BY order_date + +#Writing method 2: join table 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 + +# Writing method 3: Simplified version of writing method 2 +SELECT cust_id, order_date +FROM Orders +INNER JOIN OrderItems USING(order_num) +WHERE OrderItems.prod_id = 'BR01' +ORDER BY order_date +``` + +### Return the emails of all customers who purchased the product with prod_id BR01 (2)There is a table `OrderItems` representing the order product information table, `prod_id` is the product id; the `Orders` table represents the order table having `cust_id` representing the customer id and the order date `order_date`; the `Customers` table contains `cust_email` customer email and cust_id customer id + +`OrderItems` table: + +| prod_id | order_num | +| ------- | --------- | +| BR01 | a0001 | +| BR01 | a0002 | +| BR02 | a0003 | +| BR02 | a0013 | + +`Orders` table: + +| 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 | + +The `Customers` table represents customer information, `cust_id` is the customer id, `cust_email` is the customer email + +| cust_id | cust_email | +| ------- | ------------------ | +| cust10 | | +| cust1 | | +| cust2 | | + +[Problem] Return the emails of all customers who purchased the product with `prod_id` as BR01 (`cust_email` in the `Customers` table) without sorting the results. + +Tip: When it comes to the `SELECT` statement, the innermost one returns `order_num` from the `OrderItems` table, and the middle one returns `cust_id` from the `Customers` table, but the INNER JOIN syntax must be used. + +```sql +SELECT cust_email +FROM Customers +INNER JOIN Orders using(cust_id) +INNER JOIN OrderItems using(order_num) +WHERE OrderItems.prod_id = 'BR01' +``` + +### Another way to determine the best customers (2) + +The `OrderItems` table represents order information. Another way to determine the best customers is to look at how much they spend. The `OrderItems` table has the order number `order_num` and `item_price` the price at which the item was sold, and `quantity` the quantity of the item. + +| 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 | + +The `Orders` table contains the fields `order_num` order number, `cust_id` customer id + +| order_num | cust_id | +| --------- | -------- | +| a1 | cust10 | +| a2 | cust1 | +| a3 | cust2 | +| a4 | cust22 | +| a5 | cust221 | +| a7 | cust2217 | + +The customer table `Customers` has fields `cust_id` customer id, `cust_name` customer name + +| cust_id | cust_name | +|--------|---------| +| cust10 | andy | +| cust1 | ben | +| cust2 | tony | +| cust22 | tom | +| cust221 | an | +| cust2217 | hex | + +[Question] Write a SQL statement to return the customer name and total amount (`order_num` in the `OrderItems` table) whose order total price is not less than 1000. + +Tip: Need to calculate the sum (`item_price` times `quantity`). To sort the results by total amount, use the `INNER JOIN` syntax. + +```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 +``` + +## Create advanced connection + +### Retrieve each customer's name and all order numbers (1) + +The `Customers` table represents customer information containing customer id `cust_id` and customer name `cust_name` + +| cust_id | cust_name | +|--------|---------| +| cust10 | andy | +| cust1 | ben | +| cust2 | tony | +| cust22 | tom | +| cust221 | an | +| cust2217 | hex | + +The `Orders` table represents order information including order number `order_num` and customer id `cust_id` + +| order_num | cust_id | +| --------- | -------- | +| a1 | cust10 | +| a2 | cust1 | +| a3 | cust2 | +| a4 | cust22 | +| a5 | cust221 | +| a7 | cust2217 | + +[Problem] Use INNER JOIN to write SQL statements to retrieve the name of each customer (`cust_name` in the `Customers` table) and all order numbers (`order_num` in the `Orders` table), and finally return them in ascending order according to the customer name `cust_name`. + +```sql +SELECT cust_name, order_num +FROM Customers +INNER JOIN Orders +USING(cust_id) +ORDER BY cust_name +``` + +### Retrieve each customer's name and all order numbers (2) + +The `Orders` table represents order information including order number `order_num` and customer id `cust_id` + +| order_num | cust_id | +| --------- | -------- | +| a1 | cust10 | +| a2 | cust1 | +| a3 | cust2 | +| a4 | cust22 | +| a5 | cust221 | +| a7 | cust2217 | + +The `Customers` table represents customer information containing customer id `cust_id` and customer name `cust_name` + +| cust_id | cust_name | +|--------|---------| +| cust10 | andy | +| cust1 | ben | +| cust2 | tony | +| cust22 | tom | +| cust221 | an | +| cust2217 | hex | +| cust40 | ace | + +[Problem] Retrieve each customer's name (`cust_name` in the `Customers` table) and all order numbers (`order_num` in the Orders table), and list all customers, even if they have not placed an order. Finally, it is returned in ascending order according to the customer name `cust_name`. + +```sql +SELECT cust_name, order_num +FROM Customers +LEFT JOIN Orders +USING(cust_id) +ORDER BY cust_name``` + +### Return the product name and the order number associated with it + +The `Products` table contains the fields `prod_id` product id, `prod_name` product name for the product information table. + +| prod_id | prod_name | +| ------- | --------- | +| a0001 | egg | +| a0002 | sockets | +| a0013 | coffee | +| a0003 | cola | +| a0023 | soda | + +The `OrderItems` table contains the fields `order_num`, order number and product id `prod_id` for the order information table. + +| prod_id | order_num | +| ------- | --------- | +| a0001 | a105 | +| a0002 | a1100 | +| a0002 | a200 | +| a0013 | a1121 | +| a0003 | a10 | +| a0003 | a19 | +| a0003 | a5 | + +[Problem] Use outer joins (left join, right join, full join) to join the `Products` table and the `OrderItems` table, return a list of product names (`prod_name`) and related order numbers (`order_num`), and sort them in ascending order by product name. + +```sql +SELECT prod_name, order_num +FROM Products +LEFT JOIN OrderItems +USING(prod_id) +ORDER BY prod_name +``` + +### Return the product name and the total number of orders for each product + +The `Products` table contains the fields `prod_id` product id, `prod_name` product name for the product information table. + +| prod_id | prod_name | +| ------- | --------- | +| a0001 | egg | +| a0002 | sockets | +| a0013 | coffee | +| a0003 | cola | +| a0023 | soda | + +The `OrderItems` table contains the fields `order_num`, order number and product id `prod_id` for the order information table. + +| prod_id | order_num | +| ------- | --------- | +| a0001 | a105 | +| a0002 | a1100 | +| a0002 | a200 | +| a0013 | a1121 | +| a0003 | a10 | +| a0003 | a19 | +| a0003 | a5 | + +【Question】 + +Use OUTER JOIN to join the `Products` table and the `OrderItems` table, return the product name (`prod_name`) and the total number of orders for each product (not the order number), and sort by product name in ascending order. + +```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 +``` + +### List suppliers and their available product quantities + +There is a `Vendors` table containing `vend_id` (vendor id) + +| vend_id | +| ------- | +| a0002 | +| a0013 | +| a0003 | +| a0010 | + +There is a `Products` table containing `vend_id` (vendor id) and prod_id (supplied product 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 | + +[Question] List vendors (`vend_id` in `Vendors` table) and their available product quantities, including vendors with no products. You need to use OUTER JOIN and COUNT() aggregate function to calculate the quantity of each product in the `Products` table, and finally sort them in ascending order based on vend_id. + +NOTE: The `vend_id` column appears in multiple tables, so it needs to be fully qualified each time it is referenced. + +```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 +``` + +## Combined query + +The `UNION` operator combines the results of two or more queries and produces a result set containing the extracted rows from the participating queries in `UNION`. + +`UNION` basic rules: + +- The number and order of columns must be the same for all queries. +- The data types of the columns involved in the tables in each query must be the same or compatible. +- Usually the column names returned are taken from the first query. + +By default, the `UNION` operator selects distinct values. If duplicate values ​​are allowed, use `UNION ALL`. + +```sql +SELECT column_name(s) FROM table1 +UNION ALL +SELECT column_name(s) FROM table2; +``` + +The column names in the `UNION` result set are always equal to the column names in the first `SELECT` statement in the `UNION`. + +`JOIN` vs `UNION`: + +- The columns of the joined tables in `JOIN` may be different, but in `UNION` the number and order of columns must be the same for all queries. +- `UNION` puts the rows after the query together (vertically), but `JOIN` puts the columns after the query together (horizontally), i.e. it forms a Cartesian product. + +### Combine two SELECT statements (1) + +The table `OrderItems` contains order product information. The field `prod_id` represents the product id and `quantity` represents the product quantity. + +| prod_id | quantity | +| ------- | -------- | +| a0001 | 105 | +| a0002 | 100 | +| a0002 | 200 | +| a0013 | 1121 | +| a0003 | 10 | +| a0003 | 19 | +| a0003 | 5 | +|BNBG|10002| + +[Question] Combine two `SELECT` statements to retrieve product id (`prod_id`) and `quantity` from the `OrderItems` table. One `SELECT` statement filters rows with a quantity of 100, another `SELECT` statement filters products whose id starts with BNBG, and finally sorts the results by product id in ascending order. + +```sql +SELECT prod_id, quantity +FROM OrderItems +WHERE quantity = 100 +UNION +SELECT prod_id, quantity +FROM OrderItems +WHERE prod_id LIKE 'BNBG%' +``` + +### Combine two SELECT statements (2) + +The table `OrderItems` contains order product information, the field `prod_id` represents the product id, and `quantity` represents the product quantity. + +| prod_id | quantity | +| ------- | -------- | +| a0001 | 105 | +| a0002 | 100 | +| a0002 | 200 | +| a0013 | 1121 | +| a0003 | 10 | +| a0003 | 19 | +| a0003 | 5 | +|BNBG|10002| + +[Question] Combine two `SELECT` statements to retrieve product id (`prod_id`) and `quantity` from the `OrderItems` table. One `SELECT` statement filters rows with a quantity of 100, another `SELECT` statement filters products whose id starts with BNBG, and finally sorts the results by product id in ascending order. NOTE: **Only use a single SELECT statement this time. ** + +Answer: + +If only one select statement is required, use `or` instead of `union`. + +```sql +SELECT prod_id, quantity +FROM OrderItems +WHERE quantity = 100 OR prod_id LIKE 'BNBG%'``` + +### Combine the product names in the Products table and the customer names in the Customers table + +The `Products` table contains the field `prod_name` representing the product name + +| prod_name | +| --------- | +| flower | +| rice | +| ring | +| umbrella | + +The Customers table represents customer information, and cust_name represents the customer name. + +| cust_name | +| --------- | +| andy | +| ben | +| tony | +| tom | +| an | +|lee| +| hex | + +[Problem] Write a SQL statement to combine the product name (`prod_name`) in the `Products` table and the customer name (`cust_name`) in the `Customers` table and return it, and then sort the results in ascending order by product name. + +```sql +# The column names in the UNION result set are always equal to the column names in the first SELECT statement in the UNION. +SELECT prod_name +FROM Products +UNION +SELECT cust_name +FROM Customers +ORDER BY prod_name +``` + +### Check SQL statement + +The table `Customers` contains fields `cust_name` customer name, `cust_contact` customer contact information, `cust_state` customer state, `cust_email` customer `email` + +| cust_name | cust_contact | cust_state | cust_email | +| --------- | ------------ | ---------- | ------------------ | +| cust10 | 8695192 | MI | | +| cust1 | 8695193 | MI | | +| cust2 | 8695194 | IL | | + +[Problem] Correct the following incorrect 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; +``` + +After correction: + +```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; +``` + +When using `union` to combine queries, only one `order by` statement can be used, and it must be located after the last `select` statement + +Or just use `or` to do it: + +```sql +SELECT cust_name, cust_contact, cust_email +FROM Customers +WHERE cust_state = 'MI' or cust_state = 'IL' +ORDER BY cust_name; +``` + + \ No newline at end of file diff --git a/docs_en/database/sql/sql-questions-02.en.md b/docs_en/database/sql/sql-questions-02.en.md new file mode 100644 index 00000000000..c1c2986ea02 --- /dev/null +++ b/docs_en/database/sql/sql-questions-02.en.md @@ -0,0 +1,451 @@ +--- +title: Summary of common SQL interview questions (2) +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL interview questions, additions, deletions, batch insertions, imports, replacement insertions, constraints + - - meta + - name: description + content: Focus on the analysis of questions on basic operations such as addition, deletion, and modification, and summarize techniques and precautions such as batch insertion/import and replacement insertion. +--- + +> The question comes from: [Niuke Question Ba - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +## Add, delete, and modify operations + +Summary of how SQL inserts records: + +- **Normal insert (all fields)**: `INSERT INTO table_name VALUES (value1, value2, ...)` +- **Normal insert (qualified fields)**: `INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...)` +- **Multiple inserts 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 principle is to delete the original record and reinsert it after detecting a duplicate primary key or unique index key) + +### Insert record (1) + +**Description**: Niuke backend will record each user’s test paper answers to the `exam_record` table. The details of the answer records of two users are as follows: + +- User 1001 started answering test paper 9001 at 10:11:12 pm on September 1, 2021, and submitted it after 50 minutes, and received 90 points; +- User 1002 started answering paper 9002 at 7:01:02 AM on September 4, 2021, and exited the platform 10 minutes later. + +In the test paper answer record table `exam_record`, the table has been built and its structure is as follows. Please use one statement to insert these two records into the table. + +| Filed | 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 +// There is an auto-incrementing primary key, no manual assignment is 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); +``` + +### Insert record (2) + +**Description**: There is an examination paper answer record table `exam_record`, with the structure as follows, which contains user answer paper records over the years. Due to the increasing amount of data, maintenance is becoming more and more difficult, so the data table content needs to be streamlined and historical data backed up. + +Table `exam_record`: + +| Filed | 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 | + +We have created a new table `exam_record_before_2021` to back up test answer records before 2021. The structure is consistent with the `exam_record` table. Please import the completed test answer records before 2021 into this 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 +FROM exam_record +WHERE YEAR(submit_time) < 2021; +``` + +### Insert record (3) + +**Description**: There is now a set of difficult SQL test papers with ID 9003, which lasts for one and a half hours. Please insert 2021-01-01 00:00:00 as the release time into the test question information table `examination_info`. Regardless of whether the ID test paper exists, the insertion must be successful. Please try to insert it. + +Test question information table `examination_info`: + +| Filed | 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 paper ID | +| tag | varchar(32) | YES | | | (NULL) | category tag | +| difficulty | varchar(8) | YES | | | (NULL) | difficulty | +| duration | int(11) | NO | | | (NULL) | Duration (number of 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"); +``` + +### Update record (1)**Description**: There is now a test paper information table `examination_info`. The table structure is as shown below: + +| Filed | 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 paper 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 | + +Please modify all the `tag` fields whose `tag` is `PYTHON` in the **examination_info** table to `Python`. + +**Idea**: There are two ways to solve this problem. The easiest one to think of is to directly use `update + where` to specify conditional updates. The second one is to search and replace based on the fields 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 (target field, "find content", "replace content") +``` + +### Update record (2) + +**Description**: There is an examination paper answer record table exam_record, which contains user answer paper records for many years. The structure is as follows: Answer record table `exam_record`: **`submit_time`** is the completion time (note this sentence) + +| Filed | 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 | + +**Question requirements**: Please change all the == unfinished == records in the `exam_record` table that were started before September 1, 2021 == to passive completion, that is: change the completion time to '2099-01-01 00:00:00', and change the score to 0. + +**Idea**: Pay attention to the keyword in the question stem (already highlighted) `" xxx time "` before the condition, then you will immediately think of time comparison here. You can directly `xxx_time < "2021-09-01 00:00:00",` or you can use the `date()` function for comparison; the second condition is `"Unfinished"`, that is, the completion time is NULL, which is the submission time in the question ----- `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 +``` + +### Delete records (1) + +**Description**: There is an exam paper answer record table `exam_record`, which contains user answer paper records for many years. The structure is as follows: + +Answer record table `exam_record:` **`start_time`** is the start time of the test paper `submit_time` is the hand-in time, that is, the end time. + +| Filed | 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 | + +**Requirement**: Please delete the records in the `exam_record` table whose answer time is less than 5 minutes and whose score is failing (passing mark is 60 points); + +**Idea**: Although this question is an exercise in deletion, if you look carefully, it is indeed an examination of the usage of time functions. Compared with the minutes mentioned here, the commonly used functions are **`TIMEDIFF`** and **`TIMESTAMPDIFF`**. The usage of the two is slightly different, and the latter is more flexible. This all depends on personal habits. + +1. `TIMEDIFF`: the difference between two times + +```sql +TIMEDIFF(time1, time2) +``` + +Both parameters are required and are both a time or datetime expression. If the specified parameter is illegal or NULL, the function will return NULL. + +For this question, it can be used in the minute function, because TIMEDIFF calculates the difference in time. If you put a MINUTE function outside, the calculated number is the number of minutes. + +2. `TIMESTAMPDIFF`: used to calculate the time difference between two dates + +```sql +TIMESTAMPDIFF(unit,datetime_expr1,datetime_expr2) +# Parameter description +#unit: The time difference unit returned by date comparison. Commonly used optional values are as follows: +SECOND: seconds +MINUTE: minutes +HOUR: hours +DAY: day +WEEK: week +MONTH: month +QUARTER: quarter +YEAR: year +# TIMESTAMPDIFF function returns the result of datetime_expr2 - datetime_expr1 (in human words: the latter - the previous one, that is, 2-1), where datetime_expr1 and datetime_expr2 can be DATE or DATETIME type values (in human words: it can be "2023-01-01", or it can be "2023-01-01- 00:00:00") +``` + +This question requires a comparison of minutes, so TIMESTAMPDIFF(MINUTE, start time, end time) < 5 + +**Answer**: + +```sql +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 +```### Delete records (2) + +**Description**: There is an exam paper answer record table `exam_record`, which contains user answer paper records over the years. The structure is as follows: + +Answer record table `exam_record`: `start_time` is the start time of the test paper, `submit_time` is the hand-in time, that is, the end time. If it is not completed, it will be empty. + +| Filed | 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 | + +**Requirement**: Please delete the 3 records with the earliest starting time among the records in the `exam_record` table that have unfinished answers == or == the answer time is less than 5 minutes. + +**Idea**: This question is relatively simple, but you should pay attention to the information given in the question stem, the end time, if it is not completed, it will be empty, this is actually a condition + +There is another condition that is less than 5 minutes, which is similar to the previous question, but here it is ** or **, that is, only one of the two conditions is met; in addition, the usage of sorting and limit is slightly examined. + +**Answer**: + +```sql +DELETE FROM exam_record WHERE submit_time IS null OR TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 +ORDER BY start_time +LIMIT 3 +# The default is asc, desc is descending order +``` + +### Delete records (3) + +**Description**: There is an exam paper answer record table exam_record, which contains user answer paper records for many years. The structure is as follows: + +| Filed | 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 | + +**Requirement**: Please delete all records in the `exam_record` table, == and reset the auto-incrementing primary key== + +**Idea**: This question examines the difference between three delete statements. Pay attention to the highlighted part, which requires resetting the primary key; + +- `DROP`: clear the table, delete the table structure, irreversible +- `TRUNCATE`: Format the table without deleting the table structure, irreversible +- `DELETE`: delete data, reversible + +The reason why `TRUNCATE` is chosen here is: TRUNCATE can only act on tables; `TRUNCATE` will clear all rows in the table, but the table structure, its constraints, indexes, etc. remain unchanged; `TRUNCATE` will reset the auto-increment value of the table; using `TRUNCATE` will restore the space occupied by the table and indexes to their initial size. + +This question can also be done using `DELETE`, but after deletion, you still need to manually `ALTER` the table structure to set the initial value of the primary key; + +In the same way, you can also use `DROP` to directly delete the entire table, including the table structure, and then create a new table. + +**Answer**: + +```sql +TRUNCATE exam_record; +``` + +## Table and index operations + +### Create a new table + +**Description**: There is currently a user information table, which contains user information that has been registered on the platform over the years. As the Niuke platform continues to grow, the number of users has grown rapidly. In order to efficiently provide services to highly active users, it is now necessary 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 | 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 | + +As a data analyst, please create a high-quality user information table user_info_vip** with the same structure as the user information table. + +The output you should return is shown in the table below. Please write a table creation statement to record all restrictions and instructions in the table into the table. + +| Filed | 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 | + +**Idea**: If this question gives the name of the old table, you can directly `create table new table as select * from old table;` However, this question does not give the name of the old table, so you need to create it yourself. Just pay attention to the creation of default values and keys, which is relatively simple. (Note: If it is executed on Niuke.com, please note that the comment must be consistent with the comment in the question, including upper and lower case, otherwise it will not pass, and the characters must also be set) + +Answer: + +```sql +CREATE TABLE IF NOT EXISTS user_info_vip( + 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 +``` + +### Modify table + +**Description**: There is a user information table `user_info`, which contains user information registered on the platform over the years. + +**User information table `user_info`:** + +| Filed | 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 | + +**Requirements:** Please add a field `school` that can store up to 15 Chinese characters in the user information table after the field `level`; change the `job` column name in the table to `profession`, and change the `varchar` field length to 10; set the default value of `achievement` to 0. + +**Idea**: Before doing this question first, you need to understand the basic usage of the ALTER statement: + +- Add a column: `ALTER TABLE table name ADD COLUMN column name type [first | after field name];` (first: add before a column, after vice versa) +- Modify the column type or constraint: `ALTER TABLE table name MODIFY COLUMN column name new type [new constraint];` +- Modify column name: `ALTER TABLE table name change COLUMN old column name new column name type;` +- Delete column: `ALTER TABLE table name drop COLUMN column name;` +- Modify table name: `ALTER TABLE table name rename [to] new table name;` +- Put a column into the first column: `ALTER TABLE table name MODIFY COLUMN column name type first;` + +The `COLUMN` keyword can actually be omitted, but it is listed here based on the specification. + +When modifying, if there are multiple modification items, you can write them together, but pay attention to the format. + +**Answer**: + +```sql +ALTER TABLE user_info + ADD school VARCHAR(15) AFTER level, + CHANGE job profession VARCHAR(10), + MODIFY achievement INT(11) DEFAULT 0; +``` + +### Delete table + +**Description**: There is an exam paper answer record table `exam_record`, which contains user answer paper records for many years. Generally, a backup table `exam_record_{YEAR} will be created for the `exam_record` table every year, {YEAR}` is the corresponding year. + +Now with more and more data and storage shortage, please delete all the backup tables from a long time ago (2011 to 2014) (if they exist). + +**Idea**: This question is very simple, just delete it. If you find it troublesome, you can separate the tables to be deleted with commas and write them on one line; there will definitely be friends here asking: What if you want to delete many tables? Don't worry, if you want to delete many tables, you can write a script to delete them. + +**Answer**: + +```sql +DROP TABLE IF EXISTS exam_record_2011; +DROP TABLE IF EXISTS exam_record_2012; +DROP TABLE IF EXISTS exam_record_2013; +DROP TABLE IF EXISTS exam_record_2014; +``` + +### Create index + +**Description**: There is an examination paper information table `examination_info`, which contains information about various types of examination papers. In order to query the table more conveniently and quickly, you need to create the following index in the `examination_info` table, + +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 create a full-text index `full_idx_tag` on the `tag` column. + +According to the meaning of the question, the following results will be returned: + +| examination_info | 0 | PRIMARY | 1 | id | A | 0 | | | | BTREE | +| ---------------- | --- | ---------------- | --- | -------- | --- | --- | --- | --- | --- | -------- | +| examination_info | 0 | uniq_idx_exam_id | 1 | exam_id | A | 0 | | | YES | BTREE | +| examination_info | 1 | idx_duration | 1 | duration | A | 0 | | | | BTREE | +| examination_info | 1 | full_idx_tag | 1 | tag | | 0 | | | YES | FULLTEXT | + +Note: The background will compare the output results through the `SHOW INDEX FROM examination_info` statement + +**Idea**: To answer this question, you first need to understand the common index types: + +- B-Tree index: B-Tree (or balanced tree) index is the most common and default index type. It is suitable for various query conditions and can quickly locate data that meets the conditions. B-Tree index is suitable for ordinary search operations and supports equality query, range query and sorting. +- Unique index: A unique index is similar to a normal B-Tree index, except that it requires the value of the indexed column to be unique. This means that MySQL verifies the uniqueness of index columns when inserting or updating data. +- Primary key index: The primary key index is a special unique index that is used to uniquely identify each row of data in the table. Each table can only have one primary key index, which can help improve data access speed and data integrity.- Full-text indexing: Full-text indexing is used for full-text search in text data. It supports keyword searches in text fields, not just simple equality or range lookups. Full-text indexing is suitable for application scenarios that require full-text search. + +```sql +-- Example: +-- Add B-Tree index: + CREATE INDEX idx_name (index name) ON table name (field name); -- idx_name is the index name, all of the following +--Create a unique index: + CREATE UNIQUE INDEX idx_name ON table name (field name); +-- Create a primary key index: + ALTER TABLE table name ADD PRIMARY KEY (field name); +--Create a full-text index + ALTER TABLE table name ADD FULLTEXT INDEX idx_name (field name); + +-- Through the above example, you can see that both create and alter can add indexes +``` + +With the above basic knowledge, the answer to this question will become apparent. + +**Answer**: + +```sql +ALTER TABLE examination_info + ADD INDEX idx_duration(duration), + ADD UNIQUE INDEX uniq_idx_exam_id(exam_id), + ADD FULLTEXT INDEX full_idx_tag(tag); +``` + +### Delete index + +**Description**: Please delete the unique index uniq_idx_exam_id and the full-text index full_idx_tag on the `examination_info` table. + +**Idea**: This question examines the basic syntax of deleting an index: + +```sql +-- Use DROP INDEX to delete the index +DROP INDEX idx_name ON table name; + +-- Use ALTER TABLE to delete the index +ALTER TABLE employees DROP INDEX idx_email; +``` + +What needs to be noted here is that in MySQL, deleting multiple indexes at one time is not supported. Each time you delete an index, you can only specify one index name to delete. + +And the **DROP** command needs to be used with caution! ! ! + +**Answer**: + +```sql +DROP INDEX uniq_idx_exam_id ON examination_info; +DROP INDEX full_idx_tag ON examination_info; +``` + + \ No newline at end of file diff --git a/docs_en/database/sql/sql-questions-03.en.md b/docs_en/database/sql/sql-questions-03.en.md new file mode 100644 index 00000000000..2199691ade3 --- /dev/null +++ b/docs_en/database/sql/sql-questions-03.en.md @@ -0,0 +1,1292 @@ +--- +title: Summary of common SQL interview questions (3) +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL interview questions, aggregate functions, truncated average, window, problem analysis, performance + - - meta + - name: description + Content: Focusing on aggregation functions and complex statistical question types, the solution methods and implementation key points such as truncated average are explained, taking into account performance and correctness. +--- + +> The question comes from: [Niuke Question Ba - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +You can decide whether to skip more difficult or difficult questions based on your actual situation and interview needs. + +## Aggregation function + +### Truncated average of scores for difficult papers in the SQL category (harder) + +**Description**: Niuke's operations students want to check everyone's scores on the difficult papers in the SQL category. + +Please help her calculate the truncated average (the average after removing one maximum value and one minimum value) of the scores of all users who completed the difficult SQL category exams from the `exam_record` data table. + +Example data: `examination_info` (`exam_id` test paper ID, tag test paper category, `difficulty` test 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 | algorithm | medium | 80 | 2020-08-02 10:00:00 | + +Example data: `exam_record` (uid user ID, exam_id test paper ID, start_time start answering time, submit_time paper submission time, score 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) | + +The results according to your query are as follows: + +| tag | difficulty | clip_avg_score | +| --- | ---------- | --------------- | +| SQL | hard | 81.7 | + +From the `examination_info` table, we can see that test paper 9001 is a highly difficult SQL test paper. The scores for this test paper are [80,81,84,90,50]. After removing the highest and lowest scores, it is [80,81,84]. The average score is 81.6666667, which is 81.7 after rounding to one decimal place. + +**Enter description:** + +There are at least 3 valid scores in the input data + +**Idea 1:** To find the high-difficulty SQL test papers, you definitely need to connect to the examination_info table, and then find the high-difficulty courses. From examination_info, we know that the exam_id of the high-difficulty SQL is 9001, so later use exam_id = 9001 as the condition to query; + +First find exam number 9001 `select * from exam_record where exam_id = 9001` + +Then, find the maximum score `select max(score) maximum score from exam_record where exam_id = 9001` + +Next, find the minimum score `select min(score) minimum score from exam_record where exam_id = 9001` + +In the score result set obtained from the query, to remove the highest score and the lowest score, the most intuitive thing that can be thought of is NOT IN or NOT EXISTS. Here we use NOT IN to do it. + +First write out the main body `select tag, difficulty, round(avg(score), 1) clip_avg_score from examination_info info INNER JOIN exam_record record` + +**Tips**: MYSQL's `ROUND()` function, `ROUND(X)` returns the nearest integer to parameter + +Then put the above "fragmented" statements together. Note that in NOT IN, the two subqueries are related using UNION ALL, and union is used to concentrate the results of max and min in one row, thus forming the effect of one column and multiple rows. + +**Answer 1:** + +```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 + ) +``` + +This is the most intuitive and easiest solution to think of, but it still needs to be improved. This is considered opportunistic. In fact, strictly according to the requirements of the question, it should be written like this: + +```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' ) )``` + +However, you will find that there are many repeated statements, so you can use `WITH` to extract the common parts + +**Introduction to `WITH` clause**: + +The `WITH` clause, also known as a Common Table Expression (CTE), is the way to define a temporary table in a SQL query. It allows us to create a temporarily named result set in a query and reference that result set in the same query. + +Basic usage: + +```sql +WITH cte_name (column1, column2, ..., columnN) AS ( + -- Query body + SELECT... + FROM ... + WHERE... +) +-- Main query +SELECT... +FROM cte_name +WHERE... +``` + +The `WITH` clause consists of the following parts: + +- `cte_name`: Give the temporary table a name that can be referenced in the main query. +- `(column1, column2, ..., columnN)`: Optional, specify the column name of the temporary table. +- `AS`: required, indicates starting to define a temporary table. +- `CTE query body`: the actual query statement, used to define the data in the temporary table. + +One of the primary uses of the `WITH` clause is to enhance the readability and maintainability of queries, especially when multiple nested subqueries are involved or the same query logic needs to be reused. By putting this logic in a named temporary table, we can organize our queries more clearly and eliminate duplicate code. + +In addition, the `WITH` clause can also implement recursive queries in complex queries. Recursive queries allow us to perform multiple iterations of the same table in a single query, building the result set incrementally. This is useful in scenarios such as working with hierarchical data, organizational structures, and tree structures. + +**Minor detail**: MySQL versions 5.7 and earlier do not support the direct use of aliases in the `WITH` clause. + +Here is the improved answer: + +```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) +``` + +**Idea 2:** + +- Filter SQL difficult test papers: `where tag="SQL" and difficulty="hard"` +- Calculate the truncated average: `(sum-maximum value-minimum value) / (total number-2)`: + - `(sum(score) - max(score) - min(score)) / (count(score) - 2)` + - One disadvantage is that if there are multiple maximum values and minimum values, this method is difficult to filter out, but the question says ----->**`The average value after removing one maximum value and one minimum value`**, so this formula can be used here. + +**Answer 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"; +``` + +### Count the number of answers + +There is an exam paper answer record table `exam_record`. Please count the total number of answers `total_pv`, the number of completed exam papers `complete_pv`, and the number of completed exam papers `complete_exam_cnt`. + +Example data `exam_record` table (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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) | + +Example output: + +| total_pv | complete_pv | complete_exam_cnt | +| -------- | ----------- | ------------------ | +| 10 | 7 | 2 | + +Explanation: As of now, there are 10 test paper answer records, and the number of completed answers is 7 (those who exited midway are in an unfinished status, and their handover time and number of copies are NULL). There are two completed test papers, 9001 and 9002. + +**Idea**: As soon as you see the number of statistics for this question, you will definitely think of using the `COUNT` function to solve it immediately. The problem is to count different records. How to write it? This problem can be solved using subqueries (this problem can also be written using case when, the solution is similar, but the logic is different); first, before doing this problem, let us first understand the basic usage of `COUNT`; + +The basic syntax of the `COUNT()` function is as follows: + +```sql +COUNT(expression) +``` + +Among them, `expression` can be a column name, expression, constant or wildcard character. Here are some common usage examples: + +1. Count the number of all rows in the table: + +```sql +SELECT COUNT(*) FROM table_name; +``` + +2. Count the number of non-null (not NULL) values for a specific column: + +```sql +SELECT COUNT(column_name) FROM table_name; +``` + +3. Calculate the number of rows that meet the conditions: + +```sql +SELECT COUNT(*) FROM table_name WHERE condition; +``` + +4. Combined with `GROUP BY`, calculate the number of rows in each group after grouping: + +```sql +SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name; +``` + +5. Count the number of unique combinations of different column combinations: + +```sql +SELECT COUNT(DISTINCT column_name1, column_name2) FROM table_name; +``` + +When using the `COUNT()` function without specifying any arguments or using `COUNT(*)`, all rows will be counted. If you use a column name, only the number of non-null values ​​in that column will be counted. + +Additionally, the result of the `COUNT()` function is an integer value. Even if the result is zero, NULL will not be returned, which is something to keep in mind. + +**Answer**: + +```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``` + +Here we focus on the sentence `COUNT(DISTINCT exam_id, score IS NOT NULL OR NULL)`, which determines whether score is null. If so, it is true, if not, return null; note that if `or null` is not added here, it will only return false if it is not null, that is, return 0; + +`COUNT` itself cannot calculate the number of rows in multiple columns. The addition of `distinct` makes multiple columns into a whole, and the number of rows that appear can be calculated; `count distinct` only returns non-null rows during calculation. You should also pay attention to this; + +In addition, through this question get arrived ------->count plus conditional common sentence pattern `count (column judgment or null)` + +### The lowest score with a score not less than the average score + +**Description**: Please find the lowest score of the user whose SQL test paper score is not less than the average score of this type of test paper from the test paper answer record table. + +Example data exam_record table (uid user ID, exam_id test paper ID, start_time start answering time, submit_time paper submission time, score 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` table (`exam_id` test paper ID, `tag` test paper category, `difficulty` test 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 | SQL | easy | 60 | 2020-02-01 10:00:00 | +| 3 | 9003 | algorithm | medium | 80 | 2020-08-02 10:00:00 | + +Example output data: + +| min_score_over_avg | +| ------------------ | +| 87 | + +**Explanation**: Test papers 9001 and 9002 are in the SQL category. The scores for answering these two papers are [80,89,87,90], the average score is 86.5, and the minimum score that is not less than the average score is 87 + +**Idea**: This type of question is really complicated at first glance because we don’t know where to start. However, after we read and review the question carefully, we must learn to grasp the key information in the question stem. Take this question as an example: `Please find the lowest score of a user whose SQL test paper score is not less than the average score of this type of test paper from the test paper answer record table. `What effective information can you extract from it at a glance as a solution to the problem? + +Article 1: Find ==SQL== test paper score + +Article 2: This type of test paper == average score == + +Article 3: == user’s minimum score for this type of test paper == + +Then the "bridge" in the middle is == not less than == + +After splitting the conditions, complete them step by step + +```sql +-- Find the score with the tag 'SQL' [80, 89,87,90] +-- Then calculate the average score of this group +select ROUND(AVG(score), 1) from examination_info info INNER JOIN exam_record record + where info.exam_id = record.exam_id + and tag= 'SQL' +``` + +Then find the lowest score for this type of test paper, and then compare the result set `[80, 89,87,90]` with the average score to get the final answer. + +**Answer**: + +```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' ) +``` + +In fact, the requirements given by this type of question seem very "convoluted", but in fact, you need to sort it out carefully, split the big conditions into small conditions, and after splitting them one by one, finally put all the conditions together. Anyway, as long as you remember: **Focus on the trunk and manage the branches**, the problem will be easily solved. + +## Group query + +### Average active days and number of monthly active users + +**Description**: The user’s answer records in the Niuke test paper answer area are stored in the table `exam_record`, with the following content: + +`exam_record` table (`uid` user ID, `exam_id` test paper ID, `start_time` starting time of answering, `submit_time` handing in time, `score` 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 | + +Please calculate the average number of monthly active days `avg_active_days` and the number of monthly active users `mau` in the test paper answer area in each month of 2021. The sample output of the above data is as follows: + +| month | avg_active_days | mau | +| ------ | --------------- | --- | +| 202107 | 1.50 | 2 | +| 202109 | 1.25 | 4 | + +**Explanation**: In July 2021, 2 people were active, and they were active for 3 days in total (1001 was active for 1 day, and 1002 was active for 2 days), and the average number of active days was 1.5; in September 2021, 4 people were active, and they were active for 5 days in total, and the average number of active days was 1.25. The result is rounded to 2 decimal places. + +Note: Being active here means having the behavior of == handing in papers ==. + +**Idea**: After reading the question, pay attention to the highlighted part first; generally when finding the number of days and monthly active people, you will immediately think of the relevant date function; we will also split this question, refine the problem and then solve it; first, to find the number of active people, you must use `COUNT()`, then there is a pit here. I wonder if you have noticed it? User 1002 took two different test papers in September, so pay attention to removing duplicates here, otherwise the number of active people will be wrong during statistics; the second is to know the format of the date, as shown in the table above, the question is required to be displayed in the date format of `202107`, and `DATE_FORMAT` must be used for formatting. + +Basic usage: + +`DATE_FORMAT(date_value, format)` + +- The `date_value` parameter is the date or time value to be formatted. +- The `format` parameter is the specified date or time format (this is the same as the date format in Java). + +**Answer**: + +```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 +``` + +One more thing to say here, use `COUNT(DISTINCT uid, DATE_FORMAT(submit_time, '%Y%m%d'))` to count the number of combined values in the `uid` column and `submit_time` column formatted according to year, month and date. + +### Total monthly number of questions and average number of questions per day + +**Description**: There is a question practice record table `practice_record`. The sample content is as follows: + +| 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 | + +Please count the total number of monthly questions `month_q_cnt` and the average number of daily questions `avg_day_q_cnt` (sorted by month in ascending order) for users in each month of 2021, as well as the overall situation of the year. The sample data output is as follows: + +| submit_month | month_q_cnt | avg_day_q_cnt | +| ------------- | ---------- | ------------- | +| 202108 | 2 | 0.065 | +| 202109 | 3 | 0.100 | +| 2021 Summary | 5 | 0.161 | + +**Explanation**: There are 2 records of question brushing in August 2021, and the average number of questions brushed per day is 2/31=0.065 (retaining 3 decimal places); there are 3 records of brushing questions in September 2021, and the average number of questions brushed per day is 3/30=0.100; there are 5 records of brushing questions in 2021 (the annual summary average has no practical significance, here we calculate based on 31 days 5/31=0.161) + +> Niuke has adopted the latest Mysql version. If you get an error when running: ONLY_FULL_GROUP_BY, it means: for GROUP BY aggregation operation, if the column in SELECT does not appear in GROUP BY, then this SQL is illegal because the column is not in the GROUP BY clause, which means that the column found must appear after group by otherwise an error will be reported, or this field appears in the aggregate function. + +**Idea:** + +When you see the instance data, you will immediately think of the related functions. For example, `submit_month` will use `DATE_FORMAT` to format the date. Then find out the number of questions per month. + +Number of questions per month + +```sql +SELECT MONTH ( submit_time ), COUNT ( question_id ) +FROM + practice_record +GROUP BY + MONTH (submit_time) +``` + +Then in the third column, the `DAY(LAST_DAY(date_value))` function is used to find the number of days in the month of a given date. + +The sample code is as follows: + +```sql +SELECT DAY(LAST_DAY('2023-07-08')) AS days_in_month; +-- Output: 31 + +SELECT DAY(LAST_DAY('2023-02-01')) AS days_in_month; +-- Output: 28 (February in a leap year) + +SELECT DAY(LAST_DAY(NOW())) AS days_in_current_month; +-- Output: 31 (number of days in current month) +``` + +Use the `LAST_DAY()` function to get the last day of the month for a given date, then use the `DAY()` function to extract the number of days for that date. This will get the number of days in the specified month. + +It should be noted that the `LAST_DAY()` function returns a date value, while the `DAY()` function is used to extract the day part of the date value. + +After the above analysis, you can write the answer immediately. The complexity of this question lies in processing the date, and the logic is not difficult. + +**Answer**: + +```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 Summary' 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 +``` + +In the example data output, because the last row needs to get summary data, `UNION ALL` is added to the result set here; don't forget to sort at the end! + +### Valid users with more than 1 unfinished test papers (more difficult) + +**Description**: Existing test paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` starting answering time, `submit_time` handing in time, `score` score), sample data is as follows:| 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 ';')` 将每个用户的考试日期和标签以逗号分隔的形式连接成一个字符串,并按考试开始时间进行排序。 + +## 嵌套子查询### The category that users who have completed an average of no less than 3 test papers per month like to answer (more difficult) + +**Description**: Existing test paper answer record table `exam_record` (`uid`: user ID, `exam_id`: test paper ID, `start_time`: start answering time, `submit_time`: submission time, NULL if not submitted, `score`: score), sample data is as follows: + +| 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 | + +Test paper information table `examination_info` (`exam_id`: test paper ID, `tag`: test paper category, `difficulty`: test paper difficulty, `duration`: test duration, `release_time`: release time), sample data is as follows: + +| 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 | algorithm | medium | 80 | 2020-08-02 10:00:00 | + +Please count from the table the categories and number of answers that users who have an "average number of exam papers completed in the month" are not less than 3, and output them in descending order. The sample output is as follows: + +| tag | tag_cnt | +| ---- | ------- | +|C++|4| +| SQL | 2 | +| Algorithm | 1 | + +**Explanation**: The number of test papers completed by users 1002 and 1005 in September 2021 is 3, and the number of other users is less than 3; then the tag distribution results of the test papers answered by users 1002 and 1005, sorted in descending order by the number of answers, are C++, SQL, and algorithm. + +**Idea**: This question investigates the joint subquery, and the focus is on `monthly average answer >=3`, but I personally think it is not clearly stated here. It should be easier to understand if it is just checked in September; here is not >=3 every month or all the number of answers/months. Don't get it wrong. + +First check which users answer questions more than three times per month + +```sql +SELECT UID +FROM exam_record record +GROUP BY UID, + MONTH (start_time) +HAVING count(submit_time) >= 3 +``` + +After this step, we can go deeper. As long as we can understand the previous step (I mean not to be bothered by the monthly average in the question), then set up a subquery to check which users are included, and then find out the required columns in the question. Remember to sort! ! + +```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 +``` + +### Number of responders and average score on the day the test paper is released + +**Description**: Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time), sample data is as follows: + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 3100 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2100 | 6 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 | 1500 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 1100 | 4 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 5 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | Niuke No. 6 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +**Interpretation**: User 1001’s nickname is Niuke No. 1, achievement value is 3100, user level is level 7, career direction is algorithm, registration time 2020-01-01 10:00:00 + +Test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test duration, `release_time` release time) Sample data is as follows: + +| 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 | algorithm | medium | 80 | 2020-08-02 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` score) The sample data is as follows: + +| 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 | + +Please calculate the number of users with level 5 or above answering `uv` and the average score `avg_score` of each SQL category test paper after it is released, in descending order of the number of people, and for the same number of people, in ascending order of average score. The sample data output is as follows: + +| exam_id | uv | avg_score | +| ------- | --- | --------- | +| 9001 | 3 | 81.3 | + +Explanation: There is only one SQL category test paper, and the test paper ID is 9001. On the day of release (2021-09-01), 1001, 1002, 1003, and 1005 answered, but 1003 is a level 5 user, and the other 3 are level 5 or above. The scores of the three of them are [70, 80, 85, 90], and the average score is 81.3 (reserved 1 decimal places). + +**Idea**: This question seems very complicated, but first gradually split the "outer" conditions, and then put them together, and the answer will come out. Anyway, remember to work from the outside to the inside for multi-table queries. + +First connect the three tables and give some conditions. For example, if the user in the question requires `Level > 5`, then you can find out first + +```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 +``` + +Then pay attention to the requirement in the question: `After each sql category test paper is released, users should answer the questions on the same day`. Pay attention to the == on the same day==, then we will immediately think of the need to compare time. + +Compare the test paper release date and test start date: `DATE(e_info.release_time) = DATE(record.start_time)`; don’t worry about `submit_time` being null, it will be filtered out in where later. + +**Answer**: + +```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 +``` + +Pay attention to the final grouping order! Arrange them first according to the number of people, and if they are consistent, then arrange them according to the average score. + +### User level distribution of people who scored more than 80 on the test paper + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 3100 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2100 | 6 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 | 1500 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 1100 | 4 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 5 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | Niuke No. 6 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +Examination paper information table `examination_info` (`exam_id` examination paper ID, `tag` examination paper category, `difficulty` examination paper difficulty, `duration` examination 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 | C++ | easy | 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: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 的人的用户等级分布,按数量降序排序(保证数量都不同)。 The sample data result output is as follows: + +| level | level_cnt | +| ----- | --------- | +| 6 | 2 | +| 5 | 1 | + +Explanation: 9001 is a SQL-type test paper. There are 3 people who answered this test paper with a score greater than 80 points: 1002, 1003, and 1005, two at level 6 and one at level 5. + +**Idea:** This question is the same as the previous question, but the query conditions have changed. If you understand the previous question, you can solve this question in minutes. + +**Answer**: + +```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 +``` + +## Combined query + +### The number of people and times that each question and each test paper was answered + +**Description**: + +现有试卷作答记录表 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) | + +Question practice table practice_record (uid user ID, question_id question ID, submit_time submission time, score 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**Idea**: The difficulty and error-prone point of this question lies in the simultaneous use of `UNION` and `ORDER BY` + +There are the following situations: using `union` and multiple `order by` without brackets, an error will be reported! + +`order by` has no effect in clauses joined by `union`; + +For example, without parentheses: + +```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, + pvDESC +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, + pvDESC +``` + +Report a syntax error directly. If there are no brackets, there can only be one `order by` + +There is also a situation where `order by` does not work, but it works in clauses of clauses. The solution here is to put another layer of query outside. + +**Answer**: + +```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; +``` + +### People who satisfy two activities respectively + +**Description**: In order to promote more users to learn and improve on the Niuke platform, we will often provide benefits to some users who are both active and perform well. Suppose we had two operations in the past, and issued welfare coupons to those who scored 85 points on each test paper (activity1), and to those who completed the difficult test paper in half the time at least once with a score greater than 80 (activity2). + +Now, you need to screen out the people who satisfy these two activities at once and hand them over to the operations students. Please write a SQL implementation: Output the IDs and activity numbers of all people who can score 85 points on every test paper in 2021, and those who have completed a difficult test paper in half the time at least once with a score greater than 80. Sort the output by user ID. + +Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test 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 | C++ | easy | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | algorithm | medium | 80 | 2021-09-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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 | + +Sample data output: + +| uid | activity | +| ---- | --------- | +| 1001 | activity2 | +| 1003 | activity1 | +| 1004 | activity1 | +| 1004 | activity2 | + +**Explanation**: User 1001's minimum score of 81 does not meet activity 1, but he completed the 60-minute test paper in 29 minutes and 59 seconds with a score of 81, which meets activity 2; 1003's minimum score of 86 meets activity 1, and the completion time is greater than half of the test paper length, which does not meet activity 2; User 1004 completed the test paper in exactly half the time (30 minutes) and scored 85, which meets activity 1 and activity 2. + +**Idea**: This question involves time subtraction, and you need to use the `TIMESTAMPDIFF()` function to calculate the minute difference between two timestamps. + +Let’s take a look at the basic usage + +Example: + +```sql +TIMESTAMPDIFF(MINUTE, start_time, end_time) +``` + +The first parameter of the `TIMESTAMPDIFF()` function is the time unit. Here we choose `MINUTE` to return the minute difference. The second parameter is the earlier timestamp and the third parameter is the later timestamp. The function returns the difference in minutes between them + +After understanding the usage of this function, let's go back and look at the requirements of `activity1`. Just ask for the score to be greater than 85. So let's write this out first, and the subsequent ideas will be much clearer. + +```sql +SELECT DISTINCT UID +FROM exam_record +WHERE score >= 85 + AND YEAR (start_time) = '2021' +``` + +According to condition 2, then write `people who completed the difficult test paper in half the time and scored more than 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 +``` + +Then just `UNION` the two. (Special attention should be paid to the bracket problem and the position of `order by` here. The specific usage has been mentioned in the previous article) + +**Answer**: + +```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 +``` + +## Connection query + +### The number of test papers completed and the number of question exercises (difficulty) for users who meet the conditions + +**Description**:Existing user information table user_info (uid user ID, nick_name nickname, achievement achievement value, level level, job career direction, register_time registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 3100 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2300 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 | 2500 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 1200 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 5 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | Niuke No. 6 | 2000 | 6 | C++ | 2020-01-01 10:00:00 | + +Examination paper information table examination_info (exam_id examination paper ID, tag examination paper category, difficulty examination paper difficulty, duration examination 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 | C++ | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | algorithm | medium | 80 | 2021-09-01 10:00:00 | + +Examination paper answer record table exam_record (uid user ID, exam_id examination paper ID, start_time start answering time, submit_time submission time, score 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 | + +Question practice record table practice_record (uid user ID, question_id question ID, submit_time submission time, score 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 | + +Please find famous experts whose average scores on the difficult SQL test papers are greater than 80 and who are level 7, count their total number of completed test papers and total number of practice questions in 2021, and only retain users who have completed test paper records in 2021. The results are in ascending order by the number of completed test papers and in descending order by the number of practice questions. + +Sample data output is as follows: + +| uid | exam_cnt | question_cnt | +| ---- | -------- | ------------ | +| 1001 | 1 | 2 | +| 1003 | 2 | 0 | + +Explanation: Users 1001, 1003, 1004, and 1006 meet the requirements of the high-difficulty SQL test paper with an average score greater than 80, but only 1001 and 1003 are level 7 famous bosses; 1001 completed 1 test paper 1001 and practiced the questions 2 times; 1003 completed 2 test papers 9001, 9002, question not practiced (so count is 0) + +**Idea:** + +First conduct preliminary screening of conditions, for example, first find users who have taken difficult SQL test papers + +```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' +``` + +Then according to the requirements of the question, just add the conditions; + +But here’s something to note: + +First: You cannot put the `YEAR(submit_time)= 2021` condition at the end, but in the `ON` condition, because there is a situation where the left join returns all the rows of the left table and the right table is null. The purpose of placing it in the `ON` clause of the `JOIN` condition is to ensure that when connecting two tables, only records that meet the year condition will be connected. This prevents records from other years from being included in the results. That is, 1001 has taken the 2021 test paper but has not practiced it. If the condition is placed at the end, this situation will be eliminated. + +Second, it must be `COUNT(distinct er.exam_id) exam_cnt, COUNT(distinct pr.id) question_cnt, `should be added distinct, because there are many duplicate values ​​​​generated by left joins. + +**Answer**: + +```sql +SELECT er.uid AS UID, + count(DISTINCT er.exam_id) AS exam_cnt, + count(DISTINCT pr.id) AS question_cnt +FROM exam_recorder +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_recorder + 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``` + +Careful friends may find out why user 1003 can still find two exam records despite clearly limiting the conditions to `tag = 'SQL' AND difficulty = 'hard'`, one of which has an exam `tag` of `C++`; This is due to the characteristics of `LEFT JOIN`, even if there are no rows matching the right table, all records in the left table will still be retained. + +### Activity status of each level 6/7 user (difficult) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 3100 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2300 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 | 2500 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 1200 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 5 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | Niuke No. 6 | 2600 | 7 | C++ | 2020-01-01 10:00:00 | + +Examination paper information table `examination_info` (`exam_id` examination paper ID, `tag` examination paper category, `difficulty` examination paper difficulty, `duration` examination 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 | C++ | easy | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | algorithm | medium | 80 | 2021-09-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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) | + +Question practice record table `practice_record` (`uid` user ID, `question_id` question ID, `submit_time` submission time, `score` 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 | + +Please count the total number of active months, number of active days in 2021, number of active days in answering test papers in 2021, and number of active days in answering questions for each level 6/7 user, and sort them in descending order according to the total number of active months and number of active days in 2021. The output from the sample data is as follows: + +| 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 | + +**Explanation**: There are 5 level 6/7 users, of which 1006 was active for a total of 3 months in 202109, 202108, and 202008. The active dates in 2021 are 20210907, 20210804, 20210803, and 20210802 for a total of 4 days. In 2021, it is in the test paper answer area. 20210907 has been active for 1 day and has been active in the question practice area for 3 days. + +**Idea:** + +The key to this question lies in the use of `CASE WHEN THEN`, otherwise you have to write a lot of `left join` because it will produce a lot of result sets. + +The `CASE WHEN THEN` statement is a conditional expression used in SQL to perform different operations or return different results based on conditions. + +The syntax structure is as follows: + +```sql +CASE + WHEN condition1 THEN result1 + WHEN condition2 THEN result2 + ... + ELSE result +END +``` + +In this structure, you can add multiple WHEN clauses as needed, each WHEN clause is followed by a condition and a result. The condition can be any logical expression. If the condition is met, the corresponding result will be returned. + +The final ELSE clause is optional and is used to specify the default return result when all previous conditions are not met. If no `ELSE` clause is provided, `NULL` is returned by default.For example: + +```sql +SELECT score, + CASE + WHEN score >= 90 THEN 'Excellent' + WHEN score >= 80 THEN 'Good' + WHEN score >= 60 THEN 'pass' + ELSE 'failed' + END AS grade +FROM student_scores; +``` + +In the above example, the CASE WHEN THEN statement is used to return the corresponding grade based on different ranges of student scores. If the score is greater than or equal to 90, "Excellent" is returned; if the score is greater than or equal to 80, "Good" is returned; if the score is greater than or equal to 60, "Pass" is returned; otherwise, "Fail" is returned. + +After understanding the above usage, look back at the question and ask to list different active days. + +```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 +``` + +The tag here is given first to facilitate the differentiation of queries and to separate exams and answers. + +Find the users in the test paper answer area + +```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 +``` + +Then there are the users in the question and answer area. + +```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 +``` + +Finally, `UNION` the two results. Finally, don’t forget to sort the results (this question is somewhat similar to the idea of ​​divide and conquer) + +**Answer**: + +```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 +``` + + \ No newline at end of file diff --git a/docs_en/database/sql/sql-questions-04.en.md b/docs_en/database/sql/sql-questions-04.en.md new file mode 100644 index 00000000000..9c380b6e605 --- /dev/null +++ b/docs_en/database/sql/sql-questions-04.en.md @@ -0,0 +1,830 @@ +--- +title: Summary of common SQL interview questions (4) +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL interview questions, window function, ROW_NUMBER, ranking, grouping, MySQL 8 + - - meta + - name: description + content: Summarizes the usage of window functions introduced in MySQL 8, including high-frequency questions and implementation techniques for sorting and grouping statistical scenarios. +--- + +> The question comes from: [Niuke Question Ba - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +You can decide whether to skip more difficult or difficult questions based on your actual situation and interview needs. + +##Specialized window functions + +MySQL version 8.0 introduced support for window functions. The following are common window functions and their usage in MySQL: + +1. `ROW_NUMBER()`: Assign a unique integer value to each row in the query result set. + +```sql +SELECT col1, col2, ROW_NUMBER() OVER (ORDER BY col1) AS row_num +FROM table; +``` + +2. `RANK()`: Calculate the ranking of each row in the sorted results. + +```sql +SELECT col1, col2, RANK() OVER (ORDER BY col1 DESC) AS ranking +FROM table; +``` + +3. `DENSE_RANK()`: Calculate the ranking of each row in the sorted results, keeping the same ranking. + +```sql +SELECT col1, col2, DENSE_RANK() OVER (ORDER BY col1 DESC) AS ranking +FROM table; +``` + +4. `NTILE(n)`: Divide the results into n substantially uniform buckets and assign an identification number to each bucket. + +```sql +SELECT col1, col2, NTILE(4) OVER (ORDER BY col1) AS bucket +FROM table; +``` + +5. `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`: These aggregate functions can also be used in conjunction with window functions to calculate the summary, average, count, minimum and maximum values of specified columns within the window. + +```sql +SELECT col1, col2, SUM(col1) OVER () AS sum_col +FROM table; +``` + +6. `LEAD()` and `LAG()`: The LEAD function is used to get the value of the row at an offset after the current row, while the LAG function is used to get the value of the row at an offset before the current row. + +```sql +SELECT col1, col2, LEAD(col1, 1) OVER (ORDER BY col1) AS next_col1, + LAG(col1, 1) OVER (ORDER BY col1) AS prev_col1 +FROM table; +``` + +7. `FIRST_VALUE()` and `LAST_VALUE()`: The FIRST_VALUE function is used to get the first value of the specified column in the window, and the LAST_VALUE function is used to get the last value of the specified column in the window. + +```sql +SELECT col1, col2, FIRST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS first_val, + LAST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS last_val +FROM table; +``` + +Window functions usually need to be used together with the OVER clause to define the size, sorting rules and grouping method of the window. + +### The top three scores in each type of test paper + +**Description**: + +Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test 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 | algorithm | medium | 80 | 2021-09-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` submission time, score 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) | + +Find the top 3 scores for each type of test paper. If the two have the same maximum score, choose the one with the larger minimum score. If they are still the same, choose the one with the larger uid. The output from the sample data is as follows: + +| tid | uid | ranking | +| ---- | ---- | ------- | +| SQL | 1003 | 1 | +| SQL | 1004 | 2 | +| SQL | 1002 | 3 | +| Algorithm | 1005 | 1 | +| Algorithm | 1006 | 2 | +| Algorithm | 1003 | 3 | + +**Explanation**: The test paper tags with answer score records include SQL and algorithm. SQL test paper users 1001, 1002, 1003, and 1004 have answer scores. The highest scores are 81, 81, 89, and 85 respectively, and the lowest scores are 78, 81, 86, and 40. Therefore, the top three are ranked according to the highest score first and then the lowest score, which are 1003, 1004, and 1002. + +**Answer**: + +```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``` + +### The second fastest/slowest test paper whose time difference is greater than half the test paper length (more difficult) + +**Description**: + +Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test 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 | C++ | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | algorithm | medium | 80 | 2021-09-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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) | + +Find the test paper information where the difference between the second fastest and the second slowest time is greater than half of the test paper length, and sort by test paper ID in descending order. The output from the sample data is as follows: + +| exam_id | duration | release_time | +| ------- | -------- | ------------------- | +| 9001 | 60 | 2021-09-01 06:00:00 | + +**Explanation**: Test paper 9001 takes 50 minutes, 58 minutes, 30 minutes and 1 second, 19 minutes, and 10 minutes. The difference between the second fastest and the second slowest time is 50 minutes - 19 minutes = 31 minutes. The test paper duration is 60 minutes. Therefore, the condition of being greater than half of the test paper length is met, and the test paper ID, duration, and release time are output. + +**Idea:** + +The first step is to find the sequential ranking and reverse ranking of the completion time of each test paper, which is Table a; + +In the second step, establish an inner connection with the passed test paper information table b, group it according to the test paper id, use `having` to filter the ranking into the second data, convert seconds into minutes and compare, and finally sort according to the test paper id in reverse order. + +**Answer**: + +```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 +``` + +### The maximum time window for answering the test paper twice in a row (more difficult) + +**Description** + +Existing exam paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` starting answering time, `submit_time` handing in time, `score` 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 | + +Please calculate among those who have answered the test paper on at least two days in 2021, calculate the maximum time window `days_window` for answering the test paper twice in a row in that year, then according to the historical rules of the year, how many sets of test papers he will take on average in `days_window` days, sort in reverse order by the maximum time window and the average number of sets of answer papers. The output from the sample data is as follows: + +| uid | days_window | avg_exam_cnt | +| ---- | ----------- | ------------ | +| 1006 | 6 | 2.57 |**Explanation**: User 1006 answered 3 test papers in 20210901, 20210906, and 20210907 respectively. The maximum time window for two consecutive responses is 6 days (1st to 6th). He took a total of 3 test papers in the 7 days from 1st to 7th. On average, 3/7=0.428571 papers per day, so on average he will do 3 test papers in 6 days. 0.428571\*6=2.57 test papers (keep two decimal places); User 1005 took two test papers in 20210905, but only had one day's answer records, so filter them out. + +**Idea:** + +The above explanation reminds you to remove duplicates from the answer records. Don’t be fooled and don’t delete duplicates! If you remove duplicates, the test case will fail. Note that the restriction is in 2021; + +And please note that the time difference is +1 day; also note that == unsubmitted papers are also counted ==! ! ! ! (Anyway, I feel that the description of this question is unclear and the answer is not very good) + +**Answer**: + +```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 +``` + +### The completion status of users who have not completed 0 in the past three months + +**Description**: + +Existing test paper answer record table `exam_record` (`uid`: user ID, `exam_id`: test paper ID, `start_time`: start answering time, `submit_time`: handing in time, if it is empty, it means not completed, `score`: 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) | + +Find the number of test paper completions for users who have no test paper in the unfinished status in the last three months for each person who has test paper answer records, and rank them in descending order by the number of test paper completions and user ID. The output from the sample data is as follows: + +| uid | exam_complete_cnt | +| ---- | ------------------ | +| 1006 | 3 | + +**Explanation**: User 1006 has answered test papers in the last three months of 202109, 202108, and 202106, and the number of answered test papers is 3, all of which have been completed; User 1001 has answered test papers in the last three months of 202109, 202108, and 202107, and the number of answered test papers is 5, and the number of completed test papers is 4. Because there are unfinished test papers, they are filtered out. + +**Idea:** + +1. `Find the number of completed test papers for users who have no test paper answer records in the past three months and have no test paper answer records.` First of all, look at this sentence. You must first group them according to people. +2. In the past three months, you can use continuous repeated ranking, sort in reverse order, ranking <=3 +3. Count the number of answers +4. Assemble remaining conditions +5. Sort + +**Answer**: + +```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 +``` + +### Response status of the 50% users with high incompleteness rate in the past three months (difficult) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 3200 | 7 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2500 | 6 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 2200 | 5 | Algorithm | 2020-01-01 10:00:00 | + +Examination paper information table `examination_info` (`exam_id` examination paper ID, `tag` examination paper category, `difficulty` examination paper difficulty, `duration` examination 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 | SQL | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | Algorithm | hard | 80 | 2020-01-01 10:00:00 | +| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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` 得分): + +| 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 +``` + +## Aggregation window function + +### Perform min-max normalization on test paper scores + +**Description**: + +Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test 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 | C++ | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | Algorithm | hard | 80 | 2020-01-01 10:00:00 | +| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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) | + +In physics and statistical data calculation, there is a concept called min-max standardization, also known as dispersion standardization, which is a linear transformation of the original data so that the result value is mapped to [0 - 1]. + +The conversion function is: + +![](https://oss.javaguide.cn/github/javaguide/database/sql/29A377601170AB822322431FCDF7EDFE.png) + +Please perform min-max normalization on each test paper's answer record and scale it to the [0,100] interval, and output the user ID, test paper ID, and average score after normalization; finally, output the test paper ID in ascending order and the normalized score in descending order. (Note: The score interval defaults to [0,100]. If there is only one score in the answer record of a certain test paper, there is no need to use a formula. The score will still be the original score after normalization and scaling). + +The output from the sample data is as follows: + +| uid | exam_id | avg_new_score | +| ---- | ------- | ------------- | +| 1001 | 9001 | 98 | +| 1003 | 9001 | 0 | +| 1002 | 9002 | 88 | +| 1003 | 9002 | 75 | +| 1001 | 9002 | 70 | +| 1004 | 9002 | 0 | + +Explanation: The difficult papers are 9001, 9002, and 9003; + +There are 3 records of answering 9001, and the scores are 68, 89, and 90 respectively. After normalization according to the given formula, the scores are: 0, 95, 100, and the latter two scores are answered by user 1001. Therefore, user 1001’s new score for test paper 9001 is (95+100)/2≈98 (only the integer part is retained). User 1003’s score for the test paper 9001 has a new score of 0. The final results are output in ascending order of test paper ID and descending order of normalized scores. + +**Idea:** + +Note: + +1. For difficult test papers, use the max/min (col) over() window function to find the maximum and minimum values in each group according to the scores of each type of test paper, and then calculate the normalized formula. The scaling interval is [0,100], that is, min_max\*100 +2. If a certain type of test paper has only one score, there is no need to use the normalization formula, because there is only one score max_score=min_score,score, and the result may become 0 after the formula. +3. The final results are grouped by uid and exam_id to find the normalized mean. Those with a score of NULL should be filtered out. + +The last thing is to look carefully at the above formula (to be honest, this question seems very convoluted) + +**Answer**: + +```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; +``` + +### The number of answers per test paper per month and the total number of answers as of the current month + +**Description:** + +Existing test paper answer record table exam_record (uid user ID, exam_id test paper ID, start_time start answering time, submit_time handover time, score 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) | + +Please output the number of responses per month for each test paper and the total number of responses as of the current month. +The output from the sample data is as follows: + +| 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 | + +Explanation: Test paper 9001 has been answered in 4 months, 202001, 202002, 202003, and 202005. The number of answers per month is 2, 1, 3, and 1 respectively. The total number of answers as of the current month is 2, 3, 6, and 7. + +**Idea:** + +This question has two key points: counting the total number of answers as of the current month, outputting the number of monthly responses for each test paper, and outputting the total number of responses as of the current month. + +This is the key `**sum(count(*)) over(partition by exam_id order by date_format(start_time,'%Y%m'))**` + +**Answer**: + +```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 +``` + +### Question answering status every month and as of the current month (more difficult) + +**Description**: Existing test paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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) | + +Please output the number of monthly active users, the number of new users, the maximum number of new users in a single month as of the current month, and the cumulative number of users as of the current month in the monthly test paper response records since the user's answer record was created. The results are output in ascending order of month. + +The output from the sample data is as follows: + +| 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 | | |As can be seen from the above matrix, there were 2 active users in January 2020 (mau=2), and the number of new users that month was 2; + +There were 4 active users in February 2020, the number of new users in that month was 2, the maximum number of new users in a single month was 2, and the current cumulative number of users was 4. + +**Idea:** + +Difficulties: + +1. How to find new users every month + +2. Answers as of the current month + +Rough process: + +(1) Count each person’s first login month `min()` + +(2) Statistics of monthly active and new users: first get each person’s first login month, and then sum up the first login month grouping to get the number of new users in that month. + +(3) Count the maximum number of new users in a single month as of the current month, the cumulative number of users as of the current month, and finally output them in ascending order by month + +**Answer**: + +```sql +--The maximum number of new users in a single month as of the current month and the cumulative number of users as of the current month, output in ascending order by month +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 + ( + -- Statistics of monthly active users and number of new users + 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 ( + -- Count each person's first login month + 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 +``` + + \ No newline at end of file diff --git a/docs_en/database/sql/sql-questions-05.en.md b/docs_en/database/sql/sql-questions-05.en.md new file mode 100644 index 00000000000..849447aeb08 --- /dev/null +++ b/docs_en/database/sql/sql-questions-05.en.md @@ -0,0 +1,1037 @@ +--- +title: Summary of common SQL interview questions (5) +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL interview questions, null value processing, statistics, incomplete rate, CASE, aggregation + - - meta + - name: description + Content: Analyzes null value processing and statistics topics, combines CASE and aggregate functions to provide a robust implementation, and avoid common pitfalls. +--- + +> The question comes from: [Niuke Question Ba - SQL Advanced Challenge](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +You can decide whether to skip more difficult or difficult questions based on your actual situation and interview needs. + +## Null value handling + +### Count the number of unfinished papers and their unfinished rate + +**Description**: + +The existing examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` starting answering time, `submit_time` handing in time, `score` score), the data is as follows: + +| 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-09-02 12:01:01 | (NULL) | (NULL) | + +Please count the number of incomplete_cnt and incomplete_rate of the test papers with incomplete status. The output from the sample data is as follows: + +| exam_id | incomplete_cnt | complete_rate | +| ------- | -------------- | ------------- | +| 9001 | 1 | 0.333 | + +Explanation: Test Paper 9001 has records of being answered three times, of which two were completed and one was incomplete. Therefore, the number of incompletes is 1 and the incomplete rate is 0.333 (retaining 3 decimal places) + +**Ideas**: + +For this question, you only need to note that one has conditional restrictions and the other does not have conditional restrictions; either query the conditions separately and then merge them; or perform conditional judgment directly in select. + +**Answer**: + +Writing method 1: + +```sql +SELECT + exam_id, + (COUNT(*) - COUNT(submit_time)) AS incomplete_cnt, + ROUND((COUNT(*) - COUNT(submit_time)) / COUNT(*), 3) AS incomplete_rate +FROM + exam_record +GROUP BY + exam_id +HAVING + (COUNT(*) - COUNT(submit_time)) > 0; +``` + +Use `COUNT(*)` to count the total number of records in the group, `COUNT(submit_time)` only counts the number of records whose `submit_time` field is not NULL (that is, the number of completed records). Subtracting the two is the unfinished number. + +Writing method 2: + +```sql +SELECT + exam_id, + COUNT(CASE WHEN submit_time IS NULL THEN 1 END) AS incomplete_cnt, + ROUND(COUNT(CASE WHEN submit_time IS NULL THEN 1 END) / COUNT(*), 3) AS incomplete_rate +FROM + exam_record +GROUP BY + exam_id +HAVING + COUNT(CASE WHEN submit_time IS NULL THEN 1 END) > 0; +``` + +Use a `CASE` expression to return a non-`NULL` value (such as 1) when the condition is met, otherwise return `NULL`. Then use the `COUNT` function to count the number of non-`NULL` values. + +Writing method 3: + +```sql +SELECT + exam_id, + SUM(submit_time IS NULL) AS incomplete_cnt, + ROUND(SUM(submit_time IS NULL) / COUNT(*), 3) AS incomplete_rate +FROM + exam_record +GROUP BY + exam_id +HAVING + incomplete_cnt > 0; +``` + +Use the `SUM` function to sum an expression. The expression `(submit_time IS NULL)` evaluates to 1 (TRUE) when `submit_time` is `NULL` and 0 (FALSE) otherwise. Adding these 1s and 0s together gives you the number of outstanding items. + +### Average time and average score of high-difficulty test papers for level 0 users + +**Description**: + +The existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time), the data is as follows: + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 10 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 2100 | 6 | Algorithm | 2020-01-01 10:00:00 | + +Test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test duration, `release_time` release time), the data is as follows: + +| 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 | algorithm | medium | 80 | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` submission time, `score` score), the data is as follows: + +| 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 | + +Please output the average test time and average score of all high-difficulty test papers for each level 0 user, the maximum test time and 0-point processing for unfinished default test papers. The output from the sample data is as follows: + +| uid | avg_score | avg_time_took | +| ---- | --------- | ------------- | +| 1001 | 33 | 36.7 | + +Explanation: There are 1001 level 0 users, 9001 high-difficulty test papers, and there are 3 records of 1001 answering 9001, which took 20 minutes, incomplete (the test paper lasted 60 minutes), and 30 minutes (less than 31 minutes). The scores were 80 points, incomplete (processed with 0 points), and 20 points respectively. Therefore his average time is 110/3=36.7 (rounded to one decimal place) and his average score is 33 points (rounded) + +**Idea**: It is most convenient to use `IF` for this question because it involves the judgment of NULL values. Of course, `case when` can also be used, which is very similar. The difficulty of this question lies in the processing of null values. I believe that other query conditions and so on will not be difficult for everyone. + +**Answer**: + +```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_recorder + 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 +``` + +## Advanced conditional statements + +### Filter users with limited nickname achievement value and active date (harder) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 1000 | 2 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Attack No. 3 | 2200 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 2500 | 6 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 5 | 3000 | 7 | C++ | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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 | + +Question practice record table `practice_record` (`uid` user ID, `question_id` question ID, `submit_time` submission time, `score` 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 | + +Please find the user information whose nickname starts with "Niuke" and ends with "号", whose achievement value is between 1200 and 2500, and who was last active (answering questions or answering papers) in September 2021. + +The output from the sample data is as follows: + +| uid | nick_name | achievement | +| ---- | --------- | ----------- | +| 1002 | Niuke No. 2 | 1200 | + +**Explanation**: There are 1002 and 1004 whose nicknames start with "Niuke" and end with "号" and have achievement values between 1200 and 2500; + +1002 The last active test paper area was September 2021, and the last active question area was September 2021; 1004 The last active test paper area was August 2021, and the question area was not active. + +Therefore, only 1002 finally meets the condition. + +**Ideas**: + +First list the main query statements according to the conditions + +Nickname starts with "Niuke" and ends with "number": `nick_name LIKE "牛客%号"` + +Achievement value is between 1200~2500: `achievement BETWEEN 1200 AND 2500` + +The third condition is limited to September, so just write it directly: `( date_format( record.submit_time, '%Y%m' )= 202109 OR date_format( pr.submit_time, '%Y%m' )= 202109 )` + +**Answer**: + +```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 "nickname%" + 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 +``` + +### Filter the answer records of nickname rules and test paper rules (more difficult) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 1900 | 2 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 2200 | 5 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 2500 | 6 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 555 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +Examination paper information table `examination_info` (`exam_id` examination paper ID, `tag` examination paper category, `difficulty` examination paper difficulty, `duration` examination duration, `release_time` release time): + +| 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 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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 | + +Find the completed test paper IDs and average scores for test paper categories starting with the letter c (such as C, C++, c#, etc.) for users whose nicknames consist of "Niuke" + pure numbers + "号" or pure numbers, and sort them in ascending order by user ID and average score. The output from the sample data is as follows: + +| uid | exam_id | avg_score | +| ---- | ------- | --------- | +| 1002 | 9001 | 81 | +| 1002 | 9002 | 85 | +| 1005 | 9001 | 84 | +| 1006 | 9001 | 84 | + +Explanation: The users whose nicknames meet the conditions are 1002, 1004, 1005, and 1006; + +The test papers starting with c include 9001 and 9002; + +Among the answer records that meet the above conditions, the scores for 1002 and 9001 are 81 and 80, with an average score of 81 (80.5 is rounded up to 81); + +1002 completed 9002 with scores of 90, 82, and 83, with an average score of 85; + +**Ideas**: + +It’s still the same as before. Since the conditions are given, write out each condition first. + +Find users whose nicknames consist of "Niuke" + pure numbers + "号" or pure numbers: I initially wrote this: `nick_name LIKE 'Nuke% number' OR nick_name REGEXP '^[0-9]+$'`. If there is a "Nike H number" in the table, it will also pass. + +So you have to use regular rules here: `nick_name LIKE '^nickel[0-9]+号'` + +For test paper categories starting with the letter c: `e_info.tag LIKE 'c%'` or `tag regexp '^c|^C'` The first one can also match the uppercase C + +**Answer**: + +```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 "^nickname[0-9]+number $" + 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; +``` + +### Output different situations based on whether the specified record exists (difficulty) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 19 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Attack No. 3 | 22 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 25 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 555 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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 | + +Please filter the data in the table. When the number of uncompleted test papers for any level 0 user is greater than 2, output the number of uncompleted test papers and the uncompleted rate (retaining 3 decimal places) for each level 0 user. If there is no such user, output these two indicators for all users with answer records. Results are sorted in ascending order by incomplete rate. + +The output from the sample data is as follows: + +| uid | incomplete_cnt | incomplete_rate | +| ---- | -------------- | --------------- | +| 1004 | 0 | 0.000 | +| 1003 | 1 | 0.500 | +| 1001 | 4 | 0.667 | + +**Explanation**: There are 1001, 1003, and 1004 level 0 users; the number of test papers they answered and the number of unfinished papers are: 6:4, 2:1, and 0:0 respectively; + +There is a level 0 user 1001 whose number of unfinished test papers is greater than 2, so the unfinished number and uncompleted rate of these three users are output (1004 has not answered the test paper, and the unfinished rate is filled with 0 by default, and it is 0.000 after retaining 3 decimal places); + +Results are sorted in ascending order by incomplete rate. + +Attachment: If 1001 does not satisfy "the number of unfinished test papers is greater than 2", you need to output the two indicators of 1001, 1002, and 1003, because there are only the answer records of these three users in the test paper answer record table. + +**Ideas**: + +First write out the SQL that may satisfy the condition **"The number of unfinished test papers for level 0 users is greater than 2"** + +```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``` + +Then write the SQL query statements for the two situations: + +Situation 1. Query the uncompleted rate of test papers for level 0 users with conditional requirements + +```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 +``` + +Situation 2. Query the uncompletion rate of all yong users who have answer records when there are no conditional requirements. + +```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 +``` + +Putting them together is the answer + +```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), + tt1AS + (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), + tt2AS + (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 version (based on the improvements made above, the answer is shortened and the logic is stronger): + +```sql +SELECT + ui.uid, + SUM( + IF + ( start_time IS NOT NULL AND score IS NULL, 1, 0 )) AS incomplete_cnt, #3. The number of incomplete test papers + ROUND( AVG( IF ( start_time IS NOT NULL AND score IS NULL, 1, 0 )), 3 ) AS incomplete_rate #4. Incomplete rate + +FROM + user_info ui + LEFT JOIN exam_record USING ( uid ) +WHERE +CASE + + WHEN (#1. When any level 0 user has more than 2 uncompleted test papers + 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 Find each level 0 user + SELECT uid FROM user_info WHERE LEVEL = 0 ) ELSE uid IN ( #2. If there is no such user, find the user with the answer record + SELECT DISTINCT uid FROM exam_record ) + END + GROUP BY + ui.uid + ORDER BY + incomplete_rate #5.The results are sorted in ascending order by incomplete rate +``` + +### The proportion of different score performance of each user level (more difficult) + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 19 | 0 | Algorithm | 2020-01-01 10:00:00 || 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 22 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 25 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 555 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +Examination paper answer record table exam_record (uid user ID, exam_id examination paper ID, start_time start answering time, submit_time submission time, score 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 | + +In order to obtain the qualitative performance of the user's answer to the test paper, we divided the test paper scores into four score levels of excellent, medium and poor according to the cut-off point [90, 75, 60] (the cut-off point is divided into the left interval). Please count the proportion of each score level in the completed test papers of people with different user levels (the results are retained to 3 decimal places). Users who have not completed the test paper do not need to output. The results are sorted in descending order of user level and proportion. + +The output from the sample data is as follows: + +| level | score_grade | ratio | +| ----- | ----------- | ----- | +| 3 | Good | 0.667 | +| 3 | Excellent | 0.333 | +| 0 | Good | 0.500 | +| 0 | Medium | 0.167 | +| 0 | Excellent | 0.167 | +| 0 | Difference | 0.167 | + +Explanation: The number of users who have completed the test paper are 1001 and 1002; the user levels and score levels corresponding to the completed test papers are as follows: + +| uid | exam_id | score | level | score_grade | +| ---- | ------- | ----- | ----- | ----------- | +| 1001 | 9001 | 80 | 0 | Good | +| 1001 | 9002 | 75 | 0 | Good | +| 1001 | 9002 | 60 | 0 | Medium | +| 1001 | 9003 | 90 | 0 | Excellent | +| 1001 | 9001 | 20 | 0 | Poor | +| 1001 | 9002 | 89 | 0 | Good | +| 1002 | 9001 | 99 | 3 | Excellent | +| 1002 | 9003 | 82 | 3 | Good | +| 1002 | 9003 | 76 | 3 | Good | + +Therefore, the proportion of each score level for level 0 users (only 1001) is: excellent 1/6, good 1/6, average 1/6, and poor 3/6; the proportion of each score level for level 3 users (only 1002) is: excellent 1/3, good 2/3. The result is rounded to 3 decimal places. + +**Ideas**: + +First write out the condition **"Divide the test paper scores into four score levels according to the cut-off points [90, 75, 60]: excellent, medium and poor"**, you can use `case when` here + +```sql +CASE + WHEN a.score >= 90 THEN + 'Excellent' + WHEN a.score < 90 AND a.score >= 75 THEN + 'good' + WHEN a.score < 75 AND a.score >= 60 THEN + 'medium' ELSE 'poor' +END +``` + +The key point of this question is this, the rest is conditional splicing + +**Answer**: + +```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 'Excellent' + WHEN a.score < 90 + AND a.score >= 75 THEN 'good' + WHEN a.score < 75 + AND a.score >= 60 THEN 'medium' + ELSE 'poor' + 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, + ratioDESC +``` + +## Limited query + +### The three people with the earliest registration time + +**Description**: + +Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time || --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke No. 1 | 19 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-02-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 22 | 0 | Algorithm | 2020-01-02 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 5 | 1005 | Niuke No. 555 | 4000 | 7 | C++ | 2020-01-11 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-11-01 10:00:00 | + +Please find the 3 people with the earliest registration time. The output from the sample data is as follows: + +| uid | nick_name | register_time | +| ---- | ------------ | ------------------- | +| 1001 | Niuke 1 | 2020-01-01 10:00:00 | +| 1003 | Niuke No. 3 ♂ | 2020-01-02 10:00:00 | +| 1004 | Niuke No. 4 | 2020-01-02 11:00:00 | + +Explanation: Select the top three after sorting by registration time, and output their user ID, nickname, and registration time. + +**Answer**: + +```sql +SELECT uid, nick_name, register_time + FROM user_info + ORDER BY register_time + LIMIT 3 +``` + +### Completed the third page of the test paper list on the day of registration (more difficult) + +**Description**: Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | Niuke 1 | 19 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 22 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 25 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 5 | 1005 | Niuke No. 555 | 4000 | 7 | Algorithm | 2020-01-11 10:00:00 | +| 6 | 1006 | Niuke No. 6 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 7 | 1007 | Niuke No. 7 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 8 | 1008 | Niuke No. 8 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 9 | 1009 | Niuke No. 9 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 10 | 1010 | Niuke No. 10 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | +| 11 | 1011 | 666666 | 3000 | 6 | C++ | 2020-01-02 10:00:00 | + +Examination paper information table examination_info (exam_id examination paper ID, tag examination paper category, difficulty examination paper difficulty, duration examination duration, release_time release time): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | algorithm | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | Algorithm | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | SQL | medium | 70 | 2020-01-01 10:00:00 | + +Examination paper answer record table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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)Those who are looking for a job as an algorithm engineer and have completed the algorithm test paper on the day of registration will be ranked according to the highest scores in all exams they have taken. The ranking list is very long. We will display it in pages, with 3 items per page. Now you need to take out the information of the people on page 3 (the page number starts from 1). + +The output from the sample data is as follows: + +| 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 | + +Explanation: Except for 1011, all other users are looking for algorithm engineers; there are 9001 and 9002 algorithm test papers, and all 11 users completed the algorithm test papers on the day of registration; calculating the maximum time points of all their exams, only 1002 and 1008 completed two exams, and the others only completed one exam. The highest score for 1002 in the two exams was 81, and the highest score for 1008 was 99. + +Ranking by highest score is as follows: + +| 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 | + +There are 3 items per page, and the third page is the 7th to 9th items. Just return row records of 1010, 1003, and 1004. + +**Ideas**: + +1. Three items per page, that is, the information of the person on the third page needs to be retrieved, so `limit` must be used. + +2. Statistics of people who are looking for jobs as algorithm engineers and who have completed the algorithm test paper on the day of registration and the scores for each record. First find the users who meet the conditions, and then use left join to connect to find the information and the scores for each record. + +**Answer**: + +```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 = 'algorithm' + AND tag = 'algorithm' +GROUP BY t1.uid, + LEVEL, + register_time +ORDER BY max_score DESC +LIMIT 6,3 +``` + +## Text conversion function + +### Repair serialized records + +**Description**: Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` exam duration, `release_time` release time): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | -------------- | ---------- | -------- | ------------------- | +| 1 | 9001 | algorithm | hard | 60 | 2021-01-01 10:00:00 | +| 2 | 9002 | Algorithm | hard | 80 | 2021-01-01 10:00:00 | +| 3 | 9003 | SQL | medium | 70 | 2021-01-01 10:00:00 | +| 4 | 9004 | Algorithm,medium,80 | | 0 | 2021-01-01 10:00:00 | + +A student who recorded the questions accidentally entered the test category tag, difficulty, and duration of some records into the tag field at the same time. Please help find these incorrect records, split them, and output them in the correct column type. + +The output from the sample data is as follows: + +| exam_id | tag | difficulty | duration | +| ------- | ---- | ---------- | -------- | +| 9004 | algorithm | medium | 80 | + +**Ideas**: + +Let’s first learn the functions used in this question + +The `SUBSTRING_INDEX` function is used to extract the portion of a string with a specified delimiter. It accepts three parameters: the original string, the delimiter, and the number of parts specified to be returned. + +The following is the syntax of the `SUBSTRING_INDEX` function: + +```sql +SUBSTRING_INDEX(str, delimiter, count) +``` + +- `str`: the original string to be split. +- `delimiter`: string or character used as delimiter. +- `count`: Specifies the number of parts to return. + - If `count` is greater than 0, return the first `count` parts starting from the left (bounded by the delimiter). + - If `count` is less than 0, return the first `count` parts (bounded by the delimiter) starting from the right, counting from the right to the left. + +Here are some examples demonstrating the use of the `SUBSTRING_INDEX` function: + +1. Extract the first part of the string: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 1); + -- Output result: 'apple' + ``` + +2. Extract the last part of the string: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -1); + -- Output result: 'cherry' + ``` + +3. Extract the first two parts of the string: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 2); + -- Output result: 'apple,banana' + ``` + +4. Extract the last two parts of the string: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -2); + -- Output result: 'banana,cherry' + ``` + +**Answer**: + +```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 = '' +``` + +### Interception processing of nicknames that are too long + +**Description**: Existing user information table `user_info` (`uid` user ID, `nick_name` nickname, `achievement` achievement value, `level` level, `job` career direction, `register_time` registration time): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ----------------------- | ----------- | ----- | ---- | ------------------- || 1 | 1001 | Niuke 1 | 19 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 2 | 1002 | Niuke No. 2 | 1200 | 3 | Algorithm | 2020-01-01 10:00:00 | +| 3 | 1003 | Niuke No. 3 ♂ | 22 | 0 | Algorithm | 2020-01-01 10:00:00 | +| 4 | 1004 | Niuke No. 4 | 25 | 0 | Algorithm | 2020-01-01 11:00:00 | +| 5 | 1005 | Niuke No. 5678901234 | 4000 | 7 | Algorithm | 2020-01-11 10:00:00 | +| 6 | 1006 | Niuke No. 67890123456789 | 25 | 0 | Algorithm | 2020-01-02 11:00:00 | + +Some users' nicknames are particularly long, which may cause style confusion in some display scenarios. Therefore, particularly long nicknames need to be converted before outputting. Please output user information with more than 10 characters. For users with more than 13 characters, output the first 10 characters and then add three periods: "...". + +The output from the sample data is as follows: + +| uid | nick_name | +| ---- | ------------------ | +| 1005 | Niuke No. 5678901234 | +| 1006 | Niuke 67890123... | + +Explanation: There are 1005 and 1006 users with more than 10 characters, and the lengths are 13 and 17 respectively; therefore, the output of the nickname of 1006 needs to be truncated. + +**Ideas**: + +This question involves character calculation. To calculate the number of characters in a string (that is, the length of the string), you can use the `LENGTH` function or the `CHAR_LENGTH` function. The difference between these two functions is the way multibyte characters are treated. + +1. `LENGTH` function: It returns the number of bytes of the given string. For strings containing multibyte characters, each character is counted as a byte. + +Example: + +```sql +SELECT LENGTH('Hello'); -- Output result: 6, because each Chinese character in 'Hello' occupies 3 bytes each +``` + +1. `CHAR_LENGTH` function: It returns the number of characters of the given string. For strings containing multibyte characters, each character is counted as one character. + +Example: + +```sql +SELECT CHAR_LENGTH('Hello'); -- Output result: 2, because there are two characters in 'Hello', that is, two Chinese characters +``` + +**Answer**: + +```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; +``` + +### Filter statistics when case is confused (difficult) + +**Description**: + +Existing test paper information table `examination_info` (`exam_id` test paper ID, `tag` test paper category, `difficulty` test paper difficulty, `duration` test duration, `release_time` release time): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | algorithm | 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 | + +Examination paper answer information table `exam_record` (`uid` user ID, `exam_id` test paper ID, `start_time` start answering time, `submit_time` handing in time, `score` 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 | + +The category tags of the test paper may have mixed case. Please first filter out the category tags with less than 3 answers in the test paper, and count the corresponding number of answers in the original test paper after converting them to uppercase. + +If the tag has not changed after conversion, the result will not be output. + +The output from the sample data is as follows: + +| tag | answer_cnt | +| --- | ---------- | +|C++|6| + +Explanation: The papers that have been answered include 9001, 9002, 9003, and 9004. Their tags and the number of times they have been answered are as follows: + +| exam_id | tag | answer_cnt | +| ------- | ---- | ---------- || 9001 | Algorithm | 4 | +| 9002 | C++ | 6 | +| 9003 | c++ | 2 | +| 9004 | sql | 2 | + +Tags with less than 3 answers include c++ and sql. However, after being converted to uppercase, only C++ has an original answer count. Therefore, the output number of c++ after being converted to uppercase is 6. + +**Ideas**: + +First of all, this question is a bit confusing. According to the sample data, 9004 is found only once, but here it is shown that there are two times. + +Let’s take a look at the case conversion function first: + +1. The `UPPER(s)` or `UCASE(s)` function can convert all alphabetic characters in the string s into uppercase letters; + +2. The `LOWER(s)` or `LCASE(s)` function can convert all alphabetic characters in the string s into lowercase letters. + +The difficulty is that when joining the same table, you need to query different values. + +**Answer**: + +```sql +WITH a AS + (SELECT tag, + COUNT(start_time) AS answer_cnt + FROM exam_recorder + 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 lower case b upper case +AND a.tag != b.tag +WHERE a.answer_cnt < 3; +``` + + \ No newline at end of file diff --git a/docs_en/database/sql/sql-syntax-summary.en.md b/docs_en/database/sql/sql-syntax-summary.en.md new file mode 100644 index 00000000000..87185b87558 --- /dev/null +++ b/docs_en/database/sql/sql-syntax-summary.en.md @@ -0,0 +1,1208 @@ +--- +title: Summary of basic knowledge of SQL syntax +category: database +tag: + - Database basics + - SQL +head: + - - meta + - name: keywords + content: SQL syntax, DDL, DML, DQL, constraints, transactions, indexes, paradigms + - - meta + - name: description + content: Systematically organizes basic SQL syntax and terminology, covering DDL/DML/DQL, constraints and transaction indexes, forming a knowledge path from entry to practice. +--- + +> This article is compiled and improved from the following two materials: +> +> - [SQL Syntax Quick Manual](https://juejin.cn/post/6844903790571700231) +> - [MySQL Super Complete Tutorial](https://www.begtut.com/mysql/mysql-tutorial.html) + +## Basic concepts + +### Database terminology + +- `database` - A container (usually a file or set of files) that holds organized data. +- `table` - a structured list of data of a specific type. +- `schema` - information about the layout and properties of databases and tables. The schema defines how data is stored in the table, including what kind of data is stored, how the data is decomposed, how each part of the information is named, and other information. Both databases and tables have schemas. +- `column` – a field in a table. All tables are composed of one or more columns. +- `row(row)` - a record in the table. +- `primary key` - a column (or set of columns) whose value uniquely identifies each row in a table. + +### SQL syntax + +SQL (Structured Query Language), standard SQL is managed by the ANSI Standards Committee, so it is called ANSI SQL. Each DBMS has its own implementation, such as PL/SQL, Transact-SQL, etc. + +#### SQL syntax structure + +![](https://oss.javaguide.cn/p3-juejin/cb684d4c75fc430e92aaee226069c7da~tplv-k3u1fbpfcp-zoom-1.png) + +SQL syntax structures include: + +- **`clause`** - is the component of statements and queries. (In some cases, these are optional.) +- **`expression`** - can produce any scalar value, or a database table consisting of columns and rows +- **`Predicate`** - Specify conditions for SQL three-valued logic (3VL) (true/false/unknown) or Boolean truth values that need to be evaluated, and limit the effects of statements and queries, or change program flow. +- **`Query`** - Retrieve data based on specific criteria. This is an important part of SQL. +- **`Statement`** - Can permanently affect the schema and data, and can also control database transactions, program flow, connections, sessions, or diagnostics. + +#### SQL syntax points + +- **SQL statements are not case-sensitive**, but whether database table names, column names and values are distinguished depends on the specific DBMS and configuration. For example: `SELECT` is the same as `select`, `Select`. +- **Multiple SQL statements must be separated by semicolons (`;`)**. +- **All spaces are ignored** when processing SQL statements. + +SQL statements can be written in one line or divided into multiple lines. + +```sql +-- One line of SQL statement + +UPDATE user SET username='robot', password='robot' WHERE username = 'root'; + +--Multi-line SQL statement +UPDATE user +SET username='robot', password='robot' +WHERE username = 'root'; +``` + +SQL supports three types of comments: + +```sql +## Note 1 +-- Note 2 +/* Note 3 */ +``` + +### SQL Classification + +#### Data Definition Language (DDL) + +Data Definition Language (DDL) is a language in the SQL language that is responsible for the definition of data structures and database objects. + +The main function of DDL is to define database objects. + +The core instructions of DDL are `CREATE`, `ALTER`, and `DROP`. + +#### Data Manipulation Language (DML) + +Data Manipulation Language (DML) is a programming statement used for database operations to access objects and data in the database. + +The main function of DML is to **access data**, so its syntax is mainly based on **reading and writing database**. + +The core instructions of DML are `INSERT`, `UPDATE`, `DELETE`, and `SELECT`. These four instructions are collectively called CRUD (Create, Read, Update, Delete), which means add, delete, modify, and query. + +#### Transaction Control Language (TCL) + +Transaction Control Language (TCL) is used to manage transactions in the database. These are used to manage changes made by DML statements. It also allows statements to be grouped into logical transactions. + +The core instructions of TCL are `COMMIT` and `ROLLBACK`. + +#### Data Control Language (DCL) + +Data Control Language (DCL) is an instruction that can control data access rights. It can control the control rights of specific user accounts to database objects such as data tables, view tables, stored procedures, and user-defined functions. + +The core instructions of DCL are `GRANT` and `REVOKE`. + +DCL mainly focuses on controlling user access rights, so its instruction method is not complicated. The permissions that can be controlled by DCL are: `CONNECT`, `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `EXECUTE`, `USAGE`, `REFERENCES`. + +Different DBMSs and different security entities support different permission controls. + +**Let’s first introduce the usage of DML statements. The main function of DML is to read and write databases to implement additions, deletions, modifications and queries. ** + +## Add, delete, modify and check + +Add, delete, modify, and query, also known as CRUD, is a basic operation in basic database operations. + +### Insert data + +The `INSERT INTO` statement is used to insert new records into the table. + +**INSERT COMPLETE ROW** + +```sql +# insert a row +INSERT INTO user +VALUES (10, 'root', 'root', 'xxxx@163.com'); +# Insert multiple rows +INSERT INTO user +VALUES (10, 'root', 'root', 'xxxx@163.com'), (12, 'user1', 'user1', 'xxxx@163.com'), (18, 'user2', 'user2', 'xxxx@163.com'); +``` + +**Insert part of row** + +```sql +INSERT INTO user(username, password, email) +VALUES ('admin', 'admin', 'xxxx@163.com'); +``` + +**Insert the queried data** + +```sql +INSERT INTO user(username) +SELECT name +FROM account; +``` + +### Update data + +The `UPDATE` statement is used to update records in a table. + +```sql +UPDATE user +SET username='robot', password='robot' +WHERE username = 'root'; +``` + +### Delete data + +- The `DELETE` statement is used to delete records from a table. +- `TRUNCATE TABLE` can clear the table, that is, delete all rows. Explanation: The `TRUNCATE` statement does not belong to DML syntax but DDL syntax. + +**Delete specified data in the table** + +```sql +DELETE FROM user +WHERE username = 'robot'; +``` + +**Clear the data in the table** + +```sql +TRUNCATE TABLE user; +``` + +### Query data + +The `SELECT` statement is used to query data from the database. + +`DISTINCT` is used to return uniquely different values. It operates on all columns, which means that all columns must have the same value to be considered the same. + +`LIMIT` limits the number of rows returned. It can have two parameters. The first parameter is the starting row, starting from 0; the second parameter is the total number of rows returned. + +- `ASC`: ascending order (default) +- `DESC`: descending order + +**Query single column** + +```sql +SELECT prod_name +FROM products; +``` + +**Query multiple columns** + +```sql +SELECT prod_id, prod_name, prod_price +FROM products; +``` + +**Query all columns** + +```sql +SELECT * +FROM products; +``` + +**Query for different values** + +```sql +SELECT DISTINCT +vend_id FROM products; +``` + +**Limit query results** + +```sql +-- Return the first 5 rows +SELECT * FROM mytable LIMIT 5; +SELECT * FROM mytable LIMIT 0, 5; +-- Return to lines 3 ~ 5 +SELECT * FROM mytable LIMIT 2, 3; +``` + +## Sort`order by` is used to sort the result set by one column or multiple columns. By default, records are sorted in ascending order. If you need to sort records in descending order, you can use the `desc` keyword. + +`order by` When sorting multiple columns, the column that is sorted first is placed in front, and the column that is sorted later is placed in the back. Also, different columns can have different sorting rules. + +```sql +SELECT * FROM products +ORDER BY prod_price DESC, prod_name ASC; +``` + +## Grouping + +**`group by`**: + +- The `group by` clause groups records into summary rows. +- `group by` returns one record for each group. +- `group by` usually also involves aggregating `count`, `max`, `sum`, `avg`, etc. +- `group by` can group by one or more columns. +- `group by` After sorting by group field, `order by` can sort by summary field. + +**Group** + +```sql +SELECT cust_name, COUNT(cust_address) AS addr_num +FROM Customers GROUP BY cust_name; +``` + +**Sort after grouping** + +```sql +SELECT cust_name, COUNT(cust_address) AS addr_num +FROM Customers GROUP BY cust_name +ORDER BY cust_name DESC; +``` + +**`having`**: + +- `having` is used to filter the aggregated `group by` results. +- `having` is usually used together with `group by`. +- `where` and `having` can be in the same query. + +**Filter data using WHERE and 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`: Filter the specified rows. Aggregation functions (grouping functions) cannot be added later. `where` before `group by`. +- `having`: Filter grouping, usually used in conjunction with `group by` and cannot be used alone. `having` comes after `group by`. + +## Subquery + +A subquery is a SQL query nested within a larger query, also called an inner query or inner select, and the statement containing the subquery is also called an outer query or outer select. Simply put, a subquery refers to using the result of a `select` query (subquery) as the data source or judgment condition of another SQL statement (main query). + +Subqueries can be embedded in `SELECT`, `INSERT`, `UPDATE` and `DELETE` statements, and can also be used with operators such as `=`, `<`, `>`, `IN`, `BETWEEN`, `EXISTS` and other operators. + +Subqueries are often used after the `WHERE` clause and the `FROM` clause: + +- When used in the `WHERE` clause, depending on different operators, the subquery can return a single row and a single column, multiple rows and a single column, or a single row and multiple columns of data. The subquery is to return a value that can be used as the query condition of the `WHERE` clause. +- When used in the `FROM` clause, multi-row and multi-column data is generally returned, which is equivalent to returning a temporary table, so as to comply with the rule that `FROM` is followed by a table. This approach can implement joint queries on multiple tables. + +> Note: MYSQL database only supports subqueries from version 4.1, and earlier versions do not support it. + +The basic syntax of a subquery for a `WHERE` clause is as follows: + +```sql +select column_name [, column_name ] +from table1 [, table2 ] +where column_name operator + (select column_name [, column_name ] + from table1 [, table2 ] + [where]) +``` + +- Subqueries need to be placed within brackets `( )`. +- `operator` represents the operator used in the where clause. + +The basic syntax of a subquery for a `FROM` clause is as follows: + +```sql +select column_name [, column_name ] +from (select column_name [, column_name ] + from table1 [, table2 ] + [where]) as temp_table_name +where condition +``` + +The result returned by the subquery for `FROM` is equivalent to a temporary table, so you need to use the AS keyword to give the temporary table a name. + +**Subquery of subqueries** + +```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')); +``` + +The inner query is executed first before its parent query so that the results of the inner query can be passed to the outer query. You can refer to the following figure for the execution process: + +![](https://oss.javaguide.cn/p3-juejin/c439da1f5d4e4b00bdfa4316b933d764~tplv-k3u1fbpfcp-zoom-1.png) + +### WHERE + +- The `WHERE` clause is used to filter records, that is, to narrow the scope of accessed data. +- `WHERE` followed by a condition that returns `true` or `false`. +- `WHERE` can be used with `SELECT`, `UPDATE` and `DELETE`. +- Operators that can be used in the `WHERE` clause. + +| Operator | Description | +| ------- | ------------------------------------------------------- | +| = | equals | +| <> | is not equal to. Note: In some versions of SQL, this operator can be written as != | +| > | greater than | +| < | less than | +| >= | Greater than or equal to | +| <= | Less than or equal to | +| BETWEEN | Within a range | +| LIKE | Search for a pattern | +| IN | Specifies multiple possible values for a column | + +**The `WHERE` clause in the `SELECT` statement** + +```ini +SELECT * FROM Customers +WHERE cust_name = 'Kids Place'; +``` + +**The `WHERE` clause in the `UPDATE` statement** + +```ini +UPDATECustomers +SET cust_name = 'Jack Jones' +WHERE cust_name = 'Kids Place'; +``` + +**The `WHERE` clause in the `DELETE` statement** + +```ini +DELETE FROM Customers +WHERE cust_name = 'Kids Place'; +``` + +### IN AND BETWEEN + +- The `IN` operator is used in the `WHERE` clause to select any one of several specified values. +- The `BETWEEN` operator is used in the `WHERE` clause to select values ​​within a certain range. + +**IN Example** + +```sql +SELECT * +FROM products +WHERE vend_id IN ('DLL01', 'BRS01'); +``` + +**BETWEEN EXAMPLE** + +```sql +SELECT * +FROM products +WHERE prod_price BETWEEN 3 AND 5; +``` + +### AND, OR, NOT- `AND`, `OR`, `NOT` are logical processing instructions for filtering conditions. +- `AND` has higher priority than `OR`. To clarify the processing order, you can use `()`. +- The `AND` operator indicates that both left and right conditions must be met. +- The `OR` operator means that any one of the left and right conditions must be met. +- The `NOT` operator is used to negate a condition. + +**AND Example** + +```sql +SELECT prod_id, prod_name, prod_price +FROM products +WHERE vend_id = 'DLL01' AND prod_price <= 4; +``` + +**OR Example** + +```ini +SELECT prod_id, prod_name, prod_price +FROM products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'; +``` + +**NOT Example** + +```sql +SELECT * +FROM products +WHERE prod_price NOT BETWEEN 3 AND 5; +``` + +### LIKE + +- The `LIKE` operator is used in the `WHERE` clause to determine whether a string matches a pattern. +- Use `LIKE` only if the field is a text value. +- `LIKE` supports two wildcard matching options: `%` and `_`. +- Don't abuse wildcards, matching at the beginning will be very slow. +- `%` means any character appearing any number of times. +- `_` means any character appears once. + +**% Example** + +```sql +SELECT prod_id, prod_name, prod_price +FROM products +WHERE prod_name LIKE '%bean bag%'; +``` + +**\_ Example** + +```sql +SELECT prod_id, prod_name, prod_price +FROM products +WHERE prod_name LIKE '__ inch teddy bear'; +``` + +## Connect + +JOIN means "connection". As the name suggests, the SQL JOIN clause is used to join two or more tables for query. + +When connecting tables, you need to select a field in each table and compare the values ​​of these fields. Two records with the same value will be merged into one. **The essence of joining tables is to merge records from different tables to form a new table. Of course, this new table is only temporary, it only exists for the duration of this query**. + +The basic syntax for joining two tables using `JOIN` is as follows: + +```sql +select table1.column1, table2.column2... +from table1 +join table2 +on table1.common_column1 = table2.common_column2; +``` + +`table1.common_column1 = table2.common_column2` is a join condition. Only records that meet this condition will be merged into one row. You can join tables using several operators, such as =, >, <, <>, <=, >=, !=, `between`, `like`, or `not`, but the most common is to use =. + +When there are fields with the same name in two tables, in order to help the database engine distinguish the fields of which table, the table name needs to be added when writing the field names with the same name. Of course, if the written field name is unique in the two tables, you can not use the above format and just write the field name. + +In addition, if the related field names of the two tables are the same, you can also use the `USING` clause instead of `ON`, for example: + +```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; + +# If the associated field names of the two tables are the same, you can also use the USING clause: join....using() +select c.cust_name, o.order_num +from Customers c +inner join Orders o +using(cust_id) +order by c.cust_name; +``` + +**The difference between `ON` and `WHERE`**: + +- When joining tables, SQL will generate a new temporary table based on the join conditions. `ON` is the connection condition, which determines the generation of temporary tables. +- `WHERE` is to filter the data in the temporary table after the temporary table is generated to generate the final result set. At this time, there is no JOIN-ON. + +So in summary: **SQL first generates a temporary table based on ON, and then filters the temporary table based on WHERE**. + +SQL allows some modifying keywords to be added to the left of `JOIN` to form different types of connections, as shown in the following table: + +| Connection type | Description | +|------------------------------------------------ |------------------------------------------------------------------------------------------------ | +| INNER JOIN inner join | (default connection method) Rows will be returned only when there are records that meet the conditions in both tables. | +| LEFT JOIN / LEFT OUTER JOIN Left (outer) join | Returns all rows in the left table, even if there are no rows in the right table that meet the condition. | +| RIGHT JOIN / RIGHT OUTER JOIN Right (outer) join | Returns all rows in the right table, even if there are no rows in the left table that meet the condition. | +| FULL JOIN / FULL OUTER JOIN Full (outer) join | As long as one of the tables has records that meet the conditions, rows will be returned. | +| SELF JOIN | Joins a table to itself as if the table were two tables. To differentiate between two tables, at least one table needs to be renamed in the SQL statement. | +| CROSS JOIN | Cross join returns the Cartesian product of record sets from two or more joined tables. | + +The figure below shows 7 usages related to LEFT JOIN, RIGHT JOIN, INNER JOIN, and OUTER JOIN. + +![](https://oss.javaguide.cn/p3-juejin/701670942f0f45d3a3a2187cd04a12ad~tplv-k3u1fbpfcp-zoom-1.png) + +If you just write `JOIN` without adding any modifiers, the default is `INNER JOIN` + +For `INNER JOIN`, there is also an implicit way of writing, called "**Implicit inner join**", that is, there is no `INNER JOIN` keyword, and the `WHERE` statement is used to implement the inner join function. + +```sql +#Implicit inner join +select c.cust_name, o.order_num +from Customers c, Orders o +where c.cust_id = o.cust_id +order by c.cust_name; + +#Explicit inner join +select c.cust_name, o.order_num +from Customers c inner join Orders o +using(cust_id) +order by c.cust_name; +``` + +## Combination + +The `UNION` operator combines the results of two or more queries and produces a result set containing the extracted rows from the participating queries in `UNION`. + +`UNION` basic rules: + +- The number and order of columns must be the same for all queries. +- The data types of the columns involved in the tables in each query must be the same or compatible. +- Usually the column names returned are taken from the first query. + +By default, the `UNION` operator selects distinct values. If duplicate values ​​are allowed, use `UNION ALL`. + +```sql +SELECT column_name(s) FROM table1 +UNION ALL +SELECT column_name(s) FROM table2; +``` + +The column names in the `UNION` result set are always equal to the column names in the first `SELECT` statement in the `UNION`. + +`JOIN` vs `UNION`: + +- The columns of the joined tables in `JOIN` may be different, but in `UNION` the number and order of columns must be the same for all queries. +- `UNION` puts the rows after the query together (vertically), but `JOIN` puts the columns after the query together (horizontally), i.e. it forms a Cartesian product. + +## Function + +Functions tend to differ from database to database and are therefore not portable. This section mainly uses MySQL functions as an example.### Text processing + +| Function | Description | +| -------------------------- | -------------------------- | +| `LEFT()`, `RIGHT()` | The character on the left or right | +| `LOWER()`, `UPPER()` | Convert to lowercase or uppercase | +| `LTRIM()`, `RTRIM()` | Remove spaces on the left or right side | +| `LENGTH()` | Length in bytes | +| `SOUNDEX()` | Convert to speech value | + +Among them, **`SOUNDEX()`** can convert a string into an alphanumeric pattern that describes its phonetic representation. + +```sql +SELECT * +FROM mytable +WHERE SOUNDEX(col1) = SOUNDEX('apple') +``` + +### Date and time processing + +- Date format: `YYYY-MM-DD` +- Time format: `HH:MM:SS` + +| function | description | +| --------------- | ------------------------------- | +| `AddDate()` | Add a date (day, week, etc.) | +| `AddTime()` | Add a time (hour, minute, etc.) | +| `CurDate()` | Returns the current date | +| `CurTime()` | Returns the current time | +| `Date()` | Returns the date part of a datetime | +| `DateDiff()` | Calculate the difference between two dates | +| `Date_Add()` | Highly flexible date operation function | +| `Date_Format()` | Returns a formatted date or time string | +| `Day()` | Returns the day part of a date | +| `DayOfWeek()` | For a date, return the corresponding day of the week | +| `Hour()` | Returns the hour part of a time | +| `Minute()` | Returns the minute part of a time | +| `Month()` | Returns the month part of a date | +| `Now()` | Returns the current date and time | +| `Second()` | Returns the second part of a time | +| `Time()` | Returns the time part of a datetime | +| `Year()` | Returns the year part of a date | + +### Numerical processing + +| Function | Description | +| ------ | ------ | +| SIN() | sine | +| COS() | cosine | +| TAN() | tangent | +| ABS() | Absolute value | +| SQRT() | Square root | +| MOD() | Remainder | +| EXP() | index | +| PI() | Pi | +| RAND() | Random number | + +### Summary + +| function | description | +| --------- | ---------------- | +| `AVG()` | Returns the average value of a column | +| `COUNT()` | Returns the number of rows in a column | +| `MAX()` | Returns the maximum value of a column | +| `MIN()` | Returns the minimum value of a column | +| `SUM()` | Returns the sum of values in a column | + +`AVG()` ignores NULL rows. + +Use `DISTINCT` to have summary function values ​​summarize different values. + +```sql +SELECT AVG(DISTINCT col1) AS avg_col +FROM mytable +``` + +**Next, let’s introduce the usage of DDL statements. The main function of DDL is to define database objects (such as databases, data tables, views, indexes, etc.)** + +## Data definition + +### Database (DATABASE) + +#### Create database + +```sql +CREATE DATABASE test; +``` + +#### Delete database + +```sql +DROP DATABASE test; +``` + +#### Select database + +```sql +USE test; +``` + +### Data table (TABLE) + +#### Create data table + +**Normal creation** + +```sql +CREATE TABLE user ( + id int(10) unsigned NOT NULL COMMENT 'Id', + username varchar(64) NOT NULL DEFAULT 'default' COMMENT 'username', + password varchar(64) NOT NULL DEFAULT 'default' COMMENT 'password', + email varchar(64) NOT NULL DEFAULT 'default' COMMENT 'email' +) COMMENT='user table'; +``` + +**Create a new table based on an existing table** + +```sql +CREATE TABLE vip_user AS +SELECT * FROM user; +``` + +#### Delete data table + +```sql +DROP TABLE user; +``` + +#### Modify data table + +**Add Column** + +```sql +ALTER TABLE user +ADD age int(3); +``` + +**Delete Column** + +```sql +ALTER TABLE user +DROP COLUMN age; +``` + +**Modify column** + +```sql +ALTER TABLE `user` +MODIFY COLUMN age tinyint; +``` + +**Add primary key** + +```sql +ALTER TABLE user +ADD PRIMARY KEY (id); +``` + +**Delete primary key** + +```sql +ALTER TABLE user +DROP PRIMARY KEY; +``` + +### View (VIEW) + +Definition: + +- A view is a table that visualizes the result set of a SQL statement. +- A view is a virtual table and does not contain data, so it cannot be indexed. Operations on views are the same as operations on ordinary tables. + +Function: + +- Simplify complex SQL operations, such as complex joins; +- Only use part of the data from the actual table; +- Ensure data security by only giving users permission to access views; +- Change data format and presentation. + +![mysql view](https://oss.javaguide.cn/p3-juejin/ec4c975296ea4a7097879dac7c353878~tplv-k3u1fbpfcp-zoom-1.jpeg) + +#### Create view + +```sql +CREATE VIEW top_10_user_view AS +SELECT id, username +FROM user +WHERE id < 10; +``` + +#### Delete view + +```sql +DROP VIEW top_10_user_view; +``` + +### Index (INDEX) + +**Index is a data structure used for quick query and retrieval of data. Its essence can be regarded as a sorted data structure. ** + +The function of the index is equivalent to the table of contents of the book. For example: when we look up a dictionary, if there is no table of contents, then we can only go page by page to find the word we need to look up, which is very slow. If there is a table of contents, we only need to search the position of the word in the table of contents first, and then directly turn to that page. + +**Advantages**: + +- Using indexes can greatly speed up data retrieval (greatly reduce the amount of data retrieved), which is also the main reason for creating indexes. +- By creating a unique index, you can ensure the uniqueness of each row of data in the database table. + +**Disadvantages**: + +- Creating and maintaining indexes takes a lot of time. When adding, deleting, or modifying data in a table, if the data has an index, the index also needs to be dynamically modified, which will reduce SQL execution efficiency. +- Indexes require physical file storage and will also consume a certain amount of space. + +However, **Does using indexes definitely improve query performance?** + +In most cases, index queries are faster than full table scans. But if the amount of data in the database is not large, using indexes may not necessarily bring about a big improvement. + +For a detailed introduction to indexes, please read my article [MySQL Index Detailed Explanation](https://javaguide.cn/database/mysql/mysql-index.html). + +#### Create index + +```sql +CREATE INDEX user_index +ON user(id); +``` + +#### Add index + +```sql +ALTER table user ADD INDEX user_index(id) +``` + +#### Create a unique index + +```sql +CREATE UNIQUE INDEX user_index +ON user(id); +``` + +#### Delete index + +```sql +ALTER TABLE user +DROP INDEX user_index;``` + +### Constraints + +SQL constraints are used to specify rules for data in a table. + +If there is a data behavior that violates the constraint, the behavior will be terminated by the constraint. + +Constraints can be specified when the table is created (via the CREATE TABLE statement), or after the table is created (via the ALTER TABLE statement). + +Constraint type: + +- `NOT NULL` - Indicates that a column cannot store NULL values. +- `UNIQUE` - Guarantees that each row of a column must have a unique value. +- `PRIMARY KEY` - Combination of NOT NULL and UNIQUE. Ensuring that a column (or a combination of two or more columns) is uniquely identified can help make it easier and faster to find a specific record in a table. +- `FOREIGN KEY` - Ensures referential integrity that data in one table matches values ​​in another table. +- `CHECK` - Ensures that the values ​​in the column meet the specified conditions. +- `DEFAULT` - Specifies the default value when no value is assigned to the column. + +Use constraints when creating tables: + +```sql +CREATE TABLE Users ( + Id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Auto-increment Id', + Username VARCHAR(64) NOT NULL UNIQUE DEFAULT 'default' COMMENT 'username', + Password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'password', + Email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Email address', + Enabled TINYINT(4) DEFAULT NULL COMMENT 'Is it valid', + PRIMARY KEY (Id) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='User table'; +``` + +**Next, let’s introduce the usage of TCL statements. The main function of TCL is to manage transactions in the database. ** + +## Transaction processing + +The `SELECT` statement cannot be rolled back, and it is meaningless to roll back the `SELECT` statement; the `CREATE` and `DROP` statements cannot be rolled back. + +**MySQL defaults to implicit commit**. Each time a statement is executed, the statement is regarded as a transaction and then submitted. When the `START TRANSACTION` statement appears, implicit commit will be turned off; when the `COMMIT` or `ROLLBACK` statement is executed, the transaction will be automatically closed and implicit commit will be restored. + +Autocommit can be canceled by `set autocommit=0` and will not be committed until `set autocommit=1`; the `autocommit` flag is for each connection and not for the server. + +Instructions: + +- `START TRANSACTION` - directive is used to mark the starting point of a transaction. +- `SAVEPOINT` - directive is used to create a save point. +- `ROLLBACK TO` - The instruction is used to roll back to the specified retention point; if no retention point is set, it will roll back to the `START TRANSACTION` statement. +- `COMMIT` - Commit the transaction. + +```sql +-- Start transaction +START TRANSACTION; + +-- Insert operation A +INSERT INTO `user` +VALUES (1, 'root1', 'root1', 'xxxx@163.com'); + +-- Create a retention point updateA +SAVEPOINT updateA; + +-- Insert operation B +INSERT INTO `user` +VALUES (2, 'root2', 'root2', 'xxxx@163.com'); + +-- Roll back to retention point updateA +ROLLBACK TO updateA; + +-- Submit the transaction, only operation A takes effect +COMMIT; +``` + +**Next, let’s introduce the usage of DCL statements. The main function of DCL is to control user access rights. ** + +## Permission control + +To grant permissions to a user account, use the `GRANT` command. To revoke a user's permissions, use the `REVOKE` command. Here, MySQL is used as an example to introduce the practical application of permission control. + +`GRANT` grant permission syntax: + +```sql +GRANT privilege,[privilege],.. ON privilege_level +TO user [IDENTIFIED BY password] +[REQUIRE tsl_option] +[WITH [GRANT_OPTION | resource_option]]; +``` + +Just explain it briefly: + +1. Specify one or more permissions after the `GRANT` keyword. If the user is granted multiple permissions, each permission is separated by a comma. +2. `ON privilege_level` determines the level of permission application. MySQL supports global (`*.*`), database (`database.*`), table (`database.table`) and column levels. If you use column permission levels, you must specify one or a comma-separated list of columns after each permission. +3. `user` is the user to whom permissions are to be granted. If the user already exists, the `GRANT` statement will modify its permissions. Otherwise, the `GRANT` statement will create a new user. The optional clause `IDENTIFIED BY` allows you to set a new password for the user. +4. `REQUIRE tsl_option` specifies whether users must connect to the database server through secure connections such as SSL, X059, etc. +5. The optional `WITH GRANT OPTION` clause allows you to grant or remove permissions that you have from other users. In addition, you can use the `WITH` clause to allocate the resources of the MySQL database server, for example, to set the number of connections or statements that a user can use per hour. This is useful in shared environments such as MySQL shared hosting. + +`REVOKE` revoke permission syntax: + +```sql +REVOKE privilege_type [(column_list)] + [, priv_type [(column_list)]]... +ON [object_type] privilege_level +FROM user [, user]... +``` + +Just explain it briefly: + +1. Specify the list of permissions to be revoked from the user after the `REVOKE` keyword. You need to separate permissions with commas. +2. Specify the privilege level at which privileges are revoked in the `ON` clause. +3. Specify the user account whose permissions are to be revoked in the `FROM` clause. + +`GRANT` and `REVOKE` control access at several levels: + +- The entire server, using `GRANT ALL` and `REVOKE ALL`; +- The entire database, use `ON database.*`; +- For a specific table, use `ON database.table`; +- specific columns; +- Specific stored procedures. + +Newly created accounts do not have any permissions. Accounts are defined in the form `username@host`, and `username@%` uses the default hostname. MySQL account information is stored in the mysql database. + +```sql +USE mysql; +SELECT user FROM user; +``` + +The following table illustrates all allowed permissions available for the `GRANT` and `REVOKE` statements: + +| **Privilege** | **Description** | **Level** | | | | | | +| ----------------------- | ----------------------------------------------------------------------------------------------- | -------- | ------ | -------- | -------- | --- | --- | +| **Global** | Database | **Table** | **Column** | **Program** | **Agent** | | | +| ALL [PRIVILEGES] | Grants all privileges to the specified access level except GRANT OPTION | | | | | | | +| ALTER | Allows users to use the ALTER TABLE statement || ALTER ROUTINE | Allows the user to change or delete a stored routine | +| CREATE | Allows users to create databases and tables | X | +| CREATE ROUTINE | Allows the user to create a stored routine | +| CREATE TABLESPACE | Allows users to create, alter, or delete tablespaces and log file groups | X | | | | | | +| CREATE TEMPORARY TABLES | Allows users to create temporary tables using CREATE TEMPORARY TABLE | X | +| CREATE USER | Allows users to use the CREATE USER, DROP USER, RENAME USER and REVOKE ALL PRIVILEGES statements. | X | | | | | | +| CREATE VIEW | Allows users to create or modify views. | +| DELETE | Allow users to use DELETE | +| DROP | Allows users to drop databases, tables and views | +| EVENT | Enables event usage by the event scheduler. | +| EXECUTE | Allows the user to execute a stored routine | +| FILE | Allows users to read any file in the database directory. | X | | | | | | +| GRANT OPTION | Allows a user to have the ability to grant or revoke permissions from other accounts. | +| INDEX | Allows users to create or delete indexes. | +| INSERT | Allows users to use the INSERT statement | +| LOCK TABLES | Allows users to use LOCK TABLES on tables with SELECT permission | +| PROCESS | Allows users to view all processes using the SHOW PROCESSLIST statement. | X | | | | | | +| PROXY | Enable user agent. | | | | | | | +| REFERENCES | Allows users to create foreign keys | +| RELOAD | Allows users to use FLUSH operations | X | | | | | | +| REPLICATION CLIENT | Allows users to query to see the location of a master or slave server | X | | | | | | +| REPLICATION SLAVE | Allows users to read binary log events from the master server using a replication slave. | X | | | | | || SELECT | Allows users to use the SELECT statement | +| SHOW DATABASES | Allows the user to show all databases | +| SHOW VIEW | Allows users to use the SHOW CREATE VIEW statement | +| SHUTDOWN | Allows users to use the mysqladmin shutdown command | +| SUPER | Allows users to use other administrative operations such as CHANGE MASTER TO, KILL, PURGE BINARY LOGS, SET GLOBAL and mysqladmin commands | +| TRIGGER | Allows the user to use the TRIGGER operation. | +| UPDATE | Allows users to use the UPDATE statement | +| USAGE | Equivalent to "no privileges" | | | | | | | + +### Create account + +```sql +CREATE USER myuser IDENTIFIED BY 'mypassword'; +``` + +### Modify account name + +```sql +UPDATE user SET user='newuser' WHERE user='myuser'; +FLUSH PRIVILEGES; +``` + +### Delete account + +```sql +DROP USER myuser; +``` + +### View permissions + +```sql +SHOW GRANTS FOR myuser; +``` + +### Grant permissions + +```sql +GRANT SELECT, INSERT ON *.* TO myuser; +``` + +### Delete permissions + +```sql +REVOKE SELECT, INSERT ON *.* FROM myuser; +``` + +### Change password + +```sql +SET PASSWORD FOR myuser = 'mypass'; +``` + +## Stored procedure + +Stored procedures can be thought of as batch processing of a series of SQL operations. Stored procedures can be called by triggers, other stored procedures, and applications such as Java, Python, PHP, etc. + +![mysql stored procedure](https://oss.javaguide.cn/p3-juejin/60afdc9c9a594f079727ec64a2e698a3~tplv-k3u1fbpfcp-zoom-1.jpeg) + +Benefits of using stored procedures: + +- Code encapsulation ensures a certain level of security; +- Code reuse; +- High performance since it is pre-compiled. + +Create stored procedure: + +- Creating a stored procedure in the command line requires custom delimiters, because the command line ends with `;`, and the stored procedure also contains a semicolon, so this part of the semicolon will be mistakenly regarded as the terminator, causing a syntax error. +- Contains three parameters: `in`, `out` and `inout`. +- To assign a value to a variable, you need to use the `select into` statement. +- Only one variable can be assigned a value at a time, and collection operations are not supported. + +It should be noted that: **Alibaba's "Java Development Manual" forcibly prohibits the use of stored procedures. Because stored procedures are difficult to debug and extend, they are not portable. ** + +![](https://oss.javaguide.cn/p3-juejin/93a5e011ade4450ebfa5d82057532a49~tplv-k3u1fbpfcp-zoom-1.png) + +As for whether to use it in the project, it still depends on the actual needs of the project, and just weigh the pros and cons! + +### Create stored procedure + +```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; +``` + +### Use stored procedures + +```less +set @b=5; +call proc_adder(2,@b,@s); +select @s as sum; +``` + +## Cursor + +A cursor is a database query stored on the DBMS server. It is not a `SELECT` statement, but the result set retrieved by the statement. + +Cursors can be used in stored procedures to traverse a result set. + +Cursors are primarily used in interactive applications where the user needs to scroll through data on the screen and browse or make changes to the data. + +A few clear steps for using cursors: + +- Before using a cursor, you must declare (define) it. This procedure does not actually retrieve data, it simply defines the `SELECT` statement and cursor options to be used. + +- Once declared, the cursor must be opened for use. This process uses the SELECT statement defined earlier to actually retrieve the data. + +- For cursors filled with data, fetch (retrieve) rows as needed. + +- When ending use of a cursor, the cursor must be closed and, if possible, released (depending on the tool). + + DBMS). + +```sql +DELIMITER $ +CREATE PROCEDURE getTotal() +BEGIN + DECLARE total INT; + --Create a variable to receive cursor data + DECLARE sid INT; + DECLARE sname VARCHAR(10); + --Create a total variable + DECLARE sage INT; + --Create an end flag variable + DECLARE done INT DEFAULT false; + --Create cursor + DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30; + --Specify the return value at the end of the cursor loop + 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 stored procedure +call getTotal(); +``` + +## Trigger + +A trigger is a database object related to table operations. When a specified event occurs on the table where the trigger is located, the object will be called, that is, the operation event of the table triggers the execution of the trigger on the table.We can use triggers for audit tracking and record changes to another table. + +Advantages of using triggers: + +- SQL triggers provide another way to check data integrity. +- SQL triggers can capture errors in business logic in the database layer. +- SQL triggers provide another way to run scheduled tasks. By using SQL triggers, you don't have to wait for a scheduled task to run because the trigger is automatically called before or after changes are made to the data in the table. +- SQL triggers are useful for auditing changes to data in tables. + +Disadvantages of using triggers: + +- SQL triggers can only provide extended validation and cannot replace all validation. Some simple validation must be done in the application layer. For example, you can validate user input on the client side using JavaScript, or on the server side using a server-side scripting language (such as JSP, PHP, ASP.NET, Perl). +- SQL triggers are not visible when called and executed from the client application, so it is difficult to figure out what is going on in the database layer. +- SQL triggers may add overhead to the database server. + +MySQL does not allow the use of CALL statements in triggers, that is, stored procedures cannot be called. + +> Note: In MySQL, the semicolon `;` is the identifier of the end of a statement. Encountering a semicolon indicates that the statement has ended and MySQL can start execution. Therefore, the interpreter starts execution after encountering the semicolon in the trigger execution action, and then reports an error because no END matching BEGIN is found. +> +> The `DELIMITER` command will be used at this time (DELIMITER is the delimiter, which means the separator). It is a command and does not require an end-of-statement identifier. The syntax is: `DELIMITER new_delimiter`. `new_delimiter` can be set to 1 or more length symbols, the default is semicolon `;`, we can modify it to other symbols, such as `$` - `DELIMITER $`. The statement after this ends with a semicolon, and the interpreter will not react. Only when `$` is encountered, the statement is considered to have ended. Note that after using it, we should remember to modify it back. + +Prior to MySQL version 5.7.2, up to six triggers could be defined per table. + +- `BEFORE INSERT` - Activate before inserting data into the table. +- `AFTER INSERT` - activated after inserting data into the table. +- `BEFORE UPDATE` - Activate before updating data in the table. +- `AFTER UPDATE` - activated after updating data in the table. +- `BEFORE DELETE` - Activate before deleting data from the table. +- `AFTER DELETE` - activated after deleting data from the table. + +However, starting with MySQL version 5.7.2+, multiple triggers can be defined for the same trigger event and action time. + +**`NEW` and `OLD`**: + +- The `NEW` and `OLD` keywords are defined in MySQL, which are used to indicate the row of data in the table where the trigger is located that triggered the trigger. +- In `INSERT` type triggers, `NEW` is used to indicate new data that will be (`BEFORE`) or has been (`AFTER`) inserted; +- In `UPDATE` type triggers, `OLD` is used to represent the original data that will be or has been modified, and `NEW` is used to represent the new data that will be or has been modified; +- In `DELETE` type triggers, `OLD` is used to represent the original data that will be or has been deleted; +- Usage: `NEW.columnName` (columnName is a column name of the corresponding data table) + +### Create trigger + +> Tip: In order to understand the gist of triggers, it is necessary to first understand the instructions for creating triggers. + +The `CREATE TRIGGER` command is used to create a trigger. + +Syntax: + +```sql +CREATE TRIGGER trigger_name +trigger_time +trigger_event +ON table_name +FOR EACH ROW +BEGIN + trigger_statements +END; +``` + +Description: + +- `trigger_name`: trigger name +- `trigger_time`: The trigger firing time. The value is `BEFORE` or `AFTER`. +- `trigger_event`: The listening event of the trigger. The value is `INSERT`, `UPDATE` or `DELETE`. +- `table_name`: The listening target of the trigger. Specify the table on which to create the trigger. +- `FOR EACH ROW`: Row-level monitoring, Mysql fixed writing method, different from other DBMS. +- `trigger_statements`: Trigger execution actions. Is a list of one or more SQL statements. Each statement in the list must be terminated with a semicolon `;`. + +When the triggering condition of the trigger is met, the trigger execution action between `BEGIN` and `END` will be executed. + +Example: + +```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; +``` + +### View triggers + +```sql +SHOW TRIGGERS; +``` + +### Delete trigger + +```sql +DROP TRIGGER IF EXISTS trigger_insert_user; +``` + +## Article recommendation + +- [A must-have for back-end programmers: SQL high-performance optimization guide! 35+ optimization suggestions GET now!](https://mp.weixin.qq.com/s/I-ZT3zGTNBZ6egS7T09jyQ) +- [Must-have for back-end programmers: 30 tips for writing high-quality SQL suggestions](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486461&idx=1&sn=60a22279196d084cc398936fe3b37772&c hksm=cea24436f9d5cd20a4fa0e907590f3e700d7378b3f608d7b33bb52cfb96f503b7ccb65a1deed&token=1987003517&lang=zh_CN#rd) + + \ No newline at end of file diff --git a/docs_en/distributed-system/api-gateway.en.md b/docs_en/distributed-system/api-gateway.en.md new file mode 100644 index 00000000000..413afc10d82 --- /dev/null +++ b/docs_en/distributed-system/api-gateway.en.md @@ -0,0 +1,209 @@ +--- +title: API网关基础知识总结 +category: 分布式 +--- + +## 什么是网关? + +微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 + +![网关示意图](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway-overview.png) + +一般情况下,网关可以为我们提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、降级熔断、日志、监控、参数校验、协议转换等功能。 + +上面介绍了这么多功能,实际上,网关主要做了两件事情:**请求转发** + **请求过滤**。 + +由于引入网关之后,会多一步网络转发,因此性能会有一点影响(几乎可以忽略不计,尤其是内网访问的情况下)。 另外,我们需要保障网关服务的高可用,避免单点风险。 + +如下图所示,网关服务外层通过 Nginx(其他负载均衡设备/软件也行) 进⾏负载转发以达到⾼可⽤。Nginx 在部署的时候,尽量也要考虑高可用,避免单点风险。 + +![基于 Nginx 的服务端负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) + +## 网关能提供哪些功能? + +绝大部分网关可以提供下面这些功能(有一些功能需要借助其他框架或者中间件): + +- **请求转发**:将请求转发到目标微服务。 +- **负载均衡**:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。 +- **安全认证**:对用户请求进行身份验证并仅允许可信客户端访问 API,并且还能够使用类似 RBAC 等方式来授权。 +- **参数校验**:支持参数映射与校验逻辑。 +- **日志记录**:记录所有请求的行为日志供后续使用。 +- **监控告警**:从业务指标、机器指标、JVM 指标等方面进行监控并提供配套的告警机制。 +- **流量控制**:对请求的流量进行控制,也就是限制某一时刻内的请求数。 +- **熔断降级**:实时监控请求的统计信息,达到配置的失败阈值后,自动熔断,返回默认值。 +- **响应缓存**:当用户请求获取的是一些静态的或更新不频繁的数据时,一段时间内多次请求获取到的数据很可能是一样的。对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。 +- **响应聚合**:某些情况下用户请求要获取的响应内容可能会来自于多个业务服务。网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。 +- **灰度发布**:将请求动态分流到不同的服务版本(最基本的一种灰度发布)。 +- **异常处理**:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。 +- **API 文档:** 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。 +- **协议转换**:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。 +- **证书管理**:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。 + +下图来源于[百亿规模 API 网关服务 Shepherd 的设计与实现 - 美团技术团队 - 2021](https://mp.weixin.qq.com/s/iITqdIiHi3XGKq6u6FRVdg)这篇文章。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-35e102c633bbe8e0dea1e075ea3fee5dcfb.png) + +## 有哪些常见的网关系统? + +### Netflix Zuul + +Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务,基于 Java 技术栈开发,可以和 Eureka、Ribbon、Hystrix 等组件配合使用。 + +Zuul 核心架构如下: + +![Zuul 核心架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul-core-architecture.webp) + +Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。 + +![Zuul 请求声明周期](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 来做限流): + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + com.marcosbarbero.cloud + spring-cloud-zuul-ratelimit + 2.2.0.RELEASE + +``` + +[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,性能得到了大幅改进。 + +![Zuul2 架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul2-core-architecture.png) + +- GitHub 地址: +- 官方 Wiki: + +### Spring Cloud Gateway + +SpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul**。准确点来说,应该是 Zuul 1.x。SpringCloud Gateway 起步要比 Zuul 2.x 更早。 + +为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 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 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。 + +- Github 地址: +- 官网: + +### OpenResty + +根据官方介绍: + +> OpenResty is a high-performance web platform based on Nginx and Lua. It integrates a large number of sophisticated Lua libraries, third-party modules and most dependencies. Used to easily build dynamic web applications, web services and dynamic gateways that can handle ultra-high concurrency and high scalability. + +![The relationship between OpenResty, Nginx and Lua](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gatewaynginx-lua-openresty.png) + +OpenResty is based on Nginx, mainly because of its excellent high concurrency capabilities. However, because Nginx is developed in C language, the threshold for secondary development is relatively high. If you want to implement some custom logic or functions on Nginx, you need to write a C language module and recompile Nginx. + +In order to solve this problem, OpenResty perfectly integrates Lua/LuaJIT into Nginx by implementing Nginx modules such as `ngx_lua` and `stream_lua`, which allows us to embed Lua scripts inside Nginx, so that the functions of the gateway can be extended through simple Lua language, such as implementing custom routing rules, filters, caching strategies, etc. + +> Lua is a very fast dynamic scripting language that runs as fast as C. LuaJIT is a just-in-time compiler for Lua that can significantly improve the execution efficiency of Lua code. LuaJIT precompiles and caches some commonly used Lua functions and tool libraries, so that the cached bytecode can be used directly the next time it is called, thus greatly speeding up execution. + +It is recommended to read this article for getting started with OpenResty and practical gateway security: [Getting started with OpenResty and practical gateway security that every backend should know] (https://mp.weixin.qq.com/s/3HglZs06W95vF3tSa3KrXw). + +- Github address: +- Official website address: + +### Kong + +Kong is a high-performance, cloud-native, scalable, and ecologically rich gateway system based on [OpenResty](https://github.com/openresty/) (Nginx + Lua). It mainly consists of 3 components: + +- Kong Server: Nginx-based server used to receive API requests. +- Apache Cassandra/PostgreSQL: used to store operational data. +- Kong Dashboard: Officially recommended UI management tool. Of course, you can also use the RESTful method to manage the Admin api. + +![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-way.webp) + +Since Apache Cassandra/PostgreSQL is used to store data by default, Kong's entire architecture is bloated and will cause high availability problems. + +Kong provides a plug-in mechanism to extend its functionality. Plug-ins are executed during the life cycle of the API request response cycle. For example, enable the Zipkin plug-in on the service: + +```shell +$ curl -X POST http://kong:8001/services/{service}/plugins \ + --data "name=zipkin" \ + --data "config.http_endpoint=http://your.zipkin.collector:9411/api/v2/spans" \ + --data "config.sample_ratio=0.001" +``` + +Kong itself is a Lua application, and it is an encapsulated application based on Openresty. The final analysis is to use Lua to embed Nginx to give Nginx programmable capabilities, so that unlimited imagination can be done at the Nginx level in the form of plug-ins. For example, current limiting, security access policy, routing, load balancing, etc. To write a Kong plug-in, you need to follow the Kong plug-in writing specifications, write your own customized Lua script, then load it into Kong, and finally quote it. + +![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-gateway-overview.png) + +In addition to Lua, Kong can also develop plug-ins based on Go, JavaScript, Python and other languages, thanks to the corresponding PDK (plug-in development kit). + +For a detailed introduction to the Kong plug-in, it is recommended to read the official document: , which is written in more detail. + +- Github address: +- Official website address: + +### APISIX + +APISIX is a high-performance, cloud-native, scalable gateway system based on OpenResty and etcd. + +> etcd is an open source, highly available distributed key-value storage system developed using the Go language, and uses the Raft protocol for distributed consensus. + +Compared with traditional API gateways, APISIX has dynamic routing and plug-in hot loading, which is particularly suitable for API management under microservice systems. Moreover, APISIX is very convenient to connect with DevOps ecological tools such as SkyWalking (distributed link tracking system), Zipkin (distributed link tracking system), and Prometheus (monitoring system). + +![APISIX Architecture Diagram](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/apisix-architecture.png) + +As an alternative project to Nginx and Kong, APISIX is currently Apache's top open source project and the fastest graduating domestic open source project. There are currently many well-known domestic companies (such as Kingsoft, Youzan, iQiyi, Tencent, and Shell) using APISIX to handle core business traffic. + +According to the official website: "APISIX is already in production and available, and its functions, performance, and architecture are all superior to Kong." + +APISIX also supports customized plug-in development. In addition to using the Lua language to develop plug-ins, developers can also develop in the following two ways to avoid the learning cost of the Lua language: + +- Support more mainstream programming languages (such as Java, Python, Go, etc.) through Plugin Runner. In this way, back-end engineers can communicate through local RPC and develop APISIX plug-ins using familiar programming languages. The advantage of this is that it reduces development costs and improves development efficiency, but there will be some loss in performance. +- Use Wasm (WebAssembly) to develop plug-ins. Wasm is embedded into APISIX, and users can use Wasm to compile into Wasm bytecode and run it in APISIX. + +> Wasm is a binary instruction format for stack-based virtual machines, a low-level assembly language designed to be very close to compiled machine code, and very close to native performance. Wasm was originally built for the browser, but as the technology matures, it's seeing more and more use cases on the server side. + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-a240d3b113cde647f5850f4c7cc55d4ff5c.png) + +- Github address: +- Official website address: + +Related reading: + +- [Why is Apache APISIX the best API gateway? ](https://mp.weixin.qq.com/s/j8ggPGEHFu3x5ekJZyeZnA) +- [With NGINX and Kong, why do we need Apache APISIX](https://www.apiseven.com/zh/blog/why-we-need-Apache-APISIX) +- [APISIX Technology Blog](https://www.apiseven.com/zh/blog) +- [APISIX User Cases](https://www.apiseven.com/zh/usercases) (recommended) + +### Shenyu + +Shenyu is a scalable, high-performance, responsive gateway based on WebFlux, a top Apache open source project. + +![Shenyu Architecture](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/shenyu-architecture.png) + +Shenyu extends its functionality through plug-ins, which are the soul of ShenYu and are also expandable and hot-swappable. Different plug-ins implement different functions. Shenyu comes with plug-ins such as current limiting, circuit breaker, forwarding, rewriting, redirection, and route monitoring.- Github address: +- Official website address: + +## How to choose? + +Among the several common gateway systems introduced above, the three most commonly used ones are Spring Cloud Gateway, Kong, and APISIX. + +For companies whose business uses Java as the main development language, Spring Cloud Gateway is usually a good choice. Its advantages include: simple and easy to use, mature and stable, compatible with the Spring Cloud ecosystem, mature Spring community, etc. However, Spring Cloud Gateway also has some limitations and shortcomings, and generally needs to be used in conjunction with other gateways such as OpenResty. Moreover, its performance is still worse than Kong and APISIX. If you have high performance requirements, Spring Cloud Gateway is not a good choice. + +Kong and APISIX have richer functions, more powerful performance, and their technical architecture is more cloud-native. Kong is the originator of open source API gateways, with a rich ecosystem and a large user base. APISIX is a latecomer and is better. According to the APISIX official website: "APISIX is already in production and available, and its functions, performance, and architecture are all superior to Kong." Let’s briefly compare the two: + +- APISIX is based on etcd as the configuration center, so there is no single point of problem and is cloud-native friendly; while Kong is based on Apache Cassandra/PostgreSQL, which has a single point of risk and requires additional infrastructure to ensure high availability. +- APISIX supports hot updates and implements millisecond-level hot update response; while Kong does not support hot updates. +- APISIX performs better than Kong. +- APISIX supports more plug-ins and has richer functions. + +## Reference + +- Kong plug-in development tutorial [easy to understand]: +- API gateway Kong in action: +- Spring Cloud Gateway principle introduction and application: +- Why do microservices use API gateways? : + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-configuration-center.en.md b/docs_en/distributed-system/distributed-configuration-center.en.md new file mode 100644 index 00000000000..8909fc790cc --- /dev/null +++ b/docs_en/distributed-system/distributed-configuration-center.en.md @@ -0,0 +1,12 @@ +--- +title: Summary of Frequently Asked Questions in Distributed Configuration Center (Paid) +category: distributed +--- + +**Distributed Configuration Center** The relevant interview questions are my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and how to join) exclusive content, which has been compiled into the "Java Interview Guide". + +![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) + + + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-id-design.en.md b/docs_en/distributed-system/distributed-id-design.en.md new file mode 100644 index 00000000000..4a28ef3793c --- /dev/null +++ b/docs_en/distributed-system/distributed-id-design.en.md @@ -0,0 +1,173 @@ +--- +title: 分布式ID设计指南 +category: 分布式 +--- + +::: tip + +看到百度 Geek 说的一篇结合具体场景聊分布式 ID 设计的文章,感觉挺不错的。于是,我将这篇文章的部分内容整理到了这里。原文传送门:[分布式 ID 生成服务的技术原理和项目实战](https://mp.weixin.qq.com/s/bFDLb6U6EgI-DvCdLTq_QA) 。 + +::: + +网上绝大多数的分布式 ID 生成服务,一般着重于技术原理剖析,很少见到根据具体的业务场景去选型 ID 生成服务的文章。 + +本文结合一些使用场景,进一步探讨业务场景中对 ID 有哪些具体的要求。 + +## 场景一:订单系统 + +我们在商场买东西一码付二维码,下单生成的订单号,使用到的优惠券码,联合商品兑换券码,这些是在网上购物经常使用到的单号,那么为什么有些单号那么长,有些只有几位数?有些单号一看就知道年月日的信息,有些却看不出任何意义?下面展开分析下订单系统中不同场景的 id 服务的具体实现。 + +### 1、一码付 + +我们常见的一码付,指的是一个二维码可以使用支付宝或者微信进行扫码支付。 + +二维码的本质是一个字符串。聚合码的本质就是一个链接地址。用户使用支付宝微信直接扫一个码付钱,不用担心拿支付宝扫了微信的收款码或者用微信扫了支付宝的收款码,这极大减少了用户扫码支付的时间。 + +实现原理是当客户用 APP 扫码后,网站后台就会判断客户的扫码环境。(微信、支付宝、QQ 钱包、京东支付、云闪付等)。 + +判断扫码环境的原理就是根据打开链接浏览器的 HTTP header。任何浏览器打开 http 链接时,请求的 header 都会有 User-Agent(UA、用户代理)信息。 + +UA 是一个特殊字符串头,服务器依次可以识别出客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等很多信息。 + +各渠道对应支付产品的名称不一样,一定要仔细看各支付产品的 API 介绍。 + +1. 微信支付:JSAPI 支付支付 +2. 支付宝:手机网站支付 +3. QQ 钱包:公众号支付 + +其本质均为在 APP 内置浏览器中实现 HTML5 支付。 + +![文库会员支付示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-pay-one-card.png) + +文库的研发同学在这个思路上,做了优化迭代。动态生成一码付的二维码预先绑定用户所选的商品信息和价格,根据用户所选的商品动态更新。这样不仅支持一码多平台调起支付,而且不用用户选择商品输入金额,即可完成订单支付的功能,很丝滑。用户在真正扫码后,服务端才通过前端获取用户 UID,结合二维码绑定的商品信息,真正的生成订单,发送支付信息到第三方(qq、微信、支付宝),第三方生成支付订单推给用户设备,从而调起支付。 + +区别于固定的一码付,在文库的应用中,使用到了动态二维码,二维码本质是一个短网址,ID 服务提供短网址的唯一标志参数。唯一的短网址映射的 ID 绑定了商品的订单信息,技术和业务的深度结合,缩短了支付流程,提升用户的支付体验。 + +### 2、订单号 + +订单号在实际的业务过程中作为一个订单的唯一标识码存在,一般实现以下业务场景: + +1. 用户订单遇到问题,需要找客服进行协助; +2. 对订单进行操作,如线下收款,订单核销; +3. 下单,改单,成单,退单,售后等系统内部的订单流程处理和跟进。 + +很多时候搜索订单相关信息的时候都是以订单 ID 作为唯一标识符,这是由于订单号的生成规则的唯一性决定的。从技术角度看,除了 ID 服务必要的特性之外,在订单号的设计上需要体现几个特性: + +**(1)信息安全** + +编号不能透露公司的运营情况,比如日销、公司流水号等信息,以及商业信息和用户手机号,身份证等隐私信息。并且不能有明显的整体规律(可以有局部规律),任意修改一个字符就能查询到另一个订单信息,这也是不允许的。 + +类比于我们高考时候的考生编号的生成规则,一定不能是连号的,否则只需要根据顺序往下查询就能搜索到别的考生的成绩,这是绝对不可允许。 + +**(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 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求,无论请求经过的哪个服务出了故障或者处理过慢都会对前端造成影响。 + +To process multiple services called by a Web request, in order to more conveniently query which link of the service has a problem, a common solution now is to introduce distributed link tracking into the entire system. + +![In distributed link tracing](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-tracing.png) + +There are two important concepts in distributed link tracing: trace and span. Trace is the view of the entire link in the distributed system requested, and span represents the internal view of different services in the entire link. The span combined together is the view of the entire trace. + +In the entire request call chain, the request will always carry the traceid and be passed to the downstream service. Each service will also generate its own spanid internally to generate its own internal call view, and pass it to the downstream service together with the traceid. + +### 2. TraceId generation rules + +In this scenario, the generated ID must not only be unique, but also must be generated with high efficiency and high throughput. The traceid needs to have the ability to be generated independently by the server instance of the access layer. If the ID in each trace needs to be generated by requesting a public ID service, it will be a pure waste of network bandwidth resources. And it will block the transmission of user requests to downstream, increase the response time, and increase unnecessary risks. Therefore, if a server instance is required, it is best to calculate tracid and spanid by itself to avoid relying on external services. + +Generation rules: server IP + ID generation time + auto-increment sequence + current process number, for example: + +0ad1348f1403169275002100356696 + +The first 8 digits 0ad1348f are the IP of the machine that generated the TraceId. This is a hexadecimal number. Each two digits represents a segment of the IP. We convert this number into decimal for each digit to get the common IP address representation 10.209.52.143. You can also use this rule to find the first server that the request passes through. + +The following 13 bits 1403169275002 are the time when the TraceId was generated. The next four digits 1003 are a self-increasing sequence, rising from 1000 to 9000. After reaching 9000, it returns to 1000 and starts to rise again. The last 5 digits 56696 are the current process ID. In order to prevent TraceId conflicts in single-machine multiple processes, the current process ID is added at the end of TraceId. + +### 3. SpanId generation rules + +span means layer. For example, the first instance is considered the first layer, and the request is proxied or offloaded to the next instance for processing, which is the second layer, and so on. By layer, SpanId represents the position of this call in the entire call link tree. + +Assume that a server instance A receives a user request, which represents the root node of the entire call. Then the spanid value of the non-service call log record generated by layer A when processing this request is all 0. Layer A needs to call three server instances B, C, and D in sequence through RPC. Then in the log of A, the SpanId is 0.1, 0.2, and 0.3 respectively. In B, C, and D, the SpanId is also respectively. 0.1, 0.2 and 0.3; if system C calls two server instances of E and F when processing the request, then the corresponding spanids in system C are 0.2.1 and 0.2.2, and the corresponding logs of systems E and F are also 0.2.1 and 0.2.2. + +According to the above description, we can know that if all the SpanIds in a call are collected, a complete link tree can be formed. + +The essence of **spanid generation: it is achieved by controlling the automatic increment of the size and version numbers while transmitting transparently across layers. ** + +## Scenario 3: Short URL + +The main functions of short URL include URL shortening and restoration. Compared with long URLs, short URLs can be spread more easily on emails, social networks, Weibo, and mobile phones. For example, a URL that was originally very long can be generated into a corresponding short URL through a URL shortening service to avoid line breaks or exceeding character limits. + +![Short URL function](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-design-short-url.png) + +Commonly used ID generation services such as: MySQL ID auto-increment, Redis key auto-increment, number segment mode, the generated IDs are all a string of numbers. The URL shortening service converts the customer's long URL into a short URL. + +In fact, the newly generated numeric ID is spliced after the dwz.cn domain name, and the numeric ID is used directly. The length of the URL is also a bit long. The service can compress the length by converting the numeric ID to a higher base. This algorithm is increasingly used in the technical implementation of short URLs, and it can further compress the URL length. The hexadecimal compression algorithm has a wide range of application scenarios in life, for example: + +- Customer's long URL: +- ID mapped short URL: (for demonstration use, may not open correctly) +- Shortened URL after conversion: (for demonstration use, it may not open correctly) + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-id.en.md b/docs_en/distributed-system/distributed-id.en.md new file mode 100644 index 00000000000..25a6cefd964 --- /dev/null +++ b/docs_en/distributed-system/distributed-id.en.md @@ -0,0 +1,395 @@ +--- +title: 分布式ID介绍&实现方案总结 +category: 分布式 +--- + +## 分布式 ID 介绍 + +### 什么是 ID? + +日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 + +我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应一个地址。 + +简单来说,**ID 就是数据的唯一标识**。 + +### 什么是分布式 ID? + +分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。 + +我简单举一个分库分表的例子。 + +我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。 + +在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。**我们如何为不同的数据节点生成全局唯一主键呢?** + +这个时候就需要生成**分布式 ID**了。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/id-after-the-sub-table-not-conflict.png) + +### 分布式 ID 需要满足哪些要求? + +![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/distributed-id-requirements.png) + +分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。 + +一个最基本的分布式 ID 需要满足下面这些要求: + +- **全局唯一**:ID 的全局唯一性肯定是首先要满足的! +- **高性能**:分布式 ID 的生成速度要快,对本地资源消耗要小。 +- **高可用**:生成分布式 ID 的服务要保证可用性无限接近于 100%。 +- **方便易用**:拿来即用,使用方便,快速接入! + +除了这些之外,一个比较好的分布式 ID 还应保证: + +- **安全**:ID 中不包含敏感信息。 +- **有序递增**:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。 +- **有具体的业务含义**:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。 +- **独立部署**:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。 + +## 分布式 ID 常见解决方案 + +### 数据库 + +#### 数据库主键自增 + +这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。 + +![数据库主键自增](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/the-primary-key-of-the-database-increases-automatically.png) + +以 MySQL 举例,我们通过下面的方式即可。 + +**1.创建一个数据库表。** + +```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; +``` + +`stub` 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 `stub` 字段创建了唯一索引,保证其唯一性。 + +**2.通过 `replace into` 来插入数据。** + +```java +BEGIN; +REPLACE INTO sequence_id (stub) VALUES ('stub'); +SELECT LAST_INSERT_ID(); +COMMIT; +``` + +插入数据这里,我们没有使用 `insert into` 而是使用 `replace into` 来插入数据,具体步骤是这样的: + +- 第一步:尝试把数据插入到表中。 + +- 第二步:如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。 + +这种方式的优缺点也比较明显: + +- **优点**:实现起来比较简单、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 更短; +- Compatible with all snowflake algorithms (number segment mode or classic mode, big factory or small factory); +- Natively supports C#/Java/Go/C/Rust/Python/Node.js/PHP (C extension)/SQL/ and other languages, and provides multi-threaded safe dynamic library calling (FFI); +- Solve the time callback problem and support manual insertion of new IDs (when the business needs to generate new IDs in historical time, the reserved bits of this algorithm can generate 5000 per second); +- Does not rely on external storage systems; +- In the default configuration, IDs are available for 71000 years without duplication. + +The unique ID generated by the IdGenerator consists of: + +![Composition of ID generated by IdGenerator](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/idgenerator-distributed-id-schematic-diagram.png) + +- **timestamp (number of digits is not fixed)**: The time difference is the total time difference (in milliseconds) from the system time when the ID is generated minus BaseTime (base time, also called base time, origin time, epoch time, the default value is 2020). Initial is 5bits and increases with running time. If you feel that the default value is too old, you can reset it, but be aware that it is best not to change this value in the future. +- **worker id (default 6 bits)**: machine id, machine code, the most important parameter, is the unique ID that distinguishes different machines or different applications. The maximum value is limited by `WorkerIdBitLength` (default 6). If a server deploys multiple independent services, you need to specify a different WorkerId for each service. +- **sequence (default 6 bits)**: The number of sequences, which is the number of sequences per millisecond, is limited by the `SeqBitLength` (default 6) in the parameter. Increasing `SeqBitLength` will result in better performance, but the generated IDs will also be longer. + +Java language usage example: . + +## Summary + +Through this article, I have basically summarized the most common distributed ID generation solutions. + +In addition to the methods introduced above, middleware like ZooKeeper can also help us generate unique IDs. **There is no silver bullet. You must choose the solution that best suits you based on the actual project. ** + +However, this article mainly introduces the theoretical knowledge of distributed ID. In an actual interview, the interviewer may examine your design of distributed ID based on specific business scenarios. You can refer to this article: [Distributed ID Design Guide](./distributed-id-design) (it is also very helpful for the design of distributed ID in actual work). + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-lock-implementations.en.md b/docs_en/distributed-system/distributed-lock-implementations.en.md new file mode 100644 index 00000000000..bea69b2329a --- /dev/null +++ b/docs_en/distributed-system/distributed-lock-implementations.en.md @@ -0,0 +1,372 @@ +--- +title: Summary of common implementation solutions for distributed locks +category: distributed +--- + + + +Under normal circumstances, we generally choose to implement distributed locks based on Redis or ZooKeeper. Redis is used more often. I will first introduce the implementation of distributed locks using Redis as an example. + +## Implement distributed lock based on Redis + +### How to implement the simplest distributed lock based on Redis? + +Whether it is a local lock or a distributed lock, the core lies in "mutual exclusion". + +In Redis, the `SETNX` command can help us achieve mutual exclusion. `SETNX` is **SET** if **N**ot e**X**ists (corresponding to the `setIfAbsent` method in Java). If the key does not exist, the value of the key will be set. If key already exists, `SETNX` does nothing. + +```bash +> SETNX lockKey uniqueValue +(integer) 1 +> SETNX lockKey uniqueValue +(integer) 0 +``` + +To release the lock, delete the corresponding key directly through the `DEL` command. + +```bash +> DEL lockKey +(integer) 1 +``` + +In order to prevent accidentally deleting other locks, we recommend using Lua script to judge by the value (unique value) corresponding to the key. + +The Lua script was chosen to ensure the atomicity of the unlocking operation. Because Redis can execute Lua scripts in an atomic manner, thus ensuring the atomicity of the lock release operation. + +```lua +// When releasing the lock, first compare the value values corresponding to the lock to see if they are equal to avoid accidentally releasing the lock. +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +![Redis implements simple distributed lock](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png) + +This is the simplest Redis distributed lock implementation. The implementation method is relatively simple and the performance is very efficient. However, there are some problems with implementing distributed locking in this way. For example, if the application encounters some problems, such as the logic of releasing the lock suddenly hangs up, the lock may not be released, and the shared resources can no longer be accessed by other threads/processes. + +### Why do we need to set an expiration time for the lock? + +In order to prevent the lock from being released, one solution we can think of is: **Set an expiration time** for this key (that is, the lock). + +```bash +127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX +OK +``` + +- **lockKey**: the lock name; +- **uniqueValue**: a random string that uniquely identifies the lock; +- **NX**: SET can only succeed when the key value corresponding to lockKey does not exist; +- **EX**: Expiration time setting (in seconds) EX 3 indicates that this lock has an automatic expiration time of 3 seconds. Corresponding to EX is PX (in milliseconds), both of which are expiration time settings. + +**Be sure to ensure that setting the value and expiration time of the specified key is an atomic operation! ! ! ** Otherwise, there may still be a problem that the lock cannot be released. + +This can indeed solve the problem, but this solution also has loopholes: **If the time to operate the shared resource is greater than the expiration time, there will be a problem of early expiration of the lock, which will lead to the direct failure of the distributed lock. If the lock timeout is set too long, performance will be affected. ** + +You may be thinking: **If the operation of operating shared resources has not been completed, it would be great if the lock expiration time could be renewed by itself! ** + +### How to achieve graceful renewal of locks? + +For Java development partners, there is already a ready-made solution: **[Redisson](https://github.com/redisson/redisson)**. Solutions in 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 is an open source Java language Redis client that provides many out-of-the-box features, including not only the implementation of multiple distributed locks. Moreover, Redisson also supports multiple deployment architectures such as Redis stand-alone, Redis Sentinel, and Redis Cluster. + +The distributed lock in Redisson comes with an automatic renewal mechanism. It is very simple to use and the principle is relatively simple. It provides a **Watch Dog** specially used to monitor and renew the lock. If the thread operating the shared resource has not completed execution, the Watch Dog will continuously extend the expiration time of the lock, thereby ensuring that the lock will not be released due to timeout. + +![Redisson watchdog automatic renewal](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png) + +The name of the watchdog comes from the `getLockWatchdogTimeout()` method. This method returns the expiration time for the watchdog to renew the lock. The default is 30 seconds ([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6)). + +```java +//Default 30 seconds, supports modification +private long lockWatchdogTimeout = 30 * 1000; + +public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { + this.lockWatchdogTimeout = lockWatchdogTimeout; + return this; +} +public long getLockWatchdogTimeout() { + return lockWatchdogTimeout; +} +``` + +The `renewExpiration()` method contains the main logic of the watchdog: + +```java +private void renewExpiration() { + //...... + Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) throws Exception { + //...... + //Asynchronous renewal, based on Lua script + CompletionStage future = renewExpirationAsync(threadId); + future.whenComplete((res, e) -> { + if (e != null) { + //Cannot renew + log.error("Can't update lock " + getRawName() + " expiration", e); + EXPIRATION_RENEWAL_MAP.remove(getEntryName()); + return; + } + + if (res) { + // Recursive call to implement renewal + renewExpiration(); + } else { + // Cancel renewal + cancelExpirationRenewal(null); + } + }); + } + // Delay internalLockLeaseTime/3 (default 10s, which is 30/3) before calling + }, 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) + +In actual projects, it is recommended to use Curator to implement ZooKeeper distributed locks. Curator is a set of ZooKeeper Java client frameworks open sourced by Netflix. Compared with ZooKeeper's own client zookeeper, Curator's encapsulation is more complete, and various APIs can be used more conveniently. + +`Curator` mainly implements the following four locks: + +- `InterProcessMutex`: distributed reentrant exclusive lock +- `InterProcessSemaphoreMutex`: distributed non-reentrant exclusive lock +- `InterProcessReadWriteLock`: distributed read-write lock +- `InterProcessMultiLock`: A container that manages multiple locks as a single entity. When acquiring a lock, all locks are acquired. Releasing the lock will also release all lock resources (ignore locks that fail to be released). + +```java +CuratorFramework client = ZKUtils.getClient(); +client.start(); +// Distributed reentrant exclusive lock +InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); +// Distributed non-reentrant exclusive lock +InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); +// Treat multiple locks as a whole +InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2)); + +if (!lock.acquire(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("Cannot acquire multiple locks"); +} +System.out.println("Multiple locks acquired"); +System.out.println("Is there the first lock: " + lock1.isAcquiredInThisProcess()); +System.out.println("Is there a second lock: " + lock2.isAcquiredInThisProcess()); +try { + // Resource operations + resource.use(); +} finally { + System.out.println("Release multiple locks"); + lock.release(); +} +System.out.println("Is there the first lock: " + lock1.isAcquiredInThisProcess()); +System.out.println("Is there a second lock: " + lock2.isAcquiredInThisProcess()); +client.close(); +``` + +### Why use temporary sequential nodes? + +Each data node is called **znode** in ZooKeeper, which is the smallest unit of data in ZooKeeper. + +We usually divide znodes into 4 major categories: + +- **persistent (PERSISTENT) node**: Once created, it will always exist even if the ZooKeeper cluster goes down, until it is deleted. +- **Temporary (EPHEMERAL) node**: The life cycle of the temporary node is bound to the **client session (session)**. **The node disappears when the session disappears**. Moreover, **temporary nodes can only be used as leaf nodes** and cannot create child nodes. +- **Persistent Sequence (PERSISTENT_SEQUENTIAL) node**: In addition to the characteristics of the persistent (PERSISTENT) node, the names of child nodes are also sequential. For example `/node1/app0000000001`, `/node1/app0000000002`. +- **Temporary Sequential (EPHEMERAL_SEQUENTIAL) Node**: In addition to having the characteristics of temporary (EPHEMERAL) nodes, the names of child nodes are also sequential. + +It can be seen that compared with persistent nodes, the most important thing about temporary nodes is that they handle session failure differently. When the temporary node session disappears, the corresponding node disappears. In this case, it doesn't matter if an exception occurs on the client and the lock is not released in time. The session invalid node will be automatically deleted and deadlock will not occur. + +When using Redis to implement distributed locks, we use the expiration time to avoid deadlock problems caused by the lock being unable to be released, while ZooKeeper can directly use the characteristics of temporary nodes. + +Assuming that sequential nodes are not used, all clients that try to acquire a lock will add listeners to the child nodes holding the lock. When the lock is released, it will inevitably cause all clients trying to obtain the lock to compete for the lock, which is not friendly to performance. After using sequential nodes, you only need to listen to the previous node, which is more performance-friendly. + +### Why do we need to set up monitoring on the previous node? + +> Watcher (event listener) is a very important feature in ZooKeeper. ZooKeeper allows users to register some Watchers on designated nodes, and when certain events are triggered, the ZooKeeper server will notify interested clients of the event. This mechanism is an important feature of ZooKeeper in implementing distributed coordination services. + +During the same time period, many clients may acquire the lock at the same time, but only one can acquire it successfully. If the lock acquisition fails, it means that another client has successfully acquired the lock. The client that fails to acquire the lock will not keep looping to try to acquire the lock, but will register an event listener on the previous node. + +The function of this event listener is: **After the client corresponding to the current node releases the lock (that is, after the previous node is deleted, the listening event is the deletion event), notify the client that failed to acquire the lock (wake up the waiting thread, `wait/notifyAll` in Java), let it try to acquire the lock, and then successfully acquire the lock. ** + +### How to implement reentrant lock? + +Here we use Curator's `InterProcessMutex` to introduce the implementation of reentrant locks (source code address: [InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java)). + +When we call the `InterProcessMutex#acquire` method to acquire the lock, the `InterProcessMutex#internalLock` method will be called. + +```java +// Acquire the reentrant mutex until the acquisition is successful +@Override +public void acquire() throws Exception { + if (!internalLock(-1, null)) { + throw new IOException("Lost connection while trying to acquire lock: " + basePath); + } +} +``` + +The `internalLock` method will first obtain the thread currently requesting the lock, and then obtain the `lockData` corresponding to the current thread from `threadData` (`ConcurrentMap` type). `lockData` contains lock information and the number of locks, which is the key to implementing reentrant locks. + +When the lock is acquired for the first time, `lockData` is `null`. After successfully acquiring the lock, the current thread and the corresponding `lockData` will be placed in `threadData` + +```java +private boolean internalLock(long time, TimeUnit unit) throws Exception { + // Get the thread currently requesting the lock + Thread currentThread = Thread.currentThread(); + // Get the corresponding lockData + LockData lockData = threadData.get(currentThread); + // When acquiring the lock for the first time, lockData is null + if (lockData != null) { + //After the current thread acquires the lock once + // Because the current thread's lock exists, lockCount is incremented and then returned to achieve lock reentrancy. + lockData.lockCount.incrementAndGet(); + return true; + } + //Try to acquire the lock + String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); + if (lockPath != null) { + LockData newLockData = new LockData(currentThread, lockPath); + // After successfully acquiring the lock, put the current thread and the corresponding lockData into threadData. + threadData.put(currentThread, newLockData); + return true; + } + + return false; +} +``` + +`LockData` is a static inner class in `InterProcessMutex`. + +```java +private final ConcurrentMap threadData = Maps.newConcurrentMap(); + +private static class LockData +{ + //The thread currently holding the lock + final Thread owningThread; + // Lock the corresponding child node + final String lockPath; + //Number of locks + final AtomicInteger lockCount = new AtomicInteger(1); + + private LockData(Thread owningThread, String lockPath) + { + this.owningThread = owningThread; + this.lockPath = lockPath; + } +}``` + +If the lock has been acquired once and you acquire the lock again later, it will be blocked directly at `if (lockData != null)`, and then `lockData.lockCount.incrementAndGet();` will be executed to increase the number of locks by 1. + +The implementation logic of the entire reentrant lock is very simple. You can directly determine whether the current thread has acquired the lock on the client side. If so, just add 1 to the number of locks. + +## Summary + +In this article, I introduced two common ways to implement distributed locks: **Redis** and **ZooKeeper**. As for the specific choice of Redis or ZooKeeper to implement distributed locks, it still depends on the specific needs of the business. + +- If the performance requirements are relatively high, it is recommended to use Redis to implement distributed locks. It is recommended to choose the ready-made distributed lock provided by **Redisson** instead of implementing it yourself. It is not recommended to use the Redlock algorithm in actual projects because the costs and benefits are not proportional. You can consider implementing distributed locks based on Redis master-slave replication + sentinel mode. +- If the reliability requirements are relatively high, it is recommended to use ZooKeeper to implement distributed locks, and it is recommended to implement it based on the **Curator** framework. However, many projects now do not use ZooKeeper. It is not advisable to introduce ZooKeeper simply because of distributed locks. It is not recommended. It increases the complexity of the system for a small function. + +It should be noted that no matter which method you choose to implement distributed locks, including Redis, ZooKeeper or Etcd (not introduced in this article, but often used to implement distributed locks), there is no guarantee of 100% security, especially when encountering abnormal situations such as process garbage collection (GC) and network delays. + +In order to further improve the reliability of the system, it is recommended to introduce a safety net mechanism. For example, concurrency conflicts can be avoided through the **version number (Fencing Token) mechanism**. + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-lock.en.md b/docs_en/distributed-system/distributed-lock.en.md new file mode 100644 index 00000000000..7caa7daf74e --- /dev/null +++ b/docs_en/distributed-system/distributed-lock.en.md @@ -0,0 +1,85 @@ +--- +title: 分布式锁介绍 +category: 分布式 +--- + + + +网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。 + +这篇文章我们先介绍一下分布式锁的基本概念。 + +## 为什么需要分布式锁? + +在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。 + +举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况: + +- 线程 1、2、3 等多个线程同时进入抢购方法,每一个线程对应一个用户。 +- 线程 1 查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 +- 线程 2 也执行查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 +- 线程 1 继续执行,将库存数量减少 1 个,然后返回成功。 +- 线程 2 继续执行,将库存数量减少 1 个,然后返回成功。 +- 此时就发生了超卖问题,导致商品被多卖了一份。 + +![共享资源未互斥访问导致出现问题](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png) + +为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。 + +**如何才能实现共享资源的互斥访问呢?** 锁是一个比较通用的解决方案,更准确点来说是悲观锁。 + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +对于单机多线程来说,在 Java 中,我们通常使用 `ReentrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 + +下面是我对本地锁画的一张示意图。 + +![本地锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/jvm-local-lock.png) + +从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源。 + +分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,**分布式锁** 就诞生了。 + +举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。 + +下面是我对分布式锁画的一张示意图。 + +![分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock.png) + +从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。 + +## 分布式锁应该具备哪些条件? + +一个最基本的分布式锁需要满足: + +- **互斥**:任意一个时刻,锁只能被一个线程持有。 +- **高可用**:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。 +- **可重入**:一个节点获取了锁之后,还可以再次获取锁。 + +除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件: + +- **高性能**:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。 +- **非阻塞**:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。 + +## 分布式锁的常见实现方式有哪些? + +常见分布式锁实现方案如下: + +- 基于关系型数据库比如 MySQL 实现分布式锁。 +- 基于分布式协调服务 ZooKeeper 实现分布式锁。 +- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。 + +关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。 + +基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些,我专门写了一篇文章来详细介绍这两种方案:[分布式锁常见实现方案总结](./distributed-lock-implementations.md)。 + +## 总结 + +这篇文章我们主要介绍了: + +- 分布式锁的用途:分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。 +- 分布式锁的应该具备的条件:互斥、高可用、可重入、高性能、非阻塞。 +- 分布式锁的常见实现方式:关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、分布式键值存储系统比如 Redis 、Etcd 。 + + + diff --git a/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.en.md b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.en.md new file mode 100644 index 00000000000..33a12bb2026 --- /dev/null +++ b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.en.md @@ -0,0 +1,296 @@ +--- +title: ZooKeeper in action +category: distributed +tag: + - ZooKeeper +--- + +This article briefly demonstrates the use of common ZooKeeper commands and the basic use of ZooKeeper Java client Curator. The contents introduced are all the most basic operations, which can meet the basic needs of daily work. + +If there is anything that needs to be improved or perfected in the article, please point it out in the comment area and make progress together! + +## ZooKeeper installation + +### Install zookeeper using Docker + +**a. Download ZooKeeper using Docker** + +```shell +docker pull zookeeper:3.5.8 +``` + +**b.Run ZooKeeper** + +```shell +docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8 +``` + +### Connect to ZooKeeper service + +**a. Enter the ZooKeeper container** + +First use `docker ps` to view the ContainerID of ZooKeeper, and then use the `docker exec -it ContainerID /bin/bash` command to enter the container. + +**b. First enter the bin directory, and then connect to the ZooKeeper service through the `./zkCli.sh -server 127.0.0.1:2181` command** + +```bash +root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin# cd bin +``` + +If you see the following information printed out successfully on the console, it means you have successfully connected to the ZooKeeper service. + +![Connect ZooKeeper service](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/connect-zooKeeper-service.png) + +## ZooKeeper common command demonstration + +### View commonly used commands (help command) + +Use the `help` command to view common ZooKeeper commands + +### Create node (create command) + +The node1 node was created in the root directory through the `create` command, and the string associated with it is "node1" + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 "node1" +``` + +The node1 node was created in the root directory through the `create` command, and the content associated with it is the number 123 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123 +Created /node1/node1.1 +``` + +### Update node data content (set command) + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1" +``` + +### Get node data (get command) + +The `get` command can obtain the data content of the specified node and the status of the node. It can be seen that we have changed the node data content to "set node1" through the `set` command. + +```shell +[zk: zookeeper(CONNECTED) 12] get -s /node1 +set node1 +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x4b +mtime = Sun Jan 20 10:41:10 CST 2019 +pZxid = 0x4a +cversion=1 +dataVersion = 1 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 9 +numChildren = 1 + +``` + +### View subnodes in a directory (ls command) + +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] +``` + +Use the `ls` command to view the nodes in the node1 directory + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1 +[node1.1] +``` + +The ls command in ZooKeeper is similar to the ls command in Linux. This command will list all child node information under the absolute path path (listing level 1, not recursively) + +### View node status (stat command) + +Check node status through `stat` command + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1 +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x47 +mtime = Sun Jan 20 10:22:59 CST 2019 +pZxid = 0x4a +cversion=1 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 11 +numChildren = 1 +``` + +Some of the information displayed above, such as cversion, aclVersion, numChildren, etc., I have already introduced in the above article "[ZooKeeper Related Concept Summary (Getting Started)](https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html)". + +### View node information and status (ls2 command) + +The `ls2` command is more like a combination of the `ls` command and the `stat` command. The information returned by the `ls2` command includes 2 parts: + +1. Child node list +2. The stat information of the current node. + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1 +[node1.1] +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x47 +mtime = Sun Jan 20 10:22:59 CST 2019 +pZxid = 0x4a +cversion=1 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 11 +numChildren = 1 + +``` + +### Delete node (delete command) + +This command is very simple, but one thing to note is that if you want to delete a node, the node must have no child nodes. + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1 +``` + +Later I will introduce the use of Java client API and the use of open source ZooKeeper client ZkClient and Curator. + +## ZooKeeper Java client Curator is easy to use + +Curator is a set of ZooKeeper Java client frameworks open sourced by Netflix. Compared with the client zookeeper that comes with Zookeeper, Curator's encapsulation is more complete, and various APIs can be used more conveniently. + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/curator.png) + +Let’s briefly demonstrate the use of Curator! + +Curator4.0+ version has better support for ZooKeeper 3.5.x. Before starting, please add the following dependencies to your project. + +```xml + + org.apache.curator + curator-framework + 4.2.0 + + + org.apache.curator + curator-recipes + 4.2.0 + +``` + +### Connect to ZooKeeper client + +Create a `CuratorFramework` object through `CuratorFrameworkFactory`, and then call the `start()` method of the `CuratorFramework` object! + +```java +private static final int BASE_SLEEP_TIME = 1000; +private static final int MAX_RETRIES = 3; + +// Retry strategy. Retry 3 times, and will increase the sleep time between retries. +RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES); +CuratorFramework zkClient = CuratorFrameworkFactory.builder() + // the server to connect to (can be a server list) + .connectString("127.0.0.1:2181") + .retryPolicy(retryPolicy) + .build(); +zkClient.start();``` + +Description of some basic parameters: + +- `baseSleepTimeMs`: initial time to wait between retries +- `maxRetries`: Maximum number of retries +- `connectString`: list of servers to connect to +- `retryPolicy`: retry policy + +### Add, delete, modify and query data nodes + +#### Create node + +We introduced in [ZooKeeper Common Concepts Interpretation](./zookeeper-intro.md) that we usually divide znode into 4 major categories: + +- **persistent (PERSISTENT) node**: Once created, it will always exist even if the ZooKeeper cluster goes down, until it is deleted. +- **Temporary (EPHEMERAL) node**: The life cycle of the temporary node is bound to the **client session (session)**. **The node disappears when the session disappears**. Moreover, temporary nodes **can only be used as leaf nodes** and cannot create child nodes. +- **Persistent Sequence (PERSISTENT_SEQUENTIAL) node**: In addition to the characteristics of the persistent (PERSISTENT) node, the names of child nodes are also sequential. For example `/node1/app0000000001`, `/node1/app0000000002`. +- **Temporary Sequential (EPHEMERAL_SEQUENTIAL) Node**: In addition to having the characteristics of temporary (EPHEMERAL) nodes, the names of child nodes are also sequential. + +When you use ZooKeeper, you will find that there are actually 7 znode types in the `CreateMode` class, but the 4 types introduced above are the most used. + +**a.Create persistence node** + +You can create persistent nodes in the following two ways. + +```java +//Note: The following code will report an error. The specific reasons are explained below. +zkClient.create().forPath("/node1/00001"); +zkClient.create().withMode(CreateMode.PERSISTENT).forPath("/node1/00002"); +``` + +However, you will get an error when you run the above code. This is because the parent node `node1` has not been created yet. + +You can create the parent node `node1` first, and then execute the above code without error. + +```java +zkClient.create().forPath("/node1"); +``` + +The more recommended way is to use the following line of code, **`creatingParentsIfNeeded()` can ensure that the parent node is automatically created when the parent node does not exist, which is very useful. ** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/node1/00001"); +``` + +**b. Create temporary nodes** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001"); +``` + +**c. Create nodes and specify data content** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); +zkClient.getData().forPath("/node1/00001");//Get the data content of the node, and get the byte array +``` + +**d. Check whether the node is created successfully** + +```java +zkClient.checkExists().forPath("/node1/00001");//If it is not null, it means the node was created successfully +``` + +#### Delete node + +**a.Delete a child node** + +```java +zkClient.delete().forPath("/node1/00001"); +``` + +**b. Delete a node and all its child nodes** + +```java +zkClient.delete().deletingChildrenIfNeeded().forPath("/node1"); +``` + +#### Get/update node data content + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); +zkClient.getData().forPath("/node1/00001");//Get the data content of the node +zkClient.setData().forPath("/node1/00001","c++".getBytes());//Update node data content +``` + +#### Get the paths of all child nodes of a node + +```java +List childrenPaths = zkClient.getChildren().forPath("/node1"); +``` + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.en.md b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.en.md new file mode 100644 index 00000000000..49c08efc404 --- /dev/null +++ b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.en.md @@ -0,0 +1,314 @@ +--- +title: ZooKeeper相关概念总结(入门) +category: 分布式 +tag: + - ZooKeeper +--- + +相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 到底有啥用不?如果别人/面试官让你给他讲讲对于 ZooKeeper 的认识,你能回答到什么地步呢? + +拿我自己来说吧!我本人在大学曾经使用 Dubbo 来做分布式项目的时候,使用了 ZooKeeper 作为注册中心。为了保证分布式系统能够同步访问某个资源,我还使用 ZooKeeper 做过分布式锁。另外,我在学习 Kafka 的时候,知道 Kafka 很多功能的实现依赖了 ZooKeeper。 + +前几天,总结项目经验的时候,我突然问自己 ZooKeeper 到底是个什么东西?想了半天,脑海中只是简单的能浮现出几句话: + +1. ZooKeeper 可以被用作注册中心、分布式锁; +2. ZooKeeper 是 Hadoop 生态系统的一员; +3. 构建 ZooKeeper 集群的时候,使用的服务器最好是奇数台。 + +由此可见,我对于 ZooKeeper 的理解仅仅是停留在了表面。 + +所以,通过本文,希望带大家稍微详细的了解一下 ZooKeeper 。如果没有学过 ZooKeeper ,那么本文将会是你进入 ZooKeeper 大门的垫脚砖。如果你已经接触过 ZooKeeper ,那么本文将带你回顾一下 ZooKeeper 的一些基础概念。 + +另外,本文不光会涉及到 ZooKeeper 的一些概念,后面的文章会介绍到 ZooKeeper 常见命令的使用以及使用 Apache Curator 作为 ZooKeeper 的客户端。 + +_如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!_ + +## ZooKeeper 介绍 + +### ZooKeeper 由来 + +正式介绍 ZooKeeper 之前,我们先来看看 ZooKeeper 的由来,还挺有意思的。 + +下面这段内容摘自《从 Paxos 到 ZooKeeper》第四章第一节,推荐大家阅读一下: + +> ZooKeeper 最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。 +> +> 关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的 Pig 项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家 RaghuRamakrishnan 开玩笑地说:“在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园了,而 ZooKeeper 正好要用来进行分布式环境的协调一一于是,ZooKeeper 的名字也就由此诞生了。 + +### ZooKeeper 概览 + +ZooKeeper 是一个开源的**分布式协调服务**,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 + +> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。 + +ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。这些功能的实现主要依赖于 ZooKeeper 提供的 **数据存储+事件监听** 功能(后文会详细介绍到) 。 + +ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。 + +另外,很多顶级的开源项目都用到了 ZooKeeper,比如: + +- **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 集群宕机,直到将其删除。 +- **Temporary (EPHEMERAL) node**: The life cycle of the temporary node is bound to the **client session (session)**. **When the session disappears, the node disappears**. Moreover, **temporary nodes can only be used as leaf nodes** and cannot create child nodes. +- **Persistent Sequence (PERSISTENT_SEQUENTIAL) node**: In addition to the characteristics of the persistent (PERSISTENT) node, the names of child nodes are also sequential. For example `/node1/app0000000001`, `/node1/app0000000002`. +- **Temporary Sequential (EPHEMERAL_SEQUENTIAL) Node**: In addition to having the characteristics of temporary (EPHEMERAL) nodes, the names of child nodes also have sequential properties. + +Each znode consists of 2 parts: + +- **stat**: status information +- **data**: The specific content of the data stored in the node + +As shown below, I use the get command to obtain the contents of the dubbo node in the root directory. (The get command will be introduced below). + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo +#The data content associated with this data node is empty +null +#The following is some status information of the data node, which is actually the formatted output of the Stat object. +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 +``` + +The Stat class contains fields for all status information of a data node, including transaction ID (cZxid), node creation time (ctime), number of child nodes (numChildren), etc. + +Let’s take a look at what each znode status information actually represents! (The following content comes from "From Paxos to ZooKeeper Distributed Consistency Principles and Practices", because the Guide is indeed not particularly clear, so you need to learn the reference materials!): + +| znode status information | explanation | +| -------------- | ------------------------------------------------------------------------------------------------------------------ | +| cZxid | create ZXID, which is the transaction id when the data node was created | +| ctime | create time, that is, the creation time of the node | +| mZxid | modified ZXID, which is the transaction id when the node was last updated | +| mtime | modified time, which is the last update time of the node | +| pZxid | The transaction id when the node's child node list was last modified. Only changes in the child node list will update pZxid, and changes in the child node content will not be updated | +| cversion | The version number of the child node, the value increases by 1 every time the child node of the current node changes | +| dataVersion | Data node content version number. It is 0 when the node is created. Every time the node content is updated (regardless of whether the content changes or not), the value of the version number increases by 1 | +| aclVersion | The ACL version number of the node, indicating the number of changes to the ACL information of the node | +| ephemeralOwner | sessionId of the session that created this temporary node; if the current node is a persistent node, ephemeralOwner=0 | +| dataLength | Data node content length | +| numChildren | The number of child nodes of the current node | + +### Version + +As we mentioned before, corresponding to each znode, ZooKeeper will maintain a data structure called **Stat** for it. Stat records three related versions of this znode: + +- **dataVersion**: The version number of the current znode node +- **cversion**: The version of the current znode child node +- **aclVersion**: The version of the ACL of the current znode. + +### ACL (Authority Control) + +ZooKeeper uses the ACL (AccessControlLists) policy for permission control, which is similar to the permission control of UNIX file systems. + +For znode operation permissions, ZooKeeper provides the following 5 types: + +- **CREATE** : Can create child nodes +- **READ**: Can obtain node data and list its child nodes +- **WRITE** : Can set/update node data +- **DELETE** : can delete child nodes +- **ADMIN**: Permission to set node ACL + +What needs special attention is that both **CREATE** and **DELETE** are permission controls for **child nodes**. + +For identity authentication, the following methods are provided: + +- **world**: Default mode, all users can access unconditionally. +- **auth**: does not use any id, represents any authenticated user. +- **digest** :username:password authentication method: _username:password_. +- **ip**: Restrict the specified IP. + +### Watcher (event listener) + +Watcher (event listener) is a very important feature in ZooKeeper. ZooKeeper allows users to register some Watchers on designated nodes, and when certain events are triggered, the ZooKeeper server will notify interested clients of the event. This mechanism is an important feature of ZooKeeper in implementing distributed coordination services. + +![ZooKeeper Watcher mechanism](https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-watcher.png) + +_Poyin: A very useful feature. I took out a small notebook and memorized it. The Watcher (event listener) mechanism is basically inseparable from ZooKeeper when used later. _ + +### Session + +Session can be regarded as a long TCP connection between the ZooKeeper server and the client. Through this connection, the client can maintain a valid session with the server through heartbeat detection, and can also send requests to the ZooKeeper server and receive responses. It can also receive Watcher event notifications from the server through this connection. + +Session has a property called: `sessionTimeout`, `sessionTimeout` represents the session timeout. When the client connection is disconnected due to various reasons such as excessive server pressure, network failure, or the client actively disconnects, as long as any server in the cluster can be reconnected within the time specified by `sessionTimeout`, the previously created session will still be valid. + +In addition, before creating a session for a client, the server first assigns a `sessionID` to each client. Since `sessionID` is an important identifier of a ZooKeeper session, many session-related operating mechanisms are based on this `sessionID`. Therefore, no matter which server assigns `sessionID` to the client, it must be globally unique. + +## ZooKeeper cluster + +In order to ensure high availability, it is best to deploy ZooKeeper in a cluster form, so that as long as most of the machines in the cluster are available (can tolerate certain machine failures), ZooKeeper itself will still be available. Usually 3 servers can form a ZooKeeper cluster. The architecture diagram officially provided by ZooKeeper is a ZooKeeper cluster that provides services to the outside world as a whole.![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 服务器的数据状态保持一致**。 +- **Message Broadcast**:** When more than half of the Follower servers in the cluster have completed the status synchronization with the Leader server, then the entire service framework can enter the message broadcast mode. ** When a server that also complies with the ZAB protocol joins the cluster after being started, if there is already a Leader server in the cluster responsible for message broadcast, then the newly added server will consciously enter the data recovery mode: find the server where the Leader is located, synchronize data with it, and then participate in the message broadcast process together. + +### ZAB Protocol & Paxos Algorithm Article Recommendations + +There are too many things that need to be talked about and understood about **ZAB protocol & Paxos algorithm**. For details, you can read the following articles: + +- [Detailed explanation of Paxos algorithm](https://javaguide.cn/distributed-system/protocol/paxos-algorithm.html) +- [ZooKeeper and Zab Protocol · Analyze](https://wingsxdu.com/posts/database/zookeeper/) +- [Detailed explanation of Raft algorithm](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html) + +## ZooKeeper VS ETCD + +[ETCD](https://etcd.io/) is a strongly consistent distributed key-value store that provides a reliable way to store data that needs to be accessed by a distributed system or machine cluster. ETCD internally uses [Raft algorithm](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html) as the consistency algorithm, which is implemented based on Go language. + +Similar to ZooKeeper, ETCD can also be used in data publishing/subscription, load balancing, naming services, distributed coordination/notification, distributed locks and other scenarios. So how to choose between the two? + +This article by Dewu Technology [A brief analysis of how to implement high-availability architecture based on ZooKeeper] (https://mp.weixin.qq.com/s/pBI3rjv5NdS1124Z7HQ-JA) provides the following comparison table (I have further optimized it), which can be used as a reference: + +| | ZooKeeper | ETCD | +|---------------- |-------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| **Language** | Java | Go | +| **Protocol** | TCP | Grpc | +| **Interface Call** | You must use your own client to call | Can be transmitted through HTTP, you can call it through commands such as CURL | +| **Consensus Algorithm** | Zab Protocol | Raft Algorithm | +| **Watcher mechanism** | Limited, one-time trigger | One Watch can monitor all events | +| **Data model** | Directory-based hierarchical model | Refers to zk's data model, which is a flat kv model | +| **Storage** | kv storage, using ConcurrentHashMap, memory storage, generally not recommended to store large amounts of data | kv storage, using the bbolt storage engine, can handle several GB of data. | +| **MVCC** | Not supported | Supported, version control through two B+ Trees | +| **Global Session** | Defects | Implementation is more flexible and avoids security issues | +| **Permission Verification** | ACL | RBAC | +| **Transaction Capability** | Provides simple transaction capabilities | Only provides version number checking capabilities | +| **Deployment and Maintenance** | Complex | Simple | + +ZooKeeper has certain limitations in storage performance, global Session, Watcher mechanism, etc. More and more open source projects are replacing ZooKeeper with Raft implementation or other distributed coordination services, such as: [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 is relatively better, providing more stable high-load reading and writing capabilities, and improving and optimizing many problems exposed by ZooKeeper. Moreover, ETCD can basically cover all application scenarios of ZooKeeper and replace it. + +## Summary + +1. ZooKeeper itself is a distributed program (as long as more than half of the nodes survive, ZooKeeper can serve normally). +2. In order to ensure high availability, it is best to deploy ZooKeeper in a cluster form. In this way, as long as most of the machines in the cluster are available (can tolerate certain machine failures), ZooKeeper itself will still be available. +3. ZooKeeper stores data in memory, which ensures high throughput and low latency (however, the memory limits the amount of data that can be stored, and this limitation is also a further reason to keep the amount of data stored in znodes small). +4. ZooKeeper is high performance. This is especially true in applications where there are more reads than writes, since the writes cause all inter-server states to be synchronized. (More "reads" than "writes" are a typical scenario for coordinating services.) +5. ZooKeeper has the concept of temporary nodes. Transient nodes exist as long as the client session that created the transient node remains active. When the session ends, the transient node is deleted. A persistent node means that once the znode is created, the znode will always be saved on ZooKeeper unless the znode is actively removed. +6. The bottom layer of ZooKeeper actually only provides two functions: ① manage (storage, read) data submitted by user programs; ② provide data node monitoring services for user programs. + +## Reference + +- "Principles and Practices of Distributed Consistency from Paxos to ZooKeeper" +- Talk about the limitations of ZooKeeper: + + \ No newline at end of file diff --git a/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.en.md b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.en.md new file mode 100644 index 00000000000..70708cac863 --- /dev/null +++ b/docs_en/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.en.md @@ -0,0 +1,387 @@ +--- +title: ZooKeeper相关概念总结(进阶) +category: 分布式 +tag: + - ZooKeeper +--- + +> [FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 + +## 什么是 ZooKeeper + +`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理等。 + +简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 + +其实解释到分布式这个概念的时候,我发现有些同学并不是能把 **分布式和集群** 这两个概念很好的理解透。前段时间有同学和我探讨起分布式的东西,他说分布式不就是加机器吗?一台机器不够用再加一台抗压呗。当然加机器这种说法也无可厚非,你一个分布式系统必定涉及到多个机器,但是你别忘了,计算机学科中还有一个相似的概念—— `Cluster` ,集群不也是加机器吗?但是 集群 和 分布式 其实就是两个完全不同的概念。 + +比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 + +![cluster](https://oss.javaguide.cn/p3-juejin/60263e969b9e4a0f81724b1f4d5b3d58~tplv-k3u1fbpfcp-zoom-1.jpeg) + +但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 + +![distributed](https://oss.javaguide.cn/p3-juejin/0d42e7b4249144b3a77a0c519216ae3d~tplv-k3u1fbpfcp-zoom-1.jpeg) + +而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 + +![](https://oss.javaguide.cn/p3-juejin/e3662ca1a09c4444b07f15dbf85c6ba8~tplv-k3u1fbpfcp-zoom-1.jpeg) + +比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 + +## 一致性问题 + +设计一个分布式系统必定会遇到一个问题—— **因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡** 。这就是著名的 `CAP` 定理。 + +理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 + +![](https://oss.javaguide.cn/p3-juejin/38b9ff4b193e4487afe32c9710c6d644~tplv-k3u1fbpfcp-zoom-1-20230717160254318-20230717160259975.jpeg) + +而上述前者就是 `Eureka` 的处理方式,它保证了 AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了 CP(数据一致性)。 + +## 一致性协议和算法 + +而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如 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` 的典型应用场景,比如选主,注册中心等等。 + + 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出 🤝🤝🤝。 + + + diff --git a/docs_en/distributed-system/distributed-transaction.en.md b/docs_en/distributed-system/distributed-transaction.en.md new file mode 100644 index 00000000000..64569e131f3 --- /dev/null +++ b/docs_en/distributed-system/distributed-transaction.en.md @@ -0,0 +1,12 @@ +--- +title: Summary of common solutions for distributed transactions (paid) +category: distributed +--- + +**Distributed Transactions** The related interview questions are exclusive to my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and how to join), which has been compiled into the "Java Interview Guide". + +![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) + + + + \ No newline at end of file diff --git a/docs_en/distributed-system/protocol/cap-and-base-theorem.en.md b/docs_en/distributed-system/protocol/cap-and-base-theorem.en.md new file mode 100644 index 00000000000..e50a63e7ad9 --- /dev/null +++ b/docs_en/distributed-system/protocol/cap-and-base-theorem.en.md @@ -0,0 +1,161 @@ +--- +title: CAP & BASE理论详解 +category: 分布式 +tag: + - 分布式理论 +--- + +经历过技术面试的小伙伴想必对 CAP & BASE 这个两个理论已经再熟悉不过了! + +我当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。一是因为这两个分布式基础理论是学习分布式知识的必备前置基础,二是因为很多面试官自己比较熟悉这两个理论(方便提问)。 + +我们非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。 + +## CAP 理论 + +[CAP 理论/定理](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)起源于 2000 年,由加州大学伯克利分校的 Eric Brewer 教授在分布式计算原理研讨会(PODC)上提出,因此 CAP 定理又被称作 **布鲁尔定理(Brewer’s theorem)** + +2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,CAP 理论正式成为分布式领域的定理。 + +### 简介 + +**CAP** 也就是 **Consistency(一致性)**、**Availability(可用性)**、**Partition Tolerance(分区容错性)** 这三个单词首字母组合。 + +![](https://oss.javaguide.cn/2020-11/cap.png) + +CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 **Consistency**、**Availability**、**Partition Tolerance** 三个单词的明确定义。 + +因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解读。 + +在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个: + +- **一致性(Consistency)** : 所有节点访问同一份最新的数据副本 +- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。 +- **分区容错性(Partition Tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。 + +**什么是网络分区?** + +分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 **网络分区**。 + +![partition-tolerance](https://oss.javaguide.cn/2020-11/partition-tolerance.png) + +### 不是所谓的“3 选 2” + +大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。 + +> **当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。** +> +> 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。 + +因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。** 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。 + +**为啥不可能选择 CA 架构呢?** 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。 + +**选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。** + +另外,需要补充说明的一点是:**如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。** + +### CAP 实际应用案例 + +我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。 + +下图是 Dubbo 的架构图。**注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?** + +注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。 + +![](https://oss.javaguide.cn/2020-11/dubbo-architecture.png) + +常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。 + +1. **ZooKeeper 保证的是 CP。** 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。 +2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 +3. **Nacos 不仅支持 CP 也支持 AP。** + +**🐛 修正(参见:[issue#1906](https://github.com/Snailclimb/JavaGuide/issues/1906))**: + +ZooKeeper 通过可线性化(Linearizable)写入、全局 FIFO 顺序访问等机制来保障数据一致性。多节点部署的情况下, ZooKeeper 集群处于 Quorum 模式。Quorum 模式下的 ZooKeeper 集群, 是一组 ZooKeeper 服务器节点组成的集合,其中大多数节点必须同意任何变更才能被视为有效。 + +由于 Quorum 模式下的读请求不会触发各个 ZooKeeper 节点之间的数据同步,因此在某些情况下还是可能会存在读取到旧数据的情况,导致不同的客户端视图上看到的结果不同,这可能是由于网络延迟、丢包、重传等原因造成的。ZooKeeper 为了解决这个问题,提供了 Watcher 机制和版本号机制来帮助客户端检测数据的变化和版本号的变更,以保证数据的一致性。 + +### 总结 + +在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 + +在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区” + +如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。 + +总结:**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** + +### 推荐阅读 + +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) (英文,带给你不一样的思考) + +## BASE 理论 + +[BASE 理论](https://dl.acm.org/doi/10.1145/1394127.1394128)起源于 2008 年, 由 eBay 的架构师 Dan Pritchett 在 ACM 上发表。 + +### 简介 + +**BASE** 是 **Basically Available(基本可用)**、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 + +### BASE 理论的核心思想 + +即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +> That is to say, data consistency is sacrificed to meet the high availability of the system. When part of the data in the system is unavailable or inconsistent, the entire system still needs to be kept "mainly available". + +**BASE theory is essentially an extension and supplement to CAP, more specifically, a supplement to the AP scheme in CAP. ** + +**Why do you say this? ** + +We have also said this in the CAP theory section: + +> If the system is not "partitioned" and the network connection communication between nodes is normal, there will be no P. At this time, we can guarantee C and A at the same time. Therefore, if the system is "partitioned", we need to consider whether to choose CP or AP. If the system is not "partitioned", we need to think about how to ensure CA. ** + +Therefore, the AP scheme only gives up consistency when the system is partitioned, rather than giving up consistency forever. After recovery from a partition failure, the system should reach eventual consistency. This is actually where BASE theory extends. + +### Three elements of BASE theory + +![Three elements of BASE theory](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzI0LzE2MzkxNDgwNmQ5ZTE1YzY?x-oss-process=image/format,png) + +#### Basically available + +Basic availability means that the distributed system is allowed to lose part of its availability when unpredictable failures occur. However, this is by no means equivalent to the system being unavailable. + +**What does it mean to allow partial loss of availability? ** + +- **Loss in response time**: Under normal circumstances, it takes 0.5s to process a user request and return a result, but due to a system failure, the time to process a user request becomes 3 s. +- **Loss of system functions**: Under normal circumstances, users can use all functions of the system, but due to a sudden increase in system visits, some non-core functions of the system cannot be used. + +#### Soft state + +Soft state refers to allowing the data in the system to exist in an intermediate state (**data inconsistency in CAP theory**), and believing that the existence of this intermediate state will not affect the overall availability of the system, that is, allowing the system to have a delay in the process of data synchronization between data copies of different nodes. + +####Eventual consistency + +Eventual consistency emphasizes that all data copies in the system can eventually reach 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 be consistent, but it does not need to ensure the strong consistency of the system data in real time. + +> 3 levels of distributed consistency: +> +> 1. **Strong consistency**: Whatever the system writes is what is read out. +> 2. **Weak consistency**: It is not necessarily possible to read the latest written value, nor is it guaranteed that the data read after a certain amount of time will be the latest, but it will try to ensure that the data is consistent at a certain moment. +> 3. **Eventual Consistency**: An upgraded version of weak consistency. The system will ensure that data is consistent within a certain period of time. +> +> **The industry recommends the ultimate consistency level, but in some scenarios that have very strict data consistency requirements, such as bank transfers, strong consistency must be ensured. ** + +So what is the specific way to achieve eventual consistency? ["Distributed Protocols and Algorithms in Practice"] (http://gk.link/a/10rZM) is introduced like this: + +> - **Repair while reading**: When reading data, detect data inconsistencies and repair them. For example, Cassandra's Read Repair implementation. Specifically, when querying data from the Cassandra system, if it detects that the replica data on different nodes is inconsistent, the system will automatically repair the data. +> - **Repair on write**: When writing data and detecting data inconsistencies, repair them. For example, Cassandra’s Hinted Handoff implementation. Specifically, when writing data remotely between nodes in the Cassandra cluster, if the writing fails, the data will be cached and then retransmitted regularly to repair data inconsistencies. +> - **Asynchronous Repair**: This is the most commonly used method, which detects the consistency of the copy data through regular reconciliation and repairs it. + +**Repair on write** is recommended. This method has lower performance consumption. + +### Summary + +**ACID is the theory of database transaction integrity, CAP is the distributed system design theory, and BASE is an extension of the AP scheme in the CAP theory. ** + + \ No newline at end of file diff --git a/docs_en/distributed-system/protocol/consistent-hashing.en.md b/docs_en/distributed-system/protocol/consistent-hashing.en.md new file mode 100644 index 00000000000..7a320ae2ff8 --- /dev/null +++ b/docs_en/distributed-system/protocol/consistent-hashing.en.md @@ -0,0 +1,131 @@ +--- +title: 一致性哈希算法详解 +category: 分布式 +tag: + - 分布式协议&算法 + - 哈希算法 +--- + +开始之前,先说两个常见的场景: + +1. **负载均衡**:由于访问人数太多,我们的网站部署了多台服务器个共同提供相同的服务,但每台服务器上存储的数据不同。为了保证请求的正确响应,相同参数(key)的请求(比如同个 IP 的请求、同一个用户的请求)需要发到同一台服务器处理。 +2. **分布式缓存**:由于缓存数据量太大,我们部署了多台缓存服务器共同提供缓存服务。缓存数据需要尽可能均匀地分布式在这些缓存服务器上,通过 key 可以找到对应的缓存服务器。 + +这两种场景的本质,都是需要建立一个**从 key 到服务器/节点的稳定映射关系**。 + +为了实现这个目标,你首先会想到什么方案呢? + +## 普通哈希算法 + +相信大家很快就能想到 **“哈希+取模”** 这个经典组合。通过哈希函数计算出 key 的哈希值,再对服务器数量取模,从而将 key 映射到固定的服务器上。 + +公式也很简单: + +```java +node_number = hash(key) % N +``` + +- `hash(key)`: 使用哈希函数(建议使用性能较好的非加密哈希函数,例如 SipHash、MurMurHash3、CRC32、DJB)对唯一键进行哈希。 +- `% N`: 对哈希值取模,将哈希值映射到一个介于 0 到 N-1 之间的值,N 为节点数/服务器数。 + +![哈希取模](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/hashqumo.png) + +然而,传统的哈希取模算法有一个比较大的缺陷就是:**无法很好的解决机器/节点动态减少(比如某台机器宕机)或者增加的场景(比如又增加了一台机器)。** + +想象一下,服务器的初始数量为 4 台 (N = 4),如果其中一台服务器宕机,N 就变成了 3。此时,对于同一个 key,`hash(key) % 3` 的结果很可能与 `hash(key) % 4` 完全不同。 + +![哈希取模-移除节点Node2](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/hashqumo-remove-node2.png) + +这意味着几乎所有的数据映射关系都会错乱。在分布式缓存场景下,这会导致**大规模的缓存失效和缓存穿透**,瞬间将压力全部打到后端的数据库上,引发系统雪崩。 + +据估算,当节点数量从 N 变为 N-1 时,平均有 (N-1)/N 比例的数据需要迁移,这个比例 **趋近于 100%** 。这种“牵一发而动全身”的效应,在生产环境中是完全不可接受的。 + +为了更好地解决这个问题,一致性哈希算法诞生了。 + +## 一致性哈希算法 + +一致性哈希算法在 1997 年由麻省理工学院提出(这篇论文的 PDF 在线阅读地址:),是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了传统哈希算法在分布式[哈希表](https://baike.baidu.com/item/哈希表/5981869)(Distributed Hash Table,DHT)中存在的动态伸缩等问题 。 + +一致性哈希算法的底层原理也很简单,关键在于**哈希环**的引入。 + +### 哈希环 + +一致性哈希算法将哈希空间组织成一个环形结构,将数据和节点都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上。通常情况下,哈希环的起点是 0,终点是 2^32 - 1,并且起点与终点连接,故这个环的整数分布范围是 **[0, 2^32-1]** 。 + +传统哈希算法是对服务器数量取模,一致性哈希算法是对哈希环的范围取模,固定值,通常为 2^32: + +```java +node_number = hash(key) % 2^32 +``` + +服务器/节点如何映射到哈希环上呢?也是哈希取模。例如,一般我们会根据服务器的 IP 或者主机名进行哈希,然后再取模。 + +```java +hash(服务器ip)% 2^32 +``` + +如下图所示: + +![哈希环](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/consistent-hashing-circle.png) + +我们将数据和节点都映射到哈希环上,环上的每个节点都负责一个区间。对于上图来说,每个节点负责的数据情况如下: + +- **Node1:** 负责 Node4 到 Node1 之间的区域(包含 value6)。 +- **Node2:** 负责 Node1 到 Node2 之间的区域(包含 value1, value2)。 +- **Node3:** 负责 Node2 到 Node3 之间的区域(包含 value3)。 +- **Node4:** 负责 Node3 到 Node4 之间的区域(包含 value4, value5)。 + +### 节点移除/增加 + +新增节点和移除节点的情况下,哈希环的引入可以避免影响范围太大,减少需要迁移的数据。 + +还是用上面分享的哈希环示意图为例,假设 Node2 节点被移除的话,那 Node3 就要负责 Node2 的数据,直接迁移 Node2 的数据到 Node3 即可,其他节点不受影响。 + +![节点移除](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/consistent-hashing-circle-remove-node2.png) + +同样地,如果我们在 Node1 和 Node2 之间新增一个节点 Node5,那么原本应该由 Node2 负责的一部分数据(即哈希值落在 Node1 和 Node5 之间的数据,如图中的 value1)现在会由 Node5 负责。我们只需要将这部分数据从 Node2 迁移到 Node5 即可,同样只影响了相邻的节点,影响范围非常小。 + +![节点增加](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/consistent-hashing-circle-add-node5.png) + +### 数据倾斜问题 + +理想情况下,节点在环上是均匀分布的。然而,现实可能并不是这样的,尤其是节点数量比较少的时候。节点可能被映射到附近的区域,这样的话,就会导致绝大部分数据都由其中一个节点负责。 + +![数据倾斜](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/consistent-hashing-circle-unbalance.png) + +对于上图来说,每个节点负责的数据情况如下: + +- **Node1:** 负责 Node4 到 Node1 之间的区域(包含 value6)。 +- **Node2:** 负责 Node1 到 Node2 之间的区域(包含 value1)。 +- **Node3:** 负责 Node2 到 Node3 之间的区域(包含 value2,value3, value4, value5)。 +- **Node4:** 负责 Node3 到 Node4 之间的区域。 + +除了数据倾斜问题,还有一个隐患。当新增或者删除节点的时候,数据分配不均衡。例如,Node3 被移除的话,Node3 负责的所有数据都要交给 Node4,随后所有的请求都要达到 Node4 上。假设 Node4 的服务器处理能力比较差的话,那可能直接就被干崩了。理想情况下,应该有更多节点来分担压力。 + +如何解决这些问题呢?答案是引入**虚拟节点**。 + +### 虚拟节点 + +虚拟节点就是对真实的物理节点在哈希环上虚拟出几个它的分身节点。数据落到分身节点上实际上就是落到真实的物理节点上,通过将虚拟节点均匀分散在哈希环的各个部分。 + +如下图所示,Node1、Node2、Node3、Node4 这 4 个节点都对应 3 个虚拟节点(下图只是为了演示,实际情况节点分布不会这么有规律)。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/consistent-hashing/consistent-hashing-circle-virtual-node.png) + +对于上图来说,每个节点最终负责的数据情况如下: + +- **Node1**:value4 +- **Node2**:value1,value3 +- **Node3**:value5 +- **Node4**:value2,value6 + +**The benefits of introducing virtual nodes are huge:** + +1. **Data balancing:** The more virtual nodes there are, the denser the "server points" on the ring are, the more even the data distribution will naturally be, fundamentally solving the problem of data skew. Typically, the number of virtual nodes corresponding to each real node is between 100 and 200. For example, Nginx chooses to allocate 160 virtual nodes for each weight. The weight here is to distinguish servers. For example, a server with stronger processing power has a higher weight, which leads to more corresponding virtual nodes and a greater probability of being hit. +2. **Enhanced fault tolerance:** This is the most subtle thing about virtual nodes. When a physical node goes down, it is equivalent to multiple virtual nodes on the ring going offline at the same time. The data and traffic originally responsible for these virtual nodes will be naturally and evenly distributed to multiple other physical nodes on the ring to take over, without concentrating the pressure on a certain neighbor node. This greatly improves the stability and fault tolerance of the system. + +## Reference + +- In-depth analysis of Nginx Load balancing algorithm: +- Reading source code architecture series: consistent hashing: +- Summary of the principles of consistent Hash algorithm: \ No newline at end of file diff --git a/docs_en/distributed-system/protocol/gossip-protocl.en.md b/docs_en/distributed-system/protocol/gossip-protocl.en.md new file mode 100644 index 00000000000..803b31d8c0b --- /dev/null +++ b/docs_en/distributed-system/protocol/gossip-protocl.en.md @@ -0,0 +1,145 @@ +--- +title: Gossip 协议详解 +category: 分布式 +tag: + - 分布式协议&算法 + - 共识算法 +--- + +## 背景 + +在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。 + +一种比较简单粗暴的方法就是 **集中式发散消息**,简单来说就是一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。这种方法的缺陷也很明显,节点多的时候不光同步消息的效率低,还太依赖与中心节点,存在单点风险问题。 + +于是,**分散式发散消息** 的 **Gossip 协议** 就诞生了。 + +## Gossip 协议介绍 + +Gossip 直译过来就是闲话、流言蜚语的意思。流言蜚语有什么特点呢?容易被传播且传播速度还快,你传我我传他,然后大家都知道了。 + +![](./images/gossip/gossip.png) + +**Gossip 协议** 也叫 Epidemic 协议(流行病协议)或者 Epidemic propagation 算法(疫情传播算法),别名很多。不过,这些名字的特点都具有 **随机传播特性** (联想一下病毒传播、癌细胞扩散等生活中常见的情景),这也正是 Gossip 协议最主要的特点。 + +Gossip 协议最早是在 ACM 上的一篇 1987 年发表的论文 [《Epidemic Algorithms for Replicated Database Maintenance》](https://dl.acm.org/doi/10.1145/41840.41841)中被提出的。根据论文标题,我们大概就能知道 Gossip 协议当时提出的主要应用是在分布式数据库系统中各个副本节点同步数据。 + +正如 Gossip 协议其名一样,这是一种随机且带有传染性的方式将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。 + +在 Gossip 协议下,没有所谓的中心节点,每个节点周期性地随机找一个节点互相同步彼此的信息,理论上来说,各个节点的状态最终会保持一致。 + +下面我们来对 Gossip 协议的定义做一个总结:**Gossip 协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。** + +## Gossip 协议应用 + +NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等知名项目都用到了 Gossip 协议,学习 Gossip 协议有助于我们搞清很多技术的底层原理。 + +我们这里以 Redis Cluster 为例说明 Gossip 协议的实际应用。 + +我们经常使用的分布式缓存 Redis 的官方集群解决方案(3.0 版本引入) Redis Cluster 就是基于 Gossip 协议来实现集群中各个节点数据的最终一致性。 + +![Redis 的官方集群解决方案](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-fcacc1eefca6e51354a5f1fc9f2919f51ec.png) + +Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,Redis Cluster 中的各个节点基于 **Gossip 协议** 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。 + +Redis Cluster 的节点之间会相互发送多种 Gossip 消息: + +- **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 。 +- …… + +下图就是主从架构的 Redis Cluster 的示意图,图中的虚线代表的就是各个节点之间使用 Gossip 进行通信 ,实线表示主从复制。 + +![](./images/gossip/redis-cluster-gossip.png) + +有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议共享集群内信息。 + +关于 Redis Cluster 的详细介绍,可以查看这篇文章 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 。 + +## Gossip 协议消息传播模式 + +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. The speed is relatively fast. When the number of nodes is relatively large, the diffusion speed is faster than a master node disseminating information to other nodes (multicast). + +**Defects**: + +1. Messages need to be propagated to the entire network through multiple rounds of propagation. Therefore, the status of each node will inevitably be inconsistent. After all, the Gossip protocol emphasizes eventual consistency, and no one knows how long it will take to reach a consistent state of each node. + +2. Due to the Byzantine Generals Problem, malicious nodes are not allowed to exist. + +3. There may be message redundancy issues. Due to the random nature of message propagation, the same node may receive the same message repeatedly. + +## Summary + +- Gossip protocol is a communication protocol that allows sharing state in a distributed system, through this communication protocol we can disseminate information to all members of the network or cluster. +- The Gossip protocol is used by projects such as Redis, Apache Cassandra, and Consul. +- Rumor-Mongering is more suitable for scenarios where the number of nodes is large or nodes change dynamically. + +## Reference + +- A 10,000-word detailed explanation of the Redis Cluster Gossip protocol: +- "Distributed Protocols and Algorithms in Practice" +- "Redis Design and Implementation" + + \ No newline at end of file diff --git a/docs_en/distributed-system/protocol/paxos-algorithm.en.md b/docs_en/distributed-system/protocol/paxos-algorithm.en.md new file mode 100644 index 00000000000..d0ca0b8f051 --- /dev/null +++ b/docs_en/distributed-system/protocol/paxos-algorithm.en.md @@ -0,0 +1,84 @@ +--- +title: Paxos 算法详解 +category: 分布式 +tag: + - 分布式协议&算法 + - 共识算法 +--- + +## 背景 + +Paxos 算法是 Leslie Lamport([莱斯利·兰伯特](https://zh.wikipedia.org/wiki/莱斯利·兰伯特))在 **1990** 年提出了一种分布式系统 **共识** 算法。这也是第一个被证明完备的共识算法(前提是不存在拜占庭将军问题,也就是没有恶意节点)。 + +为了介绍 Paxos 算法,兰伯特专门写了一篇幽默风趣的论文。在这篇论文中,他虚拟了一个叫做 Paxos 的希腊城邦来更形象化地介绍 Paxos 算法。 + +不过,审稿人并不认可这篇论文的幽默。于是,他们就给兰伯特说:“如果你想要成功发表这篇论文的话,必须删除所有 Paxos 相关的故事背景”。兰伯特一听就不开心了:“我凭什么修改啊,你们这些审稿人就是缺乏幽默细胞,发不了就不发了呗!”。 + +于是乎,提出 Paxos 算法的那篇论文在当时并没有被成功发表。 + +直到 1998 年,系统研究中心 (Systems Research Center,SRC)的两个技术研究员需要找一些合适的分布式算法来服务他们正在构建的分布式系统,Paxos 算法刚好可以解决他们的部分需求。因此,兰伯特就把论文发给了他们。在看了论文之后,这俩大佬觉得论文还是挺不错的。于是,兰伯特在 **1998** 年重新发表论文 [《The Part-Time Parliament》](http://lamport.azurewebsites.net/pubs/lamport-paxos.pdf)。 + +论文发表之后,各路学者直呼看不懂,言语中还略显调侃之意。这谁忍得了,在 **2001** 年的时候,兰伯特专门又写了一篇 [《Paxos Made Simple》](http://lamport.azurewebsites.net/pubs/paxos-simple.pdf) 的论文来简化对 Paxos 的介绍,主要讲述两阶段共识协议部分,顺便还不忘嘲讽一下这群学者。 + +《Paxos Made Simple》这篇论文就 14 页,相比于 《The Part-Time Parliament》的 33 页精简了不少。最关键的是这篇论文的摘要就一句话: + +![](./images/paxos/paxos-made-simple.png) + +> The Paxos algorithm, when presented in plain English, is very simple. + +翻译过来的意思大概就是:当我用无修饰的英文来描述时,Paxos 算法真心简单! + +有没有感觉到来自兰伯特大佬满满地嘲讽的味道? + +## 介绍 + +Paxos 算法是第一个被证明完备的分布式系统共识算法。共识算法的作用是让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。提案的含义在分布式系统中十分宽泛,像哪一个节点是 Leader 节点、多个事件发生的顺序等等都可以是一个提案。 + +兰伯特当时提出的 Paxos 算法主要包含 2 个部分: + +- **Basic Paxos 算法**:描述的是多节点之间如何就某个值(提案 Value)达成共识。 +- **Multi-Paxos 思想**:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos 。 + +由于 Paxos 算法在国际上被公认的非常难以理解和实现,因此不断有人尝试简化这一算法。到了 2013 年才诞生了一个比 Paxos 算法更易理解和实现的共识算法—[Raft 算法](https://javaguide.cn/distributed-system/theorem&algorithm&protocol/raft-algorithm.html) 。更具体点来说,Raft 是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。 + +针对没有恶意节点的情况,除了 Raft 算法之外,当前最常用的一些共识算法比如 **ZAB 协议**、 **Fast Paxos** 算法都是基于 Paxos 算法改进的。 + +针对存在恶意节点的情况,一般使用的是 **工作量证明(POW,Proof-of-Work)**、 **权益证明(PoS,Proof-of-Stake )** 等共识算法。这类共识算法最典型的应用就是区块链,就比如说前段时间以太坊官方宣布其共识机制正在从工作量证明(PoW)转变为权益证明(PoS)。 + +区块链系统使用的共识算法需要解决的核心问题是 **拜占庭将军问题** ,这和我们日常接触到的 ZooKeeper、Etcd、Consul 等分布式中间件不太一样。 + +下面我们来对 Paxos 算法的定义做一个总结: + +- Paxos 算法是兰伯特在 **1990** 年提出了一种分布式系统共识算法。 +- 兰伯特当时提出的 Paxos 算法主要包含 2 个部分: Basic Paxos 算法和 Multi-Paxos 思想。 +- Raft 算法、ZAB 协议、 Fast Paxos 算法都是基于 Paxos 算法改进而来。 + +## Basic Paxos 算法 + +Basic Paxos 中存在 3 个重要的角色: + +1. **提议者(Proposer)**:也可以叫做协调者(coordinator),提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。 +2. **接受者(Acceptor)**:也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史; +3. **学习者(Learner)**:如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-890fa3212e8bf72886a595a34654918486c.png) + +为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。并且,一个提案被选定需要被半数以上的 Acceptor 接受。这样的话,Basic Paxos 算法还具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。 + +## Multi Paxos 思想 + +Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Multi Paxos 思想。 + +⚠️**注意**:Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。 + +由于兰伯特提到的 Multi-Paxos 思想缺少代码实现的必要细节(比如怎么选举领导者),所以在理解和实现上比较困难。 + +不过,也不需要担心,我们并不需要自己实现基于 Multi-Paxos 思想的共识算法,业界已经有了比较出名的实现。像 Raft 算法就是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现,实际项目中可以优先考虑 Raft 算法。 + +## 参考 + +- +- 分布式系统中的一致性与共识算法: + + + diff --git a/docs_en/distributed-system/protocol/raft-algorithm.en.md b/docs_en/distributed-system/protocol/raft-algorithm.en.md new file mode 100644 index 00000000000..914de02ad9d --- /dev/null +++ b/docs_en/distributed-system/protocol/raft-algorithm.en.md @@ -0,0 +1,171 @@ +--- +title: Raft 算法详解 +category: 分布式 +tag: + - 分布式协议&算法 + - 共识算法 +--- + +> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [Xieqijun](https://github.com/jun0315) 共同完成。 + +## 1 背景 + +当今的数据中心和应用程序在高度动态的环境中运行,为了应对高度动态的环境,它们通过额外的服务器进行横向扩展,并且根据需求进行扩展和收缩。同时,服务器和网络故障也很常见。 + +因此,系统必须在正常操作期间处理服务器的上下线。它们必须对变故做出反应并在几秒钟内自动适应;对客户来说的话,明显的中断通常是不可接受的。 + +幸运的是,分布式共识可以帮助应对这些挑战。 + +### 1.1 拜占庭将军 + +在介绍共识算法之前,先介绍一个简化版拜占庭将军的例子来帮助理解共识算法。 + +> 假设多位拜占庭将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成是否要进攻的一致性决定? + +解决方案大致可以理解成:先在所有的将军中选出一个大将军,用来做出所有的决定。 + +举例如下:假如现在一共有 3 个将军 A,B 和 C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就把自己当成大将军候选人,然后派信使传递选举投票的信息给将军 B 和 C,如果将军 B 和 C 还没有把自己当作候选人(自己的倒计时还没有结束),并且没有把选举票投给其他人,它们就会把票投给将军 A,信使回到将军 A 时,将军 A 知道自己收到了足够的票数,成为大将军。在有了大将军之后,是否需要进攻就由大将军 A 决定,然后再去派信使通知另外两个将军,自己已经成为了大将军。如果一段时间还没收到将军 B 和 C 的回复(信使可能会被暗杀),那就再重派一个信使,直到收到回复。 + +### 1.2 共识算法 + +共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。 + +共识算法允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组`Server`的状态机计算相同状态的副本,即使有一部分的`Server`宕机了它们仍然能够继续运行。 + +![rsm-architecture.png](https://oss.javaguide.cn/github/javaguide/paxos-rsm-architecture.png) + +`图-1 复制状态机架构` + +一般通过使用复制日志来实现复制状态机。每个`Server`存储着一份包括命令序列的日志文件,状态机会按顺序执行这些命令。因为每个日志包含相同的命令,并且顺序也相同,所以每个状态机处理相同的命令序列。由于状态机是确定性的,所以处理相同的状态,得到相同的输出。 + +因此共识算法的工作就是保持复制日志的一致性。服务器上的共识模块从客户端接收命令并将它们添加到日志中。它与其他服务器上的共识模块通信,以确保即使某些服务器发生故障。每个日志最终包含相同顺序的请求。一旦命令被正确地复制,它们就被称为已提交。每个服务器的状态机按照日志顺序处理已提交的命令,并将输出返回给客户端,因此,这些服务器形成了一个单一的、高度可靠的状态机。 + +适用于实际系统的共识算法通常具有以下特性: + +- 安全。确保在非拜占庭条件(也就是上文中提到的简易版拜占庭)下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。 +- 高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。因此,一个典型的由五台服务器组成的集群可以容忍任何两台服务器端故障。假设服务器因停止而发生故障;它们稍后可能会从稳定存储上的状态中恢复并重新加入集群。 +- 一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。 + +- 在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。 + +## 2 基础 + +### 2.1 节点类型 + +一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个: + +- `Leader`:负责发起心跳,响应客户端,创建日志,同步日志。 +- `Candidate`:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。 +- `Follower`:接受 Leader 的心跳和日志同步数据,投票给 Candidate。 + +在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。 + +![](https://oss.javaguide.cn/github/javaguide/paxos-server-state.png) + +`图-2:服务器的状态` + +### 2.2 任期 + +![](https://oss.javaguide.cn/github/javaguide/paxos-term.png) + +`图-3:任期` + +如图 3 所示,raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。raft 算法保证在给定的一个任期最少要有一个 Leader。 + +每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号;如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。 + +### 2.3 日志 + +- `entry`:每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为``其中 cmd 是可以应用到状态机的操作。 +- `log`:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。 + +## 3 领导人选举 + +raft 使用心跳机制来触发 Leader 的选举。 + +如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。 + +Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做选举超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。 + +为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生: + +- 赢得选举 +- 其他节点赢得选举 +- 一轮选举结束,无人胜出 + +赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票`(N/2+1)`,就可以成为 Leader。 + +在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况: + +- 该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。 +- 该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。 + +由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。 + +raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。 + +## 4 日志复制 + +一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(`Replicated State Machine`)执行的命令。 + +Leader 收到客户端请求后,会生成一个 entry,包含``,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。 + +If the Follower accepts the entry, it will add the entry to the end of its own log and return it to the Leader for approval. + +If the Leader receives a majority of successful responses, the Leader will apply the entry to its own state machine, and then call the entry committed and return the execution result to the client. + +raft guarantees the following two properties: + +- In two logs, if there are two entries with the same index and term, then they must have the same cmd +- In two logs, if there are two entries with the same index and term, then the entries in front of them must also be the same. + +The first property is guaranteed by "only the Leader can generate entries", and the second property requires a consistency check to ensure it. + +Under normal circumstances, the logs of the Leader and Follower are consistent. However, the crash of the Leader will cause the logs to be different, so the consistency check will fail. Leader handles log inconsistencies by forcing Followers to copy their own logs. This means that the conflict log on the Follower will be overwritten by the leader's log. + +In order to make the Follower's log consistent with its own log, the Leader needs to find the place where the Follower's log is consistent with its own log, then delete the Follower's log after that position, and then send the following log to the Follower. + +`Leader` maintains a `nextIndex` for each `Follower`, which represents the index of the next log entry that `Leader` will send to the follower. When a `Leader` takes power, it initializes `nextIndex` to its latest log entry index + 1. If the log of a `Follower` is inconsistent with that of the `Leader`, the `AppendEntries` consistency check will return failure at the next `AppendEntries RPC`. After a failure, `Leader` will decrement `nextIndex` and retry `AppendEntries RPC`. Eventually `nextIndex` will reach a place where `Leader` and `Follower` logs are consistent. At this point, `AppendEntries` will return successfully, the conflicting log entries in `Follower` will be removed, and the missing `Leader` log entries will be added. Once `AppendEntries` returns successfully, the logs of `Follower` and `Leader` are consistent, and this state will remain until the end of the term. + +## 5 Security + +### 5.1 Election restrictions + +The Leader needs to ensure that it stores all submitted log entries. This allows log entries to flow in only one direction: from Leader to Follower, and Leader will never overwrite existing log entries. + +When each Candidate sends RequestVoteRPC, it will bring the last entry information. When all nodes receive the voting information, they will compare the entry. If they find their own updates, they will refuse to vote for the candidate. + +The way to judge the old and new logs: if the terms of the two logs are different, the larger term is updated; if the terms are the same, the longer index is updated. + +### 5.2 Node crash + +If the Leader crashes and the nodes in the cluster do not receive the Leader's heartbeat information within the electionTimeout time, a new round of leader election will be triggered. During the leader election period, the entire cluster will be unavailable to the outside world. + +If Follower and Candidate crash, the handling will be much simpler. Subsequent RequestVoteRPC and AppendEntriesRPC sent to it will fail. Since all raft requests are idempotent, they will be retried infinitely if they fail. If the crash is recovered, you can receive new requests and choose to append or reject the entry. + +### 5.3 Timing and Availability + +One of the requirements of raft is that security does not depend on time: the system cannot fail just because some events occur faster or slower than expected. In order to ensure the above requirements, it is best to meet the following time conditions: + +`broadcastTime << electionTimeout << MTBF` + +- `broadcastTime`: the average response time for concurrently sending messages to other nodes; +- `electionTimeout`: election timeout; +- `MTBF(mean time between failures)`: the average health time of a single machine; + +`broadcastTime` should be an order of magnitude smaller than `electionTimeout`, in order to enable `Leader` to continuously send heartbeat information (heartbeat) to prevent `Follower` from starting an election; + +`electionTimeout` is also several orders of magnitude smaller than `MTBF` in order to make the system run stably. When the `Leader` crashes, it will be unavailable for approximately the entire `electionTimeout`; we hope that this will only be a small fraction of the total time. + +Since `broadcastTime` and `MTBF` are properties determined by the system, the time of `electionTimeout` needs to be determined. + +Generally speaking, broadcastTime is generally `0.5~20ms`, electionTimeout can be set to `10~500ms`, and MTBF is generally one or two months. + +## 6 Reference + +- +- +- +- + + \ No newline at end of file diff --git a/docs_en/distributed-system/rpc/dubbo.en.md b/docs_en/distributed-system/rpc/dubbo.en.md new file mode 100644 index 00000000000..cc14665ee07 --- /dev/null +++ b/docs_en/distributed-system/rpc/dubbo.en.md @@ -0,0 +1,459 @@ +--- +title: Dubbo常见问题总结 +category: 分布式 +tag: + - rpc +--- + +::: tip + +- Dubbo3 已经发布,这篇文章是基于 Dubbo2 写的。Dubbo3 基于 Dubbo2 演进而来,在保持原有核心功能特性的同时, Dubbo3 在易用性、超大规模微服务实践、云原生基础设施适配、安全设计等几大方向上进行了全面升级。 +- 本文中的很多链接已经失效,主要原因是因为 Dubbo 官方文档进行了修改导致 URL 失效。 + +::: + +这篇文章是我根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。欢迎补充! + +## Dubbo 基础 + +### 什么是 Dubbo? + +![Dubbo 官网](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 框架。 + +根据 [Dubbo 官方文档](https://dubbo.apache.org/zh/)的介绍,Dubbo 提供了六大核心能力 + +1. 面向接口代理的高性能 RPC 调用。 +2. 智能容错和负载均衡。 +3. 服务自动注册和发现。 +4. 高度可扩展能力。 +5. 运行期流量调度。 +6. 可视化的服务治理与运维。 + +![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 不光可以帮助我们调用远程服务,还提供了一些其他开箱即用的功能比如智能负载均衡。** + +Dubbo 目前已经有接近 34.4 k 的 Star 。 + +在 **2020 年度 OSC 中国开源项目** 评选活动中,Dubbo 位列开发框架和基础组件类项目的第 7 名。相比几年前来说,热度和排名有所下降。 + +![](https://oss.javaguide.cn/%E6%BA%90%E7%A0%81/dubbo/image-20210107153159545.png) + +Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 + +### 为什么要用 Dubbo? + +随着互联网的发展,网站的规模越来越大,用户数量越来越多。单一应用架构、垂直应用架构无法满足我们的需求,这个时候分布式服务架构就诞生了。 + +分布式服务架构下,系统被拆分成不同的服务比如短信服务、安全服务,每个服务独立提供系统的某个核心服务。 + +我们可以使用 Java RMI(Java Remote Method Invocation)、Hessian 这种支持远程调用的框架来简单地暴露和引用远程服务。但是!当服务越来越多之后,服务调用关系越来越复杂。当应用访问压力越来越大后,负载均衡以及服务监控的需求也迫在眉睫。我们可以用 F5 这类硬件来做负载均衡,但这样增加了成本,并且存在单点故障的风险。 + +不过,Dubbo 的出现让上述问题得到了解决。**Dubbo 帮助我们解决了什么问题呢?** + +1. **负载均衡**:同一个服务部署在不同的机器时该调用哪一台机器上的服务。 +2. **服务调用链路生成**:随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 +3. **服务访问压力以及时长统计、资源调度和治理**:基于访问压力实时管理集群容量,提高集群利用率。 +4. …… + +![Dubbo 能力概览](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo-features-overview.jpg) + +另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 + +**我们刚刚提到了分布式这个概念,下面再给大家介绍一下什么是分布式?为什么要分布式?** + +## 分布式基础 + +### 什么是分布式? + +分布式或者说 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) + +The official document introduces this part of load balancing in great detail. It is recommended that friends take a look at the address: [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 + +Random selection based on weights (an implementation of the weighted random algorithm). This is a load balancing strategy adopted by Dubbo by default. + +The specific implementation principle of `RandomLoadBalance` is very simple. Suppose there are two servers S1 and S2 that provide the same service. The weight of S1 is 7 and the weight of S2 is 3. + +When we distribute these weight values ​​in the coordinate interval, we will get: S1->[0, 7), S2->[7, 10). We generate a random number between [0, 10), and when the random number falls into the corresponding interval, we select the corresponding server to handle the request. + +![RandomLoadBalance](https://oss.javaguide.cn/java-guide-blog/%20RandomLoadBalance.png) + +The source code of `RandomLoadBalance` is very simple, just take a few minutes to look at it. + +> The following source code comes from the latest version 2.7.9 on the Dubbo master branch. + +```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; + //The main function of the following for loop is to calculate the sum of the weights of all providers of the service totalWeight(), + // In addition, it will also check whether the weight of each service provider is the same + 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) { + // Randomly generate a number in the range [0, totalWeight) + int offset = ThreadLocalRandom.current().nextInt(totalWeight); + // Determine which service provider range it will fall into + for (int i = 0; i < length; i++) { + if (offset < weights[i]) { + return invokers.get(i); + } + } + + return invokers.get(ThreadLocalRandom.current().nextInt(length)); + } + +} + +``` + +#### LeastActiveLoadBalance + +`LeastActiveLoadBalance` literally translates to **least active number load balancing**. + +This name is a bit unintuitive. If you don’t read the official definition of active number carefully, you won’t know what this thing is for. + +Let me put it this way! In the initial state, the activity number of all service providers is 0 (specific methods of each service provider correspond to an activity number, which I will mention in the source code later). After each request is received, the activity number of the corresponding service provider is +1. When the request is processed, the activity number is -1. + +Therefore, **Dubbo believes that whoever has fewer active numbers will have faster processing speed and better performance. In this case, I will give priority to the request to the service provider with fewer active numbers. ** + +**What if there are multiple service providers with equal active numbers? ** + +Very simple, just go through `RandomLoadBalance` again. + +```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(); + leastActive = -1; + leastCount = 0; + int[] leastIndexes = new int[length]; + int[] weights = new int[length]; + int totalWeight = 0; + int firstWeight = 0; + boolean sameWeight = true; + //The main function of this for loop is to traverse the invokers list and find the Invoker with the smallest active number + // If there are multiple Invokers with the same minimum active number, the subscripts of these Invokers in the invokers collection will also be recorded, their weights will be accumulated, and their weight values will be compared to see if they are equal. + for (int i = 0; i < length; i++) { + Invoker invoker = invokers.get(i); + // Get the active number corresponding to invoker + 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; + } + } + } + // If there is only one Invoker with the minimum active number, just return the Invoker directly. + if (leastCount == 1) { + return invokers.get(leastIndexes[0]); + } + // If there are multiple Invokers with the same minimum active number, but different weights between them + //The processing method here is consistent with 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)]); + } +}``` + +The activity number is saved through a `ConcurrentMap` in `RpcStatus`. According to the URL and the name of the method called by the service provider, we can get the corresponding activity number. That is to say, the activity number of each method in the service provider is independent of each other. + +```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` Friends should be familiar with it. This load balancing strategy is often used in sub-databases, tables, and various clusters. + +`ConsistentHashLoadBalance` is the **consistent Hash load balancing strategy**. There is no concept of weight in `ConsistentHashLoadBalance`. Which service provider handles the request is determined by the parameters of your request. That is to say, requests with the same parameters are always sent to the same service provider. + +![](https://oss.javaguide.cn/java-guide-blog/consistent-hash-data-incline.jpg) + +In addition, Dubbo also introduces the concept of virtual nodes in order to avoid data skew problems (nodes are not dispersed enough and a large number of requests fall on the same node). Virtual nodes can make nodes more dispersed and effectively balance the request volume of each node. + +![](https://oss.javaguide.cn/java-guide-blog/consistent-hash-invoker.jpg) + +The official has detailed source code analysis: [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). There is also a related [PR#5440](https://github.com/apache/dubbo/pull/5440) to fix some bugs in ConsistentHashLoadBalance in the old version. Interested friends can spend more time researching it. I won’t analyze any more here, I’ll leave this homework to you! + +#### RoundRobinLoadBalance + +Weighted round-robin load balancing. + +Polling is to assign requests to each service provider in turn. Weighted polling is based on polling, allowing more requests to fall on service providers with greater weight. For example, if there are two servers S1 and S2 that provide the same service, the weight of S1 is 7 and the weight of S2 is 3. + +If we have 10 requests, 7 will be processed by S1 and 3 by S2. + +However, if it is `RandomLoadBalance`, it is likely that 9 out of 10 requests will be processed by S1 (a probabilistic problem). + +The code implementation of `RoundRobinLoadBalance` in Dubbo has been modified and rebuilt several times. Dubbo-2.6.5 version of `RoundRobinLoadBalance` is a smooth weighted polling algorithm. + +## Dubbo serialization protocol + +### What serialization methods does Dubbo support? + +![Dubbo Supported serialization protocols](https://oss.javaguide.cn/github/javaguide/csdn/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_1 0,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70-20230309234143460.png) + +Dubbo supports multiple serialization methods: JDK's own serialization, hessian2, JSON, Kryo, FST, Protostuff, ProtoBuf, etc. + +The default serialization method used by Dubbo is hessian2. + +### Tell us what you know about these serialization protocols? + +Generally we do not directly use the serialization method that comes with the JDK. There are two main reasons: + +1. **Cross-language calling is not supported**: If you are calling services developed in other languages, it is not supported. +2. **Poor performance**: Compared with other serialization frameworks, the performance is lower. The main reason is that the byte array after serialization is larger in size, resulting in increased transmission costs. + +We generally do not consider using JSON serialization due to performance issues. + +Protostuff, ProtoBuf, and hessian2 are all cross-language serialization methods. You can consider using them if you have cross-language requirements. + +The two serialization methods Kryo and FST were introduced by Dubbo later and have very good performance. However, both are specific to the Java language. An article on the Dubbo official website mentioned that it is recommended to use Kryo as the serialization method for production environments. + +There is also a performance comparison chart of these [serialization protocols] (https://dubbo.apache.org/zh/docs/v2.7/user/serialization/#m-zhdocsv27userserialization) in Dubbo’s official documentation for reference. + +![Performance comparison of serialization protocols](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/dubbo-serialization-protocol-performance-comparison.png) + + \ No newline at end of file diff --git a/docs_en/distributed-system/rpc/http&rpc.en.md b/docs_en/distributed-system/rpc/http&rpc.en.md new file mode 100644 index 00000000000..a8e5e4491de --- /dev/null +++ b/docs_en/distributed-system/rpc/http&rpc.en.md @@ -0,0 +1,197 @@ +--- +title: 有了 HTTP 协议,为什么还要有 RPC ? +category: 分布式 +tag: + - rpc +--- + +> 本文来自[小白 debug](https://juejin.cn/user/4001878057422087)投稿,原文: 。 + +我想起了我刚工作的时候,第一次接触 RPC 协议,当时就很懵,我 HTTP 协议用的好好的,为什么还要用 RPC 协议? + +于是就到网上去搜。 + +不少解释显得非常官方,我相信大家在各种平台上也都看到过,解释了又好像没解释,都在**用一个我们不认识的概念去解释另外一个我们不认识的概念**,懂的人不需要看,不懂的人看了还是不懂。 + +这种看了,又好像没看的感觉,云里雾里的很难受,**我懂**。 + +为了避免大家有强烈的**审丑疲劳**,今天我们来尝试重新换个方式讲一讲。 + +## 从 TCP 聊起 + +作为一个程序员,假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 socket 进行编程。 + +这时候,我们可选项一般也就**TCP 和 UDP 二选一。TCP 可靠,UDP 不可靠。** 除非是马总这种神级程序员(早期 QQ 大量使用 UDP),否则,只要稍微对可靠性有些要求,普通人一般无脑选 TCP 就对了。 + +类似下面这样。 + +```ini +fd = socket(AF_INET,SOCK_STREAM,0); +``` + +其中`SOCK_STREAM`,是指使用**字节流**传输数据,说白了就是**TCP 协议**。 + +在定义了 socket 之后,我们就可以愉快的对这个 socket 进行操作,比如用`bind()`绑定 IP 端口,用`connect()`发起建连。 + +![握手建立连接流程](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/f410977cda814d32b0eff3645c385a8a~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) + +在连接建立之后,我们就可以使用`send()`发送数据,`recv()`接收数据。 + +光这样一个纯裸的 TCP 连接,就可以做到收发数据了,那是不是就够了? + +不行,这么用会有问题。 + +## 使用纯裸 TCP 会有什么问题 + +八股文常背,TCP 是有三个特点,**面向连接**、**可靠**、基于**字节流**。 + +![TCP是什么](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/acb4508111cb47d8a3df6734d04818bc~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) + +这三个特点真的概括的 **非常精辟** ,这个八股文我们没白背。 + +每个特点展开都能聊一篇文章,而今天我们需要关注的是 **基于字节流** 这一点。 + +字节流可以理解为一个双向的通道里流淌的二进制数据,也就是 **01 串** 。纯裸 TCP 收发的这些 01 串之间是 **没有任何边界** 的,你根本不知道到哪个地方才算一条完整消息。 + +![01二进制字节流](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/b82d4fcdd0c4491e979856c93c1750d7~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) + +正因为这个没有任何边界的特点,所以当我们选择使用 TCP 发送 **"夏洛"和"特烦恼"** 的时候,接收端收到的就是 **"夏洛特烦恼"** ,这时候接收端没发区分你是想要表达 **"夏洛"+"特烦恼"** 还是 **"夏洛特"+"烦恼"** 。 + +![消息对比](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)聊过这个问题。 + +说这个的目的是为了告诉大家,纯裸 TCP 是不能直接拿来用的,你需要在这个基础上加入一些 **自定义的规则** ,用于区分 **消息边界** 。 + +于是我们会把每条要发送的数据都包装一下,比如加入 **消息头** ,消息头里写清楚一个完整的包长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的 **消息体** 。 + +![消息边界长度标志](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/cb29659d4907446e9f70551c44c6369f~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp.png) + +而这里头提到的 **消息头** ,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的 **协议。** + +每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,他们可能 **有区别,但原理都类似**。 + +**于是基于 TCP,就衍生了非常多的协议,比如 HTTP 和 RPC。** + +## HTTP 和 RPC + +### RPC 其实是一种调用方式 + +我们回过头来看网络的分层图。 + +![四层网络协议](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 协议,它们都只是定义了不同消息格式的 **应用层协议** 而已。 + +**HTTP**(**H**yper **T**ext **T**ransfer **P**rotocol)协议又叫做 **超文本传输协议** 。我们用的比较多,平时上网在浏览器上敲个网址就能访问网页,这里用到的就是 HTTP 协议。 + +![HTTP调用](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)又叫做 **远程过程调用**,它本身并不是一个具体的协议,而是一种 **调用方式** 。 + +举个例子,我们平时调用一个 **本地方法** 就像下面这样。 + +```ini + res = localFunc(req) +``` + +如果现在这不是个本地方法,而是个**远端服务器**暴露出来的一个方法`remoteFunc`,如果我们还能像调用本地方法那样去调用它,这样就可以**屏蔽掉一些网络细节**,用起来更方便,岂不美哉? + +```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 协议,比如比较有名的`gRPC`,`thrift`。 + +值得注意的是,虽然大部分 RPC 协议底层使用 TCP,但实际上 **它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。** + +到这里,我们回到文章标题的问题。 + +### 那既然有 RPC 了,为什么还要有 HTTP 呢? + +其实,TCP 是 **70 年** 代出来的协议,而 HTTP 是 **90 年代** 才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有 **80 年代** 出来的`RPC`。 + +所以我们该问的不是 **既然有 HTTP 协议为什么要有 RPC** ,而是 **为什么有 RPC 还要有 HTTP 协议?** + +现在电脑上装的各种联网软件,比如 xx 管家,xx 卫士,它们都作为客户端(Client) 需要跟服务端(Server) 建立连接收发消息,此时都会用到应用层协议,在这种 Client/Server (C/S) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。 + +但有个软件不同,浏览器(Browser) ,不管是 Chrome 还是 IE,它们不仅要能访问自家公司的**服务器(Server)** ,还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 **Browser/Server (B/S)** 的协议。 + +也就是说在多年以前,**HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。** 很多软件同时支持多端,比如某度云盘,既要支持**网页版**,还要支持**手机端和 PC 端**,如果通信协议都用 HTTP 的话,那服务器只用同一套就够了。而 RPC 就开始退居幕后,一般用于公司内部集群里,各个微服务之间的通讯。 + +那这么说的话,**都用 HTTP 得了,还用什么 RPC?** + +仿佛又回到了文章开头的样子,那这就要从它们之间的区别开始说起。 + +### HTTP 和 RPC 有什么区别 + +我们来看看 RPC 和 HTTP 区别比较明显的几个点。 + +#### 服务发现 + +首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 **IP 地址和端口** 。这个找到服务对应的 IP 端口的过程,其实就是 **服务发现**。 + +在 **HTTP** 中,你知道服务的域名,就可以通过 **DNS 服务** 去解析得到它背后的 IP 地址,默认 **80 端口**。 + +而 **RPC** 的话,就有些区别,一般会有专门的中间服务去保存服务名和 IP 信息,比如 **Consul、Etcd、Nacos、ZooKeeper,甚至是 Redis**。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。由于 DNS 也是服务发现的一种,所以也有基于 DNS 去做服务发现的组件,比如 **CoreDNS**。 + +可以看出服务发现这一块,两者是有些区别,但不太能分高低。 + +#### 底层连接形式 + +以主流的 **HTTP1.1** 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(**keep alive**),之后的请求和响应都会复用这条连接。 + +而 **RPC** 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个 **连接池**,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。 + +![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 就是这么干的。 + +可以看出这一块两者也没太大区别,所以也不是关键。 + +#### 传输的内容 + +基于 TCP 传输的消息,说到底,无非都是 **消息头 Header 和消息体 Body。** + +**Header** 是用于标记一些特殊信息,其中最重要的是 **消息体长度**。 + +**Body** 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 **JSON,Protocol Buffers (Protobuf)** 。 + +这个将结构体转为二进制数组的过程就叫 **序列化** ,反过来将二进制数组复原成结构体的过程叫 **反序列化**。 + +![序列化和反序列化](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** 来 **序列化** 结构体数据。 + +我们可以随便截个图直观看下。 + +![HTTP报文](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 结构里也特别明显。 + +而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。**因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。** + +![HTTP原理](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) + +当然上面说的 HTTP,其实 **特指的是现在主流使用的 HTTP1.1**,`HTTP2`在前者的基础上做了很多改进,所以 **性能可能比很多 RPC 协议还要好**,甚至连`gRPC`底层都直接用的`HTTP2`。 + +那么问题又来了。 + +### 为什么既然有了 HTTP2,还要有 RPC 协议? + +这个是由于 HTTP2 是 2015 年出来的。那时候很多公司内部的 RPC 协议都已经跑了好些年了,基于历史原因,一般也没必要去换了。 + +## 总结 + +- 纯裸 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。 + + + diff --git a/docs_en/distributed-system/rpc/rpc-intro.en.md b/docs_en/distributed-system/rpc/rpc-intro.en.md new file mode 100644 index 00000000000..10b28b2f1ac --- /dev/null +++ b/docs_en/distributed-system/rpc/rpc-intro.en.md @@ -0,0 +1,140 @@ +--- +title: RPC基础知识总结 +category: 分布式 +tag: + - rpc +--- + +这篇文章会简单介绍一下 RPC 相关的基础概念。 + +## RPC 是什么? + +**RPC(Remote Procedure Call)** 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。 + +**为什么要 RPC ?** 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP 还是 UDP)、序列化方式等等方面。 + +**RPC 能帮助我们做什么呢?** 简单来说,通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且!我们不需要了解底层网络编程的具体细节。 + +举个例子:两个不同的服务 A、B 部署在两台不同的机器上,服务 A 如果想要调用服务 B 中的某个方法的话就可以通过 RPC 来做。 + +一言蔽之:**RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。** + +## RPC 的原理是什么? + +为了能够帮助小伙伴们理解 RPC 原理,我们可以将整个 RPC 的 核心功能看作是下面 👇 5 个部分实现的: + +1. **客户端(服务消费端)**:调用远程方法的一端。 +1. **客户端 Stub(桩)**:这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。 +1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。 +1. **服务端 Stub(桩)**:这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。 +1. **服务端(服务提供端)**:提供远程方法的一端。 + +具体原理图如下,后面我会串起来将整个 RPC 的过程给大家说一下。 + +![RPC原理图](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! + +相信小伙伴们看完上面的讲解之后,已经了解了 RPC 的原理。 + +虽然篇幅不多,但是基本把 RPC 框架的核心原理讲清楚了!另外,对于上面的技术细节,我会在后面的章节介绍到。 + +**最后,对于 RPC 的原理,希望小伙伴不单单要理解,还要能够自己画出来并且能够给别人讲出来。因为,在面试中这个问题在面试官问到 RPC 相关内容的时候基本都会碰到。** + +## 有哪些常见的 RPC 框架? + +我们这里说的 RPC 框架指的是可以让客户端直接调用服务端方法,就像调用本地方法一样简单的框架,比如我下面介绍的 Dubbo、Motan、gRPC 这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如 Feign。 + +### Dubbo + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716111053081.png) + +Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案, +涵盖 Java、Golang 等多种语言 SDK 实现。 + +Dubbo 提供了从服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716111545343.png) + +Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 + +Dubbo 算的是比较优秀的国产开源项目了,它的源码也是非常值得学习和阅读的! + +- GitHub:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo "https://github.com/apache/incubator-dubbo") +- 官网: + +### Motan + +Motan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。不过笔者倒是很少看到有公司使用,而且网上的资料也比较少。 + +很多人喜欢拿 Motan 和 Dubbo 作比较,毕竟都是国内大公司开源的。笔者在查阅了很多资料,以及简单查看了其源码之后发现:**Motan 更像是一个精简版的 Dubbo,可能是借鉴了 Dubbo 的思想,Motan 的设计更加精简,功能更加纯粹。** + +不过,我不推荐你在实际项目中使用 Motan。如果你要是公司实际使用的话,还是推荐 Dubbo ,其社区活跃度以及生态都要好很多。 + +- 从 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") + +### gRPC + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/2843b10d-0c2f-4b7e-9c3e-ea4466792a8b.png) + +gRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。其由主要面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议开发,并且支持众多开发语言。 + +**何谓 ProtoBuf?** [ProtoBuf( Protocol Buffer)](https://github.com/protocolbuffers/protobuf) 是一种更加灵活、高效的数据格式,可用于通讯协议、数据存储等领域,基本支持所有主流编程语言且与平台无关。不过,通过 ProtoBuf 定义接口和数据类型还挺繁琐的,这是一个小问题。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/image-20220716104304033.png) + +不得不说,gRPC 的通信层的设计还是非常优秀的,[Dubbo-go 3.0](https://dubbogo.github.io/) 的通信层改进主要借鉴了 gRPC。 + +不过,gRPC 的设计导致其几乎没有服务治理能力。如果你想要解决这个问题的话,就需要依赖其他组件比如腾讯的 PolarisMesh(北极星)了。 + +- 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/") + +### Thrift +Apache Thrift is Facebook's open source cross-language RPC communication framework. It has been donated to the Apache Foundation for management. Due to its cross-language features and excellent performance, it is used by many Internet companies. Competent companies will even develop a distributed service framework based on Thrift to add functions such as service registration and service discovery. + +`Thrift` supports a variety of **programming languages**, including `C++`, `Java`, `Python`, `PHP`, `Ruby`, etc. (compared to gRPC, it supports more languages). + +- Official website: [https://thrift.apache.org/](https://thrift.apache.org/ "https://thrift.apache.org/") +- Brief introduction to Thrift: [https://www.jianshu.com/p/8f25d057a5a9](https://www.jianshu.com/p/8f25d057a5a9 "https://www.jianshu.com/p/8f25d057a5a9") + +### Summary + +Although gRPC and Thrift support cross-language RPC calls, they only provide the most basic RPC framework functions and lack the support of a series of supporting service components and service governance functions. + +Dubbo is the best in terms of functionality, ecosystem, and community activity. Moreover, Dubbo has many successful cases in China such as Dangdang, Didi, etc. It is a mature and stable RPC framework that can withstand the test of production. The most important thing is that you can also find a lot of Dubbo reference materials, and the learning cost is relatively low. + +The diagram below shows Dubbo’s ecosystem. + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/eee98ff2-8e06-4628-a42b-d30ffcd2831e.png) + +Dubbo is also a component in Spring Cloud Alibaba. + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/0d195dae-72bc-4956-8451-3eaf6dd11cbd.png) + +However, Dubbo and Motan are mainly used by the Java language. Although Dubbo and Motan are currently compatible with some languages, they are not recommended. If you need to call across multiple languages, consider using gRPC. + +In summary, if it is a Java back-end technology stack and you are struggling with which RPC framework to choose, I recommend you to consider Dubbo. + +## How to design and implement an RPC framework? + +**"Handwritten RPC Framework"** is an internal booklet of my [Knowledge Planet] (https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html). I wrote 12 articles to explain how to implement a simple RPC framework from scratch based on Netty+Kyro+Zookeeper. + +Although Sparrow is small, it has all the necessary features. The project code has detailed comments and clear structure. It also integrates the Check Style standard code structure, making it very suitable for reading and learning. + +**Content Overview**: + +![](https://oss.javaguide.cn/github/javaguide/image-20220308100605485.png) + +## Now that we have the HTTP protocol, why do we need RPC? + +For a detailed answer to this question, please see this article: [With HTTP protocol, why do we need RPC? ](http&rpc.md) . + + \ No newline at end of file diff --git a/docs_en/distributed-system/spring-cloud-gateway-questions.en.md b/docs_en/distributed-system/spring-cloud-gateway-questions.en.md new file mode 100644 index 00000000000..f55eaf16c2e --- /dev/null +++ b/docs_en/distributed-system/spring-cloud-gateway-questions.en.md @@ -0,0 +1,157 @@ +--- +title: Spring Cloud Gateway常见问题总结 +category: 分布式 +--- + +> 本文重构完善自[6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理 - 悟空聊架构](https://mp.weixin.qq.com/s/XjFYsP1IUqNzWqXZdJn-Aw)这篇文章。 + +## 什么是 Spring Cloud Gateway? + +Spring Cloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul**。准确点来说,应该是 Zuul 1.x。Spring Cloud Gateway 起步要比 Zuul 2.x 更早。 + +为了提升网关的性能,Spring Cloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 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 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。 + +- GitHub 地址: +- 官网: + +## Spring Cloud Gateway 的工作流程? + +Spring Cloud Gateway 的工作流程如下图所示: + +![Spring Cloud Gateway 的工作流程](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-workflow.png) + +这是 Spring 官方博客中的一张图,原文地址:。 + +具体的流程分析: + +1. **路由判断**:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。 +2. **请求过滤**:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。 +3. **服务处理**:后端服务会对请求进行处理。 +4. **响应过滤**:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。 +5. **响应返回**:响应经过过滤处理后,返回给客户端。 + +总结:客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。 + +## Spring Cloud Gateway 的断言是什么? + +断言(Predicate)这个词听起来极其深奥,它是一种编程术语,我们生活中根本就不会用它。说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。 + +在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。 + +断言配置的示例如下,配置了两个路由规则,有一个 predicates 断言配置,当请求 url 中包含 `api/thirdparty`,就匹配到了第一个路由 `route_thirdparty`。 + +![断言配置示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-example.png) + +常见的路由断言规则如下图所示: + +![Spring Cloud GateWay 路由断言规则](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-rules.png) + +## Spring Cloud Gateway 的路由和断言是什么关系? + +Route 路由和 Predicate 断言的对应关系如下:: + +![路由和断言的对应关系](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。 + +## Spring Cloud Gateway 如何实现动态路由? + +在使用 Spring Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件或代码配置的方式。 + +Spring Cloud Gateway 作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。 + +实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据(例如服务名称、路径等),然后根据这些信息自动生成路由规则。这样,当你添加、移除或更新服务实例时,网关会自动感知并相应地调整路由规则,无需手动维护路由配置。 + +其实这些复杂的步骤并不需要我们手动实现,通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更,官方文档地址: 。 + +## Spring Cloud Gateway 的过滤器有哪些? + +过滤器 Filter 按照请求和响应可以分为两种: + +- **Pre 类型**:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 +- **Post 类型**:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。 + +另外一种分类是按照过滤器 Filter 作用的范围进行划分: + +- **GatewayFilter**:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。 +- **GlobalFilter**:全局过滤器,应用在所有路由上的过滤器。 + +### 局部过滤器 + +常见的局部过滤器如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-gatewayfilters.png) + +具体怎么用呢?这里有个示例,如果 URL 匹配成功,则去掉 URL 中的 “api”。 + +```yaml +filters: #过滤器 + - RewritePath=/api/(?.*),/$\{segment} # 将跳转路径中包含的 “api” 替换成空 +``` + +Of course, we can also customize filters, which we will not expand on in this article. + +### Global filter + +Common global filters are shown below: + +![](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 # Third-party microservice routing rules + uri: lb://passjava-member # Load balancing, forward the request to the passjava-member service registered in the registration center + predicates: # assertion + - Path=/api/member/** # If the front-end request path contains api/member, this routing rule will be applied + filters: #Filter + - RewritePath=/api/(?.*),/$\{segment} # Replace the api contained in the jump path with empty +``` + +There is a keyword `lb` here, which uses the global filter `LoadBalancerClientFilter`. When this route is matched, the request will be forwarded to the passjava-member service, and load balancing forwarding is supported, that is, passjava-member is first parsed into the host and port of the actual microservice, and then forwarded to the actual microservice. + +## Does Spring Cloud Gateway support current limiting? + +Spring Cloud Gateway comes with a current limiting filter, and the corresponding interface is `RateLimiter`. The `RateLimiter` interface has only one implementation class `RedisRateLimiter` (current limiting based on Redis + Lua). The current limiting function provided is relatively simple and difficult to use. + +Starting from Sentinel version 1.6.0, Sentinel has introduced the Spring Cloud Gateway adaptation module, which can provide current limiting in two resource dimensions: route dimension and custom API dimension. In other words, Spring Cloud Gateway can be combined with Sentinel to achieve more powerful gateway traffic control. + +## How does Spring Cloud Gateway customize global exception handling? + +In the SpringBoot project, to catch global exceptions, we only need to configure `@RestControllerAdvice` and `@ExceptionHandler` in the project. However, this method is not applicable under Spring Cloud Gateway. + +Spring Cloud Gateway provides a variety of global processing methods. The more commonly used one is to implement `ErrorWebExceptionHandler` and rewrite the `handle` method in it. + +```java +@Order(-1) +@Component +@RequiredArgsConstructor +public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler { + private final ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + // ... + } +} +``` + +## Reference + +- Spring Cloud Gateway official documentation: +- Creating a custom Spring Cloud Gateway Filter: +- Global exception handling: + + \ No newline at end of file diff --git a/docs_en/high-availability/fallback-and-circuit-breaker.en.md b/docs_en/high-availability/fallback-and-circuit-breaker.en.md new file mode 100644 index 00000000000..3c8c5a6c967 --- /dev/null +++ b/docs_en/high-availability/fallback-and-circuit-breaker.en.md @@ -0,0 +1,13 @@ +--- +title: Detailed explanation of downgrade and circuit breaker (paid) +category: high availability +icon: circuit +--- + +**Downgrade & Circuit Breaker** The relevant interview questions are exclusive to my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click on the link to view the detailed introduction and how to join), which has been compiled into ["Java Interview Guide North"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). + +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) + + + + \ No newline at end of file diff --git a/docs_en/high-availability/high-availability-system-design.en.md b/docs_en/high-availability/high-availability-system-design.en.md new file mode 100644 index 00000000000..b2ceaef044e --- /dev/null +++ b/docs_en/high-availability/high-availability-system-design.en.md @@ -0,0 +1,72 @@ +--- +title: 高可用系统设计指南 +category: 高可用 +icon: design +--- + +## 什么是高可用?可用性的判断标准是啥? + +高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。 + +一般情况下,我们使用多少个 9 来评判一个系统的可用性,比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的,这样的系统就是非常非常高可用的了!当然,也会有系统如果可用性不太好的话,可能连 9 都上不了。 + +除此之外,系统的可用性还可以用某功能的失败次数与总的请求次数之比来衡量,比如对网站请求 1000 次,其中有 10 次请求失败,那么可用性就是 99%。 + +## 哪些情况会导致系统不可用? + +1. 黑客攻击; +2. 硬件故障,比如服务器坏掉。 +3. 并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。 +4. 代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。 +5. 网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。 +6. 自然灾害或者人为破坏。 +7. …… + +## 有哪些提高系统可用性的方法? + +### 注重代码质量,测试严格把关 + +我觉得这个是最最最重要的,代码质量有问题比如比较常见的内存泄漏、循环依赖都是对系统可用性极大的损害。大家都喜欢谈限流、降级、熔断,但是我觉得从代码质量这个源头把关是首先要做好的一件很重要的事情。如何提高代码质量?比较实际可用的就是 CodeReview,不要在乎每天多花的那 1 个小时左右的时间,作用可大着呢! + +另外,安利几个对提高代码质量有实际效果的神器: + +- [Sonarqube](https://www.sonarqube.org/); +- Alibaba 开源的 Java 诊断工具 [Arthas](https://arthas.aliyun.com/doc/); +- [阿里巴巴 Java 代码规范](https://github.com/alibaba/p3c)(Alibaba Java Code Guidelines); +- IDEA 自带的代码分析等工具。 + +### 使用集群,减少单点故障 + +先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例挂了,不到一秒就会有另外一台 Redis 实例顶上。 + +### 限流 + +流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。——来自 [alibaba-Sentinel](https://github.com/alibaba/Sentinel "Sentinel") 的 wiki。 + +### 超时和重试机制设置 + +一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法再处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 + +### 熔断机制 + +超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。 + +### 异步调用 + +异步调用的话我们不需要关心最后的结果,这样我们就可以用户请求完成之后就立即返回结果,具体处理我们可以后续再做,秒杀场景用这个还是蛮多的。但是,使用异步之后我们可能需要 **适当修改业务流程进行配合**,比如**用户在提交订单之后,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**。除了可以在程序中实现异步之外,我们常常还使用消息队列,消息队列可以通过异步处理提高系统性能(削峰、减少响应所需时间)并且可以降低系统耦合性。 + +### 使用缓存 + +如果我们的系统属于并发量比较高的话,如果我们单纯使用数据库的话,当大量请求直接落到数据库可能数据库就会直接挂掉。使用缓存缓存热点数据,因为缓存存储在内存中,所以速度相当地快! + +### 其他 + +- **核心应用和服务优先使用更好的硬件** +- **监控系统资源使用情况增加报警设置。** +- **注意备份,必要时候回滚。** +- **灰度发布:** 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可 +- **定期检查/更换硬件:** 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。 +- …… + + + diff --git a/docs_en/high-availability/idempotency.en.md b/docs_en/high-availability/idempotency.en.md new file mode 100644 index 00000000000..a2c7c7ab4b8 --- /dev/null +++ b/docs_en/high-availability/idempotency.en.md @@ -0,0 +1,13 @@ +--- +title: Summary of interface idempotent solutions (paid) +category: high availability +icon: security-fill +--- + +**Interface Idempotent** The related interview questions are exclusive to my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and how to join), which has been compiled into ["Java Interview Guide North"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). + +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) + + + + \ No newline at end of file diff --git a/docs_en/high-availability/limit-request.en.md b/docs_en/high-availability/limit-request.en.md new file mode 100644 index 00000000000..bf28639b8b0 --- /dev/null +++ b/docs_en/high-availability/limit-request.en.md @@ -0,0 +1,296 @@ +--- +title: 服务限流详解 +category: 高可用 +icon: limit_rate +--- + +针对软件系统来说,限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。 + +限流可能会导致用户的请求无法被正确处理或者无法立即被处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。 + +现实生活中,处处都有限流的实际应用,就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。 + +## 常见限流算法有哪些? + +简单介绍 4 种非常好理解并且容易实现的限流算法! + +> 图片来源于 InfoQ 的一篇文章[《分布式服务限流实战,已经为你排好坑了》](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)。 + +### 固定窗口计数器算法 + +固定窗口其实就是时间窗口,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率,即固定窗口计数器算法规定了系统单位时间处理的请求数量。 + +假如我们规定系统中某个接口 1 分钟只能被访问 33 次的话,使用固定窗口计数器算法的实现思路如下: + +- 将时间划分固定大小窗口,这里是 1 分钟一个窗口。 +- 给定一个变量 `counter` 来记录当前接口处理的请求数量,初始值为 0(代表接口当前 1 分钟内还未处理请求)。 +- 1 分钟之内每处理一个请求之后就将 `counter+1` ,当 `counter=33` 之后(也就是说在这 1 分钟内接口已经被访问 33 次的话),后续的请求就会被全部拒绝。 +- 等到 1 分钟结束后,将 `counter` 重置 0,重新开始计数。 + +![固定窗口计数器算法](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) + +优点:实现简单,易于理解。 + +缺点: + +- 限流不够平滑。例如,我们限制某个接口每分钟只能访问 30 次,假设前 30 秒就有 30 个请求到达的话,那后续 30 秒将无法处理请求,这是不可取的,用户体验极差! +- 无法保证限流速率,因而无法应对突然激增的流量。例如,我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。 + +### 滑动窗口计数器算法 + +**滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版,限流的颗粒度更小。 + +滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:**它把时间以一定比例分片** 。 + +例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理不大于 `60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 + +很显然, **当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。** + +![滑动窗口计数器算法](https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png) + +优点: + +- 相比于固定窗口算法,滑动窗口计数器算法可以应对突然激增的流量。 +- 相比于固定窗口算法,滑动窗口计数器算法的颗粒度更小,可以提供更精确的限流控制。 + +缺点: + +- 与固定窗口计数器算法类似,滑动窗口计数器算法依然存在限流不够平滑的问题。 +- 相比较于固定窗口计数器算法,滑动窗口计数器算法实现和理解起来更复杂一些。 + +### 漏桶算法 + +我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。 + +如果想要实现这个算法的话也很简单,准备一个队列用来保存请求,然后我们定期从队列中拿请求来执行就好了(和消息队列削峰/限流的思想是一样的)。 + +![漏桶算法](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); + } + } +} + +``` + +Output: + +```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 +``` + +Below is a simple demo of Guava smooth preheating and current limiting. + +```java +import com.google.common.util.concurrent.RateLimiter; +import java.util.concurrent.TimeUnit; + +/** + * Search JavaGuide on WeChat and reply to "Interview Assault" to get your own original Java interview manual for free + * + * @author Guide brother + * @date 2021/10/08 19:12 + **/ +public class RateLimiterDemo { + + public static void main(String[] args) { + // 1s puts 5 tokens into the bucket, that is, 0.2s puts 1 token into the bucket + // The warm-up time is 3s, which means that the card issuance rate will gradually increase to 0.2s in the first 3s. Put 1 token into the bucket. + 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); + } + } +} +``` + +Output: + +```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 +``` + +In addition, **Bucket4j** is a very good current limiting library based on the token/leaky bucket algorithm. + +> Bucket4j address: + +Compared with Guava's current limiting tool class, the current limiting function provided by Bucket4j is more comprehensive. Not only does it support stand-alone current limiting and distributed current limiting, it can also integrate monitoring and be used with Prometheus and Grafana. + +However, after all, Guava is just a comprehensive tool library, and the out-of-the-box current limiting function it provides is quite practical in many stand-alone scenarios. + +The early version of stand-alone current limiting that comes with Spring Cloud Gateway was implemented based on Bucket4j. Later, it was replaced by **Resilience4j**. + +Resilience4j is a lightweight fault-tolerant component inspired by Hystrix. Since [Netflix announced that it will no longer actively develop Hystrix](https://github.com/NETFLIX/Hystrix/commit/a7df971cbaddd8c5e976b3cc5f14013fe6ad00e6), both Spring officials and Netflix recommend using Resilience4j for current limiting and fusing. + +> Resilience4j address: + +Under normal circumstances, in order to ensure the high availability of the system, the current limiting and circuit breaker of the project must be done together. + +Resilience4j not only provides current limiting, but also provides out-of-the-box functions such as fusing, load protection, and automatic retry to ensure system high availability. Moreover, the ecosystem of Resilience4j is also better. Many gateways use Resilience4j for current limiting and circuit breaker. + +Therefore, Resilience4j may be a better choice in most scenarios. For some relatively simple current limiting scenarios, Guava or Bucket4j are also good choices. + +## How to implement distributed current limiting? + +Distributed current limiting is targeted at distributed/microservice application architecture applications. Under this architecture, single-machine current limiting is not applicable because there will be multiple services, and multiple copies of a service may be deployed. + +Common solutions for distributed current limiting: + +- **Current limiting with middleware**: You can use Sentinel or Redis to implement the corresponding current limiting logic yourself. +- **Gateway layer current limiting**: A relatively common solution, which arranges the current limiting directly at the gateway layer. However, gateway layer current limiting usually requires the help of middleware/framework. For example, Spring Cloud Gateway's distributed current limiting implementation `RedisRateLimiter` is based on Redis+Lua. Another example is that Spring Cloud Gateway can also integrate Sentinel for current limiting. + +If you want to manually implement current limiting logic based on Redis, it is recommended to do it with Lua script. + +**Why is the Redis+Lua approach recommended? ** There are two main reasons: + +- **Reduced network overhead**: We can use Lua scripts to execute multiple Redis commands in batches. These Redis commands will be submitted to the Redis server for execution at one time, which greatly reduces network overhead. +- **Atomicity**: A Lua script can be executed as a command. During the execution of a Lua script, no other scripts or Redis commands will be executed at the same time, ensuring that the operation will not be inserted or interrupted by other instructions. + +I won’t include the specific current limiting script code here. There are many excellent ready-made current limiting scripts on the Internet for your reference. For example, the RateLimiter current limiting plug-in of the Apache gateway project ShenYu implements the token bucket algorithm/concurrent token bucket algorithm, leaky bucket algorithm, and sliding window algorithm based on Redis + Lua. + +> ShenYu address: + +![ShenYu rate limit script](https://oss.javaguide.cn/github/javaguide/high-availability/limit-request/shenyu-ratelimit-lua-scripts.png) + +In addition, if you don’t want to write Lua scripts yourself, you can also directly use `RRateLimiter` in Redisson to implement distributed current limiting. Its underlying implementation is based on Lua code + token bucket algorithm. + +Redisson is an open source Java language Redis client that provides many out-of-the-box features, such as data structure implementations commonly used in Java, distributed locks, delay queues, etc. Moreover, Redisson also supports multiple deployment architectures such as Redis stand-alone, Redis Sentinel, and Redis Cluster. + +`RRateLimiter` is very simple to use. We first need to obtain a `RRateLimiter` object, which can be obtained directly through the Redisson client. Then, just set the current limiting rules. + +```java +//Create a Redisson client instance +RedissonClient redissonClient = Redisson.create(); +// Get a current limiter object named "javaguide.limiter" +RRateLimiter rateLimiter = redissonClient.getRateLimiter("javaguide.limiter"); +// Try setting the limiter at a rate of 100 times per hour +// There are two types of RateType, OVERALL is global current limiting, ER_CLIENT is single Client current limiting (it can be considered as single-machine current limiting) +rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS); +``` + +Next we call the `acquire()` method or the `tryAcquire()` method to obtain the permission. + +```java +// Get a license and wait if the rate of the current limiter is exceeded +// acquire() is a synchronous method, the corresponding asynchronous method: acquireAsync() +rateLimiter.acquire(1); +//Try to obtain a license within 5 seconds, return true if successful, false otherwise +// tryAcquire() is a synchronous method, the corresponding asynchronous method: tryAcquireAsync() +boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); +``` + +## Summary + +This article mainly introduces common current limiting algorithms, the selection of current limiting objects, and how to implement single-machine current limiting and distributed current limiting respectively. + +## refer to- Resilience4j, a lightweight circuit breaker framework for service governance: +- Super detailed analysis of Guava RateLimiter current limiting principle: +- Practical use of Spring Cloud Gateway current limiting 👍: +- Detailed explanation of the implementation principle of Redisson distributed current limiting: +- A detailed explanation of Java current limiting interface implementation - Alibaba Cloud Developer: +- Exploration and practice of distributed current limiting solutions - Tencent Cloud Developer: + + \ No newline at end of file diff --git a/docs_en/high-availability/performance-test.en.md b/docs_en/high-availability/performance-test.en.md new file mode 100644 index 00000000000..99202f236da --- /dev/null +++ b/docs_en/high-availability/performance-test.en.md @@ -0,0 +1,179 @@ +--- +title: 性能测试入门 +category: 高可用 +icon: et-performance +--- + +性能测试一般情况下都是由测试这个职位去做的,那还需要我们开发学这个干嘛呢?了解性能测试的指标、分类以及工具等知识有助于我们更好地去写出性能更好的程序,另外作为开发这个角色,如果你会性能测试的话,相信也会为你的履历加分不少。 + +这篇文章是我会结合自己的实际经历以及在测试这里取的经所得,除此之外,我还借鉴了一些优秀书籍,希望对你有帮助。 + +## 不同角色看网站性能 + +### 用户 + +当用户打开一个网站的时候,最关注的是什么?当然是网站响应速度的快慢。比如我们点击了淘宝的主页,淘宝需要多久将首页的内容呈现在我的面前,我点击了提交订单按钮需要多久返回结果等等。 + +所以,用户在体验我们系统的时候往往根据你的响应速度的快慢来评判你的网站的性能。 + +### 开发人员 + +用户与开发人员都关注速度,这个速度实际上就是我们的系统**处理用户请求的速度**。 + +开发人员一般情况下很难直观的去评判自己网站的性能,我们往往会根据网站当前的架构以及基础设施情况给一个大概的值,比如: + +1. 项目架构是分布式的吗? +2. 用到了缓存和消息队列没有? +3. 高并发的业务有没有特殊处理? +4. 数据库设计是否合理? +5. 系统用到的算法是否还需要优化? +6. 系统是否存在内存泄露的问题? +7. 项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘? +8. …… + +### 测试人员 + +测试人员一般会根据性能测试工具来测试,然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容: + +1. 响应时间; +2. 请求成功率; +3. 吞吐量; +4. …… + +### 运维人员 + +运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 Devops 火起来后,单纯干运维的很少了。我们这里暂且还保留有这个角色。 + +## 性能测试需要注意的点 + +几乎没有文章在讲性能测试的时候提到这个问题,大家都会讲如何去性能测试,有哪些性能测试指标这些东西。 + +### 了解系统的业务场景 + +**性能测试之前更需要你了解当前的系统的业务场景。** 对系统业务了解的不够深刻,我们很容易犯测试方向偏执的错误,从而导致我们忽略了对系统某些更需要性能测试的地方进行测试。比如我们的系统可以为用户提供发送邮件的功能,用户配置成功邮箱后只需输入相应的邮箱之后就能发送,系统每天大概能处理上万次发邮件的请求。很多人看到这个可能就直接开始使用相关工具测试邮箱发送接口,但是,发送邮件这个场景可能不是当前系统的性能瓶颈,这么多人用我们的系统发邮件, 还可能有很多人一起发邮件,单单这个场景就这么人用,那用户管理可能才是性能瓶颈吧! + +### 历史数据非常有用 + +当前系统所留下的历史数据非常重要,一般情况下,我们可以通过相应的些历史数据初步判定这个系统哪些接口调用的比较多、哪些服务承受的压力最大,这样的话,我们就可以针对这些地方进行更细致的性能测试与分析。 + +另外,这些地方也就像这个系统的一个短板一样,优化好了这些地方会为我们的系统带来质的提升。 + +## 常见性能指标 + +### 响应时间 + +**响应时间 RT(Response-time)就是用户发出请求到用户收到系统处理结果所需要的时间。** + +RT 是一个非常重要且直观的指标,RT 数值大小直接反应了系统处理用户请求速度的快慢。 + +### 并发数 + +**并发数可以简单理解为系统能够同时供多少人访问使用也就是说系统同时能处理的请求数量。** + +并发数反应了系统的负载能力。 + +### QPS 和 TPS + +- **QPS(Query Per Second)** :服务器每秒可以执行的查询次数; +- **TPS(Transaction Per Second)** :服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程); + +书中是这样描述 QPS 和 TPS 的区别的。 + +> QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器 2 次,一次访问,产生一个“T”,产生 2 个“Q”。 + +### 吞吐量 + +**吞吐量指的是系统单位时间内系统处理的请求数量。** + +一个系统的吞吐量与请求对系统的资源消耗等紧密关联。请求对系统资源消耗越多,系统吞吐能力越低,反之则越高。 + +TPS、QPS 都是吞吐量的常用量化指标。 + +- **QPS(TPS)** = 并发数/平均响应时间(RT) +- **并发数** = QPS \* 平均响应时间(RT) + +## 系统活跃度指标 + +### PV(Page View) + +访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在一定统计周期内用户每打开或刷新一个页面就记录 1 次,多次打开或刷新同一页面则浏览量累计。UV 从网页打开的数量/刷新的次数的角度来统计的。 + +### UV(Unique Visitor) + +独立访客,统计 1 天内访问某站点的用户数。1 天内相同访客多次访问网站,只计算为 1 个独立访客。UV 是从用户个体的角度来统计的。 + +### DAU(Daily Active User) + +日活跃用户数量。 + +### MAU(monthly active users) + +月活跃用户人数。 + +举例:某网站 DAU 为 1200w, 用户日均使用时长 1 小时,RT 为 0.5s,求并发量和 QPS。 + +平均并发量 = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数(86400)=1200w/24 = 50w + +真实并发量(考虑到某些时间段使用人数比较少) = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数-访问量比较小的时间段假设为 8 小时(57600)=1200w/16 = 75w + +峰值并发量 = 平均并发量 \* 6 = 300w + +QPS = 真实并发量/RT = 75W/0.5=150w/s + +## 性能测试分类 + +### 性能测试 + +性能测试方法是通过测试工具模拟用户请求系统,目的主要是为了测试系统的性能是否满足要求。通俗地说,这种方法就是要在特定的运行条件下验证系统的能力状态。 + +性能测试是你在对系统性能已经有了解的前提之后进行的,并且有明确的性能指标。 + +### 负载测试 + +对被测试的系统继续加大请求压力,直到服务器的某个资源已经达到饱和了,比如系统的缓存已经不够用了或者系统的响应时间已经不满足要求了。 + +负载测试说白点就是测试系统的上限。 + +### 压力测试 + +不去管系统资源的使用情况,对系统继续加大请求压力,直到服务器崩溃无法再继续提供服务。 + +### 稳定性测试 + +模拟真实场景,给系统一定压力,看看业务是否能稳定运行。 + +## 常用性能测试工具 + +### 后端常用 + +既然系统设计涉及到系统性能方面的问题,那在面试的时候,面试官就很可能会问:**你是如何进行性能测试的?** + +推荐 4 个比较常用的性能测试工具: + +1. **Jmeter** :Apache JMeter 是 JAVA 开发的性能测试工具。 +2. **LoadRunner**:一款商业的性能测试工具。 +3. **Galtling** :一款基于 Scala 开发的高性能服务器性能测试工具。 +4. **ab** :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。 + +没记错的话,除了 **LoadRunner** 其他几款性能测试工具都是开源免费的。 + +### 前端常用 + +1. **Fiddler**:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。 +2. **HttpWatch**: 可用于录制 HTTP 请求信息的工具。 + +## 常见的性能优化策略 + +性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。 + +下面是一些性能优化时,我经常拿来自问的一些问题: + +1. 系统是否需要缓存? +2. 系统架构本身是不是就有问题? +3. 系统是否存在死锁的地方? +4. 系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏) +5. 数据库索引使用是否合理? +6. …… + + + diff --git a/docs_en/high-availability/redundancy.en.md b/docs_en/high-availability/redundancy.en.md new file mode 100644 index 00000000000..07323dfb1b9 --- /dev/null +++ b/docs_en/high-availability/redundancy.en.md @@ -0,0 +1,47 @@ +--- +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 copies of the same service. If the service being used suddenly hangs up, the system can quickly switch to the backup service, greatly reducing the system's unavailability time and improving system availability. + +For data, the idea of ​​redundancy is to back up multiple copies of the same data, which can easily improve data security. + +In fact, there are many applications of redundant ideas in daily life. + +For myself, my method of saving important files is the application of redundant thinking. The important files I use daily are synchronized on GitHub and my personal cloud disk. This ensures that even if my computer hard drive is damaged, I can retrieve my important files through GitHub or my personal cloud disk. + +High Availability Cluster (HA Cluster for short), same-city disaster recovery, remote disaster recovery, same-city multi-active and remote multi-active are the most typical applications of redundancy ideas in high-availability system design. + +- **High Availability Cluster**: Deploy two or more copies of the same service. If the service being used suddenly hangs up, you can switch to another service to ensure high availability of the service. +- **Same-city disaster recovery**: An entire cluster can be deployed in the same computer room, while in same-city disaster recovery the same services are deployed in different computer rooms in the same city. Also, the backup service does not handle requests. This can avoid unexpected situations such as power outages and fires in the computer room. +- **Remote disaster recovery**: Similar to same-city disaster recovery, the difference is that the same service is deployed in different computer rooms in different locations (usually far away, or even in different cities or countries) +- **Multi-active in the same city**: Similar to the same city disaster recovery, but the backup service can handle requests, which can make full use of system resources and improve system concurrency. +- **Multi-Activity in Remote Locations**: Deploy services in different computer rooms in different locations, and they can provide services to the outside world at the same time. + +High-availability clusters are purely for service redundancy and do not emphasize geography. Intra-city disaster recovery, remote disaster recovery, same-city multi-activity and remote multi-activity realize geographical redundancy. + +The main difference between the same city and another place is the distance between computer rooms. The distance is usually far away, even in different cities or countries. + +Compared with the traditional disaster recovery design, the most obvious change between multi-activity in the same city and multi-activity in different places is "multi-activity", that is, all sites provide services to the outside world at the same time. Living more in different places is to cope with emergencies such as fires, earthquakes and other natural or man-made disasters. + +Redundancy alone is not enough, it must be coupled with **failover**! The so-called failover simply means the rapid and automatic switching of unavailable services to available services. The entire process does not require human intervention. + +For example: In the Sentinel mode Redis cluster, if Sentinel detects that the master node fails, it will help us implement failover and automatically upgrade a slave to the master to ensure the availability of the entire Redis system. The entire process is completely automatic and requires no manual intervention. I have introduced the knowledge points and interview questions related to Redis cluster in detail in the database section of "Technical Interview Questions" in ["Java Interview Guide"](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7). Interested friends can take a look. + +Another example: Nginx can be combined with Keepalived to achieve high availability. If the Nginx master server goes down, Keepalived can automatically perform failover and the backup Nginx master server is upgraded to the main service. Moreover, this switch is transparent to the outside world, because the virtual IP used will not change. I introduced the Nginx related knowledge points and interview questions in detail in the "Server" section of the "Technical Interview Questions" of ["Java Interview Guide"](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7). Interested friends can take a look. + +It is very difficult to implement a remote multi-active architecture, and there are many factors that need to be considered. I am not talented and have never practiced the remote multi-active architecture in actual projects. My understanding of it is still based on book knowledge. + +If you want to learn more about living in a different place, I recommend a few articles that I think are pretty good: + +- [To understand how to live in a different place, just read this article - Water Drops and Silver Bullets - 2021](https://mp.weixin.qq.com/s/T6mMDdtTfBuIiEowCpqu6Q) +- [Four steps to build remote multi-activity](https://mp.weixin.qq.com/s/hMD-IS__4JE5_nQhYPYSTg) +- ["Learning Architecture from Scratch" — 28 | Guarantee of high business availability: multi-active remote architecture](http://gk.link/a/10pKZ) + +However, most of these articles also introduce conceptual knowledge. At present, there is still a lack of information on the Internet that truly introduces how to implement the multi-active architecture in different locations. + + \ No newline at end of file diff --git a/docs_en/high-availability/timeout-and-retry.en.md b/docs_en/high-availability/timeout-and-retry.en.md new file mode 100644 index 00000000000..4f5d7262ab2 --- /dev/null +++ b/docs_en/high-availability/timeout-and-retry.en.md @@ -0,0 +1,87 @@ +--- +title: 超时&重试详解 +category: 高可用 +icon: retry +--- + +由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。 + +为了最大限度的减小系统或者服务出现故障之后带来的影响,我们需要用到的 **超时(Timeout)** 和 **重试(Retry)** 机制。 + +想要把超时和重试机制讲清楚其实很简单,因为它俩本身就不是什么高深的概念。 + +虽然超时和重试机制的思想很简单,但是它俩是真的非常实用。你平时接触到的绝大部分涉及到远程调用的系统或者服务都会应用超时和重试机制。尤其是对于微服务系统来说,正确设置超时和重试非常重要。单体服务通常只涉及数据库、缓存、第三方 API、中间件等的网络调用,而微服务系统内部各个服务之间还存在着网络调用。 + +## 超时机制 + +### 什么是超时机制? + +超时机制说的是当一个请求超过指定的时间(比如 1s)还没有被处理的话,这个请求就会直接被取消并抛出指定的异常或者错误(比如 `504 Gateway Timeout`)。 + +我们平时接触到的超时可以简单分为下面 2 种: + +- **连接超时(ConnectTimeout)**:客户端与服务端建立连接的最长等待时间。 +- **读取超时(ReadTimeout)**:客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,我们关注比较多的还是读取超时。 + +一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。 + +如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。 + +这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。 + +我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。 + +### 超时时间应该如何设置? + +超时到底设置多长时间是一个难题!超时值设置太高或者太低都有风险。如果设置太高的话,会降低超时机制的有效性,比如你设置超时为 10s 的话,那设置超时就没啥意义了,系统依然可能会出现大量慢请求堆积的问题。如果设置太低的话,就可能会导致在系统或者服务在某些处理请求速度变慢的情况下(比如请求突然增多),大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。 + +通常情况下,我们建议读取超时设置为 **1500ms** ,这是一个比较普适的值。如果你的系统或者服务对于延迟比较敏感的话,那读取超时值可以适当在 **1500ms** 的基础上进行缩短。反之,读取超时值也可以在 **1500ms** 的基础上进行加长,不过,尽量还是不要超过 **1500ms** 。连接超时可以适当设置长一些,建议在 **1000ms ~ 5000ms** 之内。 + +没有银弹!超时值具体该设置多大,还是要根据实际项目的需求和情况慢慢调整优化得到。 + +更上一层,参考[美团的 Java 线程池参数动态配置](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)思想,我们也可以将超时弄成可配置化的参数而不是固定的,比较简单的一种办法就是将超时的值放在配置中心中。这样的话,我们就可以根据系统或者服务的状态动态调整超时值了。 + +## 重试机制 + +### 什么是重试机制? + +重试机制一般配合超时机制一起使用,指的是多次发送相同的请求来避免瞬态故障和偶然性故障。 + +瞬态故障可以简单理解为某一瞬间系统偶然出现的故障,并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障,频率通常较低。 + +重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。 + +### 常见的重试策略有哪些? + +常见的重试策略有两种: + +1. **固定间隔时间重试**:每次重试之间都使用相同的时间间隔,比如每隔 1.5 秒进行一次重试。这种重试策略的优点是实现起来比较简单,不需要考虑重试次数和时间的关系,也不需要维护额外的状态信息。但是这种重试策略的缺点是可能会导致重试过于频繁或过于稀疏,从而影响系统的性能和效率。如果重试间隔太短,可能会对目标系统造成过大的压力,导致雪崩效应;如果重试间隔太长,可能会导致用户等待时间过长,影响用户体验。 +2. **梯度间隔重试**:根据重试次数的增加去延长下次重试时间,比如第一次重试间隔为 1 秒,第二次为 2 秒,第三次为 4 秒,以此类推。这种重试策略的优点是能够有效提高重试成功的几率(随着重试次数增加,但是重试依然不成功,说明目标系统恢复时间比较长,因此可以根据重试次数延长下次重试时间),也能通过柔性化的重试避免对下游系统造成更大压力。但是这种重试策略的缺点是实现起来比较复杂,需要考虑重试次数和时间的关系,以及设置合理的上限和下限值。另外,这种重试策略也可能会导致用户等待时间过长,影响用户体验。 + +这两种适合的场景各不相同。固定间隔时间重试适用于目标系统恢复时间比较稳定和可预测的场景,比如网络波动或服务重启。梯度间隔重试适用于目标系统恢复时间比较长或不可预测的场景,比如网络故障和服务故障。 + +### 重试的次数如何设置? + +重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。 + +重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。 + +### 什么是重试幂等? + +超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。 + +什么情况下会出现一个请求被多次执行呢?客户端等待服务端完成请求完成超时但此时服务端已经执行了请求,只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。 + +举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。 + +### Java 中如何实现重试? + +如果要手动编写代码实现重试逻辑的话,可以通过循环(例如 while 或 for 循环)或者递归实现。不过,一般不建议自己动手实现,有很多第三方开源库提供了更完善的重试机制实现,例如 Spring Retry、Resilience4j、Guava Retrying。 + +## 参考 + +- 微服务之间调用超时的设置治理: +- 超时、重试和抖动回退: + + + diff --git a/docs_en/high-performance/cdn.en.md b/docs_en/high-performance/cdn.en.md new file mode 100644 index 00000000000..b238a5c4fe6 --- /dev/null +++ b/docs_en/high-performance/cdn.en.md @@ -0,0 +1,135 @@ +--- +title: CDN工作原理详解 +category: 高性能 +head: + - - meta + - name: keywords + content: CDN,内容分发网络 + - - meta + - name: description + content: CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。 +--- + +## 什么是 CDN ? + +**CDN** 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 **内容分发网络** 。 + +我们可以将内容分发网络拆开来看: + +- 内容:指的是静态资源比如图片、视频、文档、JS、CSS、HTML。 +- 分发网络:指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问比如北京的用户直接访问北京机房的数据。 + +所以,简单来说,**CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。** + +类似于京东建立的庞大的仓储运输体系,京东物流在全国拥有非常多的仓库,仓储网络几乎覆盖全国所有区县。这样的话,用户下单的第一时间,商品就从距离用户最近的仓库,直接发往对应的配送站,再由京东小哥送到你家。 + +![京东仓配系统](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/jingdong-wuliu-cangpei.png) + +你可以将 CDN 看作是服务上一层的特殊缓存服务,分布在全国各地,主要用来处理静态资源的请求。 + +![CDN 简易示意图](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-101.png) + +我们经常拿全站加速和内容分发网络做对比,不要把两者搞混了!全站加速(不同云服务商叫法不同,腾讯云叫 ECDN、阿里云叫 DCDN)既可以加速静态资源又可以加速动态资源,内容分发网络(CDN)主要针对的是 **静态资源** 。 + +![阿里云文档: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 服务。 + +很多朋友可能要问了:**既然是就近访问,为什么不直接将服务部署在多个不同的地方呢?** + +- 成本太高,需要部署多份相同的服务。 +- 静态资源通常占用空间比较大且经常会被访问到,如果直接使用服务器或者缓存来处理静态资源请求的话,对系统资源消耗非常大,可能会影响到系统其他服务的正常运行。 + +同一个服务在在多个不同的地方部署多份(比如同城灾备、异地灾备、同城多活、异地多活)是为了实现系统的高可用而不是就近访问。 + +## CDN 工作原理是什么? + +搞懂下面 3 个问题也就搞懂了 CDN 的工作原理: + +1. 静态资源是如何被缓存到 CDN 节点中的? +2. 如何找到最合适的 CDN 节点? +3. 如何防止静态资源被盗用? + +### 静态资源是如何被缓存到 CDN 节点中的? + +你可以通过 **预热** 的方式将源站的资源同步到 CDN 的节点中。这样的话,用户首次请求资源可以直接从 CDN 节点中取,无需回源。这样可以降低源站压力,提升用户体验。 + +如果不预热的话,你访问的资源可能不在 CDN 节点中,这个时候 CDN 节点将请求源站获取资源,这个过程是大家经常说的 **回源**。 + +> - 回源:当 CDN 节点上没有用户请求的资源或该资源的缓存已经过期时,CDN 节点需要从原始服务器获取最新的资源内容,这个过程就是回源。当用户请求发生回源的话,会导致该请求的响应速度比未使用 CDN 还慢,因为相比于未使用 CDN 还多了一层 CDN 的调用流程。 +> - 预热:预热是指在 CDN 上提前将内容缓存到 CDN 节点上。这样当用户在请求这些资源时,能够快速地从最近的 CDN 节点获取到而不需要回源,进而减少了对源站的访问压力,提高了访问速度。 + +![CDN 回源](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-back-to-source.png) + +如果资源有更新的话,你也可以对其 **刷新** ,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点回源站获取最新资源。 + +几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能(下图是阿里云 CDN 服务提供的相应功能): + +![CDN 缓存的刷新和预热](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-refresh-warm-up.png) + +**命中率** 和 **回源率** 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。 + +### 如何找到最合适的 CDN 节点? + +GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。 + +CDN 会通过 GSLB 找到最合适的 CDN 节点,更具体点来说是下面这样的: + +1. 浏览器向 DNS 服务器发送域名请求; +2. DNS 服务器向根据 CNAME( Canonical Name ) 别名记录向 GSLB 发送请求; +3. GSLB 返回性能最好(通常距离请求地址最近)的 CDN 节点(边缘服务器,真正缓存内容的地方)的地址给浏览器; +4. 浏览器直接访问指定的 CDN 节点。 + +![CDN 原理示意图](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-overview.png) + +为了方便理解,上图其实做了一点简化。GSLB 内部可以看作是 CDN 专用 DNS 服务器和负载均衡系统组合。CDN 专用 DNS 服务器会返回负载均衡系统 IP 地址给浏览器,浏览器使用 IP 地址请求负载均衡系统进而找到对应的 CDN 节点。 + +**GSLB 是如何选择出最合适的 CDN 节点呢?** GSLB 会根据请求的 IP 地址、CDN 节点状态(比如负载情况、性能、响应时间、带宽)等指标来综合判断具体返回哪一个 CDN 节点的地址。 + +### 如何防止资源被盗刷? + +如果我们的资源被其他用户或者网站非法盗刷的话,将会是一笔不小的开支。 + +解决这个问题最常用最简单的办法设置 **Referer 防盗链**,具体来说就是根据 HTTP 请求的头信息里面的 Referer 字段对请求进行限制。我们可以通过 Referer 字段获取到当前请求页面的来源页面的网站地址,这样我们就能确定请求是否来自合法的网站。 + +CDN 服务提供商几乎都提供了这种比较基础的防盗链机制。 + +![腾讯云 CDN Referer 防盗链配置](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cnd-tencent-cloud-anti-theft.png) + +不过,如果站点的防盗链配置允许 Referer 为空的话,通过隐藏 Referer,可以直接绕开防盗链。 + +通常情况下,我们会配合其他机制来确保静态资源被盗用,一种常用的机制是 **时间戳防盗链** 。相比之下,**时间戳防盗链** 的安全性更强一些。时间戳防盗链加密的 URL 具有时效性,过期之后就无法再被允许访问。 + +时间戳防盗链的 URL 通常会有两个参数一个是签名字符串,一个是过期时间。签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。 + +时间戳防盗链 URL 示例: + +```plain +http://cdn.wangsu.com/4/123.mp3? wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTime=1601026312 +``` + +- `wsSecret`:签名字符串。 +- `wsTime`: 过期时间。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/timestamp-anti-theft.png) + +时间戳防盗链的实现也比较简单,并且可靠性较高,推荐使用。并且,绝大部分 CDN 服务提供商都提供了开箱即用的时间戳防盗链机制。 + +![Qiniuyun timestamp anti-theft configuration](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/qiniuyun-timestamp-anti-theft.png) + +In addition to Referer anti-hotlinking and timestamp anti-hotlinking, you can also configure IP black and white list, IP access frequency limit configuration and other mechanisms to prevent theft. + +## Summary + +- CDN is to distribute 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. +- Based on cost, stability and ease of use, it is recommended to directly choose the out-of-the-box CDN service provided by professional cloud vendors (such as Alibaba Cloud, Tencent Cloud, Huawei Cloud, Qingyun) or CDN vendors (such as Wangsu, Lanxun). +- GSLB (Global Server Load Balance, global load balancing) is the brain of CDN and is responsible for the cooperation between multiple CDN nodes. The most commonly used one is DNS-based GSLB. CDN will find the most suitable CDN node through GSLB. +- In order to prevent static resources from being stolen, we can use **Referer anti-hotlinking** + **Timestamp anti-hotlinking**. + +## Reference + +- Timestamp hotlinking prevention - Qiniu Cloud CDN: +- What is a CDN? One article explains it clearly: +- "HTTP Protocol in Perspective" - 37 | CDN: Accelerate our network services: + + \ No newline at end of file diff --git a/docs_en/high-performance/data-cold-hot-separation.en.md b/docs_en/high-performance/data-cold-hot-separation.en.md new file mode 100644 index 00000000000..54637b7ddbe --- /dev/null +++ b/docs_en/high-performance/data-cold-hot-separation.en.md @@ -0,0 +1,68 @@ +--- +title: Detailed explanation of hot and cold data separation +category: high performance +head: + - - meta + - name: keywords + content: Separation of hot and cold data, cold data migration, cold data storage + - - meta + - name: description + content: Separation of hot and cold data refers to dividing data into cold data and hot data based on the access frequency and business importance of the data. Cold data is generally stored in low-cost, low-performance media, and hot data is stored in high-performance storage media. +--- + +## What is the separation of hot and cold data? + +Separation of hot and cold data refers to dividing data into cold data and hot data based on the access frequency and business importance of the data. Cold data is generally stored in low-cost, low-performance media, and hot data is stored in high-performance storage media. + +### Cold data and hot data + +Hot data refers to data that is frequently accessed and modified and needs to be accessed quickly. Cold data refers to data that is not accessed frequently and has low value to the current project, but needs to be preserved for a long time. + +How to distinguish between hot and cold data? There are two common ways to differentiate: + +1. **Time dimension distinction**: According to the creation time, update time, expiration time, etc. of the data, the data within a certain time period is regarded as hot data, and the data beyond this time period is regarded as cold data. For example, the order system can treat order data 1 year ago as cold data and order data within 1 year as hot data. This method is suitable for scenarios where there is a strong correlation between data access frequency and time. +2. **Access frequency differentiation**: Data with high frequency access is regarded as hot data, and data with low frequency access is regarded as cold data. For example, the content system can treat articles with very low page views as cold data and articles with high page views as hot data. This method needs to record the access frequency of the data, which is relatively expensive and is suitable for scenarios where there is a strong correlation between the access frequency and the data itself. + +Data from a few years ago is not necessarily cold data. For example, some high-quality articles are still visited by many people a few years after they were published, but most newly published articles by ordinary users are rarely visited by anyone. + +These two methods of distinguishing hot and cold data have their own advantages and disadvantages. In actual projects, they can be used in combination. + +### The idea of separation of hot and cold + +The idea of hot and cold separation is very simple, which is to classify the data and store it separately. The idea of hot and cold separation can be applied to many fields and scenarios, not just data storage, for example: + +- In the email system, you can put recent, more important emails in your inbox, and older, less important emails in the archive. +- In daily life, you can place commonly used items in a conspicuous place, and put infrequently used items in the storage room or attic. +- In the library, the most popular and frequently borrowed books can be placed in a conspicuous area alone, and the less frequently borrowed books can be placed in an inconspicuous location. +-… + +### Advantages and Disadvantages of Separating Hot and Cold Data + +- Advantages: The query performance of hot data is optimized (most users' operating experience will be better), cost saving (the corresponding database type and hardware configuration can be selected according to the different storage requirements of hot and cold data, such as placing hot data on SSD and cold data on HDD) +- Disadvantages: Increased system complexity and risks (hot and cold data need to be separated, increased risk of data errors), low statistical efficiency (cold storage data may be needed for statistics). + +## How to migrate cold data? + +Cold data migration solution: + +1. Business layer code implementation: When there is a write operation on data, the logic of hot and cold separation is triggered to determine whether the data is cold data or hot data. Cold data is put into the cold storage, and hot data is put into the hot storage. This solution will affect performance and the judgment logic of hot and cold data is not easy to determine. It also requires modification of the business layer code, so it is generally not used. +2. Task scheduling: You can use xxl-job or other distributed task scheduling platforms to regularly scan the database to find data that meets the cold data conditions, then copy them to the cold storage in batches and delete them from the hot storage. This method requires very little code modification and is very suitable for scenarios where hot and cold data are distinguished based on time. +3. Monitor the change log binlog of the database: extract the data that meets the cold data conditions from the binlog, then copy it to the cold storage, and delete it from the hot storage. This method does not require modifying the code, but it is not suitable for scenarios where hot and cold data are distinguished based on the time dimension. + +If your company has a DBA, you can also ask the DBA to manually migrate the cold data and complete the migration of the cold data to the cold storage in one go. Then, use the solution introduced above to implement subsequent cold data migration. + +## How to store cold data? + +The storage requirements of cold data are mainly large capacity, low cost, high reliability, and access speed can be appropriately sacrificed. + +Cold data storage solution: + +- Small and medium-sized factories: Just use MySQL/PostgreSQL directly (do not change the database selection and keep it consistent with the database currently used by the project), such as adding a table to store cold data of a certain business or using a separate cold storage to store cold data (involving cross-database queries, increasing system complexity and maintenance difficulty) +- Major manufacturers: Hbase (commonly used), RocksDB, Doris, Cassandra + +If the company's cost budget is sufficient, it can also directly use a distributed relational database such as TiDB to get it done in one step. TiDB 6.0 officially supports the separation of hot and cold data storage, which can reduce the cost of SSD usage. Using the data placement function of TiDB 6.0, you can realize hot and cold storage of massive data in the same cluster, store new hot data in SSD, and store historical cold data in HDD. + +## Case sharing + +- [How to quickly optimize an order form with tens of millions of data - Programmer Jidian - 2023](https://www.cnblogs.com/fulongyuanjushi/p/17910420.html) +- [Mass data hot and cold separation scheme and practice - ByteDance technical team - 2022](https://mp.weixin.qq.com/s/ZKRkZP6rLHuTE1wvnqmAPQ) \ No newline at end of file diff --git a/docs_en/high-performance/deep-pagination-optimization.en.md b/docs_en/high-performance/deep-pagination-optimization.en.md new file mode 100644 index 00000000000..3e79bab2571 --- /dev/null +++ b/docs_en/high-performance/deep-pagination-optimization.en.md @@ -0,0 +1,143 @@ +--- +title: 深度分页介绍及优化建议 +category: 高性能 +head: + - - meta + - name: keywords + content: 深度分页 + - - meta + - name: description + content: 查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。深度分页可以采用范围查询、子查询、INNER JOIN 延迟关联、覆盖索引等方法进行优化。 +--- + +## 深度分页介绍 + +查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如: + +```sql +# MySQL 在无法利用索引的情况下跳过1000000条记录后,再获取10条记录 +SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10 +``` + +## 深度分页问题的原因 + +当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源。 + +![深度分页问题](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png) + +不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。 + +![转全表扫描的临界点](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon-critical-point.png) + +MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低,它就会放弃使用索引。不过,即使偏移量很大,如果查询中使用了覆盖索引(covering index),MySQL 仍然可能会使用索引,避免回表操作。 + +## 深度分页优化建议 + +这里以 MySQL 数据库为例介绍一下如何优化深度分页。 + +### 范围查询 + +当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案: + +```sql +# 查询指定 ID 范围的数据 +SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id +# 也可以通过记录上次查询结果的最后一条记录的ID进行下一页的查询: +SELECT * FROM t_order WHERE id > 100000 LIMIT 10 +``` + +这种基于 ID 范围的深度分页优化方式存在很大限制: + +1. **ID 连续性要求高**: 实际项目中,数据库自增 ID 往往因为各种原因(例如删除数据、事务回滚等)导致 ID 不连续,难以保证连续性。 +2. **排序问题**: 如果查询需要按照其他字段(例如创建时间、更新时间等)排序,而不是按照 ID 排序,那么这种方法就不再适用。 +3. **并发场景**: 在高并发场景下,单纯依赖记录上次查询的最后一条记录的 ID 进行分页,容易出现数据重复或遗漏的问题。 + +### 子查询 + +我们先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。 + +阿里巴巴《Java 开发手册》中也有对应的描述: + +> 利用延迟关联或者子查询优化超多分页场景。 +> +> ![](https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-paging.png) + +```sql +-- 先通过子查询在主键索引上进行偏移,快速找到起始ID +SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order LIMIT 1000000, 1) LIMIT 10; +``` + +**工作原理**: + +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 条记录。 + +不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。 + +当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。 + +### 延迟关联 + +延迟关联与子查询的优化思路类似,都是通过将 `LIMIT` 操作转移到主键索引树上,减少回表次数。相比直接使用子查询,延迟关联通过 `INNER JOIN` 将子查询结果集成到主查询中,避免了子查询可能产生的临时表。在执行 `INNER JOIN` 时,MySQL 优化器能够利用索引进行高效的连接操作(如索引扫描或其他优化策略),因此在深度分页场景下,性能通常优于直接使用子查询。 + +```sql +-- 使用 INNER JOIN 进行延迟关联 +SELECT t1.* +FROM t_order t1 +INNER JOIN ( + -- 这里的子查询可以利用覆盖索引,性能极高 + SELECT id FROM t_order LIMIT 1000000, 10 +) t2 ON t1.id = t2.id; +``` + +**工作原理**: + +1. 子查询 `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` 利用主键索引快速定位目标分页的 10 条记录的 ID。 +2. 通过 `INNER JOIN` 将子查询结果与主表 `t_order` 关联,获取完整的记录数据。 + +除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。 + +```sql +-- 使用逗号进行延迟关联 +SELECT t1.* FROM t_order t1, +(SELECT id FROM t_order where id > 1000000 LIMIT 10) t2 +WHERE t1.id = t2.id; +``` + +**注意**: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 `INNER JOIN` 语法。 + +### 覆盖索引 + +索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。 + +**覆盖索引的好处:** + +- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。 +- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 + +```sql +# 如果只需要查询 id, code, type 这三列,可建立 code 和 type 的覆盖索引 +SELECT id, code, type FROM t_order +ORDER BY code +LIMIT 1000000, 10; +``` + +**⚠️注意**: + +- 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。 +- 虽然可以使用 `FORCE INDEX` 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。 + +## 总结 + +本文总结了几种常见的深度分页优化方案: + +1. **范围查询**: 基于 ID 连续性进行分页,通过记录上一页最后一条记录的 ID 来获取下一页数据。适合 ID 连续且按 ID 查询的场景,但在 ID 不连续或需要按其他字段排序时存在局限。 +2. **子查询**: 先通过子查询获取分页的起始主键值,再根据主键进行筛选分页。利用主键索引提高效率,但子查询会生成临时表,复杂场景下性能不佳。 +3. **延迟关联 (INNER JOIN)**: 使用 `INNER JOIN` 将分页操作转移到主键索引上,减少回表次数。相比子查询,延迟关联的性能更优,适合大数据量的分页查询。 +4. **覆盖索引**: 通过索引直接获取所需字段,避免回表操作,减少 IO 开销,适合查询特定字段的场景。但当结果集较大时,MySQL 可能会选择全表扫描。 + +## 参考 + +- Let’s talk about how to solve the MySQL deep paging problem - The little boy picking up snails: +- Database deep paging introduction and optimization plan - JD retail technology: +- MySQL deep paging optimization - Dewu Technology: \ No newline at end of file diff --git a/docs_en/high-performance/load-balancing.en.md b/docs_en/high-performance/load-balancing.en.md new file mode 100644 index 00000000000..b940f11262e --- /dev/null +++ b/docs_en/high-performance/load-balancing.en.md @@ -0,0 +1,267 @@ +--- +title: 负载均衡原理及算法详解 +category: 高性能 +head: + - - meta + - name: keywords + content: 客户端负载均衡,服务负载均衡,Nginx,负载均衡算法,七层负载均衡,DNS解析 + - - meta + - name: description + content: 负载均衡指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力。负载均衡可以简单分为服务端负载均衡和客户端负载均衡 这两种。服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。 +--- + +## 什么是负载均衡? + +**负载均衡** 指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力以及可靠性。负载均衡服务可以有由专门的软件或者硬件来完成,一般情况下,硬件的性能更好,软件的价格更便宜(后文会详细介绍到)。 + +下图是[《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) 「高并发篇」中的一篇文章的配图,从图中可以看出,系统的商品服务部署了多份在不同的服务器上,为了实现访问商品服务请求的分流,我们用到了负载均衡。 + +![多服务实例-负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/multi-service-load-balancing.drawio.png) + +负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段,不论是单体架构的系统还是微服务架构的系统几乎都会用到。 + +## 负载均衡分为哪几种? + +负载均衡可以简单分为 **服务端负载均衡** 和 **客户端负载均衡** 这两种。 + +服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因此,我会花更多时间来介绍。 + +### 服务端负载均衡 + +**服务端负载均衡** 主要应用在 **系统外部请求** 和 **网关层** 之间,可以使用 **软件** 或者 **硬件** 实现。 + +下图是我画的一个简单的基于 Nginx 的服务端负载均衡示意图: + +![基于 Nginx 的服务端负载均衡](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/server-load-balancing.png) + +**硬件负载均衡** 通过专门的硬件设备(比如 **F5、A10、Array** )实现负载均衡功能。 + +硬件负载均衡的优势是性能很强且稳定,缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万,绝大部分公司是根本负担不起的,业务量不大的话,真没必要非要去弄个硬件来做负载均衡,用软件负载均衡就足够了! + +在我们日常开发中,一般很难接触到硬件负载均衡,接触的比较多的还是 **软件负载均衡** 。软件负载均衡通过软件(比如 **LVS、Nginx、HAproxy** )实现负载均衡功能,性能虽然差一些,但价格便宜啊!像基础款的 Linux 服务器也就几千,性能好一点的 2~3 万的就很不错了。 + +根据 OSI 模型,服务端负载均衡还可以分为: + +- 二层负载均衡 +- 三层负载均衡 +- 四层负载均衡 +- 七层负载均衡 + +最常见的是四层和七层负载均衡,因此,本文也是重点介绍这两种负载均衡。 + +> Nginx 官网对四层负载和七层负载均衡均衡做了详细介绍,感兴趣的可以看看。 +> +> - [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 模型第四层,也就是传输层,这一层的主要协议是 TCP/UDP,负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。 +- **七层负载均衡** 工作在 OSI 模型第七层,也就是应用层,这一层的主要协议是 HTTP 。这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 **反向代理服务器** 。 + +七层负载均衡比四层负载均衡会消耗更多的性能,不过,也相对更加灵活,能够更加智能地路由网络请求,比如说你可以根据请求的内容进行优化如缓存、压缩、加密。 + +简单来说,**四层负载均衡性能很强,七层负载均衡功能更强!** 不过,对于绝大部分业务场景来说,四层负载均衡和七层负载均衡的性能差异基本可以忽略不计的。 + +下面这段话摘自 Nginx 官网的 [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 层负载均衡)来做四层负载均衡。 + +关于 Nginx 的常见知识点总结,[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 中「技术面试题篇」中已经有对应的内容了,感兴趣的小伙伴可以去看看。 + +![](https://oss.javaguide.cn/github/javaguide/image-20220328105759300.png) + +不过,LVS 这个绝大部分公司真用不上,像阿里、百度、腾讯、eBay 等大厂才会使用到,用的最多的还是 Nginx。 + +### 客户端负载均衡 + +**客户端负载均衡** 主要应用于系统内部的不同的服务之间,可以使用现成的负载均衡组件来实现。 + +在客户端负载均衡中,客户端会自己维护一份服务器的地址列表,发送请求之前,客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。 + +客户端负载均衡器和服务运行在同一个进程或者说 Java 程序里,不存在额外的网络开销。不过,客户端负载均衡的实现会受到编程语言的限制,比如说 Spring Cloud Load Balancer 就只能用于 Java 语言。 + +Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能,Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项,比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被弃用)。 + +下图是我画的一个简单的基于 Spring Cloud Load Balancer(Ribbon 也类似) 的客户端负载均衡示意图: + +![](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/spring-cloud-lb-gateway.png) + +## 负载均衡常见的算法有哪些? + +### 随机法 + +**随机法** 是最简单粗暴的负载均衡算法。 + +如果没有配置权重的话,所有的服务器被访问到的概率都是相同的。如果配置权重的话,权重越高的服务器被访问的概率就越大。 + +未加权重的随机算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权随机算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。 + +不过,随机算法有一个比较明显的缺陷:部分机器在一段时间之内无法被随机到,毕竟是概率算法,就算是大家权重一样, 也可能会出现这种情况。 + +于是,**轮询法** 来了! + +### 轮询法 + +轮询法是挨个轮询服务器处理,也可以设置权重。 + +如果没有配置权重的话,每个请求按时间顺序逐一分配到不同的服务器处理。如果配置权重的话,权重越高的服务器被访问的次数就越多。 + +未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。 + +在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。 + +平滑的加权轮训算法最早是在 Nginx 中被实现,可以参考这个 commit:。如果你认真学习过 Dubbo 负载均衡策略的话,就会发现 Dubbo 的加权轮询就借鉴了该算法实现并进一步做了优化。 + +![Dubbo 加权轮询负载均衡算法](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/dubbo-round-robin-load-balance.png) + +### 两次随机法 + +两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。 + +两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。 + +### 哈希法 + +将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。 + +在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。 + +### 一致性 Hash 法 + +和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。 + +常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。而一致性哈希法的核心思想是将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。 + +### 最小连接法 + +当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。 + +最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。 + +### 最少活跃法 + +最少活跃法和最小连接法类似,但要更科学一些。最少活跃法以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。 + +### 最快响应时间法 + +不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。 + +这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。 + +## 七层负载均衡可以怎么做? + +简单介绍两种项目中常用的七层负载均衡解决方案:DNS 解析和反向代理。 + +除了我介绍的这两种解决方案之外,HTTP 重定向等手段也可以用来实现负载均衡,不过,相对来说,还是 DNS 解析和反向代理用的更多一些,也更推荐一些。 + +### DNS 解析 + +DNS 解析是比较早期的七层负载均衡实现方式,非常简单。 + +DNS 解析实现负载均衡的原理是这样的:在 DNS 服务器中为同一个主机记录配置多个 IP 地址,这些 IP 地址对应不同的服务器。当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/6997605302452f07e8b28d257d349bf0.png) + +现在的 DNS 解析几乎都支持 IP 地址的权重配置,这样的话,在服务器性能不等的集群中请求分配会更加合理化。像我自己目前正在用的阿里云 DNS 就支持权重配置。 + +![](https://oss.javaguide.cn/github/javaguide/aliyun-dns-weight-setting.png) + +### 反向代理 + +客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。 + +Nginx 就是最常用的反向代理服务器,它可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上。 + +反向代理负载均衡同样属于七层负载均衡。 + +![](https://oss.javaguide.cn/github/javaguide/nginx-load-balance.png) + +## 客户端负载均衡通常是怎么做的? + +我们上面也说了,客户端负载均衡可以使用现成的负载均衡组件来实现。 + +**Netflix Ribbon** 和 **Spring Cloud Load Balancer** 就是目前 Java 生态最流行的两个负载均衡组件。 + +Ribbon 是老牌负载均衡组件,由 Netflix 开发,功能比较全面,支持的负载均衡策略也比较多。 Spring Cloud Load Balancer 是 Spring 官方为了取代 Ribbon 而推出的,功能相对更简单一些,支持的负载均衡也少一些。 + +Ribbon 支持的 7 种负载均衡策略: + +- `RandomRule`:随机策略。 +- `RoundRobinRule`(默认):轮询策略 +- `WeightedResponseTimeRule`:权重(根据响应时间决定权重)策略 +- `BestAvailableRule`:最小连接数策略 +- `RetryRule`:重试策略(按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null) +- `AvailabilityFilteringRule`:可用敏感性策略(先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例) +- `ZoneAvoidanceRule`:区域敏感性策略(根据服务所在区域的性能和服务的可用性来选择服务实例) + +Spring Cloud Load Balancer 支持的 2 种负载均衡策略: + +- `RandomLoadBalancer`:随机策略 +- `RoundRobinLoadBalancer`(默认):轮询策略 + +```java +public class CustomLoadBalancerConfiguration { + + @Bean + ReactorLoadBalancer randomLoadBalancer(Environment environment, + LoadBalancerClientFactory loadBalancerClientFactory) { + String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + return new RandomLoadBalancer(loadBalancerClientFactory + .getLazyProvider(name, ServiceInstanceListSupplier.class), + name); + } +} +``` + +However, Spring Cloud Load Balancer actually supports more than these two load balancing strategies. The implementation class of ServiceInstanceListSupplier also allows it to support load balancing strategies similar to Ribbon. This should be gradually improved and introduced in the future. You really can’t find it without reading the official documents, so reading the official documents is really important! + +Here are two official examples: + +- `ZonePreferenceServiceInstanceListSupplier`: implements zone-based load balancing +- `HintBasedServiceInstanceListSupplier`: implements load balancing based on hint hints + +```java +public class CustomLoadBalancerConfiguration { + //Use zone-based load balancing method + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withZonePreference() + .withCaching() + .build(context); + } +} +``` + +For a more detailed and updated introduction to Spring Cloud Load Balancer, it is recommended that you read the official document: . Everything is based on the official document. + +The polling strategy can basically meet the needs of most projects. If there are no special needs in our actual projects, the default polling strategy is usually used. Moreover, both Ribbon and Spring Cloud Load Balancer support custom load balancing strategies. + +Personally, I suggest that if you don’t need any of Ribbon’s unique features or load balancing strategies, you should give priority to the Spring Cloud Load Balancer officially provided by Spring. + +Finally, let me talk about why I don’t recommend using Ribbon. + +Spring Cloud 2020.0.0 version removes all Netflix components except Eureka. Spring Cloud Hoxton.M2 ​​is the first version to support Spring Cloud Load Balancer as a replacement for Netfix Ribbon. + +In our early days of learning microservices, we must have come into contact with the components necessary for building well-known microservice systems such as Netflix's open source Feign, Ribbon, Zuul, Hystrix, and Eureka. Many companies are still using these components today. It is no exaggeration to say that Netflix has led the development of microservices under the Java technology stack. + +![](https://oss.javaguide.cn/github/javaguide/SpringCloudNetflix.png) + +**So why is Spring Cloud so eager to remove Netflix components? ** Mainly because in 2018, Netflix announced that its open source core components Hystrix, Ribbon, Zuul, Eureka, etc. would enter maintenance status and would no longer develop new features and would only fix bugs. As a result, Spring officials had to consider removing the Netflix component. + +**Spring Cloud Alibaba** is a good choice, especially for domestic companies and individual developers. + +## Reference + +- Dry information | eBay’s 4-layer software load balancing implementation: +- HTTP Load Balancing (Nginx official documentation): +- A simple explanation of load balancing - vivo Internet technology: + + \ No newline at end of file diff --git a/docs_en/high-performance/message-queue/disruptor-questions.en.md b/docs_en/high-performance/message-queue/disruptor-questions.en.md new file mode 100644 index 00000000000..86c2a6278bb --- /dev/null +++ b/docs_en/high-performance/message-queue/disruptor-questions.en.md @@ -0,0 +1,138 @@ +--- +title: Summary of Disruptor FAQs +category: high performance +tag: + - Message queue +--- + +Disruptor is a relatively unpopular knowledge point, but if you have used Disruptor in your project experience, you are likely to be asked about it during the interview. + +The interview (social recruitment) submitted by a golfer before involved some Disruptor issues. The article portal: [Dream Come True! Successfully received offers from major manufacturers such as 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) + +This article can be seen as a brief summary of Disruptor. Each question will not go too in-depth. It is mainly aimed at interviews or a quick overview of Disruptor. + +## What is a Disruptor? + +Disruptor is an open source high-performance memory queue. It was originally created to solve the performance and memory safety issues of memory queues. It was developed by the British foreign exchange trading company LMAX. + +According to Disruptor’s official introduction, a single thread can support 6 million orders per second based on the system LMAX (new retail financial trading platform) developed by Disruptor. Martin Fowler specifically introduced the architecture of this LMAX system in an article [The LMAX Architecture](https://martinfowler.com/articles/lmax.html) written in 2011. If you are interested, you can read this article. . + +After LMAX's speech at QCon in 2010, Disruptor gained industry attention and won Oracle's official Duke's Choice Awards in 2011. + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/640.png) + +> The "Duke Choice Award" aims to recognize the most influential Java technology applications developed by individuals or companies around the world in the past year, and is hosted by Oracle. The gold content is very high! + +I specifically found the article that Oracle officially announced that it won the Duke's Choice Awards project (article address: . As can be seen from the article, other projects that won this award in the same year were famous projects such as Netty and JRebel. + +![2011 Oracle's official Duke's Choice Awards](https://oss.javaguide.cn/javaguide/image-20211015152323898.png) + +The functional advantages provided by Disruptor are similar to distributed queues such as Kafka and RocketMQ, but its scope is JVM (memory). + +- Github address: +- Official tutorial: + +Regarding how to use Disruptor in Spring Boot projects, you can read this article: [Spring Boot + Disruptor Practical Introduction](https://mp.weixin.qq.com/s/0iG5brK3bYF0BgSjX4jRiA). + +## Why use Disruptor? + +Disruptor mainly solves the performance and memory safety issues of JDK's built-in thread-safe queue. + +**Common thread-safe queues in JDK are as follows**: + +| Queue name | Lock | Whether bounded | +| ----------------------- | ----------------------- | -------- | +| `ArrayBlockingQueue` | Lock(`ReentrantLock`) | Bounded | +| `LinkedBlockingQueue` | Lock(`ReentrantLock`) | Bounded | +| `LinkedTransferQueue` | Lock-free (`CAS`) | Unbounded | +| `ConcurrentLinkedQueue` | Lock-free (`CAS`) | Unbounded | + +As can be seen from the above table: these queues are either locked and bounded, or they are lock-free and unbounded. Locked queues will inevitably affect performance, and unbounded queues run the risk of memory overflow. + +Therefore, under normal circumstances, we do not recommend using the JDK's built-in thread-safe queue. + +**Disruptor is different! It also ensures that the queue is bounded without locking and is thread-safe. ** + +The picture below is a comparison of the delay histograms of 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 is really fast. The question of why it is so fast will be introduced later. + +In addition, Disruptor also provides rich extension functions such as supporting batch operations and supporting multiple waiting strategies. + +## What is the difference between Kafka and Disruptor? + +- **Kafka**: Distributed message queue, generally used for message passing between systems or services, and can also be used as a streaming processing platform. +- **Disruptor**: Memory-level message queue, generally used for message passing between threads within the system. + +## Which components use Disruptor? + +There are quite a few open source projects that use Disruptor. Here are a few examples: + +- **Log4j2**: Log4j2 is a commonly used logging framework that implements asynchronous logging based on Disruptor. +- **SOFATracer**: SOFATracer is Ant Financial’s open source distributed application link tracking tool. It is based on Disruptor to implement asynchronous logs. +- **Storm**: Storm is an open source distributed real-time computing system that is based on Disruptor to implement message passing within the worker process (no network communication is required between threads on the same Storm node). +- **HBase**: HBase is a distributed column storage database system based on Disruptor to improve write concurrency performance. +-… + +## What are the core concepts of Disruptor? + +- **Event**: You can understand Event as a message object stored in the queue waiting to be consumed. +- **EventFactory**: The event factory is used to produce events, we need to use it when initializing the `Disruptor` class. +- **EventHandler**: Event is processed in the corresponding Handler, which you can understand as a consumer in the production-consumer model. +- **EventProcessor**: EventProcessor holds the Sequence of a specific consumer (Consumer) and provides an event loop (Event Loop) for calling event processing implementation. +- **Disruptor**: The production and consumption of events require the use of `Disruptor` objects. +- **RingBuffer**: RingBuffer (ring array) is used to save events. +- **WaitStrategy**: Wait strategy. Determines how event consumers wait for the arrival of new events when there are no events to consume. +- **Producer**: Producer, just generally refers to the user code that calls the `Disruptor` object to publish events. Disruptor does not define a specific interface or type. +- **ProducerType**: Specify whether it is a single event publisher mode or multiple event publisher mode (publisher and producer have similar meanings, I personally prefer to use publisher). +- **Sequencer**: Sequencer is the true core of Disruptor. This interface has two implementation classes `SingleProducerSequencer` and `MultiProducerSequencer`, which define concurrency algorithms for fast and correct data transfer between producers and consumers. + +The picture below is taken from the Disruptor official website and shows an example of the LMAX system using Disruptor. + +![Example of using Disruptor in LMAX system](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/disruptor-models.png) + +## What are the waiting strategies for Disruptor? + +**WaitStrategy** determines how event consumers wait for the arrival of new events when there are no events to consume. + +Common waiting strategies include the following:![Disruptor Wait Strategy](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/DisruptorWaitStrategy.png) + +- `BlockingWaitStrategy`: Based on `ReentrantLock`+`Condition` to implement waiting and wake-up operations, the implementation code is very simple and is the default waiting strategy of Disruptor. Although the slowest, it is also the option with the lowest CPU usage and the most stable. Recommended for production environments; +- `BusySpinWaitStrategy`: The performance is very good, but there is a risk of continuous spinning. Improper use will cause the CPU load to be 100%, so use with caution; +- `LiteBlockingWaitStrategy`: A lightweight waiting strategy based on `BlockingWaitStrategy`, which will omit the wake-up operation when there is no lock competition. However, the author said that the test is insufficient, so it is not recommended to use; +- `TimeoutBlockingWaitStrategy`: Waiting strategy with timeout, the business-specified processing logic will be executed after timeout; +- `LiteTimeoutBlockingWaitStrategy`: Based on the strategy of `TimeoutBlockingWaitStrategy`, the wake-up operation will be omitted when there is no lock competition; +- `SleepingWaitStrategy`: three-stage strategy, the first stage is spin, the second stage executes Thread.yield to give up the CPU, the third stage sleep execution time, and sleeps repeatedly; +- `YieldingWaitStrategy`: two-stage strategy, the first stage is spinning, and the second stage executes Thread.yield to hand over the CPU; +- `PhasedBackoffWaitStrategy`: four-stage strategy, the first stage spins for a specified number of times, the second stage spins for a specified time, the third stage executes `Thread.yield` to hand over the CPU, and the fourth stage calls the `waitFor` method of the member variable. The member variable can be set to one of the three `BlockingWaitStrategy`, `LiteBlockingWaitStrategy`, and `SleepingWaitStrategy`. + +## Disruptor Why so fast? + +- **RingBuffer (Ring Array)**: The RingBuffer inside Disruptor is implemented through an array. Since all elements in this array are created at once during initialization, the memory addresses of these elements are generally consecutive. The advantage of this is that when the producer continuously inserts new event objects into the RingBuffer, the memory addresses of these event objects can remain continuous, thereby utilizing the locality principle of the CPU cache to load adjacent event objects into the cache together to improve program performance. This is similar to MySQL's read-ahead mechanism, which pre-reads several consecutive pages into memory. In addition, RingBuffer is array-based and supports batch operations (processing multiple elements at one time), and can also avoid frequent memory allocation and garbage collection (RingBuffer is a fixed-size array, when adding new elements to the array, if the array is full, the new elements will overwrite the oldest elements). +- **Avoid false sharing problem**: The CPU cache is internally managed according to Cache Line (cache line). The general Cache Line size is about 64 bytes. In order to ensure that the target field occupies an exclusive Cache Line, the Disruptor will add byte padding (the first 56 bytes and the last 56 bytes) before and after the target field. This can avoid the problem of false sharing (False Sharing) of the Cache Line. At the same time, in order to allow the RingBuffer array to store data to occupy the cache line exclusively, the array is designed as invalid filling (128 bytes) + valid data. +- **Lock-free design**: Disruptor adopts a lock-free design to avoid competition and delays caused by traditional lock mechanisms. The lock-free implementation of Disruptor is relatively complex and is mainly implemented based on CAS, Memory Barrier, RingBuffer and other technologies. + +To sum up, the reason why Disruptor can be so fast is based on the comprehensive effect of a series of optimization strategies, which not only make full use of the characteristics of modern CPU cache structures, but also avoid common concurrency problems and performance bottlenecks. + +For a detailed introduction to the principle of Disruptor's high-performance queue, you can view this article: [A Brief Analysis of the Principle of Disruptor's High-Performance Queue](https://qin.news/disruptor/) (Refer to the article [High-Performance Queue - Disruptor](https://tech.meituan.com/2016/11/18/disruptor.html) by the Meituan technical team). + +🌈 Here is an additional point: **Why can continuous object element addresses in an array improve performance? ** + +CPU caching achieves faster reads by storing recently used data in cache, and uses a prefetch mechanism to load data from adjacent memory ahead of time to take advantage of the locality principle. + +In a computer system, the CPU primarily accesses cache and memory. Cache is a very fast, relatively small-capacity memory that is usually divided into multi-level caches, where L1, L2, and L3 represent the first-level cache, the second-level cache, and the third-level cache respectively. The closer the cache is to the CPU, the faster it is and the smaller its capacity. In comparison, the memory capacity is relatively large, but the speed is slower. + +![CPU cache model diagram](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) + +In order to speed up the data reading process, the CPU will first load the data from the memory into the cache. If the same data needs to be accessed next time, it can be read directly from the cache without accessing the memory again. This is called a **cache hit**. In addition, in order to take advantage of the **locality principle**, the CPU will also prefetch adjacent memory data based on the previously accessed memory address, because in the program, consecutive memory addresses are usually accessed frequently. This can improve the cache hit rate of the data, thereby improving the performance of the program. + +## Reference + +- Disruptor The way to high performance - waiting strategy: < The way to high performance - waiting strategy/> +- "Java Concurrent Programming in Practice" - 40 | Case Analysis (3): High-Performance Queue Disruptor: + + \ No newline at end of file diff --git a/docs_en/high-performance/message-queue/kafka-questions-01.en.md b/docs_en/high-performance/message-queue/kafka-questions-01.en.md new file mode 100644 index 00000000000..9599e6484c4 --- /dev/null +++ b/docs_en/high-performance/message-queue/kafka-questions-01.en.md @@ -0,0 +1,441 @@ +--- +title: Kafka常见问题总结 +category: 高性能 +tag: + - 消息队列 +--- + +## Kafka 基础 + +### Kafka 是什么?主要应用场景有哪些? + +Kafka 是一个分布式流式处理平台。这到底是什么意思呢? + +流平台具有三个关键功能: + +1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 +2. **容错的持久方式存储记录消息流**:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。 +3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 + +Kafka 主要有两大应用场景: + +1. **消息队列**:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。 +2. **数据处理:** 构建实时的流数据处理程序来转换或处理数据流。 + +### 和其他消息队列相比,Kafka 的优势在哪里? + +我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它跟 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下: + +1. **极致的性能**:基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。 +2. **生态系统兼容性无可匹敌**:Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。 + +实际上在早期的时候 Kafka 并不是一个合格的消息队列,早期的 Kafka 在消息队列领域就像是一个衣衫褴褛的孩子一样,功能不完备并且有一些小问题比如丢失消息、不保证消息可靠性等等。当然,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,哈哈哈,人家本来最开始就不是为了作为消息队列滴,谁知道后面误打误撞在消息队列领域占据了一席之地。 + +随着后续的发展,这些短板都被 Kafka 逐步修复完善。所以,**Kafka 作为消息队列不可靠这个说法已经过时!** + +### 队列模型了解吗?Kafka 的消息模型知道吗? + +> 题外话:早期的 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)这篇文章中介绍过。但是,这些标准的进化跟不上消息队列的演进速度,这些标准实际上已经属于废弃状态。所以,可能存在的情况是:不同的消息队列都有自己的一套消息模型。 + +#### 队列模型:早期的消息模型 + +![队列模型](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 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) + +**队列模型存在的问题:** + +假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完整的消息内容。 + +这种情况,队列模型就不好解决了。很多比较杠精的人就说:我们可以为每个消费者创建一个单独的队列,让生产者发送多份。这是一种非常愚蠢的做法,浪费资源不说,还违背了使用消息队列的目的。 + +#### 发布-订阅模型:Kafka 消息模型 + +发布-订阅模型主要是为了解决队列模型存在的问题。 + +![发布订阅模型](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)** 作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 + +**在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。** + +**Kafka 采用的就是发布 - 订阅模型。** + +> **RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。** + +## Kafka 核心概念 + +### 什么是 Producer、Consumer、Broker、Topic、Partition? + +Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这些消息的消费者可以订阅这些 **Topic(主题)**,如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue20210507200944439.png) + +上面这张图也为我们引出了,Kafka 比较重要的几个概念: + +1. **Producer(生产者)** : 产生消息的一方。 +2. **Consumer(消费者)** : 消费消息的一方。 +3. **Broker(代理)** : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。 + +同时,你一定也注意到每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念: + +- **Topic(主题)** : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。 +- **Partition(分区)** : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。这正如我上面所画的图一样。 + +> 划重点:**Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。这样是不是更好理解一点?** + +### Kafka 的多副本机制了解吗?带来了什么好处? + +还有一点我觉得比较重要的是 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。 + +> 生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。 + +**Kafka 的多分区(Partition)以及多副本(Replica)机制有什么好处呢?** + +1. Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。 +2. Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。 + +## Zookeeper 和 Kafka + +### Zookeeper 在 Kafka 中的作用是什么? + +> 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章: 。 + +下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。 + + + +ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 + +从图中我们可以看出,Zookeeper 主要为 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. …… + +### 使用 Kafka 能否不引入 Zookeeper? + +在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。 + +不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) + +## Kafka 消费顺序、消息丢失和重复消费 + +### Kafka 如何保证消息的消费顺序? + +我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是: + +1. 更改用户会员等级。 +2. 根据会员等级计算订单价格。 + +假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。 + +我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/KafkaTopicPartionsLayout.png) + +每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 **Kafka 只能为我们保证 Partition(分区) 中的消息有序。** + +> 消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。 + +所以,我们就有一种很简单的保证消息消费顺序的方法:**1 个 Topic 只对应一个 Partition**。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。 + +Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。 + +总结一下,对于如何保证 Kafka 中消息消费的顺序,有了下面两种方法: + +1. 1 个 Topic 只对应一个 Partition。 +2. (推荐)发送消息的时候指定 key/Partition。 + +当然不仅仅只有上面两种方法,上面两种方法是我觉得比较好理解的, + +### Kafka 如何保证消息不丢失? + +#### 生产者丢失消息的情况 + +生产者(Producer) 调用`send`方法发送消息之后,消息可能因为网络问题并没有发送过去。 + +所以,我们不能默认在调用`send`方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 `send` 方法发送消息实际上是异步的操作,我们可以通过 `get()`方法获取调用结果,但是这样也让它变为了同步操作,示例代码如下: + +> **详细代码见我的这篇文章:[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)** + +```java +SendResult sendResult = kafkaTemplate.send(topic, o).get(); +if (sendResult.getRecordMetadata() != null) { + logger.info("生产者成功发送消息到" + sendResult.getProducerRecord().topic() + "-> " + sendRe + sult.getProducerRecord().value().toString()); +} +``` + +但是一般不推荐这么做!可以采用为其添加回调函数的形式,示例代码如下: + +```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())); +``` + +如果消息发送失败的话,我们检查失败的原因之后重新发送即可! + +另外,这里推荐为 Producer 的`retries`(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。 + +#### 消费者丢失消息的情况 + +我们知道消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。偏移量(offset)表示 Consumer 当前消费到的 Partition(分区)的所在的位置。Kafka 通过偏移量(offset)可以保证消息在分区内的顺序性。 + +![kafka offset](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka-offset.jpg) + +当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。 + +**解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。** 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。 + +#### Kafka 弄丢了消息 + +我们知道 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。 + +**试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。** + +**设置 acks = all** + +解决办法就是我们设置 **acks = all**。acks 是 Kafka 生产者(Producer) 很重要的一个参数。 + +acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后就算被成功发送。当我们配置 **acks = all** 表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应. 这种模式是最高级别的,也是最安全的,可以确保不止一个 Broker 接收到了消息. 该模式的延迟会很高. + +**设置 replication.factor >= 3** + +为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 **replication.factor >= 3**。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。 + +**设置 min.insync.replicas > 1** + +一般情况下我们还需要设置 **min.insync.replicas> 1** ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。**min.insync.replicas** 的默认值为 1 ,在实际生产中应尽量避免默认值 1。 + +但是,为了保证整个 Kafka 服务的高可用性,你需要确保 **replication.factor > min.insync.replicas** 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 **replication.factor = min.insync.replicas + 1**。 + +**设置 unclean.leader.election.enable = false** + +> **Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false** + +我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 **unclean.leader.election.enable = false** 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。 + +### Kafka 如何保证消息不重复消费? + +**kafka 出现消息重复消费的原因:** + +- 服务端侧已经消费的数据没有成功提交 offset(根本原因)。 +- Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。 + +**解决方案:** + +- 消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。 +- 将 **`enable.auto.commit`** 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。那么这里会有个问题:**什么时候提交 offset 合适?** + - 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样 + - 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。 + +## Kafka 重试机制 + +在 Kafka 如何保证消息不丢失这里,我们提到了 Kafka 的重试机制。由于这部分内容较为重要,我们这里再来详细介绍一下。 + +网上关于 Spring Kafka 的默认重试机制文章很多,但大多都是过时的,和实际运行结果完全不一样。以下是根据 [spring-kafka-2.9.3](https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka/2.9.3) 源码重新梳理一下。 + +### 消费失败会怎么样? + +在消费过程中,当其中一个消息消费异常时,会不会卡住后续队列消息的消费?这样业务岂不是卡住了? + +生产者代码: + +```Java + for (int i = 0; i < 10; i++) { + kafkaTemplate.send(KafkaConst.TEST_TOPIC, String.valueOf(i)) + } +``` + +消费者消代码: + +```Java + @KafkaListener(topics = {KafkaConst.TEST_TOPIC},groupId = "apple") + private void customer(String message) throws InterruptedException { + log.info("kafka customer:{}",message); + Integer n = Integer.parseInt(message); + if (n%5==0){ + throw new RuntimeException(); + } + } +``` + +在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。下面是一段消费的日志,可以看出当 `test-0@95` 重试多次后会被跳过。 + +```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 + +``` + +因此,即使某个消息消费异常,Kafka 消费者仍然能够继续消费后续的消息,不会一直卡在当前消息,保证了业务的正常进行。 + +### 默认会重试多少次? + +默认配置下,消费异常会进行重试,重试次数是多少, 重试是否有时间间隔? + +看源码 `FailedRecordTracker` 类有个 `recovered` 函数,返回 Boolean 值判断是否要进行重试,下面是这个函数中判断是否重试的逻辑: + +```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())); + // 获取下一次重试的时间间隔 + 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; + } + } +``` + +Among them, the value of `BackOffExecution.STOP` is -1. + +```java +@FunctionalInterface +public interface BackOffExecution { + + long STOP = -1; + long nextBackOff(); + +} +``` + +The value of `nextBackOff` calls the `nextBackOff()` function of the `BackOff` class. If the current number of executions is greater than the maximum number of executions, `STOP` will be returned. Retry will not stop until the maximum number of executions is exceeded. + +```Java +public long nextBackOff() { + this.currentAttempts++; + if (this.currentAttempts <= getMaxAttempts()) { + return getInterval(); + } + else { + return STOP; + } +} +``` + +So what is the value of `getMaxAttempts`? Back to the beginning, when an execution error occurs, it will enter `DefaultErrorHandler`. The default constructor of `DefaultErrorHandler` is: + +```Java +public DefaultErrorHandler() { + this(null, SeekUtils.DEFAULT_BACK_OFF); +} +``` + +`SeekUtils.DEFAULT_BACK_OFF` defines: + +```Java +public static final int DEFAULT_MAX_FAILURES = 10; + +public static final FixedBackOff DEFAULT_BACK_OFF = new FixedBackOff(0, DEFAULT_MAX_FAILURES - 1); +``` + +The value of `DEFAULT_MAX_FAILURES` is 10 and `currentAttempts` is from 0 to 9, so it will be executed 10 times in total, with a retry interval of 0. + +Finally, a brief summary: Kafka consumers will retry up to 10 times under the default configuration, and the time interval between each retry is 0, that is, retry immediately. If the message cannot be successfully consumed after 10 retries, no more retries will be made and the message will be considered a consumption failure. + +### How to customize the number of retries and time interval? + +From the above code, we can know that the number of retries and time interval of the default error handler are controlled by `FixedBackOff`, which is the default when `DefaultErrorHandler` is initialized. Therefore, to customize the number of retries and time intervals, you only need to pass in the custom `FixedBackOff` when `DefaultErrorHandler` is initialized. This can be achieved by reimplementing a `KafkaListenerContainerFactory` and calling `setCommonErrorHandler` to set a new custom error handler. + +```Java +@Bean +public KafkaListenerContainerFactory kafkaListenerContainerFactory(ConsumerFactory consumerFactory) { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); + // Customize the retry interval and number of times + FixedBackOff fixedBackOff = new FixedBackOff(1000, 5); + factory.setCommonErrorHandler(new DefaultErrorHandler(fixedBackOff)); + factory.setConsumerFactory(consumerFactory); + return factory; +} +``` + +### How to alert after failed retry? + +Customizing the logic after failed retry needs to be implemented manually. The following is a simple example of rewriting the `handleRemaining` function of `DefaultErrorHandler` and adding custom alarms and other operations. + +```Java +@Slf4j +public class DelErrorHandler extends DefaultErrorHandler { + + public DelErrorHandler(FixedBackOff backOff) { + super(null,backOff); + } + + @Override + public void handleRemaining(Exception thrownException, List> records, Consumer consumer, MessageListenerContainer container) { + super.handleRemaining(thrownException, records, consumer, container); + log.info("Retry multiple times failed"); + // Custom operation + } +} +``` + +`DefaultErrorHandler` is just a default error handler, Spring Kafka also provides `CommonErrorHandler` interface. Manually implementing `CommonErrorHandler` can achieve more customized operations and has high flexibility. For example, implement different retry logic and business logic according to different error types. + +### How to process data after failed retry? + +When the maximum number of retries is reached, the data will be skipped directly and the process will continue backwards. After the code is repaired, how to re-consume the data that failed to be retried? + +**Dead Letter Queue (DLQ for short)** is a special queue in message middleware. It is mainly used to process messages that cannot be processed correctly by consumers, usually because messages are "discarded" or "dead" due to message format errors, processing failures, consumption timeouts, etc. When a message enters the queue, the consumer attempts to process it. If processing fails, or cannot be successfully processed after a certain number of retries, the message can be sent to the dead letter queue instead of being permanently discarded. In the dead letter queue, these messages that cannot be consumed normally can be further analyzed and processed in order to locate problems, fix errors, and take appropriate measures. + +`@RetryableTopic` is an annotation in Spring Kafka. It is used to configure a certain Topic to support message retry. It is more recommended to use this annotation to complete retry. + +```Java +//Retry 5 times, retry interval 100 milliseconds, maximum interval 1 second +@RetryableTopic( + attempts = "5", + backoff = @Backoff(delay = 100, maxDelay = 1000) +) +@KafkaListener(topics = {KafkaConst.TEST_TOPIC}, groupId = "apple") +private void customer(String message) { + log.info("kafka customer:{}", message); + Integer n = Integer.parseInt(message); + if (n % 5 == 0) { + throw new RuntimeException(); + } + System.out.println(n); +} +``` + +When the maximum number of retries is reached, if the message still cannot be processed successfully, the message will be sent to the corresponding dead letter queue. For the processing of the dead letter queue, you can either use `@DltHandler` to process it, or you can use `@KafkaListener` to re-consume it. + +## Reference + +- Kafka official documentation: +- Geek Time—"Kafka Core Technology and Practical Combat" Section 11: How to achieve no message loss configuration? + + \ No newline at end of file diff --git a/docs_en/high-performance/message-queue/message-queue.en.md b/docs_en/high-performance/message-queue/message-queue.en.md new file mode 100644 index 00000000000..e7d56151b2a --- /dev/null +++ b/docs_en/high-performance/message-queue/message-queue.en.md @@ -0,0 +1,314 @@ +--- +title: 消息队列基础知识总结 +category: 高性能 +tag: + - 消息队列 +--- + +::: tip + +这篇文章中的消息队列主要指的是分布式消息队列。 + +::: + +“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。 + +如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 + +## 什么是消息队列? + +我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-small.png) + +参与消息传递的双方称为 **生产者** 和 **消费者** ,生产者负责发送消息,消费者负责处理消息。 + +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) + +操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。 + +维基百科是这样介绍中间件的: + +> 中间件(英语:Middleware),又译中间件、中介层,是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。 + +简单来说:**中间件就是一类为应用软件服务的软件,应用软件是为用户服务的,用户不会接触或者使用到中间件。** + +除了消息队列之外,常见的中间件还有 RPC 框架、分布式组件、HTTP 服务器、任务调度框架、配置中心、数据库层的分库分表工具和数据迁移工具等等。 + +关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答: 。 + +随着分布式和微服务系统的发展,消息队列在系统设计中有了更大的发挥空间,使用消息队列可以降低系统耦合性、实现任务异步、有效地进行流量削峰,是分布式和微服务系统中重要的组件之一。 + +## 消息队列有什么用? + +通常来说,使用消息队列主要能为我们的系统带来下面三点好处: + +1. 异步处理 +2. 削峰/限流 +3. 降低系统耦合性 + +除了这三点之外,消息队列还有其他的一些应用场景,例如实现分布式事务、顺序保证和数据流处理。 + +如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 + +### 异步处理 + +![通过异步处理提高系统性能](https://oss.javaguide.cn/github/javaguide/Asynchronous-message-queue.png) + +将用户请求中包含的耗时操作,通过消息队列实现异步处理,将对应的消息发送到消息队列之后就立即返回结果,减少响应时间,提高用户体验。随后,系统再对消息进行消费。 + +因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,**使用消息队列进行异步处理之后,需要适当修改业务流程进行配合**,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 + +### 削峰/限流 + +**先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。** + +举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: + +![削峰](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) + +### 降低系统耦合性 + +使用消息队列还可以降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 + +生产者(客户端)发送消息到消息队列中去,消费者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。 + +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) + +**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 + +例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消费即可。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-decouple-mall-example.png) + +另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。 + +**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了另外 5 种消息模型。 + +### 实现分布式事务 + +分布式事务的解决方案之一就是 MQ 事务。 + +RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。 + +详细介绍可以查看 [分布式事务详解(付费)](https://javaguide.cn/distributed-system/distributed-transaction.html) 这篇文章。 + +![分布式事务详解 - MQ事务](https://oss.javaguide.cn/github/javaguide/csdn/07b338324a7d8894b8aef4b659b76d92.png) + +### 顺序保证 + +在很多应用场景中,处理数据的顺序至关重要。消息队列保证数据按照特定的顺序被处理,适用于那些对数据顺序有严格要求的场景。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持顺序消息。 + +### 延时/定时处理 + +消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar,都支持定时/延时消息。 + +![](https://oss.javaguide.cn/github/javaguide/tools/docker/rocketmq-schedule-message.png) + +### 即时通讯 + +MQTT(消息队列遥测传输协议)是一种轻量级的通讯协议,采用发布/订阅模式,非常适合于物联网(IoT)等需要在低带宽、高延迟或不可靠网络环境下工作的应用。它支持即时消息传递,即使在网络条件较差的情况下也能保持通信的稳定性。 + +RabbitMQ 内置了 MQTT 插件用于实现 MQTT 功能(默认不启用,需要手动开启)。 + +### 数据流处理 + +针对分布式系统产生的海量数据流,如业务日志、监控数据、用户行为等,消息队列可以实时或批量收集这些数据,并将其导入到大数据处理引擎中,实现高效的数据流管理和处理。 + +## 使用消息队列会带来哪些问题? + +- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! +- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! +- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! + +## JMS 和 AMQP + +### JMS 是什么? + +JMS(JAVA Message Service,java 消息服务)是 Java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。**JMS(JAVA Message Service,Java 消息服务)API 是一个消息服务的标准或者说是规范**,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 + +JMS 定义了五种不同的消息正文格式以及调用的消息类型,允许你发送并接收以一些不同形式的数据: + +- `StreamMessage:Java` 原始值的数据流 +- `MapMessage`:一套名称-值对 +- `TextMessage`:一个字符串对象 +- `ObjectMessage`:一个序列化的 Java 对象 +- `BytesMessage`:一个字节的数据流 + +**ActiveMQ(已被淘汰) 就是基于 JMS 规范实现的。** + +### JMS 两种消息模型 + +#### 点到点(P2P)模型 + +![队列模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-queue-model.png) + +使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) + +#### 发布/订阅(Pub/Sub)模型 + +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) + +发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者。 + +### AMQP 是什么? + +AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 + +**RabbitMQ 就是基于 AMQP 协议实现的。** + +### JMS vs AMQP + +| 对比方向 | JMS | AMQP | +| :----------: | :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 定义 | Java API | 协议 | +| 跨语言 | 否 | 是 | +| 跨平台 | 否 | 是 | +| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub | 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和 JMS 的 pub/sub 模型没有太大差别,仅是在路由机制上做了更详细的划分; | +| 支持消息类型 | 支持多种消息类型 ,我们在上面提到过 | byte[](二进制) | + +**总结:** + +- AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。 +- JMS 支持 `TextMessage`、`MapMessage` 等复杂的消息类型;而 AMQP 仅支持 `byte[]` 消息类型(复杂的类型可序列化后发送)。 +- 由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 + +## RPC 和消息队列的区别 + +RPC 和消息队列都是分布式微服务系统中重要的组件之一,下面我们来简单对比一下两者: + +- **从用途来看**:RPC 主要用来解决两个服务的远程通信问题,不需要了解底层网络的通信机制。通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。 +- **从通信方式来看**:RPC 是双向直接网络通讯,消息队列是单向引入中间载体的网络通讯。 +- **从架构上来看**:消息队列需要把消息存储起来,RPC 则没有这个要求,因为前面也说了 RPC 是双向直接网络通讯。 +- **从请求处理的时效性来看**:通过 RPC 发出的调用一般会立即被处理,存放在消息队列中的消息并不一定会立即被处理。 + +RPC 和消息队列本质上是网络通讯的两种不同的实现机制,两者的用途不同,万不可将两者混为一谈。 + +## 分布式消息队列技术选型 + +### 常见的消息队列有哪些? + +#### Kafka + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka-logo.png) + +Kafka 是 LinkedIn 开源的一个分布式流式处理平台,已经成为 Apache 顶级项目,早期被用来用于处理海量的日志,后面才慢慢发展成了一款功能全面的高性能消息队列。 + +流式处理平台具有三个关键功能: + +1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 +2. **容错的持久方式存储记录消息流**:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。 +3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 + +Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信的服务器和客户端组成,可以部署在在本地和云环境中的裸机硬件、虚拟机和容器上。 + +在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。 + +不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) + +Kafka 官网: + +Kafka 更新记录(可以直观看到项目是否还在维护): + +#### RocketMQ + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/rocketmq-logo.png) + +RocketMQ 是阿里开源的一款云原生“消息、事件、流”实时数据处理平台,借鉴了 Kafka,已经成为 Apache 顶级项目。 + +RocketMQ 的核心特性(摘自 RocketMQ 官网): + +- 云原生:生与云,长与云,无限弹性扩缩,K8s 友好 +- 高吞吐:万亿级吞吐保证,同时满足微服务与大数据场景。 +- 流处理:提供轻量、高扩展、高性能和丰富功能的流计算引擎。 +- 金融级:金融级的稳定性,广泛用于交易核心链路。 +- 架构极简:零外部依赖,Shared-nothing 架构。 +- 生态友好:无缝对接微服务、实时计算、数据湖等周边生态。 + +根据官网介绍: + +> Apache RocketMQ 自诞生以来,因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨,RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案,被广泛应用于互联网、大数据、移动互联网、物联网等领域的业务场景。 + +RocketMQ 官网: (文档很详细,推荐阅读) + +RocketMQ 更新记录(可以直观看到项目是否还在维护): + +#### RabbitMQ + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/rabbitmq-logo.png) + +RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。 + +RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性、扩展性、可靠性和高可用性等方面的卓著表现是分不开的。RabbitMQ 的具体特点可以概括为以下几点: + +- **可靠性:** RabbitMQ 使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。 +- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们讲 RabbitMQ 核心概念的时候详细介绍到。 +- **扩展性:** 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 +- **高可用性:** 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。 +- **支持多种协议:** RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。 +- **多语言客户端:** RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。 +- **易用的管理界面:** RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。 +- **插件机制:** RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制 + +RabbitMQ 官网: 。 + +RabbitMQ 更新记录(可以直观看到项目是否还在维护): + +#### Pulsar + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/pulsar-logo.png) + +Pulsar 是下一代云原生分布式消息流平台,最初由 Yahoo 开发 ,已经成为 Apache 顶级项目。 + +Pulsar 集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。 + +Pulsar 的关键特性如下(摘自官网): + +- 是下一代云原生分布式消息流平台。 +- Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。 +- 极低的发布延迟和端到端延迟。 +- 可无缝扩展到超过一百万个 topic。 +- 简单的客户端 API,支持 Java、Go、Python 和 C++。 +- 主题的多种订阅模式(独占、共享和故障转移)。 +- 通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。 +- 由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。 +- 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。 +- 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。 + +Pulsar 官网: + +Pulsar 更新记录(可以直观看到项目是否还在维护): + +#### ActiveMQ + +目前已经被淘汰,不推荐使用,不建议学习。 + +### 如何选择? + +> 参考《Java 工程师面试突击第 1 季-中华石杉老师》 + +| 对比方向 | 概要 | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Throughput | The throughput of 10,000-level ActiveMQ and RabbitMQ (ActiveMQ has the worst performance) is an order of magnitude lower than 100,000-level or even million-level RocketMQ and Kafka. | +| Availability | High availability can be achieved. ActiveMQ and RabbitMQ are both based on master-slave architecture to achieve high availability. RocketMQ is based on a distributed architecture. Kafka is also distributed, with multiple copies of one data. Even if a few machines go down, there will be no data loss or unavailability | +| Timeliness | RabbitMQ is developed based on Erlang, so it has strong concurrency capabilities, extremely good performance, low latency, reaching microsecond level, and others are ms level. | +| Function support | Pulsar has more comprehensive functions, supports multi-tenancy, multiple consumption models, persistence modes and other functions. It is the next generation of cloud-native distributed message flow platform. | +| Message loss | ActiveMQ and RabbitMQ have a very low possibility of loss. Kafka, RocketMQ and Pulsar can theoretically achieve 0 loss. | + +**Summary:** + +- The ActiveMQ community is relatively mature, but compared to the current situation, ActiveMQ's performance is relatively poor, and version iterations are very slow. It is not recommended and has been eliminated. +- Although RabbitMQ is slightly inferior to Kafka, RocketMQ and Pulsar in terms of throughput, because it is developed based on Erlang, it has strong concurrency capabilities, extremely good performance, and very low latency, reaching the microsecond level. However, because RabbitMQ is developed based on Erlang, few domestic companies have the strength to do research and customization at the Erlang source code level. If the business scenario does not have too high concurrency requirements (100,000 or one million levels), then RabbitMQ may be your first choice among these message queues. +- RocketMQ and Pulsar support strong consistency and can be used in scenarios with high message consistency requirements. +- RocketMQ is produced by Alibaba and is a Java open source project. We can read the source code directly and then customize our own company's MQ. RocketMQ has been tested in Alibaba's actual business scenarios. +- The characteristics of Kafka are actually very obvious, that is, it only provides fewer core functions, but provides ultra-high throughput, ms-level latency, extremely high availability and reliability, and the distribution can be arbitrarily expanded. At the same time, it is best for Kafka to support a smaller number of topics to ensure ultra-high throughput. The only disadvantage of Kafka is that messages may be consumed repeatedly, which will have a very slight impact on data accuracy. In the field of big data and log collection, this slight impact can be ignored. This feature is naturally suitable for big data real-time computing and log collection. If it is real-time computing, log collection and other scenarios in the field of big data, using Kafka is the industry standard, and there is absolutely no problem. The community is very active and will definitely not be pornographic. Moreover, it is almost a de facto standard in this field around the world. + +## Reference + +- "Technical Architecture of Large Websites" +- KRaft: Apache Kafka Without ZooKeeper: +- What are the usage scenarios of message queue? : + + \ No newline at end of file diff --git a/docs_en/high-performance/message-queue/rabbitmq-questions.en.md b/docs_en/high-performance/message-queue/rabbitmq-questions.en.md new file mode 100644 index 00000000000..1aa21d14577 --- /dev/null +++ b/docs_en/high-performance/message-queue/rabbitmq-questions.en.md @@ -0,0 +1,247 @@ +--- +title: RabbitMQ常见问题总结 +category: 高性能 +tag: + - 消息队列 +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,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 +--- + +> 本篇文章由 JavaGuide 收集自网络,原出处不明。 + +## RabbitMQ 是什么? + +RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 + +RabbitMQ 是使用 Erlang 编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。它同时实现了一个 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队,对路由(Routing)、负载均衡(Load balance)或者数据持久化都有很好的支持。 + +PS:也可能直接问什么是消息队列?消息队列就是一个使用队列来通信的组件。 + +## RabbitMQ 特点? + +- **可靠性**: RabbitMQ 使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。 +- **灵活的路由** : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 +- **扩展性**: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。 +- **高可用性** : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。 +- **多种协议**: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP, MQTT 等多种消息 中间件协议。 +- **多语言客户端** :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。 +- **管理界面** : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。 +- **插件机制** : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。 + +## RabbitMQ 核心概念? + +RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。 + +RabbitMQ 的整体模型架构如下: + +![图1-RabbitMQ 的整体模型架构](https://oss.javaguide.cn/github/javaguide/rabbitmq/96388546.jpg) + +下面我会一一介绍上图中的一些概念。 + +### Producer(生产者) 和 Consumer(消费者) + +- **Producer(生产者)** :生产消息的一方(邮件投递者) +- **Consumer(消费者)** :消费消息的一方(邮件收件人) + +消息一般由 2 部分组成:**消息头**(或者说是标签 Label)和 **消息体**。消息体也可以称为 payLoad ,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。 + +### Exchange(交换器) + +在 RabbitMQ 中,消息并不是直接被投递到 **Queue(消息队列)** 中的,中间还必须经过 **Exchange(交换器)** 这一层,**Exchange(交换器)** 会把我们的消息分配到对应的 **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 消息怎么传输? + +Since the creation and destruction of TCP links is expensive, and the number of concurrencies is limited by system resources, it will cause a performance bottleneck, so RabbitMQ uses channels to transmit data. Channel is a channel for producers, consumers and RabbitMQ to communicate. A channel is a virtual link established on a TCP link, and there is no limit to the number of channels on each TCP link. That is to say, RabbitMQ establishes hundreds or thousands of channels on a TCP link to achieve multi-thread processing. This TCP is shared by multiple threads. Each channel has a unique ID in RabbitMQ, ensuring channel privacy. Each channel corresponds to one thread. + +## **How ​​to ensure the reliability of messages? ** + +The message is lost when it reaches MQ, MQ itself loses it, and MQ loses it when it is consumed. + +- Producer to RabbitMQ: Transaction mechanism and Confirm mechanism. Note: Transaction mechanism and Confirm mechanism are mutually exclusive. They cannot coexist, which will cause RabbitMQ to report an error. +- RabbitMQ itself: persistence, clustering, normal mode, mirror mode. +- RabbitMQ to consumer: basicAck mechanism, dead letter queue, message compensation mechanism. + +## How to ensure the order of RabbitMQ messages? + +- Split multiple queues (message queues), each queue (message queue) has a consumer (consumer), just more queues (message queues), which is indeed a troublesome point; +- Or just have a queue (message queue) but correspond to a consumer (consumer), and then the consumer (consumer) uses a memory queue for queuing, and then distributes it to different underlying workers for processing. + +## How to ensure RabbitMQ is highly available? + +RabbitMQ is relatively representative because it is based on master-slave (non-distributed) high availability. We will use RabbitMQ as an example to explain how to achieve high availability of the first type of MQ. RabbitMQ has three modes: stand-alone mode, normal cluster mode, and mirror cluster mode. + +**Stand-alone mode** + +Demo level, usually you start it locally for fun? No one uses stand-alone mode for production. + +**Normal cluster mode** + +This means starting multiple RabbitMQ instances on multiple machines, one for each machine. The queue you create will only be placed on one RabbitMQ instance, but each instance synchronizes the metadata of the queue (metadata can be thought of as some configuration information of the queue. Through the metadata, you can find the instance where the queue is located). + +When you consume, if you are actually connected to another instance, that instance will pull data from the instance where the queue is located. This solution is mainly to improve throughput, that is, to allow multiple nodes in the cluster to serve the read and write operations of a certain queue. + +**Mirror cluster mode** + +This mode is the so-called high availability mode of RabbitMQ. What is different from the ordinary cluster mode is that in the mirror cluster mode, the queue you create, regardless of the metadata or the messages in the queue, will exist on multiple instances. That is to say, each RabbitMQ node has a complete mirror of the queue, including all the data of the queue. Then every time you write a message to the queue, the message will be automatically synchronized to the queues of multiple instances. RabbitMQ has a good management console, which is to add a policy in the background. This policy is a mirror cluster mode policy. When specified, you can require data to be synchronized to all nodes, or to a specified number of nodes. When you create a queue again, apply this policy, and the data will be automatically synchronized to other nodes. + +The advantage of this is that if any of your machines goes down, it doesn't matter. Other machines (nodes) still contain the complete data of this queue, and other consumers can go to other nodes to consume data. The disadvantages are, first, the performance overhead is too high. The messages need to be synchronized to all machines, resulting in heavy network bandwidth pressure and consumption! In RabbitMQ, the data of a queue is placed in one node. Under the mirror cluster, each node also stores the complete data of this queue. + +## How to solve the message queue delay and expiration problems? + +RabbtiMQ can set the expiration time, which is TTL. If messages remain in the queue for more than a certain period of time, they will be cleared by RabbitMQ and the data will be lost. Then this is the second pitfall. This does not mean that a large amount of data will be accumulated in mq, but that a large amount of data will be lost directly. One solution we can adopt is batch redirection, which we have done in similar scenarios online before. When there is a large backlog, we directly discard the data, and then wait until after the peak period, for example, everyone drinks coffee and stays up until after 12 o'clock in the evening, and the users all go to bed. At this time, we started to write a program, write a temporary program to find out the lost batch of data bit by bit, and then refill it into mq to make up for the data lost during the day. It can only be like this. Assume that 10,000 orders are backlogged in mq and have not been processed, and 1,000 of them are lost. You can only manually write a program to check out those 1,000 orders, and manually send them to mq to make up for them again. + + \ No newline at end of file diff --git a/docs_en/high-performance/message-queue/rocketmq-questions.en.md b/docs_en/high-performance/message-queue/rocketmq-questions.en.md new file mode 100644 index 00000000000..38b4c33d717 --- /dev/null +++ b/docs_en/high-performance/message-queue/rocketmq-questions.en.md @@ -0,0 +1,932 @@ +--- +title: RocketMQ常见问题总结 +category: 高性能 +tag: + - RocketMQ + - 消息队列 +--- + +> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) 相比原文主要进行了下面这些完善: +> +> - [分析了 RocketMQ 高性能读写的原因和顺序消费的具体实现](https://github.com/Snailclimb/JavaGuide/pull/2133) +> - [增加了消息类型、消费者类型、消费者组和生产者组的介绍](https://github.com/Snailclimb/JavaGuide/pull/2134) + +## 消息队列扫盲 + +消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧? + +所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?** + +### 消息队列为什么会出现? + +消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。 + +### 消息队列能用来干什么? + +#### 异步 + +你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗? + +很好 👍,你又提出了一个概念,**同步通信**。就比如现在业界使用比较多的 `Dubbo` 就是一个适用于各个系统之间同步通信的 `RPC` 框架。 + +我来举个 🌰 吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef37fee7e09230.jpg) + +我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。 + +当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef380429cf373e.jpg) + +这样整个系统的调用链又变长了,整个时间就变成了 550ms。 + +当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。 + +我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦 😋😋😋” 咦~~~ 为了多吃点,真恶心。 + +然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。 + +最终我们从大妈手中接过饭菜然后去寻找座位了... + +回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。 + +那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。 + +所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。 + +![](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 系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。 + +那么,如何去解决这个问题呢? + +Today's more common distributed transaction implementations include 2PC, TCC and transaction messages (half message mechanism). Each implementation has its own specific usage scenarios, but also has its own problems, and neither is a perfect solution. + +In `RocketMQ`, **transaction messages plus transaction reverse check mechanism** are used to solve distributed transaction problems. I drew a picture, you can refer to the picture to understand. + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38798d7a987f.png) + +The half message sent in the first step means that this message is invisible to the consumer before the transaction is committed. + +> So, how to write a message but not visible to the user? RocketMQ transaction message method is: if the message is a half message, the topic of the original message and the message consumption queue will be backed up, and then **change the topic** to RMQ_SYS_TRANS_HALF_TOPIC. Since the consumer group has not subscribed to the topic, the consumer cannot consume half type messages. ** Then RocketMQ will start a scheduled task to pull messages from the Topic RMQ_SYS_TRANS_HALF_TOPIC for consumption. ** According to the producer group, a service provider is obtained to send a review transaction status request, and based on the transaction status, it is decided whether to submit or rollback the message. + +You can imagine that if there is no **transaction anti-checking mechanism** starting from step 5, if there is network fluctuation and step 4 is not sent successfully, this will cause the problem of MQ not knowing whether it needs to be consumed by the consumer. It will be like a headless fly. In `RocketMQ`, the above-mentioned transaction reverse check is used to solve the problem, while in `Kafka`, an exception is usually thrown directly and the user can solve it by himself. + +You also need to note that the operation of `MQ Server` pointing to system B is no longer related to system A. That is to say, the distributed transaction in the message queue is - **local transaction and storing messages to the message queue are the same transaction**. This also produces the **eventual consistency** of the transaction, because the entire process is asynchronous, and **each system only needs to guarantee its own part of the transaction**. + +Problems encountered in practice: Transaction messages require a transaction listener to monitor whether the local transaction is successful, and the transaction listener interface is only allowed to be implemented once. That means that the local transactions of various transaction messages need to be written in an interface method, which will inevitably produce a lot of coupling and type judgment. Use the Function interface to wrap the entire business process and pass it as a parameter to the interface method of the listener. Then call the apply() method of Function to execute the business, and the transaction will also be executed in the apply() method. Decoupling the listener from the business makes it feasible in a real production environment. + +1. Simulate a need to add user browsing records + +```java +@PostMapping("/add") +@ApiOperation("Add user browsing history") +public Result add(Long userId, Long forecastLogId) { + + // Functional programming: Browse records and store them in the database + 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); + + //Send transaction messages; use the Function interface to receive local transaction operations and pass them into the method as a parameter + TransactionSendResult transactionSendResult = mqProducerService.sendTransactionMessage(jsonString, MQDestination.TAG_ADD_VIEW_HISTORY, function); + return Result.success(transactionSendResult); +} +``` + +2. Method of sending transaction messages + +```java +/** + * Send transaction message + * + * @param msgBody + * @param tag + * @param function + * @return + */ +public TransactionSendResult sendTransactionMessage(String msgBody, String tag, Function function) { + //Construct the message body + Message message = buildMessage(msgBody); + + //Construct message delivery information + String destination = buildDestination(tag); + + TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(destination, message, function); + return result; +} +``` + +3. Producer message listener, only one class is allowed to implement the listener + +```java +@Slf4j +@RocketMQTransactionListener +public class TransactionMsgListener implements RocketMQLocalTransactionListener { + + @Autowired + private RedisService redisService; + + /** + * Execute local transaction (executed when sending message successfully) + * + * @param message + * @param o + * @return commit or rollback or unknown + */ + @Override + public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { + + // 1. Get transaction ID + String transactionId = null; + try { + transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); + // 2. Determine whether the incoming function object is empty. If it is empty, it means there is no business to be executed and the message will be discarded directly. + if (o == null) { + //Messages returning ROLLBACK status will be discarded + log.info("Transaction message rolled back, no business needs to be processed transactionId={}", transactionId); + return RocketMQLocalTransactionState.ROLLBACK; + } + // Convert Object o into Function object + Function function = (Function) o; + //Execute business transactions will also be executed in function.apply + Boolean apply = function.apply(transactionId); + if (apply) { + log.info("Transaction submitted, message processed normally transactionId={}", transactionId); + //Messages that return COMMIT status will be consumed by consumers immediately + return RocketMQLocalTransactionState.COMMIT; + } + } catch (Exception e) { + log.info("Exception occurred. Return ROLLBACK transactionId={}", transactionId); + return RocketMQLocalTransactionState.ROLLBACK; + } + return RocketMQLocalTransactionState.ROLLBACK; + } + + /** + * Transaction review mechanism to check the status of local transactions + * + * @param message + * @return + */ + @Override + public RocketMQLocalTransactionState checkLocalTransaction(Message message) { + + String transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); + + // Check redis + MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); + if (Objects.isNull(mqTransaction)) { + return RocketMQLocalTransactionState.ROLLBACK; + } + return RocketMQLocalTransactionState.COMMIT; + } +}``` + +4. In the simulated business scenario, the methods here must be extracted and placed in other classes. If the caller and the callee are in the same class, transaction failure will occur. + +```java +@Component +public class ViewHistoryHandler { + + @Autowired + private IViewHistoryService viewHistoryService; + + @Autowired + private IMqTransactionService mqTransactionService; + + @Autowired + private RedisService redisService; + + /** + * Browsing records stored in database + * + * @param transactionId + * @param userId + * @param forecastLogId + * @return + */ + @Transactional + public Boolean addViewHistory(String transactionId, Long userId, Long forecastLogId) { + // Build browsing history + ViewHistory viewHistory = new ViewHistory(); + viewHistory.setUserId(userId); + viewHistory.setForecastLogId(forecastLogId); + viewHistory.setCreateTime(LocalDateTime.now()); + boolean save = viewHistoryService.save(viewHistory); + + //Local transaction information + MqTransaction mqTransaction = new MqTransaction(); + mqTransaction.setTransactionId(transactionId); + mqTransaction.setCreateTime(new Date()); + mqTransaction.setStatus(MqTransaction.StatusEnum.VALID.getStatus()); + + // 1. You can store transaction information in the database + mqTransactionService.save(mqTransaction); + + // 2. You can also choose to save redis, which has a validity period of 4 hours. '4 hours' is RocketMQ's built-in maximum review timeout. If it expires and is not confirmed, it will be forced to roll back. + redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); + + // Release comments, simulate exceptions, and rollback transactions + // int i = 10 / 0; + + return save; + } +} +``` + +5. Consume messages and idempotent processing + +```java +@Service +@RocketMQMessageListener(topic = MQDestination.TOPIC, selectorExpression = MQDestination.TAG_ADD_VIEW_HISTORY, consumerGroup = MQDestination.TAG_ADD_VIEW_HISTORY) +public class ConsumerAddViewHistory implements RocketMQListener { + // This method will be executed when the message is heard + @Override + public void onMessage(Message message) { + // Idempotent check + String transactionId = message.getTransactionId(); + + // Check redis + MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); + + // No transaction record exists + if (Objects.isNull(mqTransaction)) { + return; + } + + // Consumed + 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"); + + //Downstream business processing + // TODO record user preferences and update user portraits + + // TODO update the number of views of 'Securities Forecast Article' and recalculate the exposure ranking of the article + + //Update status to consumed + mqTransaction.setUpdateTime(new Date()); + mqTransaction.setStatus(MqTransaction.StatusEnum.CONSUMED.getStatus()); + redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); + log.info("Message heard: msg={}", JSON.toJSONString(map)); + } +} +``` + +## How to solve the message accumulation problem? + +Above we mentioned a very important function of the message queue - **peak clipping**. So what if this peak is too large and messages accumulate in the queue? + +In fact, this problem can be generalized, because there are actually only two causes of message accumulation - producers produce too fast or consumers consume too slowly. + +We can think about solving this problem from multiple angles. When the traffic reaches the peak, it is because the producer is producing too fast. We can use some **current limiting and downgrading** methods. Of course, you can also add multiple consumer instances to horizontally expand and increase consumption capacity to match the surge in production. If the consumer's consumption is too slow, we can first check whether the consumer has a large number of consumption errors, or print the log to see if any thread is stuck, lock resources are not released, etc. + +> Of course, the fastest way to solve the problem of message accumulation is to add consumer instances, but **at the same time you also need to increase the number of queues per topic**. +> +> Don’t forget that in `RocketMQ`, **a queue will only be consumed by one consumer**. If you just add consumer instances, you will be in the situation where I drew the architecture diagram for you at the beginning. + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef387d939ab66d.jpg) + +## What is retroactive consumption? + +Retroactive consumption refers to messages that `Consumer` has successfully consumed and needs to be consumed again due to business needs. In `RocketMQ`, after `Broker` delivers a successful message to `Consumer`, the **message still needs to be retained**. And re-consumption is generally based on the time dimension. For example, due to a `Consumer` system failure, the data from 1 hour ago needs to be re-consumed after recovery. Then `Broker` needs to provide a mechanism to roll back the consumption progress according to the time dimension. `RocketMQ` supports retroactive consumption according to time, and the time dimension is accurate to milliseconds. + +This is the explanation from the official document. I just copied it and used it as a popular science 😁😁😁. + +## How RocketMQ ensures high-performance reading and writing + +### Traditional IO method + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/31699457085_.pic.jpg) + +Traditional IO reading and writing is actually a read + write operation. The whole process will be divided into the following steps: + +- The user calls the read() method and starts reading data. At this time, a context switch occurs from user mode to kernel mode, which is the switch shown in the diagram 1 +- Copy disk data to kernel cache through 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`: **The storage body of the message body and metadata**, which stores the message body content written by the `Producer` side. The message content is not fixed-length. The default size of a single file is 1G, the length of the file name is 20 digits, zeros are padded on the left, and the remainder is the starting offset. For example, 00000000000000000000 represents the first file, the starting offset is 0, and the file size is 1G=1073741824; when the first file is full, the second file is 00000000001073741824, the starting offset is 1073741824, and so on. Messages are mainly written to the log file sequentially. When the file is full, the next file is written. +- `ConsumeQueue`: Message consumption queue, **The purpose of introduction is mainly to improve the performance of message consumption** (we have also talked about it before). Since `RocketMQ` is a subscription model based on the topic `Topic`, message consumption is for the topic. If you want to traverse the `commitlog` file to retrieve messages based on `Topic`, it is very inefficient. `Consumer` can find messages to be consumed based on `ConsumeQueue`. Among them, `ConsumeQueue` (logical consumption queue) ** serves as the index of the consumer message** and saves the ** starting physical offset `offset` ** of the queue message under the specified `Topic` in `CommitLog`, the message size `size` and the `HashCode` value of the message `Tag`. **The `consumequeue` file can be regarded as a `commitlog` index file based on `topic`, so the `consumequeue` folder is organized as follows: topic/queue/file three-layer organizational structure, the specific storage path is: $HOME/store/consumequeue/{topic}/{queueId}/{fileName}. Similarly, the `consumequeue` file adopts a fixed-length design. Each entry has a total of 20 bytes, which are 8 bytes of the `commitlog` physical offset, 4 bytes of message length, and 8 bytes of tag `hashcode`. A single file consists of 30W entries, and each entry can be accessed randomly like an array. The size of each `ConsumeQueue` file is about 5.72M; +- `IndexFile`: `IndexFile` (index file) provides a method to query messages by key or time interval. This is just a general introduction without any detailed introduction. + +In summary, the most important structures of the entire message storage are `CommitLoq` and `ConsumeQueue`. And `ConsumeQueue` can be roughly understood as the queue in `Topic`. + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3884c02acc72.png) + +`RocketMQ` uses a **hybrid storage structure**, that is, all queues under a single instance of `Broker` share a log data file to store messages. What is interesting is that in `Kafka` which is also highly concurrent, a storage file is allocated for each `Topic`. This is a bit like we have a lot of books that need to be put on the bookshelf. `RocketMQ` directly puts them in batches regardless of the type of books, while `Kafka` puts the books into designated classification areas. + +And why does `RocketMQ` do this? The reason is to **improve data writing efficiency**, regardless of `Topic`, which means that we have a greater chance of obtaining **batches** of messages for data writing, but it also brings a trouble that when reading messages, we need to traverse the entire large file, which is very time-consuming. + +Therefore, `ConsumeQueue` is used in `RocketMQ` as the index file of each queue to **improve the efficiency of reading messages**. We can directly calculate the global position of the index based on the message sequence number of the queue (index sequence number * index fixed length 20), then directly read this index, and then find the message based on the global position of the message recorded in the index. + +At this point, you may still be a little vague about the storage architecture of `RocketMQ`. It’s okay. Let’s understand it with the help of the diagram. + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef388763c25c62.jpg) + +emmm, isn’t it a bit complicated 🤣, don’t be timid when looking at English pictures and documents, just bite the bullet and read down. + +> If you don’t understand the above, you must read the process analysis below carefully! + +First of all, the top piece is what I just said. You can now directly understand `ConsumerQueue` as `Queue`**. + +On the far left side of the figure, the red square represents the message being written, and the dotted square represents the message waiting to be written. When the producer on the left sends a message, he will specify `Topic`, `QueueId` and the specific message content. In `Broker`, it doesn't matter which message you are, he directly stores the entire order in the CommitLog**. According to the `Topic` and `QueueId` specified by the producer, the offset of the message itself in the `CommitLog`, the size of the message itself, and the hash value of the tag are stored in the corresponding `ConsumeQueue` index file. In each queue, `ConsumeOffset` is saved, which is the consumption position of each consumer group (I mentioned it in the architecture, students who forgot can go back and take a look), and when consumers pull messages for consumption, they only need to obtain the next unconsumed message based on `ConsumeOffset`. + +The above is my general understanding of the entire message storage architecture (I will not discuss some details here, such as sparse indexes, etc.). I hope it will be helpful to you. + +Because there is a knowledge point that I forgot to mention because I was too busy writing it. It’s hard to think about where to add it, so I’ll leave it to everyone to think about 🤔🤔. + +Why is the `CommitLog` file designed to have a fixed length? Reminder: **Memory mapping mechanism**. + +## Summary + +Finally finished writing this blog. Do you remember what I said 😅? + +In this article, I mainly want to introduce to you + +1. Reasons for the emergence of message queues +2. The role of message queue (asynchronous, decoupling, peak clipping) +3. A series of problems caused by message queues (message accumulation, repeated consumption, sequential consumption, distributed transactions, etc.) +4. Two message models of message queue-queue and topic mode +5. Analyzed the technical architecture of `RocketMQ` (`NameServer`, `Broker`, `Producer`, `Consumer`) +6. Combined with `RocketMQ` to answer the solution to the side effects of message queue +7. Introduced the storage mechanism and disk flushing strategy of `RocketMQ`. + +etc. . . + + \ No newline at end of file diff --git a/docs_en/high-performance/read-and-write-separation-and-library-subtable.en.md b/docs_en/high-performance/read-and-write-separation-and-library-subtable.en.md new file mode 100644 index 00000000000..633310cd23f --- /dev/null +++ b/docs_en/high-performance/read-and-write-separation-and-library-subtable.en.md @@ -0,0 +1,292 @@ +--- +title: 读写分离和分库分表详解 +category: 高性能 +head: + - - meta + - name: keywords + content: 读写分离,分库分表,主从复制 + - - meta + - name: description + content: 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。分库就是将数据库中的数据分散到不同的数据库上。分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。 +--- + +## 读写分离 + +### 什么是读写分离? + +见名思意,根据读写分离的名字,我们就可以知道:**读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。** 这样的话,就能够小幅提升写性能,大幅提升读性能。 + +我简单画了一张图来帮助不太清楚读写分离的小伙伴理解。 + +![读写分离示意图](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation.png) + +一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。 + +### 如何实现读写分离? + +不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步: + +1. 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。 +2. 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的**主从复制**。 +3. 系统将写请求交给主数据库处理,读请求交给从数据库处理。 + +落实到项目本身的话,常用的方式有两种: + +**1. 代理方式** + +![代理方式实现读写分离](https://oss.javaguide.cn/github/javaguide/high-performance/read-and-write-separation-and-library-subtable/read-and-write-separation-proxy.png) + +我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。 + +提供类似功能的中间件有 **MySQL Router**(官方, MySQL Proxy 的替代方案)、**Atlas**(基于 MySQL Proxy)、**MaxScale**、**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)。 + +**2. 组件方式** + +在这种方式中,我们可以通过引入第三方组件来帮助我们读写请求。 + +这也是我比较推荐的一种方式。这种方式目前在各种互联网公司中用的最多的,相关的实际的案例也非常多。如果你要采用这种方式的话,推荐使用 `sharding-jdbc` ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。 + +你可以在 shardingsphere 官方找到 [sharding-jdbc 关于读写分离的操作](https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/read-write-splitting/)。 + +### 主从复制原理是什么? + +MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。 + +更具体和详细的过程是这个样子的(图片来自于:[《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) + +1. 主库将数据库中数据的变化写入到 binlog +2. 从库连接主库 +3. 从库会创建一个 I/O 线程向主库请求更新的 binlog +4. 主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收 +5. 从库的 I/O 线程将接收的 binlog 写入到 relay log 中。 +6. 从库的 SQL 线程读取 relay log 同步数据到本地(也就是再执行一遍 SQL )。 + +怎么样?看了我对主从复制这个过程的讲解,你应该搞明白了吧! + +你一般看到 binlog 就要想到主从复制。当然,除了主从复制之外,binlog 还能帮助我们实现数据恢复。 + +🌈 拓展一下: + +不知道大家有没有使用过阿里开源的一个叫做 canal 的工具。这个工具可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。很显然,这个工具的底层原理肯定也是依赖 binlog。canal 的原理就是模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。 + +另外,像咱们常用的分布式缓存组件 Redis 也是通过主从复制实现的读写分离。 + +🌕 简单总结一下: + +**MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。** + +### 如何避免主从延迟? + +读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 **主从同步延迟** 。 + +如果我们的业务场景无法容忍主从同步延迟的话,应该如何避免呢(注意:我这里说的是避免而不是减少延迟)? + +这里提供两种我知道的方案(能力有限,欢迎补充),你可以根据自己的业务场景参考一下。 + +#### 强制将读请求路由到主库处理 + +既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。 + +比如 `Sharding-JDBC` 就是采用的这种方案。通过使用 Sharding-JDBC 的 `HintManager` 分片键值管理器,我们可以强制使用主库。 + +```java +HintManager hintManager = HintManager.getInstance(); +hintManager.setMasterRouteOnly(); +// 继续JDBC操作 +``` + +对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。 + +#### 延迟读取 + +还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。 + +不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。 + +#### 总结 + +关于如何避免主从延迟,我们这里介绍了两种方案。实际上,延迟读取这种方案没办法完全避免主从延迟,只能说可以减少出现延迟的概率而已,实际项目中一般不会使用。 + +总的来说,要想不出现延迟问题,一般还是要强制将那些必须获取最新数据的读请求都交给主库处理。如果你的项目的大部分业务场景对数据准确性要求不是那么高的话,这种方案还是可以选择的。 + +### 什么情况下会出现主从延迟?如何尽量减少延迟? + +我们在上面的内容中也提到了主从延迟以及避免主从延迟的方法,这里我们再来详细分析一下主从延迟出现的原因以及应该如何尽量减少主从延迟。 + +要搞懂什么情况下会出现主从延迟,我们需要先搞懂什么是主从延迟。 + +MySQL 主从同步延时是指从库的数据落后于主库的数据,这种情况可能由以下两个原因造成: + +1. 从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据; +2. 从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。 + +与主从同步有关的时间点主要有 3 个: + +1. 主库执行完一个事务,写入 binlog,将这个时刻记为 T1; +2. 从库 I/O 线程接收到 binlog 并写入 relay log 的时刻记为 T2; +3. 从库 SQL 线程读取 relay log 同步数据本地的时刻记为 T3。 + +结合我们上面讲到的主从复制原理,可以得出: + +- T2 和 T1 的差值反映了从库 I/O 线程的性能和网络传输的效率,这个差值越小说明从库 I/O 线程的性能和网络传输效率越高。 +- T3 和 T2 的差值反映了从库 SQL 线程执行的速度,这个差值越小,说明从库 SQL 线程执行速度越快。 + +那什么情况下会出现出从延迟呢?这里列举几种常见的情况: + +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. …… + +[《MySQL 实战 45 讲》](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D)这个专栏中的[读写分离有哪些坑?](https://time.geekbang.org/column/article/77636)这篇文章也有对主从延迟解决方案这一话题进行探讨,感兴趣的可以阅读学习一下。 + +## 分库分表 + +读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:**如果 MySQL 一张表的数据量过大怎么办?** + +换言之,**我们该如何解决 MySQL 的存储压力呢?** + +答案之一就是 **分库分表**。 + +### 什么是分库? + +**分库** 就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。 + +**垂直分库** 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。 + +举个例子:说你将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。 + +![垂直分库](./images/read-and-write-separation-and-library-subtable/vertical-slicing-database.png) + +**水平分库** 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。 + +举个例子:订单表数据量太大,你对订单表进行了水平切分(水平分表),然后将切分后的 2 张订单表分别放在两个不同的数据库。 + +![水平分库](./images/read-and-write-separation-and-library-subtable/horizontal-slicing-database.png) + +### 什么是分表? + +**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 + +**垂直分表** 是对数据表列的拆分,把一张列比较多的表拆分为多张表。 + +举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。 + +**水平分表** 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。 + +举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 + +水平拆分只能解决单表数据量大的问题,为了提升性能,我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。 + +![分表](./images/read-and-write-separation-and-library-subtable/two-forms-of-sub-table.png) + +### 什么情况下需要分库分表? + +遇到下面几种场景可以考虑分库分表: + +- 单表的数据达到千万级别以上,数据库读写速度比较缓慢。 +- 数据库中的数据占用的空间越来越大,备份时间越来越长。 +- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)。 + +不过,分库分表的成本太高,如非必要尽量不要采用。而且,并不一定是单表千万级数据量就要分表,毕竟每张表包含的字段不同,它们在不错的性能下能够存放的数据量也不同,还是要具体情况具体分析。 + +之前看过一篇文章分析 “[InnoDB 中高度为 3 的 B+ 树最多可以存多少数据](https://juejin.cn/post/7165689453124517896)”,写的挺不错,感兴趣的可以看看。 + +### 常见的分片算法有哪些? + +分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。 + +常见的分片算法有: + +- **哈希分片**:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。 +- **范围分片**:按照特定的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 `id` 为 `1~299999` 的记录分到第一个表, `300000~599999` 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 +- **映射表分片**:使用一个单独的表(称为映射表)来存储分片键和分片位置的对应关系。映射表分片策略可以支持任何类型的分片算法,如哈希分片、范围分片等。映射表分片策略是可以灵活地调整分片规则,不需要修改应用程序代码或重新分布数据。不过,这种方式需要维护额外的表,还增加了查询的开销和复杂度。 +- **一致性哈希分片**:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。 +- **地理位置分片**:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。 +- **融合算法分片**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。 +- …… + +### 分片键如何选择? + +分片键(Sharding Key)是数据分片的关键字段。分片键的选择非常重要,它关系着数据的分布和查询效率。一般来说,分片键应该具备以下特点: + +- 具有共性,即能够覆盖绝大多数的查询场景,尽量减少单次查询所涉及的分片数量,降低数据库压力; +- 具有离散性,即能够将数据均匀地分散到各个分片上,避免数据倾斜和热点问题; +- 具有稳定性,即分片键的值不会发生变化,避免数据迁移和一致性问题; +- 具有扩展性,即能够支持分片的动态增加和减少,避免数据重新分片的开销。 + +实际项目中,分片键很难满足上面提到的所有特点,需要权衡一下。并且,分片键可以是表中多个字段的组合,例如取用户 ID 后四位作为订单 ID 后缀。 + +### 分库分表会带来什么问题呢? + +记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。 + +引入分库分表之后,会给系统带来什么挑战呢? + +- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。 +- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结: 。 +- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:[分布式 ID 介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html)。 +- **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。 +- …… + +另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。 + +### 分库分表有没有什么比较推荐的方案? + +Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。 + +ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。 + +ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。 + +ShardingSphere 提供的功能如下: + +![ShardingSphere 提供的功能](https://oss.javaguide.cn/github/javaguide/high-performance/shardingsphere-features.png) + +ShardingSphere 的优势如下(摘自 ShardingSphere 官方文档:): + +- 极致性能:驱动程序端历经长年打磨,效率接近原生 JDBC,性能极致。 +- 生态兼容:代理端支持任何通过 MySQL/PostgreSQL 协议的应用访问,驱动程序端可对接任意实现 JDBC 规范的数据库。 +- 业务零侵入:面对数据库替换场景,ShardingSphere 可满足业务无需改造,实现平滑业务迁移。 +- 运维低成本:在保留原技术栈不变前提下,对 DBA 学习、管理成本低,交互友好。 +- 安全稳定:基于成熟数据库底座之上提供增量能力,兼顾安全性及稳定性。 +- 弹性扩展:具备计算、存储平滑在线扩展能力,可满足业务多变的需求。 +- 开放生态:通过多层次(内核、功能、生态)插件化能力,为用户提供可定制满足自身特殊需求的独有系统。 + +另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 + +不过,还是要多提一句:**现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式!** + +### 分库分表后,数据怎么迁移呢? + +分库分表之后,我们如何将老库(单库单表)的数据迁移到新库(分库分表后的数据库系统)呢? + +比较简单同时也是非常常用的方案就是**停机迁移**,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。 + +如果你不想停机迁移数据的话,也可以考虑**双写方案**。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的: + +- 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。 +- 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。 +- 重复上一步的操作,直到老库和新库的数据一致为止。 + +想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。 + +## 总结 + +- 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 +- 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。 +- **分库** 就是将数据库中的数据分散到不同的数据库上。**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 +- 引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。 +- 现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式! +- 如果必须要手动分库分表的话,ShardingSphere 是首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 + + + diff --git a/docs_en/high-performance/sql-optimization.en.md b/docs_en/high-performance/sql-optimization.en.md new file mode 100644 index 00000000000..49074c5abc9 --- /dev/null +++ b/docs_en/high-performance/sql-optimization.en.md @@ -0,0 +1,19 @@ +--- +title: Summary of common SQL optimization methods (paid) +category: high performance +head: + - - meta + - name: keywords + content: Paging optimization, index, Show Profile, slow SQL + - - meta + - name: description + content: SQL optimization is a hot topic that everyone pays close attention to. Whether you are in an interview or at work, you are likely to encounter it. If one day there is a performance problem with an online interface you are responsible for, it needs to be optimized. Then your first thought is probably to optimize SQL optimization, because its transformation cost is much smaller compared to the code. +--- + +**Summary of common SQL optimization methods** The relevant content is my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and joining method) exclusive content, which has been compiled into ["Java Interview Guide North"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). + +![](https://oss.javaguide.cn/javamianshizhibei/sql-optimization.png) + + + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.en.md new file mode 100644 index 00000000000..38ecf94eebc --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.en.md @@ -0,0 +1,147 @@ +--- +title: 糟糕程序员的 20 个坏习惯 +category: 技术文章精选集 +author: Kaito +tag: + - 练级攻略 +--- + +> **推荐语**:Kaito 大佬的一篇文章,很实用的建议! +> +> **原文地址:** + +我想你肯定遇到过这样一类程序员:**他们无论是写代码,还是写文档,又或是和别人沟通,都显得特别专业**。每次遇到这类人,我都在想,他们到底是怎么做到的? + +随着工作时间的增长,渐渐地我也总结出一些经验,他们身上都保持着一些看似很微小的优秀习惯,但正是因为这些习惯,体现出了一个优秀程序员的基本素养。 + +但今天我们来换个角度,来看看一个糟糕程序员有哪些坏习惯?只要我们都能避开这些问题,就可以逐渐向一个优秀程序员靠近。 + +## 1、技术名词拼写不规范 + +无论是个人简历,还是技术文档,我经常看到拼写不规范的技术名词,例如 JAVA、javascript、python、MySql、Hbase、restful。 + +正确的拼写应该是 Java、JavaScript、Python、MySQL、HBase、RESTful,不要小看这个问题,很多面试官很有可能因为这一点刷掉你的简历。 + +## 2、写文档,中英文混排不规范 + +中文描述使用英文标点符号,英文和数字使用了全角字符,中文与英文、数字之间没有空格等等。 + +其中很多人会忽视中文和英文、数字之间加一个「空格」,这样排版阅读起来会更舒服。之前我的文章排版,都是遵循了这些细节。 + +## 3、重要逻辑不写注释,或写得很拖沓 + +复杂且重要的逻辑代码,很多程序员不写注释,除了自己能看懂代码逻辑,其他人根本看不懂。或者是注释虽然写了,但写得很拖沓,没有逻辑可言。 + +重要的逻辑不止要写注释,还要写得简洁、清晰。如果是一眼就能读懂的简单代码,可以不加注释。 + +## 4、写复杂冗长的函数 + +一个函数几百行,一个文件上千行代码,复杂函数不做拆分,导致代码变得越来越难维护,最后谁也不敢动。 + +基本的设计模式还是要遵守的,例如单一职责,一个函数只做一件事,开闭原则,对扩展开放,对修改关闭。 + +如果函数逻辑确实复杂,也至少要保证主干逻辑足够清晰。 + +## 5、不看官方文档,只看垃圾博客 + +很多人遇到问题不先去看官方文档,而是热衷于去看垃圾博客,这些博客的内容都是互相抄袭,错误百出。 + +其实很多软件官方文档写得已经非常好了,常见问题都能找到答案,认真读一读官方文档,比看垃圾博客强一百倍,要养成看官方文档的好习惯。 + +## 6、宣扬内功无用论 + +有些人天天追求日新月异的开源项目和框架,却不肯花时间去啃一啃底层原理,常见问题虽然可以解决,但遇到稍微深一点的问题就束手无策。 + +很多高大上的架构设计,思路其实都源于底层。想一想,像计算机体系结构、操作系统、网络协议这些东西,经过多少年演进才变为现在的样子,演进过程中遇到的复杂问题比比皆是,理解了解决这些问题的思路,再看上层技术会变得很简单。 + +## 7、乐于炫技 + +有些人天天把「高大上」的技术名词挂在嘴边,生怕别人不知道自己学了什么高深技术,嘴上乐于炫技,但别人一问他细节就会哑口无言。 + +## 8、不接受质疑 + +自己设计的方案,别人提出疑问时只会回怼,而不是理性分析利弊,抱着学习的心态交流。 + +这些人学了点东西就觉得自己很有本事,殊不知只是自己见识太少。 + +## 9、接口协议不规范 + +和别人定 API 协议全靠口头沟通,不给规范的文档说明,甚至到了测试联调时会发现,竟然和协商的还不一样,或者改协议了却不通知对接方,合作体验极差。 + +## 10、遇到问题自己死磕 + +很初级程序员容易犯的问题,遇到问题只会自己死磕,拖到 deadline 也没有产出,领导来问才知道有问题解决不了。 + +有问题及时反馈才是对自己负责,对团队负责。 + +## 11、一说就会,一写就废 + +平时技术方案吹得天花乱坠,一让他写代码就废,典型的眼高手低选手。 + +## 12、表达没有逻辑,不站在对方角度看问题 + +讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明白。 + +学会沟通和表达,是合作的基础。 + +## 13、不主动思考,伸手党 + +遇到问题不去 google,不做思考就向别人提问,喜欢做伸手党。 + +每个人的时间都很宝贵,大家都更喜欢你带着自己的思考来提问,一来可以规避很多低级问题,二来可以提高交流质量。 + +## 14、经常犯重复的错误 + +出问题后说下次会注意,但下次问题依旧,对自己不负责任,说到底是态度问题。 + +## 15、加功能不考虑扩展性 + +加新功能只关注某一小块业务,不考虑系统整体的扩展性,堆代码行为严重。 + +要学会分析需求和未来可能发生的变化,设计更通用的解决方案,降低后期开发成本。 + +## 16、接口不自测,出问题不打日志 + +自己开发的接口不自测就和别人联调,出了问题又说没打日志,协作效率极低。 + +## 17、提交代码不规范 + +很多人提交代码不写描述,或者写的是无意义的描述,尤其是修改很少代码时,这种情况会导致回溯问题成本变高。 + +制定代码提交规范,能让你在每一次提交代码时,不会做太随意的代码修改。 + +## 18、手动修改生产环境数据库 + +直连生产环境数据库修改数据,更有 UPDATE / DELETE SQL 忘写 WHERE 条件的情况,产生数据事故。 + +修改生产环境数据库一定要谨慎再谨慎,建议操作前先找同事 review 代码再操作。 + +## 19、没理清需求就直接写代码 + +很多程序员接到需求后,不怎么思考就开始写代码,需求和自己理解的有偏差,造成无意义返工。 + +多花些时间梳理需求,能规避很多不合理的问题。 + +## 20、重要设计不写文档 + +重要的设计没有文档输出,和别人交接系统时只做口头描述,丢失关键信息。 + +有时候理解一个设计方案,一个好的文档要比看几百行代码更高效。 + +## 总结 + +以上这些不良习惯,你命中几个呢?或者你身边有没有碰到这样的人? + +我认为提早规避这些问题,是成为一个优秀程序员必须要做的。这些习惯总结起来大致分为这 4 个方面: + +- 良好的编程修养 +- 谦虚的学习心态 +- 良好的沟通和表达 +- 注重团队协作 + +优秀程序员的专业技能,我们可能很难在短时间内学会,但这些基本的职业素养,是可以在短期内做到的。 + +希望你我可以有则改之,无则加勉。 + + + diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.en.md new file mode 100644 index 00000000000..5ed5311393b --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.en.md @@ -0,0 +1,172 @@ +--- +title: 美团三年,总结的10条血泪教训 +category: 技术文章精选集 +author: CityDreamer部落 +tag: + - 练级攻略 +--- + +> **推荐语**:作者用了很多生动的例子和故事展示了自己在美团的成长和感悟,看了之后受益颇多! +> +> **内容概览**: +> +> 本文的作者提出了以下十条建议,希望能对其他职场人有所启发和帮助: +> +> 1. 结构化思考与表达,提高个人影响力 +> 2. 忘掉职级,该怼就怼,推动事情往前走 +> 3. 用好平台资源,结识优秀的人,学习通识课 +> 4. 一切都是争取来的,不要等待机会,要主动寻求 +> 5. 关注商业,升维到老板思维,看清趋势,及时止损 +> 6. 培养数据思维,利用数据了解世界,指导决策 +> 7. 做一个好"销售",无论是自己还是产品,都要学会展示和说服 +> 8. 少加班多运动,保持身心健康,提高工作效率 +> 9. 有随时可以离开的底气,不要被职场所困,借假修真,提升自己 +> 10. 只是一份工作,不要过分纠结,相信自己,走出去看看 +> +> **原文地址**: + +在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成 10 条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。 + +倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。 + +## 01 结构化思考与表达 + +美团是一家特别讲究方法论的公司,人人都要熟读四大名著《高效能人士的七个习惯》、《金字塔原理》、《用图表说话》和《学会提问》。 + +与结构化思考和表达相关的,是《金字塔原理》,作者是麦肯锡公司第一位女性咨询顾问。这本书告诉我们,思考和表达的过程,就像构建金字塔(或者构建一棵树),先有整体结论,再寻找证据,证据之间要讲究相互独立、而且能穷尽(MECE 原则),论证的过程也要按特定的顺序进行,比如时间顺序、空间顺序、重要性顺序…… + +作为大厂社畜,日常很大一部分工作就是写文档、看别人文档。大家做的事,但最后呈现的结果却有很大差异。一篇逻辑清晰、详略得当的文档,给人一种如沐春风的感受,能提炼出重要信息,是好的参考指南。 + +结构化思考与表达算是职场最通用的能力,也是打造个人影响力最重要的途径之一。 + +## 02 忘掉职级,该怼就怼 + +在阿里工作时,能看到每个人的 Title,看到江湖地位高(职级高+入职时间早)的同学,即便跟自己没有汇报关系,不自然的会多一层敬畏。推进工作时,会多一层压力,对方未读或已读未回时,不知如何应对。 + +美团只能看到每个人的坑位信息,还有 Ta 的上级。工作相关的问题,可以向任何人提问,如果协同方没有及时响应,隔段时间@一次,甚至"怼一怼",都没啥问题,事情一直往前推进才最重要。除了大象消息直接提问外,还有个大杀器--TT(公司级问题流转系统),在上面提问时,加上对方主管,如果对方未及时回应,问题会自动升级,每天定时 Push,直到解决为止。 + +我见到一些很年轻的同事,他们在推动 OKR、要资源的事上,很有一套,只要能达到自己的目标,不会考虑别人的感受,最终,他们还真能把事办成。 + +当然了,段位越高的人,越能用自己的人格魅力、影响力、资源等,去影响和推动事情的进程,而不是靠对他人的 Push。只是在拿结果的事上,不要把自己太当回事,把别人太当回事,大家在一起,也只是为了完成各自的任务,忘掉职级,该怼时还得怼。 + +## 03 用好平台资源 + +没有人能在一家公司待一辈子,公司再牛,跟自己关系不大,重要的是,在有限的时间内,最大化用好平台资源。 + +在美团除了认识自己节点的同事外,有幸认识一群特别棒的协作方,还有其他 BU 的同学。 + +这些优秀的人身上,有很多共同的特质:谦虚、利他、乐于分享、双赢思维。 + +有两位做运营的同学。 + +一位是无意中关注他公众号结识上的。他公众号记录了很多职场成长、家庭建造上的思考和收获,还有定期个人复盘。他和太太都是大厂中层管理者,从文章中看到的不是他多厉害,而是非常接地气的故事。我们约饭了两次,有很多共同话题,现在还时不时有一些互动。 + +一位职级更高的同学,他在内网发起了一个"请我喝一杯咖啡,和我一起聊聊个人困惑"的活动,我报名参与了一期。和他聊天的过程,特别像是一场教练对话(最近学习教练课程时才感受到的),帮我排除干扰、聚焦目标的同时,也从他分享个人成长蜕变的过程,收获很多动力。(刚好自己最近也学习了教练技术,后面也准备采用类似的方式,去帮助曾经像我一样迷茫的人) + +还有一些协作方同学。他们工作做得超级到位,能感受到,他们在乎他人时间;稍微有点出彩的事儿,不忘记拉上更多人。利他和双赢思维,在他们身上是最好的阐释。 + +除了结识优秀的人,向他们学习外,还可以关注各个通道/工种的课程资源。 + +在大厂,多数人的角色都是螺丝钉,但千万不要局限于做一颗螺丝钉。多去学习一些通识课,了解商业交付的各个环节,看清商业世界,明白自己的定位,超越自己的定位。 + +## 04 一切都是争取来的 + +工作很多年了,很晚才明白这个道理。 + +之前一直认为,只要做好自己该做的,一定会被看见,被赏识,也会得到更多机会。但很多时候,这只是个人的一厢情愿。除了自己,不会有人关心你的权益。 + +社会主义初级阶段,我国国内的主要矛盾是人民日益增长的物质文化需要同落后的社会生产之间的矛盾。无论在哪里,资源都是稀缺的,自己在乎的,就得去争取。 + +想成长某个技能、想参与哪个模块、想做哪个项目,升职加薪……自己不提,不去争取,不会有人主动给你。 + +争不争取是一回事,能不能得到是一回事,只有争取,才有可能得到。争取了,即便没有得到,最终也没失去什么。 + +## 05 关注商业 + +大公司,极度关注效率,大部分岗位,拆解的粒度越细,效率会越高,这些对组织是有利的。但对个人来说,则很容易螺丝钉化。 + +做技术的同学,更是这样。 + +做前端的同学,不会关注数据是如何落库的;做后端的同学,不会思考页面是否存在兼容性问题;做业务开发的,不用考虑微服务诸多中间件是如何搭建起来的…… + +大部分人都想着怎么把自己这摊子事搞好,不会去思考上下游同学在做些什么,更少有人真正关注商业,关心公司的盈利模式,关心每一次产品迭代到底带来哪些业务价值。 + +把手头的事做好是应该的,但绝不能停留在此。所有的产品,只有在商业社会产生交付,让客户真正获益,才是有价值的。 + +关注商业,能帮我们升维到老板思维,明白投入产出比,抓大放小;也帮助我们,在碰到不好的业务时,及时止损;更重要的是,它帮助我们真正看清趋势,提前做好准备。 + +《五分钟商学院》系列,是很好的商业入门级书籍。尽管作者刘润最近存在争议,但不可否认,他比我们大多数人段位还是高很多,他的书值得一读。 + +## 06 培养数据思维 + +当今数字化时代,数据思维显得尤为重要。数据不仅可以帮助我们更好地了解世界,还可以指导我们的决策和行动。 + +非常幸运的是,在阿里和美团的两份经历,都是做商业化广告业务,在离钱💰最近的地方,也培养了数据的敏感性。见过商业数据指标的定义、加工、生产和应用全流程,也在不断熏陶下,能看懂大部分指标背后的价值。 + +除了直接面向业务的数据,还有研发协作全流程产生的数据。数据被记录和汇总统计后,能直观地看到每个环节的效率和质量。螺丝钉们的工作,也彻彻底底被数字量化,除了积极面对虚拟化、线上化、数字化外,我们别无他法。 + +受工作数据化的影响,生活中,我也渐渐变成了一个数据记录狂,日常运动(骑行、跑步、健走等)必须通过智能手表记录下来,没带 Apple Watch,感觉这次白运动了。每天也在很努力地完成三个圆环。 + +数据时代,我们沦为了透明人。也得益于数据被记录和分析,我们做任何事,都能快速得到反馈,这也是自我提升的一个重要环节。 + +## 07 做一个好"销售" + +就某种程度来说,所有的工作,本质都是销售。 + +这是很多大咖的观点,我也是很晚才明白这个道理。 + +我们去一家公司应聘,本质上是在讲一个「我很牛」的故事,销售的是自己;日常工作汇报、季度/年度述职、晋升答辩,是在销售自己;在任何一个场合曝光,也是在销售自己。 + +如果我们所服务的组织,对外提供的是一件产品或一项服务,所有上下游协作的同学,唯一在做的事就是,齐心协力把产品/服务卖出去, 我们本质做的还是销售。 + +所以, 千万不要看不起任何销售,也不要认为认为销售是一件很丢面子的事。 + +真正的大佬,随时随地都在销售。 + +## 08 少加班多运动 + +在职场,大家都认同一个观点,工作是做不完的。 + +我们要做的是,用好时间管理四象限法,识别重要程度和优先级,有限时间,聚焦在固定几件事上。 + +这要求我们不断提高自己的问题识别能力、拆解能力,还有专注力。 + +我们会因为部分项目的需要而加班,但不会长期加班。 + +加班时间短一点,就能腾出更多时间运动。 + +最近一次线下培训课,认识一位老师 Hubert,Hubert 是一位超级有魅力的中年大叔(可以通过「有意思教练」的课程链接到他),从外企高管的位置离开后,和太太一起创办了一家培训机构。作为公司高层,日常工作非常忙,头发也有些花白了,但一身腱子肉胜过很多健身教练,给人的状态也是很年轻。聊天得知,Hubert 经常 5 点多起来泡健身房~ + +我身边还有一些同事,跟我年龄差不多,因为长期加班,发福严重,比实际年龄看起来苍老 10+岁; + +还有同事曾经加班进 ICU,幸好后面身体慢慢恢复过来。 + +It is not uncommon for employees in a certain factory to die suddenly after working overtime for a long time. + +Reducing overtime and increasing exercise is definitely a very cost-effective thing. + +## 09 Have the confidence to leave at any time + +Today's workplace is completely different from that of our parents' generation. The diversity and change of occupations are getting faster and faster. Few people can stay in the same job or the same company for a lifetime. Except for certain specific positions, such as civil servants, public institutions, etc., most people will experience multiple career changes and adjustments during their careers. + +In business organizations, individuals are the weakest group, but don’t be the weakling. Every career and every job is a practice given to us by God. + +I really like the term "cultivating truth through false pretenses". Are the projects we participate in, large and small, important? It may be important to the company, but not necessarily to the individual. We do it because we are forced to make a living; + +On the other hand, the insights, insights, and experiences gained from participating in each project are real, and many abilities are improved through this process. + +If you understand this, you will not be trapped in the workplace and will deliberately improve yourself in various things. The more you accumulate, the deeper and broader your understanding of the nature of things will be. The more you will believe that a lot of basic knowledge is universal, the calmer your heart will be, and you will build up the confidence to leave at any time. + +## 10 Just a job + +At work, we often encounter various challenges and difficulties, such as development bottlenecks, difficult people and things to handle, and even PUA in the workplace. These experiences can leave us feeling exhausted, frustrated, and even doubting our abilities and worth. However, it is important to understand that difficulties are only temporary bumps in the road of growth, not what defines us. + +Writing summaries and reviews are great ways to help us clarify our thinking, find the root cause of the problem, and learn how to deal with similar situations. But also be careful not to fall into the trap of self-doubt and internal friction. When encountering difficulties, you should learn to believe in yourself and actively look for solutions to problems instead of dwelling on your own shortcomings and mistakes. + +On the intranet, classmates often anonymously share that they are under too much pressure at work, often suffer from insomnia or even suffer from moderate depression. Every time I see these topics, I feel very sad. It is an indisputable fact that the general environment is not good, but it does not mean that individuals have no way out. + +We tend to preset difficulties and add a lot of "buts". When the windows are full of dust, don't try to clean them. Go out and you will see a blue sky. + +##Finally + +At the end of writing, I am particularly grateful for Meituan’s experience of more than three years. Thank you to my leaders, to the friends who have fought side by side, and to every classmate I have met who is in an ordinary position like me and trying hard to bring a glimmer of light around me. All encounters are fate. \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.en.md new file mode 100644 index 00000000000..785b36251ca --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.en.md @@ -0,0 +1,50 @@ +--- +title: How programmers learn new technologies quickly +category: Technical Article Collection +tag: + - Leveling guide +--- + +> **Recommendation**: This is an article in the leveling guide of ["Java Interview Guide North"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html), sharing my views on how to quickly learn a new technology. +> +> !["Java Interview Guide" Leveling Strategy](https://oss.javaguide.cn/javamianshizhibei/training-strategy-articles.png) + +Many times, due to work reasons, we need to quickly learn a certain technology and then apply it in the project. In other words, the company we want to interview requires a certain technology that we have not been exposed to before. In order to cope with the interview needs, we need to master this technology quickly. + +As a programmer who is purely self-taught, this article briefly talks about my views on how to quickly learn a certain technology. + +When learning any technology, you must first understand what problem the technology is designed to solve. Before studying this technology in depth, you must first understand this technology from a global perspective, think about what modules it is composed of, what functions it provides, and what advantages it must have over similar technologies. + +For example, when we are learning Spring, through Spring's official documents you can know the latest technical trends of Spring, what modules Spring contains, and what problems Spring can help you solve. + +![](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/20210506110341207.png) + +For another example, when I am learning about message queues, I will first understand what role this message queue generally plays in the system and what problems it helps us solve. There are many types of message queues. When I study a specific message queue, I will compare it with the message queues I have already studied. When I was learning RocketMQ myself, I would first compare it with ActiveMQ, the first message queue I had ever studied, and think about what improvements RocketMQ had compared to ActiveMQ, what pain points it solved for ActiveMQ, what similarities and differences there were between the two. + +**The most effective and fastest way to learn a technology is to connect this technology with the technology you have learned before to form a network. ** + +Then, I suggest that you first read the tutorials in the official documentation, run the relevant Demo, and do some small projects. + +However, official documents are usually in English, and usually only domestic projects and a small number of foreign projects provide Chinese documents. Moreover, the introduction in official documents is often rough and not suitable for beginners as learning materials. + +If you don’t understand the official website’s documentation, you can also search for relevant keywords to find some high-quality blogs or videos to watch. **Be sure not to understand the principles of this technology right from the beginning. ** + +For example, when we are learning the Spring framework, I suggest that after you understand the problems that the Spring framework solves, you do not directly start studying the principles or source code of the Spring framework, but first actually experience the core functions provided by the Spring framework, IoC (Inverse of Control: Inversion of Control) and AOP (Aspect-Oriented Programming: Aspect-oriented programming), use the Spring framework to write some demos, or even use the Spring framework to do some small projects. + +In a nutshell, **Before studying the principles of this technology, you must first understand how this technology is used. ** + +Such a step-by-step learning process can gradually help you build the pleasure of learning, gain an immediate sense of accomplishment, and avoid being discouraged from directly studying theoretical knowledge. + +**When studying a certain technical principle, in order to avoid the content being too abstract, we can also practice it. ** + +For example, when we were learning the principles of Tomcat, we found that Tomcat's custom thread pool is quite interesting. Then we can also handwrite a customized version of the thread pool ourselves. Another example is when we learn the Dubbo principle, we can build a simple version of the RPC framework by ourselves. + +In addition, there are actually some differences between the technologies that need to be used in learning projects and those that need to be used in interviews. + +If you are learning a certain technology to use it in actual projects, then your focus is to learn the use of this technology and best practices, and understand the problems that may be encountered during the use of this technology. Your ultimate goal is that the technology brings real results to the project, and that the results are positive. + +If you are learning a certain technology just for the interview, then your focus should be on some of the most common questions about this technology in the interview, which is what we often call the eight-part essay. + +Many people look down upon it when it comes to Eight-Part Part Essay. In my opinion, if you don't memorize eight-part essays by rote, but think about the essence of these interview questions. In the process of preparing eight-part essays, you can also deepen your understanding of this technology. + +Finally, the most important and difficult thing is the unity of knowledge and action! Unity of knowledge and action! Unity of knowledge and action! ** Whether it is programming or other fields, the most important thing is not how much you know, but to try to integrate knowledge with action. \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.en.md new file mode 100644 index 00000000000..056a1806e51 --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.en.md @@ -0,0 +1,108 @@ +--- +title: 给想成长为高级别开发同学的七条建议 +category: 技术文章精选集 +author: Kaito +tag: + - 练级攻略 +--- + +> **推荐语**:普通程序员要想成长为高级程序员甚至是专家等更高级别,应该注意在哪些方面注意加强?开发内功修炼号主飞哥在这篇文章中就给出了七条实用的建议。 +> +> **内容概览**: +> +> 1. 刻意加强需求评审能力 +> 2. 主动思考效率 +> 3. 加强内功能力 +> 4. 思考性能 +> 5. 重视线上 +> 6. 关注全局 +> 7. 归纳总结能力 +> +> **原文地址**: + +### 建议 1:刻意加强需求评审能力 + +先从需求评审开始说。在互联网公司,需求评审是开发工作的主要入口。 + +对于普通程序员来说,一般就是根据产品经理提的需求细节,开始设想这个功能要怎么实现,开发成本大概需要多长时间。把自己当成了需求到代码之间的翻译官。很少去思考需求的合理性,对于自己做的事情有多大价值,不管也不问。 + +而对于高级别的程序员来说,并不会一开始就陷入细节,而是会更多地会从产品本身出发,询问产品经理为啥要做这个细节,目的是啥。换个说法,就是会先考虑这个需求是不是合理。 + +如果需求高级不合理就进行 PK ,要么对需求进行调整,要么就砍掉。不过要注意的是 PK 和调整需求不仅仅砍需求,还有另外一个方向,那就是对需求进行加强。 + +产品同学由于缺乏技术背景,很可能想的并不够充分,这个时候如果你有更好的想法,也完全可以提出来,加到需求里,让这个需求变得更有价值。 + +总之,高级程序员并不会一五一十地按产品经理的需求文档来进行后面的开发,而是**一切从有利于业务的角度出发思考,对产品经理的需求进行删、改、增。** + +这样的工作表面看似和开发无关,但是只有这样才能保证后续所有开发同学都是有价值的,而不是做一堆无用功。无用功做的多了会极大的挫伤开发的成就感。 + +所以,**普通程序员要想成长为更高级别的开发,一定要加强需求评审能力的培养**。 + +### 建议 2:主动思考效率 + +普通的程序员,按部就班的去写代码,有活儿来我就干,没活儿的时候我就呆着。很少去深度思考现有的这些代码为什么要这么写,这么写的好处是啥,有哪些地方存在瓶颈,我是否可以把它优化一些。 + +而高级一点程序员,并不会局限于把手头的活儿开发就算完事。他们会主动去琢磨,现在这种开发模式是不是不够的好。那么我是否能做一个什么东西能把这个效率给提升起来。 + +举一个小例子,我 6 年前接手一个项目的时候,我发现运营一个月会找我四次,就是找我给她发送一个推送。她说以前的开发都是这么帮他弄的。虽然这个需求处理起来很简单,改两行发布一下就完事。但是烦啊,你想象一下你正专心写代码呢,她又双叒来找你了,思路全被她中断了。而且频繁地操作线上本来就会引入不确定的风险,万一那天手一抽抽搞错了,线上就完蛋了。 + +我的做法就是,我专门抽了一周的时间,给她做了一套运营后台。这样以后所有的运营推送她就直接在后台上操作就完事了。我倒出精力去做其它更有价值的事情去了。 + +所以,**第二个建议就是要主动思考一下现有工作中哪些地方效率有改进的空间,想到了就主动去改进它!** + +### 建议 3:加强内功能力 + +哪些算是内功呢,我想内功修炼的读者们肯定也都很熟悉的了,指的就是大家学校里都学过的操作系统、网络等这些基础。 + +普通的程序员会觉得,这些基础知识我都会好么,我大学可是足足学了四年的。工作了以后并不会刻意来回头再来加强自己在这些基础上的深层次的提升。 + +高级的程序员,非常清楚自己当年学的那点知识太皮毛了。工作之余也会深入地去研究 Linux、研究网络等方向的底层实现。 + +事实上,互联网业界的技术大牛们很大程度是因为对这些基础的理解相当是深厚,具备了深厚的内功以后才促使他们成长为了技术大牛。 + +我很难相信一个不理解底层,只会 CURD,只会用别人框架的开发将来能在技术方向成长为大牛。 + +所以,**还建议多多锻炼底层技术内功能力**。如果你不知道怎么练,那就坚持看「开发内功修炼」公众号。 + +### 建议 4:思考性能 + +普通程序员往往就是把需求开发完了就不管了,只要需求实现了,测试通过了就可以交付了。将来流量会有多大,没想过。自己的服务 QPS 能支撑多少,不清楚。 + +而高级的程序员往往会关注自己写出来的代码的性能。 + +在需求评审的时候,他们一般就会估算大概的请求流量有多大。进而设计阶段就会根据这个量设计符合性能要求的方案。 + +在上线之前也会进行性能压测,检验一下在性能上是否符合预期。如果性能存在问题,瓶颈在哪儿,怎么样能进行优化一下。 + +所以,**第四个建议就是一定要多多主动你所负责业务的性能,并多多进行优化和改进**。我想这个建议的重要程度非常之高。但这是需要你具备深厚的内功才可以办的到的,否则如果你连网络是怎么工作的都不清楚,谈何优化! + +### 建议 5:重视线上 + +普通程序员往往对线上的事情很少去关注,手里记录的服务器就是自己的开发机和发布机,线上机器有几台,流量多大,最近有没有波动这些可能都不清楚。 + +而高级的程序员深深的明白,有条件的话,会尽量多多观察自己的线上服务,观察一下代码跑的咋样,有没有啥 error log。请求峰值的时候 CPU、内存的消耗咋样。网络端口消耗的情况咋样,是否需要调节一些参数配置。 + +当性能不尽如人意的时候,可能会回头再来思考出性能的改进方案,重新开发和上线。 + +你会发现在线上出问题的时候,能紧急扑上前线救火的都是高级一点的程序员。 + +所以,**飞哥给的第五个建议就是要多多观察线上运行情况**。只有多多关注线上,当线上出故障的时候,你才能承担的起快速排出线上问题的重任。 + +### 建议 6:关注全局 + +普通程序员是你分配给我哪个模块,我就干哪个模块,给自己的工作设定了非常小的一个边界,自己所有的眼光都聚集在这个小框框内。 + +高级程序员是团队内所有项目模块,哪怕不是他负责的,他也会去熟悉,去了解。具备这种思维的同学无论在技术上,无论是在业务上,成长的也都是最快的。在职级上得到晋升,或者是职位上得到提拔的往往都是这类同学。 + +甚至有更高级别的同学,还不止于把目光放在团队内,甚至还会关注公司内其它团队,甚至是业界的业务和技术栈。写到这里我想起了张一鸣说过的,不给自己的工作设边界。 + +所以,**建议要有大局观,不仅仅是你负责的模块,整个项目其实你都应该去关注**。而不是连自己组内同学做的是啥都不知道。 + +### 建议 7:归纳总结能力 + +普通程序员往往是工作的事情做完就拉到,很少回头去对自己的技术,对业务进行归纳和总结。 + +而高级的程序员往往都会在一件比较大的事情做完之后总结一下,做个 ppt,写个博客啥的记录下来。这样既对自己的工作是一个归纳,也可以分享给其它同学,促进团队的共同成长。 + + + diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.en.md new file mode 100644 index 00000000000..ecce3707e4a --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.en.md @@ -0,0 +1,137 @@ +--- +title: 十年大厂成长之路 +category: 技术文章精选集 +author: CodingBetterLife +tag: + - 练级攻略 +--- + +> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。 +> +> **原文地址:** + +最近这段时间,有好几个年轻的同学和我聊到自己的迷茫。其中有关于技术成长的、有关于晋升的、有关于择业的。我很高兴他们愿意听我这个“过来人”分享自己的经验。 + +我自己毕业后进入大厂,在大厂工作 12 年,我说的内容都来自于我自己或者身边人的真实情况。尤其,我会把 **【我自己走过的弯路】** 和 **【我看到过的优秀技术人的特质】** 相结合来给出建议。 + +这些内容我觉得具有普遍的指导意义,所以决定做个整理分享出来。我相信,无论你在大厂还是小厂,如果你相信这些建议,或早或晚他们会帮助到你。 + +我自己工作 12 年,走了些弯路,所以我就来讲讲,“在一个技术人 10 年的发展过程中,应该注意些什么”。我们把内容分为两块: + +1. **十年技术路怎么走** +2. **一些重要选择** + +## 01 十年技术路怎么走 + +### 【1-2 年】=> 从“菜鸟”到“职业” + +应届生刚进入到工作时,会有各种不适应。比如写好的代码会被反复打回、和团队老司机讨论技术问题会有一堆问号、不敢提问和质疑、碰到问题一个人使劲死磕等等。 + +**简单来说就是,即使日以继夜地埋头苦干,最后也无法顺利的开展工作。** + +这个阶段最重要的几个点: + +**【多看多模仿】**:比如写代码的时候,不要就像在学校完成书本作业那样只关心功能是否正确,还要关心模块的设计、异常的处理、代码的可读性等等。在你还没有了解这些内容的精髓之前,也要照猫画虎地模仿起来,慢慢地你就会越来越明白真实世界的代码是怎么写的,以及为什么要这么写。 + +做技术方案的时候也是同理,技术文档的要求你也许并不理解,但你可以先参考已有文档写起来。 + +**【脸皮厚一点】**:不懂就问,你是新人大家都是理解的。你做的各种方案也可以多找老司机们 review,不要怕被看笑话。 + +**【关注工作方式】**:比如发现需求在计划时间完不成就要尽快报风险、及时做好工作内容的汇报(例如周报)、开会后确定会议结论和 todo 项、承诺时间就要尽力完成、严格遵循公司的要求(例如发布规范、权限规范等) + +一般来说,工作 2 年后,你就应该成为一个职业人。老板可以相信任何工作交到你的手里,不会出现“意外”(例如一个重要需求明天要上线了,突然被告知上不了)。 + +### 【3-4 年】=> 从“职业”到“尖兵” + +工作两年后,对业务以及现有系统的了解已经到达了一定的程度,技术同学会开始承担更有难度的技术挑战。 + +例如需要将性能提升到某一个水位、例如需要对某一个重要模块进行重构、例如有个重要的项目需要协同 N 个团队一起完成。 + +可见,上述的这些技术问题,难度都已经远远超过一个普通的需求。解决这些问题需要有一定的技术能力,同时也需要具备更高的协同能力。 + +这个阶段最重要的几个点: + +**【技术能力提升】**:无论是公司内还是公司外的技术内容,都要多做主动的学习。基本上这个阶段的技术难题都集中在【性能】【稳定性】和【扩展性】上,而这些内容在业界都是有成型的方法论的。 + +**【主人翁精神】**:技术难题除了技术方案设计及落地外,背后还有一系列的其他工作。例如上线后对效果的观测、重点项目对于上下游改造和风险的了解程度、对于整个技改后续的计划(二期、三期的优化思路)等。 + +在工作四年后,基本上你成为了团队的一、二号技术位。很多技术难题即使不是你来落地,也是由你来决定方案。你会做调研、会做方案对比、会考虑整个技改的生命周期。 + +### 【5-7 年】=> 从“尖兵”到“专家” + +技术尖兵重点在于解决某一个具体的技术难题或者重点项目。而下一步的发展方向,就是能够承担起来一整个“业务板块”,也就是“领域技术专家”。 + +想要承担一整个“业务板块”需要 **【对业务领域有深刻的理解,同时基于这些理解来规划技术的发展方向】** 。 + +拿支付做个例子。简单的支付功能其实很容易完成,只要处理好和双联(网联和银联)的接口调用(成功、失败、异常)即可。但在很多背景下,支付没有那么简单。 + +例如,支付是一个用户敏感型操作,非常强调用户体验,如何能兼顾体验和接口的不稳定?支付接口还需要承担费用,同步和异步的接口费用不同,如何能够降本?支付接口往往还有限额等。这一系列问题的背后涉及到很多技术的设计,包括异步化、补偿设计、资金流设计、最终一致性设计等等。 + +这个阶段最重要的几个点: + +**【深入理解行业及趋势】**:密切关注行业的各种变化(新鲜的玩法、政策的变动、竞对的策略、科技等外在因素的影响等等),和业务同学加强沟通。 + +**【深入了解行业解决方案】**:充分对标已有的国内外技术方案,做深入学习和尝试,评估建设及运维成本,结合业务趋势制定计划。 + +### 【8-10 年】=> 从“专家”到“TL” + +其实很多时候,如果能做到专家,基本也是一个 TL 的角色了,但这并不代表正在执行 TL 的职责。 + +专家虽然已经可以做到“为业务发展而规划好技术发展”,但问题是要怎么落地呢?显然,靠一个人的力量是不可能完成建设的。所以,这里的 TL 更多强调的不是“领导”这个职位,而是 **【通过聚合一个团队的力量来实施技术规划】** 。 + +所以,这里的 TL 需要具备【团队技术培养】【合理分配资源】【确认工作优先级】【激励与奖惩】等各种能力。 + +这个阶段最重要的几个点: + +**【学习管理学】**:这里的管理学当然不是指 PUA,而是指如何在每个同学都有各自诉求的现实背景下,让个人目标和团队目标相结合,产生向前发展的动力。 + +**【始终扎根技术】**:很多时候,工作重心偏向管理以后,就会荒废技术。但事实是,一个优秀的领导永远是一个优秀的技术人。参与一起讨论技术方案并给予指导、不断扩展自己的技术宽度、保持对技术的好奇心,这些是让一个技术领导持续拥有向心力的关键。 + +## 02 一些重要选择 + +下面来聊聊在十年间我们可能会碰到的一些重要选择。这些都是真实的血与泪的教训。 + +### 我该不该转岗? + +大厂都有转岗的机制。转岗可以帮助员工寻找自己感兴趣的方向,也可以帮助新型团队招募有即战力的同学。 + +转岗看似只是在公司内部变动,但你需要谨慎决定。 + +本人转岗过多次。虽然还在同一家公司,但转岗等同于换工作。无论是领域沉淀、工作内容、信任关系、协作关系都是从零开始。 + +针对转岗我的建议是:**如果你是想要拓宽自己的技术广度,也就是抱着提升技术能力的想法,我觉得可以转岗。但如果你想要晋升,不建议你转岗。**晋升需要在一个领域的持续积淀和在一个团队信任感的持续建立。 + +当然,转岗可能还有其他原因,例如家庭原因、身体原因等,这个不展开讨论了。 + +### 我该不该跳槽? + +跳槽和转岗一样,往往有很多因素造成,不能一概而论,我仅以几个场景来说: + +**【晋升失败】**:扪心自问,如果你觉得自己确实还不够格,那你就踏踏实实继续努力。如果你觉得评委有失偏颇,你可以尝试去外面面试一下,让市场来给你答案。 + +**【成长局限】**:觉得自己做的事情没有挑战,无法成长。你可以和老板聊一下,有可能是因为你没有看到其中的挑战,也有可能老板没有意识到你的“野心”。 + +**【氛围不适】**:一般来自于新入职或者领导更换,这种情况下不适是正常的。我的建议是,**如果一个环境是“对事不对人”的,那就可以留下来**,努力去适应,这种不适应只是做事方式不同导致的。但如果这个环境是“对人不对事”的话,走吧。 + +### 跳槽该找怎样的工作? + +我们跳槽的时候往往会同时面试好几家公司。行情好的时候,往往可以收到多家 offer,那么我们要如何选择呢? + +考虑一个 offer 往往有这几点:【公司品牌】【薪资待遇】【职级职称】【技术背景】。每个同学其实都有自己的诉求,所以无论做什么选择都没有对错之分。 + +我的一个建议是:**你要关注新岗位的空间,这个空间是有希望满足你的期待的**。 + +比如,你想成为架构师,那新岗位是否有足够的技术挑战来帮助你提升技术能力,而不仅仅是疲于奔命地应付需求? + +比如,你想往技术管理发展,那新岗位是否有带人的机会?是否有足够的问题需要搭建团队来解决? + +比如,你想扎根在某个领域持续发展(例如电商、游戏),那新岗位是不是延续这个领域,并且可以碰到更多这个领域的问题? + +当然,如果薪资实在高到无法拒绝,以上参考可以忽略! + +## 结语 + +以上就是我对互联网从业技术人员十年成长之路的心得,希望在你困惑和关键选择的时候可以帮助到你。如果我的只言片语能够在未来的某个时间帮助到你哪怕一点,那将是我莫大的荣幸。 + + + diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.en.md new file mode 100644 index 00000000000..667b98c55e2 --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.en.md @@ -0,0 +1,206 @@ +--- +title: 程序员的技术成长战略 +category: 技术文章精选集 +author: 波波微课 +tag: + - 练级攻略 +--- + +> **推荐语**:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。 +> +> **原文地址:** + +## 1. 前言 + +在波波的微信技术交流群里头,经常有学员问关于技术人该如何学习成长的问题,虽然是微信交流,但我依然可以感受到小伙伴们焦虑的心情。 + +**技术人为啥焦虑?** 恕我直言,说白了是胆识不足格局太小。胆就是胆量,焦虑的人一般对未来的不确定性怀有恐惧。识就是见识,焦虑的人一般看不清楚周围世界,也看不清自己和适合自己的道路。格局也称志向,容易焦虑的人通常视野窄志向小。如果从战略和管理的视角来看,就是对自己和周围世界的认知不足,没有一个清晰和长期的学习成长战略,也没有可执行的阶段性目标计划+严格的执行。 + +因为问此类问题的学员很多,让我感觉有点烦了,为了避免重复回答,所以我专门总结梳理了这篇长文,试图统一来回答这类问题。如果后面还有学员问类似问题,我会引导他们来读这篇文章,然后让他们用三个月、一年甚至更长的时间,去思考和回答这样一个问题:**你的技术成长战略究竟是什么?** 如果你想清楚了这个问题,有清晰和可落地的答案,那么恭喜你,你只需按部就班执行就好,根本无需焦虑,你实现自己的战略目标并做出成就只是一个时间问题;否则,你仍然需要通过不断磨炼+思考,务必去搞清楚这个人生的大问题!!! + +下面我们来看一些行业技术大牛是怎么做的。 + +## 二. 跟技术大牛学成长战略 + +我们知道软件设计是有设计模式(Design Pattern)的,其实技术人的成长也是有成长模式(Growth Pattern)的。波波经常在 Linkedin 上看一些技术大牛的成长履历,探究其中的成长模式,从而启发制定自己的技术成长战略。 + +当然,很少有技术大牛会清晰地告诉你他们的技术成长战略,以及每一年的细分落地计划。但是,这并不妨碍我们通过他们的过往履历和产出成果,去溯源他们的技术成长战略。实际上, **越是牛逼的技术人,他们的技术成长战略和路径越是清晰,我们越容易从中探究出一些成功的模式。** + +### 2.1 系统性能专家案例 + +国内的开发者大都热衷于系统性能优化,有些人甚至三句话离不开高性能/高并发,但真正能深入这个领域,做到专家级水平的却寥寥无几。 + +我这边要特别介绍的这个技术大牛叫 **Brendan Gregg** ,他是系统性能领域经典书《System Performance: Enterprise and the Cloud》(中文版[《性能之巅:洞悉系统、企业和云计算》](https://www.amazon.cn/dp/B08GC261P9))的作者,也是著名的[性能分析利器火焰图(Flame Graph)](https://github.com/brendangregg/FlameGraph)的作者。 + +Brendan Gregg 之前是 Netflix 公司的高级性能架构师,在 Netflix 工作近 7 年。2022 年 4 月,他离开了 Netflix 去了 Intel,担任院士职位。 + +![](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/)上,可以说他是一个非常高产的技术大牛。 + +![性能工具](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 是名副其实的,世界级的,系统性能领域的大神级人物。 + +### 2.2 从开源到企业案例 + +我要分享的第二个技术大牛是 **Jay Kreps**,他是知名的开源消息中间件 Kafka 的创始人/架构师,也是 Confluent 公司的联合创始人和 CEO,Confluent 公司是围绕 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 年初)和社区生态的建设。 + +到 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 年。 + +![Confluent创始人三人组](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231805796.png) + +上图是 Confluent 创始人三人组,一个非常有意思的组合,一个中国人(左),一个印度人(右),中间的 Jay Kreps 是美国人。 + +我之所以对 Kafka 和 Jay Kreps 的印象特别深刻,是因为在 2012 年下半年,我在携程框架部也是专门搞大数据采集的,我还开发过一套功能类似 Kafka 的 Log Collector + Agent 产品。我记得同时期有不止 4 个同类型的开源产品:Facebook Scribe、Apache Chukwa、Apache Flume 和 Apache Kafka。现在回头看,只有 Kafka 走到现在发展得最好,这个和创始人的专注和持续投入是分不开的,当然背后和几个创始人的技术大格局也是分不开的。 + +当年我对战略性思维几乎没有概念,还处在**什么技术都想学、认为各种项目做得越多越牛的阶段**。搞了半年的数据采集以后,我就掉头搞其它“更有趣的”项目去了(从这个事情的侧面,也可以看出我当年的技术格局是很小的)。中间我陆续关注过 Jay 的一些创业动向,但是没想到他能把 Confluent 公司发展到目前这个规模。现在回想,其实在十年前,Jay Kreps 对自己的技术成长就有比较明确的战略性思考,也具有大的技术格局和成事的一些必要特质。Jay Kreps 和 Kafka 给我上了一堂生动的技术战略和实践课。 + +### 2.3 技术媒体大 V 案例 + +介绍到这里,有些同学可能会反驳说:波波你讲的这些大牛都是学历背景好,功底扎实起点高,所以他们才更能成功。其实不然,这里我再要介绍一位技术媒体界的大 V 叫 Brad Traversy,大家可以看[他的 Linkedin 简历](https://www.linkedin.com/in/bradtraversy/),背景很一般,学历差不多是一个非正规的社区大学(相当于大专),没有正规大厂工作经历,有限几份工作一直是在做网站外包。 + +![](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 万。 + +![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/160b0bc4f689413757b9b5e2448f940b.png) + +Brad Traversy 目前是自由职业者,他的 Youtube 广告+Udemy 课程的收入相当不错。 + +就是这样一位技术媒体大 V,你很难想象,在年轻的时候,贴在他身上的标签是:不良少年,酗酒,抽烟,吸毒,纹身,进监狱。。。直 + +到结婚后的第一个孩子诞生,他才开始担起责任做出改变,然后凭借对技术的一腔热情,开始在 Youtube 平台上持续输出免费课程。从此他找到了适合自己的战略目标,然后人生开始发生各种积极的变化。。。如果大家对 Brad Traversy 的过往经历感兴趣,推荐观看他在 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 年后,他的订阅量开始出现拐点。**如果把这些数据画出来,将会是一条非常漂亮的复利曲线**。 + +### 2.4 案例小结 + +Brendan Gregg,Jay Kreps 和 Brad Traversy 三个人走的技术路线各不相同,但是他们的成功具有共性或者说模式: + +**1、找到了适合自己的长期战略目标。** + +- Brendan Gregg: 成为系统性能领域顶级专家 +- Jay Kreps:开创基于 Kafka 开源消息队列的企业服务公司,并将公司做到上市 +- Brad Traversy: 成为技术媒体领域大 V 和课程讲师,并以此作为自己的职业 + +**2、专注深耕一个(或有限几个相关的)细分领域(Niche),保持定力,不随便切换领域。** + +- Brendan Gregg:系统性能领域 +- Jay Kreps: 消息中间件/实时计算领域+创业 +- Brad Traversy: 技术媒体/教学领域,方向 Web 开发 + 编程语言 + +**3、长期投入,三人都持续投入了 10 年。** + +**4、年度细分计划+持续可量化的价值产出(Persistent & Measurable Value Output)。** + +- Brendan Gregg:除公司日常工作产出以外,每年有超过 10 份以上的技术文档和演讲视频产出,平均每年有 2.5 个开源工具产出。十年共产出书籍 2 本,其中《System Performance》已经更新到第二版。 +- Jay Kreps:总体有开源产品+公司产出,1 本书产出,每年有 Kafka 和周边产品发版若干。 +- Brad Traversy: 每年有 Youtube 免费视频产出(平均每年 80+)+Udemy 收费视频课产出(平均每年 1.5 门)。 + +**5、以终为始是牛人和普通人的一大区别。** + +普通人通常走一步算一步,很少长远规划。牛人通 常是先有远大目标,然后采用倒推法,将大目标细化到每年/月/周的详细落地计划。Brendan Gregg,Jay Kreps 和 Brad Traversy 三人都是以终为始的典型。 + +![以终为始](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231833871.png) + +上面总结了几位技术大牛的成长模式,其中一个重点就是:这些大牛的成长都是通过 **持续有价值产出(Persistent Valuable Output)** 来驱动的。持续产出为啥如此重要,这个还要从下面的学习金字塔说起。 + +## 三、学习金字塔和刻意训练 + +![学习金字塔](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231836811.png) + +学习金字塔是美国缅因州国家训练实验室的研究成果,它认为: + +> 1. 我们平时上课听讲之后,学习内容平均留存率大致只有 5%左右; +> 2. 书本阅读的平均留存率大致只有 10%左右; +> 3. 学习配上视听效果的课程,平均留存率大致在 20%左右; +> 4. 老师实际动手做实验演示后的平均留存率大致在 30%左右; +> 5. 小组讨论(尤其是辩论后)的平均留存率可以达到 50%左右; +> 6. 在实践中实际应用所学之后,平均留存率可以达到 75%左右; +> 7. 在实践的基础上,再把所学梳理出来,转而再传授给他人后,平均留存率可以达到 90%左右。 + +上面列出的 7 种学习方法,前四种称为 **被动学习** ,后三种称为 **主动学习**。 + +拿学游泳做个类比,被动学习相当于你看别人游泳,而主动学习则是你自己要下水去游。我们知道游泳或者跑步之类的运动是要燃烧身体卡路里的,这样才能达到锻炼身体和长肌肉的效果(肌肉是卡路里燃烧的结果)。如果你只是看别人游泳,自己不实际去游,是不会长肌肉的。同样的,主动学习也是要燃烧脑部卡路里的,这样才能达到训练大脑和长脑部“肌肉”的效果。 + +我们也知道,燃烧身体的卡路里,通常会让人感觉不舒适,如果燃烧身体卡路里会让人感觉舒适的话,估计这个世界上应该不会有胖子这类人。同样,燃烧脑部卡路里也会让人感觉不适、紧张、出汗或语无伦次,如果燃烧脑部卡路里会让人感觉舒适的话,估计这个世界上人人都很聪明,人人都能发挥最大潜能。当然,这些不舒适是短期的,长期会使你更健康和聪明。波波一直认为, **人与人之间的先天身体其实都差不多,但是后天身体素质和能力有差异,这些差异,很大程度是由后天对身体和大脑的训练质量、频度和强度所造成的。** + +明白这个道理之后,心智成熟和自律的人就会对自己进行持续地 **刻意训练** 。这个刻意训练包括对身体的训练,比如波波现在每天坚持跑步 3km,走 3km,每天做 60 个仰卧起坐,5 分钟平板撑等等,每天保持让身体燃烧一定量的卡路里。刻意训练也包括对大脑的训练,比如波波现在每天做项目写代码 coding(训练脑+手),平均每天在 B 站上输出十分钟免费视频(训练脑+口头表达),另外有定期总结输出公众号文章(训练脑+文字表达),还有每天打半小时左右的平衡球(下图)或古墓丽影游戏(训练小脑+手),每天保持让大脑燃烧一定量的卡路里,并保持一定强度(适度不适感)。 + +![平衡球游戏](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231839985.png) + +关于刻意训练的专业原理和方法论,推荐看书籍《刻意练习》。 + +![刻意练习](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/format,png-20230309231842735.png) + +注意,如果你平时从来不做举重锻炼的,那么某天突然做举重会很不适应甚至受伤。脑部训练也是一样的,如果你从来没有做过视频输出,那么刚开始做会很不适应,做出来的视频质量会很差。不过没有关系,任何训练都是一个循序渐进,不断强化的过程。等大脑相关区域的"肌肉"长出来以后,会逐步进入正循环,后面会越来越顺畅,相关"肌肉"会越来越发达。所以,和健身一样,健脑也不能遇到困难就放弃,需要循序渐进(Incremental)+持续地(Persistent)刻意训练。 + +理解了学习金字塔和刻意训练以后,现在再来看 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛的做法,他们的学习成长都是建立在持续有价值产出的基础上的,这些产出都是刻意训练+燃烧脑部卡路里的成果。他们的产出要么是建立在实践基础上的产出,例如 Jay Kreps 的 Kafka 开源项目和 Confluent 公司;要么是在实践的基础上,再整理传授给其他人的产出,例如,Brendan Greeg 的技术演讲 ppt/视频,书籍,还有 Brad Traversy 的教学视频等等。换句话说,他们一直在学习金字塔的 5 ~ 7 层主动和高效地学习。并且,他们的学习产出还可以获得用户使用,有客户价值(Customer Value),有用户就有反馈和度量。记住,有反馈和度量的学习,也称闭环学习,它是能够不断改进提升的;反之,没有反馈和度量的学习,无法改进提升。 + +现在,你也应该明白,晒个书单秀个技能图谱很简单,读个书上个课也不难。但是要你给出 5 ~ 10 年的总体技术成长战略,再基于这个战略给出每年的细分落地计划(尤其是产出计划),然后再严格按计划执行,这的确是很难的事情。这需要大量的实践训练+深度思考,要燃烧大量的脑部卡路里!但这是上天设置的进化法则,成长为真正的技术大牛如同成长为一流的运动员,是需要通过燃烧与之相匹配量的卡路里来交换的。成长为真正的技术大牛,也是需要通过产出与之匹配的社会价值来交换的,只有这样社会才能正常进化。你推进了社会进化,社会才会回馈你。如果不是这样,社会就无法正常进化。 + +## 四、战略思维的诞生 + +![思考周期和机会点](https://oss.javaguide.cn/p3-juejin/dc87167f53b243d49f9f4e8c7fe530a1~tplv-k3u1fbpfcp-zoom-1.png) + +一般毕业生刚进入企业工作的时候,思考大都是以天/星期/月为单位的,基本上都是今天学个什么技术,明天学个什么语言,很少会去思考一年甚至更长的目标。这是个眼前漆黑看不到的懵懂时期,捕捉到机会点的能力和概率都非常小。 + +工作了三年以后,悟性好的人通常会以一年为思考周期,制定和实施一些年度计划。这个时期是相信天赋和比拼能力的阶段,可以捕捉到一些小机会。 + +工作了五年以后,一些悟性好的人会产生出一定的胆识和眼光,他们会以 3 ~ 5 年为周期来制定和实施计划,开始主动布局去捕捉一些中型机会点。 + +工作了十年以后,悟性高的人会看到模式和规则变化,例如看出行业发展模式,还有人才的成长模式等,于是开始诞生出战略性思维。然后他们会以 5 ~ 10 年为周期来制定和实施自己的战略计划,开始主动布局去捕捉一些中大机会点。Brendan Gregg,Jay Kreps 和 Brad Traversy 都是属于这个阶段的人。 + +当然还有很少一些更牛的时代精英,他们能够看透时代和人性,他们的思考是以一生甚至更长时间为单位的,这些超人不在本文讨论范围内。 + +## 五、建议 + +**1、以 5 ~ 10 年为周期去布局谋划你的战略。** + +现在大学生毕业的年龄一般在 22 ~ 23 岁,那么在工作了十年后,也就是在你 32 ~ 33 岁的时候,你也差不多看了十年了,应该对自己和周围的世界(你的行业和领域)有一个比较深刻的领悟了。**如果你到这个年纪还懵懵懂懂,今天抓东明天抓西,那么只能说你的胆识格局是相当的低**。在当前 IT 行业竞争这么激烈的情况下,到 35 岁被下岗可能就在眼前了。 + +有了战略性思考,你应该以 5 ~ 10 年为周期去布局谋划你的战略。以 Brendan Gregg,Jay Kreps 和 Brad Traversy 这些大牛为例,**人生若真的要干点成就出来,投入周期一般都要十年的**。从 33 岁开始,你大致有 3 个十年,因为到 60 岁以后,一般人都老眼昏花干不了大事了。如果你悟性差一点,到 40 岁才开始规划,那么你大致还有 2 个十年。如果你规划好了,这 2 ~ 3 个十年可以成就不小的事业。否则,你很可能一生都成就不了什么事业,或者一直在帮助别人成就别人的事业。 + +**2、专注自己的精力。** + +考虑到人生能干事业的时间也就是 2 ~ 3 个十年,你会发现人生其实很短暂,这时候你会把精力都投入到实现你的十年战略上去,没有时间再浪费在比如网上的闲聊和扯皮争论上去。 + +**3、细分落地计划尤其是产出计划。** + +有了十年战略方向,下一步是每年的细分落地计划,尤其是产出计划。这些计划主要应该工作在学习金字塔的 5/6/7 层。**产出应该是刻意训练+燃烧卡路里的结果,每天让身体和大脑都保持燃烧一定量的卡路里**。 + +**4、产出有价值的东西形成正反馈。** + +产出应该有客户价值,自己能学习(自己成长进化),对别人还有用(推动社会成长进化),这样可以得到**用户回馈和度量**,形成一个闭环,可以持续改进和提升你的学习。 + +**5、少即是多。** + +深耕一个(或有限几个相关的)领域。所有细分计划应该紧密围绕你的战略展开。克制内心欲望,不要贪多和分心,不要被喧嚣的世界所迷惑。 + +**6、战略方向+细分计划都要写下来,定期 review 优化。** + +**7、要有定力,持续努力。** + +曲则全、枉则直,战略实现是不可能直线的。战略方向和细分计划通常要按需调整,尤其在早期,但是最终要收敛。如果老是变不收敛,就是缺乏战略定力,是个必须思考和解决的大问题。 + +别人的成长战略可以参考,但是不要刻意去模仿,你有你自己的颜色,**你应该成为独一无二的你**。 + +战略方向和细分计划明确了,接下来就是按部就班执行,十年如一日铁打不动。 + +**8、慢就是快。** + +战略目标的实现也和种树一样是生长出来的,需要时间耐心栽培,记住**慢就是快。**焦虑纠结的时候,像念经一样默念王阳明《传习录》中的教诲: + +> 立志用功,如种树然。方其根芽,犹未有干;及其有干,尚未有枝;枝而后叶,叶而后花实。初种根时,只管栽培灌溉。勿作枝想,勿作花想,勿作实想。悬想何益?但不忘栽培之功,怕没有枝叶花实? +> +> 译文: +> +> 实现战略目标,就像种树一样。刚开始只是一个小根芽,树干还没有长出来;树干长出来了,枝叶才能慢慢长出来;树枝长出来,然后才能开花和结果。刚开始种树的时候,只管栽培灌溉,别老是纠结枝什么时候长出来,花什么时候开,果实什么时候结出来。纠结有什么好处呢?只要你坚持投入栽培,还怕没有枝叶花实吗? + + + diff --git a/docs_en/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.en.md b/docs_en/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.en.md new file mode 100644 index 00000000000..b4e0f839e18 --- /dev/null +++ b/docs_en/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.en.md @@ -0,0 +1,110 @@ +--- +title: 工作五年之后,对技术和业务的思考 +category: 技术文章精选集 +author: 知了一笑 +tag: + - 练级攻略 +--- + +> **推荐语**:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。 +> +> **原文地址:** + +苦海无边,回头无岸。 + +## 01 前言 + +晃晃悠悠的,在互联网行业工作了五年,默然回首,你看哪里像灯火阑珊处? + +初入职场,大部分程序员会觉得苦学技术,以后会顺风顺水升职加薪,这样的想法没有错,但是不算全面,五年后你会不会继续做技术写代码这是核心问题。 + +初入职场,会觉得努力加班可以不断提升能力,可以学到技术的公司就算薪水低点也可以接受,但是五年之后会认为加班都是在不断挤压自己的上升空间,薪水低是人生的天花板。 + +这里想说的关键问题就是:初入职场的认知和想法大部分不会再适用于五年后的认知。 + +工作五年之后面临的最大压力就是选择:职场天花板,技术能力天花板,薪水天花板,三十岁天花板。 + +如何面对这些问题,是大部分程序员都在思考和纠结的。做选择的唯一参考点就是:利益最大化,这里可以理解为职场更好的升职加薪,顺风顺水。 + +五年,变化最大不是工作经验,能力积累,而是心态,清楚的知道现实和理想之间是存在巨大的差距。 + +## 02 学会适应变化,并积累能力 + +回首自己的职场五年,最认可的一句话就是:学会适应变化,并积累能力。 + +变化的就是,五年的时间技术框架更新迭代,开发工具的变迁,公司环境队友的更换,甚至是不同城市的流浪,想着能把肉体和灵魂安放在一处,有句很经典的话就是:唯一不变的就是变化本身。 + +要积累的是:解决问题的能力,思考方式,拓宽认知。 + +这种很难直白的描述,属于个人认知的范畴,不同的人有不一样的看法,所以只能站在大众化的角度去思考。 + +首先聊聊技术,大部分小白级别的,都希望自己的技术能力不断提高,争取做到架构师级别,但是站在当前的互联网环境中,这种想法实现难度还是偏高,这里既不是打击也不是为了抬杠。 + +可以观察一下现状,技术团队大的 20-30 人,小的 10-15 人,能有一个架构师去专门管理底层框架都是少有现象。 + +这个问题的原因很多,首先架构师的成本过高,环境架构也不是需要经常升级,说的难听点可能框架比项目生命周期更高。 + +所以大部分公司的大部分业务,基于现有大部分成熟的开源框架都可以解决,这也就导致架构师这个角色通常由项目主管代替或者级别较高的开发直接负责,这就是现实情况。 + +这就导致技术框架的选择思路就是:只选对的。即这方面的人才多,开源解决方案多,以此降低技术方面对公司业务发展的影响。 + +那为什么还要不断学习和积累技术能力?如果没有这个能力,程序员岗位可能根本走不了五年之久,需要用技术深度积累不断解决工作中的各种问题,用技术的广度提升自己实现业务需求的认知边界,这是安放肉体的根本保障。 + +这就是导致很多五年以后的程序员压力陡然升高的原因,走向管理岗的另一个壁垒就是业务思维和认知。 + +## 03 提高业务能力的积累 + +程序员该不该用心研究业务,这个问题真的没有纠结的必要,只要不是纯技术型的公司,都需要面对业务。 + +不管技术、运营、产品、管理层,都是在面向业务工作。 + +从自己职场轨迹来看,五年变化最大就是解决业务问题的能力,职场之初面对很多业务场景都不知道如何下手,到几年之后设计业务的解决方案。 + +这是大部分程序员在职场前五年跳槽就能涨薪的根本原因,面对业务场景,基于积累的经验和现有的开源工具,能快速给出合理的解决思路和实现过程。 + +工作五年可能对技术底层的清晰程度都没有初入职场的小白清楚,但是写的程序却可以避开很多坑坑洼洼,对于业务的审视也是很细节全面。 + +解决业务能力的积累,对于技术视野的宽度需求更甚,比如职场初期对于海量数据的处理束手无策,但是在工作几年之后见识数据行业的技术栈,真的就是技术选型的视野问题。 + +什么是衡量技术能力的标准?站在一个共识的角度上看:系统的架构与代码设计能适应业务的不断变化和各种需求。 + +相对比与技术,业务的变化更加快速频繁,高级工程师或者架构师之所以薪资高,这些角色一方面能适应业务的迭代,并且在工作中具有一定前瞻性,会考虑业务变化的情况下代码复用逻辑,这样的能力是需要一定的技术视野和业务思维的沉淀。 + +所以职场中:业务能说的井井有条,代码能写的明明白白,得到机会的可能性更大。 + +## 04 不同的阶段技术和业务的平衡和选择 + +从理性的角度看技术和业务两个方面,能让大部分人职场走的平稳顺利,但是不同的阶段对两者的平衡和选择是不一样的。 + +在思考如何选择的时候,可以参考二八原则的逻辑,即在任何一组东西中,最重要的只占其中一小部分,约 20%,其余 80%尽管是多数,却是次要的,因此又称二八定律。 + +个人真的非常喜欢这个原则,大部分人都不是天才,所以很难三心二意同时做好几件事情,在同一时间段内应该集中精力做好一件事件。 + +但是单纯的二八原则模式可能不适应大部分职场初期的人,因为初期要学习很多内容,如何在职场生存:专业能力,职场关系,为人处世,产品设计等等。 + +当然这些东西不是都要用心刻意学习,但是合理安排二二六原则或其他组合是更明智的,首先是专业能力要重点练习,其次可以根据自己的兴趣合理选择一到两个方面去慢慢了解,例如产品,运营,运维,数据等,毕竟三五年以后会不会继续写代码很难说,多给自己留个机会总是有备无患。 + +在职场初期,基本都是从技术角度去思考问题,如何快速提升自己的编码能力,在公司能稳定是首要目标,因此大部分时间都是在做基础编码和学习规范,这时可能 90%的心思都是放在基础编码上,另外 10%会学习环境架构。 + +最多一到两年,就会开始独立负责模块需求开发,需要自己设计整个代码思路,这里业务就会进入视野,要懂得业务上下游关联关系,学会思考如何设计代码结构,才能在需求变动的情况下代码改动较少,这个时候可能就会放 20%的心思在业务方面,30%学习架构方式。 + +三到五年这个时间段,是解决问题能力提升最快的时候,因为这个阶段的程序员基本都是在开发核心业务链路,例如交易、支付、结算、智能商业等模块,需要对业务整体有较清晰的把握能力,不然就是给自己挖坑,这个阶段要对业务流付出大量心血思考。 + +越是核心的业务线,越是容易爆发各种问题,如果在日常工作中不花心思处理各种细节问题,半夜异常自动的消息和邮件总是容易让人憔悴。 + +所以努力学习技术是提升自己,培养自己的业务认知也同样重要,个人认为这二者的分量平分秋色,只是需要在合适的阶段做出合理的权重划分。 + +## 05 学会在职场做选择和生存 + +基于技术能力和业务思维,学会在职场做选择和生存,这些是职场前五年一路走来的最大体会。 + +不管是技术还是业务,这两个概念依旧是个很大的命题,不容易把握,所以学会理清这两个方面能力中的公共模块是关键。 + +不管技术还是业务,都不可能从一家公司完全复制到另一家公司,但是可以把一家公司的技术框架,业务解决方案学会,并且带到另一家公司,例如技术领域内的架构、设计、流程、数据管理,业务领域内的思考方式、产品逻辑、分析等,这些是核心能力并且是大部分公司人才招聘的要求,所以这些才是工作中需要重点积累的。 + +人的精力是有限的,而且面对三十这个天花板,各种事件也会接连而至,在职场中学会合理安排时间并不断提升核心能力,这样才能保证自己的竞争力。 + +职场就像苦海无边,回首望去可能也没有岸边停泊,但是要具有换船的能力或者有个小木筏也就大差不差了。 + + + diff --git a/docs_en/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.en.md b/docs_en/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.en.md new file mode 100644 index 00000000000..e9af312f533 --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.en.md @@ -0,0 +1,342 @@ +--- +title: 如何在技术初试中考察程序员的技术能力 +category: 技术文章精选集 +author: 琴水玉 +tag: + - 面试 +--- + +> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错! +> +> **内容概览:** +> +> - 实战与理论结合。比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? +> - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 +> - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 +> +> **原文地址**: + +## 灵魂三连问 + +1. 你觉得人怎么样? 【表达能力、沟通能力、学习能力、总结能力、自省改进能力、抗压能力、情绪管理能力、影响力、团队管理能力】 +2. 如果让他独立完成项目的设计和实现,你觉得他能胜任吗? 【系统设计能力、项目管理能力】 +3. 他的分析和解决问题的能力,你的评价是啥?【原理理解能力、实战应用能力】 + +## 考察目标和思路 + +首先明确,技术初试的考察目标: + +- 候选人的技术基础; +- 候选人解决问题的思路和能力。 + +技术基础是基石(冰山之下的东西),占七分, 解决问题的思路和能力是落地(冰山之上露出的部分),占三分。 业务和技术基础考察,三七开。 + +核心考察目标:分析和解决问题的能力。 + +技术层面:深度 + 应用能力 + 广度。 对于校招或社招 P6 级别以下,要多注重 深度 + 应用能力,广度是加分项; 在 P6 之上,可增加 广度。 + +- 校招:基础扎实,思维敏捷。 主要考察内容:基础数据结构与算法、进程与并发、内存管理、系统调用与 IO 机制、网络协议、数据库范式与设计、设计模式、设计原则、编程习惯; +- 社招:经验丰富,里外兼修。 主要考察内容:有一定深度的基础技术机制,比如 Java 内存模型及内存泄露、 JVM 机制、类加载机制、数据库索引及查询优化、缓存、消息中间件、项目、架构设计、工程规范等。 + +### 技术基础是什么? + +作为技术初试官,怎么去考察技术基础?究竟什么是技术基础?是知道什么,还是知道如何思考?知识作为现有的成熟原理体系,构成了基础的重要组成部分,而知道如何思考亦尤为重要。俗话说,知其然而知其所以然。知其然,是指熟悉现有知识体系,知其所以然,则是自底向上推导,真正理解知识的来龙去脉,理解为何是这样而不是那样。毕竟,对于本质是逻辑的程序世界而言,并无定法。知道如何思考,并能缜密地设计和开发,深入到细节,这就是技术基础吧。 + +### 为什么要考察技术基础? + +程序员最重要的两种技术思维能力,是逻辑思维能力和抽象设计能力。逻辑思维能力是基础,抽象设计能力是高阶。 考察技术基础,正好可以同时考察这两种思维能力。能不能理解基础技术概念及关联,是考察逻辑思维能力;能不能把业务问题抽象成技术问题并合理的组织映射,是考察抽象设计能力。 + +绝大部分业务问题,都可以抽象成技术问题。在某种意义上,业务问题只是技术问题的领域化表述。 + +因此,通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。 + +### 为什么不能单考察业务维度? + +因为业务方面通常比较熟悉,可能就直接按照现有方案说出来了,很难考察到候选人的深入理解、横向拓展和归纳总结能力。 + +这一点,建议有针对性地考察下候选人的归纳总结能力:比如, 微服务搭建或开发或维护/保证系统稳定性或性能方面的过程中,你收获了哪些可以分享的经验? + +### 为什么要考察业务维度? + +技术基础考察,容易错过的地方是,候选人的非技术能力特质,比如沟通组织能力、带项目能力、抗压能力、解决实际问题的能力、团队影响力、其它性格特质等。 + +## 考察方法 + +### 技术基础考察 + +技术基础怎么考察?通过有效的多角度的发问模式来考察。 + +**是什么-为什么** + +是什么考察对概念的基本理解,为什么考察对概念的实现原理。 + +比如索引是什么? 索引是如何实现的? + +**引导-横向发问-深入发问** + +引导性,比如 “你对 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; +- 这些技术基础正是程序世界里最有趣最激动人心的地方。如果对这些不感兴趣,就很难在这个领域里深入进去,不如及早转行从事其它职业,非技术的世界一直都很精彩广阔(有时我也想多出去走走,不想局限于技术世界); +- The technical foundation is the programmer's internal strength, while the specific technology is the moves. If you only have moves but not deep internal skills, you will be easily vulnerable to the competition from experts (competition from outstanding practitioners and difficult diseases); +- With a solid professional and technical foundation, the upper limit of what can be achieved is higher, and it is more likely to be capable of solving complex technical problems in the future, or to be able to come up with better solutions on the same problems; +- People like to cooperate with people who are similar to themselves, and good people tend to get better results by cooperating with good people. If most people in a team have a good technical foundation, and a person with a weak technical foundation comes in, the cost of collaboration will become higher. If you want to work with good people to get better results, then you must at least be able to match the technical foundation with good people; +- Expanding other talents on the basis of CRUD is also a good choice, but this will not be a true programmer's posture. At most, it will be talents with technical foundations such as product managers, project managers, HR, operations, full bookings and other positions. This is a matter of career choice, which goes beyond the scope of examining programmers. + +**Don’t worry if you can’t answer a question** + +If the interviewer asks you a lot of questions and doesn't answer some of them, don't worry. The interviewer is probably just testing your technical depth and breadth, and then judging whether you have reached a certain water mark. + +The point is: you answered some questions very deeply, which also reflects your in-depth thinking ability. + +I only realized this when I became a technical interviewer. Of course, not every technical interviewer thinks this way, but I think this should be a more appropriate way. + +## References + +- [9 major misunderstandings of technical interviewers](https://zhuanlan.zhihu.com/p/51404304) +- [How to be a good interviewer? ](https://www.zhihu.com/question/26240321) + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/interview/my-personal-experience-in-2021.en.md b/docs_en/high-quality-technical-articles/interview/my-personal-experience-in-2021.en.md new file mode 100644 index 00000000000..35dbb51eb6a --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/my-personal-experience-in-2021.en.md @@ -0,0 +1,201 @@ +--- +title: 校招进入飞书的个人经验 +category: 技术文章精选集 +author: 月色真美 +tag: + - 面试 +--- + +> **推荐语**:这篇文章的作者校招最终去了飞书做开发。在这篇文章中,他分享了自己的校招经历以及个人经验。 +> +> **原文地址**: + +## 基本情况 + +我是 C++主要是后台开发的方向。 + +2021 春招入职字节飞书客户端,入职字节之前拿到了百度 offer(音视频直播部分) 以及腾讯 PCG (微视、后台开发)的 HR 面试通过(还没有收到录用意向书)。 + +## 不顺利的春招过程 + +### 春招实习对我来说不太顺利 + +实验室在 1 月份元旦的那天正式可以放假回家,但回家仍然继续“远程工作”,工作并没有减少,每天日复一日的测试,调试我们开发的“流媒体会议系统”。 + +在 1 月的倒数第三天,我们开了“年终总结”线上会议。至此,作为研二基本上与实验室的工作开始告别。也正式开始了春招复习的阶段。 + +2 月前已经间歇性的开始准备,无非就是在 LeetCode 上面刷刷题目,一天刷不了几道,后面甚至象征性的刷一下每日一题。对我的算法刷题帮助很少。 + +2 月份开始,2 月初的时候,LeetCode 才刷了大概 40 多道题目,挤出了几周时间更新了 handsome 主题的 8.x 版本,这又是一个繁忙的几周。直到春节的当天正式发布,春节过后又开始陆陆续续用一些时间修复 bug,发布修复版本。2 月份这样悄悄溜走。 + +### 找实习的过程 + +**2021-3 月初** + +3 月 初的时候,投了阿里提前批,没想到阿里 3 月 4 号提前批就结束了,那一天约的一面的电话面也被取消了。紧接了开学实验室开会同步进度的时候,发现大家都一面/二面/三面的进度,而我还没有投递的进度。 + +**2021-3-8** + +投递了字节飞书 + +**2021-4 月初** + +字节第一次一面,腾讯第一次一面 + +**2021-4 中旬** + +美团一、二面,腾讯第二次一面和二面,百度三轮面试,通过了。 + +**2021-4 底** + +腾讯第三次一面和字节第二次一面 + +**2021-5 月初** + +腾讯第三次二面和字节第二次二面,后面这两个都通过了 + +#### 阿里 + +第一次投了钉钉,没想到因为行测做的不好,在简历筛选给拒绝了。 + +第二次阿里妈妈的后端面试,一面电话面试,我感觉面的还可以,最后题目也做出来了。最后反问阶段问对我的面试有什么建议,面试官说投阿里最好还是 Java 的…… 然后电话结束后就给我拒了…… + +当时真的心态有点崩,问了这个晚上 7 点半的面试,一直看书晚上都没吃…… + +所以春招和阿里就无缘了。 + +#### 美团 + +美团一面的面试官真的人很好。也很轻松,因为他们是 Java 岗位,也没问 c++知识,聊了一些基础知识,后面半个小时就是聊非技术问题,比如最喜欢网络上的某位程序员是谁,如何写出优雅的代码,推荐的技术类的书籍之类的。当时回答王垠是比较喜欢的程序员,面试官笑了说他也很喜欢。面试的氛围感觉很好。 + +二面的时候全程就问简历上的一个项目,问了大概 90 分钟,感觉他从一开始就有点不太想要我的感觉,很大原因我觉的是我是 c++,转 Java 可能成本还是有一些的。最后问 HR 说结果待定,几天后通知被拒了。 + +#### 百度 + +百度一共三轮面试,在一个下午一起进行,真的很刺激。一面就是很基础的一些 c++问题,写了一个题目说一下思路没让运行(真的要运行还不一定能运行起来:)) + +二面也是基础,第一个题目合并两个有序数组,第二个题目写归并排序,写的结果不对,又给我换了一个题目,树的 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 都是实习转正的。 +- **控制好实习的时间**,因为边实习边准备秋招挺累的,一般实习的时候工作压力也挺大,没什么时间刷题。 + +### 面试准备 + +#### 项目经历 + +我觉得我们实验室项目是没问题的,重要是要讲好。 + +- **项目介绍** + +首先可能让你介绍一下这个项目是什么东西,以及**为什么要去做这个项目**。 + +- **项目的结果** + +然后可能会问这个项目的一些数据上最终结果,比如会议系统能够同时多少人使用,或者量化的体验,比如流畅度,或者是一些其他的一些优势。 + +- **项目中的困难** + +最后都会问过程中有没有遇到什么困难、挑战的,以及怎么解决的。这个过程中主要考察这个项目的技术点是什么。 + +> What does difficulty mean? Personally, I think it’s mainly the problems that took several days to solve that are difficulties. + +Give two examples: + +**The first example is troubleshooting bugs**. For example, there is a memory leak problem that took a week to troubleshoot. It is considered a difficulty. Then the process of solving this difficulty is **how to locate the problem**. For example, if we first search for relevant information based on the error, it will definitely not be easy to find the cause directly. Instead, we will find some **keywords** in these information, such as some tools. Then our use of this tool is a process of solving the problem. + +**The second example is the design of a requirement solution**. For example, if a certain requirement is completed, we may have multiple feasible design solutions to achieve this requirement. The process of solving this difficulty is **our thinking about the reasons why we finally chose this method and the advantages and disadvantages of other design solutions**. + +[Asked during the interview: What is the most difficult problem you have encountered at work? *Discover problems, solve problems.-CSDN Blog*During the interview, I was asked how to solve difficulties at work](https://blog.csdn.net/u012423865/article/details/79452713) + +Some people say that my solution is to search through Baidu, but in fact, the details are to search for a certain error or problem first, but it is certainly not possible to search for the code answer at once, but to find a certain keyword in an answer, and then we continue to search for the keyword to obtain other information. + +#### Written test + +I don’t think the written test for finding an internship is too difficult. Generally, if it has 4 questions, if you answer 1-2 questions, you will almost have a chance to be interviewed. + +To solve the common question, LeetCode Top100. It was very painful to answer the questions at the beginning, but after 40 questions, you started to feel a little better. It is recommended to start with linked lists and binary trees. There are many techniques for array type questions that cannot be used universally. + +- ::Be sure to use the whiteboard for training::, be sure to use the whiteboard, not only to remember the API for the interview, but more importantly, after you become proficient in using the whiteboard, you will be more proficient in writing code and your ideas will be more independent and less dependent. +- Algorithm questions are the most important. The end point is not difficult questions, but simple, medium, common, and high-frequency questions. Practice makes perfect, and you must be familiar with them. +- During the written test of the interview, if a problem occurs, you must apply to use a local IDE for debugging as soon as possible. Otherwise, it may take a long time to find the problem and the opportunity is wasted. + +#### Interview + +The interview usually lasts for one hour and is divided into two parts. The first half will ask about some basic knowledge or project experience, and the second half will consist of questions. + +**Basic knowledge review does not need to be reviewed systematically at the beginning. The first thing is to ensure that high-frequency questions must be answered**, such as the must-ask questions about computer networks and operating systems. You can find common questions by reading more interviews. Even if you fail to answer the more partial questions, it will not have a decisive impact. + +- **Read more interviews!!!!!!** Don't keep immersing yourself in learning, but look at what other people have asked frequently asked questions. +- For internship work, the **knowledge points and common questions must be complete!!!!!!**. If it is not so precise, it is not a big problem, it must be complete, it must be complete! ! ! ! +- **Speak as much as possible about what you don’t know! ! ! ! ** If it doesn’t work, just go somewhere else! ! ! In short, it guides the interviewer to talk about what he knows. +- The style of the written test in the interview is different from the previous written test. The questions in the written test in the interview are not too difficult, but the test requires calm thinking, elegant code, and no bugs. Think clearly first! ! ! Writing! ! ! +- When describing the difficulty of the project, do not talk about the difficulty of document research. Answering this part of the question should be more about the technical difficulty. What technology was used to solve the problem in the end. This part of the technology can allow the interviewer to ask more questions to know his technical ability. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/interview/screen-candidates-for-packaging.en.md b/docs_en/high-quality-technical-articles/interview/screen-candidates-for-packaging.en.md new file mode 100644 index 00000000000..87ea763aa4c --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/screen-candidates-for-packaging.en.md @@ -0,0 +1,119 @@ +--- +title: 如何甄别应聘者的包装程度 +category: 技术文章精选集 +author: Coody +tag: + - 面试 +--- + +> **推荐语**:经常听到培训班待过的朋友给我说他们的老师是怎么教他们“包装”自己的,不光是培训班,我认识的很多朋友也都会在面试之前“包装”一下自己,所以这个现象是普遍存在的。但是面试官也不都是傻子,通过下面这篇文章来看看面试官是如何甄别应聘者的包装程度。 +> +> **原文地址**: + +## 前言 + +上到职场干将下到职场萌新,都会接触到包装简历这个词语。当你简历投到心仪的公司,公司内负责求职的工作人员是如何甄别简历的包装程度的?我根据自己的经验写下了这篇文章,谁都不是天才,包装无可厚非,切勿对号入座! + +## 正文 + +在互联网极速膨胀的社会背景下,各行各业涌入互联网的 IT 民工日益增大。 + +早在 2016 年,我司发布了 Java、Ios 工程师的招聘信息,就 Java 工程师单个岗位而言,日收简历近 200 份,Ios 日收简历近一千份。 + +没错,这就是当年培训机构对 Ios 工程师这个岗位发起的市场讨伐。而随着近几年的发展,市场供大于求现象日益严重。人员摸底成为用人单位对人才考核的重大难题。 + +笔者初次与求职者以面试的形式进行沟通是 2015 年 6 月。由于当时笔者从业时间短,经验不够丰富,错过了一些优秀的求职者。 + +三年后的,今天,笔者再次因公司规模扩大而深入与求职者进行沟通。 + +### 1.初选如何鉴别劣质简历 + +培训机构除了提供技术培训,往往还提供**简历编写指导**、**面试指导**。很多潜移默化的东西,我们很难甄别。但培训机构包装的简历,存在千遍一律的特征。 + +**年龄较小却具备高级文凭** + +年龄较小却具备高级文凭,这个或许不能作为一项标准,但是大部分的应聘者,均符合传统文凭的市场情况。个别技术爱好者可能通过自考获得文凭,这种情况需提供独有的技术亮点。 + +**年龄较大却几乎不具备技术经验** + +年龄较大却几乎不具备技术经验,相对前一点,这个问题就比较严重了。大家都知道,一个正常的人,对新事物的接受能力会随着年龄的增长而降低,互联网技术也包括其内。如果一个人年龄较大不具备技术经验,那么只有两种情况: + +1. 中途转行(通过培训、自学等方式强行入行)。 +2. 由于能力问题,已有的经验不敢写入简历中(能力与经验/薪资不符)。 + +**项目经验多为管理系统** + +项目经验,这一项用来评估应聘者的水平太合适不过了。随着互联网的发展迭代,每一年都会出来很多创新型的互联网公司和新兴行业。笔者最近发布的招聘需求里面。CRM 系统、商城、XX 管理系统、问卷系统、课堂系统占了 90%的份额。试问现在 2019 年,内部管理系统这么火爆么。言归正传,我们对于简历的评估,应当多考虑“确有其事”的项目。比如说该人员当时就职于 XX 公司,该公司当时的背景下确实研发了该项目(外包除外)。 + +**项目的背景不符合互联网发展背景** + +项目背景,每年的市场走向不同,从早些年的电商、彩票风波,到后来的 O2O、夺宝、直播、新零售。每个系列的产品的出现,都符合市场的定义。如果简历中出现 18 年、19 年才刚立项做彩票(15 年政府禁止互联网彩票)、O2O、商城、夺宝(17 年初禁止夺宝类产品)、直播等产品。显然是非常不符合市场需求的。这种情况下需考虑具体情况是否存在理解空间。 + +**缺乏新意** + +不同工作经验下多个项目技术架构或项目结构一致,缺乏新意。一般情况而言,不同的公司技术栈不同,甚至产品的走向和模式完全不同。故此,当一个应聘者多家公司的多个项目中写到的技术千遍一律,业务流程异曲同工。看似整洁,实则更加缺乏说服力。 + +**技术过于新颖,对旧技术却只字不提** + +技术过于新颖,根据互联网技术发展的走向来看,我们在不断向新型技术靠拢。但是任何企业作为资历深厚的 CTO、架构师来说。往往会选择更稳定、更成熟、学习成本更低的已有技术。对新技术的追求不会过于明显。而培训机构则是“哪项技术火我们就教哪项”。故此,出现了很多走入互联网行业的新人对旧技术一窍不通。甚至很多技术都没听过。 + +**工作经验较丰富,但从事的工作较低级。** + +工作经验比较丰富,单从事的工作比较低级,这里存在很大的问题,要么就是原公司没法提供合理的舞台给该人员更好的发展空间,要么就是该人员能力不够,没法完成更高级的工作。当然,还有一种情况就是该人员包装过多的经验导致简历中不和谐。这种情况需要评估公司规模和背景。 + +**公司背景跨省跨市** + +可能很多用人单位和鄙人一样,最近接受到的简历,90%为跨市跳槽的人员。其中武汉占了 60%以上。均为武汉 XX 网络科技有限公司。公司规模均小于 50 人。也有厦门、宁波、南京等等。这个问题笔者就不提了,大家都懂的。跨地区跳槽不好查证。 + +**缺少业余热情于技术的证明** + +有些眼高手低的技术员,做了几个管理系统。用到的技术确是各种分布式、集群、高并发、大数据、消息队列、搜索引擎、镜像容器、多数据库、数据中心等等。期望的薪资也高于行业标准。一个对技术很热情的人,业余时间肯定在技术方面花费过不少时间。那么可以从该人员的博客、git 地址入手。甚至可以通过手机号、邮箱、昵称、马甲。去搜索引擎进行搜集,核实该人员是否在论坛、贴吧、开源组织有过技术背景。 + +### 2. 进入面试阶段,如何甄别对方的水分 + +在甄别对方水分这一块,并没有明确的标准,但是笔者可以提几个点。这也是笔者在实际面试中惯用的做法。 + +**通过公司规模、团队规模、人员分配是否合理、人员合作方式来判断对方是否具备工作经验** + +当招聘初级、初中级 IT 人员的时候,可以询问一些问题,比如公司有多少人、产品团队多少人、产品、技术、后端、前端、客户端、UI、测试各多少人。工作中如何合作的、产品做了多少时间、何时上线的、上线后多长时间迭代一个版本、多长时间迭代一个活动、发展至今多少用户(后端)、多大并发等等(后端)。根据笔者的经验,如果一个人没有任何从业周期,面对这些问题的时候,或多或少答非所问或者给出的答案非常不合理。 + +**背景公司入职时间、项目立项实现、完工时间、产品技术栈、迭代流程的核实** + +很多应聘者对于简历过于包装,只为了追求更高的薪资。当我们问起:你是 xx 年 xx 月入职的该公司?你们项目是 xx 年 xx 月上线的?你们项目使用到 xx 技术?你们每次上线前夕是如何评审的。面对这些问题,应聘者给出的答案经常与简历不符合。这样问题就来了。关于项目使用到的技术,很多项目我们可以通过搜索该项目的地址、APP。通过 HTTP 协议、技术特征、抛出异常特征来大致判别对方使用到的技术。如果应聘者给出的答案明显与之不匹配,嘿嘿。 + +**通过技术深度,甄别对方的技术水平** + +1. 确定对方的技术栈,如:你做过最满意的项目是哪个,为什么?你最喜欢使用的技术是哪些,为什么? + +2. 确定对方项目的发展程度,如:你们产品做了多久,迭代了多久,发布了多少版本,发展到了多少用户,带来多大并发,多少流水? + +3. 确定对方的技术属性,如:平时你会通过什么渠道跟其他技术人形成技术沟通与交流,主要交流过哪些技术? + +笔者最近接待的面试者,很多面试者的简历上,写着层出不穷的各种技术,为了不跨越求职者的技术栈,笔者专门挑应聘者简历写到或用到的技术来进行询问。笔者举几个例子。 + +**1)某求职者简历上写着熟练使用 Redis。** + +1. 介绍一下你使用过 Redis 的哪些数据结构,并描述一下使用的业务场景; +2. 介绍一下你操作 Redis 用到的是什么插件; +3. 介绍一下你们使用的序列化方式; +4. 介绍一下你们使用 Redis 遇到过给你印象较深的问题; + +**2)某求职者声称熟练 HTTP 协议并编写过爬虫。** + +1. 介绍一下你所了解的几个 HTTP head 头并描述其用途; +2. 如果前端提交成功,后端无法接受数据,这时候你将如何排查问题; +3. 描述一下 HTTP 基本报文结构; +4. 如果服务器返回 Cookie,存储在响应内容里面 head 头的字段叫做什么; +5. 当服务端返回 Transfer-Encoding:chunked 代表什么含义 +6. 是否了解分段加载并描述下其技术流程。 + +当然,面向不同的技术,对应的技术深度自然也不一样。 + +大体上的套路便是如此:你说你杀过猪。那么你杀过几头猪,分别是啥时候,杀过多大的猪,有啥毛色。事实上对方可能给你的回答是:杀过、十几头、杀过五十斤的、杀过绿色、黄色、红色、蓝色的猪。那么问题就来了。 + +然而笔者碰到的问题是:使用 Git 两年却不知道 GitHub、使用 Redis 一年却不知道数据结构也不知道序列化、专业做爬虫却不懂 `content-type` 含义、使用搜索引擎技术却说不出两个分词插件、使用数据库读写分离却不知道同步延时等等。 + +写在最后,笔者认为在招聘途中,并不是不允许求职者包装,但是尽可能满足能筹平衡。虽然这篇文章没有完美的结尾,但是笔者提供了面试失败的各种经验。笔者最终招到了如意的小伙伴。也希望所有技术面试官早日找到符合自己产品发展的 IT 伙伴。 + + + diff --git a/docs_en/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.en.md b/docs_en/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.en.md new file mode 100644 index 00000000000..e950d68b233 --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.en.md @@ -0,0 +1,120 @@ +--- +title: 阿里技术面试的一些秘密 +category: 技术文章精选集 +author: 龙叔 +tag: + - 面试 +--- + +> **推荐语**:详细介绍了求职者在面试中应该具备哪些能力才会有更大概率脱颖而出。 +> +> **原文地址:** + +最近我的工作稍微轻松些,就被安排去校招面试了 + +当时还是有些**激动**的,以前都是被面试的,现在我自己也成为一个面试别人的面试官 + +接下来就谈谈我的面试心得(谈谈阿里面试的秘籍) + +## 我是怎么筛选简历的? + +面试之前都是要筛选简历,这个大家应该知道 + +阿里对待招聘非常负责任,面试官必须对每位同学的简历进行查看和筛选,如果不合适还需要写清楚理由 + +对于校招生来说,第一份工作非常重要,而且校招的面试机会也只有一次,一旦收到大家的简历意味着大家非常认可和喜爱阿里这家公司 + +所以我们对每份简历都会认真看,大家可以非常放心,不会无缘无故挂掉大家的简历 + +尽管我们报以非常负责任的态度,但有些同学们的简历实在是难以下看 + +关于如何写简历,我之前写过类似的文章,这里就把之前的文章放这里让大家看看 [一份好的简历应该有哪些内容](https://mp.weixin.qq.com/s?__biz=MzI4MDYzNDc1Mg==&mid=2247484010&idx=1&sn=afbe90c8446f5f21631cae750431d3ee&scene=21#wechat_redirect) + +在筛选简历的时候会有以下信息非常重要,大家一定要认真写 + +- **项目经历**,具体写法可以看上面提到的文章 +- **个人含金量比较高的奖项**,比如 ACM 奖牌、计算机竞赛等 +- **个人技能** 这块会看,但是大多数简历写法都差不多,尽量写得**言简意赅** +- **重要期刊论文发表、开源项目** 加分项 + +这些信息非常重要,我筛选简历的时候这些信息占整份简历的比重 4/5 左右 + +## 面试的时候我会注重哪些方面? + +### **表达要清楚** + +这点是硬伤,在面试的时候有些同学半天说不清楚自己做的项目,我都在替你着急 + +描述项目有个简单的方法论,我自己总结的 大家看看适不适合自己 + +- 最好言简意赅的描述一下你的项目背景,让面试官很快知道项目干了啥(让面试官很快对项目感兴趣) +- 说下项目用了哪些技术,做技术的用了哪些技术得说清楚,面试官会对你的技术比较感兴趣 +- 解决了什么问题,做项目肯定是为了解决问题,总不能为了做项目而做项目吧(解决问题的能力非常重要) +- 遇到哪些难题,如何突破这些难题,项目遇到困难问题很正常,突破困难才是一次好的成长 +- 项目还有哪些完善的地方,不可能设计出完美的执行方案,有待改进说明你对项目认识深刻,思考深入 + +一场面试时间一般 60—80 分钟,好的表达有助于彼此之间了解更多的问题 + +### **基础知识要扎实** + +校招非常注重基础知识,所以这块问的问题比较多,我一般会结合你项目去问,看看同学对技术是停留在用的阶段还是有自己的深入思考 + +每个方向对基础知识要求不同,但有些基础知识是通用的 + +比如**数据结构与算法**、**操作系统**、**计算机网络** 等 + +这些基础技术知识一定要掌握扎实,技术岗位都会或多或少去问这些基础 + +### **动手能力很重要** + +action,action,action ,重要的事情说三遍,做技术的不可能光靠一张嘴,能落地才是最重要的 + +面试官除了问你基础知识和项目还会去考考你的动手能力,面试时间一般不会太长,根据岗位的不同一般会让同学们写一些算法题目 + +阿里面试,不会给你出非常变态的算法题目 + +主要还是考察大家的动手能力、思考问题的能力、数据结构的应用能力 + +在写代码的过程中,我也总结了自己的方法论: + +- 上来不要先写,审题、问清楚题目意图,不要自以为是的去理解思路,工作中 沟通需求、明确需求、提出质疑和建议是非常好的习惯 +- 接下来说思路 思路错了写到一半再去改会非常浪费时间 +- 描述清楚之后,先写代码思路的步骤注释,一边写注释,脑子里迭代一遍自己的思路是否正确,是否是最优解 +- 最后,代码规范 + +## 除了上面这些常规的方面 + +其实,现在面试已经非常**卷**了,上面说的这些很多都是 **八股文** + +有些学生会拿到很多面试题目和答案,反复的去记忆,面试官问问题他就开始在脑子里面检索答案 + +我一般问几个问题就知道该学生是不是在背八股文了。 + +对于背八股文的同学,我真的非常难过。 + +尽管你背的很好,但不能给你过啊,得对得起自己职责,得对公司负责啊! + +背的在好,不如理解一个知识点,理解一个知识点会有助于你去理解很多其他的知识点,很多知识点连起来就是一个知识体系。 + +当面试官问你体系中的任何一个问题,都可以把这个体系讲给他听,不是**背诵** 。 + +深入理解问题,我会比较关注。 + +我在面试过程中,会通过一个问题去问一串问题,慢慢就把整体体系串起来。 + +你的**比赛**和**论文**是你的亮点,这些东西是非常重要的加分项。 + +我也会在面试中穿插一些**开放性题目**,都是思考题 考验一个同学思考问题的方式。 + +## 最后 + +作为一个面试官,我很想对大家说,每个企业都非常渴望人才,都希望找到最适合企业发展的人 + +面试的时候面试官会尽量去挖掘你的价值。 + +但是,面试时间有限,同学们一定要在有限的时间里展现出自己的**能力**和**无限的潜力** 。 + +最后,祝愿优秀的你能找到自己理想的工作! + + + diff --git a/docs_en/high-quality-technical-articles/interview/summary-of-spring-recruitment.en.md b/docs_en/high-quality-technical-articles/interview/summary-of-spring-recruitment.en.md new file mode 100644 index 00000000000..68137b8e0e1 --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/summary-of-spring-recruitment.en.md @@ -0,0 +1,161 @@ +--- +title: 普通人的春招总结(阿里、腾讯offer) +category: 技术文章精选集 +author: 钟期既遇 +tag: + - 面试 +--- + +> **推荐语**:牛客网热帖,写的很全面!暑期实习,投了阿里、腾讯、字节,拿到了阿里和腾讯的 offer。 +> +> **原文地址:** +> +> **下篇**:[十年饮冰,难凉热血——秋招总结](https://www.nowcoder.com/discuss/804679) + +## 背景 + +写这篇文章的时候,腾讯 offer 已经下来了,春招也算结束了,这次找暑期实习没有像去年找日常实习一样海投,只投了 BAT 三家,阿里和腾讯收获了 offer,字节没有给面试机会,可能是笔试太拉垮了。 + +楼主大三,双非本科,我的春招的起始时间应该是 2 月 20 日到 3 月 23 日收到阿里意向书为止,但是从 3 月 7 日蚂蚁技术终面面完之后就没有面过技术面了,只面过两个 HR 面,剩下的时间都在等 offer。最开始是找朋友内推了字节财经的日常实习,但是到现在还在简历评估,后面又投了财经的暑期实习,笔试之后就一直卡在流程里了。腾讯是一开始被天美捞了,一面挂了之后被 PCG 捞了,最后走完了流程。阿里提前批投了好多部门,蚂蚁最先走完了终面,就录入了系统,最后拿了 offer。这一路走过来真的是酸甜苦辣都经历过,因为学历自卑过,以至于想去考研。总而言之,一定要找一个搭档和你一起复习,比如说 @你怕是个憨批哦,这是我实验室的同学,也是我们实验室的队长,这个人是真的强,阿里核心部门都拿遍了,他在我复习的过程中给了我很多帮助。 + +## 写这个帖子的目的 + +1. 写给自己:总结反思一下大学前三年以及找工作的一些经历与感悟。 +2. 写给还在找实习的朋友:希望自己的经历以及面经]能给你们一些启发和帮助。 +3. 写给和我一样有着大厂梦的学弟学妹们:你们还有很长的准备时间,无论你之前在干什么,没有目标也好,碌碌无为也好,没找对方向也好,只要从现在开始,找对学习的方向,并且坚持不懈的学上一年两年,一定可以实现你的梦想的。 + +## 我的大学经历 + +先简单聊聊一下自己大学的经历。 + +本人无论文、无比赛、无 ACM,要啥奖没啥奖,绩点还行,不是很拉垮,也不亮眼。保研肯定保不了,考研估计也考不上。 + +大一时候加入了工作室,上学期自学了 C 语言和数据结构,从寒假开始学 Java,当时还不知道 Java 那么卷,我得到的消息是 Java 好找工作,这里就不由得感叹信息差的重要性了,我当时只知道前端、后端和安卓开发,而我确实对后端开发感兴趣,但是因为信息差,我只知道 Java 可以做后端开发,并不知道后端开发其实是一个很局限的概念,后面才慢慢了解到后台开发、服务端开发这些名词,也不知道 C++、Golang 等语言也可以做后台开发,所以就学了 Java。但其实 Java 更适合做业务,C++ 更适合做底层开发、服务端开发,我虽然对业务不反感,但是对 OS、Network 这些更感兴趣一些,当然这些会作为我的一些兴趣,业余时间会自己去研究下。 + +### 学习路线 + +大概学习的路线就是: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 的内卷只是这个原因造成的吗?当然不是,我认为还有一个原因就是培训机构的兴起,让这个行业的门槛进一步降低,你要学什么东西,怎么学,都有人给你安排好了,这是造成内卷的第二个原因。第三个原因就是非科班转码,其它行业的凋落和互联网行业的繁荣形成了鲜明对比,导致很多其它专业的人也自学计算机,找互联网的工作,导致这个行业的人越来越多,蛋糕就那么大,分蛋糕的人却越来越多。 + +其实内卷也不一定是个坏现象,这说明阶级上升的通道还没有完全关闭,还是有不少人愿意通过努力来改变现状,这也一定程度上会加快行业的发展,社会的发展。选择权在你自己手上,你可以选择回老家躺平或者进互联网公司内卷,如果选择后者的话,我的建议还是尽早占下坑位,因为唯一不变的是变化,你永远不知道三年后是什么样子。 + +## 祝福 + +惟愿诸君,前程似锦! + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/interview/technical-preliminary-preparation.en.md b/docs_en/high-quality-technical-articles/interview/technical-preliminary-preparation.en.md new file mode 100644 index 00000000000..19a9eff354e --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/technical-preliminary-preparation.en.md @@ -0,0 +1,216 @@ +--- +title: 从面试官和候选者的角度谈如何准备技术初试 +category: 技术文章精选集 +author: 琴水玉 +tag: + - 面试 +--- + +> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错! +> +> **内容概览:** +> +> - 通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。 +> - 实战与理论结合。比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? +> - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 +> - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 +> +> **原文地址:** + +## 考察目标和思路 + +首先明确,技术初试的考察目标: + +- 候选人的技术基础; +- 候选人解决问题的思路和能力。 + +技术基础是基石(冰山之下的东西),占七分, 解决问题的思路和能力是落地(冰山之上露出的部分),占三分。 业务和技术基础考察,三七开。 + +## 技术基础考察 + +### 为什么要考察技术基础? + +程序员最重要的两种技术思维能力,是逻辑思维能力和抽象设计能力。逻辑思维能力是基础,抽象设计能力是高阶。 考察技术基础,正好可以同时考察这两种思维能力。能不能理解基础技术概念及关联,是考察逻辑思维能力;能不能把业务问题抽象成技术问题并合理的组织映射,是考察抽象设计能力。 + +绝大部分业务问题,都可以抽象成技术问题。在某种意义上,业务问题只是技术问题的领域化表述。 + +因此,**通过技术基础考察候选者,才能考察到候选者的真实技术实力:技术深度和广度。** + +### 技术基础怎么考察? + +技术基础怎么考察?通过有效的多角度的发问模式来考察。 + +#### 是什么-为什么 + +是什么考察对概念的基本理解,为什么考察对概念的实现原理。 + +比如:索引是什么? 索引是如何实现的? + +#### 引导-横向发问-深入发问 + +引导性,比如 “你对 Java 同步工具熟悉吗?” 作个试探,得到肯定答复后,可以进一步问:“你熟悉哪些同步工具类?” 了解候选者的广度; + +获取候选者的回答后,可以进一步问:“ 谈谈 `ConcurrentHashMap` 或 `AQS` 的实现原理?” + +一个人在多大程度上把技术原理能讲得清晰,包括思路和细节,说明他对技术的掌握能力有多强。 + +#### 跳跃式/交叉式发问 + +比如:讲到哈希高效查找,可以谈谈哈希一致性算法 。 两者既有关联又有很多不同点。也是一种技术广度的考察方法。 + +#### 总结性发问 + +比如:你在做 XXX 中,获得了哪些可以分享的经验? 考察候选人的归纳总结能力。 + +#### 实战与理论结合 + +比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? + +比如,候选人有谈到 SQL 优化和索引优化,那就正好谈谈索引的实现原理,如何建立最佳索引? + +再比如,候选人有谈到事务,那就正好谈谈事务实现原理,隔离级别,快照实现等; + +#### 熟悉与不熟悉结合 + +针对候选人简历上写的熟悉的部分,和没有写出的都问下。比如候选人简历上写着:熟悉 JVM 内存模型, 那我就考察下内存管理相关(熟悉部分),再考察下 Java 并发工具类(不确定是否熟悉部分)。 + +#### 死知识与活知识结合 + +比如,查找算法有哪些?顺序查找、二分查找、哈希查找。这些大家通常能说出来,也是“死知识”。 + +这些查找算法各适用于什么场景?在你工作中,有哪些场景用到了哪些查找算法?为什么? 这些是“活知识”。 + +#### 学习或工作中遇到的 + +有时,在学习和工作中遇到的问题,也可以作为面试题。 + +比如,最近在学习《操作系统导论》并发部分,有一章节是如何使数据结构成为线程安全的。这里就有一些可以提问的地方:如何实现一个锁?如何实现一个线程安全的计数器?如何实现一个线程安全的链表?如何实现一个线程安全的 `Map` ?如何提升并发的性能? + +工作中遇到的问题,也可以抽象提炼出来,作为技术基础面试题。 + +#### 技术栈适配度发问 + +如果候选人(简历上所写的)使用的某些技术与本公司的技术栈比较契合,则可以针对这些技术点进行深入提问,考察候选人在这些技术点的掌握程度。如果掌握程度比较好,则技术适配度相对更高一些。 + +当然,这一点并不能作为筛掉那些没有使用该技术栈的候选人的依据。比如本公司使用 `MongoDB` 和 `MySQL`, 而一个候选人没有用过 `Mongodb,` 但使用过 `MySQL`, `Redis`, `ES`, `HBase` 等多种存储系统,那么适配度并不比仅使用过 `MySQL` 和 `MongoDB` 的候选人逊色,因为他所涉及的技术广度更大,可以推断出他有足够能力掌握 `Mongodb`。 + +#### 创造有个性的面试题库 + +每个技术面试官都会有一个面试题库。持续积累面试题库,日常中突然想到的问题,就随手记录下来。 + +## 业务维度考察 + +### 为什么要考察业务维度? + +技术基础考察,容易错过的地方是,候选人的非技术能力特质,比如沟通组织能力、带项目能力、抗压能力、解决实际问题的能力、团队影响力、其它性格特质等。 + +### 为什么不能单考察业务维度? + +因为业务方面通常比较熟悉,可能就直接按照现有方案说出来了,很难考察到候选人的深入理解、横向拓展和归纳总结能力。 + +这一点,建议有针对性地考察下候选人的归纳总结能力:比如, 微服务搭建或开发或维护/保证系统稳定性或性能方面的过程中,你收获了哪些可以分享的经验? + +## 解决问题能力考察 + +仅仅只是技术基础还不够,通常最好结合实际业务,针对他项目里的业务,抽象出技术问题进行考察。 + +解决思路重在层层递进。这一点对于面试官的要求也比较高,兼具良好的倾听能力、技术深度和业务经验。首先要仔细倾听候选人的阐述,找到适当的技术切入点,然后进行发问。如果进不去,那就容易考察失败。 + +### 设计问题 + +- 比如多个机器间共享大量业务对象,这些业务对象之间有些联合字段是重复的,如何去重? +- 如果瞬时有大量请求涌入,如何保证服务器的稳定性? + +### 项目经历 + +项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。 + +一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 + +## 面试官如何做好一场面试? + +### 预先准备 + +面试官也需要做一些准备。比如熟悉候选者的技能优势、工作经历等,做一个面试设计。 + +在面试将要开始时,做好面试准备。此外,面试官也需要对公司的一些基本情况有所了解,尤其是公司所使用技术栈、业务全景及方向、工作内容、晋升制度等,这一点技术型候选人问得比较多。 + +### 面试启动 + +一般以候选人自我介绍启动,不过候选人往往会谈得比较散,因此,我会直接提问:谈谈你有哪些优势以及自己觉得可以改进的地方? + +然后以一个相对简单的基础题作为技术提问的开始:你熟悉哪些查找算法?大多数人是能答上顺序查找、二分查找、哈希查找的。 + +### 问题设计 + +提前阅读候选人简历,从简历中筛选出关键词,根据这些关键词进行有针对性地问题设计。 + +比如候选人简历里提到 `MVVM` ,可以问 `MVVM` 与 `MVC` 的区别; 提到了观察者模式,可以谈谈观察者模式,顺便问问他还熟悉哪些设计模式。 + +### 宽松氛围 + +即使问的问题比较多比较难,也要注意保持宽松氛围。 + +在面试前,根据候选人基本信息适当调侃一下,比如一位候选人叫汪奎,那我就说:之前我们团队有位叫袁奎,我们都喊他奎爷。 + +在面试过程中,适当提示,或者给出少量自己的看法,也能缓解候选人的紧张情绪。 + +### 学会倾听 + +多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 + +引导候选人表现他最优势的一面,让他或她感觉好一些:毕竟一场面试双方都付出了时间和精力,不应该是面试官 Diss 候选人的场合,而应该让彼此有更好的交流。很大可能,你也能从候选人那里学到不少东西。 + +面试这件事,只不过双方的角色和立场有所不同,但并不代表面试官的水平就一定高于候选人。 + +### 记录重点 + +认真客观地记录候选人的回答,尽可能避免任何主观评价,亦不作任何加工(比如自己给总结一下,总结能力也是候选人的一个特质)。 + +## 作出判断 + +面试过程是一种铺垫,关键的是作出判断。 + +作出判断最容易陷入误区的是:贪深求全。总希望候选人技术又深入又全面。实际上,这是一种奢望。如果候选人的技术能力又深入又全面,很可能也会面临两种情况: + +1. 候选人有更好的选择; +2. The candidate may have deficiencies in other aspects, such as teamwork. + +A more appropriate scale is: + +1. Whether his or her technical level is qualified for the current job; +2. How his or her technical level compares with that of fellow team members; +3. Whether his or her skill level matches his or her years of experience and whether he or she has the potential to perform more complex tasks. + +**Different ages value different things. ** + +For engineers with less than three years of experience, more attention should be paid to their technical foundation, because this represents their future potential; at the same time, their performance in actual development, such as teamwork, business experience, stress resistance, enthusiasm and ability for active learning, etc., should also be examined. + +For engineers with more than three years of experience, more attention should be paid to their business experience and problem-solving abilities, to see how he or she analyzes specific problems, and to examine the depth and breadth of his or her technical foundation within the business scope. + +I am also learning how to judge a candidate's true technical level and whether he or she fits the needs. + +## Message to Candidates + +### Focus on technical foundations + +A common doubt is: most of the time when developing business systems, the design and implementation of data structures and algorithms are basically not involved. Why should we examine the implementation principle of `HashMap`? Why should we learn basic courses such as data structures and algorithms, operating systems, and network communications? + +Now I can give an answer: + +- As mentioned above, the vast majority of business problems will actually eventually be mapped to basic technical issues: the implementation of data structures and algorithms, memory management, concurrency control, network communication, etc.; these are the cornerstones of understanding large-scale programs on the modern Internet and solving difficult program problems. Unless you can bless yourself to never encounter difficult problems, you will always be satisfied with writing CRUD; +- These technical foundations are the most interesting and exciting part of the programming world. If you are not interested in these, it will be difficult to go deep into this field. It is better to switch to other professions as soon as possible. The non-technical world has always been exciting and vast (sometimes I also want to go out more and do not want to be limited to the technical world); +- The technical foundation is the programmer's internal strength, while the specific technology is the moves. If you only have moves but not deep internal skills, you will be easily vulnerable to the competition from experts (competition from outstanding practitioners and difficult diseases); +- With a solid professional and technical foundation, the upper limit of what can be achieved is higher, and it is more likely to be capable of solving complex technical problems in the future, or to be able to come up with better solutions on the same problems; +- People like to cooperate with people who are similar to themselves, and good people tend to get better results by cooperating with good people. If most people in a team have a good technical foundation, and a person with a weak technical foundation comes in, the cost of collaboration will become higher. If you want to work with good people to get better results, then you must at least be able to match the technical foundation with good people; +- Expanding other talents on the basis of CRUD is also a good choice, but this will not be a true programmer's posture. At most, it will be talents with technical foundations such as product managers, project managers, HR, operations, full bookings and other positions. This is a matter of career choice, which goes beyond the scope of examining programmers. + +### Don’t worry if you can’t answer a question + +If the interviewer asks you a lot of questions and doesn't answer some of them, don't worry. The interviewer is probably just testing your technical depth and breadth, and then judging whether you have reached a certain water mark. + +The point is: you answered some questions very deeply, which also reflects your in-depth thinking ability. + +I only realized this when I became a technical interviewer. Of course, not every technical interviewer thinks this way, but I think this should be a more appropriate way. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.en.md b/docs_en/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.en.md new file mode 100644 index 00000000000..c1b9cfdb7fb --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.en.md @@ -0,0 +1,362 @@ +--- +title: 一位大龄程序员所经历的面试的历炼和思考 +category: 技术文章精选集 +author: 琴水玉 +tag: + - 面试 +--- + +> **推荐语**:本文的作者,今年 36 岁,已有 8 年 JAVA 开发经验。在阿里云三年半,有赞四年半,已是标准的大龄程序员了。在这篇文章中,作者给出了一些关于面试和个人能力提升的一些小建议,非常实用! +> +> **内容概览**: +> +> 1. 个人介绍,是对自己的一个更为清晰、深入和全面的认识契机。 +> 2. 简历是充分展示自己的浓缩精华,也是重新审视自己和过往经历的契机。不仅仅是简要介绍技能和经验,更要最大程度凸显自己的优势领域(差异化)。 +> 3. 我个人是不赞成海投的,而倾向于定向投。找准方向投,虽然目标更少,但更有效率。 +> 4. 技术探索,一定要先理解原理。原理不懂,就会浮于表层,不能真正掌握它。技术原理探究要掌握到什么程度?数据结构与算法设计、考量因素、技术机制、优化思路。要在脑中回放,直到一切细节而清晰可见。如果能够清晰有条理地表述出来,就更好了。技术原理探究,一定要看源码。看了源码与没看源码是有区别的。没看源码,虽然说得出来,但终是隔了一层纸;看了源码,才捅破了那层纸,有了自己的理解,也就能说得更加有底气了。当然,也可能是我缺乏演戏的本领。 +> 5. 要善于从失败中学习。正是在杭州四个月空档期的持续学习、思考、积累和提炼,以及面试失败的反思、不断调整对策、完善准备、改善原有的短板,采取更为合理的方式,才在回武汉的短短两个周内拿到比较满意的 offer 。 +> 6. 面试是通过沟通来理解双方的过程。面试中的问题,千变万化,但有一些问题是需要提前准备好的。 +> +> **原文地址**: + +从每一段经历中学习,在每一件事情中修行。善于从挫折中学习。 + +## 引子 + +我今年 36 岁,已有 8 年 JAVA 开发经验。在阿里云三年半,有赞四年半,已是标准的大龄程序员了。 + +在多年的读书、学习和思考中,我的价值观、人生观和世界观也逐步塑造成型。我意识到自己的志趣在于做教育文化方面,因此在半冲动之下,8 月份下旬,裸辞去找工作了。有限理性难以阻挡冲动的个性。不建议裸辞,做事应该有规划、科学合理。 + +尽管我最初认为自己“有理想有目标有意愿有能力”,找一份教育开发的工作应该不难,但事实上我还是过于乐观了。现实很快给我泼了一瓢瓢冷水。我屡战屡败,又屡败屡战。惊讶地发现自己还有这个韧性。面试是一项历炼,如果没有被失败击倒,那么从中会生长出一份韧性,这种韧性能让人走得更远。谁没有经历过失败的历练呢?失败是最伟大的导师了,如果你愿意跟他学一学的话。 + +在面试的过程中,我很快发现自己的劣势: + +- 投入精力做业务,技术深度不够,对原理的理解局限于较浅的层次; +- 视野不够开阔,局限于自己所做的订单业务线,对其它关联业务线(比如商品、营销、支付等)了解不够; +- 思维不够开阔,大部分时间投入在开发和测试上,对运维、产品、业务、商业层面思考都思考不多; +- 缺乏管理经验,年龄偏大;这两项劣势我一度低估,但逐渐凸显出来,甚至让我一度不自信,但最终我还是走出来了。 + +但我也有自己的优势。职业竞争的基本法则是稀缺性和差异化。能够解决大型项目的架构设计和攻克技术难题,精通某个高端技术领域是稀缺性体现;而能够做事能做到缜密周全精细化,有高并发大流量系统开发经验,则是差异性体现。稀缺性是上策,差异化是中策,而降格以求就是下策了。 + +我缺乏稀缺性优势,但还有一点差异化优势: + +- 对每一份工作都很踏实,时间均在 3 年 - 5 年之间,有一点大厂光环,能获得更多面试机会(虽然不一定能面上); +- 坚持写博客,孜孜不倦地追求软件开发的“道”,时常思考记录开发中遇到的问题及解决方案; +- 做事认真严谨,能够从整体分析和思考问题,也很注重基础提升; +- 对工程质量、性能优化、稳定性建设、业务配置化设计有实践经验; +- 大流量微服务系统的长期开发维护经验。 + +我投出简历的公司并不多。在不多的面试中,我逐渐意识到网上的“斩获几十家大厂 offer”的说法并不可信。理由如下: + +- 如果能真斩获大量大厂 offer ,面试的级别很大概率是初级工程师。要知道面试 4 年以上的工程师,面试的深度和广度令人发指,从基础的算法、到各种中间件的原理机制到实际运维架构,无所不包,真个是沉浸在“技术的海洋”,除非一个人的背景和实力非常强大,平时也做了非常深且广的沉淀; +- 一个背景和实力非常强大的人,是不会有兴趣去投入这么多精力去面各种公司,仅仅是为了吹嘘自己有多能耐;实力越强的人,他会有自己的选择逻辑,投的简历会更定向精准。话说,他为什么不花更多精力投入在那些能够让他有最大化收益的优秀企业呢? +- 培训机构做的广告。因为他们最清楚新手需要的是信心,哪怕是伪装出来的信心。 + +好了,闲话不多说了。我讲讲自己在面试中所经受的历练和思考吧。 + +## 准备工作 + +人生或许很长,但面试的时间很短,最长不过一小时或一个半小时。别人如何在短短一小时内能够更清晰地认识长达三十多年的你呢?这就需要你做大量细致的准备工作了。在某种程度上,面试与舞蹈有异曲同工之妙:台上五分钟,台下十年功。 + +准备工作主要包括简历准备、个人介绍、公司了解、技术探索、表述能力、常见问题、中高端职位、好的心态。准备工作是对自身和对外部世界的一次全面深入的重新认知。 + +初期,我以为自己准备很充分,简历改改就完事了。随着一次次受挫,才发现自己的准备很不充分。在现在的我看来,准备七分,应变三分。准备,就是要知己知彼,知道对方会问哪些问题(通常是系统/项目/技术的深度和广度)、自己应当如何作答;应变,就是当自己遇到不会、不懂、不知道的问题时,如何合理地展示自己的解决思路,以及根据面试中答不上来的问题查漏补缺,夯实基础。 + +这个过程,实际上也是学习的过程。持续的反思和提炼、学习新的内容、重新认识自己和过往经历等。 + +### 简历准备 + +最开始,我做得比较简单。把以前的简历拿出来,添加上新的工作经历,略作修改,但整体上模板基本不变。 + +在基本面上,我做的是较为细致的,诚实地写上了自己擅长和熟悉的技能和经验经历,排版也尽力做得整洁美观(学过一些 UI 设计)。不浮夸也不故作谦虚。 + +在扩展面上,我做的还是不够的。有一天,一位猎头打电话给我,问:“你最大的优势是什么?”。我顿时说不上来。当时也未多加思考。在后续面试屡遭失败之后,一度有些不自信之后,我开始仔细思考自己的优势来。然后将“对工程质量、性能优化、稳定性建设、业务配置化设计有深入思考和实践经验”写在了“技能素养”栏的第一行,因为这确实是我所做过的、最实在且脚踏实地的且具备概括性的。 + +有时,简历内容的编排顺序也很重要。之前,我把掌握的语言及技术写在前面,而“项目管理能力和团队影响力”之类的写在后面。但投年糕妈妈之后,未有面试直接被拉到不合适里面,受到了刺激,我意识到或许是对方觉得我管理经验不足。因此,刻意将“项目管理能力和团队影响力”提到了前面,表示自己是重视管理方面的,不过,投过新的简历之后,没有回应。我意识到,这样的编排顺序可能会让人误解我是管理能力偏重的(事实上有一位 HR 问我是不是还在写代码),但实际上管理方面我是欠缺的,最后,我还是调回了原来的顺序,凸出自己“工程师的本色”。后面,我又做了一些语句的编排上的修改。 + +随着面试的进展,有时,也会发现自己的简历上写得不够或者以前做得不够的地方。比如,在订单导出这段经历里,我只是写了大幅提升性能和稳定性,显得定性描述化,因此,我添加了一些量化的东西(2w 阻塞 => 300w+,1w/1min)作为证实;比如,8 月份离职,到 12 月份面试的时候,有一段空档期,有些企业会问到这个。因此,我索性加了一句话,说明这段时间我在干些啥;比如,代表性系统和项目,每一个系统和项目的价值和意义(不一定写在上面,但是心里要有数)。功夫要下足。 + +再比如,我很详细地写了有赞的工作经历及经验,但阿里云的那段基本没动。而有些企业对这段经历更感兴趣,我却觉得没太多可说的,留在脑海里的只有少量印象深刻的东西,以及一些博客文章的记录,相比这段工作经历来说显得太单薄。这里实质上不是简历的问题,而是过往经历复盘的问题。建议,在每个项目结束后,都要写个自我复盘。避免时间将这些可贵的经历冲淡。 + +每个人其实都有很多可说的东西,但记录下来的又有多少呢?值得谈道的有多少呢?过往不努力,面试徒伤悲。 + +**简历更新的心得**: + +- 简历是充分展示自己的浓缩精华,也是重新审视自己和过往经历的契机; +- 不仅仅是简要介绍技能和经验,更要最大程度凸显自己的优势领域(差异化); +- 增强工作经历的表述,凸显贡献,赢得别人的认可; +- 复盘并记录每一个项目中的收获,为跳槽和面试打下好的铺垫。 + +### 个人介绍 + +面试前通常会要求做个简要的个人介绍。个人介绍通常作为进入面试的前奏曲和缓冲阶段,缓和下紧张气氛。 + +我最开始的个人介绍,个性啊业余生活啊工作经历啊志趣啊等等,似乎不知道该说些什么。实际上,个人介绍是一个充分展示自己的主页。主页应当让自己最最核心的优势一目了然(需要挖掘自己的经历并仔细提炼)。我现在的个人介绍一般会包括:个性(比如偏安静)、做事风格(工作认真严谨、注重质量、善于整体思考)、最大优势(owner 意识、执行力、工程把控能力)、工作经历简述(在每个公司的工作负责什么、贡献了什么、收获了什么)。个人介绍简明扼要,无需赘言。 + +个人介绍,是对自己的一个更为清晰、深入和全面的认识契机。 + +### 公司了解 + +很多人可能跟我一样,对公司业务了解甚少,就直接投出去了。这样其实是不合理的。首先,我个人是不赞成海投的,而倾向于定向投。找准方向投,虽然目标更少,但更有效率。这跟租房一样,我一般在豆瓣上租房,虽然目标源少,但逮着一个就是好运。 + +投一家公司,是因为这家公司符合意向,值得争取,而不是因为这是一家公司。就像找对象,不是为了找一个女人。要确定这家公司是否符合意向,就应当多去了解这家公司:主营业务、未来发展及规划、所在行业及地位、财务状况、业界及网络评价等。 + +在面试的过程中适当谈到公司的业务及思考,是可加分项。亦可用于“你有什么想问的?”的提问。 + +### 技术探索 + +技术能力是一个技术人的基本素养。因此,我觉得,无论未来做什么工作,技术能力过硬,总归是最不可或缺的不可忽视的。 + +原理和设计思想是软件技术中最为精髓的东西。一般软件技术可以分为两个方面: + +- 原理:事物如何工作的基本规律和流程; +- 架构:如何组织大规模逻辑的艺术。 + +**技术探索,一定要先理解原理。原理不懂,就会浮于表层,不能真正掌握它。技术原理探究要掌握到什么程度?数据结构与算法设计、考量因素、技术机制、优化思路。要在脑中回放,直到一切细节而清晰可见。如果能够清晰有条理地表述出来,就更好了。** + +**技术原理探究,一定要看源码。看了源码与没看源码是有区别的。没看源码,虽然说得出来,但终是隔了一层纸;看了源码,才捅破了那层纸,有了自己的理解,也就能说得更加有底气了。当然,也可能是我缺乏演戏的本领。** + +我个人不太赞成刷题式面试。虽然刷题确实是进厂的捷径,但也有缺点: + +- 它依然是别人的知识体系,而不是自己总结的知识体系; +- 技术探究是为了未来的工作准备,而不是为了应对一时之需,否则即使进去了还是会处于麻痹状态。 + +经过系统的整理,我逐步形成了适合自己的技术体系结构:[“互联网应用服务端的常用技术思想与机制纲要”](https://www.cnblogs.com/lovesqcc/p/13633409.html) 。在这个基础上,再博采众长,看看面试题进行自测和查漏补缺,是更恰当的方式。我会在这个体系上深耕细作。 + +### 表述能力 + +目前,绝大多数企业的主要面试形式是通过口头沟通进行的,少部分企业可能有笔试或机试。口头沟通的形式是有其局限性的。对表述能力的要求比较高,而对专业能力的凸显并不明显。一个人掌握的专业和经验的深度和广度,很难通过几分钟的表述呈现出来。往往深度和广度越大,反而越难表述。而技术人员往往疏于表达。 + +我平时写得多说得少,说起来不利索。有时没讲清楚背景,就直接展开,兼之啰嗦、跳跃和回旋往复(这种方式可能更适合写小说),让面试官有时摸不着头脑。表述的条理性和清晰性也是很重要的。不妨自己测试一下:Dubbo 的架构设计是怎样的? Redis 的持久化机制是怎样的?然后自己回答试试看。 + +表述能力的基本法则: + +- 先总后分,先整体后局部; +- 先说基本思路,然后说优化; +- 体现互动。先综述,然后向面试官询问要听哪方面,再分述。避免自己一脑瓜子倾倒出来,让面试官猝不及防;系统设计的场景题,多问一些要求,比如时间要求、空间要求、要支持多大数据量或并发量、是否要考虑某些情况等。 + +### 常见问题 + +面试是通过沟通来理解双方的过程。面试中的问题,千变万化,但有一些问题是需要提前准备好的。 + +比如“灵魂 N 问”: + +- 你为什么从 XXX 离职? +- 你的期望薪资是多少? +- 你有一段空档期,能解释下怎么回事么? +- 你的职业规划是怎样的? + +高频技术问题: + +- 基础:数据结构与算法、网络; +- 微服务:技术体系、组件、基础设施等; +- Dubbo:Dubbo 整体架构、扩展机制、服务暴露、引用、调用、优雅停机等; +- MySQL:索引与事务的实现原理、SQL 优化、分库分表; +- Redis : 数据结构、缓存、分布式锁、持久化机制、复制机制; +- 分布式:分布式事务、一致性问题; +- 消息中间件:原理、对比; +- 架构:架构设计方法、架构经验、设计模式; +- 性能优化:JVM、GC、应用层面的性能优化; +- 并发基础:ConcurrentHashMap, AQS, CAS,线程池等; +- 高并发:IO 多路复用;缓存问题及方案; +- 稳定性:稳定性的思想及经验; +- 生产问题:工具及排查方法。 + +### 中高端职位 + +说起来,我这人可能有点不太自信。我是怀着“踏实做一个工程师”的思想投简历的。 + +对于大龄程序员,企业的期望更高。我的每一份“高级工程师”投递,自动被转换为“技术专家”或“架构师”。无力反驳,倍感压力。面试中高端职位,需要更多准备: + +- 你有带团队经历吗? +- 在你 X 年的工作经历中,有多少时间用于架构设计? +- 架构过程是怎样的?你有哪些架构设计思想或方法论? + +如果不作准备,就被一下子问懵,乱了阵脚。实际上,我或许还是存着侥幸心理把“技术专家”和“架构师”岗位当做“高工”来面试的,也就无一不遭遇失败了。显然,我把次序弄反了:应当以“技术专家”和“架构师”的规格来面试高级工程师。 + +好吧,那就迎难而上吧!我不是惧怕挑战的人。 + +此外,“技术专家”和“架构师”职位应当至少留一天的时间来准备。已经有丰富经验的技术专家和架构师可以忽略。 + +### 好的心态 + +保持好的心态也尤为重要。我经历了“乐观-不自信-重拾信心”的心态变化过程。 + +很长一段时间,由于“求成心切”,生怕某个技术问题回答不上来搞砸,因此小心谨慎,略显紧张,结果已经梳理好的往往说不清楚或者说得不够有条理。冲着“拿 offer ”的心态去面试,真的很难受,会觉得每场面试都很被动那么难过,甚至有点想要“降格以求”。 + +有时,我在想:咋就混成这个样子了呢?按理来说,这个时候我应该有能力去追求自己喜爱的事业了啊!还是平时有点松懈了,视野狭窄,积累不够,导致今天的不利处境。 + +我是一个守时的人,也希望对方尽可能守时。杭州的面试官中,基本是守时的,即使迟到也在心理接受范围内,回武汉面试后,节奏就有点被少量企业带偏了。有一两次,我甚至不确定面试官什么时候进入会议。我想,难道这是人才应该受到的“礼待”吗?我有点被轻微冒犯的感觉了。不过我还是“很有涵养地”表示没事。但我始终觉得:面试官迟到,是对人才的不尊重。进入不尊重人才的公司,我是怀有疑虑的。良禽择木而栖,良臣择主而事。难道我能因为此刻的不利处境,而放弃一些基本的原则底线,而屈从于一份不尊重人才的 offer 吗? + +我意识到:一个人应当用其实力去赢得对方的尊重和赏识,以后的合作才会更顺畅。不若,哪怕惜其无缘,亦不可强留。无论别人怎么存疑,心无旁骛地打磨实力,挖掘自己的才干和优势,终会发出自己的光芒。因此,我的心态顿时转变了:应当专注去沟通,与对方充分认识了解,赢得对方心服的认可,而不是拿到一张入门券,成为干活的工具。 + +有一个“石头和玉”的小故事,把自己当做人才,并努力去提升自己,才能获得“人才的礼遇”;把自己当石头贱卖,放松努力,也就只能得到“石头的礼遇”。尽管一个人不一定马上就具备人才的能力,但在自己的内心里,就应当从人才的视角去观察待入职的企业,而不仅仅是为了找一份“赚更多钱”的工作。 + +此外,焦虑也是不必要的。焦虑的实质是现实与目标的差距。一个人总可以评估目标的合理性及如何达成目标。如果目标过高,则适当调整目标级别;目标可行,则作出合理的决策,并通过持续的努力和恰当的出击来实现目标。决策、努力和出击能力都是可以持续修炼的。 + +## 面试历炼 + +技术人的面试还是更偏重于技术,因此,技术的深度和广度还是要好好准备的。面试官和候选人的处境是不一样的,一个面试官问的只是少量点,但是多个面试官合起来就是一个面。明白这一点,作为面试官的你就不要忘乎所以,以为自己就比候选人厉害。 + +我面的企业不多,因为我已经打算从事教育事业,用“志趣和驱动力”这项就直接过滤了很多企业的面试邀请。在杭州面试的基本是教育企业,连阿里华为等抛来的橄榄枝都婉拒了(尽管我也不一定能面上)。虽然做法有点“直男”,但投入最多精力于自己期望从事的行业和事业,才是值得的。 + +我所认为的教育事业,并不局限于现在常谈起的在线教育或 K12 教育,而是一个教育体系,任何可以更好滴起到教育效果的事业,包括而不限于教学、阅读、音乐、设计等。 + +### 接力棒科技-高工 + +面的第一家。畅谈一番后,没音讯了。但我也没有太在意。面试官问的比较偏交易业务性的东西,较深的就是如何保证应用的数据一致性了。 + +此时的我,就像在路上扔了一颗探路的小石子,尚未意识到自己的处境。 + +### 网易云音乐-高工 + +接着是网易云音乐。大厂就是大厂。一面问的尽是缓存、分布式锁、Dubbo、ZK, MQ 中间件相关的机制。很遗憾,由于我平时关于技术原理的沉淀还是很少,基本是“一问两不知”,挂得很出彩。 + +此时,我初步意识到自己的技术底子还很薄弱,也就开始了广阔的技术学习和夯实,自底向上地梳理原理和逻辑,系统地进行整理总结,最终初步形成了自己的互联网服务端技术知识体系结构。 + +### 铭师堂-技术专家 + +架构师面试的。问的相对多了一些,DB, Redis 等。反馈是技术还行,但缺乏管理经验。这是我第一次意识到大龄程序员缺乏管理经验的不利。中小企业的技术专家线招聘中,往往附加了管理经验的需求。应聘时要注意。 + +缺乏管理经验,该怎么办呢?思考过一段时间后,我的想法是: + +- 改变能改变的,不能改变的,学习它。比如技术原理的学习是我能够改变的,但管理经验属于难以一时改变的,那就多了解点管理的基本理论吧。 +- 从经历中挖掘相关经验。虽然我没有正式带团队的实际经验,但是有带项目和带工程师,管控某个业务线的基本管理经验。多多挖掘自己的经历。 + +### 字节教育-高工 + +字节教育面试,我给自己挖了不少坑往里跳。 + +比如面试官问,讲一个你比较成就感的项目经历。我选择的是近 4 年前的周期购项目。虽然这是我入职有赞的第一个有代表性的项目,但时间太久,又没有详细记录,很多技术细节遗忘不清晰了。我讲到当时印象比较深的“一体化”设计思想,却忘记了当时为什么会有这种思想(未做仔细记录)。 + +再比如,一个上课的场景题,我问是用 CS 架构还是 BS 架构?面试官说用 CS 架构吧。这不是给自己挖坑吗?明明自己不熟悉 CS 架构,何必问这个选择呢,不如直接按照 BS 架构来讲解。哎! + +字节教育给我的反馈是:业务 Sense 不错,系统设计能力有待提高。我觉得还是比较中肯的。因此,也开始注重系统设计实战方面的文章阅读和思考训练。 + +经验是: + +- 做项目时,要详细记录每个项目的技术栈、技术决策及原因、技术细节,为面试做好铺垫; +- 提前准备好印象最深刻的最代表性的系统和项目,避免选择距离当前时间较久的缺乏详细记录的项目; +- 选择熟悉的项目和架构,至少有好的第一印象,不然给面试官的印象就是你啥都不会。 + +### 咪咕数媒-架构师 + +好家伙,一下子 3 位面试官群面。可能我以前经历的太少了吧。似乎国企面试较高端职位,喜欢采取这种形式。兼听则明偏听则暗嘛。问的问题也很广泛,从 ES 的基本原理,到机房的数据迁移。有些技术机制虽然学习过,但不牢固,不清晰,答的也不好。比如 ES 的搜索原理优化,讲过倒排索引后,我对 Term Index 和 Trie 树 讲不清楚。这说明,知道并不代表真正理解了。只有能够清晰有条理地把思路和细节都讲清楚,才算是真正理解了。 + +印象深刻的是,有一个问题:你有哪些架构思想?这是第一次被问到架构设计方面的东西,我顿时有点慌乱。虽然平时多有思考,也有写过文章,却没有形成系统精炼的方法论,结果就是答的比较凌乱。 + +### 涂鸦智能-高工 + +应聘涂鸦智能,是因为我觉得这家企业不错。优秀的企业至少应该多沟通一下,说不准以后有合作机会呢!看问题的思维要开阔一些,不能死守在自己想到的那一个事情上。 + +涂鸦智能给我的整体观感还是不错的。面试官也很有礼貌有耐心,整体架构、技术和项目都问了很多,问到了我熟悉的地方,答得也还可以。也许我的经验正好是切中他们的需求吧。 + +若不是当时想做教育的执念特别强,我很大概率会入职涂鸦智能。物联网在我看来应该是很有趣的领域。 + +### 跟谁学-技术专家 + +“跟谁学”基本能答上来。不过反馈是:对于提问抓重点的能力有所欠缺,对于技术的归纳整理也不够。我当时还有点不服气,认为自己写了那么多文章,也算是有不少思考,怎能算是总结不够呢?顶多是有技术盲点。技术犹如海洋,谁能没有盲点? + +不过现在反观,确实距离自己应该有的程度不够。对技术原理机制和生产问题排查的总结不够,不够清晰细致;对设计实践的经验总结也不够,不够系统扎实。这个事情还要持续深入地去做。 + +此外,面得越多,越发现自己的表述能力确实有所欠缺。啰嗦、容易就一点展开说个没完、脱离背景直接说方案、跳跃、回旋往复,然后面试官很可能没耐心了。应该遵循“先总后分”、“基本思路-实现-优化”的一些基本逻辑来作答会更好一些。表述能力真的很重要,不可只顾着敲代码。还有每次面教育企业就不免紧张,生怕错过这个机会。 + +这是第二家直接告诉我年龄与经验不匹配的企业,加深了我对年龄偏大的忧虑,以致于开始有点不自信了。 + +那么我又是怎么重拾信心的呢?有一句老话:“留得青山在,不怕没柴烧”。就算我年龄比较大,如果我的技术能力打磨得足够硬朗,就不信找不到一家能够认可我的企业。大不了我去做开源项目好了。具备好的技术能力,并不一定就局限在企业的范围内去发挥作用,也没必要局限于那些被年龄偏见所蒙蔽的人的认知里。外界的认可固然重要,内在的可贵性却远胜于外在。 + +### 亿童文教-架构师 + +也是采用的 3 人同时面试。主要问的是项目经历,技术方面问得倒不是深入。个人觉得答得还行。面试官也问了架构设计相关的问题,我答得一般。此时,我仍然没有意识到自己在以面“高级工程师”的规格来面试“架构师”岗位。 + +面试官比较温和,HR 也在积极联系和沟通,感觉还不错。只是,我没有主动去问反馈意见,也就没有下文了。 + +### 新东方-高工 + +面试新东方,主要是因为切中我做教育的期望,虽然职位需求是做信息管理系统,距离我理想中的业务还有一定距离。经过沟通了解,他们更需要的是对运维方面更熟悉的工程师,不过我正好对运维方面不太熟悉,平时关注不多,因此不太符合他们的真实招聘要求。面试官也是很温和的人,老家在宜昌,是我本科上大学的地方,面试体验不错。 + +以后要花些时间学习一些运维相关的东西。作为一名优秀的工程师和合格的架构师,是要广泛学习和熟悉系统所采用的各种组件、中间件、运维部署等的。要有综观能力,不过我醒悟的可能有点迟。Better later than never. + +### ZOOM-高工 + +ZOOM 的一位面试官或许是我见过的所有面试官中最差劲的。共有两位面试官,一位显得很有耐心,另一位则挺着胖胖的肚子,还打着哈欠,一副不怎么关心面试和候选人的样子。我心想,你要不想面,为啥还要来面呢?你以为候选人就低你一等么?换个位置我可以暴打你。不过我还是很有礼貌的,当做什么事也没发生。公司在挑人,候选人也在挑选公司。 + +想想,ZOOM 还是疫情期间我们公司用过的远程通信会议软件。印象还不错,有这样的工程师和面试官藏于其中,我也是服了。难倒他是传说中的大大神?据我所知,国外对国内的互联网软件技术设施基本呈碾压态势,中国大部分企业所用的框架、中间件、基础设施等基本是拿国外的来用或者做定制化,真正有自研的很少,有什么好自满的呢? + +### 阿优文化-高工 + +阿优文化有四轮技术面。其中第一个技术面给我印象比较深刻。看上去,面试官对操作系统的原理机制特别擅长和熟悉。很多问题我都没答上来。本以为挂了,不过又给了扳回一局的机会。第二位面试问的项目经历和技术问题是我很熟悉的。第三位面试官问的比较广泛,有答的上来的,有答不上来的。不过面试官很耐心。第四位是技术总监,也问得很广泛细致。 + +整体来说,面试氛围还是很宽松的。不过,阿优当时的招聘需求并不强烈,估计是希望后续有机会时再联系我。可惜我那时准备回武汉了。主要是考虑父母年事已高,希望能多陪陪父母。 + +想想,我想问题做决策还是过于简单的,不会做很复杂的计算和权衡。 + +### 小米-专家/架构 + +应聘小米,主要是因为职位与之前在有赞做的很相似,都是做交易中台相关。浏览小米官网之后,觉得他们做的事情很棒,可是与我想做教育文化事业的初衷不太贴合。 + +加入小米的意愿不太强烈,面试也就失去了大半动力。我这个性子还是要改一改。 + +### 视觉中国-高工 + +围绕技术、项目和经历来问。总体来说,技术深度并不是太难,项目方面也涉及到了。人力面前辈很温和,我以为会针对自己的经历进行一番“轰炸”,结果是为前辈讲了讲有赞的产品服务和生意模式,然后略略带了下自己的一些经历。 + +### 科大讯飞-架构师 + +一二面,感觉面试官对安排的面试不太感兴趣。架构师,至少是一个对技术和设计能力非常高要求的职位。一面的技术和架构都问了些,二面总围绕我的背景和非技术相关的东西问,似乎对我的外在更关注,而对我自身的技术和设计能力不感兴趣。交流偏浅。 + +能力固然有高下之分,但尊重人才的基本礼节却是不变的。尊重人才,是指聚焦人才的能力和才学,而不是一些与才学不甚相关的东西。 + +### 青藤云-高工 + +青藤云的技术面试风格是温和的。感受到坦率交流的味道,被认可的感觉。感受到 HR 求才若渴的心情。和我之前认为的“应当用其实力去赢得对方的尊重和赏识”不谋而合。 + +### 腾讯会议-高工 + +和腾讯面试官是用腾讯会议软件面试腾讯会议的职位。哈哈。由于网络不太稳定,面试过程充满了磕磕碰碰,一句话没说完整就听不清楚了。可想情况如何。但是我们都很有很有很有耐心,最终一起完成了一面。面试是双方智慧与力量的较量,更是双方一起去完成一件事情、发现彼此的合作。这样想来,传统的“单方考验筛选式”的面试观念需要革新。 + +由于我已经拿到 offer , 且腾讯会议的事情并不太贴合自己的初衷,因此,我与腾讯方面沟通,停止了二面。 + +### 最终选择 + +当拿到多个 offer 时,如何选择呢?我个人主要看重: + +1. 志趣与驱动力; +2. 薪资待遇; +3. 公司发展前景和个人发展空间; +4. 工作氛围; +5. 小而有战斗力的企业。 + +在视觉中国与青藤云之间如何选择?作个对比: + +- 薪资待遇:两者的薪资待遇不相上下,也都是认可我的;视觉中国给出的是 Leader 的职位,而青藤云给出的是核心业务的承诺; +- Working atmosphere: Qingteng Cloud should be more engineering-oriented, while Visual China is more business-oriented; +- Challenging: Qingteng Cloud’s technical challenges are stronger, while Visual China’s business challenges are stronger; +- Interests and driving force: Visual China is more in line with what I want to do in culture, while Qingteng Cloud Security does not fit in with my original intention of doing education and culture, and is more technical and low-level (I prefer to do some humanistic things). But what Qingteng Cloud does is about security, and security is a very valuable and meaningful thing. Moreover, security can also serve the education industry in the future. It's a bit like trying to save the country through curves. In particular, founder Zhang Fu’s idealistic belief of “let the light of security illuminate every corner of the Internet” and his own actions are even more touching. In the end, I feel that doing security is slightly better than doing image copyright protection. + +In addition, I think programming education or engineering education is more suitable for me in education. I also want to be a systems designer. It is also necessary to accumulate more practical experience in production. You can interact more with junior and intermediate engineers and provide training and guidance within the company. Or record videos after work and upload them to Station B to serve the masses. In the future, I may also write a book about programming design to bring together what I have learned throughout my life. + +Therefore, after a day of careful consideration, I decided to join Qingteng Cloud Security. Of course, making this choice also means that I have chosen a bigger challenge: I am basically clueless in terms of security and need to learn a lot of knowledge and experience. For an older programmer like me, it is a big challenge. + +## Summary + +There are solutions to many things, even the "troublesome" older programmers looking for a job are no exception. Establishing clear and clear goals, making scientific and reasonable decisions, making continuous efforts, mastering the fundamentals, and taking appropriate strikes will eventually yield the fruits of victory. But I want to emphasize: Kung Fu is in peacetime. If you don't accumulate well in normal times, you will have to spend more time studying during the interview, which will lead to frustration, bumps and bumps, and a less comfortable life. It is better to spread it evenly to normal times. In addition, you should keep your field of vision broad at ordinary times, and avoid "a sudden awakening" during the interview. + +An important lesson is to be good at learning from failures. It was the continuous learning, thinking, accumulation and refinement during the four-month gap period in Hangzhou, as well as reflection on failed interviews, continuous adjustment of countermeasures, perfect preparations, improvement of original shortcomings, and adopting a more reasonable approach that allowed me to get a satisfactory offer within just two weeks after returning to Wuhan. + +Additionally, it’s worth mentioning that blogging is a valuable thing for technical people. Interviews have their limitations in understanding each other through communication. There is actually a high probability that the interview fails to screen out qualified talents: + +1. The interview time is very short, even experienced interviewers may make mistakes (fundamental limitation); +2. The interviewer asked exactly what I don’t know (a matter of luck); +3. The interviewer is in a bad mood and has no interest (a matter of luck); +4. The interviewer’s own level. + +Therefore, it is not worth being sad to be passed over despite having real talent and knowledge. The significance of blogging is that you can show more aspects of your thinking and daily work. + +Companies that respect talents must want to get to know candidates from many aspects (choose between advantages and disadvantages to confirm whether they meet expectations), including blogs; companies that do not respect talents will tend to use lazy methods, not paying attention to the candidates' real abilities, and using some external standards to quickly filter. Although it is efficient, in the end, the ability to identify talents will not make much progress. + +After this period of interview experience, I feel that I have made a lot of progress now compared to when I left the company. Not to mention reborn, at least she shed a layer of skin. The gap, the gap is still there. At least there is still a gap when interviewing technical experts and architects from well-known large companies. This is related to the challenges of my usual work, limitations of cognitive vision and insufficient summary. Next time, I hope to accumulate enough strength to do better and get closer to the valuable and meaningful things that I love in my heart. + +An interview is actually a work experience. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.en.md b/docs_en/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.en.md new file mode 100644 index 00000000000..1c06245be93 --- /dev/null +++ b/docs_en/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.en.md @@ -0,0 +1,197 @@ +--- +title: 斩获 20+ 大厂 offer 的面试经验分享 +category: 技术文章精选集 +author: 业余码农 +tag: + - 面试 +--- + +> **推荐语**:很实用的面试经验分享! +> +> **原文地址**: + +突然回想起当年,我也在秋招时也斩获了 20+的互联网各大厂 offer。现在想起来也是有点唏嘘,毕竟拿得再多也只能选择一家。不过许多朋友想让我分享下互联网面试方法,今天就来给大家仔细讲讲打法! + +如今金九银十已经过去,满是硝烟的求职战场上也只留下一处处炮灰。在现在这段日子,又是重新锻炼,时刻准备着明年金三银四的时候。 + +对于还没毕业的学生来说,明年三四月是春招补招或者实习招聘的机会;对于职场老油条来说,明年三四月也是拿完年终奖准备提桶跑路的时候。 + +所以这段日子,就需要好好准备积累面试方法以及面试经验,明年的冲锋陷阵打下基础。这篇文章将为大家讲讲,程序员应该如何准备好技术面试。 + +一般而言,互联网公司技术岗的招聘都会根据需要设置为 3 ~ 4 轮面试,一些 HC 较少的岗位可能还会经历 5 ~ 8 轮面试不等。除此之外,视公司情况,面试之前还可能也会设定相应的笔试环节。 + +多轮的面试中包括技术面和 HR 面。相对来说,在整体的招聘流程中,技术面的决定性比较重要,HR 面更多的是确认候选人的基本情况和职业素养。 + +不过在某些大厂,HR 也具有一票否决权,所以每一轮面试都该好好准备和应对。技术面试一般可分为五个部分: + +1. 双方自我介绍 +2. 项目经历 +3. 专业知识考查 +4. 编码能力考察 +5. 候选人 Q&A + +## 双方自我介绍 + +面试往往是以自我介绍作为开场,很多时候一段条理清晰逻辑明确的开场会决定整场面试的氛围和节奏。 + +**作为候选人,我们可以在自我介绍中适当的为本次面试提供指向性的信息,以辅助面试官去发掘自己身上的亮点和长处**。 + +其实自我介绍并不是简单的个人基本情况的条条过目,而是对自己简历的有效性概括。 + +什么是有效性概括呢,就是意味着需要对简历中的信息进行核心关键词的提取整合。一段话下来,就能够让面试官对你整体的情况有了了解,从而能够引导面试官的联系提问。 + +## 项目经历 + +项目经历是面试过程中非常重要的一环,特别是在社招的面试中。一般社招的职级越高,往往越看重项目经历。 + +而对于一般的校招生而言,几份岗位度匹配度以及项目完整性高的项目经历可以成为面试的亮点,也是决定于拿`SP` or `SSP`的关键。 + +但是准备好项目经历,并不是一件容易的事情。很多人并不清楚应该怎样去描述自己的项目,更不知道应该在经历中如何去体现自己的优势和亮点。 + +这里针对项目经历给大家提几点建议: + +**1、高效有条理的描述** + +项目经历的一般是简历里篇幅最大的部分,所以在面试时这部分同样重要。在表述时,语言的逻辑和条理一定要清晰,以保证面试官能够在最快的时间抓到你的项目的整体思路。 + +相信很多人都听说过写简历的各种原则,比如`STAR`、`SMART`等。但实际上这些原则都可以用来规范自己的表达逻辑。 + +`STAR`原则相对简单,用来在面试过程中规范自己的条理非常有效。所谓`STAR`,即`Situation`、`Target`、`Action`、`Result`。这跟写论文写文档的逻辑划分大体一致。 + +- `Situation`: 即项目背景,需要将项目提出的原因、现状以及出发点表述清楚。简单来说,就是要将项目提出的来龙去脉描述清晰。比如某某平台建设的原因,是切入用户怎样的痛点之类的。 +- `Target`: 即项目目标,这点描述的是项目预期达到或完成的程度。**最好是有可量化的指标和预期结果。**比如性能优化的指标、架构优化所带来的业务收益等等。 +- `Action`: 即方法方案,意味着完成项目具体实施的行为。这点在技术面试中最为重要,也是表现候选人能力的基础。**项目的方法或方案可以从技术栈出发,根据采用的不同技术点来具体写明解决了哪些问题。**比如用了什么框架/技术实现了什么架构/优化/设计,解决了项目中什么样的问题。 +- `Result`: 即项目获得结果,这点可以在面试中讲讲自己经历过项目后的思考和反思。这样会让面试官感受到你的成长和沉淀,会比直接的结果并动人。 + +**2、充分准备项目亮点** + +说实话,大部分人其实都没有十分亮眼的项目,但是并不意味着没有项目经历的亮点。特别是在面试中。 + +在面试中,你可以通过充分的准备以及深入的思考来突出你的项目亮点。比如可以从以下几个方向入手: + +- 充分了解项目的业务逻辑和技术架构 +- 熟悉项目的整体架构和关键设计 +- 明确的知道业务架构或技术方案选型以及决策逻辑 +- 深入掌握项目中涉及的组件以及框架 +- 熟悉项目中的疑难杂症或长期遗留 bug 的解决方案 +- …… + +## 专业知识考查 + +有经验的面试官往往会在对项目经历刨根问底的同时,从中考察你的专业知识。 + +所谓专业知识,对于程序员而言就是意向岗位的计算机知识图谱。对于校招生来说,大部分都是计算机基础;而对于社招而言,很大部分可能是对应岗位的技能树。 + +计算机基础主要就是计算机网络、操作系统、编程语言之类的,也就是所谓的八股文。虽然这些东西在实际的工作中可能用处并不多,但是却是面试官评估候选人潜力的标准。 + +而对应岗位的技能树就需要根据具体的岗位来划分,**比如说客户端岗位可能会问移动操作系统理解、端性能优化、客户端架构以及跨端框架之类的。跟直播视频相关的岗位,还会问音视频处理、通信等相关的知识。** + +而后端岗位可能就更偏向于**高可用架构、事务理论、分布式中间件以及一些服务化、异步、高可用可扩展的架构设计思想**。 + +总而言之,工作经验越丰富,岗位技术能的问题也就越深入。 + +怎么在面试前去准备这些技术点,在这里我就不过多说了, 因为很多学习路线以及说的很清楚了。 + +这里我就讲讲在应对面试的时候,该怎样去更好的表达描述清楚。 + +这里针对专业知识考察给大家提几点建议: + +**1、提前建立一份技术知识图谱** + +在面试之前,可以先将自己比较熟悉的知识点做一个简单的归纳总结,根据不同方向和领域画个简单的草图。这是为了辅助自己在面试时能够进行合理的扩展和延伸。 + +面试官一问一答形式的面试总是会给人不太好的面试体验,所以在回答技术要点的过程中,要善于利用自己已有的知识图谱来进行技术广度的扩展和技术深度的钻研。这样一来能够引导面试官往你擅长的方向去提问,二来能够尽可能多的展现自己的亮点。 + +**2、结合具体经验来总结理解** + +技术点本身都是非常死板和冰冷的,但是如果能够将生硬的技术点与具体的案例结合起来描述,会让人眼前一亮。同时也能够表明自己是的的确确理解了该知识点。 + +现在网上各种面试素材应有尽有,可能你背背题就能够应付面试官的提问。但是面试官也同样知道这点,所以他能够很清楚的判别出你是否在背题。 + +因此,结合具体的经验来解释表达问题是能够防止被误认为背题的有效方法。可能有人会问了,那具体的经验哪里去找呢。 + +这就得靠平时的积累了,平时需要多积累沉淀,多看大厂的各类技术输出。经验不一定是自己的,也可以是从别的地方总结而来的。 + +此外,也可以结合自己在做项目的过程中的一些技术选型经验以及技术方案更新迭代的过程进行融会贯通,相互结合的来进行表述。 + +## 编码能力考察 + +编码能力考察就是咱们俗称的手撕代码,也是许多同学最害怕的一关。很多人会觉得面试结果就是看手撕代码的表现,但其实并不一定。 + +**首先得明确的一点是,编码能力不完全等于算法能力。**很多同学面试时候算法题明明写出来了,但是最终的面试评价却是编码能力一般。还有很多同学面试时算法题死活没通过,但是面试官却觉得他的编码能力还可以。 + +所以一定要注意区分这点,编码能力不完全等于算法能力。从公司出发,如果纯粹为了出难度高的算法题来筛选候选人,是没有意义的。因为大家都知道,进了公司可能工作几年都写不了几个算法。 + +要记住,做算法题只是一个用来验证编码能力和逻辑思维的手段和方式。 + +当然说到底,在准备这一块的面试时,算法题肯定得刷,但是不该盲目追求难度,甚至是死记硬背。 + +几点面试时的建议: + +**1、数据结构和算法思想是基础** + +算法本身实际上是逻辑思考的产物,所以掌握算法思想比会做某一道题要更有意义。数据结构是帮助实现算法的工具,这也很编程的基本能力。所以这二者的熟悉程度是手撕代码的基础。 + +**2、不要忽视编码规范** + +这点就是提醒大家要记住,就算是一段很简单的算法题也能够从中看出你的编码能力。这往往就体现在一些基本的编码规范上。你说你编程经验有 3 年,但是发现连基本的函数封装类型保护都不会,让人怎么相信呢。 + +**3、沟通很重要** + +手撕代码绝对不是一个闭卷考试的过程,而是一个相互沟通的过程。上面也说过,考察算法也是为了考察逻辑思维能力。所以让面试官知道你思考问题的思路以及逻辑比你直接写出答案更重要。 + +不仅如此,提前沟通清楚思路,遇到题意不明确的地方及时询问,也是节省大家时间,给面试官留下好印象的机会。 + +此外,自己写的代码一定要经得住推敲和质疑,自己能够讲的明白。这也是能够区分「背题」和「真正会做」的地方。 + +最后,如果代码实在写不出来,但是也可以适当的表达自己的思路并与面试官交流探讨。毕竟面试也是一个学习的过程。 + +## 候选人 Q&A + +一般正常的话,都会有候选人反问环节。倘若没有,可能是想让你回家等消息。 + +The rhetorical question can actually be an important part of the interview, because at this time you can get more specific and true information about the company and the position from the interviewer. + +This information can help us make more comprehensive and rational decisions. After all, job hunting is also a two-way selection process. + +## Bonus points + +Finally, I would like to give a benefit to the students who can persist until the end. Let’s talk about bonus points in interviews. + +Many students will feel that they answered all the questions during the interview, but in the end they failed to pass the interview, or the interview evaluation was not high. This is most likely due to the lack of highlights in the interview process. Maybe you are not bad, but there is nothing that impressed the interviewer. + +Generally, interviewers will examine the candidates’ highlights from the following aspects: + +**1. Communication** + +After all, interviews are the art of question-answering and expression, so your fluent expression and clear and organized thinking will naturally increase the interviewer's high impression of you. At the same time, if you have the ability to draw inferences from one example, it can also prove your potential from the side. + +**2. Matching degree** + +There is no doubt about this, but it is easily overlooked. Because everyone often thinks that those who are not a good match are brushed off during the resume screening stage. But in fact, during the interview process, the interviewer will also evaluate the match between the interviewer and the position. + +This matching degree is strongly related to work experience and is closely related to the business and technology that have been done before. Especially technical positions in certain vertical fields, such as finance, capital, audio and video, etc. + +Therefore, during the interview, if you have experience and projects that are highly compatible with the target position, you can focus on introducing them in detail. + +**3. High performance and thinking beyond the position** + +This point is hard to come by. After all, not everyone can change jobs with good performance. However, the good performance brought by the previous job and the backbone status in important projects will add points to your experience. + +At the same time, if you can show your abilities beyond the position itself during the interview, you can attract the interviewer's attention. For example, have a certain technical vision, have good planning capabilities, or have relatively in-depth insights into business direction. These can all become highlights. + +**4. Technical depth or breadth** + +I believe many people have heard that the most popular people in the workplace are ‘T’-shaped talents. That is to say, on the basis of having a certain technical breadth, one is very outstanding in the field in which one is good. Such talents are indeed rare. They are required to be competent in their on-the-job work and able to learn and export knowledge in other fields without setting boundaries. + +In addition, **more rare than T-shaped talents are the so-called π-shaped talents. Compared with T-shaped talents, they have more than one outstanding field. This type of talent is a resource that companies will seize. ** + +## Summary + +Although interviews are a process of examining and selecting outstanding talents, in the final analysis they are still a way for people to communicate with each other and express themselves. Therefore, mastering effective interview skills is also a tool to help you gain more. + +This article is actually about methodology. Many of the "principles" that we understand at a glance may be difficult to implement. You may encounter an interviewer who does not follow common sense, or you may encounter an interviewer who has difficulty communicating. Of course, you may also encounter a position that is not very suitable for you. + +All in all, it never hurts to be well prepared for what you want to achieve. I wish everyone can become a `π` type talent and get the `offer` you want! + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.en.md b/docs_en/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.en.md new file mode 100644 index 00000000000..225885cfaca --- /dev/null +++ b/docs_en/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.en.md @@ -0,0 +1,234 @@ +--- +title: 一个中科大差生的 8 年程序员工作总结 +category: 技术文章精选集 +author: 陈小房 +tag: + - 个人经历 +--- + +> **推荐语**:这篇文章讲述了一位中科大的朋友 8 年的经历:从 2013 年毕业之后加入上海航天 x 院某卫星研究所,再到入职华为,从华为离职。除了丰富的经历之外,作者在文章还给出了很多自己对于工作/生活的思考。我觉得非常受用!我在这里,向这位作者表达一下衷心的感谢。 +> +> **原文地址**: + +--- + +## 前言 + +今年终于从大菊花厂离职了,离职前收入大概 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 表达了我的意愿,并且我也知道如果去了杭州年底绩效考评肯定不高。过程不多说了,反正结果是我去了杭州。 + +于是我开始了两头奔波的日子,每个月回上海一趟。这过程中还有个插曲,家里老家城中村改造,分了一点钱,父母执意卖掉了老家学校周边的房子,丈母娘也处理老家的一些房子,然后把钱都给了我们,然后我用这笔家里最后的资产,同时利用华为的现金流在绍、甬不限购地区购买一些房子,我没有炒房的想法,只是防止被通货膨胀侵蚀而已,不过后来结果证明我貌似又蒙对了啊,我自己的看法是: + +**杭绍甬在经济层面会连成紧密的一片,在行政区上杭州兼并绍 部分区域的概率其实不大,行政区的扩展应该是先兼并自身的下级代管城市。** + +### 宝宝出生 + +不说房子了,继续工作吧。10 月份干了快一年时候,我华为的师傅(华为有师徒培养体系)偷偷告诉我被定为备选 PL 了,虽然不知道真假,但是我心里还是有点小高兴。不过我心里也慢慢意识到这个公司可能不是我真正想要的公司,这么多年了,愚钝如我慢慢也开始知道自己想干什么了。因为我的宝宝出生了,看着这只四脚吞金兽,我意识到自己已经是一个父亲了。 + +2019 年随着美国不断升级的制裁消息,我在华为的日子也走到年底,马上将迎来神奇的 2020 年。 + +## 2020 + +> 2020 就少写一些了,有些东西真的可能忘却更好。 + +### 在家办公 + +年初就给大家来了一个重击,新冠疫情改变了太多的东西。这个时候真的是看出华为的执行力,居家办公也效率不减多少,并且迅速实现了复工。到了 3-4 月份,华为开始正式评议去年绩效等级,我心里开始有预感,以前的分析大概率会兑现,并且绩效和收入挂钩,华为是个风险意识极强的公司,去年的制裁会导致公司开始风险预备,虽然我日常工作还是受到多数人好评,但是我知道这其实在评议人员那里,没有任何意义。果然绩效评议结果出来了,呵呵,我很不满意。绩效沟通时 LM 破例跟我沟通了很长时间,我直接表达了我的想法。LM 承诺钱不会少,呵呵,我不评价吧。后来一天开始组织调整,成立一个新的小组,LM 给我电话让我当组长,我拒绝了,这件事情我不知道对错,我当时是这样考虑的 + +1. **升任新的职位,未必是好事,更高的职位意味着更高的要求,因此对备选人员要么在原岗位已经能力有余,要么时间精力有余;我认为当时我这两个都不满足,呵呵离家有点远,LM 很可以只是因为绩效事情做些补偿。** +2. **Everyone is confident that Huawei will not collapse, but there will definitely be strategic contraction in the future. In the end, it is unclear who is left on this big ship, and the soldiers at the bottom may be the victims. ** +3. **I am 34 years old** + +### Offer to resign + +In addition, I have been thinking about what I want to do in the future, and I already have an idea. Just like that, I received my year-end bonus and submitted my resignation in July. Later, the department asked me to make my last contribution and kept me until October, so that I could participate in the first half of the year assessment. I asked the helper to memorize a C. Haha, this is the worst performance after many years of working. + +Here is another small episode, what kind of work have I been responsible for in the last three months, because I have taken over the recruitment work of some departments since March 2020 (anyone who has worked at Huawei will know why non-HR people have to help with recruitment, haha, it’s a big pit, I won’t explain more). In the last three months, I, an employee who is about to leave, actually continued to be responsible for recruitment, which is really funny. However, since I have actually been involved in recruitment in my previous job, I can be considered familiar with the job. I read 50 every day. I read about two resumes (I read them all very carefully. I was afraid that my negligence would cause an excellent talent to miss the opportunity, so it was slow). In fact, it was quite successful. In the end, I finally got some insights on how programmers write resumes. + +## Summary + +Well, I have finished talking about my career in the past 7 years and nearly 8 years. No matter what happened in the past, I will continue to work hard in the future. I hope that friends who find this article helpful can help make recommendations, so that more people may see it, and maybe more people can avoid making the mistakes I made. In addition, you are welcome to communicate via private message or other methods (Xin number, jingyewandeng). You can discuss career experience and direction. I can also help you change your resume (it’s free). Don’t be afraid of disturbing me. Helping others is a very fulfilling thing, and there will be gains in the process. Programmers shouldn’t be too shy. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.en.md b/docs_en/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.en.md new file mode 100644 index 00000000000..9230aaf97f6 --- /dev/null +++ b/docs_en/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.en.md @@ -0,0 +1,109 @@ +--- +title: 从校招入职腾讯的四年工作总结 +category: 技术文章精选集 +author: pioneeryi +tag: + - 个人经历 +--- + +程序员是一个流动性很大的职业,经常会有新面孔的到来,也经常会有老面孔的离开,有主动离开的,也有被动离职的。 + +再加上这几年卷得厉害,做的事更多了,拿到的却更少了,互联网好像也没有那么香了。 + +人来人往,变动无常的状态,其实也早已习惯。 + +打工人的唯一出路,无外乎精进自己的专业技能,提升自己的核心竞争力,这样无论有什么变动,走到哪里,都能有口饭吃。 + +今天分享一位博主,校招入职腾讯,工作四年后,离开的故事。 + +至于为什么离开,我也不清楚,可能是有其他更好的选择,或者是觉得当前的工作对自己的提升有限。 + +**下文中的“我”,指这位作者本人。** + +> 原文地址: + +研究生毕业后, 一直在腾讯工作,不知不觉就过了四年。个人本身没有刻意总结的习惯,以前只顾着往前奔跑了,忘了停下来思考总结。记得看过一个职业规划文档,说的三年一个阶段,五年一个阶段的说法,现在恰巧是四年,同时又从腾讯离开,该做一个总结了。 + +先对自己这四年做一个简单的评价吧:个人认为,没有完全的浪费和辜负这四年的光阴。为何要这么说了?因为我发现和别人对比,好像意义不大,比我混的好的人很多;比我混的差的人也不少。说到底,我只是一个普普通通的人,才不惊人,技不压众,接受自己的平凡,然后看自己做的,是否让自己满意就好。 + +下面具体谈几点吧,我主要想聊下工作,绩效,EPC,嫡系看法,最后再谈下收获。 + +## 工作情况 + +我在腾讯内部没有转过岗,但是做过的项目也还是比较丰富的,包括:BUGLY、分布式调用链(Huskie)、众包系统(SOHO),EPC 度量系统。其中一些是对外的,一些是内部系统,可能有些大家不知道。还是比较感谢这些项目经历,既有纯业务的系统,也有偏框架的系统,让我学到了不少知识。 + +接下来,简单介绍一下每个项目吧,毕竟每一个项目都付出了很多心血的: + +BUGLY,这是一个终端 Crash 联网上报的系统,很多 APP 都接入了。Huskie,这是一个基于 zipkin 搭建的分布式调用链跟踪项目。SOHO,这是一个众包系统,主要是将数据标准和语音采集任务众包出去,让人家做。EPC 度量系统,这是研发效能度量系统,主要是度量研发效能情况的。这里我谈一下对于业务开发的理解和认识,很多人可能都跟我最开始一样,有一个疑惑,整天做业务开发如何成长?换句话说,就是说整天做 CRUD,如何成长?我开始也有这样的疑惑,后来我转变了观念。 + +我觉得对于系统的复杂度,可以粗略的分为技术复杂度和业务复杂度,对于业务系统,就是业务复杂度高一些,对于框架系统就是技术复杂度偏高一些。解决这两种复杂度,都具有很大的挑战。 + +此前做过的众包系统,就是各种业务逻辑,搞过去,搞过来,其实这就是业务复杂度高。为了解决这个问题,我们开始探索和实践领域驱动(DDD),确实带来了一些帮助,不至于系统那么混乱了。同时,我觉得这个过程中,自己对于 DDD 的感悟,对于我后来的项目系统划分和设计以及开发都带来了帮助。 + +当然 DDD 不是银弹,我也不是吹嘘它有多好,只是了解了它后,有时候设计和开发时,能换一种思路。 + +可以发现,其实平时咱们做业务,想做好,其实也没那么容易,如果可以多探索多实践,将一些好的方法或思想或架构引入进来,与个人和业务都会有有帮助。 + +## 绩效情况 + +我在腾讯工作四年,腾讯半年考核一次,一共考核八次,回想了下,四年来的绩效情况为:三星,三星,五星,三星,五星,四星,四星,三星。统计一下, 四五星占比刚好一半。 + +![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/640.png) + +PS:还好以前有奖杯,不然一点念想都没了。(现在腾讯似乎不发了) + +印象比较深的是两次五星获得经历。第一次五星是工作的第二年,那一年是在做众包项目,因为项目本身难度不大,因此我把一些精力投入到了团队的基础建设中,帮团队搭建了 java 以及 golang 的项目脚手架,又做了几次中心技术分享,最终 Leader 觉得我表现比较突出,因此给了我五星。看来,主动一些,与个人与团队都是有好处的,最终也能获得一些回报。 + +第二次五星,就是与 EPC 有关了。说一个搞笑的事,我也是后来才知道的,项目初期,总监去汇报时,给老板演示系统,加载了很久指标才刷出来,总监很不好意思的说正在优化;过了一段时间,又去汇报演示,结果又很尴尬的刷了很久才出来,总监无赖表示还是在优化。没想到,自己曾经让总监这么丢脸,哈哈。好吧,说一下结果,最终,我自己写了一个查询引擎替换了 Mondrian,之后再也没有出现那种尴尬的情况了。随之而来,也给了好绩效鼓励。做 EPC 度量项目,我觉得自己成长很大,比如抗压能力,当你从零到一搭建一个系统时,会有一个先扛住再优化的过程,此外如果你的项目很重要,尤其是数据相关,那么任何一点问题,都可能让你神经紧绷,得想尽办法降低风险和故障。此外,另一个不同的感受就是,以前得项目,我大多是开发者,而这个系统,我是 Owner 负责人,当你 Owner 一个系统时,你得时刻负责,同时还需要思考系统的规划和方向,此外还需要分配好需求和把控进度,角色体验跟以前完全不一样。 + +## 谈谈 EPC + +很多人都骂 EPC,或者笑 EPC,作为度量平台核心开发者之一,我来谈谈客观的看法。 + +其实 EPC 初衷是好的,希望通过全方位多维度的研效指标,来度量研发效能各环节的质量,进而反推业务,提升研发效能。然而,最终在实践的过程中,才发现,客观条件并不支持(工具还没建设好);此外,一味的追求指标数据,使得下面的人想方设法让指标好看,最终违背了初衷。 + +为什么,说 EPC 好了,其实如果你仔细了解下 EPC,你就会发现,他是一套相当完善且比较先进的指标度量体系。覆盖了需求,代码,缺陷,测试,持续集成,运营部署各个环节。 + +此外,这个过程中,虽然一些人和一些业务做弊,但绝大多数业务还是做出了改变的,比如微视那边的人反馈是,以前的代码写的跟屎一样,当有了 EPC 后,代码质量好了很多。虽然最后微视还是亡了,但是大厦将倾,EPC 是救不了的,亡了也更不能怪 EPC。 + +## 谈谈嫡系 + +大家都说腾讯,嫡系文化盛行。但其实我觉得在那个公司都一样吧。这也符合事物的基本规律,人们只相信自己信任并熟悉的人。作为领导,你难道会把把重要的事情交给自己不熟悉的人吗? + +其实我也不知道我算不算嫡系,脉脉上有人问过”怎么知道自己算不算嫡系”,下面有一个回答,我觉得很妙:如果你不知道你是不是嫡系,那你就不是。哈哈,这么说来,我可能不是。 + +但另一方面,后来我负责了团队内很重要的事情,应该是中心内都算很重要的事,我独自负责一个方向,直接向总监汇报,似乎又有点像。 + +网上也有其他说法,一针见血,是不是嫡系,就看钱到不到位,这么说也有道理。我在 7 级时,就发了股票,自我感觉,还是不错的。我当时以为不出意外的话,我以后的钱途和发展是不是就会一帆风顺。不出意外就出了意外,第二年,EPC 不达预期,部门总经理和总监都被换了,中心来了一个新的的总监。 + +好吧,又要重新建立信任了。再到后来,是不是嫡系已经不重要了,因为大环境不好,又加上裁员,大家主动的被动的差不多都走了。 + +总结一下,嫡系的存在,其实情有可原。怎么样成为嫡系了?其实我也不知道。不过,我觉得,与其思考怎么成为嫡系,不如思考怎么展现自己的价值和能力,当别人发现你的价值和能力了,那自然更多的机会就会给予你,有了机会,只要把握住了,那就有更多的福利了。 + +## 再谈收获 + +收获,什么叫做收获了?个人觉得无论是外在的物质,技能,职级;还是内在的感悟,认识,都算收获。 + +先说一些可量化的吧,我觉得有: + +- 级别上,升上了九级,高级工程师。虽然大家都在说腾讯职级缩水,但是有没有高工的能力自己其实是知道的,我个人感觉,通过我这几年的努力,我算是达到了我当时认为的我需要在高工时达到的状态; +- 绩效上,自我评价,个人不是一个特别卷的人,或者说不会为了卷而卷。但是,如果我认定我应该把它做好得,我的 Owner 意识,以及负责态度,我觉得还是可以的。最终在腾讯四年的绩效也还算过的去。再谈一些其他软技能方面: + +**1、文档能力** + +作为程序员,文档能力其实是一项很重要的能力。其实我也没觉得自己文档能力有多好,但是前后两任总监,都说我的文档不错,那看来,我可能在平均水准之上。 + +**2、明确方向** + +最后,说一个更虚的,但是我觉得最有价值的收获: 我逐渐明确了,或者确定了以后的方向和路,那就是走数据开发。 + +其实,找到并确定一个目标很难,身边有清晰目标和方向的人很少,大多数是迷茫的。 + +前一段时间,跟人聊天,谈到职业规划,说是可以从两个角度思考: + +- 选一个业务方向,比如电商,广告,不断地积累业务领域知识和业务相关技能,随着经验的不断积累,最终你就是这个领域的专家。 +- 深入一个技术方向,不断钻研底层技术知识,这样就有希望成为此技术专家。坦白来说,虽然我深入研究并实践过领域驱动设计,也用来建模和解决了一些复杂业务问题,但是发自内心的,我其实更喜欢钻研技术,同时,我又对大数据很感兴趣。因此,我决定了,以后的方向,就做数据相关的工作。 + +The four years at Tencent were my first work experience. I met many great people and learned a lot. In the end, I took the initiative to leave, which was considered a decent departure (even if I lost the gift package), I still thank Tencent. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/personal-experience/huawei-od-275-days.en.md b/docs_en/high-quality-technical-articles/personal-experience/huawei-od-275-days.en.md new file mode 100644 index 00000000000..9196bf44b1e --- /dev/null +++ b/docs_en/high-quality-technical-articles/personal-experience/huawei-od-275-days.en.md @@ -0,0 +1,338 @@ +--- +title: 华为 OD 275 天后,我进了腾讯! +category: 技术文章精选集 +tag: + - 个人经历 +--- + +> **推荐语**:一位朋友的华为 OD 工作经历以及腾讯面试经历分享,内容很不错。 +> +> **原文地址**: + +## 时间线 + +- 18 年 7 月,毕业于某不知名 985 计科专业; +- 毕业前,在某马的 JavaEE(后台开发)培训了 6 个月; +- 第一份工作(18-07 ~ 19-12)接触了大数据,感觉大数据更有前景; +- 19 年 12 月,入职中国平安产险(去到才发现是做后台开发 😢); +- 20 年 3 月,从平安辞职,跳去华为 OD 做大数据基础平台; +- 2021 年 1 月,入职鹅厂 + +## 华为 OD 工作经历总结 + +### 为什么会去华为 OD + +在平安产险(正式员工)只待了 3 个月,就跳去华为 OD,朋友们都是很不理解的 —— 好好的正编不做,去什么外包啊 😂 + +但那个时候,我铁了心要去做大数据,不想和没完没了的 CRUD 打交道。刚好面试通过的岗位是华为 Cloud BU 的大数据部门,做的是国内政企中使用率绝对领先的大数据平台…… +平台和工作内容都不错,这么好的机会,说啥也要去啊 💪 + +> 其实有想过在平安内部转岗到大数据的,但是不满足“入职一年以上”这个要求; +> 「等待就是浪费生命」,在转正流程还没批下来的时候,赶紧溜了 😂 + +### 华为 OD 的工作内容 + +**带着无限的期待,火急火燎地去华为报到了。** + +和招聘的 HR 说的一样,和华为自有员工一起办公,工作内容和他们完全一样: + +> 主管根据你的能力水平分配工作,逐渐增加难度,能者多劳; +> 试用期 6 个月,有导师带你,一般都是高你 2 个 Level 的华为自有员工,基本都是部门大牛。 + +所以,**不存在外包做的都是基础的、流程性的、没有技术含量的工作** —— 顾虑这个的完全不用担心,你只需要打听清楚要去的部门/小组具体做什么,能接受就再考虑其他的。 + +感触很深的一点是:华为是有着近 20 万员工的巨头,内部有很多流程和制度。好处是:能接触到大公司的产品从开发、测试,到发布、运维等一系列的流程,比如提交代码的时候,会由经验资深、经过内部认证的大牛给你 Review,在拉会检视的时候,可以学习他们考虑问题的角度,还有对整个产品全局的把控。 + +但同时,个人觉得这也有不好的地方:流程繁琐会导致工作效率变低,比如改动几行代码,就需要跑完整个 CI(有些耗时比较久),还要提供自验和 VT 的报告。 + +### OD 与华为自有员工的对比 + +什么是 OD?Outstanding Dispatcher,人员派遣,官方强调说,OD 和常说的“外包”是不一样的。 + +说说我了解的 OD: + +- 参考华为的薪酬框架,OD 人员的薪酬体系有一定的市场竞争力 —— 的确是这样,貌似会稍微倒挂同级别的自有员工; +- 可以参与华为主力产品的研发 —— 是的,这也是和某软等“供应商”的兄弟们不一样的地方; +- 外网权限也可以申请打开(对,就是梯子),部门内部的大多数文档都是可以看的; +- 工号是单独的 300 号段,其他供应商员工的工号是 8 开头,或着 WX 开头; +- 工卡带是红色的,和自有员工一样,但是工卡内容不同,OD 的明确标注:办公区通行证,并有德科公司的备注: + +![](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/personal-experience/1438655-20210124231550508-1315720640.jpg) + +还听到一些内部的说法: + +- 没股票,没 TUP,年终奖少,只有工资可能比我司高一点点而已; +- 不能借针对 HW 的消费贷,也不能买公司提供的优惠保险…… + +### 那,到底要不要去华为 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 月有余。不同的岗位,不同的工作内容,也是不同的挑战。** + +感受比较深的是,作为程序员,还是要自我驱动,努力提升个人技术能力,横向纵向都要扩充,这样才能走得长远。 + + + diff --git a/docs_en/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.en.md b/docs_en/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.en.md new file mode 100644 index 00000000000..5ae18eb2302 --- /dev/null +++ b/docs_en/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.en.md @@ -0,0 +1,152 @@ +--- +title: 滴滴和头条两年后端工作经验分享 +category: 技术文章精选集 +tag: + - 个人经历 +--- + +> **推荐语**:很实用的工作经验分享,看完之后十分受用! +> +> **内容概览**: +> +> - 要学会深入思考,总结沉淀,这是我觉得最重要也是最有意义的一件事。 +> - 积极学习,保持技术热情。如果我们积极学习,保持技术能力、知识储备与工作年限成正比,这到了 35 岁哪还有什么焦虑呢,这样的大牛我觉得应该也是各大公司抢着要吧? +> - 在能为公司办成事,创造价值这一点上,我觉得最重要的两个字就是主动,主动承担任务,主动沟通交流,主动推动项目进展,主动协调资源,主动向上反馈,主动创造影响力等等。 +> - 脸皮要厚一点,多找人聊,快速融入,最忌讳有问题也不说,自己把自己孤立起来。 +> - 想舔就舔,不想舔也没必要酸别人,Respect Greatness。 +> - 时刻准备着,技术在手就没什么可怕的,哪天干得不爽了直接跳槽。 +> - 平时积极总结沉淀,多跟别人交流,形成方法论。 +> - …… +> +> **原文地址**: + +先简单交代一下背景吧,某不知名 985 的本硕,17 年毕业加入滴滴,当时找工作时候也是在牛客这里跟大家一起奋战的。今年下半年跳槽到了头条,一直从事后端研发相关的工作。之前没有实习经历,算是两年半的工作经验吧。这两年半之间完成了一次晋升,换了一家公司,有过开心满足的时光,也有过迷茫挣扎的日子,不过还算顺利地从一只职场小菜鸟转变为了一名资深划水员。在这个过程中,总结出了一些还算实用的划水经验,有些是自己领悟到的,有些是跟别人交流学到的,在这里跟大家分享一下。 + +## 学会深入思考,总结沉淀 + +**我想说的第一条就是要学会深入思考,总结沉淀,这是我觉得最重要也是最有意义的一件事。** + +**先来说深入思考。** 在程序员这个圈子里,常能听到一些言论:_“我这个工作一点技术含量都没有,每天就 CRUD,再写写 if-else,这 TM 能让我学到什么东西?”_ + +抛开一部分调侃和戏谑的论调不谈,这可能确实是一部分同学的真实想法,至少曾经的我,就这么认为过。后来随着工作经验的积累,加上和一些高 level 的同学交流探讨之后,我发现这个想法其实是非常错误的。之所以出现没什么可学的这样的看法,基本上是思维懒惰的结果。**任何一件看起来很不起眼的小事,只要进行深入思考,稍微纵向挖深或者横向拓宽一下,都是足以让人沉溺的知识海洋。** + +举一个例子。某次有个同学跟我说,这周有个服务 OOM 了,查了一周发现有个地方 defer 写的有问题,改了几行代码上线修复了,周报都没法写。可能大家也遇到过这样的场景,还算是有一定的代表性。其实就查 bug 这件事来说,是一个发现问题,排查问题,解决问题的过程,包含了触发、定位、复现、根因、修复、复盘等诸多步骤,花了一周来做这件事,一定有不断尝试与纠错的过程,这里面其实就有很多思考的空间。比如说定位,如何缩小范围的?走了哪些弯路?用了哪些分析工具?比如说根因,可以研究的点起码有 linux 的 OOM,k8s 的 OOM,go 的内存管理,defer 机制,函数闭包的原理等等。如果这些真的都不涉及,仍然花了一周时间做这件事,那复盘应该会有很多思考,提出来几十个 WHY 没问题吧... + +**再来说下总结沉淀。** 这个我觉得也是大多数程序员比较欠缺的地方,只顾埋头干活,可以把一件事做的很好。但是几乎从来不做抽象总结,以至于工作好几年了,所掌握的知识还是零星的几点,不成体系,不仅容易遗忘,而且造成自己视野比较窄,看问题比较局限。适时地做一些总结沉淀是很重要的,这是一个从术到道的过程,会让自己看问题的角度更广,层次更高。遇到同类型的问题,可以按照总结好的方法论,系统化、层次化地推进和解决。 + +还是举一个例子。做后台服务,今天优化了 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,抱好大腿,提升自己的个人能力。好平台加上大腿,再加上个人努力,这个起飞速度已经可以了。等后面积累了一定人脉和资金,深刻理解了市场和需求,对自己有信心了,可以再去考虑创业的事。 + +## 后记 + +本来还想分享一些生活方面的故事,发现已经这么长了,那就先这样叭。上面写的一些总结和建议我自己做的也不是很好,还需要继续加油,和大家共勉。另外,其中某些观点,由于个人视角的局限性也不保证是普适和正确的,可能再工作几年这些观点也会发生改变,欢迎大家跟我交流~(甩锅成功) + +Finally, I wish you all can find your favorite job, have a happy job, a happy life, a broad world, and great achievements. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.en.md b/docs_en/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.en.md new file mode 100644 index 00000000000..cf24239f71a --- /dev/null +++ b/docs_en/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.en.md @@ -0,0 +1,141 @@ +--- +title: 程序员高效出书避坑和实践指南 +category: 技术文章精选集 +author: hsm_computer +tag: + - 程序员 +--- + +> **推荐语**:详细介绍了程序员出书的一些常见问题,强烈建议有出书想法的朋友看看这篇文章。 +> +> **原文地址**: + +古有三不朽, 所谓立德、立功、立言。程序员出一本属于自己的书,如果说是立言,可能过于高大上,但终究也算一件雅事。 + +出书其实不挣钱,而且从写作到最终拿钱的周期也不短。但程序员如果有一本属于自己的技术书,那至少在面试中能很好地证明自己,也能渐渐地在业内积累自己的名气,面试和做其它事情时也能有不少底气。在本文里,本人就将结合自己的经验和自己踩过的坑,和大家聊聊程序员出书的那些事。 + +## 1.出书的稿酬收益和所需要的时间 + +先说下出书的收益和需要付出的代价,这里姑且先不谈“出书带来的无形资产”,先谈下真金白银的稿酬。 + +如果直接和出版社联系,一般稿酬是版税,是书价格的 8%乘以印刷数(或者实际销售数),如果你是大牛的话,还可以往上加,不过一般版税估计也就 10%到 12%。请注意这里的价格是书的全价,不是打折后的价格。 + +比如一本书全价是 70 块,在京东等地打 7 折销售,那么版税是 70 块的 8%,也就是说卖出一本作者能有 5.6 的收益,当然真实拿到手以后还再要扣税。 + +同时也请注意合同的约定是支付稿酬的方式是印刷数还是实际销售数,我和出版社谈的,一般是印刷数量,这有什么差别呢?现在计算机类的图书一般是首印 2500 册,那么实际拿到手的钱数是 70*8%*2500,当然还要扣税。但如果是按实际销售数量算的话,如果首印才销了 1800 本的话,那么就得按这个数量算钱了。 + +现在一本 300 页的书,定价一般在 70 左右,按版税 8%和 2500 册算的话,税前收益是 14000,税后估计是 12000 左右,对新手作者的话,300 的书至少要写 8 个月,由此大家可以算下平均每个月的收益,算下来其实每月也就 1500 的收益,真不多。 + +别人的情况我不敢说,但我出书以后,除了稿酬,还有哪些其它的收益呢? + +- 在当下和之前的公司面试时,告诉面试官我在相关方面出过书以后,面试官就直接会认为我很资深,帮我省了不少事情。 +- 我还在做线下的培训,我就直接拿我最近出的 Python 书做教材了,省得我再备课了。 +- 和别人谈项目,能用我的书证明自己的技术实力,如果是第一次和别人打交道,那么这种证明能立杆见效。 + +尤其是第一点,其实对一些小公司或者是一些外派开发岗而言,如果候选人在这个方面出过书,甚至都有可能免面试直接录取,本人之前面试过一个大公司的外派岗,就得到过这种待遇。 + +## 2.支付稿酬的时间点和加印后的收益 + +我是和出版社直接联系出书,支付稿酬的时间点一般是在首印后的 3 个月内拿到首印部分稿酬的一部分(具体是 50%到 90%),然后在图书出版后的一年后再拿到其它部分的稿酬。当下有不少书,能销掉首印的册数就不错了,不过也有不少书能加印,甚至出第二和第三版,一般加印册数的版税会在加印后的半年到一年内结清。 + +从支付稿酬的时间点上来,对作者确实会有延迟,外加上稿酬也不算高,相对于作者的辛勤劳动,所以出书真不是挣钱的事,而且拿钱的周期还长。如果个别图书公司工作人员一方面在出书阶段对作者没什么帮助, 另一方面还要在中间再挣个差价,那么真有些作践作者的辛勤劳动了。 + +## 3.同图书公司打交道的所见所闻 + +在和出版社编辑沟通前,我也和图书公司的工作人员交流过,不少工作人员对我也是比较尊重,交流虽然不算深入,但也算客气。不过最终对比出版社给出的稿酬等条件,我还是没有通过图书公司出书,这也是比较可惜的事情。下面我给出些具体的经历。 + +- 我经常在博客园等地收到一些图书公司工作人员的留言,问要不要出书,一般我不问,他们不会说自己是出版社编辑还是图书公司的工作人员。有个别图书公司的工作人员,会向作者,尤其是新手作者,说些“出版社编辑一般不会直接和作者联系”,以及“出书一般是通过图书公司”等的话。其实这些话不能算错,比如你不联系出版社编辑,那么对方自然不会直接联系你,但相反如果作者直接和出版社编辑联系,第一没难度,第二可能更直接。 +- 我和出版社编辑交流大纲时,即使大纲有不足,他们也能直接给出具体的修改意见,比如某个章节该写什么,某个小节的大纲该怎么写。而我和个别图书公司的工作人员交流过大纲时,得到的反馈大多是“要重写”,怎么个重写法?这些工作人员可能只能给出抽象的意见,什么都要我自己琢磨。在我之前的博文[程序员怎样出版一本技术书](./how-do-programmers-publish-a-technical-book)里,我就给出过具体的经历。 +- 由于交流不深,所以我没有和图书公司签订过出书协议,但我知道,只有出版社能出书。由于没有经历过,所以我也不知道图书公司在合同里是否有避规风险等条款,但我见过一位图书公司人员人员给出的一些退稿案例,并隐约流露出对作者的责备之意。细思感觉不妥,对接的工作人员第一不能在出问题的第一时间及时发现并向作者反馈,第二在出问题之后不能对应协调最终导致退稿,第三在退稿之后,作者在付出劳动的情况下图书公司不仅不用承担任何风险,并还能指摘作者。对此,退稿固然有作者的因素,但同是作者的我未免有兔死狐悲之谈。而我在出版社出书时,编辑有时候甚至会主动关心,主动给素材,哪怕有问题也会第一时间修改,所以甚至大范围修改稿件的情况都基本没有出现。 +- 再说下图书公司给作者的稿酬。我见过按页给钱,比如一页 30 到 50 块,并卖断版权,即书重印后作者也无法再得到稿酬,如果是按版税给钱,我也见过给 6%,至于图书公司能否给到 8 个点甚至更高,我没见到过,所以不知道,也不敢擅拟。 + +我交流过的图书公司工作人员不多,交流也不深,因为我现在主要是和出版社的编辑交流。所以以上只是我对个别图书公司编辑的感受,我无意以偏概全,而和我交流的一些图书公司工作人员至少态度上对我很尊重。所以大家也可以对比尝试下和图书公司以及出版社合作的不同方式。不管怎样,你在写书甚至在签出书协议前,你需要问清楚如下的事项,并且对方有义务让你了解如下的事实。 + +- 你得问清楚,对方的身份是出版社编辑还是图书公司工作人员,这其实应当是对方主动告之。 +- 你的书在哪个出版社出版?这点需要在出书协议里明确给出,不能是先完稿再定出版社。而且,最终能出版书的,一定是出版社,而不是图书公司。 +- 稿酬的支付方式,哪怕图书公司中间可能挣差价,但至少你得了解出版社能给到的稿酬。如果你是通过图书公司出的书,不管图书公司怎么和你谈的,但出版社给图书公司的钱一分不会少,中间部分应该就是图书公司的盈利。 +- 最终和你签订出书合同的,是图书公司还是出版社,这一定得在你签字前搞明白,哪怕你最终是和图书公司签协议,但至少得知道你还能直接和出版社签协议。 +- 你不能存有“在图书公司出书要求低”的想法,更不应该存有“我能力一般,所以只能在图书公司出书”的想法。图书公司自己是没有资格出书的,所以他们也是会把稿件交给出版社,所以该有的要求一点也不会低。你的大纲在出版社编辑那边通不过,那么在图书公司的工作人员那边同样通不过,哪怕你索要的稿酬少,图书公司方面对应的要求一定也不会降低。 + +如果你明知“图书公司和出版社的差别”,并还是和图书公司合作,这个是两厢情愿的事情。但如果对方“不主动告知”,而你在不了解两者差异的基础上同图书公司合作,那么对方也无可指摘。不过兼听则明,大家如果要出书,不妨和出版社和图书公司都去打打交道对比下。 + +## 4.如何直接同国内计算机图书的知名出版社编辑联系 + +我在清华大学出版社、机械工业出版社、北京大学出版社和电子工业出版社出过书,出书流程也比较顺畅,和编辑打交道也比较愉快。我个人无意把国内出版社划分成三六九等,但计算机行业,比较知名的出版社有清华、机工、电子工业和人邮这四家,当然其它出版社在计算机方面也出版过精品书。 + +如何同这些知名出版社的编辑直接打交道? + +- 直接到官网,一般官网上都直接有联系方式。 +- 你在博客园等地发表文章,会有人找你出书,其中除了图书公司的工作人员外,也有出版社编辑,一般出版社的编辑会直接说明身份,比如我是 xx 出版社的编辑 xx。 +- 本人也和些出版社的编辑联系过,大家如果要,我可以给。 + +那怎么去找图书公司的工作人员?一般不用主动找,你发表若干博文后,他们会主动找你。如果你细问,“您是出版社编辑还是图书公司的编辑”,他们会表明身份,如果你再细问,那么他们可能会站在图书公司的立场上解释出版社和图书公司的差异。 + +从中大家可以看到,不管你最终是否写成书,但去找知名出版社的编辑,并不难。并且,你找到后,他们还会进一步和你交流选题。 + +## 5.定选题和出书的流程 + +这里给出我和出版社编辑交流合作,最终出书的流程。 + +第一,联系上出版社编辑后,先讨论选题,你可以选择一个你比较熟悉的方向,或者你愿意专攻的方向,这个方向可以是 java 分布式组件,Spring cloud 全家桶,微服务,或者是 Python 数据分析,机器学习或深度学习等。这方面你如果有扎实的项目经验那最好,如果你当下虽然不熟悉,但你有毅力经过短时间的系统学习确保你写的内容能成系统或者能帮到别人,那么你也可以在这方面出书。 + +第二,定好选题方向后,你可以先列出大纲,比如以 Python 数据分析为例,你可以定 12 个章节,第一章讲语法,第二章讲 numpy 类等等,以此类推,你定大纲的时候,可以参考别人书的目录,从而制定你的写作内容。定好大纲以后,你可以和编辑交流,当编辑也认可这个大纲以后,就可以定出版协议。 + +对一般作者而言,出版协议其实差不多,稿酬一般是 8 个点,写作周期是和出版社协商,支付周期可能也大同小异,然后出版社会买断这本书的电子以及各种文字的版权。但如果作者是大牛,那么这些细节都可以和出版社协商。 + +然后是写书,这是很枯燥的,尤其是写最后几章的时候。我一般是工作日每天用半小时,两天周末周末用 4,5 个小时写,这样一般半年能写完一本 300 页的书,关于高效写书的技巧,后文会详细提及。 + +在写书时,一般建议每写好一个章节就交给编辑审阅,这样就不会导致太大问题的出现,而且如果是新手作者,刚开始的措辞和写作技巧都需要积累,这样出版社的编辑在开始阶段也能及时帮到作者。 + +当你写完把稿件交到编辑以后,可能会有三校三审的事情,在其中同我合作的编辑会帮助我修改语法和错别字等问题,然后会形成一个修改意见让我确认和修改。我了解下来,如果在图书公司出书,退稿的风险一般就发生在这个阶段,因为图书公司可能是会一次性地把稿件提交给出版社。但由于我会把每个章节都直接提交给出版社编辑审阅,所以即使有大问题,那么在写开始几个章节时都已经暴露并修改,所以最后的修改意见一般不会太长。也就是说,如果是直接和出版社沟通,在三校三审阶段,工作量可能未必大,我一般是在提交一本书以后,由编辑做这个事情,然后我就继续策划并开始写后一本书。 + +最后就是拿稿酬,之前已经说了,作者其实不应该对稿酬有太大的期望,也就是聊胜于无。但如果一不小心写了本销量在 5000 乃至 10000 本左右的畅销书,那么可能在一年内也能有 5 万左右的额外收益,并能在业内积累些名气。 + +## 6.出案例书比出经验书要快 + +对一些作者而言,尤其是新手作者,出书不容易,往往是开始几个章节干劲十足,后面发现问题越积越多,外加工作一忙,就不了了之了,或者用 1 年以上的时间才能完成一本书。对此,我的感受是,一本 300 到 400 书的写作周期最长是 8 个月。为了能在这个时间段里完成一本书,我对应给出的建议是,新手作者可以写案例书,别先写介绍经验类的书。 + +什么叫案例书?比如一本书里用一个大案例贯穿,系统介绍一个知识点,比如小程序开发,或者全栈开发等。或者一本书一个章节放一个案例,在一本书里给出 10 个左右 Python 深度学习方面的案例。什么叫经验类书呢?比如介绍面试经验的书就属于这这种,或者一些技术大牛写的介绍分布式高并发开发经验的书也算经验类书。 + +请注意这里并没有区分两类书的差异,只是对新手作者而言,案例书好写。因为在其中,更多的是看图说话,先给出案例(比如 Python 深度学习里的图像识别案例),然后通过案例介绍 API 的用法(比如 Python 对应库的用法),以及技术的综合要点(比如如何用 Python 库综合实现图像识别功能)。并且案例书里需要作者主观发挥的点比较少,作者无需用自己的话整理相关的经验。对新手作者而言,在组织文字介绍经验时,可能会有自己明白但说不上来的感觉,这样一方面就无法达到预期的效果,另一方面还有可能因为无法有效表述而导致进度的延迟。 + +但相反对于案例书,第一案例一般可以借鉴别人的,第二介绍现存的技术总比介绍自己的经验要容易,第三一般还有同类的书可以供作者参考,所以作者不大需要斟酌措辞,新手作者用半年到八个月的时间也有可能写完一本。当作者通过写几本书积累一定经验后,再去挑战经验类书,在这种情况下,写出来的经验类书就有可能畅销了。 + +那么具体而言,怎么高效出一本案例书呢? + +- 对整本书而言,先用少量章节介绍搭建环境和通用基本语法的内容。 +- 在写每个章节案例时,用到总分总的结构,先总体介绍下你这个案例的需求功能,以及要用的技术点,再分开介绍每个功能点的代码实现,最后再总结下这些功能点的使用要点。 +- 在介绍案例中具体代码时,也可以用到总分总的结构,即先总体介绍下这段代码的结构,再分别给出关键代码的说明,最后再给出运行效果并综述其中技术的实现要点。 + +这样的话,刚开始可以是 1 个月一个章节,写到后面熟练以后估计一个月能写两个章节,这样 8 个月完成一本书,也就不是不可能了。 + +## 7.如何在参考现有内容的基础上避免版权问题 + +写书时,一般多少都需要参考现有的代码和现有的书,但这绝不是重复劳动。比如某位作者整合了不同网站上多个案例,然后系统地讲述了 Python 数据分析,这样虽然现成资料都有,但对读者来说,就能一站式学习。同样地,比如在 Python 神经网络方面,现有 2,3 本书分别给出了若干人脸识别等若干案例,但如果你有效整合到一起,并加他人的基础上加上你的功能,那对读者来说也是有价值的。 + +这里就涉及到版权问题,先要说明,作者不能抱有任何幻想,如果出了版权问题,书没出版还好,如果已经出版了,作者不仅要赔钱,而且在业内就会有不好的名声,可谓身败名裂。但其实要避免版权问题一点也不难。 + +- 不能抄袭网上现有的内容,哪怕一句也不行。对此,作者可以在理解人家语句含义的基础上改写。不能抄袭人家书上现有的目录,更不能抄袭人家书上的话,同样一句也不行,对应的解决方法同样是在理解的基础上改写。 +- 不能抄袭 GitHub 上或者任何地方别人的代码,哪怕这个代码是开源的。对此,你可以在理解对方代码的基础上,先运行通,然后一定得自己新建一个项目,在你的项目里参考别人的代码实现你的功能,在这个过程中不能有大段的复制粘贴操作。也就是说,你的代码和别人的代码,在注释,变量命名,类名和方法名上不能有雷同的地方,当然你还可以额外加上你自己的功能。 +- 至于在写技术和案例介绍时,你就可以用你自己的话来说,这样也不会出现版权问题。 + +用了上述办法以后,作者就可以在参考现有资料的基础上,充分加上属于你的功能,写上你独到的理解,从而高效地出版属于你自己的书。 + +## 8.新手作者需要着着重避免的问题 + +在上文里详细给出了出书的流程,并通过案例书,给出了具体的习作方法,这里就特别针对新手作者,给出些需要注意的实践要点。 + +- 技术书不同于文艺书,在其中首先要确保把技能知识点讲清楚,然后再此基础上可以适当加上些风趣生动的措辞。所以对新手作者而言,甚至可以直接用朴素的文字介绍案例技术,而无需过多考虑文字上的生动性。 +- 内容需要针对初学者,在介绍技术时,从最基本的零基础讲起,别讲太深的。这里以 Python 机器学习为例,可以从什么是机器学习以及 Python 如何实现机器学习讲起,但如果首先就讲机器学习里的实践经验,就未必能确保初学者能学会。 +- 新手作者恨不得把自己知道的都写出来。这种态度非常好,但需要考虑读者的客观接受水平所以需要在写书前设置个预期效果,比如零基础的 Python 开发人员读了我的书以后至少能干活。这个预期效果别不可行,比如不能是“零基础的 Python 开发人员读了我书以后能达到 3 年开发的水准”。这样就可以根据预先制定的效果,制定写作内容,从在你的书就能更着重讲基础知识,这样读者就能有真正有收获。 + +不过话说回来,如果新手作者直接和出版社编辑联系,找个热门点的方向,并根据案例仔细讲解技术,甚至都有可能写出销量过万的畅销书。 + +## 9.总结:在国内知名出版社出书,其实是个体力活 + +可能当下,写公众号和录视频等的方式,挣钱收益要高于出书,不过话可以这样说,经营公众号和录制视频也是个长期的事情,在短时间里可能未必有收益,如果不是系统地发表内容的话,可能甚至不会有收益。所以出书可能是个非常好的前期准备工作,你靠出书系统积累了素材,靠出书整合了你的知识体系,那么在此基础上,靠公众号或者录视频挣钱可能就会事半功倍。 + +从上文里大家可以看到,在出书前期,联系出版社编辑和定选题并不难,如果要写案例书,那么在参考别人内容的基础上,要写完一般书可能也不是高不可攀的事情。甚至可以这样说,出书是个体力活,只要坚持,要出本书并不难,只是你愿不愿意坚持下去的问题。但一旦你有了属于自己的技术书,那么在找工作时,你就能自信地和面试官说你是这方面的专家,在你的视频、公众号和文字里,你也能正大光明地说,你是计算机图书的作者。更为重要的是,和名校、大厂经历一样,属于你的技术书同样是证明程序员能力的重要证据,当你通过出书有效整合了相关方面的知识体系后,那么在这方面,不管是找工作,或者是干私活,或者是接项目做,你都能理直气壮地和别人说:我能行! + + + diff --git a/docs_en/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.en.md b/docs_en/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.en.md new file mode 100644 index 00000000000..f54ccc3d77e --- /dev/null +++ b/docs_en/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.en.md @@ -0,0 +1,114 @@ +--- +title: 程序员最该拿的几种高含金量证书 +category: 技术文章精选集 +tag: + - 程序员 +--- + +证书是能有效证明自己能力的好东西,它就是你实力的象征。在短短的面试时间内,证书可以为你加不少分。通过考证来提升自己,是一种性价比很高的办法。不过,相比金融、建筑、医疗等行业,IT 行业的职业资格证书并没有那么多。 + +下面我总结了一下程序员可以考的一些常见证书。 + +## 软考 + +全国计算机技术与软件专业技术资格(水平)考试,简称“软考”,是国内认可度较高的一项计算机技术资格认证。尽管一些人吐槽其实际价值,但在特定领域和情况下,它还是非常有用的,例如软考证书在国企和事业单位中具有较高的认可度、在某些城市软考证书可以用于积分落户、可用于个税补贴。 + +软考有初、中、高三个级别,建议直接考高级。相比于 PMP(项目管理专业人士认证),软考高项的难度更大,特别是论文部分,绝大部分人都挂在了论文部分。过了软考高项,在一些单位可以内部挂证,每个月多拿几百。 + +![软考高级证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/ruankao-advanced-certification%20.jpg) + +官网地址:。 + +备考建议:[2024 年上半年,一次通过软考高级架构师考试的备考秘诀 - 阿里云开发者](https://mp.weixin.qq.com/s/9aUXHJ7dXgrHuT19jRhCnw) + +## PAT + +攀拓计算机能力测评(PAT)是一个专注于考察算法能力的测评体系,由浙江大学主办。该测评分为四个级别:基础级、乙级、甲级和顶级。 + +通过 PAT 测评并达到联盟企业规定的相应评级和分数,可以跳过学历门槛,免除筛选简历和笔试环节,直接获得面试机会。具体有哪些公司可以去官网看看: 。 + +对于考研浙江大学的同学来说,PAT(甲级)成绩在一年内可以作为硕士研究生招生考试上机复试成绩。 + +![PAT(甲级)成绩作用](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pat-enterprise-alliance.png) + +## PMP + +PMP(Project Management Professional)认证由美国项目管理协会(PMI)提供,是全球范围内认可度最高的项目管理专业人士资格认证。PMP 认证旨在提升项目管理专业人士的知识和技能,确保项目顺利完成。 + +![PMP 证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pmp-certification.png) + +PMP 是“一证在手,全球通用”的资格认证,对项目管理人士来说,PMP 证书含金量还是比较高的。放眼全球,很多成功企业都会将 PMP 认证作为项目经理的入职标准。 + +但是!真正有价值的不是 PMP 证书,而是《PMBOK》 那套项目管理体系,在《PMBOK》(PMP 考试指定用书)中也包含了非常多商业活动、实业项目、组织规划、建筑行业等各个领域的项目案例。 + +另外,PMP 证书不是一个高大上的证书,而是一个基础的证书。 + +## ACP + +ACP(Agile Certified Practitioner)认证同样由美国项目管理协会(PMI)提供,是项目管理领域的另一个重要认证。与 PMP(Project Management Professional)注重传统的瀑布方法论不同,ACP 专注于敏捷项目管理方法论,如 Scrum、Kanban、Lean、Extreme Programming(XP)等。 + +## OCP + +Oracle Certified Professional(OCP)是 Oracle 公司提供的一项专业认证,专注于 Oracle 数据库及相关技术。这个认证旨在验证和认证个人在使用和管理 Oracle 数据库方面的专业知识和技能。 + +下图展示了 Oracle 认证的不同路径和相应的认证级别,分别是核心路径(Core Track)和专业路径(Speciality Track)。 + +![OCP 认证路径](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/oracle-certified-professional.jpg) + +## 阿里云认证 + +阿里云(Alibaba Cloud)提供的专业认证,认证方向包括云计算、大数据、人工智能、Devops 等。职业认证分为 ACA、ACP、ACE 三个等级,除了职业认证之外,还有一个开发者 Clouder 认证,这是专门为开发者设立的专项技能认证。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/aliyun-professional-certification.png) + +官网地址:。 + +## 华为认证 + +华为认证是由华为技术有限公司提供的面向 ICT(信息与通信技术)领域的专业认证,认证方向包括网络、存储、云计算、大数据、人工智能等,非常庞大的认证体系。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/huawei-professional-certification.png) + +## AWS 认证 + +AWS 云认证考试是 AWS 云计算服务的官方认证考试,旨在验证 IT 专业人士在设计、部署和管理 AWS 基础架构方面的技能。 + +AWS 认证分为多个级别,包括基础级、从业者级、助理级、专业级和专家级(Specialty),涵盖多个角色和技能: + +- **基础级别**: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 等,专注于特定技术领域的深度知识和技能。 + +备考建议:[小白入门云计算的最佳方式,是去考一张 AWS 的证书(附备考经验)](https://mp.weixin.qq.com/s/xAqNOnfZ05GDRuUbAiMHIA) + +## Google Cloud 认证 + +与 AWS 认证不同,Google Cloud 认证只有一门助理级认证(Associate Cloud Engineer),其他大部分为专业级(专家级)认证。 + +备考建议:[如何备考谷歌云认证](https://mp.weixin.qq.com/s/Vw5LGPI_akA7TQl1FMygWw) + +官网地址: + +## 微软认证 + +微软的认证体系主要针对其 Azure 云平台,分为基础级别、助理级别和专家级别,认证方向包括云计算、数据管理、开发、生产力工具等。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/microsoft-certification.png) + +## Elastic 认证 + +Elastic 认证是由 Elastic 公司提供的一系列专业认证,旨在验证个人在使用 Elastic Stack(包括 Elasticsearch、Logstash、Kibana 、Beats 等)方面的技能和知识。 + +如果你在日常开发核心工作是负责 ElasticSearch 相关业务的话,还是比较建议考的,含金量挺高。 + +Currently, Elastic certification certificates are divided into four categories: Elastic Certified Engineer, Elastic Certified Analyst, Elastic Certified Observability Engineer, and Elastic Certified SIEM Specialist. + +It is recommended to take the **Elastic Certified Engineer**, which is the basic certification of Elastic Stack and examines core skills such as installation, configuration, management and maintenance of Elasticsearch clusters. + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/elastic-certified-engineer-certification.png) + +## Others + +- PostgreSQL certification: Domestic PostgreSQL certification is divided into specialist level (PCA), expert level (PCP) and master level (PCM). It mainly tests PostgreSQL database management and optimization. The price is slightly expensive and not highly recommended. +- Kubernetes certification: Cloud Native Computing Foundation (CNCF) provides several official certifications, such as Certified Kubernetes Administrator (CKA), Certified Kubernetes Application Developer (CKAD), which mainly examine Kubernetes skills and knowledge. \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.en.md b/docs_en/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.en.md new file mode 100644 index 00000000000..2d0dcc41d53 --- /dev/null +++ b/docs_en/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.en.md @@ -0,0 +1,93 @@ +--- +title: 程序员怎样出版一本技术书 +category: 技术文章精选集 +author: hsm_computer +tag: + - 程序员 +--- + +> **推荐语**:详细介绍了程序员应该如何从头开始出一本自己的书籍。 +> +> **原文地址**: + +在面试或联系副业的时候,如果能令人信服地证明自己的实力,那么很有可能事半功倍。如何证明自己的实力?最有信服力的是大公司职位背景背书,没有之一,比如在 BAT 担任资深架构,那么其它话甚至都不用讲了。 + +不过,不是每个人入职后马上就是大公司架构师,在上进的路上,还可以通过公众号,专栏博文,GitHub 代码量和出书出视频等方式来证明自己。和其它方式相比,属于自己的技术图书由于经过了国家级出版社的加持,相对更能让别人认可自己的实力,而对于一些小公司而言,一本属于自己的书甚至可以说是免面试的通行证。所以在本文里,就将和广大程序员朋友聊聊出版技术书的那些事。 + +## 1.不是有能力了再出书,而是在出书过程中升能力 + +我知道的不少朋友,是在工作 3 年内出了第一本书,有些优秀的,甚至在校阶段就出书了。 + +与之相比还有另外一种态度,不少同学可能想,要等到技术积累到一定程度再写。其实这或许就不怎么积极了,边写书,边升技术,而且写出的书对人还有帮助,这绝对可以做到的。 + +比如有同学向深入了解最近比较热门的 Python 数据分析和机器学习,那么就可以在系统性的学习之后,整理之前学习到的爬虫,数据分析和机器学习的案例,根据自己的理解,用适合于初学者的方式整理一下,然后就能出书了。这种书,对资深的人帮助未必大,但由于包含案例,对入门级的读者绝对有帮助,因为这属于现身说法。而且话说回来,如果没有出书这个动力,或者学习过程也就是浅尝辄止,或者未必能全身心地投入,有了出书这个目标,更能保证学习的效果。 + +## 2.适合初级开发,高级开发和架构师写的书 + +之前也提到了,初级开发适合写案例书,就拿 Python 爬虫数据分析机器学习题材为例,可以先找几本这方面现成的书,这些书里,或者章节内容不同,但一起集成看的话,应该可以包含这方面的内容。然后就参考别人书的思路,比如一章写爬虫,一章写 pandas,一章写 matplotlib 等等,整合起来,就可以用 若干个章节构成一本书了。总之,别人书里包含什么内容,你别照抄,但可以参考别人写哪些技术点。 + +定好章节后,再定下每个章节的小节,比如第三章讲爬虫案例,那么可以定 3.1 讲爬虫概念,3.2 讲如何搭建 Scrapy 库,3.3 讲如何开发 Scrapy 爬虫案例,通过先章再节的次序,就可以定好一本书的框架。由于是案例书,所以是先给运行通的代码,再用这些代码案例教别人入门,所以案例未必很深,但需要让初学者看了就能懂,而且按照你给出的知识体系逐步学习之后,能理解这个主题的内容。并且,能在看完你这本书以后,能通过调通你给出的爬虫,机器学习等的案例,掌握这一领域的知识,并能从事这方面的基本开发。这个目标,对初级开发而言,稍微用点心,费点时间,应该不难达到。 + +而对于高级开发和架构师而言,除了写存粹案例书以外,还可以在书里给出你在大公司里总结出来的开发经验,也就是所谓踩过的坑,比如 Python 在用 matplotlib 会图例时,在设置坐标轴方面有哪些技巧,设置时会遇到哪些常见问题,如果在书里大量包含这种经验,你的书含金量更高。 + +此外,高级开发和架构师还可以写一些技术含量更高的书,比如就讲高并发场景下的实践经验,或者 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. The text can be vivid, but overly fancy text cannot be used to cover up the lack of connotation of the book. In other words, the best-selling book must have practical content and be able to solve the practical problems of beginners. For example, in the direction of Python machine learning, write a use case that covers currently commonly used machine learning algorithms, one algorithm per chapter, and the cases include visualization, data analysis, crawlers and other elements. If the visualization effect is more attractive, the book is more likely to sell well. + +4 You must not be perfunctory. Code adjustment does not count, but strive to be concise. The explanatory text is mostly oriented to readers. In terms of content, it is ensured that readers will understand it at a glance and gain something from reading it. Maybe this point is very abstract, but I have personally experienced it after writing several books. + +## 7. In summary, publishing a book is only a milestone, and programmers should never stop on the road to progress. + +Publishing a book is not easy, because not everyone is willing to spend time and effort writing a book every night and every weekend for six months to eight months. But publishing a book is not difficult. After all, time is used up. Publishing a book is just a matter of debugging code and writing text, plus at most the cost of communicating with people. + +In fact, the income from publishing books is not high. Calculated, the monthly income is about 3k. If you cooperate with a book publishing company, it is estimated to be less, but it can at least prove your strength. But you can’t stop there after publishing a book, because there are so many talented people in big companies that they don’t even need to publish a book to prove their ability. + +So how to maximize the benefits brought by publishing a book? Firstly, this can help you get into a big factory. Having your own book during the interview is definitely a bonus. Secondly, you can use this to open columns, record videos, or open public accounts on major websites. After all, having the endorsement of a publishing house can convince others of your abilities. Third, you must use the learning methods and motivation accumulated while writing books to continue to specialize in higher-level technologies. With the skills, you can not only earn more money in big factories, but also make money more efficiently through corporate training and other methods. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/work/32-tips-improving-career.en.md b/docs_en/high-quality-technical-articles/work/32-tips-improving-career.en.md new file mode 100644 index 00000000000..60f70b9efb8 --- /dev/null +++ b/docs_en/high-quality-technical-articles/work/32-tips-improving-career.en.md @@ -0,0 +1,72 @@ +--- +title: 32 summaries to teach you how to improve your workplace experience +category: Technical Article Collection +tag: + - work +--- + +> **Recommendation**: A sharing of workplace experience by an Alibaba developer. +> +> **Original address:** + +## Shortcut to growth + +- It is good to have a humble attitude at the beginning of employment, but do not use "I am a newbie" as a psychological safety line; +- Writing a technical blog takes about two weeks, but it may be the fastest way to grow; +- Be sure to read two books: The Pyramid Principle and The Seven Habits of Highly Effective People (the name of this book is like Success Learning, but it actually talks about how to build character); +- Ask more about what and why, go back to the source and solve the problem. The problem you are trying to avoid will always be waiting for you at the next intersection; +- Don’t indulge in the false sense of security brought by busyness. The most real security is the determination and pursuit of goals; +- Don’t worry too much about temporary gains and losses. In a fair environment, suffering a loss is a blessing, not chicken soup; +- Don’t limit your thinking and skills to roles such as front-end, back-end, and testing, and position yourself as the terminator of business domain problems; +- Curiosity and love are the biggest shortcuts to growth. Long-termists will recognize the value of their work, even higher than the current recognition (KPI) given by the organization. + +## Kungfu in daily life + +- Each line of code should represent your current highest level. Small details that you think don’t matter may be hidden arrows that hurt you in the promotion field; +- The biweekly report is not a work diary. Don’t be pushed by time. At the very least, you must know what will be in the next biweekly report (driven by small goals); +- If you feel that your daily routine is filled with trivial work, unskilled work, doing chores for seniors, etc., you can try to classify the things at hand, and imagine that each category is a small grid, and the end point of these grids is your goal. In this way, every day is no longer a mechanical requirement, but a planned filling in the grid, working hard for the goal, and even adding requirements to yourself, because you can clearly see where you want to go; +- Daily words and deeds are a microscope of ability. Most people may not realize that their strength and weakness are so obvious. Don't try to cover it up needlessly, let alone get away with it. + +> The last one probably means that sometimes we care about our performance in the spotlight (duty reports, promotions, weekly reports, reports, etc.), thinking that everyone will evaluate ourselves based on this. In fact, how everyone completes business needs, helps classmates around them, and creates value on a daily basis is the basis for everyone to evaluate themselves. And what kind of characteristics each person has, partners who have worked with them three times can accurately evaluate them. Performances in the spotlight can only deceive themselves. + +## Learn to be managed + +> Superiors and supervisors generally refer to it, and development counterparts such as PD supervisors are also within the scope. + +- Don’t spread negative emotions and don’t always complain; +- It is easier to gain respect by being neither humble nor arrogant to your superiors, but do not refute each other's views in public and communicate differences in private; +- Do a good job in upward management, especially in aligning expectations and communicating about performance surprises. Both parties are actually responsible, but you are the one who is unlucky; +- Try to think from the perspective of a supervisor: + +- - In this way, many decisions that seemed unbelievable in the past can be understood; + - Don’t care about who performs, who gets the credit, etc. The importance of sharing the team’s worries and winning the trust of the supervisor is far higher than these; + - Don’t understand this principle as supremacy, which is the most disgusting thing. + +## Thinking transformation + +- Defining problems is a high-level ability. Form a closed loop of thinking as early as possible: discover problems -> define problems -> solve problems -> eliminate problems; +- Be value-oriented, do things results-oriented, and talk about things problem-oriented; +- I can’t explain clearly, but most likely it’s not because I am a practical person, but because I haven’t thought clearly about it, which is more obvious in the promotion field; +- When a person is good at solving problems in a certain scene, he may become more and more inseparable from this scene as time goes by (it is difficult to be labeled with a label, and it is even harder to tear off a label). + +## Keep emotions in check + +- Learn to control your emotions, no one will listen carefully to what an angry person is saying; +- No matter how wronged or angry you are, you must remain rational and don’t let yourself become the kind of person who needs to be coaxed; +- Only those who are confident enough will admit their problems frankly. Many times we are irritated just because the other person pointed out our deep self-esteem; +- What hurts us most is neither what others do nor our own mistakes, but our response to them. + +## Become a Leader + +> Managers have subordinates, and Leaders have followers. You don’t need many managers, but everyone can be a Leader. + +- The person who convinces you and is willing to follow you is not the manager in his position, but the person who helps you. The same is true if you want to convince others; +- Don’t make negative comments about people easily. Evaluations based on one-sided knowledge may be inaccurate, and inadvertent spreading will cause great trouble to the other party; +- Leaders will have a particularly painful life if they do not agree with the company’s mission, vision, and values; +- Don’t deny your teammates when times are difficult, and give more timely and positive feedback; +- The most important thing for a captain is not to build a ship, but to inspire sailors' yearning for the sea; +- The leader’s natural responsibility is to keep the team alive. The only way is to achieve the goals of superiors, bosses, and company operators. The more difficult the time, the more obvious it becomes; +- The leader's important responsibility is to identify what the team needs to do, and to strengthen the belief so that everyone can do it. The more difficult the time, the more determined it is; +- A leader should make everyone he meets feel important and needed. + + \ No newline at end of file diff --git a/docs_en/high-quality-technical-articles/work/employee-performance.en.md b/docs_en/high-quality-technical-articles/work/employee-performance.en.md new file mode 100644 index 00000000000..5280a4ce53d --- /dev/null +++ b/docs_en/high-quality-technical-articles/work/employee-performance.en.md @@ -0,0 +1,133 @@ +--- +title: 聊聊大厂的绩效考核 +category: 技术文章精选集 +tag: + - 工作 +--- + +> **内容概览**: +> +> - 在大部分公司,绩效跟你的年终奖、职级晋升、薪水涨幅等等福利是直接相关的。 +> - 你的上级、上上级对你的绩效拥有绝对的话语权,这是潜规则,放到任何公司都是。成年人的世界,没有绝对的公平,绩效考核尤为明显。 +> - 提升绩效的打法: +> - 短期打法:找出 1-2 件事,体现出你的独特价值(抓关键事件)。 +> - 长期打法:通过一步步信任的建立,成为团队的核心人员或者是老板的心腹,具备不可替代性。 +> +> **原文地址**: + +在新公司度过了一个完整的 Q3 季度,被打了绩效,也给下属打了绩效,感慨颇深。 + +今天就好好聊聊**大厂打工人最最关心的「绩效考核」**,谈谈它背后的逻辑以及潜规则,摸清楚了它,你在大厂这片丛林里才能更好的生存下去。 + +## 大厂的绩效到底有多重要? + +先从公司角度,谈谈为什么需要绩效考核? + +有一个著名的管理者言论,即:企业战略的上三路和下三路。 + +> 上三路是使命、愿景、价值观,下三路是组织、人才、KPI。下三路需要确保上三路能执行下去,否则便是空谈。那怎么才能达成呢? + +马老板在湖畔大学的课堂上,对底下众多 CEO 学员说,“只能靠 KPI。没有 KPI,一切都是空话,组织和公司是不会进步的”。 + +所以,KPI 一般是用来承接企业战略的。身处大厂的打工者们,也能深深感受到:每个季度的 KPI 是如何从大 Boss、到 Boss、再到基层,一层层拆解下来的,最终让所有人朝着一个方向行动,这便是 KPI 对于公司的意义。 + +然鹅,并非每个员工都会站在 CEO 的高度去理解 KPI 的价值,大家更关注的是 KPI 对于我个人来说到底有什么意义? + +在互联网大厂,每家公司都会设定一套绩效考核体系,字节用的是 OKR,阿里用的是 KPI,通常都是「271」 制度,即: + +> 20% 的比例是 A+ 和 A,对应明星员工。 +> +> 70% 的比例是 B,对应普通员工。 +> +> 10% 的比例是 C 和 C-,对应需要绩效改进或者淘汰的员工。 + +有了三六九等,然后才有了利益分配。 + +**在大厂,绩效结果跟奖金、晋升、薪水涨幅、股票授予是直接相关的。在内卷的今天,甚至可以直接划上等号。** + +绩效好的员工,奖金必然多,一年可能调薪两次,晋升答辩时能 PK 掉绩效一般的人,职级低的人甚至可以晋升免试。 + +而绩效差的人,有可能一年白干,甚至走人(大厂的末尾淘汰是不成文的规定)。 + +总之,你能想到的直接利益都和「绩效」息息相关。所以,在大厂这片高手众多的丛林里,多琢磨下绩效背后的逻辑,既是生存之道,更是一技之长。 + +## 你是怎么看待绩效的? + +凡是用来考核人的规则,大部分人在潜意识里都想去突破它,而不是被束缚。 + +至少在我刚工作的前几年,看着身边有些同事因为背个 C 黯然离开的时候,觉得绩效考核就是一个冷血的管理工具。 + +尤其遇到自己看不上的领导时,对于他给我打的绩效,其实也是很不屑的。 + +到今天,实在见过太多的反面案例了,自己也踩过一些坑,逐渐认识到:当初的想法除了让自己心里爽一点,好像起不到任何作用,甚至会让我的工作方式变形。 + +当思维方式变了,也就改变了我对绩效的态度,至少有两点我认为是打工人需要看清的。 + +**第一,你的上级、上上级对你的绩效拥有绝对的话语权,这是潜规则,放到任何公司都是。** + +大家可以去看看身边发展特别好的人,除了有很强的个人能力以外,几乎都是善于利用规则,而不是去挑战规则的人。 + +当然,我并不是说你要一味地去跪舔你的领导,而是表达:工作中不要站在领导的对立面去做对抗,如果领导做法很过分,要么直接沟通去影响他,要么选择离开。 + +**第二,成年人的世界,没有绝对的公平,绩效考核尤为明显。** + +我所待过的团队,绩效考核还是相对公平的,虽然也存在受照顾的情况,但都是个例。 + +另外就是,技术岗的绩效考核不同于销售或者运营岗,很容易指标化。 + +需求吞吐量、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 种是长期打法:通过一步步信任的建立,成为团队的核心人员或者是老板的心腹,具备不可替代性。 + +上面两种打法都是大的思路,还有很多锦上添花的技巧,比如:加强主动汇报(抹平领导的信息差)、让关键干系人给你点赞(能影响到你领导做出绩效决策的人)。 + +## 写在最后 + +有人的地方就有江湖,有江湖就一定有规则,大厂平面看似平静,其实在绩效考核、晋升等利益点面前,都是一场厮杀。 + +当大家攻山头的能力都很强时,**到底做成什么样才算做好了?**当你弄清楚了这个玄机,职场也就看透了。 + +如果这篇文章让你有一点启发,来个点赞和在看呀!我是武哥,我们下期见! + + + diff --git a/docs_en/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.en.md b/docs_en/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.en.md new file mode 100644 index 00000000000..6ef44b4b9e5 --- /dev/null +++ b/docs_en/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.en.md @@ -0,0 +1,95 @@ +--- +title: 新入职一家公司如何快速进入工作状态 +category: 技术文章精选集 +tag: + - 工作 +--- + +> **推荐语**:强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面! +> +> **原文地址**: + +![新入职一家公司如何快速进入状态](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) + +一年一度的金三银四跳槽大戏即将落幕,相信很多跳槽的小伙伴们已经找到了心仪的工作,即将或已经有了新的开始。 + +相信有过跳槽经验的小伙伴们都知道,每到一个新的公司面临的可能都是新的业务、新的技术、新的团队……这些可能会打破你原来工作思维、编码习惯、合作方式…… + +而于公司而言,又不能给你几个月的时间去慢慢的熟悉。这个时候,如何快速进入工作状态,尽快发挥自己的价值是非常重要的。 + +有些人可能会很幸运,入职的公司会有完善的流程与机制,通过一带一、各种培训等方式可以在短时间内快速的让新人进入工作状态。有些人可能就没有那么幸运了,就比如我在几年前跳槽进入某厂的时候,当时还没有像我们现在这么完善的带新人融入的机制,又赶上团队最忙的一段时间,刚一入职的当天下午就让给了我几个线上问题去排查,也没有任何的文档和培训。遇到情况,很多人可能会因为难以快速适应,最终承受不起压力而萌生退意。 + +![bad175e3a380bea.](https://hunter-picgos.oss-cn-shanghai.aliyuncs.com/picgo/bad175e3a380bea..jpg) + +那么,**我们应该如何去快速的让自己进入工作状态,适应新的工作节奏呢?** + +新的工作面对着一堆的代码仓库,很多人常常感觉无从下手。但回顾一下自己过往的工作与项目的经验,我们可以发现它们有着异曲同工之处。当开始一个新的项目,一般会经历几个步骤:需求->设计->开发->测试->发布,就这么循环往复,我们完成了一个又一个的项目。 + +![项目流程](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/image-20220704191430466.png) + +而在这个过程中主要有四个方面的知识那就是业务、技术、项目与团队贯穿始终。新入职一家公司,我们第一阶段的目标就是要具备能够跟着团队做项目的能力,因此我们所应尽快掌握的知识点也要从这四个方面入手。 + +## 业务 + +很多人可能会认为作为一个技术人,最应该了解的不应该是技术吗?于是他们在进入一家公司后,就迫不及待的研究起来了一些技术文档,系统架构,甚至抱起来源代码就开始“啃”,如果你也是这么做的,那就大错特错了!在几乎所有的公司里,技术都是作为一个工具存在的,虽然它很重要,但是它也是为了承载业务所存在的,技术解决了如何做的问题,而业务却告诉我们,做什么,为什么做。一旦脱离了业务,那么技术的存在将毫无意义。 + +想要了解业务,有两个非常重要的方式 + +**一是靠问** + +如果你加入的团队,有着完善的业务培训机制,详尽的需求文档,也许你不需要过多的询问就可以了解业务,但这只是理想中的情况,大多数公司是没有这个条件的。因此我们只能靠问。 + +这里不得不提的是,作为一个新人一定要有一定的脸皮厚度,不懂就要问。我见过很多新人会因为内向、腼腆,遇到疑问总是不好意思去问,这导致他们很长一段时间都难以融入团队、承担更重要的责任。不怕要怕挨训、怕被怼,而且我相信绝对多数的程序员还是很好沟通的! + +**二是靠测试** + +我认为测试绝对是一个人快速了解团队业务的方式。通过测试我们可以走一走自己团队所负责项目的整体流程,如果遇到自己走不下去或想不通的地方及时去问,在这个过程中我们自然而然的就可以快速的了解到核心的业务流程。 + +在了解业务的过程中,我们应该注意的是不要让自己过多的去追求细节,我们的目的是先能够整体了解业务流程,我们面向哪些用户,提供了哪些服务…… + +## 技术 + +在我们初步了解完业务之后,就该到技术了,也许你已经按捺不住翻开源代码的准备了,但还是要先提醒你一句先不要着急。 + +这个时候我们应该先按照自己了解到的业务,结合自己过往的工作经验去思考一下如果是自己去实现这个系统,应该如何去做?这一步很重要,它可以在后面我们具体去了解系统的技术实现的时候去对比一下与自己的实现思路有哪些差异,为什么会有这些差异,哪些更好,哪些不好,对于不好我们可以提出自己的意见,对于更好的我们可以吸收学习为己用! + +接下来,我们就是要了解技术了,但也不是一上来就去翻源代码。 **应该按照从宏观到细节,由外而内逐步地对系统进行分析。** + +首先,我们应该简单的了解一下 **自己团队/项目的所用到的技术栈** ,Java 还是.NET、亦或是多种语言并存,项目是前后端分离还是服务端全包,使用的数据库是 MySQL 还是 PostgreSQL……,这样我们可能会对所用到的技术和框架,以及自己所负责的内容有一定的预期,这一点有的人可能在面试的时候就会简单了解过。 + +下一步,我们应该了解的是 **系统的宏观业务架构** 。自己的团队主要负责哪些系统,每个系统又主要包含哪些模块,又与哪些外部系统进行交互……对于这些,最好可以通过流程图或者思维导图等方式整理出来。 + +然后,我们要做的是看一下 **自己的团队提供了哪些对外的接口或者服务** 。每个接口和服务所提供功能是什么。这一点我们可以继续去测试自己的系统,这个时候我们要看一看主要流程中主要包含了哪些页面,每个页面又调用了后端的哪些接口,每个后端接口又对应着哪个代码仓库。(如果是单纯做后端服务的,可以看一下我们提供了哪些服务,又有哪些上游服务,每个上游服务调用自己团队的哪些服务……),同样我们应该用画图的形式整理出来。 + +接着,我们要了解一下 **自己的系统或服务又依赖了哪些外部服务** ,也就是说需要哪些外部系统的支持,这些服务也许是团队之外、公司之外,也可能是其他公司提供的。这个时候我们可以简单的进入代码看一下与外部系统的交互是怎么做的,包括通讯框架(REST、RPC)、通讯协议…… + +到了代码层面,我们首先应该了解每个模块代码的层次结构,一个模块分了多少层,每个层次的职责是什么,了解了这个就对系统的整个设计有了初步的概念,紧接着就是代码的目录结构、配置文件的位置。 + +最后,我们可以寻找一个示例,可以是一个接口,一个页面,让我们的思路跟随者代码的运行的路线,从入参到出参,完整的走一遍来验证一下我们之前的了解。 + +到了这里我们对于技术层面的了解就可以先告一段落了,我们的目的知识对系统有一个初步的认知,更细节的东西,后面我们会有大把的时间去了解 + +## 项目与团队 + +上面我们提到,新入职一家公司,第一阶段的目标是有跟着团队做项目的能力,接下来我们要了解的就是项目是如何运作的。 + +我们应该把握从需求设计到代码编写入库最终到发布上线的整个过程中的一些关键点。例如项目采用敏捷还是瀑布的模式,一个迭代周期是多长,需求的来源以及展现形式,有没有需求评审,代码的编写规范是什么,编写完成后如何构建,如何入库,有没有提交规范,如何交付测试,发布前的准备是什么,发布工具如何使用…… + +关于项目我们只需要观察同事,或者自己亲身经历一个迭代的开发,就能够大概了解清楚。 + +在了解项目运作的同时,我们还应该去了解团队,同样我们应该先从外部开始,我们对接了哪些外部团队,比如需求从哪里来,是否对接公司外部的团队,提供服务的上游团队有哪些,依赖的下游团队有哪些,团队之间如何沟通,常用的沟通方式是什么…… + +接下来则是团队内部,团队中有哪些角色,每个人的职责是什么,这样遇到问题我们也可以清楚的找到对应的同事寻求帮助。是否有一些定期的活动与会议,例如每日站会、周例会,是否有一些约定俗成的规矩,是否有一些内部评审,分享机制…… + +## 总结 + +新入职一家公司,面临新的工作挑战,能够尽快进入工作状态,实现自己的价值,将会给你带来一个好的开始。 + +作为一个程序员,能够尽快进入工作状态,意味着我们首先应该具备跟着团队做项目的能力,这里我站在了一个后端开发的角度上从业务、技术、项目与团队四个方面总结了一些方法和经验。 + +关于如何快速进入工作状态,如果你有好的方法与建议,欢迎在评论区留言。 + +最后我们用一张思维导图来回顾一下这篇文章的内容。如果你觉得这篇文章对你有所帮助,可以关注文末公众号,我会经常分享一些自己成长过程中的经验与心得,与大家一起学习与进步。 + + + diff --git a/docs_en/home.en.md b/docs_en/home.en.md new file mode 100644 index 00000000000..bcaa0da49e6 --- /dev/null +++ b/docs_en/home.en.md @@ -0,0 +1,426 @@ +--- +icon: creative +title: JavaGuide (Java Learning & Interview Guide) +--- + +::: tip Friendly reminder + +- **Interview Special Edition**: Friends who are preparing for Java interviews may consider the special interview edition: **["Java Interview Guide North"](./zhuanlan/java-mian-shi-zhi-bei.md)** (high quality, specially created for interviews, and consumed with JavaGuide). +- **Knowledge Planet**: Exclusive interview booklet/one-on-one communication/resume modification/exclusive job search 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, be sure to make sure you really need to join again). +- **Usage Suggestions**: Skilled interviewers dig out technical issues based on project experience. Be sure not to memorize technical eight-part essays by rote! For detailed learning suggestions, please refer to: [JavaGuide usage suggestions](./javaguide/use-suggestion.md). +- **Ask for a Star**: If you think the content of JavaGuide is helpful to you, please click a free Star. This is the greatest encouragement to me. Thank you for walking together and encouraging each other! Portal: [GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide). +- **Reprint Notice**: All the following articles are original to JavaGuide unless stated as reprinted at the beginning of the article. Please indicate the source at the beginning of the article for reprinting. If malicious plagiarism/transportation is discovered, legal weapons will be used to protect one's rights. Let us maintain a good technical creation environment together! + +::: + +## Java + +### Basics + +**Knowledge points/Summary of interview questions**: (Must read: +1: ): + +- [Java basic common knowledge points & summary of interview questions (Part 1)](./java/basis/java-basic-questions-01.md) +- [Java Basic Common Knowledge Points & Summary of Interview Questions (Part 2)](./java/basis/java-basic-questions-02.md) +- [Java basic common knowledge points & summary of interview questions (Part 2)](./java/basis/java-basic-questions-03.md) + +**Detailed explanation of important knowledge points**: + +- [Why is there only passing by value in Java? ](./java/basis/why-there-only-value-passing-in-java.md) +- [Java serialization detailed explanation](./java/basis/serialization.md) +- [Generics & wildcards detailed explanation](./java/basis/generics-and-wildcards.md) +- [Detailed explanation of Java reflection mechanism](./java/basis/reflection.md) +- [Detailed explanation of Java proxy mode](./java/basis/proxy.md) +- [BigDecimal detailed explanation](./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) +- [Java syntactic sugar detailed explanation](./java/basis/syntactic-sugar.md) + +### Collection + +**Summary of knowledge points/interview questions**: + +- [Java Collection Common Knowledge Points & Summary of Interview Questions (Part 1)](./java/collection/java-collection-questions-01.md) (Must read: +1:) +- [Java Collection Common Knowledge Points & Summary of Interview Questions (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 core source code + expansion mechanism analysis](./java/collection/arraylist-source-code.md) +- [LinkedList core source code analysis](./java/collection/linkedlist-source-code.md) +- [HashMap core source code + underlying data structure analysis](./java/collection/hashmap-source-code.md) +- [ConcurrentHashMap core source code + underlying data structure analysis](./java/collection/concurrent-hash-map-source-code.md) +- [LinkedHashMap core source code analysis](./java/collection/linkedhashmap-source-code.md) +- [CopyOnWriteArrayList core source code analysis](./java/collection/copyonwritearraylist-source-code.md) +- [ArrayBlockingQueue core source code analysis](./java/collection/arrayblockingqueue-source-code.md) +- [PriorityQueue core source code analysis](./java/collection/priorityqueue-source-code.md) +- [DelayQueue core source code analysis](./java/collection/priorityqueue-source-code.md) + +###IO + +- [Summary of IO basic knowledge](./java/io/io-basis.md) +- [IO design pattern summary](./java/io/io-design-patterns.md) +- [IO model detailed explanation](./java/io/io-model.md) +- [NIO core knowledge summary](./java/io/nio-basis.md) + +### Concurrency + +**Knowledge points/Summary of interview questions**: (must read: +1:) + +- [Java concurrency common knowledge points & summary of interview questions (Part 1)](./java/concurrent/java-concurrent-questions-01.md) +- [Java concurrency common knowledge points & summary of interview questions (Part 2)](./java/concurrent/java-concurrent-questions-02.md) +- [Java concurrency common knowledge points & summary of interview questions (Part 2)](./java/concurrent/java-concurrent-questions-03.md) + +**Detailed explanation of important knowledge points**: + +- [Detailed explanation of optimistic lock and pessimistic lock](./java/concurrent/optimistic-lock-and-pessimistic-lock.md) +- [CAS detailed explanation](./java/concurrent/cas.md) +- [JMM (Java Memory Model) Detailed Explanation](./java/concurrent/jmm.md) +- **Thread Pool**: [Java thread pool detailed explanation](./java/concurrent/java-thread-pool-summary.md), [Java thread pool best practices](./java/concurrent/java-thread-pool-best-practices.md) +- [ThreadLocal detailed explanation](./java/concurrent/threadlocal.md) +- [Java Concurrent Container Summary](./java/concurrent/java-concurrent-collections.md) +- [Atomic atomic class summary](./java/concurrent/atomic-classes.md) +- [AQS detailed explanation](./java/concurrent/aqs.md) +- [CompletableFuture detailed explanation](./java/concurrent/completablefuture-intro.md) + +### JVM (must read :+1:) + +This part of JVM mainly refers to [JVM Virtual Machine Specification-Java8](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) and Mr. Zhou Zhiming’s ["In-depth Understanding of Java Virtual Machine (3rd Edition)"](https://book.douban.com/subject/34907497/) (strongly recommended to read it multiple times!). + +- **[Java Memory Area](./java/jvm/memory-area.md)** +- **[JVM Garbage Collection](./java/jvm/jvm-garbage-collection.md)** +- [Class file structure](./java/jvm/class-file-structure.md) +- **[Class loading process](./java/jvm/class-loading-process.md)** +- [Classloader](./java/jvm/classloader.md) +- [[To be completed] Summary of the most important JVM parameters (the translation is half complete)](./java/jvm/jvm-parameters-intro.md) +- [[Additional meal] Vernacular to introduce you to JVM](./java/jvm/jvm-intro.md)- [JDK Monitoring and Troubleshooting Tools](./java/jvm/jdk-monitoring-and-troubleshooting-tools.md) + +### New features + +- **Java 8**: [Java 8 new features summary (translation)] (./java/new-features/java8-tutorial-translate.md), [Java8 common new features summary] (./java/new-features/java8-common-new-features.md) +- [Java 9 new features overview](./java/new-features/java9.md) +- [Java 10 new features overview](./java/new-features/java10.md) +- [Java 11 new features overview](./java/new-features/java11.md) +- [Java 12 & 13 new features overview](./java/new-features/java12-13.md) +- [Java 14 & 15 new features overview](./java/new-features/java14-15.md) +- [Java 16 new features overview](./java/new-features/java16.md) +- [Java 17 new features overview](./java/new-features/java17.md) +- [Java 18 new features overview](./java/new-features/java18.md) +- [Java 19 new features overview](./java/new-features/java19.md) +- [Java 20 new features overview](./java/new-features/java20.md) +- [Java 21 new features overview](./java/new-features/java21.md) +- [Java 22 & 23 new features overview](./java/new-features/java22-23.md) +- [Java 24 new features overview](./java/new-features/java24.md) +- [Java 25 new features overview](./java/new-features/java25.md) + +## Computer Basics + +### Operating System + +- [Common operating system knowledge points & summary of interview questions (Part 1)](./cs-basics/operating-system/operating-system-basic-questions-01.md) +- [Common operating system knowledge points & summary of interview questions (Part 2)](./cs-basics/operating-system/operating-system-basic-questions-02.md) +- **Linux**: + - [Summary of essential Linux basic knowledge for back-end programmers](./cs-basics/operating-system/linux-intro.md) + - [Summary of basic knowledge of Shell programming](./cs-basics/operating-system/shell-intro.md) + +### Network + +**Summary of knowledge points/interview questions**: + +- [Common computer network knowledge points & summary of interview questions (Part 1)](./cs-basics/network/other-network-questions.md) +- [Common computer network knowledge points & summary of interview questions (Part 2)](./cs-basics/network/other-network-questions2.md) +- [Summary of the contents of "Computer Network" by Teacher Xie Xiren (supplement)](./cs-basics/network/computer-network-xiexiren-summary.md) + +**Detailed explanation of important knowledge points**: + +- [Detailed explanation of OSI and TCP/IP network layering model (basic)](./cs-basics/network/osi-and-tcp-ip-model.md) +- [Summary of common protocols in the application layer (application layer)](./cs-basics/network/application-layer-protocol.md) +- [HTTP vs HTTPS (application layer)](./cs-basics/network/http-vs-https.md) +- [HTTP 1.0 vs HTTP 1.1 (application layer)](./cs-basics/network/http1.0-vs-http1.1.md) +- [HTTP common status codes (application layer)](./cs-basics/network/http-status-codes.md) +- [DNS Domain Name System Detailed Explanation (Application Layer)](./cs-basics/network/dns.md) +- [TCP three-way handshake and four-way wave (transport layer)](./cs-basics/network/tcp-connection-and-disconnection.md) +- [TCP Transmission Reliability Guarantee (Transport Layer)](./cs-basics/network/tcp-reliability-guarantee.md) +- [Detailed explanation of ARP protocol (network layer)](./cs-basics/network/arp.md) +- [Detailed explanation of NAT protocol (network layer)](./cs-basics/network/nat.md) +- [Summary of common network attack means (security)](./cs-basics/network/network-attack-means.md) + +### Data structure + +**Illustrated data structure:** + +- [Linear data structure: array, linked list, stack, queue](./cs-basics/data-structure/linear-data-structure.md) +- [Graph](./cs-basics/data-structure/graph.md) +- [Heap](./cs-basics/data-structure/heap.md) +- [Tree](./cs-basics/data-structure/tree.md): Focus on [red-black tree](./cs-basics/data-structure/red-black-tree.md), B-, B+, B\* tree, LSM tree + +Other commonly used data structures: + +- [Bloom filter](./cs-basics/data-structure/bloom-filter.md) + +### Algorithm + +This part of the algorithm is very important. If you don’t know how to learn algorithms, you can read what I wrote: + +- [Algorithm learning books + resource recommendations](https://www.zhihu.com/question/323359308/answer/1545320858). +- [How to flash Leetcode?](https://www.zhihu.com/question/31092580/answer/1534887374) + +**Summary of common algorithm problems**: + +- [Summary of several common string algorithm problems](./cs-basics/algorithms/string-algorithm-problems.md) +- [Summary of several common linked list algorithm problems](./cs-basics/algorithms/linkedlist-algorithm-problems.md) +- [The sword refers to offer some programming questions](./cs-basics/algorithms/the-sword-refers-to-offer.md) +- [Top Ten Classic Sorting Algorithms](./cs-basics/algorithms/10-classical-sorting-algorithms.md) + +In addition, [GeeksforGeeks](https://www.geeksforgeeks.org/fundamentals-of-algorithms/) This website summarizes common algorithms and is relatively comprehensive and systematic. + +[![Banner](https://oss.javaguide.cn/xingqiu/xingqiu.png)](./about-the-author/zhishixingqiu-two-years.md) + +## Database + +### Basics + +- [Summary of basic database knowledge](./database/basis.md) +- [Summary of NoSQL basic knowledge](./database/nosql.md) +- [Detailed explanation of character set](./database/character-set.md) +-SQL: + - [Summary of basic knowledge of SQL syntax](./database/sql/sql-syntax-summary.md) + - [Summary of common SQL interview questions](./database/sql/sql-questions-01.md) + +###MySQL + +**Summary of knowledge points/interview questions:** + +- **[MySQL common knowledge points & summary of interview questions](./database/mysql/mysql-questions-01.md)** (must read: +1:) +- [MySQL High Performance Optimization Specification Recommendations Summary](./database/mysql/mysql-high-performance-optimization-specification-recommendations.md) + +**Important knowledge points:** + +- [MySQL index detailed explanation](./database/mysql/mysql-index.md) +- [MySQL transaction isolation level graphic and text explanation)](./database/mysql/transaction-isolation-level.md)- [Detailed explanation of MySQL's three major logs (binlog, redo log and undo log)](./database/mysql/mysql-logs.md) +- [InnoDB storage engine implementation of MVCC](./database/mysql/innodb-implementation-of-mvcc.md) +- [The execution process of SQL statements in MySQL](./database/mysql/how-sql-executed-in-mysql.md) +- [MySQL query cache detailed explanation](./database/mysql/mysql-query-cache.md) +- [MySQL execution plan analysis](./database/mysql/mysql-query-execution-plan.md) +- [MySQL auto-increment primary key must be continuous](./database/mysql/mysql-auto-increment-primary-key-continuous.md) +- [MySQL time type data storage suggestions](./database/mysql/some-thoughts-on-database-storage-time.md) +- [MySQL index invalidation caused by implicit conversion](./database/mysql/index-invalidation-caused-by-implicit-conversion.md) + +### Redis + +**Knowledge points/Summary of interview questions**: (Must read: +1: ): + +- [Redis common knowledge points & summary of interview questions (Part 1)](./database/redis/redis-questions-01.md) +- [Redis common knowledge points & summary of interview questions (Part 2)](./database/redis/redis-questions-02.md) + +**Important knowledge points:** + +- [Detailed explanation of 3 commonly used cache read and write strategies](./database/redis/3-commonly-used-cache-read-and-write-strategies.md) +- [Detailed explanation of 5 basic data structures of Redis](./database/redis/redis-data-structures-01.md) +- [Detailed explanation of three special data structures of Redis](./database/redis/redis-data-structures-02.md) +- [Detailed explanation of Redis persistence mechanism](./database/redis/redis-persistence.md) +- [Detailed explanation of Redis memory fragmentation](./database/redis/redis-memory-fragmentation.md) +- [Summary of common blocking causes in Redis](./database/redis/redis-common-blocking-problems-summary.md) +- [Detailed explanation of Redis cluster](./database/redis/redis-cluster.md) + +### MongoDB + +- [MongoDB common knowledge points & summary of interview questions (Part 1)](./database/mongodb/mongodb-questions-01.md) +- [MongoDB common knowledge points & summary of interview questions (Part 2)](./database/mongodb/mongodb-questions-02.md) + +## Search engine + +[Summary of common Elasticsearch interview questions (paid)](./database/elasticsearch/elasticsearch-questions-01.md) + +![JavaGuide official public account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) + +## Development tools + +###Maven + +- [Summary of Maven core concepts](./tools/maven/maven-core-concepts.md) +- [Maven Best Practices](./tools/maven/maven-best-practices.md) + +### Gradle + +[Summary of Gradle core concepts](./tools/gradle/gradle-core-concepts.md) (optional, Maven is still more common in China) + +### Docker + +- [Summary of Docker core concepts](./tools/docker/docker-intro.md) +- [Docker in action](./tools/docker/docker-in-action.md) + +### Git + +- [Summary of Git core concepts](./tools/git/git-intro.md) +- [Summary of GitHub practical tips](./tools/git/github-tips.md) + +## System Design + +- [Summary of common system design interview questions](./system-design/system-design-questions.md) +- [Summary of common interview questions on design patterns](./system-design/design-pattern.md) + +### Basics + +- [RestFul API concise tutorial](./system-design/basis/RESTfulAPI.md) +- [A concise tutorial on software engineering: a concise tutorial](./system-design/basis/software-engineering.md) +- [Code Naming Guide](./system-design/basis/naming.md) +- [Code Refactoring Guide](./system-design/basis/refactoring.md) +- [Unit Testing Guide](./system-design/basis/unit-test.md) + +### Commonly used frameworks + +#### Spring/SpringBoot (must read :+1:) + +**Knowledge points/Summary of interview questions**: + +- [Spring common knowledge points & interview questions summary](./system-design/framework/spring/spring-knowledge-and-questions-summary.md) +- [SpringBoot common knowledge points & interview questions summary](./system-design/framework/spring/springboot-knowledge-and-questions-summary.md) +- [Summary of common annotations in Spring/Spring Boot](./system-design/framework/spring/spring-common-annotations.md) +- [SpringBoot Getting Started Guide](https://github.com/Snailclimb/springboot-guide) + +**Detailed explanation of important knowledge points**: + +- [Detailed explanation of IoC & AOP (quick understanding)](./system-design/framework/spring/ioc-and-aop.md) +- [Spring transaction details](./system-design/framework/spring/spring-transaction.md) +- [Detailed explanation of design patterns in Spring](./system-design/framework/spring/spring-design-patterns-summary.md) +- [Detailed explanation of SpringBoot automatic assembly principles](./system-design/framework/spring/spring-boot-auto-assembly-principles.md) + +#### MyBatis + +[Summary of MyBatis common interview questions](./system-design/framework/mybatis/mybatis-interview.md) + +### Security + +#### Authentication and authorization + +- [Detailed explanation of the basic concepts of authentication and authorization](./system-design/security/basis-of-authority-certification.md) +- [Detailed explanation of JWT basic concepts](./system-design/security/jwt-intro.md) +- [Analysis of advantages and disadvantages of JWT and solutions to common problems](./system-design/security/advantages-and-disadvantages-of-jwt.md) +- [SSO single sign-on detailed explanation](./system-design/security/sso-intro.md) +- [Detailed explanation of authority system design](./system-design/security/design-of-authority-system.md) + +#### Data Security + +- [Summary of common encryption algorithms](./system-design/security/encryption-algorithms.md) +- [Summary of sensitive word filtering solutions](./system-design/security/sentive-words-filter.md) +- [Summary of data desensitization solutions](./system-design/security/data-desensitization.md) +- [Why do data verification need to be done on both the front and back ends](./system-design/security/data-validation.md) + +### Scheduled tasks + +[Detailed explanation of Java scheduled tasks](./system-design/schedule-task.md) + +### Web real-time message push[Detailed explanation of Web real-time message push](./system-design/web-real-time-message-push.md) + +## Distributed + +### Theory&Algorithm&Protocol + +- [Interpretation of CAP theory and BASE theory](./distributed-system/protocol/cap-and-base-theorem.md) +- [Interpretation of Paxos algorithm](./distributed-system/protocol/paxos-algorithm.md) +- [Raft algorithm interpretation](./distributed-system/protocol/raft-algorithm.md) +- [Gossip protocol detailed explanation](./distributed-system/protocol/gossip-protocl.md) +- [Detailed explanation of consistent hashing algorithm](./distributed-system/protocol/consistent-hashing.md) + +### RPC + +- [Summary of RPC basic knowledge](./distributed-system/rpc/rpc-intro.md) +- [Dubbo common knowledge points & interview questions summary](./distributed-system/rpc/dubbo.md) + +### ZooKeeper + +> These two articles may have overlapping content, so it is recommended to read them both. + +- [Summary of ZooKeeper related concepts (getting started)](./distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md) +- [Summary of ZooKeeper related concepts (advanced)](./distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md) + +### API Gateway + +- [Summary of basic knowledge of API gateway](./distributed-system/api-gateway.md) +- [Spring Cloud Gateway common knowledge points & summary of interview questions](./distributed-system/spring-cloud-gateway-questions.md) + +### Distributed ID + +- [Distributed ID common knowledge points & summary of interview questions](./distributed-system/distributed-id.md) +- [Distributed ID Design Guide](./distributed-system/distributed-id-design.md) + +### Distributed lock + +- [Introduction to distributed locks](https://javaguide.cn/distributed-system/distributed-lock.html) +- [Summary of common implementation solutions for distributed locks](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) + +### Distributed transactions + +[Common knowledge points about distributed transactions & summary of interview questions](./distributed-system/distributed-transaction.md) + +### Distributed Configuration Center + +[Common knowledge points of distributed configuration center & summary of interview questions](./distributed-system/distributed-configuration-center.md) + +## High performance + +### Database optimization + +- [Database read-write separation and sub-database and sub-table](./high-performance/read-and-write-separation-and-library-subtable.md) +- [Data hot and cold separation](./high-performance/data-cold-hot-separation.md) +- [Summary of common SQL optimization methods](./high-performance/sql-optimization.md) +- [Introduction to deep pagination and optimization suggestions](./high-performance/deep-pagination-optimization.md) + +### Load balancing + +[Common knowledge points on load balancing & summary of interview questions](./high-performance/load-balancing.md) + +### CDN + +[CDN (Content Delivery Network) Common Knowledge Points & Summary of Interview Questions](./high-performance/cdn.md) + +### Message queue + +- [Summary of basic knowledge of message queue](./high-performance/message-queue/message-queue.md) +- [Disruptor common knowledge points & summary of interview questions](./high-performance/message-queue/disruptor-questions.md) +- [RabbitMQ common knowledge points & summary of interview questions](./high-performance/message-queue/rabbitmq-questions.md) +- [RocketMQ common knowledge points & summary of interview questions](./high-performance/message-queue/rocketmq-questions.md) +- [Kafka common knowledge points & summary of interview questions](./high-performance/message-queue/kafka-questions-01.md) + +## High availability + +[High Availability System Design Guide](./high-availability/high-availability-system-design.md) + +### Redundant design + +[Detailed explanation of redundancy design](./high-availability/redundancy.md) + +### Current limiting + +[Detailed explanation of service current limit](./high-availability/limit-request.md) + +### Downgrade & Circuit Breaker + +[Detailed explanation of fallback and circuit breaker](./high-availability/fallback-and-circuit-breaker.md) + +### Timeout & Retry + +[Detailed explanation of timeout & retry](./high-availability/timeout-and-retry.md) + +### Cluster + +Deploy multiple copies of the same service to avoid single points of failure. + +### Disaster recovery design and multi-activity in remote locations + +**Disaster Recovery** = Disaster Recovery + Backup. + +- **Backup**: Back up several copies of all important data generated by the system. +- **Disaster Recovery**: Establish two identical systems in different places. When a system somewhere suddenly hangs up, the entire application system can be switched to another one so that the system can provide services normally. + +**Remote multi-activity** describes the deployment of services in remote locations and the services are provided to the outside world at the same time. The main difference from traditional disaster recovery design is "multi-activity", that is, all sites provide services to the outside world at the same time. Living more in different places is to cope with emergencies such as fires, earthquakes and other natural or man-made disasters. + +## Star Trends + +![Stars](https://api.star-history.com/svg?repos=Snailclimb/JavaGuide&type=Date) + +## Official account + +If you want to follow my updated articles and shared information in real time, you can follow my public account "**JavaGuide**". + +![JavaGuide official public account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) \ No newline at end of file diff --git a/docs_en/interview-preparation/how-to-handle-interview-nerves.en.md b/docs_en/interview-preparation/how-to-handle-interview-nerves.en.md new file mode 100644 index 00000000000..bee56e18399 --- /dev/null +++ b/docs_en/interview-preparation/how-to-handle-interview-nerves.en.md @@ -0,0 +1,68 @@ +--- +title: 面试太紧张怎么办? +category: 面试准备 +icon: security-fill +--- + +很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,可以说是深有体会。其实,**紧张是很正常的**——它代表你对面试的重视,也来自于对未知结果的担忧。但如果过度紧张,反而会影响你的临场发挥。 + +下面,我就分享一些自己的心得,帮大家更好地应对面试中的紧张情绪。 + +## 试着接受紧张情绪,调整心态 + +首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它: + +- **搞清楚面试的本质**:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 KPI 面试,这家公司可能压根就没有真正的招聘需求。失败的原因也可能是某些知识点、项目经验或表达方式未能充分展现出你的能力。即便这次面试未通过,也不妨碍你继续尝试其他公司,完全不慌! +- **不要害怕面试官**:很多求职者平时和同学朋友交流沟通的蛮好,一到面试就害怕了。面试官和求职者双方是平等的,以后说不定就是同事关系。也不要觉得面试官就很厉害,实际上,面试官的水平也参差不齐。他们提出的问题,可能自己也没有完全理解。 +- **给自己积极的心理暗示**:告诉自己“有点紧张没关系,这只能让我更专注,心跳加快是我在给自己打气,我一定可以回答的很好!”。 + +## 提前准备,减少不确定性 + +**不确定性越多,越容易紧张。** 如果你能够在面试前做充分的准备,很多“未知”就会消失,紧张情绪自然会减轻很多。 + +### 认真准备技术面试 + +- **优先梳理核心知识点**:比如计算基础、数据库、Java 基础、Java 集合、并发编程、SpringBoot(这里以 Java 后端方向为例)等。如果时间不够,可以分轻重缓急,有重点地复习。强烈推荐阅读一下 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html)这篇文章。 +- **精心准备项目经历**:认真思考你简历上最重要的项目(面试以前两个项目为主,尤其是第一个),它们的技术难点、业务逻辑、架构设计,以及可能被面试官深挖的点。把你的思考总结成可能出现的面试问题,并尝试回答。 + +### 模拟面试和自测 + +- **约朋友或同学互相提问**:以真实的面试场景来进行演练,并及时对回答进行诊断和反馈。 +- **线上练习**:很多平台都提供 AI 模拟面试,能比较真实地模拟面试官提问情境。 +- **面经**:平时可以多看一些前辈整理的面经,尤其是目标岗位或目标公司的面经,总结高频考点和常见问题。 +- **技术面试题自测**:在 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」 ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。其中,每一个问题都有提示和重要程度说明,非常适合用来自测。 + +[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」概览: + +![技术面试题自测篇](https://oss.javaguide.cn/javamianshizhibei/technical-interview-questions-self-test.png) + +### 多表达 + +平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。 + +我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。 + +### 多面试 + +- **先小厂后大厂**:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。 +- **积累“失败经验”**:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。 + +### 保证休息 + +- **留出充裕时间**:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。 +- **保证休息**:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。 + +## 遇到不会的问题不要慌 + +一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。 + +在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。 + +## 面试结束后的复盘 + +很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要: + +1. **记录面试中的问题**:无论回答得好坏,都把它们写下来。如果问到了一些没想过的问题,可以认真思考并在面试后补上答案。 +2. **反思自己的表现**:有没有遇到卡壳的地方?是知识没准备到还是过于紧张导致表达混乱?下次如何改进? +3. **持续完善自己的“面试题库”**:把新的问题补充进去,不断拓展自己的知识面,也逐步降低对未知问题的恐惧感。 + diff --git a/docs_en/interview-preparation/internship-experience.en.md b/docs_en/interview-preparation/internship-experience.en.md new file mode 100644 index 00000000000..92841bb61cd --- /dev/null +++ b/docs_en/interview-preparation/internship-experience.en.md @@ -0,0 +1,56 @@ +--- +title: What should I do if I don’t have internship experience in school recruitment? +category: interview preparation +icon: experience +--- + +Since the current interviews are too lengthy, for students who are hesitant about looking for an internship, I personally suggest that both undergraduates and graduate students should seek good internship opportunities before participating in campus recruitment interviews, especially internship opportunities in large factories, whether daily internships or summer internships. Of course, if internships in large factories are not available, internships in small and medium-sized factories are also acceptable. + +However, it is really hard to find internships now. There are a lot of students who failed to find internships this year, and some of them are even students from 211/985 prestigious schools. + +If we really can’t find a suitable internship, there’s nothing we can do. We should spend more time doing the following three things: + +1. Enhance project experience +2. Continue to improve your resume +3. Prepare for technical interviews + +## Reinforce project experience + +If you don’t have internship experience in the school recruitment, you will be at a disadvantage in finding a job (no way, it’s too busy), and you need to make more efforts in the project experience part to make up for it. + +It is recommended that you try your best to strengthen your project experience, improve existing projects or do more highlight projects, and try to make up for it through project experience as much as possible. + +The focus of your interview is the knowledge points related to your project experience. If your project experience is relatively simple, the interviewer will not know what to ask. In addition, there is a high probability that you will be asked about knowledge points that are not covered in your project experience but are mentioned in the skill introduction. Skills like Redis are basically necessary for interviewing Java back-end positions. I think most interviewers should ask about it. + +It is recommended to read this article on the website: [Project Experience Guide](https://javaguide.cn/interview-preparation/project-experience-guide.html). + +## **Improve resume** + +You must definitely pay attention to your resume! It is recommended to spend at least 2 to 3 days specifically improving your resume. Moreover, it will continue to be improved in the future. + +For interviewers, they will pay more attention to the following dimensions when screening resumes: + +1. **Internship/Work Experience**: See if you have good internship experience. Internship/work experience in a large company and related to the interview position is the best. +2. **Award-winning experience**: If you have an award-winning experience with relatively high value (high-profile events such as ACM, Alibaba Cloud Tianchi), it will also be a bonus point, especially for school recruitment. This type of job seeker is the target of many big companies (but it does not mean that you can enter a big company after winning an award, you still need to perform well in the interview). For social recruitment, award-winning experience plays a relatively small role, and past work experience and project experience are usually more valued. +3. **Project experience**: Project experience is very important for the interview. The interviewer will focus on it, and it is also the focus of competent interview questions. +4. **Skills matching**: See whether your skills meet the needs of the position. Before submitting your resume, be sure to confirm whether your skills profile lacks any skills required for the position you want to submit. +5. **Education**: Compared with other industries, programmer job interviews are relatively tolerant of academic qualifications. As long as you are outstanding in other aspects, you can make up for the shortcomings of academic qualifications. You have to know that in many industries, such as lawyers and finance, academic qualifications are a stepping stone. If the academic qualifications do not meet the requirements, there will be no direct interview opportunities. However, as interviews are becoming more and more demanding, some large factories, state-owned enterprises and research institutes have begun to require academic qualifications. Many positions require 211/985, and even a master's degree is required. In short, it is difficult to change academic qualifications. If the school is poor, just apply to companies that do not have clear requirements for academic qualifications and strive to improve your hard power in other aspects. + +For most job seekers, internship/work experience, project experience, and skill matching are more important. However, it is not ruled out that some companies will block people because of their academic qualifications. + +For detailed guidelines on writing a programmer resume, please refer to this article: [Programmer Resume Writing Guide (Important)](https://javaguide.cn/interview-preparation/resume-guide.html). + +## **Prepare for technical interviews** + +Before the interview, you must prepare in advance for common interview questions, which are eight-part essays: + +- What knowledge points may be involved in your interview and which knowledge points are the key points. +- What questions are frequently asked in interviews and how should you answer them during the interview? (It is strongly not recommended to memorize by rote. First: How much can you remember by memorizing this way? How long can you remember it? Second: It is difficult to persist in learning by memorizing questions!) + +For the key points of Java back-end interview review, please read this article: [What are the key points of Java back-end interview?](https://javaguide.cn/interview-preparation/key-points-of-interview.html). + +Different types of companies have different emphasis on skills requirements. For example, Tencent and Byte may pay more attention to computer basics such as networks and operating systems. Companies like Alibaba and Meituan may value your project experience and practical capabilities more. + +You must not hold the thought that the examination of eight-legged essays or basic questions is of little significance. If you review with this kind of thinking, the effect may not be very good. In fact, I personally think it is very meaningful. Eight-part essay or basic knowledge will also be frequently used in daily development. For example, if you don't understand the rejection strategy and core parameter configuration of the thread pool, you may not understand how to use the thread pool in actual projects, and problems may easily arise. Moreover, in fact, this kind of basic questions are the easiest to prepare for, while various underlying principles, system design, scenario questions, and digging into your project are the most difficult! + +My first recommendation for eight-part essay materials is my ["Java Interview Guide North"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) and [JavaGuide](https://javaguide.cn/home.html). It contains not only original eight-part essays, but also a lot of useful information that is helpful for actual development. In addition to my information, you can also go online to find some other high-quality articles and videos to watch. \ No newline at end of file diff --git a/docs_en/interview-preparation/interview-experience.en.md b/docs_en/interview-preparation/interview-experience.en.md new file mode 100644 index 00000000000..e6c27b459f6 --- /dev/null +++ b/docs_en/interview-preparation/interview-experience.en.md @@ -0,0 +1,30 @@ +--- +title: Summary of high-quality interviews (paid) +category: Knowledge Planet +icon: experience +--- + +The ancients said: "You can attack jade with stones from other mountains." Being good at learning from the successful experiences or failures of other people’s interviews can help you avoid many detours. + +In **["Java Interview Guide North"](../zhuanlan/java-mian-shi-zhi-bei.md)**'s **"Interview Chapter"**, I shared 15+ high-quality Java back-end interview articles, including those recruited from schools, some recruited from companies, some from large factories, and some from small and medium-sized factories. + +If you are a student who is not in a major, you can also find the corresponding interview experience written by students who are not in a major in these articles. + +![](https://oss.javaguide.cn/githubjuejinjihua/thinkimage-20220612185810480.png) + +Moreover, [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) also has a special topic dedicated to sharing interviews and interview questions, which will share many high-quality interviews and interview questions. + +![](https://oss.javaguide.cn/xingqiu/image-20220304120018731.png) + +![](https://oss.javaguide.cn/xingqiu/image-20220628101743381.png) + +![](https://oss.javaguide.cn/xingqiu/image-20220628101805897.png) + +Compared with interview interviews on Niuke.com or other websites, the interviews compiled in "Java Interview Guide" are of higher quality, and I will provide high-quality reference materials. + +Many students want to say: "Why not just give a specific answer?". The main reasons are as follows: + +1. The reference materials should be explained in more detail, and they can also allow you to review relevant knowledge points. +2. The reference materials given are basically my original work. If I want to improve the answers to interview questions later, I don’t need to modify the answers I wrote in the previous interview one by one (many questions in the interview are relatively similar). Of course, it is impossible for my original articles to cover every point of the interview. For answers to some of the interview questions, I have selected high-quality articles written by other technical bloggers, and the quality of the articles is very high. + + \ No newline at end of file diff --git a/docs_en/interview-preparation/java-roadmap.en.md b/docs_en/interview-preparation/java-roadmap.en.md new file mode 100644 index 00000000000..2282e19f333 --- /dev/null +++ b/docs_en/interview-preparation/java-roadmap.en.md @@ -0,0 +1,35 @@ +--- +title: Java learning route (latest version, 4w+ words) +category: interview preparation +icon: path +--- + +::: tip Important note + +This learning route maintains **annual systematic revision**, strictly synchronizes with the latest developments in the Java technology ecosystem and the recruitment market, **ensuring the timeliness and forward-lookingness of the content**. + +::: + +After a month of careful polishing, the author has comprehensively upgraded the existing learning route based on the latest requirements for Java back-end development job recruitment. This upgrade covers the addition and deletion of technology stacks, optimization of learning paths, and updates of supporting learning resources, and strives to build a knowledge system that is more in line with the growth curve of Java developers. + +Overview of bright color palettes: + +![Java Learning Roadmap PDF Overview - Bright Color Plate](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map-pdf.png) + +Dark palette overview: + +![Java Learning Roadmap PDF Overview - Dark Version](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map-pdf-dark.png) + +This may be the most thoughtful and comprehensive Java backend learning route you have ever seen. This learning route contains a total of **4w+** words, but you don’t have to worry about too much content and not being able to finish it. Based on the difficulty of learning, I will divide the necessary content suitable for finding a job in a small factory, as well as the learning path suitable for gradually improving Java back-end development capabilities. + +![Java learning roadmap](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map.png) + +For beginners, you can follow the learning routes and materials recommended in this article to study systematically; for experienced developers, you can follow this article to learn more about Java back-end development and improve your personal competitiveness. + +When looking at this learning route, it is recommended to match it with [Java Interview Key Points Summary (Important)](https://javaguide.cn/interview-preparation/key-points-of-interview.html), which can make you more purposeful in the learning process. + +Since there is too much content in this learning path, I have organized it into a PDF version (**55** pages in total) for your convenience. This PDF has two reading versions, night and day, to meet everyone's different needs. + +The method to obtain this learning route is very simple: just reply "**Route**" in the background of the public account "**JavaGuide**" to obtain it. + +![JavaGuide official public account](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) \ No newline at end of file diff --git a/docs_en/interview-preparation/key-points-of-interview.en.md b/docs_en/interview-preparation/key-points-of-interview.en.md new file mode 100644 index 00000000000..9b86c069eba --- /dev/null +++ b/docs_en/interview-preparation/key-points-of-interview.en.md @@ -0,0 +1,44 @@ +--- +title: Java后端面试重点总结 +category: 面试准备 +icon: star +--- + +::: tip 友情提示 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: + +## Java 后端面试哪些知识点是重点? + +**准备面试的时候,具体哪些知识点是重点呢?如何把握重点?** + +给你几点靠谱的建议: + +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. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。 + +看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。 + +## 如何更高效地准备八股文? + +对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。 + +我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。 + +举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。 + +**一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来,这样毫无意义!效率最低,对自身帮助也最小!** + +还要注意适当“投机取巧”,不要单纯死记八股,有些技术方案的实现有很多种,例如分布式 ID、分布式锁、幂等设计,想要完全记住所有方案不太现实,你就重点记忆你项目的实现方案以及选择该种实现方案的原因就好了。当然,其他方案还是建议你简单了解一下,不然也没办法和你选择的方案进行对比。 + +想要检测自己是否搞懂或者加深印象,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。 + +另外,准备八股文的过程中,强烈建议你花个几个小时去根据你的简历(主要是项目经历部分)思考一下哪些地方可能被深挖,然后把你自己的思考以面试问题的形式体现出来。面试之后,你还要根据当下的面试情况复盘一波,对之前自己整理的面试问题进行完善补充。这个过程对于个人进一步熟悉自己的简历(尤其是项目经历)部分,非常非常有用。这些问题你也一定要多花一些时间搞懂吃透,能够流畅地表达出来。面试问题可以参考 [Java 面试常见问题总结(2024 最新版)](https://t.zsxq.com/0eRq7EJPy),记得根据自己项目经历去深入拓展即可! + +最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。 + diff --git a/docs_en/interview-preparation/project-experience-guide.en.md b/docs_en/interview-preparation/project-experience-guide.en.md new file mode 100644 index 00000000000..3a70616c767 --- /dev/null +++ b/docs_en/interview-preparation/project-experience-guide.en.md @@ -0,0 +1,115 @@ +--- +title: 项目经验指南 +category: 面试准备 +icon: project +--- + +::: tip 友情提示 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: + +## 没有项目经验怎么办? + +没有项目经验是大部分应届生会碰到的一个问题。甚至说,有很多有工作经验的程序员,对自己在公司做的项目不满意,也想找一个比较有技术含量的项目来做。 + +说几种我觉得比较靠谱的获取项目经验的方式,希望能够对你有启发。 + +### 实战项目视频/专栏 + +在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。 + +你可以通过慕课网、哔哩哔哩、拉勾、极客时间、培训机构(比如黑马、尚硅谷)等渠道获取到适合自己的实战项目视频/专栏。 + +![慕课网实战课](https://oss.javaguide.cn/javamianshizhibei/mukewangzhiazhanke.png) + +尽量选择一个适合自己的项目,没必要必须做分布式/微服务项目,对于绝大部分同学来说,能把一个单机项目做好就已经很不错了。 + +我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。 + +我在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中也说过: + +> 个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。 +> +> 其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。 + +跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。 + +### 实战类开源项目 + +GitHub 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。 + +你可以参考 [Java 优质开源实战项目](https://javaguide.cn/open-source-project/practical-project.html "Java 优质开源实战项目") 上面推荐的实战类开源项目,质量都很高,项目类型也比较全面,涵盖博客/论坛系统、考试/刷题系统、商城系统、权限管理系统、快速开发脚手架以及各种轮子。 + +![Java 优质开源实战项目](https://oss.javaguide.cn/javamianshizhibei/javaguide-practical-project.png) + +一定要记住:**不光要做,还要改进,改善。不论是实战项目视频或者专栏还是实战类开源项目,都一定会有很多可以完善改进的地方。** + +### 从头开始做 + +自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。 + +这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。 + +### 参加各种大公司组织的各种大赛 + +如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。 + +![阿里云天池大赛](https://oss.javaguide.cn/xingqiu/up-673f598477242691900a1e72c5d8b26df2c.png) + +### 参与实际项目 + +通常情况下,你有如下途径接触到企业实际项目的开发: + +1. 老师接的项目; +2. 自己接的私活; +3. 实习/工作接触到的项目; + +老师接的项目和自己接的私活通常都是一些偏业务的项目,很少会涉及到性能优化。这种情况下,你可以考虑对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。 + +实习/工作接触到的项目类似,如果遇到一些偏业务的项目,也是要自己私下对项目进行改进优化。 + +尽量是真的对项目进行了优化,这本身也是对个人能力的提升。如果你实在是没时间去实践的话,也没关系,吃透这个项目优化手段就好,把一些面试可能会遇到的问题提前准备一下。 + +## 有没有还不错的项目推荐? + +**[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。 + +![优质 Java 实战项目推荐](https://oss.javaguide.cn/javamianshizhibei/project-experience-guide.png) + +这篇文章一共推荐了 15+ 个实战项目,有业务类的,也有轮子类的,有开源项目、也有视频教程。对于参加校招的小伙伴,我更建议做一个业务类项目加上一个轮子类的项目。 + +## 我跟着视频做的项目会被面试官嫌弃不? + +很多应届生都是跟着视频做的项目,这个大部分面试官都心知肚明。 + +不排除确实有些面试官不吃这一套,这个也看人。不过我相信大多数面试官都是能理解的,毕竟你在学校的时候实际上是没有什么获得实际项目经验的途径的。 + +大部分应届生的项目经验都是自己在网上找的或者像你一样买的付费课程跟着做的,极少部分是比较真实的项目。 从你能想着做一个实战项目来说,我觉得初衷是好的,确实也能真正学到东西。 但是,究竟有多少是自己掌握了很重要。看视频最忌讳的是被动接受,自己多改进一下,多思考一下!就算是你跟着视频做的项目,也是可以优化的! + +**如果你想真正学到东西的话,建议不光要把项目单纯完成跑起来,还要去自己尝试着优化!** + +简单说几个比较容易的优化点: + +1. **全局异常处理**:很多项目这方面都做的不是很好,可以参考我的这篇文章:[《使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!》](https://mp.weixin.qq.com/s/Y4Q4yWRqKG_lw0GLUsY2qw) 来做优化。 +2. **项目的技术选型优化**:比如使用 Guava 做本地缓存的地方可以换成 **Caffeine** 。Caffeine 的各方面的表现要更加好!再比如 Controller 层是否放了太多的业务逻辑。 +3. **数据库方面**:数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离? +4. **缓存**:项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度? +5. **安全**:项目是否存在安全问题? +6. …… + +另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看: 。 + +最后,**再给大家推荐一个 IDEA 优化代码的小技巧,超级实用!** + +分析你的代码:右键项目-> Analyze->Inspect Code + +![](https://oss.javaguide.cn/xingqiu/up-651672bce128025a135c1536cd5dc00532e.png) + +扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。 + +![](https://oss.javaguide.cn/xingqiu/up-05c83b319941995b07c8020fddc57f26037.png) + +并且,你还可以自定义检查规则。 + +![](https://oss.javaguide.cn/xingqiu/up-6b618ad3bad0bc3f76e6066d90c8cd2f255.png) + diff --git a/docs_en/interview-preparation/resume-guide.en.md b/docs_en/interview-preparation/resume-guide.en.md new file mode 100644 index 00000000000..02c4d85fed9 --- /dev/null +++ b/docs_en/interview-preparation/resume-guide.en.md @@ -0,0 +1,295 @@ +--- +title: 程序员简历编写指南 +category: 面试准备 +icon: jianli +--- + +::: tip 友情提示 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: + +## 前言 + +一份好的简历可以在整个申请面试以及面试过程中起到非常重要的作用。 + +**为什么说简历很重要呢?** 我们可以从下面几点来说: + +**1、简历就像是我们的一个门面一样,它在很大程度上决定了是否能够获得面试机会。** + +- 假如你是网申,你的简历必然会经过 HR 的筛选,一张简历 HR 可能也就花费 10 秒钟左右看一下,然后决定你能否进入面试。 +- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 + +另外,就算你通过了第一轮的筛选获得面试机会,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 + +**2、简历上的内容很大程度上决定了面试官提问的侧重点。** + +- 一般情况下你的简历上注明你会的东西才会被问到(Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些算是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 Redis 的一些问题,再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。 +- 技能熟练度在很大程度上也决定了面试官提问的深度。 + +在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。一般情况下,技术能力和学习能力比较厉害的,写出来的简历也比较棒! + +## 简历模板 + +简历的样式真的非常非常重要!!!如果你的简历样式丑到没朋友的话,面试官真的没有看下去的欲望。一天处理上百份的简历的痛苦,你不懂! + +我这里的话,推荐大家使用 Markdown 语法写简历,然后再将 Markdown 格式转换为 PDF 格式后进行简历投递。如果你对 Markdown 语法不太了解的话,可以花半个小时简单看一下 Markdown 语法说明: 。 + +下面是我收集的一些还不错的简历模板: + +- 适合中文的简历模板收集(推荐,开源免费): +- 木及简历(推荐,部分免费) : +- 简单简历(推荐,部分免费): +- 极简简历(免费): +- Markdown 简历排版工具(开源免费): +- 站长简历(收费,支持 AI 生成): +- typora+markdown+css 自定义简历模板 : +- 超级简历(部分收费) : + +上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(校招生 2 页之内,社招生 3 页之内,记得精炼语言,不要过多废话)。 + +再总结几点 **简历排版的注意事项**: + +- 尽量简洁,不要太花里胡哨。 +- 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。 +- 中文和数字英文之间加上空格的话看起来会舒服一点。 + +另外,知识星球里还有真实的简历模板可供参考,地址: (需加入[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)获取)。 + +![](https://oss.javaguide.cn/javamianshizhibei/image-20230918073550606.png) + +## 简历内容 + +### 个人信息 + +- 最基本的 :姓名(身份证上的那个)、年龄、电话、籍贯、联系方式、邮箱地址 +- 潜在加分项 : Github 地址、博客地址(如果技术博客和 Github 上没有什么内容的话,就不要写了) + +示例: + +![](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 ,你自身的实力必须要强。 + +## 简历修改 + +So far, I have helped at least **6000+** golfers with free resume modification services. Due to limited personal energy, resume modification is limited to readers who have joined Planet. If you need help reviewing your resume, you can join [**JavaGuide Official Knowledge Planet**](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html#%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B9) (click the link to view a detailed introduction). + +![img](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) + +Although the fee is only one percent of the training class/training camp, the content in Knowledge Planet is of higher quality and the services provided are more comprehensive. It is very suitable for students who are preparing for Java interviews and learning Java. + +The following are some of the services provided by Planet (click on the image below to get a detailed introduction to Knowledge Planet): + +[![Planet Service](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) + +Here is another limited time exclusive coupon: + +![Knowledge Planet 30 yuan coupon](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) \ No newline at end of file diff --git a/docs_en/interview-preparation/self-test-of-common-interview-questions.en.md b/docs_en/interview-preparation/self-test-of-common-interview-questions.en.md new file mode 100644 index 00000000000..f70771468c2 --- /dev/null +++ b/docs_en/interview-preparation/self-test-of-common-interview-questions.en.md @@ -0,0 +1,19 @@ +--- +title: Self-test on common interview questions (paid) +category: Knowledge Planet +icon: security-fill +--- + +Before the interview, it is strongly recommended that you take a self-test with common interview questions to check your mastery. This is a very practical tip for preparing for technical interviews. + +In the **"Technical Interview Questions Self-Assessment"** of **["Java Interview Guide"](../zhuanlan/java-mian-shi-zhi-bei.md)**, I summarized the most common interview questions on the most important knowledge points in Java interviews and presented them in the way of interview questions. + +![](https://oss.javaguide.cn/xingqiu/image-20220628102643202.png) + +I will give the importance of each interview question for self-test, so that you can selectively self-test according to your own situation when time is tight. Moreover, I will also give 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) + + \ No newline at end of file diff --git a/docs_en/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.en.md b/docs_en/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.en.md new file mode 100644 index 00000000000..ffe5c5db6e1 --- /dev/null +++ b/docs_en/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.en.md @@ -0,0 +1,207 @@ +--- +title: 如何高效准备Java面试? +category: 知识星球 +icon: path +--- + +::: tip 友情提示 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +::: + +你身边是否有这样的朋友:编程能力比你强,求职结果却不如你?其实**技术好≠面试能过** —— 如今的面试早已不是 “会写代码就行”,不做准备就去面,大概率是 “撞枪口”。 + +我们大多是普通开发者,没有顶会论文或竞赛大奖加持,面对 “面试造火箭,工作拧螺丝钉” 的常态,只能靠扎实准备突围。但准备面试不等于耍小聪明或者死记硬背面试题。 **一定不要对面试抱有侥幸心理。打铁还需自身硬!** 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习! + +这篇文章就从宏观视角,带你搞懂程序员该如何系统准备面试:从求职导向学习,到简历优化、面试冲刺,帮你少走弯路,高效拿下心仪 offer。 + +## 尽早以求职为导向来学习 + +我是比较建议还在学校的同学尽可能早一点以求职为导向来学习的。 + +**这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。** + +但是!不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”! + +我在之前的很多次分享中都强调过:**一定要用心学习计算机基础知识!操作系统、计算机组成原理、计算机网络真的不是没有实际用处的学科!!!** + +你会发现大厂面试你会用到,以后工作之后你也会用到。我分别列举 2 个例子吧! + +- **面试中**:像字节、腾讯这些大厂的技术面试以及几乎所有公司的笔试都会考操作系统相关的问题。 +- **工作中**:在实际使用缓存的时候,软件层次而言的缓存思想,则是源自数据库速度、Redis(内存中间件)速度、本地内存速度之间的不匹配;而在计算机存储层次结构设计中,我们也能发现同样的问题及缓存思想的使用:内存用于解决磁盘访问速度过慢的问题,CPU 用三级缓存缓解寄存器和内存之间的速度差异。它们面临的都是同一个问题(速度不匹配)和同一个思想,那么计算机先驱者在存储层次结构设计上对缓存性能的优化措施,同样也适用于软件层次缓存的性能优化。 + +**如何求职为导向学习呢?** 简答来说就是:根据招聘要求整理一份目标岗位的技能清单,然后按照技能清单去学习和提升。 + +1. 你首先搞清楚自己要找什么工作 +2. 然后根据招聘岗位的要求梳理一份技能清单 +3. 根据技能清单写好最终的简历 +4. 最后再按照简历的要求去学习和提升。 + +这其实也是 **以终为始** 思想的运用。 + +**何为以终为始?** 简单来说,以终为始就是我们可以站在结果来考虑问题,从结果出发,根据结果来确定自己要做的事情。 + +你会发现,其实几乎任何领域都可以用到 **以终为始** 的思想。 + +## 了解投递简历的黄金时间 + +面试之前,你肯定是先要搞清楚春招和秋招的具体时间的。 + +正所谓金三银四,金九银十,错过了这个时间,很多公司都没有 HC 了。 + +**秋招一般 7 月份就开始了,大概一直持续到 9 月底。** + +**春招一般 3 月份就开始了,大概一直持续到 4 月底。** + +很多公司(尤其大厂)到了 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://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) (配合 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. Don’t hold the thought that the test of eight-legged essay or basic questions is of little significance. If you review with this kind of thinking, the effect may not be very good. In fact, I personally think it is very meaningful. Eight-part essay or basic knowledge will also be frequently used in daily development. For example, if you don't understand the rejection strategy and core parameter configuration of the thread pool, you may not understand how to use the thread pool in actual projects, and problems may easily arise. +5. The hand-shredded algorithm is the standard for current technical interviews, so prepare as early as possible! +6. Job matching is important. Campus recruitment is usually more tolerant of the research direction of your project experience. Even if your project experience has nothing to do with the specific business of the corresponding company, the impact is actually not big. It's different when it comes to social recruitment. After all, the company wants to recruit people who can come and work directly. If you have relevant experience, it will be easier for the company. + +7. Review the interview promptly after the interview. The interview is like a new journey, failure and victory are normal. Therefore, I advise you not to be discouraged or lose your morale because of failed interviews. Don’t be complacent just because you passed the interview, a better future will be waiting for you, keep up the good work! \ No newline at end of file diff --git a/docs_en/java/basis/bigdecimal.en.md b/docs_en/java/basis/bigdecimal.en.md new file mode 100644 index 00000000000..2fb2bc34542 --- /dev/null +++ b/docs_en/java/basis/bigdecimal.en.md @@ -0,0 +1,368 @@ +--- +title: BigDecimal detailed explanation +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: BigDecimal, floating point precision, decimal arithmetic, compareTo, rounding rules, RoundingMode, divide, Alibaba specifications + - - meta + - name: description + content: Explain the usage scenarios and core API of BigDecimal, solve floating-point precision issues, and summarize common rounding rules and best practices. +--- + +"Alibaba Java Development Manual" mentions: "In order to avoid loss of precision, you can use `BigDecimal` to perform floating-point operations." + +Is there a risk of precision loss in floating-point number operations? Indeed it will! + +Sample code: + +```java +float a = 2.0f - 1.9f; +float b = 1.8f - 1.7f; +System.out.println(a);// 0.100000024 +System.out.println(b);// 0.099999905 +System.out.println(a == b); // false +``` + +**Why is there a risk of precision loss when operating floating point numbers `float` or `double`? ** + +This has a lot to do with the computer's mechanism for saving decimals. We know that the computer is binary, and when the computer represents a number, the width is limited. When the infinite loop of decimals is stored in the computer, it can only be truncated, which will lead to the loss of decimal precision. This also explains why decimal decimals cannot be accurately represented in binary. + +For example, 0.2 in decimal system cannot be accurately converted into binary decimal: + +```java +// The process of converting 0.2 to a binary number is to multiply by 2 until there are no decimals. +// In this calculation process, the integer part obtained is arranged from top to bottom and is 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 (loop occurs) +... +``` + +For more information about floating point numbers, it is recommended to read the article [Computer System Basics (4) Floating Point Numbers](http://kaito-kidd.com/2018/08/08/computer-system-float-point/). + +## BigDecimal Introduction + +`BigDecimal` can perform operations on decimals without losing precision. + +Usually, most business scenarios that require precise decimal operation results (such as scenarios involving money) are done through `BigDecimal`. + +"Alibaba Java Development Manual" mentions: ** To judge the equivalence between floating point numbers, basic data types cannot be compared with ==, and packaged data types cannot be judged with equals. ** + +![](https://oss.javaguide.cn/javaguide/image-20211213101646884.png) + +We have introduced the specific reasons in detail above, so we will not mention them here. + +To solve the problem of loss of accuracy in floating-point operations, you can directly use `BigDecimal` to define the value of a decimal, and then perform decimal operations. + +```java +BigDecimal a = new BigDecimal("1.0"); +BigDecimal b = new BigDecimal("0.9"); +BigDecimal c = new BigDecimal("0.8"); + +BigDecimal x = a.subtract(b); +BigDecimal y = b.subtract(c); + +System.out.println(x.compareTo(y));// 0 +``` + +## BigDecimal common methods + +### Create + +When we use `BigDecimal`, in order to prevent loss of precision, it is recommended to use its `BigDecimal(String val)` constructor or `BigDecimal.valueOf(double val)` static method to create objects. + +"Alibaba Java Development Manual" also mentions this part of the content, as shown in the figure below. + +![](https://oss.javaguide.cn/javaguide/image-20211213102222601.png) + +### Addition, subtraction, multiplication and division + +The `add` method is used to add two `BigDecimal` objects, and the `subtract` method is used to subtract two `BigDecimal` objects. The `multiply` method is used to multiply two `BigDecimal` objects, and the `divide` method is used to divide two `BigDecimal` objects. + +```java +BigDecimal a = new BigDecimal("1.0"); +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));//Cannot be divided, throws ArithmeticException +System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11 +``` + +What needs to be noted here is that when we use the `divide` method, try to use the 3 parameter version, and do not select `UNNECESSARY` for `RoundingMode`, otherwise you are likely to encounter `ArithmeticException` (when an infinite loop of decimals cannot be divided), where `scale` represents how many decimals to retain, and `roundingMode` represents the retention rule. + +```java +public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { + return divide(divisor, scale, roundingMode.oldMode); +} +``` + +There are many retention rules, here are a few: + +```java +public enum RoundingMode { + // 2.4 -> 3, 1.6 -> 2 + // -1.6 -> -2 , -2.4 -> -3 + UP(BigDecimal.ROUND_UP), + // 2.4 -> 2, 1.6 -> 1 + // -1.6 -> -1 , -2.4 -> -2 + DOWN(BigDecimal.ROUND_DOWN), + // 2.4 -> 3, 1.6 -> 2 + // -1.6 -> -1 , -2.4 -> -2 + CEILING(BigDecimal.ROUND_CEILING), + // 2.5 -> 2, 1.6 -> 1 + // -1.6 -> -2 , -2.5 -> -3 + FLOOR(BigDecimal.ROUND_FLOOR), + // 2.4 -> 2, 1.6 -> 2 + // -1.6 -> -2 , -2.4 -> -2 + HALF_UP(BigDecimal.ROUND_HALF_UP), + //...... +} +``` + +### Size comparison + +`a.compareTo(b)` : Returns -1 if `a` is less than `b`, 0 if `a` is equal to `b`, and 1 if `a` is greater than `b`. + +```java +BigDecimal a = new BigDecimal("1.0"); +BigDecimal b = new BigDecimal("0.9"); +System.out.println(a.compareTo(b));// 1 +``` + +### How many decimal places should be kept? + +Set the number of decimal places and retention rules through the `setScale` method. There are many kinds of retention rules. You don’t need to remember them. IDEA will prompt you. + +```java +BigDecimal m = new BigDecimal("1.255433"); +BigDecimal n = m.setScale(3,RoundingMode.HALF_DOWN); +System.out.println(n);// 1.255 +``` + +## BigDecimal equivalence comparison problem + +"Alibaba Java Development Manual" mentioned: + +![](https://oss.javaguide.cn/github/javaguide/java/basis/image-20220714161315993.png) + +`BigDecimal` code example that causes problems when using the `equals()` method for equality comparison: + +```java +BigDecimal a = new BigDecimal("1"); +BigDecimal b = new BigDecimal("1.0"); +System.out.println(a.equals(b));//false +``` + +This is because the `equals()` method not only compares the value (value) but also the precision (scale), while the `compareTo()` method ignores the precision when comparing.The scale of 1.0 is 1 and the scale of 1 is 0, so the result of `a.equals(b)` is false. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/image-20220714164706390.png) + +The `compareTo()` method can compare the values ​​of two `BigDecimal`, and returns 0 if they are equal. If the first number is greater than the second number, 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 +``` + +## BigDecimal tool class sharing + +There is a `BigDecimal` tool class with a large number of users on the Internet, which provides multiple static methods to simplify the operation of `BigDecimal`. + +I made a simple improvement to it and share the source code: + +```java +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Gadget class to simplify BigDecimal calculations + */ +public class BigDecimalUtil { + + /** + *Default division operation precision + */ + private static final int DEF_DIV_SCALE = 10; + + private BigDecimalUtil() { + } + + /** + * Provides precise addition operations. + * + * @param v1 summand + * @param v2 addend + * @return the sum of the two parameters + */ + public static double add(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.add(b2).doubleValue(); + } + + /** + * Provides precise subtraction operations. + * + * @param v1 minuend + * @param v2 subtraction + * @return the difference between the two parameters + */ + public static double subtract(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.subtract(b2).doubleValue(); + } + + /** + * Provides precise multiplication operations. + * + * @param v1 multiplicand + * @param v2 multiplier + * @return the product of two parameters + */ + public static double multiply(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.multiply(b2).doubleValue(); + } + + /** + * Provides (relatively) accurate division operations. When the division cannot be completed, the division operation is accurate to + * There are 10 decimal places after the decimal point, and subsequent digits are rounded up to even numbers. + * + * @param v1 dividend + * @param v2 divisor + * @return the quotient of the two parameters + */ + public static double divide(double v1, double v2) { + return divide(v1, v2, DEF_DIV_SCALE); + } + + /** + * Provides (relatively) accurate division operations. When inexhaustible division occurs, the scale parameter indicates + * To determine the accuracy, subsequent numbers will be rounded to doubles. + * + * @param v1 dividend + * @param v2 divisor + * @param scale means that it needs to be accurate to several decimal places. + * @return the quotient of the two parameters + */ + public static double divide(double v1, double v2, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue(); + } + + /** + * Provides accurate decimal place rounding processing. + * + * @param v Numbers that need to be rounded into pairs + * @param scale How many decimal places to keep after the decimal point? + * @return The result after rounding + */ + public static double round(double v, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = BigDecimal.valueOf(v); + BigDecimal one = new BigDecimal("1"); + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * Provide precise type conversion (Float) + * + * @param v The number to be converted + * @return returns the conversion result + */ + public static float convertToFloat(double v) { + BigDecimal b = BigDecimal.valueOf(v); + return b.floatValue(); + } + + /** + * Provide accurate type conversion (Int) without rounding to double + * + * @param v The number to be converted + * @return returns the conversion result + */ + public static int convertsToInt(double v) { + BigDecimal b = BigDecimal.valueOf(v); + return b.intValue(); + } + + /** + * Provide precise type conversion (Long) + * + * @param v The number to be converted + * @return returns the conversion result + */ + public static long convertsToLong(double v) { + BigDecimal b = BigDecimal.valueOf(v); + return b.longValue(); + } + + /** + * Returns the larger of the two numbers + * + * @param v1 The first number to be compared + * @param v2 The second number to be compared + * @return Returns the larger of the two numbers + */ + public static double returnMax(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.max(b2).doubleValue(); + } + + /** + * Returns the smaller of the two numbers + * + * @param v1 The first number to be compared + * @param v2 The second number to be compared + * @return returns the smaller of the two numbers + */ + public static double returnMin(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.min(b2).doubleValue(); + } + + /** + * Accurately compare two numbers + * + * @param v1 The first number to be compared + * @param v2 The second number to be compared + * @return If the two numbers are the same, return 0, if the first number is greater than the second number, return 1, otherwise return -1 + */ + public static int compareTo(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.compareTo(b2); + } + +}``` + +Related issue: [It is recommended to set the retention rule to RoundingMode.HALF_EVEN, that is, rounding to double, #2129](https://github.com/Snailclimb/JavaGuide/issues/2129). + +![RoundingMode.HALF_EVEN](https://oss.javaguide.cn/github/javaguide/java/basis/RoundingMode.HALF_EVEN.png) + +## Summary + +There is no way to accurately represent floating point numbers in binary, so there is a risk of loss of precision. + +However, Java provides `BigDecimal` to operate on floating point numbers. The implementation of `BigDecimal` makes use of `BigInteger` (used to operate large integers). The difference is that `BigDecimal` adds the concept of decimal places. + + \ No newline at end of file diff --git a/docs_en/java/basis/generics-and-wildcards.en.md b/docs_en/java/basis/generics-and-wildcards.en.md new file mode 100644 index 00000000000..b31fa053cd2 --- /dev/null +++ b/docs_en/java/basis/generics-and-wildcards.en.md @@ -0,0 +1,27 @@ +--- +title: Detailed explanation of generics & wildcards +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: Generics, wildcards, type erasure, upper bound wildcards, lower bound wildcards, PECS, generic methods + - - meta + - name: description + content: Analyze the syntax and principles of Java generics and wildcards, covering high-frequency knowledge points such as type erasure, boundaries and PECS principles. +--- + +**Generics & Wildcards** The relevant interview questions are my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and how to join) exclusive content, which has been compiled into ["Java Interview Guide"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) (click the link to view the detailed introduction and how to obtain it). + +Part of the content of ["Java Interview Guide"](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) is shown below. You can regard it as a supplement to [JavaGuide](https://javaguide.cn/#/), and the two can be used together. + +![](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) + +["Java Interview Guide North"](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) is just one of the many materials within Planet. Planet also has many other high-quality materials such as [Exclusive Column](https://javaguide.cn/zhuanlan/), Java programming videos, and PDF materials. + +![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) + + + + \ No newline at end of file diff --git a/docs_en/java/basis/java-basic-questions-01.en.md b/docs_en/java/basis/java-basic-questions-01.en.md new file mode 100644 index 00000000000..e2873c7d8e5 --- /dev/null +++ b/docs_en/java/basis/java-basic-questions-01.en.md @@ -0,0 +1,1119 @@ +--- +title: Java基础常见面试题总结(上) +category: Java +tag: + - Java基础 +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基础常见知识点和面试题总结,希望对你有帮助! +--- + + + +## 基础概念与常识 + +### Java 语言有哪些特点? + +1. 简单易学(语法简单,上手容易); +2. 面向对象(封装,继承,多态); +3. 平台无关性( Java 虚拟机实现平台无关性); +4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); +5. 可靠性(具备异常处理和自动内存管理机制); +6. 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源); +7. 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的); +8. 支持网络编程并且很方便; +9. 编译与解释并存; +10. …… + +> **🐛 修正(参见:[issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))**:C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接: + +🌈 拓展一下: + +“Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态才是! + +### 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 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。 + +除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。 + +### ⭐️JVM vs JDK vs JRE + +#### JVM + +Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 + +如下图所示,不同编程语言(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 次方(不溢出的情况下)。 +- `>>`: Shift right with sign, shift a few bits to the right, complement the sign bit in the high bits, and discard the low bits. The high bits of positive numbers are padded with 0, and the high bits of negative numbers are padded with 1. `x >> n`, equivalent to x divided by 2 raised to the nth power. +- `>>>`: Unsigned right shift, the sign bit is ignored, and the empty bits are filled with 0. + +Although the shift operation can essentially be divided into left shift and right shift, in practical applications, the right shift operation needs to consider the way the sign bit is processed. + +Since `double` and `float` have special behavior in binary, they cannot be used for shift operations. + +The shift operator actually supports only `int` and `long` types. Before the compiler shifts the `short`, `byte`, and `char` types, it will convert them to the `int` type before operating. + +**What happens if the number of bits shifted exceeds the number of bits occupied by the value? ** + +When the int type left shift/right shift operation is greater than or equal to 32 bits, the remainder (%) will be calculated first and then the left shift/right shift operation will be performed. That is to say, a left shift/right shift of 32 bits is equivalent to no shift operation (32%32=0), and a left shift/right shift of 42 bits is equivalent to a left shift/right shift of 10 bits (42%32=10). When the long type performs a left shift/right shift operation, since the binary corresponding to long is 64 bits, the base of the remainder operation also becomes 64. + +That is to say: `x<<42` is equivalent to `x<<10`, `x>>42` is equivalent to `x>>10`, `x >>>42` is equivalent to `x >>> 10`. + +**Left shift operator code example**: + +```java +int i = -1; +System.out.println("Initial data: " + i); +System.out.println("Binary string corresponding to initial data: " + Integer.toBinaryString(i)); +i <<= 10; +System.out.println("Data shifted 10 bits to the left " + i); +System.out.println("The binary character corresponding to the data shifted 10 bits to the left " + Integer.toBinaryString(i)); +``` + +Output: + +```plain +Initial data: -1 +The binary string corresponding to the initial data: 111111111111111111111111111111111 +Data shifted left by 10 bits -1024 +The binary character corresponding to the data shifted left by 10 bits is 11111111111111111111110000000000 +``` + +Since when the left shift number is greater than or equal to 32 bits, the remainder (%) will be calculated first and then the left shift operation is performed. Therefore, the following code's left shift of 42 bits is equivalent to a left shift of 10 bits (42%32=10), and the output result is the same as the previous code. + +```java +int i = -1; +System.out.println("Initial data: " + i); +System.out.println("Binary string corresponding to initial data: " + Integer.toBinaryString(i)); +i <<= 42; +System.out.println("Data shifted 10 bits to the left " + i); +System.out.println("The binary character corresponding to the data shifted 10 bits to the left " + Integer.toBinaryString(i)); +``` + +The right shift operator is used similarly, but due to space issues, it will not be demonstrated here. + +### What is the difference between continue, break and return? + +In a loop structure, when the loop condition is not met or the number of loops reaches the required number, the loop will end normally. However, sometimes you may need to terminate the loop early when a certain condition occurs during the loop. In this case, you need to use the following keywords: + +1. `continue`: refers to jumping out of the current cycle and continuing to the next cycle. +2. `break`: refers to jumping out of the entire loop body and continuing to execute the statements below the loop. + +`return` is used to jump out of the method and end the running of the method. return is generally used in two ways: + +1. `return;`: Directly use return to end method execution, used for methods without return value functions +2. `return value;`: return a specific value, used for methods that have return value functions + +Think about it: What is the result of running the following statement? + +```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"); +} +``` + +Running results: + +```plain +0 +xixi +1 +2 +xixi +3 +haha +``` + +## ⭐️Basic data types + +### Do you know the basic data types in Java? + +There are 8 basic data types in Java, which are: + +- 6 number types: + - 4 integer types: `byte`, `short`, `int`, `long` + - 2 types of floating point types: `float`, `double` +- 1 character type: `char` +- 1 boolean type: `boolean`. + +The default values ​​and the space occupied by these 8 basic data types are as follows: + +| Basic type | Number of bits | Bytes | Default value | Value range | +| :-------- | :--- | :--- | :------ | --------------------------------------------------------------- | +| `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 | + +It can be seen that the maximum positive number that can be represented by `byte`, `short`, `int`, and `long` has all been reduced by 1. Why is this? This is because in two's complement notation, the highest bit is used to represent the sign (0 represents a positive number, 1 represents a negative number), and the remaining bits represent the numerical part. So, if we want to represent the largest positive number, we need to set all but the highest bit to 1. If we add 1 more, it will overflow and become a negative number. + +For `boolean`, the official documentation does not clearly define it, and it depends on the specific implementation of the JVM vendor. The logical understanding is that it occupies 1 bit, but in practice, the factor of efficient computer storage will be taken into consideration. + +In addition, the size of the storage space occupied by each basic type in Java does not change with changes in machine hardware architecture like most other languages. This invariance in storage space is one of the reasons why Java programs are more portable than programs written in most other languages ​​(mentioned in Section 2.2 of "Java Programming Ideas"). + +**Notice:**1. When using `long` type data in Java, you must add **L** after the value, otherwise it will be parsed as an integer. +2. When using `float` type data in Java, you must add **f or F** after the value, otherwise it will not be compiled. +3. `char a = 'h'`char: single quotation mark, `String a = "hello"`: double quotation mark. + +These eight basic types have corresponding packaging classes: `Byte`, `Short`, `Integer`, `Long`, `Float`, `Double`, `Character`, `Boolean`. + +### What is the difference between basic types and packaged types? + +- **Purpose**: In addition to defining some constants and local variables, we rarely use basic types to define variables in other places such as method parameters and object properties. Also, wrapper types can be used with generics, while basic types cannot. +- **Storage method**: Local variables of basic data types are stored in the local variable table in the Java virtual machine stack, and member variables of basic data types (not modified by `static`) are stored in the heap of the Java virtual machine. Wrapper types belong to object types and we know that almost all object instances exist in the heap. +- **Space occupied**: Compared with package types (object types), basic data types tend to occupy very small space. +- **Default value**: If the member variable packaging type is not assigned a value, it is `null`, while the basic type has a default value and is not `null`. +- **Comparison method**: For basic data types, `==` compares values. For wrapped data types, `==` compares the memory address of the object. All comparisons of values ​​between integer wrapper class objects use the `equals()` method. + +**Why is it said that almost all object instances exist in the heap? ** This is because after the HotSpot virtual machine introduces JIT optimization, it will perform escape analysis on the object. If it is found that an object does not escape outside the method, it may be allocated on the stack through scalar replacement to avoid allocating memory on the heap. + +⚠️ Note: **It is a common misunderstanding that basic data types are stored on the stack! ** Where basic data types are stored depends on their scope and how they are declared. If they are local variables, they will be stored on the stack; if they are member variables, they will be stored in the heap/method area/metaspace. + +```java +public class Test { + //Member variables, stored in the heap + int a = 10; + // Member variables modified by static are located in the method area in JDK 1.7 and before, and are stored in the metaspace after JDK 1.8. They are not stored in the heap. + // Variables belong to classes, not objects. + static int b = 20; + + public void method() { + //Local variables, stored on the stack + int c = 30; + static int d = 40; // Compilation error, static cannot be used to modify local variables in methods + } +} +``` + +### Do you understand the caching mechanism of packaging types? + +Most wrapper types of Java basic data types use a caching mechanism to improve performance. + +The four packaging classes `Byte`, `Short`, `Integer`, and `Long` create corresponding types of cache data with values ​​**[-128, 127]** by default. `Character` creates cache data with values ​​in the range **[0,127]**. `Boolean` directly returns `TRUE` or `FALSE`. + +For `Integer`, the cache upper limit can be modified through the JVM parameter `-XX:AutoBoxCacheMax=`, but the lower limit -128 cannot be modified. In actual use, it is not recommended to set an excessively large value to avoid wasting memory or even OOM. + +For `Byte`, `Short`, `Long`, `Character` there is no parameter similar to `-XX:AutoBoxCacheMax` that can be modified, so the cache range is fixed and cannot be adjusted through JVM parameters. `Boolean` directly returns predefined `TRUE` and `FALSE` instances, without the concept of cache range. + +**Integer cache source code:** + +```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` cache source code:** + +```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` cache source code:** + +```java +public static Boolean valueOf(boolean b) { + return (b ? TRUE : FALSE); +} +``` + +If the corresponding range is exceeded, new objects will still be created. The size of the cache range is just a trade-off between performance and resources. + +The two floating-point type wrapper classes `Float` and `Double` do not implement caching mechanism. + +```java +Integer i1 = 33; +Integer i2 = 33; +System.out.println(i1 == i2);//output true + +Float i11 = 333f; +Float i22 = 333f; +System.out.println(i11 == i22); // Output false + +Double i3 = 1.2; +Double i4 = 1.2; +System.out.println(i3 == i4); // Output false +``` + +Let's look at a question: Is the output of the following code `true` or `false`? + +```java +Integer i1 = 40; +Integer i2 = new Integer(40); +System.out.println(i1==i2); +``` + +`Integer i1=40` This line of code will cause boxing, which means that this line of code is equivalent to `Integer i1=Integer.valueOf(40)`. Therefore, `i1` directly uses the object in the cache. And `Integer i2 = new Integer(40)` will create a new object directly. + +Therefore, the answer is `false` . Did you answer it correctly? + +Remember: **All comparisons of values ​​between integer wrapper class objects use the equals method**. + +![](https://oss.javaguide.cn/github/javaguide/up-1ae0425ce8646adfb768b5374951eeb820d.png) + +### Do you understand automatic boxing and unboxing? What is the principle? + +**What is an automatic unpacking box? ** + +- **Boxing**: Wrap basic types with their corresponding reference types; +- **Unboxing**: Convert the packaging type to a basic data type; + +Example: + +```java +Integer i = 10; //boxing +int n = i; //unboxing +``` + +The bytecode corresponding to the above two lines of code is: + +```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 +``` + +From the bytecode, we found that boxing actually calls the `valueOf()` method of the packaging class, and unboxing actually calls the `xxxValue()` method. + +therefore,- `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` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +**为什么成员变量有默认值?** + +核心原因是为了保证对象状态的安全和可预测性。 + +成员变量和局部变量在这个规则上不同,主要是因为它们的**生命周期**不一样,导致了编译器对它们的“控制力”也不同。 + +- **局部变量**只活在一个方法里,编译器能清楚地看到它是否在使用前被赋值,所以编译器会强制你必须手动赋值,否则就报错。 +- **成员变量**是跟着对象走的,它的值可能在构造函数里赋,也可能在后面的某个 `setter` 方法里赋。编译器在编译时**无法预测**它到底什么时候会被赋值。 + +并且,如果一个变量没有被初始化,它的内存里存放的就是“垃圾值”——之前那块内存遗留下的任意数据。如果程序读取并使用了这个垃圾值,就会产生完全不可预测的结果,比如一个数字变成了随机数,一个对象引用变成了非法地址,这会直接导致程序崩溃或出现诡异的 bug。 + +为了避免你拿到一个含有“垃圾值”的危险对象,Java干脆为所有成员变量提供了一个安全的默认值(如 null 或 0),作为一种**安全兜底机制**。 + +成员变量与局部变量代码示例: + +```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. 构造方法无法被重写 + +#### 总结 + +综上:**重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。** + +| 区别点 | 重载 (Overloading) | 重写 (Overriding) | +| -------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | +| **发生范围** | 同一个类中。 | 父类与子类之间(存在继承关系)。 | +| **Method signature** | The method names **must be the same**, but the **parameter lists must be different** (the type, number, or order of parameters must be different in at least one item). | Method names and parameter lists must be exactly the same**. | +| **Return Type** | It has nothing to do with the return value type and can be modified arbitrarily. | The return type of a subclass method must be **the same** as the return type of the parent class method, or be a **subclass** of it. | +| **Access modifier** | It has nothing to do with the access modifier and can be modified arbitrarily. | The access rights of subclass methods cannot be lower than the access rights of parent class methods. (public > protected > default > private) | +| **Binding period** | Compile-time binding or static binding | Run-time binding (Run-time Binding) or dynamic binding | + +**The rewriting of methods must follow "Two Same, Two Small and One Big"** (The following content is excerpted from "Crazy Java Lecture Notes", [issue#892](https://github.com/Snailclimb/JavaGuide/issues/892)): + +- "Two identical" means the method name is the same and the formal parameter list is the same; +- "Two small" means that the return value type of the subclass method should be smaller or equal to the return value type of the parent class method, and the exception class declared by the subclass method should be smaller or equal to the exception class thrown by the parent class method; +- "One big" means that the access rights of subclass methods should be greater or equal to the access rights of parent class methods. + +⭐️ Regarding **rewritten return value type**, some additional explanation is needed here. The above statement is not very clear and accurate: if the return type of the method is void and basic data type, the return value cannot be modified when rewriting. But if the return value of the method is a reference type, you can return a subclass of the reference type when overriding it. + +```java +public class Hero { + public String name() { + return "superhero"; + } +} +public class SuperMan extends Hero{ + @Override + public String name() { + return "Superman"; + } + public Hero hero() { + return new Hero(); + } +} + +public class SuperSuperMan extends SuperMan { + @Override + public String name() { + return "super superhero"; + } + + @Override + public SuperMan hero() { + return new SuperMan(); + } +} +``` + +### What are variable length parameters? + +Starting from Java5, Java supports the definition of variable-length parameters. The so-called variable-length parameters allow parameters of variable length to be passed in when calling a method. For example, the following method can accept 0 or more parameters. + +```java +public static void method1(String... args) { + //...... +} +``` + +In addition, a variadic parameter can only be used as the last parameter of a function, but it may or may not be preceded by any other parameters. + +```java +public static void method2(String arg1, String... args) { + //...... +} +``` + +**What should you do if you encounter method overloading? Will methods with fixed parameters or variable parameters be matched first? ** + +The answer is that methods with fixed parameters will be matched first, because methods with fixed parameters have a higher matching degree. + +Let's prove it with the following example. + +```java +/** + * Search JavaGuide on WeChat and reply to "Interview Assault" to get your own original Java interview manual for free + * + * @author Guide brother + * @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"); + } +} +``` + +Output: + +```plain +ab +a +b +c +d +``` + +In addition, Java's variable parameters will actually be converted into an array after compilation. We can see this by looking at the `class` file generated after compilation. + +```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); + } + + } + //...... +} +``` + +## Reference + +- What is the difference between JDK and JRE?: +- Oracle vs OpenJDK: +- Differences between Oracle JDK and OpenJDK: +- Completely understand Java's shift operator: + + \ No newline at end of file diff --git a/docs_en/java/basis/java-basic-questions-02.en.md b/docs_en/java/basis/java-basic-questions-02.en.md new file mode 100644 index 00000000000..7bb90e2e70b --- /dev/null +++ b/docs_en/java/basis/java-basic-questions-02.en.md @@ -0,0 +1,890 @@ +--- +title: Java基础常见面试题总结(中) +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: 面向对象, 面向过程, OOP, POP, Java对象, 构造方法, 封装, 继承, 多态, 接口, 抽象类, 默认方法, 静态方法, 私有方法, 深拷贝, 浅拷贝, 引用拷贝, Object类, equals, hashCode, ==, 字符串, String, StringBuffer, StringBuilder, 不可变性, 字符串常量池, intern, 字符串拼接, Java基础, 面试题 + - - meta + - name: description + content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! +--- + + + +## 面向对象基础 + +### ⭐️面向对象和面向过程的区别 + +面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同: + +- **面向过程编程(POP)**:面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 +- **面向对象编程(OOP)**:面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 + +相比较于 POP,OOP 开发的程序一般具有下面这些优点: + +- **易维护**:由于良好的结构和封装性,OOP 程序通常更容易维护。 +- **易复用**:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。 +- **易扩展**:模块化设计使得系统扩展变得更加容易和灵活。 + +POP 的编程方式通常更为简单和直接,适合处理一些较简单的任务。 + +POP 和 OOP 的性能差异主要取决于它们的运行机制,而不仅仅是编程范式本身。因此,简单地比较两者的性能是一个常见的误区(相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) )。 + +![ POP 和 OOP 性能比较不合适](https://oss.javaguide.cn/github/javaguide/java/basis/pop-vs-oop-performance.png) + +在选择编程范式时,性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。 + +现代编程语言基本都支持多种编程范式,既可以用来进行面向过程编程,也可以进行面向对象编程。 + +下面是一个求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案。 + +**面向对象**: + +```java +public class Circle { + // 定义圆的半径 + private double radius; + + // 构造函数 + public Circle(double radius) { + this.radius = radius; + } + + // 计算圆的面积 + public double getArea() { + return Math.PI * radius * radius; + } + + // 计算圆的周长 + public double getPerimeter() { + return 2 * Math.PI * radius; + } + + public static void main(String[] args) { + // 创建一个半径为3的圆 + Circle circle = new Circle(3.0); + + // 输出圆的面积和周长 + System.out.println("圆的面积为:" + circle.getArea()); + System.out.println("圆的周长为:" + circle.getPerimeter()); + } +} +``` + +我们定义了一个 `Circle` 类来表示圆,该类包含了圆的半径属性和计算面积、周长的方法。 + +**面向过程**: + +```java +public class Main { + public static void main(String[] args) { + // 定义圆的半径 + double radius = 3.0; + + // 计算圆的面积和周长 + double area = Math.PI * radius * radius; + double perimeter = 2 * Math.PI * radius; + + // 输出圆的面积和周长 + System.out.println("圆的面积为:" + area); + System.out.println("圆的周长为:" + perimeter); + } +} +``` + +我们直接定义了圆的半径,并使用该半径直接计算出圆的面积和周长。 + +### 创建一个对象用什么运算符?对象实体与对象引用有何不同? + +new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。 + +- 一个对象引用可以指向 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(); + } +} +``` + +Test: + +```java +Person person1 = new Person(new Address("Wuhan")); +Person person1Copy = person1.clone(); +// false +System.out.println(person1.getAddress() == person1Copy.getAddress()); +``` + +As can be seen from the output structure, it is obvious that the clone object of `person1` and the `Address` object contained in `person1` are already different. + +**So what is a reference copy? ** Simply put, reference copy means two different references pointing to the same object. + +I specially drew a picture to describe shallow copy, deep copy and reference copy: + +![shallow&deep-copy](https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png) + +## ⭐️Object + +### What are the common methods of the Object class? + +The Object class is a special class and the parent class of all classes. It mainly provides the following 11 methods: + +```java +/** + * The native method is used to return the Class object of the current runtime object. It is modified with the final keyword, so subclasses are not allowed to override it. + */ +public final native Class getClass() +/** + * Native method, used to return the hash code of the object, mainly used in hash tables, such as HashMap in JDK. + */ +public native int hashCode() +/** + * Used to compare whether the memory addresses of two objects are equal. The String class overrides this method to compare whether the values of strings are equal. + */ +public boolean equals(Object obj) +/** + * Native method, used to create and return a copy of the current object. + */ +protected native Object clone() throws CloneNotSupportedException +/** + * Returns a hexadecimal string of the hash code of the class name instance. It is recommended that all subclasses of Object override this method. + */ +public String toString() +/** + * native method and cannot be overridden. Wake up a thread waiting on this object's monitor (the monitor is equivalent to the concept of a lock). If there are multiple threads waiting, only one will be woken up at will. + */ +public final native void notify() +/** + * native method and cannot be overridden. Like notify, the only difference is that it wakes up all threads waiting on this object's monitor instead of just one thread. + */ +public final native void notifyAll() +/** + * native method and cannot be overridden. Pauses the thread's execution. Note: The sleep method does not release the lock, but the wait method releases the lock, and timeout is the waiting time. + */ +public final native void wait(long timeout) throws InterruptedException +/** + * Added nanos parameter, which represents additional time (in nanoseconds, range is 0-999999). Therefore, nanos nanoseconds need to be added to the timeout time. . + */ +public final void wait(long timeout, int nanos) throws InterruptedException +/** + * Same as the previous two wait methods, except that this method keeps waiting and has no concept of timeout. + */ +public final void wait() throws InterruptedException +/** + * Operations triggered when an instance is recycled by the garbage collector + */ +protected void finalize() throws Throwable { } +``` + +### The difference between == and equals() + +**`==`** has different effects on basic types and reference types: + +- For basic data types, `==` compares values. +- For reference data types, `==` compares the memory address of the object. + +> Because Java only transfers by value, for ==, whether it is comparing basic data types or reference data type variables, the essence of comparison is the value, but the value stored in the reference type variable is the address of the object. + +**`equals()`** cannot be used to determine variables of basic data types, and can only be used to determine whether two objects are equal. The `equals()` method exists in the `Object` class, and the `Object` class is the direct or indirect parent class of all classes, so all classes have the `equals()` method. + +`Object` class `equals()` method: + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +There are two usage cases for the `equals()` method: + +- **The class does not override the `equals()` method**: When comparing two objects of this class through `equals()`, it is equivalent to comparing the two objects through "==". The default method used is the `equals()` method of the `Object` class. +- **Class overrides the `equals()` method**: Generally we override the `equals()` method to compare whether the attributes in two objects are equal; if their attributes are equal, true is returned (that is, the two objects are considered equal). + +Take an example (this is just for example. In fact, if you write it like this, a smart IDE like IDEA will prompt you to replace `==` with `equals()`): + +```java +String a = new String("ab"); // a is a reference +String b = new String("ab"); // b is another reference, the content of the object is the same +String aa = "ab"; // put in constant pool +String bb = "ab"; // Find from the constant pool +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 +``` + +The `equals` method in `String` has been overridden, because the `equals` method of `Object` compares the memory address of the object, while the `equals` method of `String` compares the value of the object. + +When creating an object of type `String`, the virtual machine will look in the constant pool to see if there is an existing object with the same value as the value to be created, and if so, assign it to the current reference. If not, recreate a `String` object in the constant pool. + +`String` class `equals()` method: + +```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; +} +``` + +### What is hashCode() used for? + +The function of `hashCode()` is to get the hash code (`int` integer), also called hash code. The purpose of this hash code is to determine the index position of the object in the hash table. + +![hashCode() method](https://oss.javaguide.cn/github/javaguide/java/basis/java-hashcode-method.png) + +`hashCode()` is defined in the `Object` class of the JDK, which means that any class in Java contains the `hashCode()` function. Another thing to note is that the `hashCode()` method of `Object` is a native method, which is implemented in C language or C++.> ⚠️ Note: The default for this method in **Oracle OpenJDK8** is "use thread local state to implement Marsaglia's xor-shift random number generation", not "address" or "address conversion", different JDK/VM may be different. There are six generation methods in **Oracle OpenJDK8** (the fifth one is the return address), the fifth one is enabled by adding the VM parameter: -XX:hashCode=4. Reference source code: +> +> - (line 1127) +> - (starting at line 537) + +```java +public native int hashCode(); +``` + +The hash table stores key-value pairs, and its characteristics are: it can quickly retrieve the corresponding "value" based on the "key". This uses hash codes! (You can quickly find the object you need)** + +### Why do we need hashCode? + +Let's take "How to check duplicates in `HashSet`" as an example to explain why there is `hashCode`? + +The following paragraph is excerpted from my Java enlightenment book "Head First Java": + +> When you add an object to `HashSet`, `HashSet` will first calculate the `hashCode` value of the object to determine the location where the object is added. It will also compare it with the `hashCode` values of other objects that have been added. If there is no matching `hashCode`, `HashSet` will assume that the object does not appear repeatedly. But if objects with the same `hashCode` value are found, the `equals()` method will be called to check whether the `hashCode` equal objects are really the same. If the two are the same, `HashSet` will not let the join operation succeed. If different, it will be rehashed to another location. In this way, we greatly reduce the number of `equals`, which greatly improves the execution speed. + +In fact, `hashCode()` and `equals()` are both used to compare whether two objects are equal. + +**So why does the JDK provide these two methods at the same time? ** + +This is because in some containers (such as `HashMap`, `HashSet`), with `hashCode()`, it will be more efficient to determine whether the element is in the corresponding container (refer to the process of adding elements into `HashSet`)! + +We also mentioned the process of adding elements to `HashSet` earlier. If `HashSet` has multiple objects of the same `hashCode` during comparison, it will continue to use `equals()` to determine whether they are really the same. In other words, `hashCode` helps us greatly reduce the search cost. + +**Then why not just provide the `hashCode()` method? ** + +This is because the `hashCode` value of two objects is equal does not mean that the two objects are equal. + +**Then why are two objects not necessarily equal even if they have the same `hashCode` value? ** + +Because the hashing algorithm used by `hashCode()` may just cause multiple objects to return the same hash value. The worse the hash algorithm is, the easier it is to collide, but this is also related to the characteristics of the data value range distribution (the so-called hash collision means that different objects get the same `hashCode`). + +To sum it up: + +- If the `hashCode` values of two objects are equal, then the two objects are not necessarily equal (hash collision). +- We only consider two objects equal if their `hashCode` values ​​are equal and the `equals()` method also returns `true`. +- If the `hashCode` values ​​of two objects are not equal, we can directly consider the two objects to be unequal. + +I believe that after reading my previous introduction to `hashCode()` and `equals()`, the following question will not be difficult for you. + +### Why must the hashCode() method be overridden when overriding equals()? + +Because the `hashCode` values ​​of two equal objects must be equal. That is to say, if the `equals` method determines that two objects are equal, then the `hashCode` values ​​of the two objects must also be equal. + +If the `hashCode()` method is not overridden when overriding `equals()`, it may result in the `equals` method judging that two objects are equal, but the `hashCode` values ​​are not equal. + +**Thinking**: If the `hashCode()` method is not rewritten when rewriting `equals()`, what problems may occur when using `HashMap`. + +**Summary**: + +- The `equals` method determines that two objects are equal, then the `hashCode` values of the two objects must also be equal. +- Two objects with the same `hashCode` value are not necessarily equal (hash collision). + +For more information about `hashCode()` and `equals()`, you can check out: [Answers to several questions about Java hashCode() and equals()](https://www.cnblogs.com/skywang12345/p/3324958.html) + +## String + +### ⭐️What is the difference between String, StringBuffer and StringBuilder? + +**Variability** + +`String` is immutable (the reason will be analyzed in detail later). + +Both `StringBuilder` and `StringBuffer` inherit from the `AbstractStringBuilder` class. In `AbstractStringBuilder`, character arrays are also used to save strings, but they are not modified with the `final` and `private` keywords. The most important thing is that the `AbstractStringBuilder` class also provides many methods for modifying strings such as the `append` method. + +```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; + } + //... +} +``` + +**Thread Safety** + +The objects in `String` are immutable, which can be understood as constants and are thread-safe. `AbstractStringBuilder` is the common parent class of `StringBuilder` and `StringBuffer`, and defines some basic operations of strings, such as `expandCapacity`, `append`, `insert`, `indexOf` and other public methods. `StringBuffer` adds a synchronization lock to the method or adds a synchronization lock to the calling method, so it is thread-safe. `StringBuilder` does not add synchronization locks to the method, so it is not thread-safe. + + + +**Performance** + +Every time the `String` type is changed, a new `String` object is generated, and the pointer is pointed to the new `String` object. `StringBuffer` operates on the `StringBuffer` object itself every time, rather than generating new objects and changing object references. Under the same circumstances, using `StringBuilder` can only achieve a performance improvement of about 10%~15% compared to using `StringBuffer`, but it will run the risk of multi-threading insecurity. + +**Summary of the use of the three:** + +- Manipulate small amounts of data: suitable for `String` +- Single-threaded operation of large amounts of data in string buffer: applicable to `StringBuilder` +- Multi-threaded operation of string buffer to operate large amounts of data: applicable to `StringBuffer` + +### ⭐️Why is String immutable? + +The `final` keyword is used in the `String` class to modify the character array to save the string, ~~so the `String` object is immutable. ~~ + +```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 +// 1.在字符串常量池中查询字符串对象 "ab",如果没有则创建"ab"并放入字符串常量池 +// 2.将字符串对象 "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 source code interpretation: +- R big (RednaxelaFX) answer about constant folding: + + \ No newline at end of file diff --git a/docs_en/java/basis/java-basic-questions-03.en.md b/docs_en/java/basis/java-basic-questions-03.en.md new file mode 100644 index 00000000000..4f66dab77da --- /dev/null +++ b/docs_en/java/basis/java-basic-questions-03.en.md @@ -0,0 +1,592 @@ +--- +title: Summary of common Java basic interview questions (Part 2) +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: Java exception handling, Java generics, Java reflection, Java annotations, Java SPI mechanism, Java serialization, Java deserialization, Java IO stream, Java syntax sugar, Java basic interview questions, Checked Exception, Unchecked Exception, try-with-resources, reflection application scenarios, serialization protocol, BIO, NIO, AIO, IO model + - - meta + - name: description + content: The highest quality summary of Java basic common knowledge points and interview questions on the Internet. I hope it will be helpful to you! +--- + + + +##Exception + +**Java Exception Class Hierarchy Diagram Overview**: + +![Java exception class hierarchy diagram](https://oss.javaguide.cn/github/javaguide/java/basis/types-of-exceptions-in-java.png) + +### What is the difference between Exception and Error? + +In Java, all exceptions have a common ancestor, the `Throwable` class in the `java.lang` package. The `Throwable` class has two important subclasses: + +- **`Exception`**: Exceptions that can be handled by the program itself can be caught through `catch`. `Exception` can be divided into Checked Exception (checked exception, must be handled) and Unchecked Exception (unchecked exception, need not be handled). +- **`Error`**: `Error` is an error that cannot be handled by the program. ~~We cannot capture it through `catch`~~ It is not recommended to capture it through `catch`. For example, Java virtual machine running error (`Virtual MachineError`), virtual machine insufficient memory error (`OutOfMemoryError`), class definition error (`NoClassDefFoundError`), etc. When these exceptions occur, the Java Virtual Machine (JVM) generally chooses to terminate the thread. + +### The difference between ClassNotFoundException and NoClassDefFoundError + +- `ClassNotFoundException` is an Exception that occurs when a class cannot be found during dynamic loading using reflection. It is expected and can be caught and processed. +- `NoClassDefFoundError` is an Error. It is a class that exists during compilation and cannot be linked at runtime (for example, the jar package is missing). It is an environmental problem that causes the JVM to be unable to continue. + +### ⭐️What is the difference between Checked Exception and Unchecked Exception? + +**Checked Exception** is a checked exception. During the compilation process of Java code, if the checked exception is not handled by the `catch` or `throws` keyword, it will not be compiled. + +For example, the following IO operation code: + +![](https://oss.javaguide.cn/github/javaguide/java/basis/checked-exception.png) + +Except for `RuntimeException` and its subclasses, other `Exception` classes and their subclasses are checked exceptions. Common checked exceptions include: IO-related exceptions, `ClassNotFoundException`, `SQLException`... + +**Unchecked Exception** is **Unchecked Exception**. During the compilation process of Java code, we can compile normally even if we do not handle unchecked exceptions. + +`RuntimeException` and its subclasses are collectively called unchecked exceptions. The common ones are (it is recommended to write them down, as they will be frequently used in daily development): + +- `NullPointerException` (null pointer error) +- `IllegalArgumentException` (parameter error such as method parameter type error) +- `NumberFormatException` (string to number conversion error, subclass of `IllegalArgumentException`) +- `ArrayIndexOutOfBoundsException` (array out of bounds error) +- `ClassCastException` (type conversion error) +- `ArithmeticException` (arithmetic error) +- `SecurityException` (security error such as insufficient permissions) +- `UnsupportedOperationException` (unsupported operation error such as repeatedly creating the same user) +-… + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unchecked-exception.png) + +### Do you prefer to use Checked Exception or Unchecked Exception? + +Use Unchecked Exception by default, use Checked Exception only when necessary. + +We can think of Unchecked Exceptions (such as `NullPointerException`) as code bugs. The best way to deal with a bug is to expose it and then fix the code, rather than using try-catch to cover it up. + +In general, use Checked Exception only in one situation: when the exception is part of the business logic and the caller must handle it. For example, an insufficient balance exception. This is not a bug, but a normal business branch. I need to use Checked Exception to force the caller to handle this situation, such as prompting the user to recharge. This way, the code can be kept as simple as possible while ensuring the integrity of key business logic. + +### What are the common methods of Throwable class? + +- `String getMessage()`: Returns detailed information when the exception occurs +- `String toString()`: Returns a brief description of the exception when it occurred +- `String getLocalizedMessage()`: Returns the localized information of the exception object. Use a subclass of `Throwable` to override this method to generate localized information. If a subclass does not override this method, the information returned by this method is the same as the result returned by `getMessage()` +- `void printStackTrace()`: Prints the exception information encapsulated by the `Throwable` object on the console + +### How to use try-catch-finally? + +- `try` block: used to catch exceptions. It may be followed by zero or more `catch` blocks. If there is no `catch` block, it must be followed by a `finally` block. +- `catch` block: used to handle exceptions caught by try. +- `finally` block: The statements in the `finally` block will be executed regardless of whether the exception is caught or handled. When a `return` statement is encountered in a `try` block or a `catch` block, the `finally` statement block will be executed before the method returns. + +Code example: + +```java +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); +} finally { + System.out.println("Finally"); +} +``` + +Output: + +```plain +Try to do something +Catch Exception -> RuntimeException +Finally +``` + +**Note: Do not use return!** in the finally statement block. When there are return statements in both the try statement and the finally statement, the return statement in the try statement block will be ignored. This is because the return value in the try statement will be temporarily stored in a local variable. After the return in the finally statement is executed, the value of this local variable becomes the return value in the finally statement. + +Code example: + +```java +public static void main(String[] args) { + System.out.println(f(2)); +} + +public static int f(int value) { + try { + return value * value; + } finally { + if (value == 2) { + return 0; + } + } +} +``` + +Output: + +```plain +0 +``` + +### Will the code in finally be executed? + +Not necessarily! In some cases, the code in finally will not be executed.For example, if the virtual machine is terminated before finally, the code in finally will not be executed. + +```java +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); + // Terminate the currently running Java virtual machine + System.exit(1); +} finally { + System.out.println("Finally"); +} +``` + +Output: + +```plain +Try to do something +Catch Exception -> RuntimeException +``` + +In addition, the code in the `finally` block will not be executed in the following 2 special cases: + +1. The thread where the program is located dies. +2. Switch off the CPU. + +Related issue: . + +🧗🏻 Let’s take a step further: analyze the implementation principle behind the syntactic sugar of `try catch finally` from a bytecode perspective. + +### How to use `try-with-resources` instead of `try-catch-finally`? + +1. **Scope of application (definition of resources):** Any object that implements `java.lang.AutoCloseable` or `java.io.Closeable` +2. **Execution order of closing resources and finally blocks:** In a `try-with-resources` statement, any catch or finally block runs after the declared resource is closed + +"Effective Java" clearly states: + +> When faced with resources that must be closed, we should always use `try-with-resources` instead of `try-finally`. The resulting code is shorter and clearer, and the exceptions generated are more useful to us. The `try-with-resources` statement makes it easier to write code that must close resources, which is almost impossible with `try-finally`. + +Resources like `InputStream`, `OutputStream`, `Scanner`, `PrintWriter`, etc. in Java require us to call the `close()` method to manually close it. Generally, we implement this requirement through the `try-catch-finally` statement, as follows: + +```java +//Read the contents of the text file +Scanner scanner = null; +try { + scanner = new Scanner(new File("D://read.txt")); + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException e) { + e.printStackTrace(); +} finally { + if (scanner != null) { + scanner.close(); + } +} +``` + +Modify the above code using the `try-with-resources` statement since Java 7: + +```java +try (Scanner scanner = new Scanner(new File("test.txt"))) { + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); +} +``` + +Of course, when multiple resources need to be closed, it is very simple to use `try-with-resources` to implement it. If you still use `try-catch-finally`, it may cause a lot of problems. + +Multiple resources can be declared in a `try-with-resources` block by using semicolons to separate them. + +```java +try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { + int b; + while ((b = bin.read()) != -1) { + bout.write(b); + } +} +catch (IOException e) { + e.printStackTrace(); +} +``` + +### ⭐️What should we pay attention to when using exceptions? + +- Do not define exceptions as static variables, because this will cause confusion in the exception stack information. Every time an exception is thrown manually, we need to manually new an exception object to throw. +- The exception information thrown must be meaningful. +- It is recommended to throw more specific exceptions. For example, when converting a string to a number format error, `NumberFormatException` should be thrown instead of its parent class `IllegalArgumentException`. +- Avoid repeated logging: If enough information (including exception type, error information, stack trace, etc.) has been recorded where the exception is caught, then the same error information should not be recorded again when the exception is thrown again in the business code. Repeated logging bloats log files and may obscure the actual cause of a problem, making it more difficult to track down and resolve. +-… + +## Generics + +### What are generics? What does it do? + +**Java Generics** is a new feature introduced in JDK 5. Using generic parameters can enhance the readability and stability of the code. + +The compiler can detect generic parameters and specify the type of object passed in through generic parameters. For example, `ArrayList persons = new ArrayList()` This line of code indicates that the `ArrayList` object can only be passed in `Person` objects. If other types of objects are passed in, an error will be reported. + +```java +ArrayList extends AbstractList +``` + +Moreover, the native `List` return type is `Object`, which requires manual type conversion before it can be used. The compiler automatically converts it after using generics. + +### What are the ways to use generics? + +Generics are generally used in three ways: **generic class**, **generic interface**, and **generic method**. + +**1. Generic class**: + +```java +//Here T can be written as any identifier. Common parameters such as T, E, K, V, etc. are often used to represent generics. +//When instantiating a generic class, the specific type of T must be specified +public class Generic{ + + private T key; + + public Generic(T key) { + this.key = key; + } + + public T getKey(){ + return key; + } +} +``` + +How to instantiate a generic class: + +```java +Generic genericInteger = new Generic(123456); +``` + +**2. Generic interface**: + +```java +public interface Generator { + public T method(); +} +``` + +Implement a generic interface without specifying a type: + +```java +class GeneratorImpl implements Generator{ + @Override + public T method() { + return null; + } +} +``` + +Implement the generic interface and specify the type: + +```java +class GeneratorImpl implements Generator { + @Override + public String method() { + return "hello"; + } +} +``` + +**3. Generic methods**: + +```java + public static < E > void printArray( E[] inputArray ) + { + for (E element : inputArray ){ + System.out.printf( "%s ", element ); + } + System.out.println(); + } +``` + +Use: + +```java +// Create arrays of different types: Integer, Double and Character +Integer[] intArray = { 1, 2, 3 }; +String[] stringArray = { "Hello", "World" }; +printArray(intArray); +printArray( stringArray );``` + +> 注意: `public static < E > void printArray( E[] inputArray )` 一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 `` + +### 项目中哪里用到了泛型? + +- 自定义接口通用返回结果 `CommonResult` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型 +- 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 +- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。 +- …… + +## ⭐️反射 + +关于反射的详细解读,请看这篇文章 [Java 反射机制详解](https://javaguide.cn/java/basis/reflection.html) 。 + +### 什么是反射? + +简单来说,Java 反射 (Reflection) 是一种**在程序运行时,动态地获取类的信息并操作类或对象(方法、属性)的能力**。 + +通常情况下,我们写的代码在编译时类型就已经确定了,要调用哪个方法、访问哪个字段都是明确的。但反射允许我们在**运行时**才去探知一个类有哪些方法、哪些属性、它的构造函数是怎样的,甚至可以动态地创建对象、调用方法或修改属性,哪怕这些方法或属性是私有的。 + +正是这种在运行时“反观自身”并进行操作的能力,使得反射成为许多**通用框架和库的基石**。它让代码更加灵活,能够处理在编译时未知的类型。 + +### 反射有什么优缺点? + +**优点:** + +1. **灵活性和动态性**:反射允许程序在运行时动态地加载类、创建对象、调用方法和访问字段。这样可以根据实际需求(如配置文件、用户输入、注解等)动态地适应和扩展程序的行为,显著提高了系统的灵活性和适应性。 +2. **框架开发的基础**:许多现代 Java 框架(如 Spring、Hibernate、MyBatis)都大量使用反射来实现依赖注入(DI)、面向切面编程(AOP)、对象关系映射(ORM)、注解处理等核心功能。反射是实现这些“魔法”功能不可或缺的基础工具。 +3. **解耦合和通用性**:通过反射,可以编写更通用、可重用和高度解耦的代码,降低模块之间的依赖。例如,可以通过反射实现通用的对象拷贝、序列化、Bean 工具等。 + +**缺点:** + +1. **性能开销**:反射操作通常比直接代码调用要慢。因为涉及到动态类型解析、方法查找以及 JIT 编译器的优化受限等因素。不过,对于大多数框架场景,这种性能损耗通常是可以接受的,或者框架本身会做一些缓存优化。 +2. **安全性问题**:反射可以绕过 Java 语言的访问控制机制(如访问 `private` 字段和方法),破坏了封装性,可能导致数据泄露或程序被恶意篡改。此外,还可以绕过泛型检查,带来类型安全隐患。 +3. **代码可读性和维护性**:过度使用反射会使代码变得复杂、难以理解和调试。错误通常在运行时才会暴露,不像编译期错误那样容易发现。 + +相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。 + +### 反射的应用场景? + +我们平时写业务代码可能很少直接跟 Java 的反射(Reflection)打交道。但你可能没意识到,你天天都在享受反射带来的便利!**很多流行的框架,比如 Spring/Spring Boot、MyBatis 等,底层都大量运用了反射机制**,这才让它们能够那么灵活和强大。 + +下面简单列举几个最场景的场景帮助大家理解。 + +**1.依赖注入与控制反转(IoC)** + +以 Spring/Spring Boot 为代表的 IoC 框架,会在启动时扫描带有特定注解(如 `@Component`, `@Service`, `@Repository`, `@Controller`)的类,利用反射实例化对象(Bean),并通过反射注入依赖(如 `@Autowired`、构造器注入等)。 + +**2.注解处理** + +注解本身只是个“标记”,得有人去读这个标记才知道要做什么。反射就是那个“读取器”。框架通过反射检查类、方法、字段上有没有特定的注解,然后根据注解信息执行相应的逻辑。比如,看到 `@Value`,就用反射读取注解内容,去配置文件找对应的值,再用反射把值设置给字段。 + +**3.动态代理与 AOP** + +想在调用某个方法前后自动加点料(比如打日志、开事务、做权限检查)?AOP(面向切面编程)就是干这个的,而动态代理是实现 AOP 的常用手段。JDK 自带的动态代理(Proxy 和 InvocationHandler)就离不开反射。代理对象在内部调用真实对象的方法时,就是通过反射的 `Method.invoke` 来完成的。 + +```java +public class DebugInvocationHandler implements InvocationHandler { + private final Object target; // 真实对象 + + public DebugInvocationHandler(Object target) { this.target = target; } + + // proxy: 代理对象, method: 被调用的方法, args: 方法参数 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("切面逻辑:调用方法 " + method.getName() + " 之前"); + // 通过反射调用真实对象的同名方法 + Object result = method.invoke(target, args); + System.out.println("切面逻辑:调用方法 " + method.getName() + " 之后"); + return result; + } +} +``` + +**4.对象关系映射(ORM)** + +像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。 + +## 注解 + +### 何谓注解? + +`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 + +注解本质是一个继承了`Annotation` 的特殊接口: + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface Override { + +} + +public interface Override extends Annotation{ + +} +``` + +JDK 提供了很多内置的注解(比如 `@Override`、`@Deprecated`),同时,我们还可以自定义注解。 + +### 注解的解析方法有哪几种? + +注解只有被解析之后才会生效,常见的解析方法有两种: + +- **编译期直接扫描**:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 +- **运行期通过反射处理**:像框架中自带的注解(比如 Spring 框架的 `@Value`、`@Component`)都是通过反射来进行处理的。 + +## ⭐️SPI + +关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](https://javaguide.cn/java/basis/spi.html) 。 + +### 何谓 SPI? + +SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 + +SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + +很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 + + + +### SPI 和 API 有什么区别? + +**那 SPI 和 API 有啥区别?** + +说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: + +![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) + +一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 + +- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 +- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 + +举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 + +### SPI 的优缺点? + +通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: + +- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。 +- 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 + +## ⭐️序列化和反序列化 + +关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](https://javaguide.cn/java/basis/serialization.html) ,里面涉及到的知识点和面试题更全面。 + +### 什么是序列化?什么是反序列化? + +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +简单来说: + +- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 +- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 + +对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 + +下面是序列化和反序列化常见应用场景: + +- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化; +- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化; +- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化; +- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 + +维基百科是如是介绍序列化的: + +> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** + +![](https://oss.javaguide.cn/github/javaguide/a478c74d-2c48-40ae-9374-87aacf05188c.png) + +

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

+ +**序列化协议对应于 TCP/IP 4 层模型的哪一层?** + +我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢? + +1. 应用层 +2. 传输层 +3. 网络层 +4. 网络接口层 + +![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) + +如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么? + +因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。 + +### 如果有些字段不想进行序列化怎么办? + +对于不想进行序列化的变量,使用 `transient` 关键字修饰。 + +`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 + +关于 `transient` 还有几点注意: + +- `transient` 只能修饰变量,不能修饰类和方法。 +- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 +- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 + +### 常见序列化协议有哪些? + +JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 + +像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 + +### 为什么不推荐使用 JDK 自带的序列化? + +我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因: + +- **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 +- **性能差**:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 +- **存在安全问题**:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:[应用安全:JAVA 反序列化漏洞之殇](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/) 。 + +## I/O + +关于 I/O 的详细解读,请看下面这几篇文章,里面涉及到的知识点和面试题更全面。 + +- [Java IO 基础知识总结](https://javaguide.cn/java/io/io-basis.html) +- [Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html) +- [Java IO 模型详解](https://javaguide.cn/java/io/io-model.html) + +### Java IO 流了解吗? + +IO 即 `Input/Output`,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。 + +Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 + +- `InputStream`/`Reader`: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- `OutputStream`/`Writer`: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +### I/O 流为什么要分为字节流和字符流呢? + +The essence of the question is: **Whether it is file reading and writing or network sending and receiving, the smallest storage unit of information is bytes, so why are I/O stream operations divided into byte stream operations and character stream operations? ** + +Personally, I think there are two main reasons: + +- The character stream is obtained by converting bytes by the Java virtual machine. This process is quite time-consuming; +- If we don't know the encoding type, garbled characters can easily occur when using byte streams. + +### What are the design patterns in Java IO? + +Reference answer: [Summary of Java IO design patterns](https://javaguide.cn/java/io/io-design-patterns.html) + +### What is the difference between BIO, NIO and AIO? + +Reference answer: [Detailed explanation of Java IO model](https://javaguide.cn/java/io/io-model.html) + +## Syntactic sugar + +### What is syntactic sugar? + +**Syntactic sugar** refers to a special syntax designed by a programming language to facilitate programmers to develop programs. This syntax has no impact on the functionality of the programming language. To achieve the same function, code written based on syntax sugar is often simpler, more concise and easier to read. + +For example, `for-each` in Java is a commonly used syntax sugar, and its principle is actually based on ordinary for loops and iterators. + +```java +String[] strs = {"JavaGuide", "Public account: JavaGuide", "Blog: https://javaguide.cn/"}; +for (String s : strs) { + System.out.println(s); +} +``` + +However, the JVM does not actually recognize syntax sugar. In order for Java syntax sugar to be executed correctly, it needs to be desugared by the compiler, that is, it is converted into the basic syntax recognized by the JVM during the program compilation stage. This also shows that the real support for syntactic sugar in Java is the Java compiler rather than the JVM. If you look at the source code of `com.sun.tools.javac.main.JavaCompiler`, you will find that one step in `compile()` is to call `desugar()`. This method is responsible for decoding the implementation of syntax sugar. + +### What are the common syntactic sugars in Java? + +The most commonly used syntactic sugars in Java include generics, automatic unboxing, variable-length parameters, enumerations, inner classes, enhanced for loops, try-with-resources syntax, lambda expressions, etc. + +For a detailed explanation of these syntactic sugars, please read this article [Java Syntactic Sugar Detailed Explanation](./syntactic-sugar.md). + + \ No newline at end of file diff --git a/docs_en/java/basis/java-keyword-summary.en.md b/docs_en/java/basis/java-keyword-summary.en.md new file mode 100644 index 00000000000..e8a152bc8d7 --- /dev/null +++ b/docs_en/java/basis/java-keyword-summary.en.md @@ -0,0 +1,324 @@ +--- +title: Java 关键字总结 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: Java 关键字,final,static,this,super,abstract,interface,enum,volatile,transient + - - meta + - name: description + content: 梳理常见 Java 关键字的语义与用法差异,便于快速查阅与掌握。 +--- + +# final,static,this,super 关键字总结 + +## final 关键字 + +**final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:** + +1. final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法; + +2. final 修饰的方法不能被重写; + +3. final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。 + +说明:使用 final 方法的原因有两个: + +1. 把方法锁定,以防任何继承类修改它的含义; +2. 效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。 + +## static 关键字 + +**static 关键字主要有以下四种使用场景:** + +1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` +2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +3. **静态内部类(static 修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。 +4. **静态导包(用来导入类中的静态资源,1.5 之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 + +## this 关键字 + +this 关键字用于引用类的当前实例。 例如: + +```java +class Manager { + Employees[] employees; + void manageEmployees() { + int totalEmp = this.employees.length; + System.out.println("Total employees: " + totalEmp); + this.report(); + } + void report() { } +} +``` + +在上面的示例中,this 关键字用于两个地方: + +- this.employees.length:访问类 Manager 的当前实例的变量。 +- this.report():调用类 Manager 的当前实例的方法。 + +此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 + +## super 关键字 + +super 关键字用于从子类访问父类的变量和方法。 例如: + +```java +public class Super { + protected int number; + protected void showNumber() { + System.out.println("number = " + number); + } +} +public class Sub extends Super { + void bar() { + super.number = 10; + super.showNumber(); + } +} +``` + +在上面的例子中,Sub 类访问父类成员变量 number 并调用其父类 Super 的 `showNumber()` 方法。 + +**使用 this 和 super 要注意的问题:** + +- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 +- this、super 不能用在 static 方法中。 + +**简单解释一下:** + +被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this 和 super 是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 + +## 参考 + +- +- + +# static 关键字详解 + +## static 关键字主要有以下四种使用场景 + +1. 修饰成员变量和成员方法 +2. 静态代码块 +3. 修饰类(只能修饰内部类) +4. 静态导包(用来导入类中的静态资源,1.5 之后的新特性) + +### 修饰成员变量和成员方法(常用) + +被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。 + +方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 + +HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 + +调用格式: + +- `类名.静态变量名` +- `类名.静态方法名()` + +如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 + +测试方法: + +```java +public class StaticBean { + String name; + //静态变量 + static int age; + public StaticBean(String name) { + this.name = name; + } + //静态方法 + static void sayHello() { + System.out.println("Hello i am java"); + } + @Override + public String toString() { + return "StaticBean{"+ + "name=" + name + ",age=" + age + + "}"; + } +} +``` + +```java +public class StaticDemo { + public static void main(String[] args) { + StaticBean staticBean = 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 + } +} +``` + +### 静态代码块 + +静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块 —> 非静态代码块 —> 构造方法)。 该类不管创建多少对象,静态代码块只执行一次. + +静态代码块的格式是 + +```plain +static { +语句体; +} +``` + +一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM 加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM 将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。 + +![](https://oss.javaguide.cn/github/javaguide/88531075.jpg) + +静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问. + +### 静态内部类 + +静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: + +1. 它的创建是不需要依赖外围类的创建。 +2. 它不能使用任何外围类的非 static 成员变量和方法。 + +Example(静态内部类实现单例模式) + +```java +public class Singleton { + //声明为 private 避免调用默认构造方法创建对象 + private Singleton() { + } + // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + public static Singleton getUniqueInstance() { + return SingletonHolder.INSTANCE; + } +} +``` + +当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()`方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 + +这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 + +### 静态导包 + +格式为:import static + +这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法 + +```java + //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 + //如果只想导入单一某个静态方法,只需要将*换成对应的方法名即可 +import static java.lang.Math.*;//换成import static java.lang.Math.max;即可指定单一静态方法max导入 +public class Demo { + public static void main(String[] args) { + int max = max(1,2); + System.out.println(max); + } +} +``` + +## 补充内容 + +### 静态方法与非静态方法 + +静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。 + +Example + +```java +class Foo { + int i; + public Foo(int i) { + this.i = i; + } + public static String method1() { + return "An example string that doesn't depend on i (an instance variable)"; + } + public int method2() { + return this.i + 1; //Depends on i + } +} +``` + +你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行 + +```java +Foo bar = new Foo(1); +bar.method2(); +``` + +总结: + +- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 +- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 + +### `static{}`静态代码块与`{}`非静态代码块(构造代码块) + +相同点:都是在 JVM 加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些 static 变量进行赋值。 + +不同点:静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次 new 执行一次,之后不再执行,而非静态代码块在每 new 一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 + +> **🐛 修正(参见:[issue #677](https://github.com/Snailclimb/JavaGuide/issues/677))**:静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行,即 new 或者 `Class.forName("ClassDemo")` 都会执行静态代码块。 +> 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:`Arrays` 类,`Character` 类,`String` 类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. + +Example: + +```java +public class Test { + public Test() { + System.out.print("默认构造方法!--"); + } + //非静态代码块 + { + System.out.print("非静态代码块!--"); + } + //静态代码块 + static { + System.out.print("静态代码块!--"); + } + private static void test() { + System.out.print("静态方法中的内容! --"); + { + System.out.print("静态方法中的代码块!--"); + } + } + public static void main(String[] args) { + Test test = new Test(); + Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- + } +} +``` + +上述代码输出: + +```plain +静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- +``` + +当只执行 `Test.test();` 时输出: + +```plain +静态代码块!--静态方法中的内容! --静态方法中的代码块!-- +``` + +当只执行 `Test test = new Test();` 时输出: + +```plain +静态代码块!--非静态代码块!--默认构造方法!-- +``` + +非静态代码块与构造函数的区别是:非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。 + +### 参考 + +- +- +- + + + diff --git a/docs_en/java/basis/proxy.en.md b/docs_en/java/basis/proxy.en.md new file mode 100644 index 00000000000..2846b68953d --- /dev/null +++ b/docs_en/java/basis/proxy.en.md @@ -0,0 +1,407 @@ +--- +title: Detailed explanation of Java proxy mode +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: proxy mode, static proxy, dynamic proxy, JDK dynamic proxy, CGLIB, cross-cutting enhancement, design pattern + - - meta + - name: description + content: Detailed explanation of the static and dynamic implementation of Java proxy mode, and understanding of the principles and application scenarios of JDK/CGLIB dynamic proxy. +--- + +## 1. Proxy mode + +The proxy pattern is a relatively easy-to-understand design pattern. To put it simply: **We use proxy objects to replace access to real objects, so that we can provide additional functional operations and extend the functions of the target object without modifying the original target object. ** + +**The main function of the proxy mode is to extend the functionality of the target object. For example, you can add some custom operations before and after a method of the target object is executed. ** + +For example: the bride hired her aunt to handle the groom's questions on her behalf. The questions the bride received were all processed and filtered by her aunt. The aunt here can be regarded as a proxy object that represents you. The behavior (method) of the proxy 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

+ +The proxy mode has two implementation methods: static proxy and dynamic proxy. Let’s first look at the implementation of the static proxy mode. + +## 2. Static proxy + +In static proxy, we enhance each method of the target object manually (the code will be demonstrated later), which is very inflexible (for example, once a new method is added to the interface, both the target object and the proxy object must be modified) and troublesome (a separate proxy class needs to be written for each target class). There are very few actual application scenarios, and there are almost no scenarios where static proxies are used in daily development. + +Above we are talking about static proxies from the perspective of implementation and application. From the JVM level, **static proxies turn interfaces, implementation classes, and proxy classes into actual class files during compilation. ** + +Static proxy implementation steps: + +1. Define an interface and its implementation class; +2. Create a proxy class that also implements this interface +3. Inject the target object into the proxy class, and then call the corresponding method in the target class in the corresponding method of the proxy class. In this case, we can shield access to the target object through the proxy class, and do what we want to do before and after the target method is executed. + +Shown below through code! + +**1. Define the interface for sending text messages** + +```java +public interface SmsService { + String send(String message); +} +``` + +**2. Implement the interface for sending text messages** + +```java +public class SmsServiceImpl implements SmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +``` + +**3. Create a proxy class and also implement the interface for sending text messages** + +```java +public class SmsProxy implements SmsService { + + private final SmsService smsService; + + public SmsProxy(SmsService smsService) { + this.smsService = 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.Actual use** + +```java +public class Main { + public static void main(String[] args) { + SmsService smsService = new SmsServiceImpl(); + SmsProxy smsProxy = new SmsProxy(smsService); + smsProxy.send("java"); + } +} +``` + +After running the above code, the console prints out: + +```bash +before method send() +send message:java +after method send() +``` + +It can be seen from the output that we have added the `send()` method of `SmsServiceImpl`. + +## 3. Dynamic proxy + +Compared with static proxies, dynamic proxies are more flexible. We do not need to create a separate proxy class for each target class, and we do not need to implement the interface. We can directly proxy the implementation class (CGLIB dynamic proxy mechanism). + +**From a JVM perspective, a dynamic proxy dynamically generates class bytecode at runtime and loads it into the JVM. ** + +When it comes to dynamic proxies, Spring AOP and RPC frameworks should be mentioned. Their implementation relies on dynamic proxies. + +**Dynamic proxy is relatively rarely used in our daily development, but it is almost a must-use technology in the framework. After learning dynamic agents, it is also very helpful for us to understand and learn the principles of various frameworks. ** + +As far as Java is concerned, there are many ways to implement dynamic proxy, such as **JDK dynamic proxy**, **CGLIB dynamic proxy** and so on. + +[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) uses JDK dynamic proxy. Let’s first take a look at the use of JDK dynamic proxy. + +In addition, although [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) does not use **CGLIB dynamic proxy**, we will briefly introduce its use and comparison with **JDK dynamic proxy** here. + +### 3.1. JDK dynamic proxy mechanism + +#### 3.1.1. Introduction + +**In the Java dynamic proxy mechanism, the `InvocationHandler` interface and the `Proxy` class are the core. ** + +The most frequently used method in the `Proxy` class is: `newProxyInstance()`. This method is mainly used to generate a proxy object. + +```java + public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException + { + ... + } +``` + +This method has a total of 3 parameters: + +1. **loader**: Class loader, used to load proxy objects. +2. **interfaces**: some interfaces implemented by the proxy class; +3. **h**: An object that implements the `InvocationHandler` interface; + +To implement a dynamic proxy, you must also implement `InvocationHandler` to customize the processing logic. When our dynamic proxy object calls a method, the call to this method will be forwarded to the `invoke` method of the class that implements the `InvocationHandler` interface. + +```java +public interface InvocationHandler { + + /** + * When you use a proxy object to call a method, this method will actually be called. + */ + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +} +``` + +The `invoke()` method has the following three parameters: + +1. **proxy**: dynamically generated proxy class +2. **method**: Corresponds to the method called by the proxy class object +3. **args**: parameters of the current method methodThat is to say: **When the proxy object you create through `newProxyInstance()` of the `Proxy` class calls a method, it will actually call the `invoke()` method of the class that implements the `InvocationHandler` interface. ** You can customize the processing logic in the `invoke()` method, such as what to do before and after the method is executed. + +#### 3.1.2. Steps to use JDK dynamic proxy class + +1. Define an interface and its implementation class; +2. Customize `InvocationHandler` and override the `invoke` method. In the `invoke` method we will call the native method (the method of the proxy class) and customize some processing logic; +3. Create a proxy object through the `Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)` method; + +#### 3.1.3. Code examples + +This may be a bit empty and difficult to understand. Let me give you an example. Let’s experience it! + +**1. Define the interface for sending text messages** + +```java +public interface SmsService { + String send(String message); +} +``` + +**2. Implement the interface for sending text messages** + +```java +public class SmsServiceImpl implements SmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +``` + +**3. Define a JDK dynamic proxy class** + +```java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @author shuang.kou + * @createTime May 11, 2020 11:23:00 + */ +public class DebugInvocationHandler implements InvocationHandler { + /** + * The real object in the proxy class + */ + private final Object target; + + public DebugInvocationHandler(Object target) { + this.target = target; + } + + @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()` method: When our dynamic proxy object calls a native method, the `invoke()` method is actually called, and then the `invoke()` method calls the native method of the proxy object on our behalf. + +**4. Get the factory class of the proxy object** + +```java +public class JdkProxyFactory { + public static Object getProxy(Object target) { + return Proxy.newProxyInstance( + target.getClass().getClassLoader(), // Class loader of target class + target.getClass().getInterfaces(), // The interfaces that the agent needs to implement, multiple can be specified + new DebugInvocationHandler(target) // Custom InvocationHandler corresponding to the proxy object + ); + } +} +``` + +`getProxy()`: mainly obtains the proxy object of a certain class through the `Proxy.newProxyInstance()` method + +**5.Actual use** + +```java +SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); +smsService.send("java"); +``` + +After running the above code, the console prints out: + +```plain +before method send +send message:java +after method send +``` + +### 3.2. CGLIB dynamic proxy mechanism + +#### 3.2.1. Introduction + +**One of the most fatal problems with JDK dynamic proxy is that it can only proxy classes that implement interfaces. ** + +**In order to solve this problem, we can use the CGLIB dynamic proxy mechanism to avoid it. ** + +[CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_) is a bytecode generation library based on [ASM](http://www.baeldung.com/java-asm), which allows us to modify and dynamically generate bytecode at runtime. CGLIB implements proxies through inheritance. Many well-known open source frameworks use [CGLIB](https://github.com/cglib/cglib). For example, in the AOP module in Spring: if the target object implements the interface, the JDK dynamic proxy is used by default, otherwise the CGLIB dynamic proxy is used. + +**In the CGLIB dynamic proxy mechanism, the `MethodInterceptor` interface and the `Enhancer` class are the core. ** + +You need to customize `MethodInterceptor` and override the `intercept` method. `intercept` is used to intercept methods that enhance the proxied class. + +```java +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**: The proxy object (the object that needs to be enhanced) +2. **method**: intercepted method (method that needs to be enhanced) +3. **args**: method input parameters +4. **proxy**: used to call the original method + +You can dynamically obtain the proxy class through the `Enhancer` class. When the proxy class calls a method, the `intercept` method in `MethodInterceptor` is actually called. + +#### 3.2.2. Steps to use CGLIB dynamic proxy class + +1. Define a class; +2. Customize `MethodInterceptor` and override the `intercept` method. `intercept` is used to intercept methods that enhance the proxy class, similar to the `invoke` method in JDK dynamic proxy; +3. Create the proxy class through `create()` of the `Enhancer` class; + +#### 3.2.3. Code examples + +Unlike JDK, dynamic proxies require no additional dependencies. [CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_) is actually an open source project. If you want to use it, you need to manually add related dependencies. + +```xml + + cglib + cglib + 3.3.0 + +``` + +**1. Implement a class that uses Alibaba Cloud to send text messages** + +```java +package github.javaguide.dynamicProxy.cglibDynamicProxy; + +public class AliSmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +```**2. Customize `MethodInterceptor` (method interceptor) ** + +```java +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; + +/** + * Custom MethodInterceptor + */ +public class DebugMethodInterceptor implements MethodInterceptor { + + + /** + * @param o The proxy object itself (note that it is not the original object, if method.invoke(o, args) is used, it will cause a loop call) + * @param method The intercepted method (method that needs to be enhanced) + * @param args method input parameters + * @param methodProxy High-performance method calling mechanism to avoid reflection overhead + */ + @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. Get the proxy class** + +```java +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 class loader + enhancer.setClassLoader(clazz.getClassLoader()); + //Set the proxy class + enhancer.setSuperclass(clazz); + //Set method interceptor + enhancer.setCallback(new DebugMethodInterceptor()); + //Create proxy class + return enhancer.create(); + } +} +``` + +**4.Actual use** + +```java +AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class); +aliSmsService.send("java"); +``` + +After running the above code, the console prints out: + +```bash +before method send +send message:java +after method send +``` + +### 3.3. Comparison between JDK dynamic proxy and CGLIB dynamic proxy + +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. ** In addition, CGLIB dynamic proxy intercepts method calls of the proxied class by generating a subclass of the proxied class. Therefore, classes and methods declared as final types cannot be proxied, and private methods cannot be proxied. +2. In terms of efficiency between the two, JDK dynamic proxy is better in most cases. With the upgrade of JDK version, this advantage becomes more obvious. + +## 4. Comparison between static proxy and dynamic proxy + +1. **Flexibility**: Dynamic proxy is more flexible. It does not need to implement an interface. It can directly proxy the implementation class, and there is no need to create a proxy class for each target class. In addition, in a static proxy, once a new method is added to the interface, both the target object and the proxy object must be modified, which is very troublesome! +2. **JVM level**: Static proxy turns interfaces, implementation classes, and proxy classes into actual class files during compilation. The dynamic proxy dynamically generates class bytecode at runtime and loads it into the JVM. + +## 5. Summary + +This article mainly introduces two implementations of the proxy mode: static proxy and dynamic proxy. Covers the actual combat between static proxy and dynamic proxy, the difference between static proxy and dynamic proxy, the difference between JDK dynamic proxy and Cglib dynamic proxy, etc. + +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) . + + \ No newline at end of file diff --git a/docs_en/java/basis/reflection.en.md b/docs_en/java/basis/reflection.en.md new file mode 100644 index 00000000000..dec9402e744 --- /dev/null +++ b/docs_en/java/basis/reflection.en.md @@ -0,0 +1,192 @@ +--- +title: Detailed explanation of Java reflection mechanism +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: Reflection, Class, Method, Field, dynamic proxy, runtime analysis, framework principles + - - meta + - name: description + content: Systematically explains the core concepts and common usage of Java reflection, and understands runtime capabilities by combining dynamic proxy and the underlying mechanism of the framework. +--- + +## What is reflection? + +If you have studied the underlying principles of the framework or have written the framework yourself, you must be familiar with the concept of reflection. + +Reflection is called the soul of the framework mainly because it gives us the ability to analyze classes and execute methods in classes at runtime. + +Through reflection you can obtain all properties and methods of any class, and you can also call these methods and properties. + +## Do you know the application scenarios of reflection? + +For example, we usually write business code most of the time, and we rarely come into contact with scenarios where the reflection mechanism is directly used. + +However, this does not mean that reflection is useless. On the contrary, it is reflection that allows you to use various frameworks so easily. Frameworks such as Spring/Spring Boot, MyBatis, etc. all use reflection mechanisms extensively. + +**Dynamic proxies are also used extensively in these frameworks, and the implementation of dynamic proxies also relies on reflection. ** + +For example, the following is a sample code for implementing dynamic proxy through JDK, 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; + + public DebugInvocationHandler(Object target) { + 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); + System.out.println("after method " + method.getName()); + return result; + } +} + +``` + +In addition, the implementation of **annotations**, a powerful tool in Java, also uses reflection. + +Why does a `@Component` annotation declare a class as a Spring Bean when you use Spring? Why do you read the value in the configuration file through a `@Value` annotation? How exactly does it work? + +These are all because you can analyze the class based on reflection and then obtain the annotations on the parameters of the class/property/method/method. After you obtain the annotations, you can perform further processing. + +## Let’s talk about the advantages and disadvantages of the reflection mechanism + +**Advantages**: It can make our code more flexible and provide convenience for various frameworks to provide out-of-the-box functions. + +**Disadvantages**: It gives us the ability to analyze operation classes at runtime, which also increases security issues. For example, you can ignore the security check of generic parameters (the security check of generic parameters occurs at compile time). In addition, the performance of reflection is slightly worse, but it actually has little impact on the framework. Related reading: [Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) + +## Reflection in practice + +### Four ways to obtain Class objects + +If we obtain this information dynamically, we need to rely on the Class object. The Class object tells the running program about the methods, variables and other information of a class. Java provides four ways to obtain Class objects: + +**1. You can use it if you know the specific class:** + +```java +Class alunbarClass = TargetObject.class; +``` + +But we generally don’t know the specific class. We basically obtain the Class object by traversing the classes under the package. Obtaining the Class object in this way will not be initialized. + +**2. Get the full path of the incoming class through `Class.forName()`: ** + +```java +Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); +``` + +**3. Obtain through object instance `instance.getClass()`: ** + +```java +TargetObject o = new TargetObject(); +Class alunbarClass2 = o.getClass(); +``` + +**4. Pass in the class path through the class loader `xxxClassLoader.loadClass()` to obtain: ** + +```java +ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject"); +``` + +Obtaining the Class object through the class loader will not be initialized, which means that the static code block and static object will not be executed without a series of steps including initialization. + +### Some basic operations of reflection + +1. Create a class `TargetObject` that we want to use reflection operations on. + +```java +package cn.javaguide; + +public class TargetObject { + private String value; + + public TargetObject() { + value = "JavaGuide"; + } + + public void publicMethod(String s) { + System.out.println("I love " + s); + } + + private void privateMethod() { + System.out.println("value is " + value); + } +} +``` + +2. Use reflection to operate the methods and properties of this class + +```java +package cn.javaguide; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Main { + public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { + /** + * Get the Class object of the TargetObject class and create a TargetObject class instance + */ + Class targetClass = Class.forName("cn.javaguide.TargetObject"); + TargetObject targetObject = (TargetObject) targetClass.newInstance(); + /** + * Get all methods defined in the TargetObject class + */ + Method[] methods = targetClass.getDeclaredMethods(); + for (Method method : methods) { + System.out.println(method.getName()); + } + + /** + * Get the specified method and call it + */ + Method publicMethod = targetClass.getDeclaredMethod("publicMethod", + String.class); + + publicMethod.invoke(targetObject, "JavaGuide"); + + /** + * Get the specified parameters and modify them + */ + Field field = targetClass.getDeclaredField("value"); + //In order to modify the parameters in the class, we cancel the security check + field.setAccessible(true); + field.set(targetObject, "JavaGuide"); + + /** + * Call private method + */ + Method privateMethod = targetClass.getDeclaredMethod("privateMethod"); + //In order to call the private method we cancel the security check + privateMethod.setAccessible(true); + privateMethod.invoke(targetObject); + } +}``` + +Output content: + +```plain +publicMethod +privateMethod +I love JavaGuide +value is JavaGuide +``` + +**Note**: Some readers mentioned that the above code will throw a `ClassNotFoundException` exception when running. The specific reason is that you did not replace the package name of this code with the package where the `TargetObject` you created is located. +You can refer to: this article. + +```java +Class targetClass = Class.forName("cn.javaguide.TargetObject"); +``` + + \ No newline at end of file diff --git a/docs_en/java/basis/serialization.en.md b/docs_en/java/basis/serialization.en.md new file mode 100644 index 00000000000..b6d5d9d1363 --- /dev/null +++ b/docs_en/java/basis/serialization.en.md @@ -0,0 +1,234 @@ +--- +title: Java 序列化详解 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: 序列化,反序列化,Serializable,transient,serialVersionUID,ObjectInputStream,ObjectOutputStream,协议 + - - meta + - name: description + content: 讲解 Java 对象的序列化/反序列化机制与关键细节,涵盖 transient、版本号与常见应用场景。 +--- + +## 什么是序列化和反序列化? + +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +简单来说: + +- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 +- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 + +对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 + +下面是序列化和反序列化常见应用场景: + +- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化; +- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化; +- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化; +- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 + +维基百科是如是介绍序列化的: + +> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** + +![](https://oss.javaguide.cn/github/javaguide/a478c74d-2c48-40ae-9374-87aacf05188c.png) + +

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

+ +**序列化协议对应于 TCP/IP 4 层模型的哪一层?** + +我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢? + +1. 应用层 +2. 传输层 +3. 网络层 +4. 网络接口层 + +![TCP/IP 四层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png) + +如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么? + +因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。 + +## 常见序列化协议有哪些? + +JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 + +像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 + +### JDK 自带的序列化方式 + +JDK 自带的序列化,只需实现 `java.io.Serializable`接口即可。 + +```java +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +@ToString +public class RpcRequest implements Serializable { + private static final long serialVersionUID = 1905122041950251207L; + private String requestId; + private String interfaceName; + private String methodName; + private Object[] parameters; + private Class[] paramTypes; + private RpcMessageTypeEnum rpcMessageTypeEnum; +} +``` + +**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` 不是作为对象状态的一部分被序列化的,而是被序列化机制本身用作一个特殊的“指纹”或“版本号”。 + +当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中(像是在保存一个版本号,而不是保存 `static` 变量本身的状态);在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `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` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。 + +**如果有些字段不想进行序列化怎么办?** + +对于不想进行序列化的变量,可以使用 `transient` 关键字修饰。 + +`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 + +关于 `transient` 还有几点注意: + +- `transient` 只能修饰变量,不能修饰类和方法。 +- For variables modified with `transient`, the variable value will be set to the default value of the type after deserialization. For example, if the `int` type is modified, the result after deserialization is `0`. +- `static` variables do not belong to any object (Object), so they will not be serialized regardless of whether they are modified with the `transient` keyword. + +**Why is it not recommended to use the serialization that comes with JDK? ** + +We rarely or almost never directly use the serialization method that comes with JDK. The main reasons are as follows: + +- **Cross-language calling is not supported**: It is not supported if services developed in other languages are called. +- **Poor performance**: Compared with other serialization frameworks, the performance is lower. The main reason is that the byte array after serialization is larger in size, resulting in increased transmission costs. +- **Security Issue**: There is no problem with serialization and deserialization per se. But when the input deserialized data can be controlled by the user, then the attacker can construct malicious input, let the deserialization generate unexpected objects, and execute the constructed arbitrary code in the process. Related reading: [Application Security: JAVA Deserialization Vulnerability - Cryin](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/), [What’s going on with Java Deserialization Security Vulnerability? - Monica](https://www.zhihu.com/question/37562657/answer/1916596031). + +### Kryo + +Kryo is a high-performance serialization/deserialization tool that has high running speed and small bytecode size due to its variable-length storage characteristics and the use of bytecode generation mechanism. + +In addition, Kryo is already a very mature serialization implementation and has been widely used in Twitter, Groupon, Yahoo, and many well-known open source projects (such as Hive and Storm). + +[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) uses kryo for serialization. The codes related to serialization and deserialization are as follows: + +```java +/** + * Kryo serialization class, Kryo serialization efficiency is very high, but only compatible with Java language + * + * @author shuang.kou + * @createTime May 13, 2020 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: Serialize the object into a byte array + 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: Deserialize the object from the byte array + Object o = kryo.readObject(input, clazz); + kryoThreadLocal.remove(); + return clazz.cast(o); + } catch (Exception e) { + throw new SerializeException("Deserialization failed"); + } + } + +} +``` + +GitHub address: [https://github.com/EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo). + +### Protobuf + +Protobuf comes from Google, has excellent performance, supports multiple languages, and is cross-platform. It is too cumbersome to use because you need to define the IDL file yourself and generate the corresponding serialization code. Although this is inflexible, on the other hand, protobuf does not have the risk of serialization vulnerabilities. + +> Protobuf contains definitions of serialization formats, libraries for various languages, and an IDL compiler. Normally you need to define a proto file and then use the IDL compiler to compile it into the language you need + +A simple proto file is as follows: + +```protobuf +// version of protobuf +syntax = "proto3"; +// SearchRequest will be compiled into corresponding objects in different programming languages, such as class in Java and struct in Go. +message Person { + //string type field + string name = 1; + // int type field + int32 age = 2; +} +``` + +GitHub address: [https://github.com/protocolbuffers/protobuf](https://github.com/protocolbuffers/protobuf). + +### ProtoStuff + +Due to Protobuf's poor ease of use, its older brother Protostuff was born. + +protostuff is based on Google protobuf, but provides more features and easier usage. Although it is easier to use, it does not mean that ProtoStuff's performance is worse. + +GitHub address: [https://github.com/protostuff/protostuff](https://github.com/protostuff/protostuff). + +### Hessian + +Hessian is a lightweight, custom-described binary RPC protocol. Hessian is an older serialization implementation and is also cross-language. + +![](https://oss.javaguide.cn/github/javaguide/8613ec4c-bde5-47bf-897e-99e0f90b9fa3.png) + +The serialization method enabled by default in Dubbo2.x is Hessian2. However, Dubbo has modified Hessian2, but the general structure is still the same. + +### Summary + +Kryo is a serialization method specifically for the Java language and has very good performance. If your application is specifically for the Java language, you can consider using it. An article on the Dubbo official website mentioned that it is recommended to use Kryo as the serialization method for production environments. (Article address: ). + +![](https://oss.javaguide.cn/github/javaguide/java/569e541a-22b2-4846-aa07-0ad479f07440-20230814090158124.png)Things like Protobuf, ProtoStuff, and hessian are all cross-language serialization methods. You can consider using them if you have cross-language requirements. + +In addition to the serialization methods I introduced above, there are also things like Thrift and Avro. + + \ No newline at end of file diff --git a/docs_en/java/basis/spi.en.md b/docs_en/java/basis/spi.en.md new file mode 100644 index 00000000000..c357593f9d7 --- /dev/null +++ b/docs_en/java/basis/spi.en.md @@ -0,0 +1,562 @@ +--- +title: Java SPI 机制详解 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: Java SPI机制 + - - meta + - name: description + content: SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 +--- + +> 本文来自 [Kingshion](https://github.com/jjx0708) 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) 。 + +面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。为了解决这个问题,SPI 应运而生,它提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。 + +SPI 机制也解决了 Java 类加载体系中双亲委派模型带来的限制。[双亲委派模型](https://javaguide.cn/java/jvm/classloader.html)虽然保证了核心库的安全性和一致性,但也限制了核心库或扩展库加载应用程序类路径上的类(通常由第三方实现)。SPI 允许核心或扩展库定义服务接口,第三方开发者提供并部署实现,SPI 服务加载机制则在运行时动态发现并加载这些实现。例如,JDBC 4.0 及之后版本利用 SPI 自动发现和加载数据库驱动,开发者只需将驱动 JAR 包放置在类路径下即可,无需使用`Class.forName()`显式加载驱动类。 + +## SPI 介绍 + +### 何谓 SPI? + +SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 + +SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + +很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 + + + +### SPI 和 API 有什么区别? + +**那 SPI 和 API 有啥区别?** + +说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: + +![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) + +一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 + +- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 +- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 + +举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 + +## 实战演示 + +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)); + } +} +``` + +Create a new `Main` class (service user, caller), start the program and view the results. + +```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"); + } +} +``` + +Program results: + +> Logger service provider not found in info +> No Logger service provider found in debug + +At this time, we only have an empty interface and do not provide any implementation for the `Logger` interface, so the corresponding results are not printed as expected in the output results. + +You can use commands or use IDEA directly to package the entire program into a jar package. + +### Service Provider + +Next, create a new project to implement the `Logger` interface + +The directory structure of the new project `service-provider` is as follows: + +```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 + +``` + +Create a new `Logback` class + +```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 print log: " + s); + } + + @Override + public void debug(String s) { + System.out.println("Logback debug print log: " + s); + } +} + +``` + +Import the jar of `service-provider-interface` into the project. + +Create a new lib directory, copy the jar package, and add it to the project. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/523d5e25198444d3b112baf68ce49daetplv-k3u1fbpfcp-watermark.png) + +Click OK again. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/f4ba0aa71e9b4d509b9159892a220850tplv-k3u1fbpfcp-watermark.png) + +Next, you can import some classes and methods in the jar package into the project, just like the JDK tool class import package. + +Implement the `Logger` interface, create a new `META-INF/services` folder in the `src` directory, and then create a new file `edu.jiangxuan.up.spi.Logger` (the full class name of SPI). The content in the file is: `edu.jiangxuan.up.spi.service.Logback` (the full class name of Logback, that is, the package name + class name of the SPI implementation class). + +**This is the standard agreed upon by the JDK SPI mechanism ServiceLoader. ** + +Here is a brief explanation: The SPI mechanism in Java is that every time a class is loaded, it will first find the files in the services folder under the `META-INF` folder in the directory relative to the class, load all the files under this folder into the memory, and then find the specific implementation class of the corresponding interface based on the file names and file contents of these files. After finding the implementation class, you can use reflection to generate the corresponding object and save it in a list In the list, you can get the corresponding instance object through iteration or traversal to generate different implementations. + +Therefore, some standard requirements will be put forward: the file name must be the full class name of the interface, and the content inside must be the full class name of the implementation class. There can be multiple implementation classes, just wrap it in a new line. When there are multiple implementation classes, they will be loaded one by one iteratively. + +Next, also package the `service-provider` project into a jar package. This jar package is the implementation of the service provider. Usually the pom dependency we import into maven is similar to this, except that we have not published this jar package to the maven public warehouse, so we can only manually add it to the project where it needs to be used. + +### Effect display + +In order to display the effect more intuitively, I will create a new project here specifically for testing: `java-spi-test` + +Then first import the interface jar package of `Logger`, and then import the jar package of the specific implementation class. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/image-20220723215812708-165858469599214.png) + +Create a new Main method test: + +```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("Hello"); + loggerService.debug("Test Java SPI mechanism"); + } +} +``` + +The running results are as follows: + +> Logback info print log: Hello +> Logback debug print log: test Java SPI mechanism + +It means that the implementation class imported into the jar package has taken effect. + +If we do not import the jar package of the specific implementation class, then the result of the program running at this time will be: + +> Logger service provider not found in info +> No Logger service provider found in debug + +By using the SPI mechanism, we can see that the coupling between the service (`LoggerService`) and the service provider is very low. If we want to change to another implementation, we only need to modify the specific implementation of the `Logger` interface in the `service-provider` project. We only need to change a jar package, or there can be multiple implementations in one project. Isn't this the principle of SLF4J? + +If the requirements change one day, and you need to output the log to the message queue, or do some other operations, there is no need to change the implementation of Logback at this time. You only need to add a service implementation (service-provider). You can add a new implementation in this project or introduce a new service implementation jar package from the outside. We can select a specific service implementation (service-provider) in the service (LoggerService) to complete the operations we need. + +Then let’s talk about the key principles of Java SPI work in detail - **ServiceLoader**. + +## ServiceLoader + +### ServiceLoader specific implementation + +If you want to use Java's SPI mechanism, you need to rely on `ServiceLoader` to achieve it, so let's take a look at how `ServiceLoader` does it specifically: + +`ServiceLoader` is a tool class provided by JDK, located under the `package java.util;` package. + +```plain +A facility to load implementations of a service. +``` + +This is the official comment from the JDK: **A tool for loading service implementations. ** + +Looking further down, we find that this class is a `final` type, so it cannot be modified by inheritance, and it implements the `Iterable` interface. The reason why iterators are implemented is to facilitate the subsequent we can obtain the corresponding service implementation through iteration. + +```java +public final class ServiceLoader implements Iterable{ xxx...} +``` + +You can see a familiar constant definition:`private static final String PREFIX = "META-INF/services/";` + +The following is the `load` method: You can find that the `load` method supports two overloaded input parameters; + +```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); +} +``` + +The mechanism for solving third-party class loading is actually contained in `ClassLoader cl = Thread.currentThread().getContextClassLoader();`, `cl` is **Thread Context ClassLoader** (Thread Context ClassLoader). This is a class loader held by each thread. The JDK is designed to allow an application or container (such as a web application server) to set this class loader so that the core class library can load application classes through it. + +The thread context class loader is the application class loader (Application ClassLoader) by default, which is responsible for loading classes on the classpath. When the core library needs to load an application-provided class, it can do so using a thread context class loader. This way, even core library code loaded by the bootstrap class loader can load and use classes loaded by the application class loader. + +According to the calling sequence of the code, the `reload()` method is implemented through an inner class `LazyIterator`. Let’s continue looking below. + +After `ServiceLoader` implements the method of `Iterable` interface, it has the ability to iterate. When this `iterator` method is called, it will first search in the `Provider` cache of `ServiceLoader`. If there is no hit in the cache, it will search in `LazyIterator`. + +```java + +public Iterator iterator() { + return new Iterator() { + + Iterator> knownProviders + = providers.entrySet().iterator(); + + public boolean hasNext() { + if (knownProviders.hasNext()) + return true; + return lookupIterator.hasNext(); // Call LazyIterator + } + + public S next() { + if (knownProviders.hasNext()) + return knownProviders.next().getValue(); + return lookupIterator.next(); // Call LazyIterator + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + }; +} +``` + +When calling `LazyIterator`, the specific implementation is as follows: + +```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 { + //Get the corresponding configuration file through PREFIX (META-INF/services/) and class name to get the specific implementation class + 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 +}``` + +Many people may find this a bit complicated, but it doesn’t matter. I have implemented a simple small model of `ServiceLoader`. The process and principles are consistent. You can start by implementing a simple version yourself: + +### Implement a ServiceLoader yourself + +I'll post the code first: + +```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 { + + //Corresponding interface Class template + private final Class service; + + // There can be multiple corresponding implementation classes, encapsulated with List + private final List providers = new ArrayList<>(); + + // class loader + private final ClassLoader classLoader; + + // A method exposed to external use. By calling this method, you can start loading your own customized implementation process. + public static MyServiceLoader load(Class service) { + return new MyServiceLoader<>(service); + } + + // Privatize the constructor + private MyServiceLoader(Class service) { + this.service = service; + this.classLoader = Thread.currentThread().getContextClassLoader(); + doLoad(); + } + + //Key method, loading the logic of the specific implementation class + private void doLoad() { + try { + // Read the files under the META-INF/services package in all jar packages. The file name is the interface name, and the content in the file is the path to the specific implementation class plus the full class name. + Enumeration urls = classLoader.getResources("META-INF/services/" + service.getName()); + // Traverse the retrieved files one by one + while (urls.hasMoreElements()) { + // Get the current file + URL url = urls.nextElement(); + System.out.println("File = " + url.getPath()); + // Create link + URLConnection urlConnection = url.openConnection(); + urlConnection.setUseCaches(false); + // Get the file input stream + InputStream inputStream = urlConnection.getInputStream(); + // Get cache from file input stream + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + // Get the full class name of the implementation class from the file content + String className = bufferedReader.readLine(); + + while (className != null) { + // Get the instance of the implementation class through reflection + Class clazz = Class.forName(className, false, classLoader); + // If the declared interface is of the same type as the specific implementation class, (which can be understood as a kind of polymorphism in Java, the relationship between the interface and the implementation class, parent class, subclass, etc.), then construct an instance + if (service.isAssignableFrom(clazz)) { + Constructor constructor = (Constructor) clazz.getConstructor(); + S instance = constructor.newInstance(); + //Add the currently constructed instance object to the Provider's list + providers.add(instance); + } + // Continue to read the implementation class of the next line. There can be multiple implementation classes, and you only need to wrap the line. + className = bufferedReader.readLine(); + } + } + } catch (Exception e) { + System.out.println("Exception in reading file..."); + } + } + + //Return the list of specific implementation classes corresponding to the spi interface + public List getProviders() { + return providers; + } +} +``` + +The key information has basically been described through code comments. + +The main process is: + +1. Find the corresponding file from the `/META-INF/services` directory of the jar package through the URL tool class. +2. Read the name of this file to find the corresponding spi interface. +3. Read the full class name of the specific implementation class in the file through the `InputStream` stream. +4. Based on the obtained full class name, first determine whether it is the same type as the spi interface. If so, then construct the corresponding instance object through the reflection mechanism. +5. Add the constructed instance object to the list of `Providers`. + +## Summary + +In fact, it is not difficult to find that the specific implementation of the SPI mechanism is essentially completed through reflection. That is: **We declare the specific implementation class to be exposed for external use in the `META-INF/services/` file according to regulations. ** + +In addition, the SPI mechanism is applied in many frameworks: the basic principles of the Spring framework are also similar. There is also the Dubbo framework that provides the same SPI extension mechanism. However, the specific implementation of the SPI mechanism in the Dubbo and spring frameworks is slightly different from what we learned today. However, the overall principles are the same. I believe that by studying the SPI mechanism in the JDK, you can understand everything and deepen your understanding of other advanced frameworks. + +The SPI mechanism can greatly improve the flexibility of interface design, but the SPI mechanism also has some shortcomings, such as: + +1. Iteratively loads all implementation classes, which is relatively inefficient; +2. When multiple `ServiceLoader` `load` at the same time, there will be concurrency problems. + + \ No newline at end of file diff --git a/docs_en/java/basis/syntactic-sugar.en.md b/docs_en/java/basis/syntactic-sugar.en.md new file mode 100644 index 00000000000..9dbea3abead --- /dev/null +++ b/docs_en/java/basis/syntactic-sugar.en.md @@ -0,0 +1,885 @@ +--- +title: Detailed explanation of Java syntactic sugar +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: syntactic sugar, automatic boxing and unboxing, generics, enhanced for, variable parameters, enumerations, inner classes, type inference + - - meta + - name: description + content: Summarizes common syntax sugars in Java and the "sugar-unblocking" principle at compile time, helping to improve efficiency while understanding the underlying mechanism and avoiding misuse. +--- + +> Author: Hollis +> +> Original text: + +Syntactic sugar is a common knowledge point asked in Java interviews at major companies. + +From the perspective of Java compilation principles, this article goes deep into bytecode and class files, peels off the cocoons, and understands the principles and usage of syntactic sugar in Java. It helps everyone learn how to use Java syntactic sugar while also understanding the principles behind these syntactic sugars. + +## What is syntactic sugar? + +**Syntactic Sugar**, also known as sugar-coated syntax, is a term invented by British computer scientist Peter.J. Landin. It refers to a certain syntax added to a computer language. This syntax has no impact on the function of the language, but is more convenient for programmers to use. In short, syntactic sugar makes programs more concise and more readable. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/syntactic-sugar/image-20220818175953954.png) + +> Interestingly, in the field of programming, in addition to syntactic sugar, there are also terms such as syntactic salt and syntactic saccharine. The space is limited and I will not expand on this here. + +There is syntactic sugar in almost every programming language we are familiar with. The author believes that the amount of syntactic sugar is one of the criteria for judging whether a language is awesome or not. Many people say that Java is a "low-sugar language". In fact, various sugars have been added to the Java language since Java 7, mainly developed under the "Project Coin" project. Although some people still think that Java is low in sugar now, it will continue to develop in the direction of "high sugar" in the future. + +## What are the common syntactic sugars in Java? + +As mentioned earlier, the existence of syntactic sugar is mainly for the convenience of developers. But in fact, the **Java virtual machine does not support these syntactic sugars. These syntax sugars will be reduced to simple basic syntax structures during the compilation phase. This process is the decoding of syntax sugars. ** + +Speaking of compilation, everyone must know that in the Java language, the `javac` command can compile the source file with the suffix `.java` into bytecode with the suffix `.class` 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 one step in `compile()` is to call `desugar()`. This method is responsible for decoding the implementation of syntax sugar. + +The most commonly used syntactic sugars in Java include generics, variable-length parameters, conditional compilation, automatic unboxing, inner classes, etc. This article mainly analyzes the principles behind these syntactic sugars. Step by step, peel off the sugar coating and see the essence. + +We will use [decompilation](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650120609&idx=1&sn=5659f96310963ad57d55b48cee63c7 88&chksm=f36bbc80c41c3596a1e4bf9501c6280481f1b9e06d07af354474e6f3ed366fef016df673a7ba&scene=21#wechat_redirect), you can pass [Decompilers online](http://www.javadecompilers.com/) Decompiles Class files online. + +### switch supports String and enumeration + +As mentioned earlier, starting from Java 7, the syntactic sugar in the Java language has gradually become richer. One of the more important ones is that `switch` in Java 7 begins to support `String`. + +Before we start, let’s do some popular science. `switch` in Java itself supports basic types. For example, `int`, `char`, etc. For `int` types, numerical comparisons are performed directly. For the `char` type, its ascii code is compared. Therefore, for the compiler, only integers can be used in `switch`, and any type of comparison must be converted to integers. For example `byte`. `short`, `char` (ascii code is integer type) and `int`. + +Then let’s take a look at `switch`’s support for `String`. There is the following code: + +```java +public class switchDemoString { + public static void main(String[] args) { + String str = "world"; + switch (str) { + case "hello": + System.out.println("hello"); + break; + case "world": + System.out.println("world"); + break; + default: + break; + } + } +} +``` + +The decompiled content is as follows: + +```java +public class switchDemoString +{ + public switchDemoString() + { + } + public static void main(String args[]) + { + String str = "world"; + String s; + switch((s = str).hashCode()) + { + default: + break; + case 99162322: + if(s.equals("hello")) + System.out.println("hello"); + break; + case 113318802: + if(s.equals("world")) + System.out.println("world"); + break; + } + } +} +``` + +Seeing this code, you know that the original **string switch is implemented through the `equals()` and `hashCode()` methods. ** Fortunately, the `hashCode()` method returns `int`, not `long`. + +A closer look reveals that what is being switched is actually the hash value, and then a security check is performed by comparing it using the equals method. This check is necessary because the hashes may collide. So its performance is not as good as using enums for `switch` or using pure integer constants, but it's not bad either. + +### Generics + +We all know that many languages support generics, but what many people don't know is that different compilers handle generics in different ways. Usually, a compiler has two ways to handle generics: `Code specialization` and `Code sharing`. C++ and C# use the `Code specialization` processing mechanism, while Java uses the `Code sharing` mechanism. + +> Code sharing creates a unique bytecode representation for each generic type, and maps instances of the generic type to this unique bytecode representation. Mapping multiple instances of a generic type to a unique bytecode representation is achieved through type erasure (`type erasue`). + +In other words, **for the Java virtual machine, he does not know the syntax of `Map map` at all. Syntactic sugar needs to be decoded through type erasure during the compilation phase. ** + +The main process of type erasure is as follows: 1. Replace all generic parameters with their leftmost boundary (top-level parent type) type. 2. Remove all type parameters. + +The following code: + +```java +Map map = new HashMap(); +map.put("name", "hollis"); +map.put("wechat", "Hollis"); +map.put("blog", "www.hollischuang.com"); +``` + +After decoding the syntax sugar, it becomes: + +```java +Map map = new HashMap(); +map.put("name", "hollis"); +map.put("wechat", "Hollis"); +map.put("blog", "www.hollischuang.com"); +``` + +The following code: + +```java +public static > A max(Collection xs) { + Iterator xi = xs.iterator(); + A w = xi.next(); + while (xi.hasNext()) { + A x = xi.next(); + if (w.compareTo(x) < 0) + w = x; + } + return w; +}``` + +After type erasure it will become: + +```java + public static Comparable max(Collection xs){ + Iterator xi = xs.iterator(); + Comparable w = (Comparable)xi.next(); + while(xi.hasNext()) + { + Comparable x = (Comparable)xi.next(); + if(w.compareTo(x) < 0) + w = x; + } + return w; +} +``` + +**There are no generics in the virtual machine, only ordinary classes and ordinary methods. The type parameters of all generic classes will be erased at compile time. Generic classes do not have their own unique `Class` class object. For example, there is no `List.class` or `List.class`, but only `List.class`. ** + +### Automatic boxing and unboxing + +Automatic boxing means Java automatically converts primitive type values into corresponding objects. For example, converting an int variable into an Integer object is called boxing. Conversely, converting an Integer object into an int type value is called unboxing. Because the boxing and unboxing here are automatic and non-human conversions, they are called automatic boxing and unboxing. The corresponding encapsulation classes of primitive types byte, short, char, int, long, float, double and boolean are Byte, Short, Character, Integer, Long, Float, Double and Boolean. + +Let’s first look at the autoboxing code: + +```java + public static void main(String[] args) { + int i = 10; + Integer n = i; +} +``` + +The decompiled code is as follows: + +```java +public static void main(String args[]) +{ + int i = 10; + Integer n = Integer.valueOf(i); +} +``` + +Let’s look at the code for automatic unboxing: + +```java +public static void main(String[] args) { + + Integer i = 10; + int n = i; +} +``` + +The decompiled code is as follows: + +```java +public static void main(String args[]) +{ + Integer i = Integer.valueOf(10); + int n = i.intValue(); +} +``` + +It can be seen from the decompiled content that the `valueOf(int)` method of `Integer` is automatically called during boxing. What is automatically called during unboxing is the `intValue` method of `Integer`. + +Therefore, the **boxing process is implemented by calling the wrapper's valueOf method, and the unboxing process is implemented by calling the wrapper's xxxValue method. ** + +### Variable length parameters + +Variable arguments (`variable arguments`) are a feature introduced in Java 1.5. It allows a method to take any number of values ​​as parameters. + +Consider the following variadic code, where the `print` method receives variadic arguments: + +```java +public static void main(String[] args) + { + print("Holis", "Public account: Hollis", "Blog: www.hollischuang.com", "QQ: 907607222"); + } + +public static void print(String... strs) +{ + for (int i = 0; i < strs.length; i++) + { + System.out.println(strs[i]); + } +} +``` + +Decompiled code: + +```java + public static void main(String args[]) +{ + print(new String[] { + "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222" + }); +} + +public static transient void print(String strs[]) +{ + for(int i = 0; i < strs.length; i++) + System.out.println(strs[i]); + +} +``` + +It can be seen from the decompiled code that when variable parameters are used, they will first create an array. The length of the array is the number of actual parameters passed when calling the method, and then put all the parameter values ​​into this array, and then pass this array as a parameter to the called method. (Note: `transient` is only meaningful when modifying member variables. The "modified method" here is because the same value is used in javassist to represent `transient` and `vararg` respectively. See [Here](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32). + +### Enumeration + +Java SE5 provides a new type - Java's enumeration type. The keyword `enum` can create a limited set of named values into a new type, and these named values can be used as regular program components. This is a very useful feature. + +If you want to see the source code, you must first have a class. So what is the enumeration type? Is it `enum`? The answer is obviously no. `enum` is just like `class`. It is just a keyword. It is not a class. So what class is the enumeration maintained by? We simply write an enumeration: + +```java +public enum t { + SPRING,SUMMER; +} +``` + +Then we use decompilation to see how this code is implemented. The code content after decompilation is as follows: + +```java +//The Java compiler will automatically process the enumeration name into a legal class name (the first letter is capitalized): t -> T +public final class T extends Enum +{ + private T(String s, int i) + { + super(s, i); + } + public static T[] values() + { + T at[]; + int i; + T at1[]; + System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i); + return at1; + } + + public static T valueOf(String s) + { + return (T)Enum.valueOf(demo/T, s); + } + + public static final T SPRING; + public static final T SUMMER; + private static final T ENUM$VALUES[]; + static + { + SPRING = new T("SPRING", 0); + SUMMER = new T("SUMMER", 1); + ENUM$VALUES = (new T[] { + SPRING, SUMMER + }); + } +} +``` + +By decompiling the code, we can see that `public final class T extends Enum`, indicating that this class inherits the `Enum` class, and the `final` keyword tells us that this class cannot be inherited. + +**When we use `enum` to define an enumeration type, the compiler will automatically help us create a `final` type class that inherits the `Enum` class, so the enumeration type cannot be inherited. ** + +### Inner class + +Inner classes are also called nested classes. The inner class can be understood as an ordinary member of the outer class. + +**The reason why inner classes are also syntactic sugar is that they are just a compile-time concept. `outer.java` defines an inner class `inner`. Once compiled successfully, two completely different `.class` files will be generated, namely `outer.class` and `outer$inner.class`. So the name of the inner class can be the same as the name of its outer class. ** + +```java +public class OuterClass { + private String userName; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public static void main(String[] args) { + + } + + class InnerClass{ + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +}``` + +After the above code is compiled, two class files will be generated: `OuterClass$InnerClass.class` and `OuterClass.class`. When we try to decompile the `OuterClass.class` file, the command line will print the following: `Parsing OuterClass.class...Parsing inner class OuterClass$InnerClass.class... Generating OuterClass.jad`. He will decompile both files and generate an `OuterClass.jad` file together. The contents of the file are as follows: + +```java +public class OuterClass +{ + classInnerClass + { + public String getName() + { + return name; + } + public void setName(String name) + { + this.name = name; + } + private String name; + final OuterClass this$0; + + InnerClass() + { + this.this$0 = OuterClass.this; + super(); + } + } + + public OuterClass() + { + } + public String getUserName() + { + return userName; + } + public void setUserName(String userName){ + this.userName = userName; + } + public static void main(String args1[]) + { + } + private String userName; +} +``` + +**Why inner classes can use private attributes of outer classes**: + +We add a method in InnerClass to print the userName attribute of the external class + +```java +//Omit other attributes +public class OuterClass { + private String userName; + ... + class InnerClass{ + ... + public void printOut(){ + System.out.println("Username from OuterClass:"+userName); + } + } +} + +// At this point, use the javap -p command to decompile OuterClass: +public classOuterClass { + private String userName; + ... + static String access$000(OuterClass); +} +// At this point, the decompilation result of InnerClass: +class OuterClass$InnerClass { + final OuterClass this$0; + ... + public void printOut(); +} + +``` + +In fact, after compilation is completed, the inner instance will have a reference to the outer instance `this$0`, but a simple `outer.name` cannot access the private property. From the decompilation results, we can see that there is a bridge method `static String access$000(OuterClass)` in outer, which happens to return the String type, that is, the userName attribute. It is through this method that the inner class accesses the private properties of the outer class. So the decompiled `printOut()` method is roughly as follows: + +```java +public void printOut() { + System.out.println("Username from OuterClass:" + OuterClass.access$000(this.this$0)); +} +``` + +Supplement: + +1. Anonymous inner classes, local inner classes, and static inner classes also obtain private attributes through bridge methods. +2. The static inner class has no reference to `this$0` +3. Anonymous inner classes and local inner classes use local variables through copying, and the variables cannot be modified after they are initialized. The following is an example: + +```java +public class OuterClass { + private String userName; + + public void test(){ + //Here i can no longer be modified after it is initialized to 1. + int i=1; + class Inner{ + public void printName(){ + System.out.println(userName); + System.out.println(i); + } + } + } +} +``` + +After decompilation: + +```java +//javap command decompiles the result of Inner +//i is copied into the inner class and is final +class OuterClass$1Inner { + final int val$i; + final OuterClass this$0; + OuterClass$1Inner(); + public void printName(); +} + +``` + +### Conditional compilation + +- Under normal circumstances, every line of code in the program must be compiled. But sometimes for the purpose of optimizing the program code, you want to compile only part of it. In this case, you need to add conditions to the program, so that the compiler will only compile the code that meets the conditions and discard the code that does not meet the conditions. This is conditional compilation. + +As in C or CPP, conditional compilation can be achieved through prepared statements. In fact, conditional compilation can also be implemented in Java. Let’s look at a piece of code first: + +```java +public class ConditionalCompilation { + public static void main(String[] args) { + final boolean DEBUG = true; + if(DEBUG) { + System.out.println("Hello, DEBUG!"); + } + + final boolean ONLINE = false; + + if(ONLINE){ + System.out.println("Hello, ONLINE!"); + } + } +} +``` + +The decompiled code is as follows: + +```java +public class ConditionalCompilation +{ + + publicConditionalCompilation() + { + } + + public static void main(String args[]) + { + boolean DEBUG = true; + System.out.println("Hello, DEBUG!"); + boolean ONLINE = false; + } +} +``` + +First of all, we found that there is no `System.out.println("Hello, ONLINE!");` in the decompiled code. This is actually conditional compilation. When `if(ONLINE)` is false, the compiler does not compile the code inside it. + +Therefore, the conditional compilation of **Java syntax is implemented through the if statement that determines the condition to be a constant. Its principle is also syntactic sugar for the Java language. According to the if condition's true or false judgment, the compiler directly eliminates the code block with a false branch. The conditional compilation implemented in this way must be implemented in the method body, and conditional compilation cannot be performed on the structure of the entire Java class or the attributes of the class. This is indeed more limited than the conditional compilation of C/C++. At the beginning of the Java language design, the function of conditional compilation was not introduced. Although it has limitations, it is better than nothing. ** + +### Assertion + +In Java, the `assert` keyword was introduced from JAVA SE 1.4. In order to avoid errors caused by using the `assert` keyword in older versions of Java code, Java does not enable assertion checking by default during execution (at this time, all assertion statements will be ignored!). If you want to enable assertion checking, you need to use the switch `-enableassertions` or `-ea` to enable it. + +Look at a piece of code that contains assertions: + +```java +public class AssertTest { + public static void main(String args[]) { + int a = 1; + int b = 1; + assert a == b; + System.out.println("Public account: Hollis"); + assert a != b : "Hollis"; + System.out.println("Blog: www.hollischuang.com"); + } +}``` + +The decompiled code is as follows: + +```java +public class AssertTest { + public AssertTest() + { + } + public static void main(String args[]) +{ + int a = 1; + int b = 1; + if(!$assertionsDisabled && a != b) + throw new AssertionError(); + System.out.println("\u516C\u4F17\u53F7\uFF1AHollis"); + if(!$assertionsDisabled && a == b) + { + throw new AssertionError("Hollis"); + } else + { + System.out.println("\u535A\u5BA2\uFF1Awww.hollischuang.com"); + return; + } +} + +static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus(); + +} +``` + +Obviously, the decompiled code is much more complex than our own code. Therefore, we save a lot of code by using the syntactic sugar of assert. **In fact, the underlying implementation of assertion is the if language. If the assertion result is true, nothing is done and the program continues to execute. If the assertion result is false, the program throws AssertError to interrupt the execution of the program. **`-enableassertions` will set the value of the \$assertionsDisabled field. + +### Numeric literal + +In Java 7, numeric literals, whether integers or floating point numbers, allow any number of underscores to be inserted between numbers. These underscores will not affect the numerical value of the literal, and are intended to facilitate reading. + +For example: + +```java +public class Test { + public static void main(String... args) { + int i = 10_000; + System.out.println(i); + } +} +``` + +After decompilation: + +```java +public class Test +{ + public static void main(String[] args) + { + int i = 10000; + System.out.println(i); + } +} +``` + +After decompilation, `_` is deleted. In other words, the ** compiler does not recognize the `_` in the numeric literal and needs to remove it during the compilation stage. ** + +### for-each + +I believe everyone is familiar with the enhanced for loop (`for-each`), which is often used in daily development. It requires a lot less code than the for loop. So how is this syntactic sugar implemented? + +```java +public static void main(String... args) { + String[] strs = {"Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"}; + for (String s : strs) { + System.out.println(s); + } + 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[]) +{ + String strs[] = { + "Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com" + }; + String args1[] = strs; + int i = args1.length; + for(int j = 0; j < i; j++) + { + String s = args1[j]; + System.out.println(s); + } + + List strList = ImmutableList.of("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com"); + String s; + for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s)) + s = (String)iterator.next(); + +} +``` + +The code is very simple. The implementation principle of **for-each is actually using ordinary for loops and iterators. ** + +### try-with-resource + +In Java, very expensive resources such as file operations, IO streams, and database connections must be closed promptly through the close method after use. Otherwise, the resources will remain open, which may lead to memory leaks and other problems. + +The common way to close a resource is to release it in the `finally` block, that is, call the `close` method. For example, we often write code like this: + +```java +public static void main(String[] args) { + BufferedReader br = null; + try { + String line; + br = new BufferedReader(new FileReader("d:\\hollischuang.xml")); + while ((line = br.readLine()) != null) { + System.out.println(line); + } + } catch (IOException e) { + //handle exception + } finally { + try { + if (br != null) { + br.close(); + } + } catch (IOException ex) { + //handle exception + } + } +} +``` + +Starting from Java 7, jdk provides a better way to close resources. Use the `try-with-resources` statement and rewrite the above code. The effect is as follows: + +```java +public static void main(String... args) { + try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + } catch (IOException e) { + //handle exception + } +} +``` + +Look, this is great news. Although I usually used `IOUtils` to close the stream before, and did not write a lot of code in `finally`, this new syntax sugar seems to be much more elegant. Take a look at his back: + +```java +public static transient void main(String args[]) + { + BufferedReader br; + Throwable throwable; + br = new BufferedReader(new FileReader("d:\\ hollischuang.xml")); + throwable = null; + String line; + try + { + while((line = br.readLine()) != null) + System.out.println(line); + } + catch(Throwable throwable2) + { + throwable = throwable2; + throw throwable2; + } + if(br != null) + if(throwable != null) + try + { + br.close(); + } + catch(Throwable throwable1) + { + throwable.addSuppressed(throwable1); + } + else + br.close(); + break MISSING_BLOCK_LABEL_113; //This label is a generated error by the decompilation tool, (not the content of the Java syntax itself) and is a temporary placeholder for the decompilation tool. Normally the bytecode generated by the compiler will not contain such invalid tags. + Exception exception; + exception; + if(br != null) + if(throwable != null) + try + { + br.close(); + } + catch(Throwable throwable3) + { + throwable.addSuppressed(throwable3); + } + else + br.close(); + throw exception; + IOException ioexception; + ioexception; + } +}``` + +**In fact, the principle behind it is also very simple. The compiler does all the operations of closing resources that we did not do for us. Therefore, it is once again confirmed that the function of syntax sugar is to facilitate programmers to use it, but in the end it still has to be converted into a language that the compiler understands. ** + +### Lambda expression + +Regarding lambda expression, some people may have doubts, because some people on the Internet say that it is not syntactic sugar. Actually, I would like to correct this statement. **Lambda expressions are not syntactic sugar for anonymous inner classes, but they are syntactic sugar. The implementation actually relies on several lambda-related APIs provided by the underlying JVM. ** + +Let’s look at a simple lambda expression first. Traverse a list: + +```java +public static void main(String... args) { + List strList = ImmutableList.of("Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"); + + strList.forEach( s -> { System.out.println(s); } ); +} +``` + +Why is it said that it is not syntactic sugar for inner classes? As we said before about inner classes, inner classes will have two class files after compilation, but classes containing lambda expressions will only have one file after compilation. + +The decompiled code is as follows: + +```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"); + strList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); +} + +private static /* synthetic */ void lambda$main$0(String s) { + System.out.println(s); +} +``` + +It can be seen that in the `forEach` method, the `java.lang.invoke.LambdaMetafactory#metafactory` method is actually called. The fourth parameter `implMethod` of this method specifies the method implementation. You can see that a `lambda$main$0` method is actually called here for output. + +Let's look at a slightly more complicated one. First filter the List and then output it: + +```java +public static void main(String... args) { + List strList = ImmutableList.of("Hollis", "Public account: Hollis", "Blog: www.hollischuang.com"); + + List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList()); + + HollisList.forEach( s -> { System.out.println(s); } ); +} +``` + +The decompiled code is as follows: + +```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"); + 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)()); +} + +private static /* synthetic */ void lambda$main$1(Object s) { + System.out.println(s); +} + +private static /* synthetic */ boolean lambda$main$0(String string) { + return string.contains("Hollis"); +} +``` + +The two lambda expressions call the `lambda$main$1` and `lambda$main$0` methods respectively. + +**So, the implementation of lambda expression actually relies on some underlying APIs. During the compilation phase, the compiler will decompose the lambda expression and convert it into a way to call the internal API. ** + +## Possible pitfalls + +### Generics + +**1. When generics encounter overloading** + +```java +public class GenericTypes { + + public static void method(List list) { + System.out.println("invoke method(List list)"); + } + + public static void method(List list) { + System.out.println("invoke method(List list)"); + } +} +``` + +The above code has two overloaded functions because their parameter types are different, one is `List` and the other is `List`. However, this code cannot be compiled. Because as we said before, the parameters `List` and `List` are erased after compilation and become the same native type List. The erasure action causes the signatures of the two methods to become exactly the same. + +**2. When generics encounter catch** + +Generic type parameters cannot be used in catch statements for Java exception handling. Because exception handling is performed by the JVM at run time. Since the type information is erased, the JVM cannot distinguish between the two exception types `MyException` and `MyException` + +**3. When generics contain static variables** + +```java +public class StaticTest{ + public static void main(String[] args){ + GT gti = new GT(); + gti.var=1; + GT gts = new GT(); + gts.var=2; + System.out.println(gti.var); + } +} +class GT{ + public static int var=0; + public void nothing(T x){} +} +``` + +The output result of the above code is: 2! + +Some students may mistakenly think that generic classes are different classes corresponding to different bytecodes. In fact, +Due to type erasure, all generic class instances are associated with the same bytecode, and the static variables of the generic class are shared. `GT.var` and `GT.var` in the above example are actually a variable. + +### Automatic boxing and unboxing + +**Object equality comparison** + +```java +public static void main(String[] args) { + Integer a = 1000; + Integer b = 1000; + Integer c = 100; + Integer d = 100; + System.out.println("a == b is " + (a == b)); + System.out.println(("c == d is " + (c == d))); +} +``` + +Output result: + +```plain +a == b is false +c == d is true +```In Java 5, a new feature was introduced on Integer operations to save memory and improve performance. Integer objects are cached and reused by using the same object reference. + +> Applies to the integer value range -128 to +127. +> +> Applies only to autoboxing. Creating objects using constructors does not apply. + +### Enhanced for loop + +```java +for (Student stu : students) { + if (stu.getId() == 2) + students.remove(stu); +} +``` + +Will throw `ConcurrentModificationException` exception. + +Iterator works in a separate thread and has a mutex lock. After the Iterator is created, it will create a single-linked index table pointing to the original object. When the number of original objects changes, the contents of this index table will not change synchronously, so when the index pointer moves backward, the object to be iterated cannot be found, so according to the fail-fast principle, the Iterator will immediately throw a `java.util.ConcurrentModificationException` exception. + +So `Iterator` does not allow the iterated object to be changed while it is working. But you can use `Iterator`'s own method `remove()` to delete objects. The `Iterator.remove()` method will delete the current iteration object while maintaining the consistency of the index. + +## Summary + +We introduced 12 commonly used syntactic sugars in Java. The so-called syntax sugar is just a syntax provided to developers to facilitate development. But this syntax is only known to developers. In order to be executed, it needs to be desugared, that is, converted into syntax recognized by the JVM. When we de-sugar the syntax, you will find that the convenient syntax we use every day is actually composed of other simpler syntaxes. + +With these syntactic sugars, we can greatly improve efficiency in daily development, but at the same time we must avoid overuse. It is best to understand the principles before use to avoid pitfalls. + + \ No newline at end of file diff --git a/docs_en/java/basis/unsafe.en.md b/docs_en/java/basis/unsafe.en.md new file mode 100644 index 00000000000..06abf701f56 --- /dev/null +++ b/docs_en/java/basis/unsafe.en.md @@ -0,0 +1,853 @@ +--- +title: Java 魔法类 Unsafe 详解 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: Unsafe,低级操作,内存访问,CAS,堆外内存,本地方法,风险 + - - meta + - name: description + content: 介绍 sun.misc.Unsafe 的能力与典型用法,涵盖内存与对象操作、CAS 支持及风险与限制。 +--- + +> 本文整理完善自下面这两篇优秀的文章: +> +> - [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) + + + +阅读过 JUC 源码的同学,一定会发现很多并发工具类都调用了一个叫做 `Unsafe` 的类。 + +那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚! + +## Unsafe 介绍 + +`Unsafe` 是位于 `sun.misc` 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 `Unsafe` 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 `Unsafe` 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 `Unsafe` 的使用一定要慎重。 + +另外,`Unsafe` 提供的这些功能的实现需要依赖本地方法(Native Method)。你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 **`native`** 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 **本地代码**。 + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717115231125.png) + +**为什么要使用本地方法呢?** + +1. 需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用。 +2. 对于其他语言已经完成的一些现成功能,可以使用 Java 直接调用。 +3. 程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。 + +在 JUC 包的很多并发工具类在实现并发机制时,都调用了本地方法,通过它们打破了 Java 运行时的界限,能够接触到操作系统底层的某些功能。对于同一本地方法,不同的操作系统可能会通过不同的方式来实现,但是对于使用者来说是透明的,最终都会得到相同的结果。 + +## Unsafe 创建 + +`sun.misc.Unsafe` 部分源码如下: + +```java +public final class Unsafe { + // 单例对象 + private static final Unsafe theUnsafe; + ...... + private Unsafe() { + } + @CallerSensitive + public static Unsafe getUnsafe() { + Class var0 = Reflection.getCallerClass(); + // 仅在引导类加载器`BootstrapClassLoader`加载时才合法 + if(!VM.isSystemDomainLoader(var0.getClassLoader())) { + throw new SecurityException("Unsafe"); + } else { + return theUnsafe; + } + } +} +``` + +`Unsafe` 类为一单例实现,提供静态方法 `getUnsafe` 获取 `Unsafe`实例。这个看上去貌似可以用来获取 `Unsafe` 实例。但是,当我们直接调用这个静态方法的时候,会抛出 `SecurityException` 异常: + +```bash +Exception in thread "main" java.lang.SecurityException: Unsafe + at sun.misc.Unsafe.getUnsafe(Unsafe.java:90) + at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12) +``` + +**为什么 `public static` 方法无法被直接调用呢?** + +这是因为在`getUnsafe`方法中,会对调用者的`classLoader`进行检查,判断当前类是否由`Bootstrap classLoader`加载,如果不是的话那么就会抛出一个`SecurityException`异常。也就是说,只有启动类加载器加载的类才能够调用 Unsafe 类中的方法,来防止这些方法在不可信的代码中被调用。 + +**为什么要对 Unsafe 类进行这么谨慎的使用限制呢?** + +`Unsafe` 提供的功能过于底层(如直接访问系统内存资源、自主管理内存资源等),安全隐患也比较大,使用不当的话,很容易出现很严重的问题。 + +**如若想使用 `Unsafe` 这个类的话,应该如何获取其实例呢?** + +这里介绍两个可行的方案。 + +1、利用反射获得 Unsafe 类中已经实例化完成的单例对象 `theUnsafe` 。 + +```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; + // 1. 分配初始内存 + long oldAddr = unsafe.allocateMemory(size); + System.out.println("Initial address: " + oldAddr); + + // 2. 向初始内存写入数据 + unsafe.putInt(oldAddr, 16843009); // 写入 0x01010101 + System.out.println("Value at oldAddr: " + unsafe.getInt(oldAddr)); + + // 3. 重新分配内存 + long newAddr = unsafe.reallocateMemory(oldAddr, size * 2); + System.out.println("New address: " + newAddr); + + // 4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr + // 所以 newAddr 的前4个字节应该和 oldAddr 的内容一样 + System.out.println("Value at newAddr (first 4 bytes): " + unsafe.getInt(newAddr)); + + // 关键:之后所有操作都应该基于 newAddr,oldAddr 已失效! + try { + // 5. 在新内存块的后半部分写入新数据 + unsafe.putInt(newAddr + size, 33686018); // 写入 0x02020202 + + // 6. 读取整个8字节的long值 + System.out.println("Value at newAddr (full 8 bytes): " + unsafe.getLong(newAddr)); + + } finally { + // 7. 只释放最后有效的内存地址 + unsafe.freeMemory(newAddr); + // 如果尝试 freeMemory(oldAddr),将会导致 double free 错误! + } +} +``` + +先看结果输出: + +```plain +Initial address: 140467048086752 +Value at oldAddr: 16843009 +New address: 140467048086752 +Value at newAddr (first 4 bytes): 16843009 +Value at newAddr (full 8 bytes): 144680345659310337 +``` + +`reallocateMemory` 的行为类似于 C 语言中的 realloc 函数,它会尝试在不移动数据的情况下扩展或收缩内存块。其行为主要有两种情况: + +1. **原地扩容**:如果当前内存块后面有足够的连续空闲空间,`reallocateMemory` 会直接在原地址上扩展内存,并返回原始地址。 +2. **异地扩容**:如果当前内存块后面空间不足,它会寻找一个新的、足够大的内存区域,将旧数据拷贝过去,然后释放旧的内存地址,并返回新地址。 + +**结合本次的运行结果,我们可以进行如下分析:** + +**第一步:初始分配与写入** + +- `unsafe.allocateMemory(size)` 分配了 4 字节的堆外内存,地址为 `140467048086752`。 +- `unsafe.putInt(oldAddr, 16843009)` 向该地址写入了 int 值 `16843009`,其十六进制表示为 `0x01010101`。`getInt` 读取正确,证明写入成功。 + +**第二步:原地内存扩容** + +- `long newAddr = unsafe.reallocateMemory(oldAddr, size * 2)` 尝试将内存块扩容至 8 字节。 +- 观察输出 New address: `140467048086752`,我们发现 `newAddr` 与 `oldAddr` 的值**完全相同**。 +- 这表明本次操作触发了“原地扩容”。系统在原地址 `140467048086752` 后面找到了足够的空间,直接将内存块扩展到了 8 字节。在这个过程中,旧的地址 `oldAddr` 依然有效,并且就是 `newAddr`,数据也并未发生移动。 + +**第三步:验证数据与写入新数据** + +- `unsafe.getInt(newAddr)` 再次读取前 4 个字节,结果仍是 `16843009`,验证了原数据完好无损。 +- `unsafe.putInt(newAddr + size, 33686018)` 在扩容出的后 4 个字节(偏移量为 4)写入了新的 int 值 `33686018`(十六进制为 `0x02020202`)。 + +**第四步:读取完整数据** + +- `unsafe.getLong(newAddr)` 从起始地址读取一个 long 值(8 字节)。此时内存中的 8 字节内容为 `0x01010101` (低地址) 和 `0x02020202` (高地址) 的拼接。 +- 在小端字节序(Little-Endian)的机器上,这 8 字节在内存中会被解释为十六进制数 `0x0202020201010101`。 +- 这个十六进制数转换为十进制,结果正是 `144680345659310337`。这完美地解释了最终的输出结果。 + +**第五步:安全的内存释放** + +- `finally` 块中,`unsafe.freeMemory(newAddr)` 安全地释放了整个 8 字节的内存块。 +- 由于本次是原地扩容(`oldAddr == newAddr`),所以即使错误地多写一句 `freeMemory(oldAddr)` 也会导致二次释放的严重错误。 + +#### 典型应用 + +`DirectByteBuffer` 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。`DirectByteBuffer` 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。 + +**为什么要使用堆外内存?** + +- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。 +- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。 + +下图为 `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; + } +} +``` + +In the `while` loop of the main thread, add a memory barrier to test whether the modification of `flag` can be sensed: + +```java +public static void main(String[] args){ + ChangeThread changeThread = new ChangeThread(); + new Thread(changeThread).start(); + while (true) { + boolean flag = changeThread.isFlag(); + unsafe.loadFence(); //Add read memory barrier + if (flag){ + System.out.println("detected flag changed"); + break; + } + } + System.out.println("main thread end"); +} +``` + +Running results: + +```plain +subThread change flag to:false +detected flag changed +main thread end +``` + +And if you delete the `loadFence` method in the above code, then the main thread will not be able to sense the changes in `flag` and will always loop in `while`. The above process can be represented by a diagram: + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144703446.png) + +Friends who understand the Java Memory Model (`JMM`) should know that running threads do not directly read variables in the main memory. They can only operate variables in their own working memory and then synchronize them to the main memory, and the working memory of threads cannot be shared. The process in the above figure is that the sub-thread synchronizes the modified results to the main thread with the help of the main memory, and then modifies the work space in the main thread and breaks out of the loop. + +#### Typical applications + +In Java 8, a new lock mechanism-`StampedLock` was introduced, which can be regarded as an improved version of read-write lock. `StampedLock` provides an implementation of optimistic read locking. This optimistic read lock is similar to a lock-free operation and does not block the writing thread from acquiring the write lock at all, thereby alleviating the "hunger" phenomenon of the writing thread when there is more reading and less writing. Since the optimistic read lock provided by `StampedLock` does not block the writing thread from acquiring the read lock, when the thread shared variable is loaded from the main memory to the thread working memory, there will be data inconsistency problems. + +To solve this problem, the `validate` method of `StampedLock` will add a `load` memory barrier through the `loadFence` method of `Unsafe`. + +```java +public boolean validate(long stamp) { + U.loadFence(); + return (stamp & SBITS) == (state & SBITS); +} +``` + +### Object operations + +#### Introduction + +**Example** + +```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; + } + } + +} +``` + +Output result: + +```plain +value before putInt: 0 +value after putInt: 42 +value after putInt: 42 +``` + +**Object Properties** + +We have already tested the memory offset acquisition of object member attributes and the modification of field attribute values in the above example. In addition to the previous `putInt` and `getInt` methods, Unsafe provides all 8 basic data types as well as the `put` and `get` methods of `Object`, and all `put` methods can directly modify the data in memory without access permissions. Reading the comments in the openJDK source code, I found that the reading and writing of basic data types and `Object` are slightly different. The basic data type is a property value (`value`) that is directly manipulated, while the operation of `Object` is based on a reference value (`reference value`). The following are the reading and writing methods of `Object`: + +```java +//Get an object reference at the specified offset address of the object +public native Object getObject(Object o, long offset); +//Write an object reference at the specified offset address of the object +public native void putObject(Object o, long offset, Object x); +``` + +In addition to ordinary reading and writing of object properties, `Unsafe` also provides **volatile reading** and **ordered writing** methods. The coverage of `volatile` reading and writing methods is the same as that of ordinary reading and writing, including all basic data types and `Object` types, taking the `int` type as an example: + +```java +//Read an int value at the specified offset address of the object, supporting volatile load semantics +public native int getIntVolatile(Object o, long offset); +//Write an int at the specified offset address of the object, supporting volatile store semantics +public native void putIntVolatile(Object o, long offset, int x); +``` + +Compared with ordinary reading and writing, `volatile` reading and writing has a higher cost because it needs to ensure visibility and ordering. When executing the `get` operation, the attribute value will be forcibly obtained from the main memory. When the `put` method is used to set the attribute value, the value will be forcibly updated to the main memory, thereby ensuring that these changes are visible to other threads. + +There are three methods for orderly writing: + +```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); +``` + +The cost of ordered writing is relatively low compared to `volatile`, because it only guarantees the orderliness when writing, but does not guarantee visibility. That is, the value written by one thread cannot guarantee that other threads will immediately see it. In order to resolve the differences here, we need to further supplement the knowledge about memory barriers. First, we need to understand the concepts of two instructions: + +- `Load`: Copies the data in the main memory to the processor's cache +- `Store`: Flushes data cached by the processor into main memory + +The difference between sequential writing and `volatile` writing is that the memory barrier type added during sequential writing is `StoreStore` type, while the memory barrier added during `volatile` writing is of `StoreLoad` type, as shown in the following figure: + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144834132.png)In the ordered write method, the `StoreStore` barrier is used, which ensures that `Store1` flushes data to memory immediately, before `Store2` and subsequent store instruction operations. In `volatile` writing, the `StoreLoad` barrier is used. This barrier ensures that `Store1` immediately refreshes data to memory. This operation precedes `Load2` and subsequent load instructions. Moreover, the `StoreLoad` barrier will cause all memory access instructions before the barrier, including storage instructions and access instructions, to be executed before the memory access instructions after the barrier are executed. + +To sum up, among the above three types of writing methods, in terms of writing efficiency, the efficiency gradually decreases in the order of `put`, `putOrder`, and `putVolatile`. + +**Object instantiation** + +Using the `allocateInstance` method of `Unsafe` allows us to instantiate objects in an unconventional way. First, define an entity class and assign values to its member variables in the constructor: + +```java +@Data +public class A { + private int b; + public A(){ + this.b =1; + } +} +``` + +Create objects in different ways based on constructors, reflection and `Unsafe` methods for comparison: + +```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()); +} +``` + +The printed results are 1, 1, and 0 respectively, indicating that during the object creation process through the `allocateInstance` method, the constructor of the class will not be called. When creating an object in this way, only the `Class` object is used, so if you want to skip the initialization phase of the object or skip the safety check of the constructor, you can use this method. In the above example, if the constructor of class A is changed to `private` type, the object will not be created through the constructor and reflection (the object can be created after setAccessible through the constructor object), but the `allocateInstance` method is still valid. + +#### Typical applications + +- **Conventional object instantiation method**: The methods we usually use to create objects are essentially created through the new mechanism. However, a characteristic of the new mechanism is that when a class only provides a parameterized constructor and does not explicitly declare a parameterless constructor, the parameterized constructor must be used for object construction. When using a parameterized constructor, a corresponding number of parameters must be passed to complete object instantiation. +- **Unconventional instantiation method**: Unsafe provides the allocateInstance method, which can create this type of instance object only through the Class object, and there is no need to call its constructor, initialization code, JVM security check, etc. It suppresses modifier detection, that is, even if the constructor is privately modified, it can be instantiated through this method, and the corresponding object can be created by simply mentioning the class object. Due to this feature, allocateInstance has corresponding applications in java.lang.invoke, Objenesis (which provides an object generation method that bypasses the class constructor), and Gson (used during deserialization). + +### Array operations + +#### Introduction + +The two methods `arrayBaseOffset` and `arrayIndexScale` are used together to locate the memory location of each element in the array. + +```java +//Return the offset address of the first element in the array +public native int arrayBaseOffset(Class arrayClass); +//Return the size occupied by an element in the array +public native int arrayIndexScale(Class arrayClass); +``` + +#### Typical applications + +These two methods related to data operations have typical applications in `AtomicIntegerArray` under the `java.util.concurrent.atomic` package (which can implement atomic operations on each element in the `Integer` array), as shown in the `AtomicIntegerArray` source code below, through `arrayBaseOffset` and `arrayIndexScale` of `Unsafe` Get the offset address `base` and the single element size factor `scale` of the first element of the array respectively. Subsequent related atomic operations rely on these two values ​​to position elements in the array. The `getAndAdd` method shown in Figure 2 below obtains the offset address of an array element through the `checkedByteOffset` method, and then implements atomic operations through CAS. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144927257.png) + +### CAS operations + +#### Introduction + +This part is mainly about methods of CAS related operations. + +```java +/** + *CAS + * @param o Contains the object of the field to be modified + * @param offset The offset of a field in the object + * @param expected expected value + * @param update update value + * @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); +``` + +**What is CAS?** CAS stands for Compare And Swap, which is a technology commonly used when implementing concurrent algorithms. A CAS operation consists of three operands - the memory location, the expected original value, and the new value. When performing a CAS operation, the value of the memory location is compared with the expected original value. If they match, the processor will automatically update the location value to the new value. Otherwise, the processor will not do anything. We all know that CAS is an atomic instruction of the CPU (cmpxchg instruction) and will not cause the so-called data inconsistency problem. The underlying implementation of the CAS methods (such as `compareAndSwapXXX`) provided by `Unsafe` is the CPU instruction `cmpxchg`. + +#### Typical applications + +CAS operations are widely used in the concurrency tool classes of the JUC package. CAS has been mentioned many times in the previous articles introducing `synchronized` and `AQS`, and it plays an extensive role in concurrency tool classes as optimistic locking. In the `Unsafe` class, `compareAndSwapObject`, `compareAndSwapInt` and `compareAndSwapLong` methods are provided to implement CAS operations on `Object`, `int` and `long` types. Take the `compareAndSwapInt` method as an example: + +```java +public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); +``` + +In the parameter, `o` is the object that needs to be updated, and `offset` is the offset of the integer field in object `o`. If the value of this field is the same as `expected`, the value of the field is set to the new value of `x`, and this update cannot be interrupted, which is an atomic operation. Here is an example using `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(); + } + } +}``` + +Running the code will output: + +```plain +1 2 3 4 5 6 7 8 9 +``` + +If you paste the above code into the IDE and run it, you will find that the target output result cannot be obtained. A friend has pointed out this problem on Github: [issue#2650](https://github.com/Snailclimb/JavaGuide/issues/2650). Here is the corrected code: + +```java +private volatile int a = 0; // Shared variable, initial value is 0 +private static final Unsafe unsafe; +private static final long fieldOffset; + +static { + try { + // Get Unsafe instance + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + unsafe = (Unsafe) theUnsafe.get(null); + // Get the memory offset of field a + fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a")); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize Unsafe or field offset", e); + } +} + +public static void main(String[] args) { + CasTest casTest = new CasTest(); + + Thread t1 = new Thread(() -> { + for (int i = 1; i <= 4; i++) { + casTest.incrementAndPrint(i); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 5; i <= 9; i++) { + casTest.incrementAndPrint(i); + } + }); + + t1.start(); + t2.start(); + + // Wait for the thread to finish so you can observe the complete output (optional, for demonstration) + try { + t1.join(); + t2.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } +} + +// Encapsulate the increment and print operations in a more atomic method +private void incrementAndPrint(int targetValue) { + while (true) { + int currentValue = a; // Read the current value of a + // Only attempt to update if the current value of a is equal to the previous value of the target value + if (currentValue == targetValue - 1) { + if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) { + // CAS is successful, indicating that a is successfully updated to targetValue + System.out.print(targetValue + " "); + break; //Exit the loop after successfully updating and printing + } + // If CAS fails, it means that the value of a was modified by other threads between reading currentValue and executing CAS. + // At this time, currentValue is no longer the latest value of a, and you need to re-read and try again. + } + // If currentValue != targetValue - 1, it means that it is not the current thread's turn to update yet. + // Or it has been updated by other threads, giving up the CPU to other threads. + // For strictly sequential increment scenarios, if current > targetValue - 1, it may mean a logic error or an infinite loop. + // But in this example, we expect the threads to execute sequentially. + Thread.yield(); // Prompts the CPU scheduler to switch threads to reduce invalid spins + } +} +``` + +In the above example, we create two threads, both of which try to modify the shared variable a. When each thread calls the `incrementAndPrint(targetValue)` method: + +1. The current value of a `currentValue` will be read first. +2. Check if `currentValue` is equal to `targetValue - 1` (i.e. the previous value expected). +3. If the condition is met, call `unsafe.compareAndSwapInt()` to try to update `a` from `currentValue` to `targetValue`. +4. If the CAS operation is successful (returns true), print `targetValue` and exit the loop. +5. If the CAS operation fails, or `currentValue` does not meet the conditions, the current thread will continue to loop (spin) and try to give up the CPU through `Thread.yield()` until it is successfully updated and printed or the conditions are met. + +This mechanism ensures that each number (from 1 to 9) is successfully set and printed only once, and in sequence. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144939826.png) + +Things to note are: + +1. **Spin logic:** The `compareAndSwapInt` method itself only performs a comparison and swap operation and returns the result immediately. Therefore, to ensure that the operation eventually succeeds (if the value is as expected), we need to explicitly implement spin logic in the code (such as a `while(true)` loop) that keeps trying until the CAS operation succeeds. +2. **`AtomicInteger` implementation:** The `java.util.concurrent.atomic.AtomicInteger` class in JDK uses similar CAS operations and spin logic to implement its atomic `getAndIncrement()`, `compareAndSet()` and other methods. Using `AtomicInteger` directly is generally safer and more recommended, as it encapsulates the underlying complexity. +3. **ABA problem:** The CAS operation itself has an ABA problem (if a value changes from A to B, and then back to A, the CAS check will think that the value has not changed). In some scenarios, if the value change history is important, it may be necessary to use `AtomicStampedReference` to solve it. But in this simple incremental scenario, ABA issues usually don't matter. +4. **CPU consumption:** Long-term spin will consume CPU resources. When competition is fierce or conditions are not met for a long time, you can consider adding more complex backoff strategies (such as `Thread.sleep()` or `LockSupport.parkNanos()`) for optimization. + +### Thread scheduling + +#### Introduction + +The `Unsafe` class provides `park`, `unpark`, `monitorEnter`, `monitorExit` and `tryMonitorEnter` methods for thread scheduling. + +```java +//Cancel blocking thread +public native void unpark(Object thread); +//Block thread +public native void park(boolean isAbsolute, long time); +//Obtain object lock (reentrant lock) +@Deprecated +public native void monitorEnter(Object o); +//Release object lock +@Deprecated +public native void monitorExit(Object o); +//Try to acquire the object lock +@Deprecated +public native boolean tryMonitorEnter(Object o); +``` + +The methods `park` and `unpark` can realize the suspension and recovery of threads. Suspending a thread is achieved through the `park` method. After calling the `park` method, the thread will be blocked until conditions such as timeout or interruption occur; `unpark` can terminate a suspended thread and return it to normal. + +In addition, three methods related to `monitor` in the `Unsafe` source code have been marked as `deprecated` and are not recommended to be used: + +```java +//Get object lock +@Deprecated +public native void monitorEnter(Object var1); +//Release object lock +@Deprecated +public native void monitorExit(Object var1); +//Try to obtain the object lock +@Deprecated +public native boolean tryMonitorEnter(Object var1);``` + +The `monitorEnter` method is used to obtain the object lock, and `monitorExit` is used to release the object lock. If this method is executed on an object that is not locked by `monitorEnter`, an `IllegalMonitorStateException` exception will be thrown. The `tryMonitorEnter` method attempts to acquire the object lock and returns `true` if successful, otherwise it returns `false`. + +#### Typical applications + +The core class `AbstractQueuedSynchronizer` (AQS) of the Java lock and synchronizer framework implements thread blocking and awakening by calling `LockSupport.park()` and `LockSupport.unpark()`, and the `park` and `unpark` methods of `LockSupport` are actually implemented by calling the `park` and `unpark` methods of `Unsafe`. + +```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); +} +``` + +The `park` method of `LockSupport` calls the `park` method of `Unsafe` to block the current thread. After this method blocks the thread, it will not continue to execute until other threads call the `unpark` method to wake up the current thread. The following example tests these two methods of `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"); +} +``` + +The program output is: + +```plain +park main mainThread +subThread try to unpark mainThread +unpark mainThread success +``` + +The process of program running is also relatively easy to understand. After the child thread starts running, it sleeps first to ensure that the main thread can call the `park` method to block itself. After the child thread sleeps for 5 seconds, it calls the `unpark` method to wake up the main thread so that the main thread can continue to execute. The entire process is shown in the figure below: + +![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144950116.png) + +### Class operations + +#### Introduction + +`Unsafe` related operations on `Class` mainly include class loading and static variable operation methods. + +**Static property reading related methods** + +```java +//Get the offset of the static attribute +public native long staticFieldOffset(Field f); +//Get the object pointer of the static attribute +public native Object staticFieldBase(Field f); +//Determine whether the class needs to be initialized (used to detect before obtaining the static properties of the class) +public native boolean shouldBeInitialized(Class c); +``` + +Create a class containing static properties for testing: + +```java +@Data +public class User { + public static String name="Hydra"; + int age; +} +private void staticTest() throws Exception { + User user=new User(); + // You can also use the following statement to trigger class initialization + // 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); +} +``` + +Running results: + +```plain +false +Hydra +``` + +In the object operation of `Unsafe`, we learned to obtain the object property offset through the `objectFieldOffset` method and access the value of the variable based on it, but it does not apply to static properties in the class. In this case, you need to use the `staticFieldOffset` method. In the above code, `Class` is only relied on in the process of obtaining the `Field` object, and it no longer depends on `Class` when obtaining the properties of static variables. + +In the above code, a `User` object is first created. This is because if a class has not been initialized, then its static properties will not be initialized, and the last field attribute obtained will be `null`. Therefore, before obtaining static properties, you need to call the `shouldBeInitialized` method to determine whether the class needs to be initialized before obtaining it. If you delete the statement that creates the User object, the running result will become: + +```plain +true +null +``` + +**Using the `defineClass` method allows the program to dynamically create a class at runtime** + +```java +public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain); +``` + +In actual use, you can only pass in the byte array, the subscript of the starting byte and the byte length to be read. By default, the class loader (`ClassLoader`) and protection domain (`ProtectionDomain`) are derived from the instance that calls this method. In the following example, the function of decompiling the generated class file is implemented: + +```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(); + } +} +``` + +In the above code, a `class` file is first read and converted into a byte array through the file stream. Then a class is dynamically created using the `defineClass` method, and its instantiation is subsequently completed. The process is as shown in the figure below, and the class created in this way will skip all security checks of the JVM.![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717145000710.png) + +In addition to the `defineClass` method, Unsafe also provides a `defineAnonymousClass` method: + +```java +public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); +``` + +This method can be used to dynamically create an anonymous class. In a `Lambda` expression, ASM is used to dynamically generate bytecode, and then this method is used to define an anonymous class that implements the corresponding functional interface. Among the new features released in JDK 15, in the Hidden classes section, it is pointed out that the `defineAnonymousClass` method of `Unsafe` will be deprecated in future versions. + +#### Typical applications + +Lambda expression implementation needs to rely on the `defineAnonymousClass` method of `Unsafe` to define an anonymous class that implements the corresponding functional interface. + +### System information + +#### Introduction + +This section contains two methods for obtaining system-related information. + +```java +//Return the size of the system pointer. The return value is 4 (32-bit systems) or 8 (64-bit systems). +public native int addressSize(); +//The size of the memory page, this value is a power of 2. +public native int pageSize(); +``` + +#### Typical applications + +These two methods have relatively few application scenarios. In the `java.nio.Bits` class, when using `pageCount` to calculate the number of required memory pages, the `pageSize` method is called to obtain the size of the memory page. In addition, when using the `copySwapMemory` method to copy memory, the `addressSize` method is called to detect the situation of 32-bit systems. + +## Summary + +In this article, we first introduce the basic concepts and working principles of `Unsafe`, and on this basis, we explain and practice its API. I believe that through this process, everyone will find that `Unsafe` can indeed provide us with programming convenience in certain scenarios. But back to the topic at the beginning, there are indeed some security risks when using these conveniences. In my opinion, it is not terrible that a technology has unsafe factors. What is terrible is that it is abused during use. Although there were rumors that the `Unsafe` class would be removed in Java 9, it has still survived into Java 16. According to the logic that existence is reasonable, as long as it is used properly, it can still bring us a lot of help. Therefore, in the end, it is recommended that everyone use `Unsafe` with caution and avoid abuse. + + \ No newline at end of file diff --git a/docs_en/java/basis/why-there-only-value-passing-in-java.en.md b/docs_en/java/basis/why-there-only-value-passing-in-java.en.md new file mode 100644 index 00000000000..f1615ca49a2 --- /dev/null +++ b/docs_en/java/basis/why-there-only-value-passing-in-java.en.md @@ -0,0 +1,225 @@ +--- +title: Detailed explanation of Java value passing +category: Java +tag: + - Java basics +head: + - - meta + - name: keywords + content: value transfer, reference transfer, parameter transfer, object reference, example analysis, method invocation + - - meta + - name: description + content: Explain the Java parameter passing model through examples, and clarify common misunderstandings between value passing and reference passing. +--- + +Before we begin, let’s first understand the following two concepts: + +- Formal parameters & actual parameters +- Pass by value & pass by reference + +## Formal parameters & actual parameters + +The definition of a method may use **parameters** (methods with parameters). Parameters are divided into: + +- **Actual parameters (actual parameters, Arguments)**: Parameters used to be passed to functions/methods, must have a certain value. +- **Formal parameters (Parameters)**: used to define functions/methods, receive actual parameters, and do not need to have definite values. + +```java +String hello = "Hello!"; +// hello is an actual parameter +sayHello(hello); +// str is a formal parameter +void sayHello(String str) { + System.out.println(str); +} +``` + +## Pass by value & pass by reference + +There are two ways that programming languages pass actual parameters to methods (or functions): + +- **Value passing**: The method receives a copy of the actual parameter value and creates a copy. +- **Pass by reference**: The method receives directly the address of the actual parameter, not the value in the actual parameter. This is the pointer. At this time, the formal parameter is the actual parameter. Any modification to the formal parameter will be reflected in the actual parameter, including reassignment. + +Many programming languages ​​(such as C++, Pascal) provide two methods of parameter passing, but in Java there is only value passing. + +## Why does Java only pass by value? + +**Why is it said that Java only transfers by value? ** Without too much nonsense, I will prove it to you through 3 examples. + +### Case 1: Passing basic type parameters + +Code: + +```java +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + swap(num1, num2); + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + System.out.println("a = " + a); + System.out.println("b = " + b); +} +``` + +Output: + +```plain +a=20 +b = 10 +num1 = 10 +num2 = 20 +``` + +Analysis: + +In the `swap()` method, the values of `a` and `b` are exchanged and will not affect `num1` and `num2`. Because the values ​​of `a` and `b` are just copied from `num1` and `num2`. In other words, a and b are equivalent to copies of `num1` and `num2`. No matter how the contents of the copies are modified, they will not affect the original itself. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/java-value-passing-01.png) + +Through the above example, we already know that a method cannot modify a parameter of a basic data type, but it is different when using an object reference as a parameter. Please see Case 2. + +### 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) { + // 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) + +After reading this case, many people must think that Java uses reference passing for reference type parameters. + +Actually, no, what is passed here is still a value, but this value is just the address of the actual parameter! + +In other words, the parameter of the `change` method copies the address of `arr` (actual parameter), so it and `arr` point to the same array object. This also explains why modifications to the formal parameters within the method will affect the actual parameters. + +In order to refute more strongly that Java does not use reference passing for reference type parameters, let’s look at the following case! + +### Case 3: Passing reference type parameters 2 + +```java +public class Person { + private String name; + // Omit the constructor, Getter&Setter methods +} + +public static void main(String[] args) { + Person xiaoZhang = new Person("小张"); + Person xiaoli = new Person("小李"); + swap(xiaoZhang, xiaoLi); + System.out.println("xiaoZhang:" + xiaoZhang.getName()); + 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()); +} +``` + +Output: + +```plain +person1:Xiao Li +person2:Xiao Zhang +xiaozhang:xiaozhang +xiaoli:小李 +``` + +Analysis: + +What's going on? ? ? The interchange of formal parameters of two reference types does not affect the actual parameters! + +The parameters `person1` and `person2` of the `swap` method are just the addresses of the copied actual parameters `xiaoZhang` and `xiaoLi`. Therefore, the exchange of `person1` and `person2` is just the exchange of the two copied addresses, and will not affect the actual parameters `xiaoZhang` and `xiaoLi`. + +![](https://oss.javaguide.cn/github/javaguide/java/basis/java-value-passing-03.png) + +## What does reference passing look like? + +Seeing this, I believe you already know that there is only value transfer in Java, not reference transfer. +But what does pass-by-reference actually look like? Let's take the `C++` code as an example to let you see the true face of pass-by-reference. + +```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"; +} +``` + +Output result: + +```plain +invoke before: 10 +incr before: 10 +incr after: 11 +invoke after: 11 +``` + +Analysis: It can be seen that modifications to the formal parameters in the `incr` function can affect the values of the actual parameters. Note: The data type of the `incr` formal parameter here is `int&`, so it is passed by reference. If `int` is used, it is still passed by value! + +## Why doesn't Java introduce passing by reference? + +Passing by reference seems to be very good, and the value of the actual parameter can be modified directly within the method. However, why doesn't Java introduce passing by reference? + +**Note: The following are personal opinions and do not come from Java official:** + +1. For security reasons, the operations performed on the value inside the method are unknown to the caller (the method is defined as an interface, and the caller does not care about the specific implementation). You can also imagine that if you take a bank card to withdraw money, you will withdraw 100 and deduct 200. Isn’t it terrible?2. James Gosling, the father of Java, saw many shortcomings of C and C++ at the beginning of the design, so he wanted to design a new language Java. When he designed Java, he followed the principle of simplicity and ease of use, and abandoned many "features" that would cause problems if developers didn't pay attention. There were fewer things in the language itself, and there were less things for developers to learn. + +## Summary + +The way to pass actual parameters to a method (or function) in Java is **pass by value**: + +- If the parameter is a basic type, it is very simple. What is passed is a copy of the literal value of the basic type, and a copy will be created. +- If the parameter is a reference type, what is passed is a copy of the address value in the heap of the object referenced by the actual parameter, and a copy will also be created. + +## Reference + +- Chapter 4, Section 4.5 of "Java Core Technology Volume I" Basic Knowledge, Tenth Edition +- [Is Java passed by value or by reference? - Hollis' answer - Zhihu](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/) + + \ No newline at end of file diff --git a/docs_en/java/collection/arrayblockingqueue-source-code.en.md b/docs_en/java/collection/arrayblockingqueue-source-code.en.md new file mode 100644 index 00000000000..4bc68296c5e --- /dev/null +++ b/docs_en/java/collection/arrayblockingqueue-source-code.en.md @@ -0,0 +1,776 @@ +--- +title: ArrayBlockingQueue 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: ArrayBlockingQueue,阻塞队列,生产者消费者,有界队列,JUC,put,take,线程池,ReentrantLock,Condition + - - meta + - name: description + content: 讲解 ArrayBlockingQueue 的有界阻塞队列实现与典型生产者-消费者使用,结合线程池工作队列分析锁与条件的并发设计。 +--- + +## 阻塞队列简介 + +### 阻塞队列的历史 + +Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 `java.util.concurrent`,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。 + +为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。 + +随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善: + +1. JDK1.6 版本:增加 `SynchronousQueue`,一个不存储元素的阻塞队列。 +2. JDK1.7 版本:增加 `TransferQueue`,一个支持更多操作的阻塞队列。 +3. JDK1.8 版本:增加 `DelayQueue`,一个支持延迟获取元素的阻塞队列。 + +### 阻塞队列的思想 + +阻塞队列就是典型的生产者-消费者模型,它可以做到以下几点: + +1. 当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空。 +2. 当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费。 +3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。 +4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。 + +总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put`、`take`、`offer`、`poll` 等 API 即可实现多线程之间的生产和消费。 + +这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。 + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) {// ...} +``` + +## ArrayBlockingQueue 常见方法及测试 + +简单了解了阻塞队列的历史之后,我们就开始重点讨论本篇文章所要介绍的并发容器——`ArrayBlockingQueue`。为了后续更加深入的了解 `ArrayBlockingQueue`,我们不妨基于下面几个实例了解以下 `ArrayBlockingQueue` 的使用。 + +先看看第一个例子,我们这里会用两个线程分别模拟生产者和消费者,生产者生产完会使用 `put` 方法生产 10 个元素给消费者进行消费,当队列元素达到我们设置的上限 5 时,`put` 方法就会阻塞。 +同理消费者也会通过 `take` 方法消费元素,当队列为空时,`take` 方法就会阻塞消费者线程。这里笔者为了保证消费者能够在消费完 10 个元素后及时退出。便通过倒计时门闩,来控制消费者结束,生产者在这里只会生产 10 个元素。当消费者将 10 个元素消费完成之后,按下倒计时门闩,所有线程都会停止。 + +```java +public class ProducerConsumerExample { + + public static void main(String[] args) throws InterruptedException { + + // 创建一个大小为 5 的 ArrayBlockingQueue + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); + + // 创建生产者线程 + 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); +} +``` + +After understanding the common operations of `BlockingQueue`, we know that `ArrayBlockingQueue` implements the `BlockingQueue` method and rewrites it, then fills it into the `AbstractQueue` method. From this, we know where the `offer` method of the `add` method of `AbstractQueue` above is implemented. + +```java +public boolean add(E e) { + //AbstractQueue's offer comes from the lower ArrayBlockingQueue's offer method implemented and rewritten from BlockingQueue. + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); +} +``` + +### Initialization + +Before understanding the details of `ArrayBlockingQueue`, we might as well take a look at its constructor and understand its initialization process. From the source code, we can see that `ArrayBlockingQueue` has 3 construction methods, and the core construction method is the one below. + +```java +// capacity represents the initial capacity of the queue, fair represents the fairness of the lock +public ArrayBlockingQueue(int capacity, boolean fair) { + //If the set queue size is less than 0, throw IllegalArgumentException directly. + if (capacity <= 0) + throw new IllegalArgumentException(); + //Initialize an array to store the elements of the queue + this.items = new Object[capacity]; + //Create a lock that blocks queue process control + lock = new ReentrantLock(fair); + //Use lock to create two conditions to control queue production and consumption + notEmpty = lock.newCondition(); + notFull = lock.newCondition(); +} +``` + +There are two core member variables in this construction method, `notEmpty` (not empty) and `notFull` (not full). We need to pay special attention to them. They are the key to achieving the orderly work of producers and consumers. I will explain this in detail in the subsequent source code analysis. Here we only need to have a preliminary understanding of the structure of the blocking queue. + +The other two construction methods are based on the above construction method. By default, we will use the following construction method, which means that `ArrayBlockingQueue` uses an unfair lock, that is, after each producer or consumer thread receives the notification, the competition for the lock is random. + +```java + public ArrayBlockingQueue(int capacity) { + this(capacity, false); + } +``` + +There is also a less commonly used construction method. After initializing the capacity and unfairness of the lock, it also provides a `Collection` parameter. It is not difficult to see from the source code that this construction method directly stores the elements of the externally passed collection into the blocking queue during initialization. + +```java +public ArrayBlockingQueue(int capacity, boolean fair, + Collection c) { + //Initialize capacity and lock fairness + this(capacity, fair); + + final ReentrantLock lock = this.lock; + //Lock and store the elements in c into the underlying array of ArrayBlockingQueue + lock.lock(); + try { + int i = 0; + try { + //Traverse and add elements to the array + for (E e : c) { + checkNotNull(e); + items[i++] = e; + } + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalArgumentException(); + } + //Record the current queue capacity + count = i; + //Update the next put or offer or add the position to the underlying array of the queue using the add method + putIndex = (i == capacity) ? 0 : i; + } finally { + //Release the lock after completing the traversal + lock.unlock(); + } +} +``` + +### Blocking acquisition and new elements + +`ArrayBlockingQueue` blocking acquisition and new elements correspond to the producer-consumer model. Although it also supports non-blocking acquisition and new elements (such as `poll()` and `offer(E e)` methods, which will be introduced later), they are generally not used. + +`ArrayBlockingQueue` The blocking method of obtaining and adding elements is: + +- `put(E e)`: Insert elements into the queue. If the queue is full, this method will block until space is available in the queue or the thread is interrupted. +- `take()`: Get and remove the element at the head of the queue. If the queue is empty, this method will block until the queue is not empty or the thread is interrupted. + +The key to the implementation of these two methods lies in the two conditional objects `notEmpty` (not empty) and `notFull` (not full), which we mentioned in the constructor method above. + +Next, the author will use two pictures to let everyone understand how these two conditions are used in the blocking queue. + +![ArrayBlockingQueue non-empty condition](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-take.png) + +Assume that our code consumer starts first. When it finds that there is no data in the queue, the non-empty condition will suspend the thread, that is, the thread will be suspended when the waiting condition is not empty. Then the CPU execution right reaches the producer. The producer finds that the data can be stored in the queue, so he stores the data in it and notifies that the condition is not empty. At this time, the consumer will be awakened to the queue and use methods such as `take` to obtain the value. + +![ArrayBlockingQueue not full condition](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notFull-put.png) + +In subsequent executions, the producer's production speed is much greater than the consumer's consumption speed, so the producer fills the queue and tries to store data into the queue again. It is found that the queue is full, so the blocking queue suspends the current thread and waits until it is full. Then the consumer takes the CPU execution rights to consume, so the queue can store new data and sends a non-full notification. At this time, the suspended producer will wait for the arrival of CPU execution rights and try to store data in the queue again. + +After briefly understanding the interaction process of the blocking queue based on two conditions, we might as well take a look at the source code of the `put` and `take` methods. + +```java +public void put(E e) throws InterruptedException { + //Make sure the inserted element is not null + checkNotNull(e); + //Lock + final ReentrantLock lock = this.lock; + //The lockInterruptibly() method is used instead of the lock() method here to be able to respond to interrupt operations. If it is interrupted while waiting to acquire the lock, this method will throw an InterruptedException exception. + lock.lockInterruptibly(); + try { + //If the array length is equal to count, it means that the queue is full, and the current thread will be suspended and placed in the AQS queue, waiting for insertion when the queue is not full (non-full condition). + //During the waiting period, the lock will be released and other threads can continue to operate on the queue. + while (count == items.length) + notFull.await(); + //If the queue can store elements, call enqueue to enqueue the elements. + enqueue(e); + } finally { + //Release the lock + lock.unlock(); + } +} +``` + +The `enqueue` method is called internally in the `put` method to implement elements into the queue. Let's continue to take a closer look at the implementation details of the `enqueue` method: + +```java +private void enqueue(E x) { + //Get the array at the bottom of the queue + final Object[] items = this.items; + //Set the value of the putindex position to the x we passed in + items[putIndex] = x; + //Update putindex, if putindex is equal to the length of the array, update it to 0 + if (++putIndex == items.length) + putIndex = 0; + //queue length+1 + count++; + //Notify that the queue is not empty, and those threads blocked by obtaining elements can continue to work. + notEmpty.signal(); +}``` + +From the source code, we can see that the logic of the enqueue operation is to append a new element to the array. The overall execution steps are: + +1. Get the underlying array `items` of `ArrayBlockingQueue`. +2. Save the element to the `putIndex` location. +3. Update `putIndex` to the next position. If `putIndex` is equal to the queue length, it means that `putIndex` has reached the end of the array, and the next insertion needs to start from 0. (`ArrayBlockingQueue` uses the idea of a circular queue, that is, cyclically reusing an array from beginning to end) +4. Update the value of `count` to represent the current queue length + 1. +5. Call `notEmpty.signal()` to notify the queue that the queue is not empty and the consumer can get values ​​from the queue. + +Since then we have understood the process of the `put` method. In order to have a more complete understanding of the design of the producer-consumer model of `ArrayBlockingQueue`, we continue to look at the `take` method that blocks the acquisition of queue elements. + +```java +public E take() throws InterruptedException { + //Get the lock + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + //If the number of elements in the queue is 0, interrupt the current thread and store it in the AQS queue, and wait for the elements to be obtained and removed when the queue is not empty (non-empty condition) + while (count == 0) + notEmpty.await(); + //If the queue is not empty, call dequeue to get the elements + return dequeue(); + } finally { + //Release the lock + lock.unlock(); + } +} +``` + +After understanding the `put` method, it is very simple to look at the `take` method. Its core logic is exactly the opposite of the `put` method. For example, the `put` method waits for the queue to be full when the queue is not full to insert elements (non-full condition), while the `take` method waits for the queue to be non-empty to obtain and remove elements (non-empty condition). + +The `take` method internally calls the `dequeue` method to dequeue elements, and its core logic is also opposite to that of the `enqueue` method. + +```java +private E dequeue() { + //Get the array at the bottom of the blocking queue + final Object[] items = this.items; + @SuppressWarnings("unchecked") + //Get the element at takeIndex position from the queue + E x = (E) items[takeIndex]; + //Make takeIndex empty + items[takeIndex] = null; + //takeIndex moves backward, if it is equal to the length of the array, it is updated to 0 + if (++takeIndex == items.length) + takeIndex = 0; + //Decrease the queue length by 1 + count--; + if (itrs != null) + itrs.elementDequeued(); + //Notify those interrupted threads that the current queue status is not full and can continue to store elements. + notFull.signal(); + return x; +} +``` + +Since the steps of the `dequeue` method (dequeuing) are roughly similar to the `enqueue` method (enqueueing) introduced above, they will not be repeated here. + +To help understand, I drew a picture to show how the two condition objects `notEmpty` (not empty) and `notFull` (not full) control the storage and access of `ArrayBlockingQueue`. + +![ArrayBlockingQueue is not empty and not full](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-notFull.png) + +- **Consumer**: When the consumer takes out an element from the queue through operations such as `take` or `poll`, it will notify the queue that it is not full. At this time, those producers who are waiting for the queue to be not full will be awakened to wait for CPU time slices to join the queue. +- **Producer**: When the producer saves the element into the queue, it will trigger a notification that the queue is not empty. At this time, the consumer will be awakened to wait for the CPU time slice to try to obtain the element. In this way, the two condition objects form a loop, controlling the storage and retrieval between multiple threads. + +### Non-blocking acquisition and new elements + +`ArrayBlockingQueue` non-blocking method of obtaining and adding elements is: + +- `offer(E e)`: Insert the element into the end of the queue. If the queue is full, this method will return false directly without waiting and blocking the thread. +- `poll()`: Get and remove the element at the head of the queue. If the queue is empty, this method will directly return null and will not wait and block the thread. +- `add(E e)`: Insert elements into the end of the queue. If the queue is full, an `IllegalStateException` exception will be thrown, and the underlying method is based on the `offer(E e)` method. +- `remove()`: Removes the element at the head of the queue. If the queue is empty, a `NoSuchElementException` exception will be thrown. The underlying layer is based on `poll()`. +- `peek()`: Gets but does not remove the element at the head of the queue. If the queue is empty, this method will directly return null without waiting and blocking the thread. + +Let’s take a look at the `offer` method first. The logic is similar to `put`. The only difference is that the current thread will not be blocked when the queue fails, but `false` will be returned directly. + +```java +public boolean offer(E e) { + //Make sure the inserted element is not null + checkNotNull(e); + //Get the lock + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //The queue is full and returns false directly. + if (count == items.length) + return false; + else { + // Otherwise, add the element to the queue and return true directly + enqueue(e); + return true; + } + } finally { + //Release the lock + lock.unlock(); + } + } +``` + +The `poll` method has the same principle. If it fails to obtain an element, it will directly return null and will not block the thread that obtains the element. + +```java +public E poll() { + final ReentrantLock lock = this.lock; + //Lock + lock.lock(); + try { + //If the queue is empty, return null directly, otherwise return the element value after dequeuing. + return (count == 0) ? null : dequeue(); + } finally { + lock.unlock(); + } + } +``` + +The `add` method actually encapsulates `offer`. As shown in the following code, you can see that `add` will call `offer` without a specified time. If the entry into the queue fails, an exception will be thrown directly. + +```java +public boolean add(E e) { + return super.add(e); + } + + +public boolean add(E e) { + //Call the offer method and throw an exception directly if it fails. + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); + } +``` + +The `remove` method is the same. Call `poll`. If `null` is returned, it means that there are no elements in the queue and an exception will be thrown directly. + +```java +public E remove() { + E x = poll(); + if (x != null) + return x; + else + throw new NoSuchElementException(); + } +``` + +The logic of the `peek()` method is also very simple, and the `itemAt` method is called internally. + +```java +public E peek() { + //Lock + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //Return null when the queue is empty + return itemAt(takeIndex); + } finally { + //Release the lock + lock.unlock(); + } + } + +//Return the element at the specified position in the queue +@SuppressWarnings("unchecked") +final E itemAt(int i) { + return (E) items[i]; +}``` + +### Blocking acquisition and new elements within the specified timeout period + +On the basis of `offer(E e)` and `poll()` non-blocking acquisition and new elements, the designer provides `offer(E e, long timeout, TimeUnit unit)` and `poll(long timeout, TimeUnit unit)` with waiting time for blocking addition and acquisition of elements within the specified timeout period. + +```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 { + //The queue is full, enter the loop + while (count == items.length) { + //The queue is still full when the time is up, then return false directly. + if (nanos <= 0) + return false; + //Block nanos time and wait until it is not full + nanos = notFull.awaitNanos(nanos); + } + enqueue(e); + return true; + } finally { + lock.unlock(); + } + } +``` + +It can be seen that the `offer` method with a timeout will wait for the time period passed by the user when the queue is full. If the element cannot be stored within the specified time, it will directly return `false`. + +```java +public E poll(long timeout, TimeUnit unit) throws InterruptedException { + long nanos = unit.toNanos(timeout); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + //The queue is empty, wait in a loop, if the time is still empty, return null directly + while (count == 0) { + if (nanos <= 0) + return null; + nanos = notEmpty.awaitNanos(nanos); + } + return dequeue(); + } finally { + lock.unlock(); + } + } +``` + +In the same way, the same goes for `poll` with a timeout. If the queue is empty, it will wait within the specified time. If the queue is still empty when the time is up, null will be returned directly. + +### Determine whether the element exists + +`ArrayBlockingQueue` provides `contains(Object o)` to determine whether the specified element exists in the queue. + +```java +public boolean contains(Object o) { + //If the target element is empty, return false directly. + if (o == null) return false; + //Get the element array of the current queue + final Object[] items = this.items; + //Lock + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // If the queue is not empty + if (count > 0) { + final int putIndex = this.putIndex; + //Traverse from the head of the queue + int i = takeIndex; + do { + if (o.equals(items[i])) + return true; + if (++i == items.length) + i = 0; + } while (i != putIndex); + } + return false; + } finally { + //Release the lock + lock.unlock(); + } +} +``` + +## ArrayBlockingQueue Comparison of methods of obtaining and adding elements + +To help understand `ArrayBlockingQueue`, let’s compare the methods of obtaining and adding elements mentioned above. + +New elements: + +| Method | How to handle when the queue is full | Method return value | +| ---------------------------------------- | -------------------------------------------------------- | ---------- | +| `put(E e)` | The thread blocks until interrupted or awakened | void | +| `offer(E e)` | Return false directly | boolean | +| `offer(E e, long timeout, TimeUnit unit)` | Block within the specified timeout, and return false if the addition is not successful after the specified time | boolean | +| `add(E e)` | Directly throw `IllegalStateException` exception | boolean | + +Get/remove elements: + +| Method | How to handle when the queue is empty | Method return value | +| ----------------------------------- | --------------------------------------------------- | ---------- | +| `take()` | The thread blocks until interrupted or awakened | E | +| `poll()` | returns null | E | +| `poll(long timeout, TimeUnit unit)` | Block within the specified timeout. If it exceeds the specified time or is empty, return null | E | +| `peek()` | returns null | E | +| `remove()` | Directly throw `NoSuchElementException` exception | boolean | + +![](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-get-add-element-methods.png) + +## ArrayBlockingQueue related interview questions + +### What is ArrayBlockingQueue? What are its characteristics? + +`ArrayBlockingQueue` is a bounded queue implementation class of the `BlockingQueue` interface. It is often used for data sharing between multi-threads. The bottom layer is implemented using an array, as can be seen from its name. + +The capacity of `ArrayBlockingQueue` is limited and once created, the capacity cannot be changed.In order to ensure thread safety, the concurrency control of `ArrayBlockingQueue` uses the reentrant lock `ReentrantLock`. Whether it is an insertion operation or a read operation, the lock needs to be acquired before the operation can be performed. Moreover, it also supports fair and unfair lock access mechanisms. The default is unfair lock. + +Although `ArrayBlockingQueue` is called a blocking queue, it also supports non-blocking acquisition and new elements (such as `poll()` and `offer(E e)` methods). However, adding elements when the queue is full will throw an exception. When the queue is empty, the elements obtained are null and are generally not used. + +### What is the difference between ArrayBlockingQueue and LinkedBlockingQueue? + +`ArrayBlockingQueue` and `LinkedBlockingQueue` are two blocking queue implementations commonly used in Java concurrency packages. They are both thread-safe. However, there are also the following differences between them: + +- Underlying implementation: `ArrayBlockingQueue` is implemented based on arrays, while `LinkedBlockingQueue` is implemented based on linked lists. +- Whether it is bounded: `ArrayBlockingQueue` is a bounded queue and the capacity must be specified when it is created. `LinkedBlockingQueue` can be created without specifying the capacity. The default is `Integer.MAX_VALUE`, which is unbounded. But it is also possible to specify the queue size, thus making it bounded. +- Whether the locks are separated: The locks in `ArrayBlockingQueue` are not separated, that is, the same lock is used for production and consumption; the locks in `LinkedBlockingQueue` are separated, that is, `putLock` is used for production and `takeLock` is used for consumption. This can prevent lock contention between producer and consumer threads. +- Memory usage: `ArrayBlockingQueue` needs to allocate array memory in advance, while `LinkedBlockingQueue` dynamically allocates linked list node memory. This means that `ArrayBlockingQueue` will occupy a certain amount of memory space when it is created, and often the memory requested is larger than the actual memory used, while `LinkedBlockingQueue` gradually occupies memory space according to the increase of elements. + +### What is the difference between ArrayBlockingQueue and ConcurrentLinkedQueue? + +`ArrayBlockingQueue` and `ConcurrentLinkedQueue` are two queue implementations commonly used in the Java concurrency package. They are both thread-safe. However, there are also the following differences between them: + +- Underlying implementation: `ArrayBlockingQueue` is implemented based on arrays, while `ConcurrentLinkedQueue` is implemented based on linked lists. +- Whether it is bounded: `ArrayBlockingQueue` is a bounded queue and the capacity must be specified when it is created, while `ConcurrentLinkedQueue` is an unbounded queue and can dynamically increase its capacity. +- Whether to block: `ArrayBlockingQueue` supports blocking and non-blocking methods of obtaining and adding elements (generally only the former is used). `ConcurrentLinkedQueue` is unbounded and only supports non-blocking methods of obtaining and adding elements. + +### What is the implementation principle of ArrayBlockingQueue? + +The implementation principles of `ArrayBlockingQueue` are mainly divided into the following points (herein, blocking acquisition and new elements are introduced as examples): + +- `ArrayBlockingQueue` internally maintains a fixed-length array for storing elements. +- Synchronize read and write operations by using the `ReentrantLock` lock object, that is, thread safety is achieved through the lock mechanism. +- Implement waiting and wake-up operations between threads through `Condition`. + +Here is a detailed introduction to the specific implementation of waiting and waking up between threads (you don’t need to remember the specific methods, just answer the key points in the interview): + +- When the queue is full, the producer thread will call the `notFull.await()` method to let the producer wait and insert when the queue is not full (non-full condition). +- When the queue is empty, the consumer thread will call the `notEmpty.await()` method to let the consumer wait and consume when the queue is not empty (non-empty condition). +- When a new element is added, the producer thread will call the `notEmpty.signal()` method to wake up the consumer thread waiting for consumption. +- When an element is taken out of the queue, the consumer thread will call the `notFull.signal()` method to wake up the producer thread that is waiting to insert the element. + +Additional information about the `Condition` interface: + +> `Condition` is only available after JDK1.5. It has good flexibility. For example, it can implement multi-channel notification function, that is, multiple `Condition` instances (i.e. object monitors) can be created in a `Lock` object. **Thread objects can be registered in the specified `Condition`, so that thread notification can be selectively carried out and it is more flexible in scheduling threads. When using the `notify()/notifyAll()` method to notify, the thread to be notified is selected by the JVM. Using the `ReentrantLock` class combined with the `Condition` instance can implement "selective notification"**. This function is very important and is provided by the `Condition` interface by default. The `synchronized` keyword is equivalent to only one `Condition` instance in the entire `Lock` object, and all threads are registered in it. If the `notifyAll()` method is executed, all threads in the waiting state will be notified, which will cause great efficiency problems. The `signalAll()` method of a `Condition` instance will only wake up all waiting threads registered in the `Condition` instance. + +## References + +- In-depth understanding of Java series | Detailed explanation of BlockingQueue usage: +- An in-depth explanation of the blocking queue BlockingQueue and its typical implementation ArrayBlockingQueue: +- Literacy in concurrent programming: ArrayBlockingQueue underlying principles and practice: + \ No newline at end of file diff --git a/docs_en/java/collection/arraylist-source-code.en.md b/docs_en/java/collection/arraylist-source-code.en.md new file mode 100644 index 00000000000..50444f2bda0 --- /dev/null +++ b/docs_en/java/collection/arraylist-source-code.en.md @@ -0,0 +1,974 @@ +--- +title: ArrayList 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: ArrayList,动态数组,ensureCapacity,RandomAccess,扩容机制,序列化,add/remove,索引访问,性能,Vector 区别,列表实现 + - - meta + - name: description + content: 系统梳理 ArrayList 的底层原理与常见用法,包含动态数组结构、扩容策略、接口实现以及与 Vector 的差异与性能特点。 +--- + + + +## ArrayList 简介 + +`ArrayList` 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 `ArrayList` 实例的容量。这可以减少递增式再分配的数量。 + +`ArrayList` 继承于 `AbstractList` ,实现了 `List`, `RandomAccess`, `Cloneable`, `java.io.Serializable` 这些接口。 + +```java + +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable{ + + } +``` + +- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 +- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 +- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 + +![ArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arraylist-class-diagram.png) + +### ArrayList 和 Vector 的区别?(了解即可) + +- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[]`存储,适用于频繁的查找工作,线程不安全 。 +- `Vector` 是 `List` 的古老实现类,底层使用`Object[]` 存储,线程安全。 + +### 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 与 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 expansion mechanism analysis + +### Let’s start with the constructor of ArrayList + +There are three ways to initialize ArrayList. The source code of the construction method is as follows (JDK8): + +```java +/** + *Default initial capacity size + */ +private static final int DEFAULT_CAPACITY = 10; + +private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + +/** + * Default constructor, using initial capacity 10 to construct an empty list (no parameter construction) + */ +public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; +} + +/** + * Constructor with initial capacity parameters. (User-specified capacity) + */ +public ArrayList(int initialCapacity) { + if (initialCapacity > 0) {//Initial capacity is greater than 0 + //Create an array of initialCapacity size + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) {//Initial capacity is equal to 0 + //Create an empty array + this.elementData = EMPTY_ELEMENTDATA; + } else {//Initial capacity is less than 0, throws an exception + throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); + } +} + + +/** + *Construct a list containing the elements of the specified collection, which are returned in order using the iterator of the collection + *If the specified collection is 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; + } +} +``` + +Careful students will definitely find that: **When creating `ArrayList` with the parameterless construction method, an empty array is actually initialized and assigned. Capacity is actually allocated when an element is actually added to the array. That is, when the first element is added to the array, the array capacity is expanded to 10. ** We will talk about this when we analyze the expansion of `ArrayList` below! + +> Supplement: When JDK6 new constructs an `ArrayList` object without parameters, it directly creates an `Object[]` array `elementData` with a length of 10. + +### Step by step analysis of ArrayList expansion mechanism + +Here we take the `ArrayList` created by the parameterless constructor as an example. + +#### add method + +```java +/** +* Appends the specified elements to the end of this list. +*/ +public boolean add(E e) { + // Before adding elements, call the ensureCapacityInternal method first + ensureCapacityInternal(size + 1); // Increments modCount!! + // Here we see that the essence of adding elements to ArrayList is equivalent to assigning values to the array. + elementData[size++] = e; + return true; +} +``` + +**Note**: JDK11 removed the `ensureCapacityInternal()` and `ensureExplicitCapacity()` methods + +The source code of the `ensureCapacityInternal` method is as follows: + +```java +// Calculate the required capacity based on the given minimum capacity and current array elements. +private static int calculateCapacity(Object[] elementData, int minCapacity) { + // If the current array element is an empty array (initial situation), return the larger value of the default capacity and the minimum capacity as the required capacity + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + // Otherwise, return the minimum capacity directly + return minCapacity; +} + +// Ensure that the internal capacity reaches the specified minimum capacity. +private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); +} +``` + +The `ensureCapacityInternal` method is very simple, and the `ensureExplicitCapacity` method is directly called internally: + +```java +//Determine whether expansion is needed +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + //Determine whether the current array capacity is enough to store minCapacity elements + if (minCapacity - elementData.length > 0) + //Call the grow method to expand the capacity + grow(minCapacity); +} +``` + +Let’s analyze it in detail: + +- When we want to `add` the first element to `ArrayList`, `elementData.length` is 0 (because it is still an empty list), because the `ensureCapacityInternal()` method is executed, so `minCapacity` is 10 at this time. At this time, `minCapacity - elementData.length > 0` is established, so the `grow(minCapacity)` method will be entered. +- When `add` the second element, `minCapacity` is 2, and `elementData.length` (capacity) is expanded to `10` after adding the first element. At this time, `minCapacity - elementData.length > 0` does not hold, so the `grow(minCapacity)` method will not be entered (executed). +- When adding the 3rd, 4th... to the 10th element, the grow method will still not be executed, and the array capacity will be 10. + +Until the 11th element is added, `minCapacity` (which is 11) is larger than `elementData.length` (which is 10). Enter the `grow` method to expand the capacity. + +#### grow method + +```java +/** + * Maximum array size to allocate + */ +private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + +/** + * The core method of ArrayList expansion. + */ +private void grow(int minCapacity) { + // oldCapacity is the old capacity, newCapacity is the new capacity + int oldCapacity = elementData.length; + // Shift oldCapacity right one bit, the effect is equivalent to oldCapacity /2, + // We know that bit operations are much faster than integer division operations. The result of the entire operation is to update the new capacity to 1.5 times the old capacity. + int newCapacity = oldCapacity + (oldCapacity >> 1); + + // Then check whether the new capacity is greater than the minimum required capacity. If it is still less than the minimum required capacity, then use the minimum required capacity as the new capacity of the array. + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + + // If the new capacity is greater than MAX_ARRAY_SIZE, enter (execute) the `hugeCapacity()` method to compare minCapacity and MAX_ARRAY_SIZE, + // If minCapacity is greater than the maximum capacity, the new capacity is `Integer.MAX_VALUE`, otherwise, the new capacity size is MAX_ARRAY_SIZE, which is `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)`, so the capacity of ArrayList will become about 1.5 times after each expansion (oldCapacity is an even number, which is 1.5 times, otherwise it is about 1.5 times)! ** Odd and even are different, for example: 10+10/2 = 15, 33+33/2=49. If it is an odd number, the decimal will be discarded. + +> ">>" (shift operator): >>1 shifting one bit to the right is equivalent to dividing by 2, and shifting n bits to the right is equivalent to dividing by 2 raised to the nth power. Here oldCapacity is obviously shifted right by 1 bit so it is equivalent to oldCapacity /2. For binary operations on big data, displacement operators are much faster than ordinary operators because the program only moves them and does not perform calculations, which improves efficiency and saves resources. + +**Let’s explore the `grow()` method through an example: ** + +- When the first element of `add` is added, `oldCapacity` is 0. After comparison, the first if judgment is established, `newCapacity = minCapacity` (is 10). But the second if judgment will not be established, that is, if `newCapacity` is not larger than `MAX_ARRAY_SIZE`, it will not enter the `hugeCapacity` method. The array capacity is 10, return true in the `add` method, and size is increased to 1. +- When the 11th element of `add` enters the `grow` method, `newCapacity` is 15, which is larger than `minCapacity` (which is 11), and the first if judgment is not true. If the new capacity is not greater than the maximum size of the array, the `hugeCapacity` method will not be entered. The array capacity is expanded to 15, return true in the add method, and size is increased to 11. +- And so on... + +**Here is a little more important knowledge point that is easily overlooked:** + +- The `length` property in Java is for arrays. For example, if you declare an array and want to know the length of the array, you use the length property. +- The `length()` method in Java is for strings. If you want to see the length of the string, use the `length()` method. +- The `size()` method in Java is for generic collections. If you want to see how many elements this generic has, just call this method to check! + +#### hugeCapacity() method + +From the source code of the `grow()` method above, we know: if the new capacity is greater than `MAX_ARRAY_SIZE`, enter (execute) the `hugeCapacity()` method to compare `minCapacity` and `MAX_ARRAY_SIZE`. If `minCapacity` is greater than the maximum capacity, the new capacity is `Integer.MAX_VALUE`, otherwise, the new capacity size is `MAX_ARRAY_SIZE` is `Integer.MAX_VALUE - 8`. + +```java +private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + // Compare minCapacity and MAX_ARRAY_SIZE + // If minCapacity is large, use Integer.MAX_VALUE as the size of the new array + // If MAX_ARRAY_SIZE is large, use MAX_ARRAY_SIZE as the size of the new array + // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; +} +``` + +### `System.arraycopy()` and `Arrays.copyOf()` methods + +If we read the source code, we will find that these two methods are called extensively in `ArrayList`. For example: this method is used in the expansion operation we talked about above and methods such as `add(int index, E element)` and `toArray()`! + +#### `System.arraycopy()` method + +Source code: + +```java + // We found that arraycopy is a native method. Next, we explain the specific meaning of each parameter. + /** + * Copy array + * @param src source array + * @param srcPos starting position in the source array + * @param dest target array + * @param destPos starting position in the target array + * @param length The number of array elements to copy + */ + public static native void arraycopy(Object src, int srcPos, + Object dest, int destPos, + int length); +``` + +Scenario: + +```java + /** + * Inserts the specified element at the specified position in this list. + *First call rangeCheckForAdd to perform a limit check on the index; then call the ensureCapacityInternal method to ensure that the capacity is large enough; + * Then move all members after the index one position back; insert the element into the index position; and finally increase the size by 1. + */ + public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + //arraycopy() method implements array copying itself + //elementData: source array; index: starting position in the source array; elementData: target array; index + 1: starting position in the target array; size - index: the number of array elements to be copied; + System.arraycopy(elementData, index, elementData, index + 1, size - index); + elementData[index] = element; + size++; + } +``` + +Let's write a simple method to test the following: + +```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] + " "); + } + } + +} +``` + +Result: + +```plain +0 1 99 2 3 0 0 0 0 0 +``` + +#### `Arrays.copyOf()` method + +Source code: + +```java + public static int[] copyOf(int[] original, int newLength) { + //Apply for a new array + int[] copy = new int[newLength]; + // Call System.arraycopy to copy the data in the source array and return the new array + System.arraycopy(original, 0, copy, 0, + Math.min(original.length, newLength)); + return copy; + } +``` + +Scenario: + +```java + /** + Returns an array containing all the elements in this list in correct order (from first to last element); the runtime type of the returned array is that of the specified array. + */ + public Object[] toArray() { + //elementData: array to be copied; size: length to be copied + return Arrays.copyOf(elementData, size); + } +``` + +Personally, I feel that using the `Arrays.copyOf()` method is mainly to expand the original array. The test code is as follows: + +```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); + } +}``` + +Result: + +```plain +10 +``` + +#### The connection and difference between the two + +**Contact:** + +Looking at the source code of the two, you can find that `copyOf()` actually calls the `System.arraycopy()` method internally + +**Difference:** + +`arraycopy()` requires a target array. It copies the original array to your own defined array or the original array, and you can choose the starting point and length of the copy and the position in the new array. `copyOf()` means the system automatically creates an array internally and returns the array. + +### `ensureCapacity` method + +There is an `ensureCapacity` method in the `ArrayList` source code. I don’t know if you have noticed it. This method `ArrayList` has not been called internally, so it is obviously provided for users to call. So what does this method do? + +```java + /** + If necessary, increase the capacity of this ArrayList instance to ensure that it can hold at least the number of elements specified by the minimum capacity parameter. + * + * @param minCapacity the minimum required capacity + */ + 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); + } + } + +``` + +In theory, it is better to use the `ensureCapacity` method before adding a large number of elements to an `ArrayList` to reduce the number of incremental reallocations + +We actually test the effect of the following method through the following code: + +```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("Before using the ensureCapacity method: "+(endTime - startTime)); + + } +} +``` + +Running results: + +```plain +Before using ensureCapacity method: 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("After using the ensureCapacity method: "+(endTime1 - startTime1)); + } +} +``` + +Running results: + +```plain +After using ensureCapacity method: 1773 +``` + +By running the results, we can see that using the `ensureCapacity` method before adding a large number of elements to `ArrayList` can improve performance. However, this performance gap is almost negligible. Moreover, it is impossible to add so many elements to `ArrayList` in actual projects. + + \ No newline at end of file diff --git a/docs_en/java/collection/concurrent-hash-map-source-code.en.md b/docs_en/java/collection/concurrent-hash-map-source-code.en.md new file mode 100644 index 00000000000..21bd6b9400d --- /dev/null +++ b/docs_en/java/collection/concurrent-hash-map-source-code.en.md @@ -0,0 +1,607 @@ +--- +title: ConcurrentHashMap 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: ConcurrentHashMap,线程安全,分段锁,Segment,CAS,红黑树,链表,并发级别,JDK7,JDK8,并发容器 + - - meta + - name: description + content: 对比 JDK7/8 的 ConcurrentHashMap 实现,解析分段锁、CAS、链表/红黑树等并发设计,理解线程安全 Map 的核心原理。 +--- + +> 本文来自公众号:末读代码的投稿,原文地址: 。 + +上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? + +## 1. ConcurrentHashMap 1.7 + +### 1. 存储结构 + +![Java 7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) + +Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,你也可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 + +### 2. 初始化 + +通过 `ConcurrentHashMap` 的无参构造探寻 `ConcurrentHashMap` 的初始化流程。 + +```java + /** + * Creates a new, empty map with a default initial capacity (16), + * load factor (0.75) and concurrencyLevel (16). + */ + public ConcurrentHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); + } +``` + +无参构造中调用了有参构造,传入了三个参数的默认值,他们的值是。 + +```java + /** + * 默认初始化容量 + */ + static final int DEFAULT_INITIAL_CAPACITY = 16; + + /** + * 默认负载因子 + */ + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * 默认并发级别 + */ + static final int DEFAULT_CONCURRENCY_LEVEL = 16; +``` + +接着看下这个有参构造函数的内部实现逻辑。 + +```java +@SuppressWarnings("unchecked") +public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) { + // 参数校验 + if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) + throw new IllegalArgumentException(); + // 校验并发级别大小,大于 1<<16,重置为 65536 + if (concurrencyLevel > MAX_SEGMENTS) + concurrencyLevel = MAX_SEGMENTS; + // Find power-of-two sizes best matching arguments + // 2的多少次方 + int sshift = 0; + int ssize = 1; + // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值 + while (ssize < concurrencyLevel) { + ++sshift; + ssize <<= 1; + } + // 记录段偏移量 + this.segmentShift = 32 - sshift; + // 记录段掩码 + this.segmentMask = ssize - 1; + // 设置容量 + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量 + int c = initialCapacity / ssize; + if (c * ssize < initialCapacity) + ++c; + int cap = MIN_SEGMENT_TABLE_CAPACITY; + //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数 + while (cap < c) + cap <<= 1; + // create segments and segments[0] + // 创建 Segment 数组,设置 segments[0] + Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor), + (HashEntry[])new HashEntry[cap]); + Segment[] ss = (Segment[])new Segment[ssize]; + UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] + this.segments = ss; +} +``` + +总结一下在 Java 7 中 ConcurrentHashMap 的初始化逻辑。 + +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; +} +``` + +The above source code analyzes the processing flow of `ConcurrentHashMap` when putting a data. The specific process is summarized below. + +1. Calculate the position of the key to be put and obtain the `Segment` at the specified position. + +2. If the `Segment` at the specified position is empty, initialize this `Segment`. + + **Initialization Segment process:** + + 1. Check whether the `Segment` of the calculated position is null. + 2. Continue initialization for null and create a `HashEntry` array using the capacity and load factor of `Segment[0]`. + 3. Check again whether the calculated `Segment` at the specified position is null. + 4. Initialize this Segment using the created `HashEntry` array. + 5. Spin to determine whether the calculated `Segment` at the specified position is null, and use CAS to assign a value to `Segment` at this position. + +3. `Segment.put` inserts key and value values. + +The operations of obtaining the `Segment` segment and initializing the `Segment` segment were explored above. The put method of `Segment` in the last line has not been checked yet, so continue the analysis. + +```java +final V put(K key, int hash, V value, boolean onlyIfAbsent) { + // Obtain the ReentrantLock exclusive lock. If it cannot be obtained, scanAndLockForPut is used to obtain it. + HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); + V oldValue; + try { + HashEntry[] tab = table; + // Calculate the data location to be put + int index = (tab.length - 1) & hash; + // CAS gets the value of index coordinates + HashEntry first = entryAt(tab, index); + for (HashEntry e = first;;) { + if (e != null) { + // Check whether key already exists. If it exists, traverse the linked list to find the position and replace the value after finding it. + 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 { + // The value of first does not indicate that the index position already has a value, there is a conflict, and the head of the linked list is interpolated. + if (node != null) + node.setNext(first); + else + node = new HashEntry(hash, key, value, first); + int c = count + 1; + // If the capacity is greater than the expansion threshold and less than the maximum capacity, expand the capacity. + if (c > threshold && tab.length < MAXIMUM_CAPACITY) + rehash(node); + else + // Index position assignment node, node may be an element or the head of a linked list + setEntryAt(tab, index, node); + ++modCount; + count = c; + oldValue = null; + break; + } + } + } finally { + unlock(); + } + return oldValue; +} +``` + +Since `Segment` inherits `ReentrantLock`, the lock can be easily acquired inside `Segment`, and the put process uses this function. + +1. `tryLock()` acquires the lock. If it cannot be acquired, use the **`scanAndLockForPut`** method to continue acquiring it. + +2. Calculate the index position where the put data is to be placed, and then obtain the `HashEntry` at this position. + +3. Traverse put new elements, why do we need to traverse? Because the `HashEntry` obtained here may be an empty element, or the linked list may already exist, so it must be treated differently. + + If **`HashEntry` does not exist** at this location: + + 1. If the current capacity is greater than the expansion threshold and less than the maximum capacity, **expand** will be performed. + 2. Insert directly. + + If **`HashEntry` exists at this location**: + + 1. Determine whether the key and hash values of the current element in the linked list are consistent with the key and hash values to be put. If consistent, replace the value + 2. If there is inconsistency, get the next node in the linked list and replace the value until the same value is found, or there is no identical value in the linked list. + 1. If the current capacity is greater than the expansion threshold and less than the maximum capacity, **expand** will be performed. + 2. Directly insert the head of the linked list. + +4. If the position to be inserted already exists before, return the old value after replacement, otherwise return null. + +The `scanAndLockForPut` operation in the first step is not introduced here. The operation performed by this method is to continuously spin `tryLock()` to acquire the lock. When the number of spins is greater than the specified number, use `lock()` to block and acquire the lock. Get the `HashEntry` at the next hash position in sequence while spinning. + +```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 + // Spin to acquire lock + 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) { + // After the spin reaches the specified number of times, block until the lock is acquired. + 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. Expansion rehash + +The expansion of `ConcurrentHashMap` will only double its original size. When the data in the old array is moved to the new array, the position will either remain unchanged or change to `index+ oldSize`. The node in the parameter will be inserted into the specified position using linked list **head interpolation** after expansion. + +```java +private void rehash(HashEntry node) { + HashEntry[] oldTable = table; + // old capacity + int oldCapacity = oldTable.length; + // New capacity, doubled + int newCapacity = oldCapacity << 1; + //New expansion threshold + threshold = (int)(newCapacity * loadFactor); + //Create new array + HashEntry[] newTable = (HashEntry[]) new HashEntry[newCapacity]; + // New mask, the default 2 is 4 after expansion, -1 is 3, and the binary value is 11. + int sizeMask = newCapacity - 1; + for (int i = 0; i < oldCapacity ; i++) { + // Traverse the old array + HashEntry e = oldTable[i]; + if (e != null) { + HashEntry next = e.next; + // Calculate the new position. The new position can only be the same or the old position + the old capacity. + int idx = e.hash & sizeMask; + if (next == null) // Single node on list + // If the current position is not a linked list, but just an element, assign the value directly + newTable[idx] = e; + else { // Reuse consecutive sequence at same slot + // If it is a linked list + HashEntry lastRun = e; + int lastIdx = idx; + // The new location can only be the same or the old location + old capacity. + // After the traversal is completed, the positions of the elements after lastRun are all the same. + for (HashEntry last = next; last != null; last = last.next) { + int k = last.hash & sizeMask; + if (k != lastIdx) { + lastIdx = k; + lastRun = last; + } + } + //, the positions of the elements after lastRun are all the same, and they are directly assigned to the new position as a linked list. + newTable[lastIdx] = lastRun; + // Clone remaining nodes + for (HashEntry p = e; p != lastRun; p = p.next) { + // Traverse the remaining elements and interpolate the head to the specified k position. + 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); + } + } + } + } + //Head insertion method inserts new nodes + int nodeIndex = node.hash & sizeMask; // add the new node + node.setNext(newTable[nodeIndex]); + newTable[nodeIndex] = node; + table = newTable; +} +``` + +Some students may be confused about the last two for loops. The first for here is to find such a node. The new positions of all next nodes after this node are the same. Then assign this as a linked list to the new location. The second for loop is to insert the remaining elements into the linked list at the specified position through head interpolation. ~~The reason for this implementation may be based on probability statistics. Students with in-depth research can express their opinions. ~~ + +The second internal `for` loop uses `new HashEntry(h, p.key, v, n)` to create a new `HashEntry` instead of reusing the previous one, because if the previous one is reused, the thread that is traversing (such as executing the `get` method) will not be able to traverse due to the modification of the pointer. As said in the comments: + +> Replaced nodes will be garbage collected when they are no longer referenced by any read threads that may be traversing the table concurrently. +> +> 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 + +Why do we need to use another `for` loop to find `lastRun`? Actually, it is to reduce the number of object creations, as mentioned in the annotation: + +> Statistically, under the default threshold, when the table capacity is doubled, only about one-sixth of the nodes need to be cloned. +> +> Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles. + +### 5. get + +It's very simple to get here, the get method only requires two steps. + +1. Calculate the storage location of key. +2. Traverse the specified location to find the value of the same key. + +```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; + // Calculate the storage location of 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) { + // If it is a linked list, traverse to find the value with the same key. + K k; + if ((k = e.key) == key || (e.hash == h && key.equals(k))) + return e.value; + } + } + return null; +} +``` + +## 2. ConcurrentHashMap 1.8 + +### 1. Storage structure + +![Java8 ConcurrentHashMap storage structure (picture from javadoop)](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png)It can be found that Java8's ConcurrentHashMap has changed significantly compared to Java7. It is no longer the previous **Segment array + HashEntry array + linked list**, but **Node array + linked list/red-black tree**. When the conflicting linked list reaches a certain length, the linked list will be converted into a red-black tree. + +### 2. Initialize 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) { + //If sizeCtl < 0, it means that another thread has successfully executed CAS and is initializing. + if ((sc = sizeCtl) < 0) + //Give up CPU usage rights + 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; +} +``` + +It can be found from the source code that the initialization of `ConcurrentHashMap` is completed through **spin and CAS** operations. What needs attention inside is the variable `sizeCtl` (abbreviation of sizeControl), whose value determines the current initialization state. + +1. -1 indicates that it is being initialized and other threads need to spin and wait. +2. -N indicates that the table is being expanded. The high 16 bits represent the identification stamp of the expansion. The low 16 bits minus 1 are the number of threads undergoing expansion. +3. 0 indicates the table initialization size, if the table is not initialized +4. \>0 represents the threshold for table expansion, if the table has been initialized. + +### 3. put + +Go through the put source code directly. + +```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 and value cannot be empty + if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; + for (Node[] tab = table;;) { + // f = target position element + Node f; int n, i, fh; // The hash value of the element at the target location is stored after fh + if (tab == null || (n = tab.length) == 0) + //The array bucket is empty, initialize the array bucket (spin+CAS) + tab = initTable(); + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // The bucket is empty, CAS is put in without locking, and if successful, it will just break out. + 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; + // Use synchronized lock to join the node + synchronized (f) { + if (tabAt(tab, i) == f) { + // Description is a linked list + if (fh >= 0) { + binCount = 1; + // Loop to add new or overwrite nodes + 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) { + // red-black tree + 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. Calculate hashcode based on key. + +2. Determine whether initialization is required. + +3. It is the Node located by the current key. If it is empty, it means that the current location can write data. Use CAS to try to write. If it fails, the spin is guaranteed to be successful. + +4. If the `hashcode == MOVED == -1` at the current location is required, expansion is required. + +5. If neither is satisfied, use the synchronized lock to write data. + +6. If the number is greater than `TREEIFY_THRESHOLD`, the tree method will be executed. In `treeifyBin`, it will first determine that the current array length is ≥64 before converting the linked list into a red-black tree. + +### 4. get + +The get process is relatively simple, just go through the source code directly. + +```java +public V get(Object key) { + Node[] tab; Node e, p; int n, eh; K ek; + // The hash location where the key is located + int h = spread(key.hashCode()); + if ((tab = table) != null && (n = tab.length) > 0 && + (e = tabAt(tab, (n - 1) & h)) != null) { + // If the element at the specified position exists, the hash value of the head node is the same + if ((eh = e.hash) == h) { + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + // If the key hash values are equal and the key values are the same, the element value is returned directly + return e.val; + } + else if (eh < 0) + // The hash value of the head node is less than 0, indicating that it is expanding or it is a red-black tree, find + return (p = e.find(h, key)) != null ? p.val : null; + while ((e = e.next) != null) { + // It is a linked list, traverse and search + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; +} +``` + +To summarize the get process: + +1. Calculate the position based on the hash value. +2. Find the specified position. If the head node is what you are looking for, return its value directly. +3. If the hash value of the head node is less than 0, it means that it is expanding or it is a red-black tree. Find it. +4. If it is a linked list, traverse to find it. + +Summary: + +In general, `ConcurrentHashMap` has changed quite a lot in Java8 compared to Java7. + +## 3. Summary + +The segmentation lock used by `ConcurrentHashMap` in Java7 means that only one thread can operate on each Segment at the same time. Each `Segment` is a structure similar to a `HashMap` array. It can be expanded and its conflicts will be converted into a linked list. However, the number of `Segment` cannot be changed once initialized. + +The `Synchronized` lock plus CAS mechanism used by `ConcurrentHashMap` in Java8. The structure has also evolved from **`Segment` array + `HashEntry` array + linked list** in Java7 to **Node array + linked list/red-black tree**. Node is a structure similar to a HashEntry. When its conflicts reach a certain size, it will be converted into a red-black tree, and when the conflicts are less than a certain number, it will return to the linked list. + +Some students may have questions about the performance of `Synchronized`. In fact, since the introduction of the lock upgrade strategy for `Synchronized` locks, the performance is no longer a problem. Interested students can learn about the **lock upgrade** of `Synchronized` on their own. + + \ No newline at end of file diff --git a/docs_en/java/collection/copyonwritearraylist-source-code.en.md b/docs_en/java/collection/copyonwritearraylist-source-code.en.md new file mode 100644 index 00000000000..bde9829f4b2 --- /dev/null +++ b/docs_en/java/collection/copyonwritearraylist-source-code.en.md @@ -0,0 +1,324 @@ +--- +title: CopyOnWriteArrayList 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: CopyOnWriteArrayList,写时复制,COW,读多写少,线程安全 List,快照,并发性能,内存占用 + - - meta + - name: description + content: 解析 CopyOnWriteArrayList 的写时复制策略,适用读多写少场景的并发优化与权衡,理解其线程安全 List 的实现方式。 +--- + +## CopyOnWriteArrayList 简介 + +在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 + +JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。关于`java.util.concurrent` 包下常见并发容器的总结,可以看我写的这篇文章:[Java 常见并发容器总结](https://javaguide.cn/java/concurrent/java-concurrent-collections.html) 。 + +### CopyOnWriteArrayList 到底有什么厉害之处? + +对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 + +这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 + +`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 + +### Copy-On-Write 的思想是什么? + +`CopyOnWriteArrayList`名字中的“Copy-On-Write”即写时复制,简称 COW。 + +下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错: + +> 写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。 + +这里再以 `CopyOnWriteArrayList`为例介绍:当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 + +可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。 + +不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点: + +1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。 +2. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。 +3. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。 +4. …… + +## CopyOnWriteArrayList 源码分析 + +这里以 JDK1.8 为例,分析一下 `CopyOnWriteArrayList` 的底层核心源码。 + +`CopyOnWriteArrayList` 的类定义如下: + +```java +public class CopyOnWriteArrayList +extends Object +implements List, RandomAccess, Cloneable, Serializable +{ + //... +} +``` + +`CopyOnWriteArrayList` 实现了以下接口: + +- `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); +``` + +Output: + +```plain +The updated list is: [Java, Golang] +After inserting elements into the list: [PHP, Java, Golang] +List size is: 3 +Batch delete results: true +After deleting elements in batches from the list: [PHP] +After the list is cleared: [] +``` + + \ No newline at end of file diff --git a/docs_en/java/collection/delayqueue-source-code.en.md b/docs_en/java/collection/delayqueue-source-code.en.md new file mode 100644 index 00000000000..2f22fbfe0ef --- /dev/null +++ b/docs_en/java/collection/delayqueue-source-code.en.md @@ -0,0 +1,364 @@ +--- +title: DelayQueue source code analysis +category: Java +tag: + - Java collections +head: + - - meta + - name: keywords + content: DelayQueue, delay queue, Delayed, getDelay, task scheduling, PriorityQueue, unbounded queue, ReentrantLock, Condition + - - meta + - name: description + content: Introduces the principle and common scenarios of DelayQueue's delayed task queue. Use cases include delayed execution and expired deletion, and analyzes the thread safety implementation based on PriorityQueue. +--- + +## DelayQueue Introduction + +`DelayQueue` is a delay queue provided by the JUC package (`java.util.concurrent)`, which is used to implement delayed tasks such as direct cancellation of orders without payment within 15 minutes. It is a type of `BlockingQueue`. The bottom layer is an unbounded queue implemented based on `PriorityQueue`, which is thread-safe. Regarding `PriorityQueue`, you can refer to this article written by the author: [PriorityQueue source code analysis](./priorityqueue-source-code.md). + +![Implementation class of BlockingQueue](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) + +The elements stored in `DelayQueue` must implement the `Delayed` interface, and the `getDelay()` method needs to be overridden (to calculate whether it expires). + +```java +public interface Delayed extends Comparable { + long getDelay(TimeUnit unit); +} +``` + +By default, `DelayQueue` arranges tasks in ascending order of expiration time. Only when the element expires (the return value of the `getDelay()` method is less than or equal to 0) can it be removed from the queue. + +## DelayQueue Development History + +- `DelayQueue` was first introduced in Java 5 as part of the `java.util.concurrent` package to support scenarios such as time-based task scheduling and cache expiration deletion. This version only supports the implementation of delay functions and has not yet solved thread safety issues. +- In Java 6, the implementation of `DelayQueue` has been optimized to improve its performance and reliability by using `ReentrantLock` and `Condition` to solve thread safety and the efficiency of interaction between threads. +- In Java 7, the implementation of `DelayQueue` has been further optimized to improve its concurrent operation performance by using CAS operations to add and remove elements. +- In Java 8, the implementation of `DelayQueue` has not undergone major changes, but new time classes such as `Duration` and `Instant` have been introduced in the `java.time` package, making time-based scheduling using `DelayQueue` more convenient and flexible. +- In Java 9, the implementation of `DelayQueue` has undergone some minor improvements, mainly optimizing and streamlining the code. + +In general, the development history of `DelayQueue` is mainly by optimizing its implementation and improving its performance and reliability, making it more suitable for scenarios such as time-based scheduling and cache expiration deletion. + +## DelayQueue common usage scenario examples + +Here we hope that the tasks can be executed according to our expected time. For example, if we submit three tasks and require them to be executed in 1s, 2s, and 3s respectively, even if they are added out of order, the tasks that require 1s to be executed after 1s will be executed on time. + +![Delayed task](https://oss.javaguide.cn/github/javaguide/java/collection/delayed-task.png) + +We can use `DelayQueue` to achieve this, so we first need to inherit `Delayed` to implement `DelayedTask`, implement the `getDelay` method and priority comparison `compareTo`. + +```java +/** + * Delay tasks + */ +public class DelayedTask implements Delayed { + /** + * Task expiration time + */ + private long executeTime; + /** + * Task + */ + private Runnable task; + + public DelayedTask(long delay, Runnable task) { + this.executeTime = System.currentTimeMillis() + delay; + this.task = task; + } + + /** + * Check how long the current task is due + * @param unit + * @return + */ + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + /** + * The delay queue needs to be queued in ascending order by expiration time, so we need to implement compareTo to compare expiration times. + * @param o + * @return + */ + @Override + public int compareTo(Delayed o) { + return Long.compare(this.executeTime, ((DelayedTask) o).executeTime); + } + + public void execute() { + task.run(); + } +} +``` + +After completing the encapsulation of the task, it is very simple to use. Just set the expiration time and then submit the task to the delay queue. + +```java +//Create a delay queue and add tasks +DelayQueue < DelayedTask > delayQueue = new DelayQueue < > (); + +//Add tasks due in 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"))); + +// Take out the task and execute it +while (!delayQueue.isEmpty()) { + //Block to get the first due task + DelayedTask task = delayQueue.take(); + if (task != null) { + task.execute(); + } +} +``` + +It can be seen from the output that even if the author mentions the task due in 2s first, the task due in 1s, Task1, will still be executed first. + +```java +Task 1 +Task 2 +Task 3 +``` + +## DelayQueue source code analysis + +Here we take JDK1.8 as an example to analyze the underlying core source code of `DelayQueue`. + +The class definition of `DelayQueue` is as follows: + +```java +public class DelayQueue extends AbstractQueue implements BlockingQueue +{ + //... +} +``` + +`DelayQueue` inherits the `AbstractQueue` class and implements the `BlockingQueue` interface. + +![DelayQueue class diagram](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-class-diagram.png) + +### Core member variables + +The four core member variables of `DelayQueue` are as follows: + +```java +//Reentrant lock, the key to achieving thread safety +private final transient ReentrantLock lock = new ReentrantLock(); +//The collection of data stored at the bottom of the delay queue ensures that the elements are arranged in ascending order according to the expiration time +private final PriorityQueue q = new PriorityQueue(); + +//Point to the thread with the highest execution priority +private Thread leader = null; +//Implement the interaction between multiple threads waiting to wake up +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(); + } +} +``` + +Let’s take a look at the non-blocking element acquisition method `poll`. The logic is relatively simple. The overall steps are as follows: + +1. Try to acquire a reentrant lock. +2. Check the first element of the queue to determine whether the element is empty. +3. If the element is empty or the element has not expired, empty will be returned directly. +4. If the element is not empty and has expired, call `poll` directly to return. +5. Release the reentrant lock `lock`. + +The source code is as follows, readers can refer to the source code and comments by themselves: + +```java +public E poll() { + //Try to acquire a reentrant lock + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //View the first element of the queue to determine whether the element is empty + E first = q.peek(); + + //If the element is empty or the element has not expired, return empty directly. + if (first == null || first.getDelay(NANOSECONDS) > 0) + return null; + else + //If the element is not empty and has expired, call poll directly to return it. + return q.poll(); + } finally { + //Release the reentrant lock + lock.unlock(); + } +} +``` + +### View elements + +The `peek` method will be called when acquiring elements above. As the name suggests, peek only peeks at the elements in the queue. Its steps are 4 steps: + +1. Lock up. +2. Call the peek method of priority queue q to view the element at index 0. +3. Release the lock. +4. Return the element. + +```java +public E peek() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return q.peek(); + } finally { + lock.unlock(); + } +} +``` + +## DelayQueue Common Interview Questions + +### What is the implementation principle of DelayQueue? + +The bottom layer of `DelayQueue` uses the priority queue `PriorityQueue` to store elements, and `PriorityQueue` uses the idea of ​​​​a binary small top heap to ensure that elements with small values ​​are ranked first, which makes `DelayQueue` very convenient for managing the priority of delayed tasks. At the same time, `DelayQueue` also uses a reentrant lock `ReentrantLock` to ensure thread safety, ensuring that only one thread can operate the delay queue per unit time. Finally, in order to achieve the interactive efficiency of waiting and waking up between multiple threads, `DelayQueue` also uses `Condition` to complete the waiting and waking up between multiple threads through the `await` and `signal` methods of `Condition`. + +### Is the implementation of DelayQueue thread-safe? + +The implementation of `DelayQueue` is thread-safe. It implements mutually exclusive access through `ReentrantLock` and `Condition` to implement waiting and wake-up operations between threads, which can ensure security and reliability in a multi-threaded environment. + +### What are the usage scenarios of DelayQueue? + +`DelayQueue` is usually used to implement scenarios such as scheduled task scheduling and cache expiration deletion. In scheduled task scheduling, the tasks that need to be executed need to be encapsulated into delayed task objects and added to `DelayQueue`. `DelayQueue` will automatically sort in ascending order according to the remaining delay time (default) to ensure that tasks can be executed in time order. For the cache expiration scenario, after the data is cached in the memory, we can encapsulate the cached key into a delayed deletion task and add it to the `DelayQueue`. When the data expires, we get the key of the task and remove the key from the memory. + +### What is the role of the Delayed interface in DelayQueue? + +The `Delayed` interface defines the remaining delay time of the element (`getDelay`) and the comparison rules between elements (this interface inherits the `Comparable` interface). If you want elements to be stored in `DelayQueue`, you must implement the `getDelay()` method and `compareTo()` method of the `Delayed` interface, otherwise `DelayQueue` cannot know the comparison between the remaining duration of the current task and the task priority. + +### What is the difference between DelayQueue and Timer/TimerTask? + +Both `DelayQueue` and `Timer/TimerTask` can be used to implement scheduled task scheduling, but their implementation methods are different. `DelayQueue` is implemented based on the priority queue and heap sorting algorithm, which can realize the execution of multiple tasks in time order; while `Timer/TimerTask` is based on a single thread and can only be executed in the order of execution of tasks. If the execution time of a task is too long, it will affect the execution of other tasks. In addition, `DelayQueue` also supports dynamic addition and removal of tasks, while `Timer/TimerTask` can only specify tasks when creating. + +## References + +- "In-depth understanding of high-concurrency programming: JDK core technology": +- Tell the implementation methods of Java's 6 delay queues in one breath (the interviewer must also be convinced): +- Illustration of DelayQueue source code (java 8) - Xiaojiujiu of delay queue: + \ No newline at end of file diff --git a/docs_en/java/collection/hashmap-source-code.en.md b/docs_en/java/collection/hashmap-source-code.en.md new file mode 100644 index 00000000000..a051ac4ef9f --- /dev/null +++ b/docs_en/java/collection/hashmap-source-code.en.md @@ -0,0 +1,586 @@ +--- +title: HashMap 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: HashMap,哈希表,散列冲突,拉链法,红黑树,JDK1.8,扰动函数,负载因子,扩容,rehash,树化阈值,TREEIFY_THRESHOLD,MIN_TREEIFY_CAPACITY,非线程安全,hashCode,数组+链表 + - - meta + - name: description + content: 深入解析 HashMap 底层实现,涵盖 JDK1.7/1.8 结构差异、hash 计算与扰动函数、负载因子与扩容、链表转红黑树的树化机制等关键细节。 +--- + + + +> 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! + +## HashMap 简介 + +HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。 + +`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 + +JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 + +## 底层数据结构分析 + +### JDK1.8 之前 + +JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。 + +HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 + +所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 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)时,会首先调用 `treeifyBin()`方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 `resize()` 方法对数组扩容。相关源码这里就不贴了,重点关注 `treeifyBin()`方法即可! + +![jdk1.8之后的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) + +**类的属性:** + +```java +public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { + // 序列号 + private static final long serialVersionUID = 362498820763181265L; + // 默认的初始容量是16 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + // 最大容量 + static final int MAXIMUM_CAPACITY = 1 << 30; + // 默认的负载因子 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树 + static final int TREEIFY_THRESHOLD = 8; + // 当桶(bucket)上的结点数小于等于这个值时树转链表 + static final int UNTREEIFY_THRESHOLD = 6; + // 桶中结构转化为红黑树对应的table的最小容量 + static final int MIN_TREEIFY_CAPACITY = 64; + // 存储元素的数组,总是2的幂次倍 + transient Node[] table; + // 一个包含了映射中所有键值对的集合视图 + transient Set> entrySet; + // 存放元素的个数,注意这个不等于数组的长度。 + transient int size; + // 每次扩容和更改map结构的计数器 + transient int modCount; + // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容 + int threshold; + // 负载因子 + final float loadFactor; +} +``` + +- **loadFactor 负载因子** + + loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 + + **loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**。 + + 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 + +- **threshold** + + **threshold = capacity \* loadFactor**,**当 Size>threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 + +**Node 节点类源码:** + +```java +// 继承自 Map.Entry +static class Node implements Map.Entry { + final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 + final K key;//键 + V value;//值 + // 指向下一个节点 + Node next; + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + // 重写hashCode()方法 + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + // 重写 equals() 方法 + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } +} +``` + +**树节点类源码:** + +```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(int hash, K key, V val, Node next) { + super(hash, key, val, next); + } + // 返回根节点 + final TreeNode root() { + for (TreeNode r = this, p;;) { + if ((p = r.parent) == null) + return r; + r = p; + } +``` + +## HashMap 源码分析 + +### 构造方法 + +HashMap 中有四个构造方法,它们分别如下: + +```java + // 默认构造函数。 + 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); + } +``` + +> 需要特别注意的是:传入的 `initialCapacity` 并不是最终的数组容量。`HashMap` 会调用 `tableSizeFor()` 将其**向上取整为大于或等于该值的最小 2 的幂次方**,并暂时保存到 `threshold` 字段。真正的 `table` 数组会在第一次扩容(`resize()`)时才初始化为这个大小。 +> +> 例如:`initialCapacity = 9` → `threshold = 16` → `table` 长度最终为 16。 + +**putMapEntries 方法:** + +```java +final void putMapEntries(Map m, boolean evict) { + int s = m.size(); + if (s > 0) { + // 判断table是否已经初始化 + if (table == null) { // pre-size + /* + * 未初始化,s为m的实际元素个数,ft=s/loadFactor => s=ft*loadFactor, 跟我们前面提到的 + * 阈值=容量*负载因子 是不是很像,是的,ft指的是要添加s个元素所需的最小的容量 + */ + float ft = ((float)s / loadFactor) + 1.0F; + int t = ((ft < (float)MAXIMUM_CAPACITY) ? + (int)ft : MAXIMUM_CAPACITY); + /* + * 根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所 + * 需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。 + * 注意这里不是初始化阈值 + */ + if (t > threshold) + threshold = tableSizeFor(t); + } + // 已初始化,并且m元素个数大于阈值,进行扩容处理 + else if (s > threshold) + resize(); + // 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容 + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + putVal(hash(key), key, value, false, evict); + } + } +} +``` + +### put 方法 + +HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。 + +**对 putVal 方法添加元素的分析如下:** + +1. 如果定位到的数组位置没有元素 就直接插入。 +2. 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 + +![ ](https://oss.javaguide.cn/github/javaguide/database/sql/put.png) + +```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) { + Node[] tab; Node p; int n, i; + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素(处理hash冲突) + else { + Node e; K k; + //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + // 判断插入的是否是红黑树节点 + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 不是红黑树节点则说明为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 + // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。 + // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + // 表示在桶中找到key值、hash值与插入元素相等的结点 + if (e != null) { + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +``` + +**我们再来对比一下 JDK1.7 put 方法的代码** + +**对于 put 方法的分析如下:** + +- ① 如果定位到的数组位置没有元素 就直接插入。 +- ② 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素。 + +```java +public V put(K key, V value) + if (table == EMPTY_TABLE) { + 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) { // 先遍历 + Object k; + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + + modCount++; + addEntry(hash, key, value, i); // 再插入 + return null; +} +``` + +### get 方法 + +```java +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +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) { + // 数组元素相等 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + // 桶中不止一个节点 + if ((e = first.next) != null) { + // 在树中get + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + // 在链表中get + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +### resize 方法 + +进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize 方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。 + +```java +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + // 超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 没超过最大值,就扩充为原来的2倍 + 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中,此时只需要将其作为新的数组容量 + newCap = oldThr; + else { + // signifies using defaults 无参构造函数创建的对象在这里计算容量和阈值 + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + if (newThr == 0) { + // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化, + // 或者扩容前的旧容量小于16,在这里计算新的resize上限 + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + if (oldTab != null) { + // 把每个bucket都移动到新的buckets中 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + // 只有一个节点,直接计算元素新的位置即可 + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + // 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。 + // 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。 + ((TreeNode)e).split(this, newTab, j, oldCap); + else { + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + // 原索引 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + // 原索引+oldCap + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 原索引放到bucket里 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // 原索引+oldCap放到bucket里 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +## HashMap common method testing + +```java +package map; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +public class HashMapDemo { + + public static void main(String[] args) { + HashMap map = new HashMap(); + //Keys cannot be repeated, values can be repeated + map.put("san", "Zhang San"); + map.put("si", "李思"); + map.put("wu", "王五"); + map.put("wang", "老王"); + map.put("wang", "Old Wang 2");//Old Wang is overwritten + map.put("lao", "老王"); + System.out.println("-------Directly output hashmap:-------"); + System.out.println(map); + /** + * Traverse HashMap + */ + // 1. Get all keys in Map + System.out.println("-------foreach gets all the keys in the Map:------"); + Set keys = map.keySet(); + for (String key : keys) { + System.out.print(key+" "); + } + System.out.println();//Line break + // 2. Get all values in the Map + System.out.println("-------foreach gets all the values in the Map:------"); + Collection values = map.values(); + for (String value : values) { + System.out.print(value+" "); + } + System.out.println();//Line break + // 3. Get the value of the key and get the value corresponding to the key at the same time. + System.out.println("-------get the value of key and also get the value corresponding to key:-------"); + Set keys2 = map.keySet(); + for (String key : keys2) { + System.out.print(key + ":" + map.get(key)+" "); + + } + /** + * If you want to traverse both key and value, this method is recommended, because if you first obtain the keySet and then execute map.get(key), map will perform two traversals internally. + * Once when obtaining the keySet, and once when traversing all keys. + */ + // When I call the put(key, value) method, the key and value will first be encapsulated into + //In the static inner class object Entry, add the Entry object to the array, so we want to get + //For all key-value pairs in the map, we only need to get all the Entry objects in the array, and then + // Call the getKey() and getValue() methods in the Entry object to obtain the key-value pair + Set> entries = map.entrySet(); + for (java.util.Map.Entry entry : entries) { + System.out.println(entry.getKey() + "--" + entry.getValue()); + } + + /** + *Other common methods of HashMap + */ + 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, Li Si 2):"+map); + } + +} +``` + + \ No newline at end of file diff --git a/docs_en/java/collection/java-collection-precautions-for-use.en.md b/docs_en/java/collection/java-collection-precautions-for-use.en.md new file mode 100644 index 00000000000..05e11a44e67 --- /dev/null +++ b/docs_en/java/collection/java-collection-precautions-for-use.en.md @@ -0,0 +1,470 @@ +--- +title: Summary of precautions for using Java collections +category: Java +tag: + - Java collections +head: + - - meta + - name: keywords + content: Java collections, usage precautions, empty judgment, isEmpty, size, concurrent containers, best practices, ConcurrentLinkedQueue + - - meta + - name: description + content: Summarizes common precautions and best practices for using Java collections, covering null detection, concurrent container features, etc. to help avoid error-prone points and performance problems. +--- + +In this article, I summarize common precautions about the use of collections and their specific principles based on the "Alibaba Java Development Manual". + +It is strongly recommended that friends read it several times to avoid these low-level problems when writing code. + +## Set empty + +The description of "Alibaba Java Development Manual" is as follows: + +> **To determine whether all elements in the collection are empty, use the `isEmpty()` method instead of the `size()==0` method. ** + +This is because the `isEmpty()` method is more readable and has a time complexity of `O(1)`. + +The time complexity of the `size()` method of most of the collections we use is also `O(1)`. However, there are also many whose complexity is not `O(1)`, such as `ConcurrentLinkedQueue` under the `java.util.concurrent` package. The `isEmpty()` method of `ConcurrentLinkedQueue` is judged by the `first()` method, where the `first()` method returns the first node in the queue whose value is not `null` (the reason why the node value is `null` is the logical deletion used in the iterator) + +```java +public boolean isEmpty() { return first() == null; } + +Node first() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + boolean hasItem = (p.item != null); + if (hasItem || (q = p.next) == null) { // The current node value is not empty or reaches the end of the queue + updateHead(h, p); // Set head to p + return hasItem ? p : null; + } + else if (p == q) continue restartFromHead; + else p = q; // p = p.next + } + } +} +``` + +Since the `updateHead(h, p)` method is executed when inserting and deleting elements, the execution time complexity of this method can be approximately `O(1)`. The `size()` method needs to traverse the entire linked list, and the time complexity is `O(n)` + +```java +public int size() { + int count = 0; + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + if (++count == Integer.MAX_VALUE) + break; + return count; +} +``` + +In addition, the time complexity of the `size()` method and the `isEmpty()` method in `ConcurrentHashMap` 1.7 is also different. `ConcurrentHashMap` 1.7 stores the number of elements in each `Segment`, the `size()` method needs to count the number of each `Segment`, and `isEmpty()` only needs to find the first `Segment` that is not empty. However, the `size()` method and `isEmpty()` in `ConcurrentHashMap` 1.8 both need to call the `sumCount()` method, and its time complexity is related to the size of the `Node` array. The following is the source code of the `sumCount()` method: + +```java +final long sumCount() { + CounterCell[] as = counterCells; CounterCell a; + long sum = baseCount; + if (as != null) + for (int i = 0; i < as.length; ++i) + if ((a = as[i]) != null) + sum += a.value; + return sum; +} +``` + +This is because in a concurrent environment, `ConcurrentHashMap` stores the number of nodes in each `Node` in the `CounterCell[]` array. In `ConcurrentHashMap` 1.7, the number of elements is stored in each `Segment`, the `size()` method needs to count the number of each `Segment`, and `isEmpty()` only needs to find the first `Segment` that is not empty. + +## Convert collection to Map + +The description of "Alibaba Java Development Manual" is as follows: + +> **When using the `toMap()` method of the `java.util.stream.Collectors` class to convert to a `Map` collection, be sure to note that an NPE exception will be thrown when the value is null. ** + +```java +class Person { + private String name; + private String phoneNumber; + // getters and setters +} + +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 why below. + +First, let's look at the `toMap()` method of the `java.util.stream.Collectors` class. We can see that it internally calls the `merge()` method of the `Map` interface. + +```java +public static > +Collector toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction, + Supplier mapSupplier) { + BiConsumer accumulator + = (map, element) -> map.merge(keyMapper.apply(element), + valueMapper.apply(element), mergeFunction); + return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); +} +``` + +The `merge()` method of the `Map` interface is as follows. This method is the default implementation in the interface. + +> If you don’t know the new features of Java 8, please read this article: ["Summary of New Features of Java 8"](https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw). + +```java +default V merge(K key, V value, + BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + Objects.requireNonNull(value); + V oldValue = get(key); + V newValue = (oldValue == null) ? value : + remappingFunction.apply(oldValue, value); + if(newValue == null) { + remove(key); + } else { + put(key, newValue); + } + return newValue; +}``` + +The `merge()` method will first call the `Objects.requireNonNull()` method to determine whether value is null. + +```java +public static T requireNonNull(T obj) { + if (obj == null) + throw new NullPointerException(); + return obj; +} +``` + +> `Collectors` also provides the `toMap()` method without mergeFunction. However, if a key conflict occurs at this time, a `duplicateKeyException` exception will be thrown. Therefore, it is strongly recommended to use the `toMap()` method with a required mergeFunction. + +## Collection traversal + +The description of "Alibaba Java Development Manual" is as follows: + +> **Do not perform `remove/add` operations on elements in a foreach loop. Please use the `Iterator` method to remove elements. If you perform concurrent operations, you need to lock the `Iterator` object. ** + +By decompiling, you will find that the underlying syntax of foreach still relies on `Iterator`. However, the `remove/add` operation directly calls the collection's own method, not the `remove/add` method of `Iterator` + +This causes `Iterator` to inexplicably find that one of its elements has been `removed/add`, and then it will throw a `ConcurrentModificationException` to prompt the user that a concurrent modification exception has occurred. This is the **fail-fast mechanism** generated in the single-threaded state. + +> **fail-fast mechanism**: When multiple threads modify the fail-fast collection, `ConcurrentModificationException` may be thrown. This situation may occur even in a single thread, as mentioned above. +> +> Related reading: [What is fail-fast](https://www.cnblogs.com/54chensongxia/p/12470446.html). + +Starting from Java8, you can use the `Collection#removeIf()` method to delete 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); /* Remove all even numbers in the list */ +System.out.println(list); /* [1, 3, 5, 7, 9] */ +``` + +In addition to directly using `Iterator` to perform traversal operations as described above, you can also: + +- Use a normal for loop +- Use fail-safe collection classes. All collection classes under the `java.util` package are fail-fast, and all classes under the `java.util.concurrent` package are fail-safe. +-… + +## Collection deduplication + +The description of "Alibaba Java Development Manual" is as follows: + +> **You can take advantage of the unique characteristics of the `Set` element to quickly deduplicate a collection and avoid using `contains()` of `List` to traverse deduplication or determine inclusion operations. ** + +Here we take `HashSet` and `ArrayList` as examples. + +```java +// Set deduplication code example +public static Set removeDuplicateBySet(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new HashSet<>(); + } + return new HashSet<>(data); +} + +// List deduplication code example +public static List removeDuplicateByList(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new ArrayList<>(); + + } + List result = new ArrayList<>(data.size()); + for (T current : data) { + if (!result.contains(current)) { + result.add(current); + } + } + return result; +} + +``` + +The core difference between the two is the implementation of the `contains()` method. + +The `contains()` method of `HashSet` relies on the `containsKey()` method of `HashMap`, and its time complexity is close to O(1) (O(1) when there is no hash conflict). + +```java +private transient HashMap map; +public boolean contains(Object o) { + return map.containsKey(o); +} +``` + +We have N elements inserted into Set, so the time complexity is close to O (n). + +The `contains()` method of `ArrayList` is done by traversing all elements, and the time complexity is close to O(n). + +```java +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +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++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} + +``` + +## Convert collection to array + +The description of "Alibaba Java Development Manual" is as follows: + +> **To use the method of converting a collection to an array, you must use the `toArray(T[] array)` of the collection, and pass in an empty array of the same type and length 0. ** + +The parameter of the `toArray(T[] array)` method is a generic array. If no parameters are passed in the `toArray` method, an `Object` type array is returned. + +```java +String [] s= new String[]{ + "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" +}; +List list = Arrays.asList(s); +Collections.reverse(list); +//If no type is specified, an error will be reported +s=list.toArray(new String[0]); +``` + +Due to JVM optimization, it is now better to use `new String[0]` as a parameter of the `Collection.toArray()` method. `new String[0]` acts as a template and specifies the type of the returned array. 0 is to save space, because it is only used to describe the returned type. For details, see: + +## Convert array to collection + +The description of "Alibaba Java Development Manual" is as follows: + +> **When using the tool class `Arrays.asList()` to convert an array into a collection, you cannot use its methods related to modifying the collection. Its `add/remove/clear` method will throw an `UnsupportedOperationException` exception. ** + +I encountered a similar pitfall in a previous project. + +`Arrays.asList()` is quite common in daily development. We can use it 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 statement +List myList = Arrays.asList("Apple","Banana", "Orange"); +``` + +The JDK source code explains this method: + +```java +/** + *Returns a fixed-size list backed by the specified array. This method acts as a bridge between array-based and collection-based APIs, + * Used in conjunction with Collection.toArray(). The returned List is serializable and implements the RandomAccess interface. + */ +public static List asList(T... a) { + return new ArrayList<>(a); +}``` + +Let’s summarize the usage precautions below. + +**1. `Arrays.asList()` is a generic method, and the array passed must be an object array, not a basic type. ** + +```java +int[] myArray = {1, 2, 3}; +List myList = Arrays.asList(myArray); +System.out.println(myList.size());//1 +System.out.println(myList.get(0));//array address value +System.out.println(myList.get(1));//Error report: ArrayIndexOutOfBoundsException +int[] array = (int[]) myList.get(0); +System.out.println(array[0]);//1 +``` + +When an array of native data types is passed in, the actual parameter obtained by `Arrays.asList()` is not the elements in the array, but the array object itself! At this time, the only element of `List` is this array, which explains the above code. + +We can solve this problem by using a wrapper type array. + +```java +Integer[] myArray = {1, 2, 3}; +``` + +**2. Using the collection modification methods: `add()`, `remove()`, `clear()` will throw an exception. ** + +```java +List myList = Arrays.asList(1, 2, 3); +myList.add(4);//Error when running: UnsupportedOperationException +myList.remove(1);//Error when running: UnsupportedOperationException +myList.clear();//Error when running: UnsupportedOperationException +``` + +The `Arrays.asList()` method returns not `java.util.ArrayList`, but an internal class of `java.util.Arrays`. This internal class does not implement the collection modification methods or does not override these methods. + +```java +List myList = Arrays.asList(1, 2, 3); +System.out.println(myList.getClass());//class java.util.Arrays$ArrayList +``` + +The picture below is a simple source code of `java.util.Arrays$ArrayList`. We can see what methods this class overrides. + +```java + private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable + { + ... + + @Override + public E get(int index) { + ... + } + + @Override + public E set(int index, E element) { + ... + } + + @Override + public int indexOf(Object o) { + ... + } + + @Override + public boolean contains(Object o) { + ... + } + + @Override + public void forEach(Consumer action) { + ... + } + + @Override + public void replaceAll(UnaryOperator operator) { + ... + } + + @Override + public void sort(Comparator c) { + ... + } + } +``` + +Let's take another look at the `add/remove/clear` method of `java.util.AbstractList` and we will know why `UnsupportedOperationException` is thrown. + +```java +public E remove(int index) { + throw new UnsupportedOperationException(); +} +public boolean add(E e) { + add(size(), e); + return true; +} +public void add(int index, E element) { + throw new UnsupportedOperationException(); +} + +public void clear() { + removeRange(0, size()); +} +protected void removeRange(int fromIndex, int toIndex) { + ListIterator it = listIterator(fromIndex); + for (int i=0, n=toIndex-fromIndex; i List arrayToList(final T[] array) { + final List l = new ArrayList(array.length); + + for (final T s : array) { + l.add(s); + } + return l; +} + + +Integer [] myArray = { 1, 2, 3 }; +System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList +``` + +2. The easiest way + +```java +List list = new ArrayList<>(Arrays.asList("a", "b", "c")) +``` + +3. Use Java8’s `Stream` (recommended) + +```java +Integer [] myArray = { 1, 2, 3 }; +List myList = Arrays.stream(myArray).collect(Collectors.toList()); +//Basic types can also be converted (relying on the boxing operation of boxed) +int [] myArray2 = { 1, 2, 3 }; +List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); +``` + +4. Use Guava + +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/gu ava/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 method: (parameters cannot be empty) + +```java +List il = ImmutableList.of("string", "elements"); // from varargs +List il = ImmutableList.copyOf(aStringArray); // from array +``` + +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 +List l2 = Lists.newArrayList(aStringArray); // from array +List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs``` + +5、使用 Apache Commons Collections + +```java +List list = new ArrayList(); +CollectionUtils.addAll(list, str); +``` + +6、 使用 Java9 的 `List.of()`方法 + +```java +Integer[] array = {1, 2, 3}; +List list = List.of(array); +``` + + \ No newline at end of file diff --git a/docs_en/java/collection/java-collection-questions-01.en.md b/docs_en/java/collection/java-collection-questions-01.en.md new file mode 100644 index 00000000000..1aaff84e1f9 --- /dev/null +++ b/docs_en/java/collection/java-collection-questions-01.en.md @@ -0,0 +1,614 @@ +--- +title: Java集合常见面试题总结(上) +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: Collection,List,Set,Queue,Deque,PriorityQueue + - - meta + - name: description + content: Java集合常见知识点和面试题总结,希望对你有帮助! +--- + + + + + +## 集合概述 + +### Java 集合概览 + +Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`。 + +Java 集合框架如下图所示: + +![Java 集合框架概览](https://oss.javaguide.cn/github/javaguide/java/collection/java-collection-hierarchy.png) + +注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了`AbstractList`, `NavigableSet`等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。 + +### ⭐️说说 List, Set, Queue, Map 四者的区别? + +- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。 +- `Set`(注重独一无二的性质): 存储的元素不可重复的。 +- `Queue`(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 +- `Map`(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。 + +### 集合框架底层数据结构总结 + +先来看一下 `Collection` 接口下面的集合。 + +#### List + +- `ArrayList`:`Object[]` 数组。详细可以查看:[ArrayList 源码分析](./arraylist-source-code.md)。 +- `Vector`:`Object[]` 数组。 +- `LinkedList`:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。详细可以查看:[LinkedList 源码分析](./linkedlist-source-code.md)。 + +#### Set + +- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素。 +- `LinkedHashSet`: `LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。 +- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树)。 + +#### Queue + +- `PriorityQueue`: `Object[]` 数组来实现小顶堆。详细可以查看:[PriorityQueue 源码分析](./priorityqueue-source-code.md)。 +- `DelayQueue`:`PriorityQueue`。详细可以查看:[DelayQueue 源码分析](./delayqueue-source-code.md)。 +- `ArrayDeque`: 可扩容动态双向数组。 + +再来看看 `Map` 接口下面的集合。 + +#### 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`:红黑树(自平衡的排序二叉树)。 + +### 如何选用集合? + +我们主要根据集合的特点来选择合适的集合。比如: + +- 我们需要根据键值获取到元素值时就选用 `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 源码分析](https://javaguide.cn/java/collection/linkedlist-source-code.html) 。 + +![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 指向后一个节点。 + +![Bidirectional linked list](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) + +**Two-way circular linked list:** The next of the last node points to the head, and the prev of the head points to the last node, forming a ring. + +![Bidirectional circular linked list](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-circular-linkedlist.png) + +#### Supplementary content: RandomAccess interface + +```java +public interface RandomAccess { +} +``` + +Looking at the source code we find that actually nothing is defined in the `RandomAccess` interface. So, in my opinion, the `RandomAccess` interface is nothing more than an identifier. What does it identify? Indicates that classes implementing this interface have random access capabilities. + +In the `binarySearch()` method, it determines whether the incoming list is an instance of `RandomAccess`. If so, call the `indexedBinarySearch()` method. If not, then call the `iteratorBinarySearch()` method. + +```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. + +The idea of fast failure is to indicate faults and stop operation in advance for possible exceptions. By discovering and stopping errors as early as possible, the risk of cascading faulty systems can be reduced. + +Most of the collections under the `java.util` package (such as `ArrayList`, `HashMap`) do not support thread safety. In order to detect thread safety risks caused by concurrent operations in advance, it is proposed to maintain a `modCount` to record the number of modifications. During the iteration, it is judged whether there are concurrent operations by comparing the expected number of modifications `expectedModCount` and `modCount` to see if there are concurrent operations, thereby achieving fast failure and ensuring that unnecessary complex code execution is avoided when exceptions occur. + +**ArrayList (fail-fast) example:** + +```java + // Use thread-unsafe ArrayList, which is a fail-fast collection + List list = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(2); + + for (int i = 0; i < 5; i++) { + list.add(i); + } + System.out.println("Initial list: " + list); + + Thread t1 = new Thread(() -> { + try { + for (Integer i : list) { + System.out.println("Iterator Thread (t1) sees: " + i); + Thread.sleep(100); + } + } catch (ConcurrentModificationException e) { + System.err.println("!!! Iterator Thread (t1) caught ConcurrentModificationException as expected."); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + }); + + Thread t2 = new Thread(() -> { + try { + Thread.sleep(50); + System.out.println("-> Modifier Thread (t2) is removing element 1..."); + list.remove(Integer.valueOf(1)); + System.out.println("-> Modifier Thread (t2) finished removal."); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + }); + + t1.start(); + t2.start(); + latch.await(); + + System.out.println("Final list state: " + list); +``` + +Output: + +``` +Initial list: [0, 1, 2, 3, 4] +Iterator Thread (t1) sees: 0 +-> Modifier Thread (t2) is removing element 1... +-> Modifier Thread (t2) finished removal. +!!! Iterator Thread (t1) caught ConcurrentModificationException as expected. +Final list state: [0, 2, 3, 4] +``` + +After the program modifies the list in thread t2, the next iteration of thread t1 immediately throws a `ConcurrentModificationException`. This is because the ArrayList iterator checks whether `modCount` has been changed every time `next()` is called. Once it is discovered that the collection has been modified without the iterator's knowledge, it will "fail fast" immediately to prevent unpredictable consequences from continuing operations on inconsistent data. + +In this regard, we also give the `next` method when the underlying iterator of the `for` loop obtains the next element. You can see that its internal `checkForComodification` has logic for comparing the number of modifications: + +```java + public E next() { + //Check if there are concurrent modifications + checkForCommodification(); + //...... + //Return the next element + return (E) elementData[lastRet = i]; + } + +final void checkForCommodification() { + //When the current number of loop traversals is inconsistent with the expected number of modifications, ConcurrentModificationException will be thrown + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + }``` + +And `fail-safe` means safe failure. It is designed to recover and continue running even in the face of unexpected situations, which makes it particularly suitable for uncertain or unstable environments: + +> 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. + +This idea is often used in concurrent containers. The most classic implementation is the implementation of `CopyOnWriteArrayList`. Through the idea of copy-on-write (Copy-On-Write), a snapshot is copied when the modification operation is performed, and addition or deletion is completed based on this snapshot. After the deletion operation, point the underlying array reference of `CopyOnWriteArrayList` to this new array space, thereby avoiding concurrent operation security issues caused by interference from concurrent modifications during iteration. Of course, this approach also has the disadvantage that real-time results cannot be obtained during traversal operations: + +![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png) + +Correspondingly, we also give the core code of `CopyOnWriteArrayList` to implement `fail-safe`. You can see that its implementation is to obtain the array reference through `getArray` and then obtain a snapshot of the array through `Arrays.copyOf`. After completing the addition operation based on this snapshot, modify the reference address pointed to by the underlying `array` variable to complete copy-on-write: + +```java +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //Get the original array + Object[] elements = getArray(); + int len = elements.length; + //Copy a memory snapshot based on the original array + Object[] newElements = Arrays.copyOf(elements, len + 1); + //Perform adding operation + newElements[len] = e; + //array points to the new array + setArray(newElements); + return true; + } finally { + lock.unlock(); + } + } +``` + +## Set + +### The difference between Comparable and Comparator + +The `Comparable` interface and the `Comparator` interface are both interfaces used for sorting in Java. They play an important role in implementing size comparison and sorting between class objects: + +- The `Comparable` interface is actually from the `java.lang` package and it has a `compareTo(Object obj)` method for sorting +- The `Comparator` interface is actually from the `java.util` package and it has a `compare(Object obj1, Object obj2)` method for sorting + +Generally, when we need to use custom sorting for a collection, we have to override the `compareTo()` method or the `compare()` method. When we need to implement two sorting methods for a certain collection, such as using one sorting method for song titles and artist names in a `song` object, we can rewrite the `compareTo()` method and use a homemade `Comparator` method or use two `Comparator` To implement song title sorting and singer name sorting, the second means that we can only use the two-parameter version of `Collections.sort()`. + +#### Comparator custom sorting + +```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("Original array:"); +System.out.println(arrayList); +// void reverse(List list): reverse +Collections.reverse(arrayList); +System.out.println("Collections.reverse(arrayList):"); +System.out.println(arrayList); + +//void sort(List list), sort in ascending order of natural sorting +Collections.sort(arrayList); +System.out.println("Collections.sort(arrayList):"); +System.out.println(arrayList); +//Usage of custom sorting +Collections.sort(arrayList, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2.compareTo(o1); + } +}); +System.out.println("After customized sorting:"); +System.out.println(arrayList); +``` + +Output: + +```plain +Original array: +[-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] +After custom sorting: +[7, 4, 3, 3, -1, -5, -7, -9] +``` + +#### Override the compareTo method to sort by age + +```java +//The person object does not implement the Comparable interface, so it must be implemented so that errors will not occur and the data in the treemap can be arranged in order. +// The String class in the previous example has implemented the Comparable interface by default. For details, you can view the API documentation of the String class. Others +// Classes such as Integer have already implemented the Comparable interface, so there is no need to implement it separately. +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 overrides the compareTo method to sort by age + */ + @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("Zhang San", 30), "zhangsan"); + pdata.put(new Person("李思", 20), "lisi"); + pdata.put(new Person("王五", 10), "wangwu"); + pdata.put(new Person("小红", 5), "xiaohong"); + // Get the value of key and get the value corresponding to key at the same time + 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`,这样可以防止生产者和消费者线程之间的锁争夺。 +- Memory usage: `ArrayBlockingQueue` needs to allocate array memory in advance, while `LinkedBlockingQueue` dynamically allocates linked list node memory. This means that `ArrayBlockingQueue` will occupy a certain amount of memory space when it is created, and often the memory requested is larger than the actual memory used, while `LinkedBlockingQueue` gradually occupies memory space according to the increase of elements. + + \ No newline at end of file diff --git a/docs_en/java/collection/java-collection-questions-02.en.md b/docs_en/java/collection/java-collection-questions-02.en.md new file mode 100644 index 00000000000..db11c92ab25 --- /dev/null +++ b/docs_en/java/collection/java-collection-questions-02.en.md @@ -0,0 +1,657 @@ +--- +title: Summary of common interview questions about Java collections (Part 2) +category: Java +tag: + - Java collections +head: + - - meta + - name: keywords + content: HashMap,ConcurrentHashMap,Hashtable,List,Set + - - meta + - name: description + content: Java collects common knowledge points and a summary of interview questions. I hope it will be helpful to you! +--- + + + +## Map (important) + +### ⭐️The difference between HashMap and Hashtable + +- **Thread safety:** `HashMap` is not thread-safe, `Hashtable` is thread-safe, because the internal methods of `Hashtable` are basically modified with `synchronized`. (If you want to ensure thread safety, use `ConcurrentHashMap`!); +- **Efficiency:** Because of thread safety issues, `HashMap` is slightly more efficient than `Hashtable`. In addition, `Hashtable` is basically obsolete, do not use it in your code; +- **Support for Null key and Null value:** `HashMap` can store null keys and values, but there can only be one null as a key and multiple null as a value; Hashtable does not allow null keys and null values, otherwise a `NullPointerException` will be thrown. +- **The difference between the initial capacity size and each expansion capacity size:** ① If the initial capacity value is not specified when creating, the default initial size of `Hashtable` is 11, and each time it is expanded thereafter, the capacity becomes the original 2n+1. The default initialization size of `HashMap` is 16. Each subsequent expansion will double the capacity. ② If the initial value of the capacity is given when creating, then `Hashtable` will directly use the size you gave, and `HashMap` will expand it to a power of 2 (guaranteed by the `tableSizeFor()` method in `HashMap`, the source code is given below). In other words, `HashMap` always uses the power of 2 as the size of the hash table. Why it is a power of 2 will be introduced later. +- **Underlying data structure:** `HashMap` after JDK1.8 has undergone major changes in resolving hash conflicts. When the length of the linked list is greater than the threshold (default is 8), the linked list will be converted into a red-black tree (before converting the linked list into a red-black tree, it will be judged if the length of the current array is less than 64, then it will choose to expand the array first instead of converting to a red-black tree) to reduce the search time (I will analyze this process in conjunction with the source code later). `Hashtable` has no such mechanism. +- **Hash function implementation**: `HashMap` perturbs the hash value with a mix of high and low bits to reduce collisions, while `Hashtable` uses the `hashCode()` value of the key directly. + +**Constructor with initial capacity in `HashMap`:** + +```java + 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; + this.threshold = tableSizeFor(initialCapacity); + } + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } +``` + +The following method ensures that `HashMap` always uses a power of 2 as the size of the hash table. + +```java +/** + * Returns a power of two size for the given target capacity. + */ +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; +} +``` + +### The difference between HashMap and HashSet + +If you have seen the source code of `HashSet`, you should know: the bottom layer of `HashSet` is implemented based on `HashMap`. (The source code of `HashSet` is very, very small, because except for `clone()`, `writeObject()`, and `readObject()` which `HashSet` itself has to implement, other methods directly call the methods in `HashMap`. + +| `HashMap` | `HashSet` | +| :----------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | +| Implemented the `Map` interface | Implemented the `Set` interface | +| Store key-value pairs | Store only objects | +| Call `put()` to add elements to map | Call `add()` method to add elements to `Set` | +| `HashMap` uses keys to calculate `hashcode` | `HashSet` uses member objects to calculate `hashcode` values. `hashcode` may be the same for two objects, so the `equals()` method is used to determine the equality of objects | + +### ⭐️The difference between HashMap and TreeMap + +Both `TreeMap` and `HashMap` inherit from `AbstractMap`, but it should be noted that `TreeMap` also implements the `NavigableMap` interface and the `SortedMap` interface. + +![TreeMap inheritance diagram](https://oss.javaguide.cn/github/javaguide/java/collection/treemap_hierarchy.png) + +Implementing the `NavigableMap` interface gives `TreeMap` the ability to search for elements within the collection. + +The `NavigableMap` interface provides rich methods to explore and manipulate key-value pairs: + +1. **Directed Search**: Methods such as `ceilingEntry()`, `floorEntry()`, `higherEntry()` and `lowerEntry()` can be used to locate the closest key-value pairs that are greater than or equal to, less than or equal to, strictly greater than, or strictly less than a given key.2. **Subset operation**: The `subMap()`, `headMap()` and `tailMap()` methods can efficiently create a subset view of the original collection without copying the entire collection. +3. **Reverse order view**: The `descendingMap()` method returns a `NavigableMap` view in reverse order, allowing the entire `TreeMap` to be iterated in reverse order. +4. **Boundary operations**: Methods such as `firstEntry()`, `lastEntry()`, `pollFirstEntry()` and `pollLastEntry()` can easily access and remove elements. + +These methods are implemented based on the properties of the red-black tree data structure. The red-black tree maintains a balanced state, thus ensuring that the time complexity of the search operation is O(log n). This makes `TreeMap` a powerful tool for dealing with ordered set search problems. + +Implementing the `SortedMap` interface gives `TreeMap` the ability to sort elements in a collection based on keys. The default is to sort by key in ascending order, but we can also specify a comparator for sorting. The sample code is as follows: + +```java +/** + * @author shuang.kou + * @createTime June 15, 2020 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()); + }); + } +} +``` + +Output: + +```plain +person1 +person4 +person2 +person3 +``` + +It can be seen that the elements in `TreeMap` are already arranged in ascending order according to the age field of `Person`. + +Above, we implemented it by passing in an anonymous inner class. You can replace the code with a Lambda expression implementation: + +```java +TreeMap treeMap = new TreeMap<>((person1, person2) -> { + int num = person1.getAge() - person2.getAge(); + return Integer.compare(num, 0); +}); +``` + +**In summary, compared to `HashMap`, `TreeMap` mainly has the ability to sort the elements in the collection according to keys and the ability to search for elements in the collection. ** + +### How to check duplicates in HashSet? + +The following content is excerpted from the second edition of my Java enlightenment book "Head first java": + +> When you add an object to `HashSet`, `HashSet` will first calculate the `hashcode` value of the object to determine the location where the object is added. It will also compare it with the `hashcode` values of other added objects. If there is no matching `hashcode`, `HashSet` will assume that the object does not appear repeatedly. But if objects with the same `hashcode` value are found, the `equals()` method will be called to check whether the objects with equal `hashcode` are really the same. If the two are the same, `HashSet` will not let the join operation succeed. + +In JDK1.8, the `add()` method of `HashSet` simply calls the `put()` method of `HashMap`, and checks the return value to ensure whether there are duplicate elements. Take a look directly at the source code in `HashSet`: + +```java +// Returns: true if this set did not already contain the specified element +//Return value: Returns true when there is no element containing add in the set +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +``` + +The following description can also be seen in the `putVal()` method of `HashMap`: + +```java +// Returns: previous value, or null if none +//Return value: If there is no element at the insertion position, return null, otherwise return the previous element +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { +... +} +``` + +That is to say, in JDK1.8, no matter whether an element already exists in `HashSet`, `HashSet` will be inserted directly, but the return value of the `add()` method will tell us whether the same element exists before insertion. + +### ⭐️The underlying implementation of HashMap + +#### Before JDK1.8 + +Before JDK1.8, the bottom layer of `HashMap` was **array and linked list**, which were used together to form **linked list hash**. HashMap obtains the hash value through the `hashcode` of the key after being processed by the perturbation function, and then uses `(n - 1) & hash` to determine the location where the current element is stored (n here refers to the length of the array). If there is an element at the current location, it is determined whether the hash value and key of the element and the element to be stored are the same. If they are the same, they will be overwritten directly. If they are not the same, the conflict will be resolved through the zipper method. + +The perturbation function (`hash` method) in `HashMap` is used to optimize the distribution of hash values. By performing additional processing on the original `hashCode()`, the perturbation function can reduce collisions caused by poor `hashCode()` implementation, thereby improving the uniformity of the data distribution. + +**JDK 1.8 HashMap’s hash method source code:** + +The hash method of JDK 1.8 is more simplified than the hash method of JDK 1.7, but the principle remains the same. + +```java + static final int hash(Object key) { + int h; + // key.hashCode(): Returns the hash value, which is hashcode + // ^: bitwise XOR + // >>>: unsigned right shift, ignore the sign bit, fill the empty bits with 0 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +Compare the hash method source code of JDK1.7's HashMap. + +```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); +} +``` + +Compared with the hash method of JDK1.8, the performance of the hash method of JDK 1.7 will be slightly worse, because after all, it is disturbed 4 times. + +The so-called **"zipper method"** is: combining linked lists and arrays. That is to say, create a linked list array, and each cell in the array is a linked list. If a hash conflict is encountered, just add the conflicting value to the linked list. + +![Internal structure before jdk1.8-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) + +#### After 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 多线程操作导致死循环问题 + +The expansion operation of `HashMap` in JDK1.7 and previous versions may have an infinite loop problem in a multi-threaded environment. This is because when there are multiple elements in a bucket that need to be expanded, multiple threads operate on the linked list at the same time, and the head insertion method may cause the nodes in the linked list to point to the wrong location, thus forming a circular linked list, which will cause the operation of querying elements to fall into an infinite loop and cannot be ended. + +In order to solve this problem, the JDK1.8 version of HashMap uses the tail insertion method instead of the head insertion method to avoid the inversion of the linked list, so that the inserted node is always placed at the end of the linked list, avoiding the ring structure in the linked list. However, it is still not recommended to use `HashMap` under multi-threading, because using `HashMap` under multi-threading will still cause data overwriting problems. In a concurrent environment, it is recommended to use `ConcurrentHashMap`. + +Generally, this introduction is enough in an interview. There is no need to remember various details, and I personally don’t think it is necessary to remember them. If you want to learn more about the infinite loop problem caused by `HashMap` expansion, you can read this article by Uncle Mouse: [The infinite loop of Java HashMap](https://coolshell.cn/articles/9606.html). + +### ⭐️Why is HashMap thread-unsafe? + +`HashMap` is not thread-safe. Concurrent write operations to `HashMap` in a multi-threaded environment may cause two main problems: + +1. **Data Loss**: Concurrent `put` operations may cause one thread's writes to be overwritten by another thread. +2. **Infinite Loop**: In JDK 7 and previous versions, during concurrent expansion, the head insertion method may cause the linked list to form a loop, thus triggering an infinite loop during the `get` operation, and the CPU soars to 100%. + +Data loss exists in both JDK1.7 and JDK 1.8. Here we take JDK 1.8 as an example. + +After JDK 1.8, in `HashMap`, multiple key-value pairs may be allocated to the same bucket and stored in the form of a linked list or a red-black tree. The `put` operation of `HashMap` by multiple threads will lead to thread insecurity, specifically there is a risk of data overwriting. + +For example: + +- Two threads 1 and 2 perform put operations at the same time, and a hash conflict occurs (the insertion index calculated by the hash function is the same). +- Different threads may get CPU execution opportunities in different time slices. After the current thread 1 completes the hash conflict judgment, it hangs due to the exhaustion of the time slice. Thread 2 completed the insert operation first. +- Subsequently, Thread 1 obtains the time slice. Since the hash collision has been judged before, it will be inserted directly at this time. This causes the data inserted by Thread 2 to be overwritten by Thread 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) { + // ... + // Determine whether hash collision occurs + // (n - 1) & hash determines which bucket the element is stored in. If the bucket is empty, the newly generated node is placed in the bucket (at this time, the node is placed in the array) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // Elements already exist in the bucket (handling hash conflicts) + else { + // ... +} +``` + +Another situation is that the two threads perform `put` operations at the same time, causing the value of `size` to be incorrect, which in turn leads to data overwriting problems: + +1. When thread 1 executes the `if(++size > threshold)` judgment, assuming that the value of `size` is 10, it hangs due to the exhaustion of the time slice. +2. Thread 2 also performs the `if(++size > threshold)` judgment, obtains the value of `size` which is also 10, inserts the element into the bucket, and updates the value of `size` to 11. +3. Subsequently, thread 1 gets the time slice, it also puts the element into the bucket and updates the value of size to 11. +4. Threads 1 and 2 both performed a `put` operation, but the value of `size` only increased by 1, which resulted in only one element actually being added to the `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 the actual size is greater than the threshold, expand the capacity + if (++size > threshold) + resize(); + // Callback after insertion + afterNodeInsertion(evict); + return null; +} +``` + +### Common traversal methods of HashMap? + +[7 traversal methods and performance analysis of HashMap! ](https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw) + +**🐛 Correction (see: [issue#1411](https://github.com/Snailclimb/JavaGuide/issues/1411))**: + +This article has an incorrect performance analysis of the parallelStream traversal method. Let me start with the conclusion: **parallelStream has the highest performance when blocking exists, and parallelStream has the lowest performance when non-blocking**. + +When there is no blocking in the traversal, the performance of parallelStream is the lowest: + +```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 +``` + +After adding the blocking code `Thread.sleep(10)`, the performance of parallelStream is the highest: + +```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 +``` + +### ⭐️The difference between ConcurrentHashMap and Hashtable + +The difference between `ConcurrentHashMap` and `Hashtable` is mainly reflected in the different ways to achieve thread safety. + +- **Underlying data structure:** The bottom layer of `ConcurrentHashMap` of JDK1.7 is implemented by **segmented array + linked list**. The data structure used in JDK1.8 is the same as the structure of `HashMap`, array + linked list/red-black binary tree. The underlying data structure of `Hashtable` and `HashMap` before JDK1.8 is similar to the form of **array + linked list**. The array is the main body of HashMap, and the linked list mainly exists to solve hash conflicts; +- **Way to implement thread safety (important):** + - In JDK1.7, `ConcurrentHashMap` divided the entire bucket array into segments (`Segment`, segment lock). Each lock only locks a part of the data in the container (schematic diagram below). When multiple threads access data in different data segments in the container, there will be no lock competition and the concurrent access rate will be improved. + - By the time of JDK1.8, `ConcurrentHashMap` has abandoned the concept of `Segment`, but directly uses the data structure of `Node` array + linked list + red-black tree. Concurrency control uses `synchronized` and CAS to operate. (After JDK1.6, the `synchronized` lock has been optimized a lot) The whole thing looks like an optimized and thread-safe `HashMap`. Although the `Segment` data structure can still be seen in JDK1.8, the attributes have been simplified just to be compatible with older versions;- **`Hashtable`(same lock)**: Using `synchronized` to ensure thread safety is very inefficient. When one thread accesses a synchronized method, other threads also access the synchronized method and may enter a blocking or polling state. For example, if put is used to add elements, another thread cannot use put to add elements, nor can it use get. The competition will become more and more fierce and the efficiency will be lower. + +Next, let’s take a look at the comparison chart of the underlying data structures of the two. + +**Hashtable** : + +![Internal structure of Hashtable](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.7_hashmap.png) + +

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

+ +**ConcurrentHashMap** of JDK1.7: + +![Java7 ConcurrentHashMap storage structure](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) + +`ConcurrentHashMap` is composed of `Segment` array structure and `HashEntry` array structure. + +Each element in the `Segment` array contains a `HashEntry` array, and each `HashEntry` array belongs to a linked list structure. + +**ConcurrentHashMap** of JDK1.8: + +![Java8 ConcurrentHashMap storage structure](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) + +JDK1.8's `ConcurrentHashMap` is no longer **Segment array + HashEntry array + linked list**, but **Node array + linked list/red-black tree**. However, Node can only be used in the case of linked lists. In the case of red-black trees, **`TreeNode`** needs to be used. When the conflicting linked list reaches a certain length, the linked list will be converted into a red-black tree. + +`TreeNode` stores red-black tree nodes and is wrapped by `TreeBin`. `TreeBin` maintains the root node of the red-black tree through the `root` attribute, because when the red-black tree is rotating, the root node may be replaced by its original child node. At this point in time, if other threads want to write this red-black tree, thread unsafety problems will occur, so in `ConcurrentHashMap` `TreeBin` maintains the thread currently using this red-black tree through the `waiter` attribute to prevent other threads from entering. + +```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 thread-safe specific implementation/lower-level implementation + +#### Before JDK1.8 + +![Java7 ConcurrentHashMap storage structure](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) + +First, the data is divided into segments (this "segment" is `Segment`) for storage, and then each segment of data is assigned a lock. When a thread occupies the lock to access one segment of data, the data of other segments can also be accessed by other threads. + +**`ConcurrentHashMap` is composed of `Segment` array structure and `HashEntry` array structure**. + +`Segment` inherits `ReentrantLock`, so `Segment` is a reentrant lock and plays the role of a lock. `HashEntry` is used to store key-value pair data. + +```java +static class Segment extends ReentrantLock implements Serializable { +} +``` + +A `ConcurrentHashMap` contains an array of `Segment`, and the number of `Segment` cannot be changed once it is initialized. The size of the `Segment` array is 16 by default, which means that it can support concurrent writing by 16 threads at the same time by default. + +The structure of `Segment` is similar to `HashMap`. It is an array and linked list structure. A `Segment` contains a `HashEntry` array. Each `HashEntry` is an element of a linked list structure. Each `Segment` guards an element in a `HashEntry` array. When modifying the data of the `HashEntry` array, the corresponding one must first be obtained. `Segment` lock. In other words, concurrent writes to the same `Segment` will be blocked, but writes to different `Segment` can be executed concurrently. + +#### After JDK1.8 + +![Java8 ConcurrentHashMap storage structure](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) + +Java 8 has almost completely rewritten `ConcurrentHashMap`, and the code size has changed from more than 1000 lines in Java 7 to more than 6000 lines now. + +`ConcurrentHashMap` cancels the `Segment` segmentation lock and uses `Node + CAS + synchronized` to ensure concurrency safety. The data structure is similar to the structure of `HashMap` 1.8, array + linked list/red-black binary tree. Java 8 converts a linked list (addressing time complexity O(N)) into a red-black tree (addressing time complexity O(log(N))) when the length of the linked list exceeds a certain threshold (8). + +In Java 8, the lock granularity is finer. `synchronized` only locks the first node of the current linked list or red-black binary tree. In this way, as long as the hash does not conflict, concurrency will not occur and the reading and writing of other Nodes will not be affected, and the efficiency is greatly improved. + +### ⭐️What are the differences between the ConcurrentHashMap implementations of JDK 1.7 and JDK 1.8? + +- **Thread safety implementation**: JDK 1.7 uses `Segment` segmentation lock to ensure security. `Segment` is inherited from `ReentrantLock`. JDK1.8 abandoned the `Segment` segmented lock design and adopted `Node + CAS + synchronized` to ensure thread safety and finer lock granularity. `synchronized` only locks the first node of the current linked list or red-black binary tree. +- **Hash collision solution**: JDK 1.7 uses the zipper method, and JDK1.8 uses the zipper method combined with red-black trees (when the length of the linked list exceeds a certain threshold, the linked list is converted into a red-black tree). +- **Concurrency**: The maximum concurrency of JDK 1.7 is the number of Segments, and the default is 16. The maximum concurrency in JDK 1.8 is the size of the Node array, and the concurrency is greater. + +### ConcurrentHashMap Why key and value cannot be null? + +The key and value of `ConcurrentHashMap` cannot be null mainly to avoid ambiguity. null is a special value that means there is no object or no reference. If you use null as a key, then you can't tell whether the key exists in the `ConcurrentHashMap` or whether there is no key at all. Likewise, if you use null as a value, then you can't tell whether the value is actually stored in the `ConcurrentHashMap`, or whether it is returned because the corresponding key cannot be found. + +Taking the value of the get method as an example, there are two situations when the returned result is null: + +- The value is not in the collection; +- The value itself is null. + +This is where the ambiguity comes from. + +For details, please refer to [ConcurrentHashMap source code analysis](https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html). + +In a multi-threaded environment, when one thread operates the `ConcurrentHashMap`, other threads modify the `ConcurrentHashMap`, so it is impossible to use `containsKey(key)` to determine whether the key-value pair exists, and there is no way to solve the ambiguity problem.In contrast, `HashMap` can store null keys and values, but there can only be one null as a key and multiple null as values. If null is passed as a parameter, the value at the position where the hash value is 0 will be returned. In a single-threaded environment, there is no situation where one thread operates the HashMap and other threads modify the `HashMap`. Therefore, `contains(key)` can be used to determine whether the key-value pair exists, so as to perform corresponding processing, and there is no ambiguity problem. + +In other words, it is impossible to correctly determine whether the key-value pair exists in multi-threads (there are modifications by other threads), but it is possible in a single thread (there are no modifications by other threads). + +If you really need to use null in a ConcurrentHashMap, you can use a special static empty object instead. + +```java +public static final Object NULL = new Object(); +``` + +Finally, let me share the answer of the author of `ConcurrentHashMap` (Doug Lea) to this question: + +> 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. + +After translation, the general meaning is that ambiguity can be tolerated in a single thread, but cannot be tolerated in a multi-threaded environment. + +### ⭐️Can ConcurrentHashMap guarantee the atomicity of composite operations? + +`ConcurrentHashMap` is thread-safe, which means that it can ensure that when multiple threads read and write it at the same time, there will be no data inconsistency, and it will not cause the infinite loop problem caused by the multi-threaded operation of `HashMap` in JDK1.7 and previous versions. However, this does not mean that it can guarantee that all compound operations are atomic, so don't get confused! + +Compound operations refer to operations composed of multiple basic operations (such as `put`, `get`, `remove`, `containsKey`, etc.). For example, first determine whether a key exists `containsKey(key)`, and then insert or update `put(key, value)` based on the result. This operation may be interrupted by other threads during execution, resulting in unexpected results. + +For example, there are two threads A and B performing composite operations on `ConcurrentHashMap` at the same time, as follows: + +```java +// Thread A +if (!map.containsKey(key)) { +map.put(key, value); +} +// Thread B +if (!map.containsKey(key)) { +map.put(key, anotherValue); +} +``` + +If the execution order of threads A and B is like this: + +1. Thread A determines that key does not exist in the map +2. Thread B determines that the key does not exist in the map +3. Thread B inserts (key, anotherValue) into map +4. Thread A inserts (key, value) into map + +Then the final result is (key, value) instead of the expected (key, anotherValue). This is the problem caused by the non-atomicity of composite operations. + +**So how to ensure the atomicity of `ConcurrentHashMap` compound operations? ** + +`ConcurrentHashMap` provides some atomic compound operations, such as `putIfAbsent`, `compute`, `computeIfAbsent`, `computeIfPresent`, `merge`, etc. These methods can accept a function as a parameter, calculate a new value based on the given key and value, and update it to the map. + +The above code can be rewritten as: + +```java +// Thread A +map.putIfAbsent(key, value); +// Thread B +map.putIfAbsent(key, anotherValue); +``` + +Or: + +```java +// Thread A +map.computeIfAbsent(key, k -> value); +// Thread B +map.computeIfAbsent(key, k -> anotherValue); +``` + +Many students may say that synchronization can also be locked in this situation! It is indeed possible, but it is not recommended to use the locking synchronization mechanism, which violates the original intention of using `ConcurrentHashMap`. When using `ConcurrentHashMap`, try to use these atomic compound operation methods to ensure atomicity. + +## Collections tool class (not important) + +**`Collections` common methods of tool classes**: + +- Sort +- Find and replace operations +- Synchronization control (not recommended, please consider using the concurrent collection under the JUC package when you need a thread-safe collection type) + +### Sorting operation + +```java +void reverse(List list)//reverse +void shuffle(List list)//random sorting +void sort(List list)//Sort in ascending order of natural sorting +void sort(List list, Comparator c)//Customized sorting, the sorting logic is controlled by Comparator +void swap(List list, int i, int j)//Exchange elements at two index positions +void rotate(List list, int distance)//Rotation. When distance is a positive number, move the entire distance elements in the list to the front. When distance is a negative number, move the first distance elements of the list to the back as a whole +``` + +### Find and replace operations + +```java +int binarySearch(List list, Object key)//Perform a binary search on the List and return the index. Note that the List must be ordered. +int max(Collection coll)//Return the largest element according to the natural order of the elements. Analogy int min(Collection coll) +int max(Collection coll, Comparator c)//Return the largest element according to customized sorting. The sorting rules are controlled by the Comparatator class. Analogy int min(Collection coll, Comparator c) +void fill(List list, Object obj)//Replace all elements in the specified list with the specified element +int frequency(Collection c, Object o)//Count the number of occurrences of elements +int indexOfSubList(List list, List target)//Stats the index of the first occurrence of target in the list. If it cannot be found, it returns -1, analogous to int lastIndexOfSubList(List source, list target) +boolean replaceAll(List list, Object oldVal, Object newVal)//Replace old elements with new elements +``` + +### Synchronization control + +`Collections` provides multiple `synchronizedXxx()` methods, which can wrap the specified collection into a thread-synchronized collection, thereby solving the thread safety problem when multiple threads access the collection concurrently. + +We know that `HashSet`, `TreeSet`, `ArrayList`, `LinkedList`, `HashMap`, `TreeMap` are all thread-unsafe. `Collections` provides multiple static methods to wrap them into thread-synchronized collections. + +**It is best not to use the following methods, which are very inefficient. When you need a thread-safe collection type, please consider using the concurrent collection under the JUC package. ** + +Here's how: + +```java +synchronizedCollection(Collection c) //Returns the synchronized (thread-safe) collection supported by the specified collection. +synchronizedList(List list)//Returns a synchronized (thread-safe) List supported by the specified list. +synchronizedMap(Map m) //Returns a synchronized (thread-safe) Map supported by the specified mapping. +synchronizedSet(Set s) //Returns the synchronized (thread-safe) set supported by the specified set. +``` + + \ No newline at end of file diff --git a/docs_en/java/collection/linkedhashmap-source-code.en.md b/docs_en/java/collection/linkedhashmap-source-code.en.md new file mode 100644 index 00000000000..414692dd76a --- /dev/null +++ b/docs_en/java/collection/linkedhashmap-source-code.en.md @@ -0,0 +1,592 @@ +--- +title: LinkedHashMap source code analysis +category: Java +tag: + - Java collections +head: + - - meta + - name: keywords + content: LinkedHashMap, insertion order, access order, doubly linked list, LRU, iterative order, HashMap expansion, traversal efficiency + - - meta + - name: description + content: Analysis of LinkedHashMap, which maintains a doubly linked list based on HashMap to achieve ordered insertion/access, and its application in LRU cache and other scenarios. +--- + +## Introduction to LinkedHashMap + +`LinkedHashMap` is a collection class provided by Java. It inherits from `HashMap` and maintains a doubly linked list based on `HashMap`, so that it has the following characteristics: + +1. When traversing is supported, iteration will be carried out in order according to the insertion order. +2. Supports sorting according to element access order, suitable for encapsulating LRU cache tools. +3. Because a doubly linked list is used internally to maintain each node, the efficiency of traversal is proportional to the number of elements. Compared with HashMap, which is proportional to capacity, the iteration efficiency will be much higher. + +The logical structure of `LinkedHashMap` is shown in the figure below. It maintains a two-way linked list between each node based on `HashMap`, so that the nodes, linked lists, and red-black trees originally hashed on different buckets are associated in an orderly manner. + +![LinkedHashMap logical structure](https://oss.javaguide.cn/github/javaguide/java/collection/linkhashmap-structure-overview.png) + +## LinkedHashMap usage example + +### Insertion order traversal + +As shown below, we add elements to `LinkedHashMap` in order and then traverse. + +```java +HashMap < String, String > map = new LinkedHashMap < > (); +map.put("a", "2"); +map.put("g", "3"); +map.put("r", "1"); +map.put("e", "23"); + +for (Map.Entry < String, String > entry: map.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); +} +``` + +Output: + +```java +a:2 +g:3 +r:1 +e:23 +``` + +It can be seen that the iteration order of `LinkedHashMap` is consistent with the insertion order, which is something that `HashMap` does not have. + +### Access sequence traversal + +`LinkedHashMap` defines the sorting mode `accessOrder` (boolean type, default is false), the access order is true, and the insertion order is false. + +In order to implement access order traversal, we can use the `LinkedHashMap` constructor passing in the `accessOrder` attribute and set `accessOrder` to true, indicating that it has access ordering. + +```java +LinkedHashMap map = new LinkedHashMap<>(16, 0.75f, true); +map.put(1, "one"); +map.put(2, "two"); +map.put(3, "three"); +map.put(4, "four"); +map.put(5, "five"); +//Access element 2, the element will be moved to the end of the linked list +map.get(2); +//Access element 3, the element will be moved to the end of the linked list +map.get(3); +for (Map.Entry entry : map.entrySet()) { + System.out.println(entry.getKey() + " : " + entry.getValue()); +} +``` + +Output: + +```java +1 : one +4 : four +5 : five +2 : two +3: three +``` + +It can be seen that the iteration order of `LinkedHashMap` is consistent with the access order. + +### LRU cache + +From the previous one, we can learn that through `LinkedHashMap` we can encapsulate a simple version of LRU (**L**east **R**ecently **U**sed, least recently used) cache to ensure 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 ideas are as follows: + +- Inherits `LinkedHashMap`; +- Specify `accessOrder` as true in the constructor, so that when accessing an element, the element will be moved to the end of the linked list, and the first element of the linked list will be the least recently accessed element; +- Override the `removeEldestEntry` method, which will return a boolean value to tell `LinkedHashMap` whether the first element of the linked list needs to be removed (cache capacity is limited). + +```java +public class LRUCache extends LinkedHashMap { + private final int capacity; + + public LRUCache(int capacity) { + super(capacity, 0.75f, true); + this.capacity = capacity; + } + + /** + * Return true when size exceeds capacity, telling LinkedHashMap to remove the oldest cache item (i.e. the first element of the linked list) + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } +} +``` + +The test code is as follows. The author initialized the cache capacity to 3, and then added 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)); +} +``` + +Output: + +```java +null +null +three +four +five +``` + +Judging from the output, since the cache capacity is 3, when the 4th element is added, the 1st element will be deleted. When the 5th element is added, the 2nd element is removed. + +## LinkedHashMap source code analysis + +### Node design + +Before formally discussing `LinkedHashMap`, let us first talk about the design of the `LinkedHashMap` node `Entry`. We all know that the nodes on the `HashMap` bucket that are converted to linked lists due to conflicts will convert the linked lists into red-black trees when the following two conditions are met: + +1. ~~The number of nodes on the linked list reaches the tree threshold 7, that is, `TREEIFY_THRESHOLD - 1`. ~~ +2. The capacity of the bucket reaches the minimum tree capacity, which is `MIN_TREEIFY_CAPACITY`. + +> **🐛 Bugfix (see: [issue#2147](https://github.com/Snailclimb/JavaGuide/issues/2147))**: +> +> The threshold for the number of nodes on the linked list to become a tree is 8 instead of 7. Because the source code is traversed from the initial element of the linked list, and the subscript starts from 0, the judgment condition is set to 8-1=7. In fact, when iterating to the tail element, it is judged that the length of the entire linked list is greater than or equal to 8 before performing the tree operation. +> +> ![](https://oss.javaguide.cn/github/javaguide/java/jvm/LinkedHashMap-putval-TREEIFY.png) + +`LinkedHashMap` builds a two-way linked list for each node on the bucket based on `HashMap`, which makes the tree node converted into a red-black tree also need to have the characteristics of a two-way linked list node, that is, each tree node needs to have two addresses that reference the storage of the predecessor node and the successor node, so the design of the tree node class `TreeNode` is a relatively thorny issue. + +For this we might as well take a look at the class diagram of the node class between the two, we can see: + +1. The node internal class `Entry` of `LinkedHashMap` is based on `HashMap`, and adds `before` and `after` pointers to make the node have the characteristics of a doubly linked list. +2. The tree node `TreeNode` of `HashMap` inherits the `Entry` of `LinkedHashMap` which has the characteristics of a doubly linked list.![Relationship between LinkedHashMap and HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/map-hashmap-linkedhashmap.png) + +Many readers will have this question at this time, why does the tree node `TreeNode` of `HashMap` obtain the characteristics of a doubly linked list through `LinkedHashMap`? Why not directly implement the predecessor and successor pointers on `Node`? + +Let’s answer the first question first. We all know that `LinkedHashMap` adds bidirectional pointers to nodes on the basis of `HashMap` to achieve the characteristics of a doubly linked list. Therefore, when the internal linked list of `LinkedHashMap` is converted into a red-black tree, the corresponding node will be converted into a tree node `TreeNode`. In order to ensure that the tree node has the characteristics of a doubly linked list when using `LinkedHashMap`, so the tree node `TreeNode` Need to inherit `Entry` of `LinkedHashMap`. + +Let’s talk about the second question. We directly implement the predecessor and successor pointers on the node `Node` of `HashMap`, and then `TreeNode` directly inherits `Node` to obtain the characteristics of a doubly linked list. Why not? In fact, it is also possible to do this. However, this approach will add two unnecessary references to the node class `Node` that stores key-value pairs when using `HashMap`, occupying unnecessary memory space. + +Therefore, in order to ensure that the underlying node class `Node` of `HashMap` has no redundant references, and to ensure that the node class `Entry` of `LinkedHashMap` has a reference to the storage linked list, the designer allows the node `Entry` of `LinkedHashMap` to inherit Node and add references `before` and `after` to store the predecessor and successor nodes, so that the nodes that need to use the linked list feature can implement the required logic. Then the tree node `TreeNode` obtains the two pointers `before` and `after` by inheriting `Entry`. + +```java +static class Entry extends HashMap.Node { + Entry before, after; + Entry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } + } +``` + +But doesn't this also add two unnecessary references to `TreeNode` when using `HashMap`? Isn't this also a waste of space? + +```java +static final class TreeNode extends LinkedHashMap.Entry { + //omitted + +} +``` + +Regarding this issue, quoting a comment from the author, the authors believe that with a good `hashCode` algorithm, the probability of `HashMap` converting to a red-black tree is low. Even if the red-black tree is converted into a tree node, `TreeNode` may be changed into `Node` due to removal or expansion, so the probability of using `TreeNode` is not very high, and the waste of resource space is acceptable. + +```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 +``` + +###Construction method + +The `LinkedHashMap` constructor has 4 implementations and is relatively simple. Directly call the constructor of the parent class, `HashMap`, to complete the initialization. + +```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; +} +``` + +We also mentioned above that `accessOrder` is false by default. If we want `LinkedHashMap` to sort the key-value pairs in order of access (i.e., arrange the recently unvisited elements at the beginning of the linked list and move the recently accessed elements to the end of the linked list), we need to call the fourth constructor to set `accessOrder` to true. + +### get method + +The `get` method is the only overridden method in the `LinkedHashMap` addition, deletion, modification and query operations. When `accessOrder` is true, it will move the currently accessed element to the end of the linked list after the element query is completed. + +```java +public V get(Object key) { + Node < K, V > e; + //Get the key-value pair of key, if it is empty, return directly + if ((e = getNode(hash(key), key)) == null) + return null; + //If accessOrder is true, call afterNodeAccess to move the current element to the end of the linked list + if(accessOrder) + afterNodeAccess(e); + //Return the value of the key-value pair + return e.value; + } +``` + +As can be seen from the source code, the execution steps of `get` are very simple: + +1. Call `getNode` of the parent class, `HashMap`, to obtain the key-value pair. If it is empty, it will be returned directly. +2. Determine whether `accessOrder` is true. If it is true, it means that the linked list access order of `LinkedHashMap` needs to be ensured. Go to step 3. +3. Call `afterNodeAccess` rewritten by `LinkedHashMap` to add the current element to the end of the linked list. + +The key point is the implementation of the `afterNodeAccess` method, which is responsible for moving the element to the end of the linked list. + +```java +void afterNodeAccess(Node < K, V > e) { // move node to last + LinkedHashMap.Entry < K, V > last; + //If accessOrder and the current node is not the tail node of the linked list + if (accessOrder && (last = tail) != e) { + + //Get the current node, as well as the predecessor node and successor node + LinkedHashMap.Entry < K, V > p = + (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after; + + //Point the successor node pointer of the current node to null to disconnect it from the successor node + p.after = null; + + //If the predecessor node is empty, it means that the current node is the first node of the linked list, so the successor node is set as the first node + if(b==null) + head = a; + else + //If the predecessor node is not empty, let the predecessor node point to the successor node + b.after = a; + + //If the successor node is not empty, let the successor node point to the predecessor node + if (a != null) + a.before = b; + else + //If the successor node is empty, it means that the current node is at the end of the linked list, and let last point directly to the predecessor node. This else is actually meaningless, because the if at the beginning has ensured that p is not the tail node, so naturally after will not be null. + last = b; + + //If last is empty, it means that the current linked list has only one node p, then point head to p + if (last == null) + head = p; + else { + //On the contrary, let the predecessor pointer of p point to the tail node, and then let the predecessor pointer of the tail node point to p. + p.before = last; + last.after = p; + } + //tail points to p, and then moves node p to the end of the linked list. + tail = p; + + ++modCount; + } +}``` + +As can be seen from the source code, the `afterNodeAccess` method completes the following operations: + +1. If `accessOrder` is true and the end of the linked list is not the current node p, we need to move the current node to the end of the linked list. +2. Get the current node p, its predecessor node b and successor node a. +3. Set the successor pointer of the current node p to null to disconnect it from the successor node p. +4. Try to point the predecessor node to the successor node. If the predecessor node is empty, it means that the current node p is the first node of the linked list, so directly set the successor node a as the first node, and then append p to the end of a. +5. Try again to make the successor node a point to the predecessor node b. +6. The above operation allows the predecessor node and successor node to complete the association and separate the current node p. This step is to append the current node p to the end of the linked list. If the end of the linked list is empty, it means that the current linked list has only one node p, so just let the head point to p. +7. The above operation has successfully reached p to the end of the linked list. Finally, we point the tail pointer, which is the pointer pointing to the end of the linked list, to p. + +It can be understood in conjunction with this picture, which shows that the element with key 13 has been moved to the end of the linked list. + +![LinkedHashMap moves element 13 to the end of the linked list](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-get.png) + +It doesn’t matter if you don’t quite understand it. It’s enough to know the effect of this method. You can digest it slowly when you have time later. + +### remove method post-operation——afterNodeRemoval + +`LinkedHashMap` does not override the `remove` method, but directly inherits the `remove` method of `HashMap`. In order to ensure that the nodes in the doubly linked list will be removed simultaneously after the key-value pair is removed, `LinkedHashMap` rewrites the empty implementation method `afterNodeRemoval` of `HashMap`. + +```java +final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + //omitted + 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's removeNode will call afterNodeRemoval to perform the post-removal operation after completing the element removal. + afterNodeRemoval(node); + return node; + } + } + return null; + } +//Empty implementation +void afterNodeRemoval(Node p) { } +``` + +We can see that the `removeNode` method called inside the `remove` method inherited from `HashMap` deletes the node from the bucket and then calls `afterNodeRemoval`. + +```java +void afterNodeRemoval(Node e) { // unlink + + //Get the current node p, and the predecessor node b and successor node a of e + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + //Set the predecessor and successor pointers of p to null to disconnect them from the predecessor and successor nodes. + p.before = p.after = null; + + //If the predecessor node is empty, it means that the current node p is the first node of the linked list, just let the head pointer point to the successor node a. + if(b==null) + head = a; + else + //If the predecessor node b is not empty, let b point directly to the successor node a + b.after = a; + + //If the successor node is empty, it means that the current node p is at the end of the linked list, so just let the tail pointer point to the predecessor node a. + if(a==null) + tail = b; + else + //Instead, the predecessor pointer of the successor node points directly to the predecessor node + a.before = b; + } +``` + +As can be seen from the source code, the overall operation of the `afterNodeRemoval` method is to disconnect the current node p from the predecessor node and successor node, and wait for gc recycling. The overall steps are: + +1. Obtain the current node p, as well as p’s predecessor node b and successor node a. +2. Disconnect the current node p from its predecessor and successor nodes. +3. Try to make the predecessor node b point to the successor node a. If b is empty, it means that the current node p is at the head of the linked list. We can directly point the head to the successor node a. +4. Try to make the successor node a point to the predecessor node b. If a is empty, it means that the current node p is at the end of the linked list, so just let the tail pointer point to the predecessor node b. + +It can be understood in conjunction with this picture, which shows that the element with key 13 is deleted, that is, the element is removed from the linked list. + +![LinkedHashMap delete element 13](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-remove.png) + +It doesn’t matter if you don’t quite understand it. It’s enough to know the effect of this method. You can digest it slowly when you have time later. + +### put method post operation——afterNodeInsertion + +Similarly, `LinkedHashMap` does not implement the insertion method, but directly inherits all the insertion methods of `HashMap` for users to use. However, in order to maintain the orderliness of doubly linked list access, it does the following two things: + +1. Rewrite `afterNodeAccess` (mentioned above), if the currently inserted key already exists in `map`, because the insertion operation of `LinkedHashMap` will append the new node to the end of the linked list, so for the existing key, call `afterNodeAccess` to put it at the end of the linked list. +2. Overridden the `afterNodeInsertion` method of `HashMap`. When `removeEldestEntry` returns true, the first node of the linked list will be removed. + +We can see this in the core method `putVal` of the insertion operation of `HashMap`. + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + //omitted + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + //If the current key exists in the map, call afterNodeAccess + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + if (++size > threshold) + resize(); + //Call the insert post method, which is overridden by LinkedHashMap + afterNodeInsertion(evict); + return null; + } +``` + +The source code of the above steps has been explained above, so here we focus on the workflow of `afterNodeInsertion`. Assume that we have rewritten `removeEldestEntry` and return true when the linked list `size` exceeds `capacity`. + +```java +/** + * Return true when size exceeds capacity, telling LinkedHashMap to remove the oldest cache item (i.e. the first element of the linked list) + */ +protected boolean removeEldestEntry(Map.Entry < K, V > eldest) { + return size() > capacity; +}``` + +Take the following figure as an example. Suppose that the author finally inserted a new node 19 that does not exist. Assume that `capacity` is 4, so `removeEldestEntry` returns true, and we want to remove the first node of the linked list. + +![Insert new elements into LinkedHashMap 19](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-after-insert-1.png) + +The removal step is very simple. Check whether the first node of the linked list exists. If it exists, disconnect the relationship between the first node and the subsequent node, and let the first node pointer point to the next node, so the head pointer points to 12, and node 10 becomes an empty object without any reference, waiting for GC. + +![Insert new elements into 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; + //If evict is true and the head element of the queue is not empty and removeEldestEntry returns true, it means that we need to remove the oldest element (that is, the element at the head of the linked list). + if (evict && (first = head) != null && removeEldestEntry(first)) { + //Get the key of the key-value pair at the head of the linked list + K key = first.key; + //Call removeNode to remove the element from the bucket of HashMap, disconnect it from the doubly linked list of LinkedHashMap, and wait for gc recycling + removeNode(hash(key), key, null, false, true); + } + } +``` + +As can be seen from the source code, the `afterNodeInsertion` method completes the following operations: + +1. Determine whether `eldest` is true. Only if it is true, it means that the oldest key-value pair (i.e., the element at the head of the linked list) may need to be removed. Whether it needs to be removed specifically, you must also determine whether the linked list is empty `((first = head) != null)`, and whether the `removeEldestEntry` method returns true. Only when these two methods return true can it be determined that the current linked list is not empty, and the linked list needs to be removed. +2. Get the key of the first element of the linked list. +3. Call the `removeNode` method of `HashMap`, which we mentioned above, it will remove the node from the bucket of `HashMap`, and `LinkedHashMap` also overrides the `afterNodeRemoval` method in `removeNode`, so this step will remove the element from the bucket of `HashMap` by calling `removeNode` Removed from `LinkedHashMap` and disconnected from the doubly linked list of `LinkedHashMap`, waiting for gc recycling. + +## LinkedHashMap and HashMap traversal performance comparison + +`LinkedHashMap` maintains a doubly linked list to record the order of data insertion, so when iterating through the generated iterator, it is traversed according to the path of the doubly linked list. This is much more efficient than `HashMap` which traverses the entire bucket. + +We can verify this from the iterators of the two. Let's first look at the iterator of `HashMap`. You can see that `HashMap` uses a `nextNode` method when iterating key-value pairs. This method will return the next element pointed to by next, and will traverse the bucket starting from next to find the element Node in the next bucket that is not empty. + +```java + final class EntryIterator extends HashIterator + implements Iterator < Map.Entry < K, V >> { + public final Map.Entry < K, + V > next() { + return nextNode(); + } + } + + //Get the next Node + final Node < K, V > nextNode() { + Node < K, V > [] t; + //Get the next element next + Node < K, V > e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if(e==null) + throw new NoSuchElementException(); + //Point next to the next Node in the bucket that is not empty + if ((next = (current = e).next) == null && (t = table) != null) { + do {} while (index < t.length && (next = t[index++]) == null); + } + return e; + } +``` + +In contrast, the iterator of `LinkedHashMap` directly uses the `after` pointer to quickly locate the successor node of the current node, which is much simpler and more efficient. + +```java + final class LinkedEntryIterator extends LinkedHashIterator + implements Iterator < Map.Entry < K, V >> { + public final Map.Entry < K, + V > next() { + return nextNode(); + } + } + //Get the next Node + final LinkedHashMap.Entry < K, V > nextNode() { + //Get the next node next + LinkedHashMap.Entry < K, V > e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if(e==null) + throw new NoSuchElementException(); + //current pointer points to the current node + current = e; + //next directly positions the after pointer of the current node to the next node quickly + next = e.after; + return e; + } +``` + +In order to verify the author's point of view, the author conducted a stress test on these two containers to test the time taken to insert 10 million pieces of data and iterate 10 million pieces of data. The code is as follows: + +```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);``` + +Judging from the output results, because `LinkedHashMap` needs to maintain a doubly linked list, inserting elements will be more time-consuming than `HashMap`, but with the clear relationship between the front and rear nodes of the doubly linked list, the iteration efficiency is much more efficient than the former. However, overall it is not big, after all, the amount of data is so huge. + +```bash +map time putVal: 5880 +linkedHashMap putVal time: 7567 +map get time: 143 +linkedHashMap get time: 67 +63208969074998 +``` + +## LinkedHashMap common interview questions + +### What is LinkedHashMap? + +`LinkedHashMap` is a subclass of `HashMap` in the Java collection framework. It inherits all properties and methods of `HashMap`, and overrides the `afterNodeRemoval`, `afterNodeInsertion`, and `afterNodeAccess` methods based on `HashMap`. Make it have the characteristics of sequential insertion and ordered access. + +### How does LinkedHashMap iterate elements in insertion order? + +It is the default behavior of `LinkedHashMap` to iterate elements in insertion order. `LinkedHashMap` internally maintains a doubly linked list to record the insertion order of elements. Therefore, when iterating over elements using an iterator, the elements are in the same order as they were originally inserted. + +### How does LinkedHashMap iterate elements in order of access? + +`LinkedHashMap` can iterate elements in access order specified by the `accessOrder` parameter in the constructor. When `accessOrder` is true, each time an element is accessed, the element will be moved to the end of the linked list, so the next time the element is accessed, it will become the last element in the linked list, thereby iterating the elements in the order of access. + +### How does LinkedHashMap implement LRU caching? + +Set `accessOrder` to true and override the `removeEldestEntry` method to return true when the linked list size exceeds capacity, so that each time an element is accessed, it will be moved to the end of the linked list. Once the insertion operation causes `removeEldestEntry` to return true, the cache is deemed to be full, and `LinkedHashMap` will remove the first element of the linked list, so that we can implement an LRU cache. + +### What is the difference between LinkedHashMap and HashMap? + +`LinkedHashMap` and `HashMap` are both implementation classes of the Map interface in the Java collection framework. The biggest difference between them is the order in which the elements are iterated. The order in which `HashMap` iterates elements is undefined, while `LinkedHashMap` provides the functionality to iterate elements in insertion order or access order. In addition, `LinkedHashMap` internally maintains a doubly linked list to record the insertion order or access order of elements, while `HashMap` does not have this linked list. Therefore, the insertion performance of `LinkedHashMap` may be slightly lower than that of `HashMap`, but it provides more functions and the iteration efficiency is more efficient than `HashMap`. + +## References + +- Detailed analysis of LinkedHashMap source code (JDK1.8): +- HashMap and LinkedHashMap: +- Derived from LinkedHashMap source code: + \ No newline at end of file diff --git a/docs_en/java/collection/linkedlist-source-code.en.md b/docs_en/java/collection/linkedlist-source-code.en.md new file mode 100644 index 00000000000..184225182bc --- /dev/null +++ b/docs_en/java/collection/linkedlist-source-code.en.md @@ -0,0 +1,523 @@ +--- +title: LinkedList 源码分析 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: LinkedList,双向链表,Deque,插入删除复杂度,随机访问,头尾操作,List 接口,链表结构 + - - meta + - name: description + content: 详解 LinkedList 的数据结构与接口实现,分析头尾插入删除的时间复杂度、与 ArrayList 的差异以及不支持随机访问的原因。 +--- + + + +## LinkedList 简介 + +`LinkedList` 是一个基于双向链表实现的集合类,经常被拿来和 `ArrayList` 做比较。关于 `LinkedList` 和`ArrayList`的详细对比,我们 [Java 集合常见面试题总结(上)](./java-collection-questions-01.md)有详细介绍到。 + +![双向链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) + +不过,我们在项目中一般是不会使用到 `LinkedList` 的,需要用到 `LinkedList` 的场景几乎都可以使用 `ArrayList` 来代替,并且,性能通常会更好!就连 `LinkedList` 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 `LinkedList` 。 + +![](https://oss.javaguide.cn/github/javaguide/redisimage-20220412110853807.png) + +另外,不要下意识地认为 `LinkedList` 作为链表就最适合元素增删的场景。我在上面也说了,`LinkedList` 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。 + +### LinkedList 插入和删除元素的时间复杂度? + +- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。 + +### LinkedList 为什么不能实现 RandomAccess 接口? + +`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。 + +## LinkedList 源码分析 + +这里以 JDK1.8 为例,分析一下 `LinkedList` 的底层核心源码。 + +`LinkedList` 的类定义如下: + +```java +public class LinkedList + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable +{ + //... +} +``` + +`LinkedList` 继承了 `AbstractSequentialList` ,而 `AbstractSequentialList` 又继承于 `AbstractList` 。 + +阅读过 `ArrayList` 的源码我们就知道,`ArrayList` 同样继承了 `AbstractList` , 所以 `LinkedList` 会有大部分方法和 `ArrayList` 相似。 + +`LinkedList` 实现了以下接口: + +- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 +- `Deque` :继承自 `Queue` 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。需要注意,`Deque` 的发音为 "deck" [dɛk],这个大部分人都会读错。 +- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 +- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 + +![LinkedList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist--class-diagram.png) + +`LinkedList` 中的元素是通过 `Node` 定义的: + +```java +private static class Node { + E item;// 节点值 + Node next; // 指向的下一个节点(后继节点) + Node prev; // 指向的前一个节点(前驱结点) + + // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点 + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +### 初始化 + +`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++; +} +``` + +### Get elements + +There are 3 methods related to obtaining elements in `LinkedList`: + +1. `getFirst()`: Get the first element of the linked list. +2. `getLast()`: Get the last element of the linked list. +3. `get(int index)`: Get the element at the specified position in the linked list. + +```java +// Get the first element of the linked list +public E getFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return f.item; +} + +// Get the last element of the linked list +public E getLast() { + final Node l = last; + if(l==null) + throw new NoSuchElementException(); + return l.item; +} + +// Get the element at the specified position in the linked list +public E get(int index) { + //Check if the subscript is out of bounds, throw an exception if it is out of bounds + checkElementIndex(index); + // Return the element corresponding to the subscript in the linked list + return node(index).item; +} +``` + +The core here lies in the `node(int index)` method: + +```java +// Return the non-empty node with the specified index +Node node(int index) { + // Assert that the subscript is not out of bounds + // assert isElementIndex(index); + // If index is less than half of size, start searching from the front (search backward), otherwise search forward + if (index < (size >> 1)) { + Node x = first; + // Traverse, loop and search backward until 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; + } +} +``` + +This method is called internally by methods such as `get(int index)` or `remove(int index)` to obtain the corresponding node. + +As can be seen from the source code of this method, this method determines whether to start traversing from the head or the end of the linked list by comparing the index value with half the size of the linked list. If the index value is less than half of size, the traversal starts from the head of the linked list, otherwise it starts from the end of the linked list. In this way, the target node can be found in a shorter time, making full use of the characteristics of the doubly linked list to improve efficiency. + +### Delete element + +`LinkedList` has a total of 5 methods related to deleting elements: + +1. `removeFirst()`: Remove and return the first element of the linked list. +2. `removeLast()`: Remove and return the last element of the linked list. +3. `remove(E e)`: Delete the specified element that appears for the first time in the linked list. If the element does not exist, it returns false. +4. `remove(int index)`: Delete the element at the specified index and return the value of the element. +5. `void clear()`: Remove all elements in this linked list. + +```java +//Delete and return the first element of the linked list +public E removeFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} + +//Delete and return the last element of the linked list +public E removeLast() { + final Node l = last; + if(l==null) + throw new NoSuchElementException(); + return unlinkLast(l); +} + +//Delete the specified element that appears for the first time in the linked list, return false if the element does not exist +public boolean remove(Object o) { + // If the specified element is null, traverse the linked list to find the first null element and delete it. + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + // If not null, traverse the linked list to find the node to be deleted + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} + +//Delete the element at the specified position in the linked list +public E remove(int index) { + //Check if the subscript is out of bounds, throw an exception if it is out of bounds + checkElementIndex(index); + return unlink(node(index)); +} +``` + +The core here lies in the `unlink(Node x)` method: + +```java +E unlink(Node x) { + // Assert that x is not null + // assert x != null; + // Get the elements of the current node (that is, the node to be deleted) + final E element = x.item; + // Get the next node of the current node + final Node next = x.next; + // Get the previous node of the current node + final Node prev = x.prev; + + // If the previous node is empty, the current node is the head node + if (prev == null) { + // Directly let the head of the linked list point to the next node of the current node + first = next; + } else { // If the previous node is not empty + // Point the next pointer of the previous node to the next node of the current node + prev.next = next; + // Set the prev pointer of the current node to null to facilitate GC recycling + x.prev = null; + } + + // If the next node is empty, it means the current node is the tail node + if (next == null) { + // Directly let the tail of the linked list point to the previous node of the current node + last = prev; + } else { // If the next node is not empty + // Point the prev pointer of the next node to the node before the current node + next.prev = prev; + // Set the next pointer of the current node to null to facilitate GC recycling + x.next = null; + } + + // Set the current node element to null to facilitate GC recycling + x.item = null; + size--; + modCount++; + return element; +} +``` + +The logic of the `unlink()` method is as follows: + +1. First obtain the predecessor and successor nodes of the node x to be deleted; +2. Determine whether the node to be deleted is the head node or the tail node: + - If x is the head node, point first to x's successor node next + - If x is the tail node, point last to the predecessor node of x, prev + - If x is neither the head node nor the tail node, perform the next step +3. Point the successor of the predecessor of the node x to be deleted to the successor next of the node to be deleted, and disconnect the link between x and x.prev; +4. Point the successor prev of the node x to be deleted to the predecessor prev of the node to be deleted, and disconnect the link between x and x.next; +5. Leave the element of node x to be deleted empty and modify the length of the linked list. + +You can refer to the picture below to understand (picture source: [LinkedList Source Code Analysis (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 method logic](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist-unlink.jpg)### Traverse the linked list + +It is recommended to use the `for-each` loop to traverse the elements in `LinkedList`. The `for-each` loop will eventually be converted into an iterator form. + +```java +LinkedList list = new LinkedList<>(); +list.add("apple"); +list.add("banana"); +list.add("pear"); + +for (String fruit : list) { + System.out.println(fruit); +} +``` + +The core of `LinkedList` traversal is the implementation of its iterator. + +```java +// bidirectional iterator +private class ListItr implements ListIterator { + //Represents the node passed by the last time the next() or previous() method was called; + private Node lastReturned; + //Indicates the next node to be traversed; + private Node next; + // Represents the subscript of the next node to be traversed, which is the subscript of the successor node of the current node; + private int nextIndex; + // Indicates the current traversal expected modification count value, which is used to compare with LinkedList's modCount to determine whether the linked list has been modified by other threads. + private int expectedModCount = modCount; + ………… +} +``` + +Below we introduce the core methods in the iterator `ListItr` in detail. + +Let’s first look at the iteration from beginning to end: + +```java +// Determine whether there is a next node +public boolean hasNext() { + // Determine whether the subscript of the next node is smaller than the size of the linked list. If so, it means there is another element that can be traversed. + return nextIndex < size; +} +// Get the next node +public E next() { + // Check whether the linked list has been modified during the iteration process + checkForCommodification(); + // Determine whether there is another node that can be traversed, if not, throw NoSuchElementException exception + if (!hasNext()) + throw new NoSuchElementException(); + // Point lastReturned to the current node + lastReturned = next; + // Point next to the next node + next = next.next; + nextIndex++; + return lastReturned.item; +} +``` + +Let’s take a look at the iteration from tail to head: + +```java +// Determine whether there is still a previous node +public boolean hasPrevious() { + return nextIndex > 0; +} + +// Get the previous node +public E previous() { + // Check whether the linked list has been modified during the iteration process + checkForCommodification(); + // If there is no previous node, throw an exception + if (!hasPrevious()) + throw new NoSuchElementException(); + // Point lastReturned and next pointers to the previous node + lastReturned = next = (next == null) ? last : next.prev; + nextIndex--; + return lastReturned.item; +} +``` + +If you need to delete or insert elements, you can also use iterators. + +```java +LinkedList list = new LinkedList<>(); +list.add("apple"); +list.add(null); +list.add("banana"); + +// The removeIf method of the Collection interface is still based on iterators. +list.removeIf(Objects::isNull); + +for (String fruit : list) { + System.out.println(fruit); +} +``` + +The method for removing elements corresponding to the iterator is as follows: + +```java +//Remove the last returned element from the list +public void remove() { + // Check whether the linked list has been modified during the iteration process + checkForCommodification(); + // If the last returned node is empty, throw an exception + if (lastReturned == null) + throw new IllegalStateException(); + + // Get the next node of the current node + Node lastNext = lastReturned.next; + //Delete the last returned node from the linked list + unlink(lastReturned); + //Modify pointer + if (next == lastReturned) + next = lastNext; + else + nextIndex--; + // Set the last returned node reference to null to facilitate GC recycling + lastReturned = null; + expectedModCount++; +} +``` + +## LinkedList common method testing + +Code: + +```java +//Create LinkedList object +LinkedList list = new LinkedList<>(); + +//Add element to the end of the linked list +list.add("apple"); +list.add("banana"); +list.add("pear"); +System.out.println("Contents of linked list: " + list); + +//Insert element at specified position +list.add(1, "orange"); +System.out.println("List content: " + list); + +// Get the element at the specified position +String fruit = list.get(2); +System.out.println("Element with index 2: " + fruit); + +//Modify the element at the specified position +list.set(3, "grape"); +System.out.println("Contents of linked list: " + list); + +//Delete the element at the specified position +list.remove(0); +System.out.println("Contents of linked list: " + list); + +//Delete the first occurrence of the specified element +list.remove("banana"); +System.out.println("Contents of linked list: " + list); + +// Get the length of the linked list +int size = list.size(); +System.out.println("List length: " + size); + +// Clear the linked list +list.clear(); +System.out.println("Cleared linked list: " + list); +``` + +Output: + +```plain +Element with index 2: banana +Linked list content: [apple, orange, banana, grape] +Linked list content: [orange, banana, grape] +Linked list content: [orange, grape] +List length: 2 +Cleared linked list: [] +``` + + \ No newline at end of file diff --git a/docs_en/java/collection/priorityqueue-source-code.en.md b/docs_en/java/collection/priorityqueue-source-code.en.md new file mode 100644 index 00000000000..3d783dd9930 --- /dev/null +++ b/docs_en/java/collection/priorityqueue-source-code.en.md @@ -0,0 +1,21 @@ +--- +title: PriorityQueue source code analysis (paid) +category: Java +tag: + - Java collections +head: + - - meta + - name: keywords + content: PriorityQueue, priority queue, binary heap, small top heap, compareTo, offer, poll, expansion, Comparator, heap sort + - - meta + - name: description + content: Overview of the heap structure and core operations of PriorityQueue, and understand the implementation details and performance characteristics of binary heap-based priority queues in insertion, deletion, and expansion. +--- + +**PriorityQueue source code analysis** is my [Knowledge Planet](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) (click the link to view the detailed introduction and joining method) exclusive content, which has been compiled into ["Java Must-Read Source Code Series"](https://javaguide.cn/zhuanlan/source-code-reading.html). + +![PriorityQueue source code analysis](https://oss.javaguide.cn/xingqiu/image-20230727084055593.png) + + + + \ No newline at end of file diff --git a/docs_en/java/concurrent/aqs.en.md b/docs_en/java/concurrent/aqs.en.md new file mode 100644 index 00000000000..ff88c7fc13f --- /dev/null +++ b/docs_en/java/concurrent/aqs.en.md @@ -0,0 +1,1598 @@ +--- +title: AQS 详解 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: AQS,AbstractQueuedSynchronizer,同步器,独占锁,共享锁,CLH 队列,acquire,release,阻塞与唤醒,条件队列 + - - meta + - name: description + content: 全面解析 AQS 的队列同步器原理与模板方法,理解其在 ReentrantLock、Semaphore 等同步器中的应用与线程阻塞唤醒机制。 +--- + + + +## AQS 介绍 + +AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 + +![](https://oss.javaguide.cn/github/javaguide/AQS.png) + +AQS 就是一个抽象类,主要用来构建锁和同步器。 + +```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() +``` + +**What is a hook method? ** A hook method is a method declared in an abstract class. It is generally modified with the `protected` keyword. It can be an empty method (implemented by a subclass) or a method implemented by default. The template design pattern controls the implementation of fixed steps through hook methods. + +Due to space issues, I won’t introduce the template method pattern in detail here. Friends who don’t know much about it can read this article: [The template method pattern modified with Java8 is really yyds!](https://mp.weixin.qq.com/s/zpScSCktFpnSWHWIQem2jg). + +Except for the hook method mentioned above, other methods in the AQS class are `final`, so they cannot be overridden by other classes. + +### AQS resource sharing method + +AQS defines two resource sharing methods: `Exclusive` (exclusive, only one thread can execute, such as `ReentrantLock`) and `Share` (shared, multiple threads can execute at the same time, such as `Semaphore`/`CountDownLatch`). + +Generally speaking, the sharing method of custom synchronizers is either exclusive or shared, and they only need to implement one of `tryAcquire-tryRelease` and `tryAcquireShared-tryReleaseShared`. But AQS also supports custom synchronizers to implement both exclusive and shared methods, such as `ReentrantReadWriteLock`. + +### AQS resource acquisition source code analysis (exclusive mode) + +The entry method for acquiring resources in exclusive mode in AQS is `acquire()`, as follows: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +In `acquire()`, the thread will first try to acquire the shared resource; if the acquisition fails, the thread will be encapsulated as a Node node and added to the waiting queue of AQS; after joining the queue, the thread in the waiting queue will try to acquire the resource, and the thread will be blocked. Correspond to the following three methods: + +- `tryAcquire()`: Try to acquire the lock (template method), `AQS` does not provide a specific implementation and is implemented by subclasses. +- `addWaiter()`: If the lock acquisition fails, the current thread will be encapsulated as a Node node and added to the CLH variant queue of AQS to wait for the lock acquisition. +- `acquireQueued()`: Block the thread and call the `tryAcquire()` method to let the threads in the queue try to acquire the lock. + +#### `tryAcquire()` Analysis + +The corresponding `tryAcquire()` template method in AQS is as follows: + +```JAVA +// AQS +protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); +} +``` + +The `tryAcquire()` method is a template method provided by AQS and does not provide a default implementation. + +Therefore, when analyzing the `tryAcquire()` method here, we will take the unfair lock (exclusive lock) of `ReentrantLock` as an example. The `tryAcquire()` internally implemented in `ReentrantLock` will call the following `nonfairTryAcquire()`: + +```JAVA +// ReentrantLock +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + // 1. Get the state in AQS + int c = getState(); + // 2. If state is 0, it proves that the lock is not occupied by other threads + if (c == 0) { + // 2.1. Update state through CAS + if (compareAndSetState(0, acquires)) { + // 2.2. If the CAS update is successful, set the lock holder to the current thread + setExclusiveOwnerThread(current); + return true; + } + } + // 3. If the current thread and the thread holding the lock are the same, it means that "lock reentrancy" has occurred. + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + // 3.1. Add 1 to the number of reentrants of the lock + setState(nextc); + return true; + } + // 4. If the lock is occupied by other threads, return false, indicating failure to acquire the lock. + return false; +} +``` + +Within the `nonfairTryAcquire()` method, resource acquisition is mainly accomplished through two core operations: + +- Update `state` variable via `CAS`. `state == 0` means the resource is not occupied. `state > 0` means that the resource is occupied, and `state` represents the number of reentries. +- Set the thread holding the resource through `setExclusiveOwnerThread()`. + +If the thread updates the `state` variable successfully, it means that the resource has been obtained, so just set the thread holding the resource as the current thread. + +#### `addWaiter()` Analysis + +After trying to obtain resources through the `tryAcquire()` method fails, the `addWaiter()` method will be called to encapsulate the current thread into a Node node and add it to the queue inside `AQS`. `addWaite()` code is as follows: + +```JAVA +// AQS +private Node addWaiter(Node mode) { + // 1. Encapsulate the current thread into a Node node. + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + // 2. If pred! = null, it proves that the tail node has been initialized, and you can directly add the Node node to the queue. + if (pred != null) { + node.prev = pred; + // 2.1. Control concurrency security through CAS. + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + // 3. Initialize the queue and add the newly created Node node to the queue. + enq(node); + return node; +} +``` + +**Concurrency safety of node enqueue:** + +In the `addWaiter()` method, you need to perform the **enqueue** operation of the Node node. Since it is in a multi-threaded environment, the `CAS` operation needs to be used to ensure concurrency safety. + +Use the `CAS` operation to update the `tail` pointer to point to the newly enqueued Node node. `CAS` can ensure that only one thread will successfully modify the `tail` pointer, thereby ensuring concurrency safety when the Node node is enqueued. + +**AQS internal queue initialization:** + +When executing `addWaiter()`, if it is found that `pred == null`, that is, the `tail` pointer is null, it proves that the queue has not been initialized. You need to call the `enq()` method to initialize the queue and add the `Node` node to the initialized queue. The code is as follows: + +```JAVA +// AQS +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { + // 1. Ensure the concurrency safety of queue initialization through CAS operation + if (compareAndSetHead(new Node())) + tail = head; + } else { + // 2. The same operation as adding nodes to the queue in the addWaiter() method + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } +} +``` + +The queue is initialized in the `enq()` method. During the initialization process, `CAS` is also required to ensure concurrency safety. + +Initializing the queue consists of two steps: initializing the `head` node and pointing `tail` to the `head` node. + +**The initialized queue is as shown below:**![](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-init.png) + +#### `acquireQueued()` Analysis + +For the convenience of reading, here is the code for `acquire()` to obtain resources in `AQS`: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +In the `acquire()` method, after adding the `Node` node to the queue through the `addWaiter()` method, the `acquireQueued()` method will be called. The code is as follows: + +```JAVA +// AQS: Let the nodes in the queue try to acquire the lock and block the thread. +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 1. Try to acquire the lock. + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 2. Determine whether the thread can be blocked. If so, block the current thread. + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + // 3. If the lock acquisition fails, the lock acquisition will be canceled and the node status will be updated to CANCELLED. + if (failed) + cancelAcquire(node); + } +} +``` + +In the `acquireQueued()` method, two main things are done: + +- **Try to acquire resources:** After the current thread joins the queue, if the predecessor node is found to be the `head` node, it means that the current thread is the first waiting node in the queue, so `tryAcquire()` is called to try to acquire resources. + +- **Block the current thread**: If the attempt to obtain resources fails, you need to block the current thread and wait to be awakened to obtain the resources. + +**1. Try to obtain resources** + +In the `acquireQueued()` method, there are a total of 2 steps to try to acquire the resource: + +- `p == head`: Indicates that the predecessor node of the current node is the `head` node. At this time, the current node is the first waiting node in the AQS queue. +- `tryAcquire(arg) == true`: Indicates that the current thread tried to acquire the resource successfully. + +After successfully acquiring the resource, the node of the current thread needs to be removed from the waiting queue. The removal operation is: set the currently waiting thread node to the `head` node (the `head` node is a virtual node and does not participate in queuing to obtain resources). + +**2. Block the current thread** + +In `AQS`, the wake-up of the current node needs to depend on the previous node. If the previous node cancels acquiring the lock, its status will change to `CANCELLED`. The node in the `CANCELLED` state has not acquired the lock, so it cannot perform the unlocking operation to wake up the current node. Therefore, nodes in the `CANCELLED` state need to be skipped before blocking the current thread. + +Use the `shouldParkAfterFailedAcquire()` method to determine whether the current thread node can be blocked, as follows: + +```JAVA +// AQS: Determine whether the current thread node can be blocked. +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + // 1. If the status of the predecessor node is normal, just return true directly. + if (ws == Node.SIGNAL) + return true; + // 2. ws > 0 means that the status of the predecessor node is abnormal, that is, the CANCELLED state, and the node in the abnormal state needs to be skipped. + if (ws > 0) { + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + // 3. If the status of the predecessor node is not SIGNAL or CANCELLED, set the status to SIGNAL. + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} +``` + +Judgment logic in the `shouldParkAfterFailedAcquire()` method: + +- If the status of the predecessor node is found to be `SIGNAL`, the current thread can be blocked. +- If the status of the predecessor node is found to be `CANCELLED`, you need to skip the node in the `CANCELLED` state. +- If it is found that the status of the predecessor node is not `SIGNAL` and `CANCELLED`, it indicates that the status of the predecessor node is in a normal state of waiting for resources, so the status of the predecessor node is set to `SIGNAL`, indicating that the predecessor node needs to wake up the subsequent node. + +After determining that the current thread can be blocked, block the current thread by calling the `parkAndCheckInterrupt()` method. `LockSupport` is used internally to implement blocking. The bottom layer of `LockSupoprt` is based on the `Unsafe` class to block threads. The code is as follows: + +```JAVA +// AQS +private final boolean parkAndCheckInterrupt() { + // 1. The thread is blocked here + LockSupport.park(this); + // 2. After the thread is awakened, return to the thread interruption state + return Thread.interrupted(); +} +``` + +**Why do we need to return to the interrupt state of the thread after it is awakened? ** + +In the `parkAndCheckInterrupt()` method, when `LockSupport.park(this)` is executed, the thread will be blocked. The code is as follows: + +```JAVA +// AQS +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + //After the thread is awakened, it needs to return to the thread interruption state + return Thread.interrupted(); +} +``` + +After the thread is awakened, `Thread.interrupted()` needs to be executed to return the interrupt status of the thread. Why is this? + +This is related to the interrupt cooperation mechanism of the thread. After the thread is awakened, it is not sure whether it was awakened by an interrupt or by `LockSupport.unpark()`, so it needs to be judged by the interrupt status of the thread. + +**In the `acquire()` method, why do you need to call `selfInterrupt()`? ** + +The `acquire()` method code is as follows: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +In the `acquire()` method, when the condition of the `if` statement returns `true`, `selfInterrupt()` will be called. This method will interrupt the current thread. Why do we need to interrupt the current thread? + +When `if` is evaluated as `true`, `tryAcquire()` needs to return `false` and `acquireQueued()` should return `true`. + +The `acquireQueued()` method returns the **interruption status** after the thread is awakened, which is returned by executing `Thread.interrupted()`. This method will clear the thread's interrupt status while returning the interrupt status.Therefore, if `if` is judged as `true`, it means that the thread's interrupt status is `true`, but after calling `Thread.interrupted()`, the thread's interrupt status is cleared to `false`, so it is necessary to re-execute `selfInterrupt()` to reset the thread's interrupt state. + +### AQS resource release source code analysis (exclusive mode) + +The entry method to release resources in exclusive mode in AQS is `release()`. The code is as follows: + +```JAVA +// AQS +public final boolean release(int arg) { + // 1. Try to release the lock + if (tryRelease(arg)) { + Node h = head; + // 2. Wake up the successor node + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +In the `release()` method, there are two main things to do: try to release the lock and wake up the successor node. The corresponding methods are as follows: + +**1. Try to release the lock** + +Try to release the lock through the `tryRelease()` method. This method is a template method and is implemented by a custom synchronizer, so here we still use `ReentrantLock` as an example. + +The `tryRelease()` method implemented in `ReentrantLock` is as follows: + +```JAVA +// ReentrantLock +protected final boolean tryRelease(int releases) { + int c = getState() - releases; + // 1. Determine whether the thread holding the lock is the current thread + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + // 2. If state is 0, it means that the current thread has no reentry times. So update free to true, indicating that the thread will release the lock. + if (c == 0) { + free = true; + // 3. Update the thread holding the resource to null + setExclusiveOwnerThread(null); + } + // 4. Update state value + setState(c); + return free; +} +``` + +In the `tryRelease()` method, the `state` value after the lock is released is first calculated to determine whether the `state` value is 0. + +- If `state == 0`, it indicates that the thread has no reentry times, update `free = true`, and modify the thread holding the resource to null, indicating that the thread completely releases the lock. +- If `state != 0`, it means that the thread still has reentry times, so the `free` value is not updated. The `free` value is `false`, which means that the thread has not completely released the lock. + +The `state` value is then updated and the `free` value is returned. The `free` value indicates whether the thread has completely released the lock. + +**2. Wake up the successor node** + +If `tryRelease()` returns `true`, it indicates that the thread has no reentry times and the lock has been completely released, so subsequent nodes need to be awakened. + +Before waking up the successor node, it is necessary to determine whether the successor node can be awakened. The judgment condition is: `h != null && h.waitStatus != 0` . Here is an explanation of why this judgment should be made: + +- `h == null`: Indicates that the `head` node has not been initialized, that is, the queue in AQS has not been initialized, so the thread node in the queue cannot be awakened. +- `h != null && h.waitStatus == 0`: Indicates that the head node has just been initialized (the initialization status of the node is 0), and the subsequent node thread has not successfully joined the queue, so there is no need to wake up the subsequent node. (When the successor node joins the queue, the status of the predecessor node will be modified to `SIGNAL`, indicating that the successor node needs to be awakened) +- `h != null && h.waitStatus != 0`: `waitStatus` may be greater than 0 or less than 0. Among them, `> 0` indicates that the node has canceled waiting to obtain resources, and `< 0` indicates that the node is in a normal waiting state. + +Next, enter the `unparkSuccessor()` method to see how to wake up the successor node: + +```JAVA +// AQS: The input parameter node here is the head node of the queue (virtual head node) +private void unparkSuccessor(Node node) { + int ws = node.waitStatus; + // 1. Clear the status of the head node to prepare for subsequent wake-up. + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + Node s = node.next; + // 2. If the successor node is abnormal, you need to traverse forward from tail to find the normal node to wake up. + 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. Wake up the successor node + LockSupport.unpark(s.thread); +} +``` + +In `unparkSuccessor()`, if the status of the head node is `< 0` (under normal circumstances, as long as there is a successor node, the status of the head node should be `SIGNAL`, that is -1), it means that the successor node needs to be awakened. Therefore, the status identifier of the head node is cleared in advance and the status is modified to 0, indicating that the operation of waking up the subsequent node has been performed. + +If `s == null` or `s.waitStatus > 0`, it indicates that the subsequent node is abnormal. At this time, the abnormal node cannot be awakened, but a node in a normal state must be found for awakening. + +Therefore, it is necessary to traverse forward from the `tail` pointer to find the first node with normal status (`waitStatus <= 0`) to wake up. + +**Why do we need to traverse forward from the `tail` pointer instead of traversing backward from the `head` pointer to find nodes in a normal state? ** + +The direction of traversal is related to the **node enqueuing operation**. How to join the team is as follows: + +```JAVA +// AQS: node enqueue method +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + if (pred != null) { + // 1. Modify the prev pointer first. + node.prev = pred; + if (compareAndSetTail(pred, node)) { + // 2. Modify the next pointer again. + pred.next = node; + return node; + } + } + enq(node); + return node; +} +``` + +In the `addWaiter()` method, adding the `node` node to the queue requires modifying the two pointers `node.prev` and `pred.next`, but these two operations are not **atomic operations**. The `node.prev` pointer is modified first, and then the `pred.next` pointer is modified. + +In extreme cases, the next node status of the `head` node may be `CANCELLED`. At this time, the newly enqueued node has only updated the `node.prev` pointer, but has not updated the `pred.next` pointer, as shown below: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-addWaiter.png) + +In this way, if you traverse backward from the `head` pointer, you cannot find the newly enqueued node, so you need to traverse forward from the `tail` pointer to find the newly enqueued node. + +### Illustration of the working principle of AQS (exclusive mode) + +At this point, the source code for obtaining resources and releasing resources in exclusive mode in AQS is finished. In order to have a clearer understanding of the working principle of AQS and node status changes, we will next understand the working principle of the entire AQS by drawing pictures. + +Since AQS is a low-level synchronization tool, the method of obtaining and releasing resources does not provide specific implementation, so here we will draw a diagram based on `ReentrantLock` to explain. + +Assume that there are a total of 3 threads trying to acquire the lock, threads `T1`, `T2` and `T3` ​​respectively. + +At this time, assume that thread `T1` acquires the lock first, and thread `T2` queues up to wait to acquire the lock. Before thread `T2` enters the queue, the AQS internal queue needs to be initialized. The `head` node has a state of `0` after initialization. The queue after initialization inside AQS is as shown below:![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process.png) + +At this point, thread `T2` attempts to acquire the lock. Since thread `T1` holds the lock, thread `T2` will enter the queue and wait to acquire the lock. At the same time, the status of the predecessor node (`head` node) will be updated from `0` to `SIGNAL`, indicating that the successor node of the `head` node needs to be awakened. At this time, the AQS internal queue is as shown below: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-2.png) + +At this point, thread `T3` attempts to acquire the lock. Since thread `T1` holds the lock, thread `T3` ​​will enter the queue and wait to acquire the lock. At the same time, the status of the predecessor node (thread `T2` node) will be updated from `0` to `SIGNAL`, indicating that the thread `T2` node needs to wake up the successor node. At this time, the AQS internal queue is as shown below: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-3.png) + +At this time, assuming that thread `T1` releases the lock, the successor node `T2` will be awakened. Thread `T2` acquires the lock after being awakened and exits from the waiting queue. + +Here, when the thread `T2` node exits the waiting queue, it does not directly remove it from the queue, but makes the thread `T2` node become the new `head` node, thereby exiting the waiting for resource acquisition. At this time, the AQS internal queue looks as follows: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-4.png) + +At this time, assuming that thread `T2` releases the lock, the successor node `T3` will be awakened. After thread `T3` ​​acquires the lock, it also exits the waiting queue, that is, changes the thread `T3` ​​node to the `head` node to exit the wait for resource acquisition. At this time, the AQS internal queue looks as follows: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-5.png) + +### AQS resource acquisition source code analysis (shared mode) + +The entry method for acquiring resources in shared mode in AQS is `acquireShared()`, as follows: + +```JAVA +// AQS +public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); +} +``` + +In the `acquireShared()` method, it will first try to acquire the shared lock. If the acquisition fails, the current thread will be added to the queue and blocked, waiting to wake up and try to acquire the shared lock, corresponding to the following two methods: `tryAcquireShared()` and `doAcquireShared()`. + +The `tryAcquireShared()` method is a template method provided by AQS, and the synchronizer implements the specific logic. Therefore, here we take `Semaphore` as an example to analyze how to obtain resources in shared mode. + +#### `tryAcquireShared()` Analysis + +Fair locks and unfair locks are implemented in `Semaphore`. Next, we will take unfair locks as an example to analyze the `tryAcquireShared()` source code. + +The `tryAcquireShared()` method overridden in `Semaphore` will call the following `nonfairTryAcquireShared()` method: + +```JAVA +// Semaphore overrides the template method of AQS +protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); +} + +// Semaphore +final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 1. Get the number of available resources. + int available = getState(); + // 2. Calculate the number of remaining resources. + int remaining = available - acquires; + // 3. If the number of remaining resources < 0, it means there are insufficient resources and returns directly; if CAS updates the state successfully, it means the current thread has obtained the shared resources and returns directly. + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } +} +``` + +In shared mode, the `state` value in AQS represents the number of shared resources. + +In the `nonfairTryAcquireShared()` method, it will continuously try to acquire resources in an infinite loop. If "the number of remaining resources is insufficient" or "the current thread successfully acquires resources", the infinite loop will be exited. The method returns **the remaining number of resources**, which is divided into 3 situations depending on the return value: + +- **Number of remaining resources > 0**: Indicates that resources are successfully obtained, and subsequent threads can also successfully obtain resources. +- **Number of remaining resources = 0**: Indicates that the resource was successfully obtained, but subsequent threads cannot successfully obtain the resource. +- **Number of remaining resources < 0**: Indicates failure to obtain resources. + +#### `doAcquireShared()` Analysis + +For the convenience of reading, here is the entry method for acquiring resources `acquireShared()`: + +```JAVA +// AQS +public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); +} +``` + +In the `acquireShared()` method, it will first try to obtain the resource through `tryAcquireShared()`. + +If the return value of the found method is `< 0`, that is, the number of remaining resources is less than 0, it indicates that the current thread failed to obtain resources. Therefore, the `doAcquireShared()` method will be entered to add the current thread to the AQS queue to wait. As follows: + +```JAVA +// AQS +private void doAcquireShared(int arg) { + // 1. Add the current thread to the queue and wait. + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head) { + // 2. If the current thread is the first node in the waiting queue, try to obtain resources. + int r = tryAcquireShared(arg); + if (r >= 0) { + // 3. Move the current thread node out of the waiting queue and wake up subsequent thread nodes. + setHeadAndPropagate(node, r); + p.next = null; // help GC + if (interrupted) + selfInterrupt(); + failed = false; + return; + } + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + // 3. If the resource acquisition fails, the resource acquisition will be canceled and the node status will be updated to CANCELLED. + if (failed) + cancelAcquire(node); + } +} +``` + +Since the current thread has failed to acquire resources, in the `doAcquireShared()` method, the current thread needs to be encapsulated as a Node node and added to the queue to wait. + +The biggest difference between obtaining resources in **shared mode** and obtaining resources in **exclusive mode** is that in shared mode, the number of resources may be greater than 1, that is, multiple threads can hold resources at the same time.Therefore, in the shared mode, when the thread is awakened and the resource is obtained, if it is found that there are remaining resources, it will try to wake up the following thread to try to obtain the resource. The corresponding `setHeadAndPropagate()` method is as follows: + +```JAVA +// AQS +private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 1. Move the current thread node out of the waiting queue. + setHead(node); + // 2. Wake up subsequent waiting nodes. + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + if (s == null || s.isShared()) + doReleaseShared(); + } +} +``` + +In the `setHeadAndPropagate()` method, certain conditions need to be met to wake up subsequent nodes, mainly 2 conditions need to be met: + +- `propagate > 0`: `propagate` represents the number of resources remaining after acquiring the resource. If `> 0`, subsequent threads can be awakened to acquire resources. +- `h.waitStatus < 0`: The `h` node here is the `head` node before executing `setHead()`. Use `< 0` when judging `head.waitStatus`, mainly to determine whether the status of the `head` node is `SIGNAL` or `PROPAGATE`. If the `head` node is `SIGNAL`, subsequent nodes can be awakened; if the `head` node status is `PROPAGATE`, subsequent nodes can also be awakened (this is to solve problems in concurrency scenarios, which will be discussed in detail later). + +The `if` judgment about **waking up subsequent waiting nodes** in the code is a little more complicated. Here is why it is written like this: + +```JAVA +if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) +``` + +- `h == null || h.waitStatus < 0` : `h == null` is used to prevent null pointer exception. Under normal circumstances, h will not be `null`, because the current node has been added to the queue before execution here, and the queue cannot be initialized yet. + + `h.waitStatus < 0` mainly determines whether the status of the `head` node is `SIGNAL` or `PROPAGATE`. It is more convenient to directly use `< 0` to determine. + +- `(h = head) == null || h.waitStatus < 0`: If the previously determined `h.waitStatus < 0` is explained here, it means there is concurrency. + + At the same time, there are other threads waking up subsequent nodes, and the value of the `head` node has been modified from `SIGNAL` to `0`. Therefore, a new `head` node is obtained here again. The `head` node obtained this time is the current thread node set by `setHead()`, and then the `waitStatus` status is determined again. + +If the `if` condition is passed, it will go to the `doReleaseShared()` method to wake up subsequent waiting nodes, as follows: + +```JAVA +private void doReleaseShared() { + for (;;) { + Node h = head; + // 1. At least one waiting thread node is required in the queue. + if (h != null && h != tail) { + int ws = h.waitStatus; + // 2. If the status of the head node is SIGNAL, the successor node can be awakened. + if (ws == Node.SIGNAL) { + // 2.1 Clear the SIGNAL status of the head node and update it to 0. Indicates that the successor node of this node has been awakened. + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 2.2 Wake up the successor node + unparkSuccessor(h); + } + // 3. If the status of the head node is 0, update it to PROPAGATE. This is to solve problems existing in concurrency scenarios, which will be discussed in detail next. + else if (ws == 0 && + !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + if (h == head) + break; + } +} +``` + +In the `doReleaseShared()` method, the `waitStatus` status of the `head` node will be judged to determine the next operation. There are two situations: + +- The status of the `head` node is `SIGNAL`: It indicates that the `head` node has a successor node that needs to be awakened, so the `SIGNAL` status of the `head` node is updated to `0` through the `CAS` operation. Clearing the `SIGNAL` status indicates that the successor node of the `head` node has been awakened. +- The status of the `head` node is `0`: indicating the existence of concurrency. You need to change `0` to `PROPAGATE` to ensure that the thread can be woken up normally in a concurrent scenario. + +#### Why is `PROPAGATE` status needed? + +When `doReleaseShared()` releases resources, step 3 is not easy to understand, that is, if the state of the `head` node is found to be `0`, update the state of the `head` node from `0` to `PROPAGATE`. + +In AQS, the `PROPAGATE` of the Node node is to deal with the problem of being unable to wake up the thread node that may occur in concurrent scenarios. `PROPAGATE` is only used once in the `doReleaseShared()` method. + +**Next, through case analysis, why is `PROPAGATE` status needed? ** + +In shared mode, the method call chain for threads to acquire and release resources is as follows: + +- The method call chain for threads to acquire resources is: `acquireShared() -> tryAcquireShared() -> Thread blocks and waits for wake-up -> tryAcquireShared() -> setHeadAndPropagate() -> if (number of remaining resources > 0) || (head.waitStatus < 0) then wake up subsequent nodes`. + +- The method call chain for a thread to release resources is: `releaseShared() -> tryReleaseShared() -> doReleaseShared()`. + +**If the status of the `head` node is not changed from `0` to `PROPAGATE` when releasing resources:** + +Assume a total of 4 threads try to acquire a resource in shared mode, for a total of 2 resources. The initial `T3` ​​and `T4` threads obtained the resource, but the `T1` and `T2` threads did not, so they were queued and waiting in the queue. + +- At time 1, threads `T1` and `T2` are in the waiting queue, and `T3` ​​and `T4` hold resources. At this time, the nodes in the waiting queue and the corresponding status are (the `waitStatus` status of the node is in parentheses): + + `head(-1) -> T1(-1) -> T2(0)` . + +- At time 2, thread `T3` ​​releases resources, updates the status of the `head` node from `SIGNAL` to `0` through the `doReleaseShared()` method, and wakes up thread `T1`, after which thread `T3` ​​exits. + + After thread `T1` is awakened, it obtains resources through `tryAcquireShared()`, but it has not yet had time to execute `setHeadAndPropagate()` to set itself as the `head` node. At this time, the status of the nodes in the waiting queue is: + + `head(0) -> T1(-1) -> T2(0)` . + +- At time 3, thread `T4` releases resources. Since the status of `head` node is `0` at this time, the successor node of `head` cannot be awakened in the `doReleaseShared()` method, and then thread `T4` exits. + +- At time 4, thread `T1` continues to execute the `setHeadAndPropagate()` method to set itself as the `head` node. + + But at this time, because the number of remaining resources returned by thread `T1` when executing the `tryAcquireShared()` method is `0`, and the status of the `head` node is `0`, thread `T1` will not wake up subsequent nodes in the `setHeadAndPropagate()` method. At this time, the status of the nodes in the waiting queue is: + + `head(-1, thread T1 node) -> T2(0)`.At this time, the thread `T2` node is in the waiting queue and cannot be awakened. The corresponding timetable is as follows: + +| Time | Thread T1 | Thread T2 | Thread T3 | Thread T4 | Wait queue | +| ------ | ------------------------------------------------------------------ | -------- | ---------------- | ------------------------------------------------------------------ | ---------------------------------- | +| Time 1 | Waiting queue | Waiting queue | Holding resources | Holding resources | `head(-1) -> T1(-1) -> T2(0)` | +| Time 2 | (Execution) After being awakened, obtain resources, but will not have time to set itself as the `head` node | Waiting queue | (Execution) release resources | Hold resources | `head(0) -> T1(-1) -> T2(0)` | +| Time 3 | | Waiting queue | Exited | (Execute) Release resources. But the `head` node status is `0` and the successor node cannot be awakened | `head(0) -> T1(-1) -> T2(0)` | +| Time 4 | (Execution) Set self to `head` node | Waiting queue | Exited | Exited | `head(-1, thread T1 node) -> T2(0)` | + +**If you change the status of the `head` node from `0` to `PROPAGATE` when the thread releases resources, you can solve the concurrency problem above, as follows: ** + +- At time 1, threads `T1` and `T2` are in the waiting queue, and `T3` and `T4` hold resources. At this time, the nodes in the waiting queue and their corresponding status are: + + `head(-1) -> T1(-1) -> T2(0)` . + +- At time 2, thread `T3` ​​releases resources, updates the status of the `head` node from `SIGNAL` to `0` through the `doReleaseShared()` method, and wakes up thread `T1`, after which thread `T3` ​​exits. + + After thread `T1` is awakened, it obtains resources through `tryAcquireShared()`, but it has not yet had time to execute `setHeadAndPropagate()` to set itself as the `head` node. At this time, the status of the nodes in the waiting queue is: + + `head(0) -> T1(-1) -> T2(0)` . + +- At time 3, thread `T4` releases resources. Since the status of `head` node is `0` at this time, the `doReleaseShared()` method will update the status of `head` node from `0` to `PROPAGATE`, and then thread `T4` exits. At this time, the status of the nodes in the waiting queue is: + + `head(PROPAGATE) -> T1(-1) -> T2(0)` . + +- At time 4, thread `T1` continues to execute the `setHeadAndPropagate()` method to set itself as the `head` node. At this time, the status of the nodes in the waiting queue is: + + `head(-1, thread T1 node) -> T2(0)`. + +- At time 5, although the number of remaining resources returned by thread `T1` when executing the `tryAcquireShared()` method is `0`, the `head` node status is `PROPAGATE < 0` (the `head` node here is the old `head` node, not the thread `T1` node that has just become the `head` node). + + Therefore, thread `T1` will wake up the subsequent `T2` node in the `setHeadAndPropagate()` method and update the status of the `head` node from `SIGNAL` to `0`. At this time, the status of the nodes in the waiting queue is: + + `head(0, thread T1 node) -> T2(0)`. + +- At time 6, after thread `T2` is awakened, it obtains the resource and sets itself as the `head` node. At this time, the status of the nodes in the waiting queue is: + + `head(0, thread T2 node)` . + +With the `PROPAGATE` state, you can avoid the situation where thread `T2` cannot be awakened. The corresponding timetable is as follows: + +| Time | Thread T1 | Thread T2 | Thread T3 | Thread T4 | Wait queue | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | -------------------------------------------------- | +| Time 1 | Waiting queue | Waiting queue | Holding resources | Holding resources | `head(-1) -> T1(-1) -> T2(0)` | +| Time 2 | (Execution) After being awakened, obtain resources, but will not have time to set itself as the `head` node | Waiting queue | (Execution) release resources | Hold resources | `head(0) -> T1(-1) -> T2(0)` || Time 3 | No further execution | Waiting queue | Exited | (Execution) Release resources. At this time, the `head` node status will be updated from `0` to `PROPAGATE` | `head(PROPAGATE) -> T1(-1) -> T2(0)` | +| Time 4 | (Execution) Set self to `head` node | Waiting queue | Exited | Exited | `head(-1, thread T1 node) -> T2(0)` | +| Time 5 | (Execution) Since the `head` node status is `PROPAGATE < 0`, subsequent nodes will be awakened in the `setHeadAndPropagate()` method. At this time, the state of the new `head` node will be updated from `SIGNAL` to `0`, and thread `T2` will be awakened | Waiting queue | Exited | Exited | `head(0, thread T1 node) -> T2(0)` | +| Time 6 | Exited | After the (execution) thread `T2` is awakened, it obtains the resource and sets itself as the `head` node | Exited | Exited | `head(0, thread T2 node)` | + +### AQS resource release source code analysis (shared mode) + +The entry method to release resources in shared mode in AQS is `releaseShared()`, and the code is as follows: + +```JAVA +// AQS +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; +} +``` + +The `tryReleaseShared()` method is a template method provided by AQS. It is also explained here using `Semaphore`, as follows: + +```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; + } +} +``` + +In the `tryReleaseShared()` method implemented by `Semaphore`, it will continuously try to release resources in an infinite loop, that is, update the `state` value through the `CAS` operation. + +If the update is successful, it proves that the resource is released successfully and the `doReleaseShared()` method will be entered. + +The `doReleaseShared()` method has undergone detailed source code analysis in the previous part of obtaining resources (shared mode) and will not be repeated here. + +## Common synchronization tools + +The following introduces several common synchronization tool classes based on AQS. + +### Semaphore(Semaphore) + +#### Introduction + +Both `synchronized` and `ReentrantLock` allow only one thread to access a certain resource at a time, and `Semaphore` (semaphore) can be used to control the number of threads accessing a specific resource at the same time. + +The use of `Semaphore` is simple. We assume here that there are `N(N>5)` threads to obtain the shared resources in `Semaphore`. The following code indicates that only 5 threads among the N threads can obtain the shared resources at the same time, and other threads will be blocked. Only the thread that obtains the shared resources can execute. Wait until a thread releases the shared resource before other blocked threads can obtain it. + +```java +//Initial number of shared resources +final Semaphore semaphore = new Semaphore(5); +// Get 1 license +semaphore.acquire(); +// Release 1 license +semaphore.release(); +``` + +When the initial number of resources is 1, `Semaphore` degenerates into an exclusive lock. + +`Semaphore` has two modes:. + +- **Fair mode:** The order in which the `acquire()` method is called is the order in which licenses are obtained, following FIFO; +- **Unfair Mode:** Preemptive. + +The two construction methods corresponding to `Semaphore` are as follows: + +```java +public Semaphore(int permits) { + sync = new NonfairSync(permits); +} + +public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); +} +``` + +**Both of these two construction methods must provide the number of permissions. The second construction method can specify whether it is fair mode or unfair mode. The default is unfair mode. ** + +`Semaphore` is usually used in scenarios where resources have clear limits on the number of accesses, such as current limiting (only in stand-alone mode, it is recommended to use Redis + Lua for current limiting in actual projects). + +#### Principle + +`Semaphore` is an implementation of shared locks. It constructs the `state` value of AQS by default as `permits`. You can understand the value of `permits` as the number of licenses. Only threads that have obtained the license can execute. + +Taking the parameterless `acquire` method as an example, calling `semaphore.acquire()`, the thread tries to obtain the license. If `state > 0`, it means that the acquisition can be successful. If `state <= 0`, it means that the number of licenses is insufficient and the acquisition fails. + +If it can be obtained successfully (`state > 0`), it will try to use the CAS operation to modify the value of `state` to `state=state-1`. If the acquisition fails, a Node node will be created and added to the waiting queue, and the current thread will be suspended. + +```java +// Get 1 license +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} + +// Get one or more licenses +public void acquire(int permits) throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireSharedInterruptibly(permits); +} +``` + +The `acquireSharedInterruptibly` method is the default implementation in `AbstractQueuedSynchronizer`. + +```java +// Obtain the license in shared mode. If the acquisition is successful, it will be returned. If it fails, it will be added to the waiting queue and the thread will be suspended. +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + //Try to obtain the license. arg is the number of licenses to obtain. When the acquisition fails, create a node and add it to the waiting queue, suspending the current thread. + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); +}``` + +Here we take the non-fair mode (`NonfairSync`) as an example to see the implementation of the `tryAcquireShared` method. + +```java +// Try to obtain resources in shared mode (resources in Semaphore are licenses): +protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); +} + +// Obtain license in unfair sharing mode +final int nonfairTryAcquireShared(int acquires) { + for (;;) { + //The current number of available licenses + int available = getState(); + /* + * Try to obtain a license. When the current number of available licenses is less than or equal to 0, a negative value is returned, indicating that the acquisition failed. + * Successful acquisition is possible only when the current available license is greater than 0. If CAS fails, it will re-acquire the latest value in a loop and try to obtain it. + */ + int remaining = available - acquires; + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } +} +``` + +Taking the parameterless `release` method as an example, calling `semaphore.release();`, the thread tries to release the license and uses the CAS operation to modify the value of `state` to `state=state+1`. After the license is released successfully, a thread in the waiting queue will be awakened at the same time. The awakened thread will retry to modify the value of `state` `state=state-1`. If `state > 0`, the token is obtained successfully, otherwise it will re-enter the waiting queue and suspend the thread. + +```java +// Release a license +public void release() { + sync.releaseShared(1); +} + +// Release one or more licenses +public void release(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + sync.releaseShared(permits); +} +``` + +The `releaseShared` method is the default implementation in `AbstractQueuedSynchronizer`. + +```java +// Release shared lock +// If tryReleaseShared returns true, wake up one or more threads in the waiting queue. +public final boolean releaseShared(int arg) { + //Release shared lock + if (tryReleaseShared(arg)) { + //Release the current node's post-waiting node + doReleaseShared(); + return true; + } + return false; +} +``` + +The `tryReleaseShared` method is a method overridden by the internal class `Sync` of `Semaphore`. The default implementation in `AbstractQueuedSynchronizer` only throws the `UnsupportedOperationException` exception. + +```java +//A method overridden in the inner class Sync +//Try to release resources +protected final boolean tryReleaseShared(int releases) { + for (;;) { + int current = getState(); + // Available licenses +1 + int next = current + releases; + if (next < current) // overflow + throw new Error("Maximum permit count exceeded"); + // CAS modifies the value of state + if (compareAndSetState(current, next)) + return true; + } +} +``` + +As you can see, the underlying methods mentioned above are basically implemented through the synchronizer `sync`. `Sync` is an internal class of `CountDownLatch`, inherits `AbstractQueuedSynchronizer` and overrides some of its methods. Moreover, Sync corresponds to two subclasses: `NonfairSync` (corresponding to unfair mode) and `FairSync` (corresponding to fair mode). + +```java +private static final class Sync extends AbstractQueuedSynchronizer { + // ... +} +static final class NonfairSync extends Sync { + // ... +} +static final class FairSync extends Sync { + // ... +} +``` + +#### Actual combat + +```java +public class SemaphoreExample { + //Number of requests + private static final int threadCount = 550; + + public static void main(String[] args) throws InterruptedException { + //Create a thread pool object with a fixed number of threads (if the number of threads in the thread pool here is too small, you will find that the execution is very slow) + ExecutorService threadPool = Executors.newFixedThreadPool(300); + //Initial license quantity + final Semaphore semaphore = new Semaphore(20); + + for (int i = 0; i < threadCount; i++) { + final int threadnum = i; + threadPool.execute(() -> {// Application of Lambda expression + try { + semaphore.acquire();//Acquire a license, so the number of runnable threads is 20/1=20 + test(threadnum); + semaphore.release(); // Release a license + } 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); // Simulate the time-consuming operation of the request + System.out.println("threadnum:" + threadnum); + Thread.sleep(1000); // Simulate the time-consuming operation of the request + } +} +``` + +Execution of the `acquire()` method blocks until a license can be obtained and then takes a license; each `release` method adds a license, which may release a blocked `acquire()` method. However, there is no actual license object, `Semaphore` just maintains a number of available licenses. `Semaphore` is often used to limit the number of threads that can obtain a certain resource. + +Of course, you can also take and release multiple licenses at once, but this is generally not necessary: + +```java +semaphore.acquire(5);//Acquire 5 licenses, so the number of runnable threads is 20/5=4 +test(threadnum); +semaphore.release(5);//Release 5 licenses +``` + +In addition to the `acquire()` method, another commonly used corresponding method is the `tryAcquire()` method, which returns false immediately if the permission cannot be obtained. + +[Supplementary content for issue645](https://github.com/Snailclimb/JavaGuide/issues/645):> `Semaphore` is implemented based on AQS and is used to control the number of threads for concurrent access, but it is different from the concept of shared locks. The constructor of `Semaphore` uses the `permits` parameter to initialize the AQS `state` variable, which represents the number of available licenses. When a thread calls the `acquire()` method to try to acquire a permission, `state` is atomically decremented by 1. If `state` is greater than or equal to 0 after being decremented by 1, `acquire()` returns successfully and the thread can continue execution. If `state` is less than 0 after decremented by 1, it means that the number of threads currently accessing concurrently has reached the `permits` limit, and the thread will be put into the AQS waiting queue and blocked, **instead of spinning and waiting**. When other threads complete their tasks and call the `release()` method, `state` is atomically incremented by 1. The `release()` operation wakes up one or more blocked threads in the AQS wait queue. These awakened threads will attempt the `acquire()` operation again, competing for available permissions. Therefore, `Semaphore` limits the number of concurrently accessed threads by controlling the number of permissions, rather than through spin and shared lock mechanisms. + +### CountDownLatch (countdown timer) + +#### Introduction + +`CountDownLatch` allows `count` threads to block in one place until all threads' tasks are completed. + +`CountDownLatch` is one-time use. The value of the counter can only be initialized once in the constructor. There is no mechanism to set its value again afterwards. When `CountDownLatch` is used, it cannot be used again. + +#### Principle + +`CountDownLatch` is an implementation of shared locks. It constructs the `state` value of AQS by default as `count`. We can see this through the constructor of `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); + } + //... +} +``` + +When the thread calls `countDown()`, it actually uses the `tryReleaseShared` method to reduce `state` with CAS operations until `state` is 0. When `state` is 0, it means that all threads have called the `countDown` method, then the threads waiting on `CountDownLatch` will be awakened and continue execution. + +```java +public void countDown() { + // Sync is the internal class of CountDownLatch, which inherits AbstractQueuedSynchronizer + sync.releaseShared(1); +} +``` + +The `releaseShared` method is the default implementation in `AbstractQueuedSynchronizer`. + +```java +// Release shared lock +// If tryReleaseShared returns true, wake up one or more threads in the waiting queue. +public final boolean releaseShared(int arg) { + //Release shared lock + if (tryReleaseShared(arg)) { + //Release the current node's post-waiting node + doReleaseShared(); + return true; + } + return false; +} +``` + +The `tryReleaseShared` method is a method overridden by the inner class `Sync` of `CountDownLatch`. The default implementation in `AbstractQueuedSynchronizer` only throws the `UnsupportedOperationException` exception. + +```java +// Decrement state until state becomes 0; +// countDown will return true only when count decrements to 0 +protected boolean tryReleaseShared(int releases) { + // Optional check whether state is 0 + for (;;) { + int c = getState(); + // If state is already 0, return false directly + if(c==0) + return false; + // Decrement state + int nextc = c-1; + //CAS operation updates the value of state + if (compareAndSetState(c, nextc)) + return nextc == 0; + } +} +``` + +Take the parameterless `await` method as an example. When calling `await()`, if `state` is not 0, it means that the task has not been executed yet, and `await()` will always block, which means that the statements after `await()` will not be executed (the `main` thread is added to the waiting queue, which is the variant CLH queue). Then, `CountDownLatch` will spin CAS to determine `state == 0`. If `state == 0`, all waiting threads will be released, and the statements after the `await()` method will be executed. + +```java +// Wait (can also be called locking) +public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +// Wait with timeout +public boolean await(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); +} +``` + +The `acquireSharedInterruptibly` method is the default implementation in `AbstractQueuedSynchronizer`. + +```java +//Try to acquire the lock, return if successful, join the waiting queue if failed, and suspend the thread +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + //Try to acquire the lock and return if successful. + if (tryAcquireShared(arg) < 0) + // If the acquisition fails, join the waiting queue and suspend the thread. + doAcquireSharedInterruptibly(arg); +} +``` + +The `tryAcquireShared` method is a method overridden by the internal class `Sync` of `CountDownLatch`. Its function is to determine whether the value of `state` is 0. If so, it returns 1, otherwise it returns -1. + +```java +protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; +} +``` + +#### Actual combat + +**Two typical uses of CountDownLatch**: + +1. A thread waits for n threads to complete execution before starting to run: Initialize the counter of `CountDownLatch` to n (`new CountDownLatch(n)`). Whenever a task thread completes execution, decrement the counter by 1 (`countdownlatch.countDown()`). When the counter value becomes 0, the thread await()` on `CountDownLatch` will be awakened. A typical application scenario is that when starting a service, the main thread needs to wait for multiple components to be loaded before continuing execution. +2. Achieve maximum parallelism for multiple threads to start executing tasks: Note that it is parallelism, not concurrency. The emphasis is that multiple threads start executing at the same time at a certain moment. Similar to a race, multiple threads are placed at the starting point, wait for the starting gun to sound, and then start running at the same time. The method is to initialize a shared `CountDownLatch` object and initialize its counter to 1 (`new CountDownLatch(1)`). Multiple threads first `coundownlatch.await()` before starting to execute the task. When the main thread calls `countDown()`, the counter becomes 0 and multiple threads are awakened at the same time. + +**CountDownLatch code example**: + +```java +public class CountDownLatchExample { + //Number of requests + private static final int THREAD_COUNT = 550; + + public static void main(String[] args) throws InterruptedException { + //Create a thread pool object with a fixed number of threads (if the number of threads in the thread pool here is too small, you will find that the execution is very slow) + // This is only used for testing. In actual scenarios, please manually assign thread pool parameters. + 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 { + // Indicates that a request has been completed + 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); + } +}``` + +In the above code, we define the number of requests as 550. When these 550 requests are processed, `System.out.println("finish");` will be executed. + +The first interaction with `CountDownLatch` is the main thread waiting for other threads. The main thread must call the `CountDownLatch.await()` method immediately after starting other threads. In this way, the operation of the main thread will block on this method until other threads complete their respective tasks. + +The other N threads must reference the latch object because they need to notify the `CountDownLatch` object that they have completed their respective tasks. This notification mechanism is accomplished through the `CountDownLatch.countDown()` method; each time this method is called, the count value initialized in the constructor is decremented by 1. So when N threads all call this method, the value of count is equal to 0, and then the main thread can resume executing its own tasks through the `await()` method. + +One more comment: Improper use of the `await()` method of `CountDownLatch` can easily cause deadlock. For example, the for loop in our code above is changed to: + +```java +for (int i = 0; i < threadCount-1; i++) { +....... +} +``` + +This will result in the value of `count` being unable to be equal to 0, which will result in waiting forever. + +### CyclicBarrier(Cyclic Barrier) + +#### Introduction + +`CyclicBarrier` is very similar to `CountDownLatch`. It can also implement technical waiting between threads, but its function is more complex and powerful than `CountDownLatch`. The main application scenarios are similar to `CountDownLatch`. + +> The implementation of `CountDownLatch` is based on AQS, while `CyclicBarrier` is based on `ReentrantLock` (`ReentrantLock` also belongs to the AQS synchronizer) and `Condition`. + +`CyclicBarrier` literally means cyclic barrier. What it does is: let a group of threads be blocked when they reach a barrier (also called a synchronization point). The barrier will not open until the last thread reaches the barrier, and all threads intercepted by the barrier will continue to work. + +#### Principle + +`CyclicBarrier` internally uses a `count` variable as a counter. The initial value of `count` is the initialization value of the `parties` attribute. Whenever a thread reaches the barrier, the counter is decremented by 1. If the count value is 0, it means that this is the last thread of this generation to reach the fence, and it will try to execute the task entered in our constructor. + +```java +//The number of threads intercepted each time +private final int parties; +//Counter +private int count; +``` + +Let’s take a brief look at the source code below. + +1. The default constructor of `CyclicBarrier` is `CyclicBarrier(int parties)`, whose parameters represent the number of threads intercepted by the barrier. Each thread calls the `await()` method to tell `CyclicBarrier` that I have reached the barrier, and then the current thread is blocked. + +```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; +} +``` + +Among them, `parties` represents the number of intercepted threads. When the number of intercepted threads reaches this value, the fence will be opened to allow all threads to pass. + +2. When calling the `await()` method of the `CyclicBarrier` object, the `dowait(false, 0L)` method is actually called. The `await()` method acts like erecting a fence, blocking threads. When the number of blocked threads reaches the value of `parties`, the fence will open and the threads can pass for execution. + +```java +public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } +} +``` + +The source code analysis of `dowait(false, 0L)` method is as follows: + +```java + // When the number of threads or the number of requests reaches count, the method after await will be executed. In the above example, the value of count is 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.lock(); + try { + final Generation g = generation; + + if (g.broken) + throw new BrokenBarrierException(); + + //If the thread is interrupted, throw an exception + if (Thread.interrupted()) { + breakBarrier(); + throw new InterruptedException(); + } + // count minus 1 + int index = --count; + // When the count is reduced to 0, it means that the last thread has reached the fence, that is, it has reached the condition after which the await method can be executed. + if (index == 0) { // tripped + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + command.run(); + ranAction = true; + //Reset count to the initialization value of the parties property + // Wake up the previously waiting thread + //The next wave of execution begins + 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 +...... +``` + +## 参考 + +- Java 并发之 AQS 详解: +- 从 ReentrantLock 的实现看 AQS 的原理及应用: + + \ No newline at end of file diff --git a/docs_en/java/concurrent/atomic-classes.en.md b/docs_en/java/concurrent/atomic-classes.en.md new file mode 100644 index 00000000000..139455c5648 --- /dev/null +++ b/docs_en/java/concurrent/atomic-classes.en.md @@ -0,0 +1,404 @@ +--- +title: Atomic Atomic Class Summary +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: Atomic class, AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference, CAS, optimistic locking, atomic operation, JUC + - - meta + - name: description + content: Overview of the types and usage scenarios of JUC atomic classes, CAS-based atomicity guarantee and concurrency performance, and understanding of the advantages and limitations of atomic classes compared to locks. +--- + +## Introduction to Atomic class + +`Atomic` translated into Chinese means "atom". In chemistry, atoms are the smallest units of matter and are indivisible in chemical reactions. In programming, `Atomic` refers to the atomicity of an operation, that is, the operation is indivisible and uninterruptible. Even when multiple threads execute at the same time, the operation is either fully executed or not executed, and the partially completed status will not be seen by other threads. + +An atomic class is simply a class with atomic operation characteristics. + +The `Atomic` class in the `java.util.concurrent.atomic` package provides a thread-safe way to manipulate individual variables. + +The `Atomic` class relies on CAS (Compare-And-Swap, compare and swap) optimistic locking to guarantee the atomicity of its methods without using traditional locking mechanisms (such as `synchronized` blocks or `ReentrantLock`). + +In this article, we only introduce the concept of Atomic atomic class. For the specific implementation principle, you can read this article written by the author: [CAS Detailed Explanation] (./cas.md). + +![JUC Atomic Class Overview](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +According to the data type of the operation, the atomic classes in the JUC package can be divided into 4 categories: + +**1.Basic type** + +Update basic types atomically + +- `AtomicInteger`: Integer atomic class +- `AtomicLong`: Long integer atomic class +- `AtomicBoolean`: Boolean atomic class + +**2. Array type** + +Update an element in an array atomically + +- `AtomicIntegerArray`: Integer array atomic class +- `AtomicLongArray`: long integer array atomic class +- `AtomicReferenceArray`: reference type array atomic class + +**3. Reference type** + +- `AtomicReference`: reference type atomic class +- `AtomicMarkableReference`: Atomic update of a marked reference type. This class associates boolean tags with references, and can also solve ABA problems that may occur when using CAS for atomic updates. +- `AtomicStampedReference`: Atomicly update a reference type with a version number. This class associates integer values ​​with references and can be used to solve atomic update data and data version numbers, and can solve ABA problems that may occur when using CAS for atomic updates. + +**🐛 Bugfix (see: [issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))**: `AtomicMarkableReference` does not solve ABA issues. + +**4. Object attribute modification type** + +- `AtomicIntegerFieldUpdater`: updater for atomically updating integer fields +- `AtomicLongFieldUpdater`: updater for atomically updating long integer fields +- `AtomicReferenceFieldUpdater`: atomically update fields in reference types + +## Basic type atomic class + +Update basic types atomically + +- `AtomicInteger`: Integer atomic class +- `AtomicLong`: Long integer atomic class +- `AtomicBoolean`: Boolean atomic class + +The methods provided by the above three classes are almost the same, so we use `AtomicInteger` as an example to introduce them here. + +**Common methods of `AtomicInteger` class**: + +```java +public final int get() //Get the current value +public final int getAndSet(int newValue)//Get the current value and set the 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 is equal to the expected value, atomically set the value to the input value (update) +public final void lazySet(int newValue)//Finally set to newValue, lazySet provides a weaker semantic than the set method, which may cause other threads to still read the old value in a short period of time, but it may be more efficient. +``` + +**`AtomicInteger` class usage example**: + +```java +//Initialize the AtomicInteger object, the initial value is 0 +AtomicInteger atomicInt = new AtomicInteger(0); + +// Use the 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); + +// Use the getAndIncrement method to get the current value and increment it by 1 +tempValue = atomicInt.getAndIncrement(); +System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); + +// Use the getAndAdd method to get the current value and increase the specified value by 5 +tempValue = atomicInt.getAndAdd(5); +System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); + +// Use the compareAndSet method for atomic conditional update, the expected value is 9, the updated value is 10 +boolean updateSuccess = atomicInt.compareAndSet(9, 10); +System.out.println("Update Success: " + updateSuccess + "; atomicInt: " + atomicInt); + +// Get the current value +int currentValue = atomicInt.get(); +System.out.println("Current value: " + currentValue); + +// Use the lazySet method to set the new value to 15 +atomicInt.lazySet(15); +System.out.println("After lazySet, atomicInt: " + atomicInt); +``` + +Output: + +```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 +``` + +## Array type atomic class + +Update an element in an array atomically + +- `AtomicIntegerArray`: Integer array atomic class +- `AtomicLongArray`: Long integer array atomic class +- `AtomicReferenceArray`: reference type array atomic class + +The methods provided by the above three classes are almost the same, so we use `AtomicIntegerArray` as an example to introduce them here. + +**Common methods of `AtomicIntegerArray` class**: + +```java +public final int get(int i) //Get the value of the element at index=i position +public final int getAndSet(int i, int newValue)//Return the current value at index=i and set it to the new value: newValue +public final int getAndIncrement(int i)//Get the value of the element at index=i position, and let the element at that position increase by itself +public final int getAndDecrement(int i) //Get the value of the element at index=i and let the element at that position decrement +public final int getAndAdd(int i, int delta) //Get the value of the element at index=i and add the expected value +boolean compareAndSet(int i, int expect, int update) //If the input value is equal to the expected value, atomically set the element value at index=i to the input value (update) +public final void lazySet(int i, int newValue)//Finally set the element at index=i to newValue. Using lazySet to set it may cause other threads to still be able to read the old value in a short period of time.``` + +**`AtomicIntegerArray` class usage example**: + +```java +int[] nums = {1, 2, 3, 4, 5, 6}; +//Create AtomicIntegerArray +AtomicIntegerArray atomicArray = new AtomicIntegerArray(nums); + +//Print the initial value in AtomicIntegerArray +System.out.println("Initial values in AtomicIntegerArray:"); +for (int j = 0; j < nums.length; j++) { + System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); +} + +// Set the value at index 0 to 2 using the getAndSet method and return the old value +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) + " "); +} + +// Use the getAndIncrement method to increment the value at index 0 by 1 and return the old value +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) + " "); +} + +// Use the getAndAdd method to increase the value at index 0 by 5 and return the old value +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) + " "); +} +``` + +Output: + +```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 +``` + +## Reference type atomic class + +Basic type atomic classes can only update one variable. If you need to update multiple variables atomically, you need to use reference type atomic classes. + +- `AtomicReference`: reference type atomic class +- `AtomicStampedReference`: Atomicly update a reference type with a version number. This class associates integer values ​​with references and can be used to solve atomic update data and data version numbers, and can solve ABA problems that may occur when using CAS for atomic updates. +- `AtomicMarkableReference`: Atomic update of a marked reference type. This class associates boolean tags with references and can also solve ABA issues that may arise when using CAS for atomic updates. ~~ + +The methods provided by the above three classes are almost the same, so we use `AtomicReference` as an example to introduce them here. + +**`AtomicReference` class usage example**: + +```java +// Person class +class Person { + private String name; + private int age; + //Omit getter/setter and toString +} + + +//Create AtomicReference object and set initial value +AtomicReference ar = new AtomicReference<>(new Person("SnailClimb", 22)); + +// print initial value +System.out.println("Initial Person: " + ar.get().toString()); + +//update value +Person updatePerson = new Person("Daisy", 20); +ar.compareAndSet(ar.get(), updatePerson); + +//Print the updated value +System.out.println("Updated Person: " + ar.get().toString()); + +//Try to update again +Person anotherUpdatePerson = new Person("John", 30); +boolean isUpdated = ar.compareAndSet(updatePerson, anotherUpdatePerson); + +//Print whether the update is successful and the final value +System.out.println("Second Update Success: " + isUpdated); +System.out.println("Final Person: " + ar.get().toString()); +``` + +Output: + +```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` class usage example**: + +```java +//Create an AtomicStampedReference object with the initial value "SnailClimb" and the initial version number 1 +AtomicStampedReference asr = new AtomicStampedReference<>("SnailClimb", 1); + +//Print initial value and version number +int[] initialStamp = new int[1]; +String initialRef = asr.get(initialStamp); +System.out.println("Initial Reference: " + initialRef + ", Initial Stamp: " + initialStamp[0]); + +//Update value and version number +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); + +//Print the updated value and version number +int[] updatedStamp = new int[1]; +String updatedRef = asr.get(updatedStamp); +System.out.println("Updated Reference: " + updatedRef + ", Updated Stamp: " + updatedStamp[0]); + +// Attempt to update with wrong version number +boolean isUpdatedWithWrongStamp = asr.compareAndSet(newRef, "John", oldStamp, newStamp + 1); +System.out.println("Update with Wrong Stamp Success: " + isUpdatedWithWrongStamp); + +//Print the final value and version number +int[] finalStamp = new int[1]; +String finalRef = asr.get(finalStamp); +System.out.println("Final Reference: " + finalRef + ", Final Stamp: " + finalStamp[0]);``` + +The output is as follows: + +```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` class usage example**: + +```java +//Create an AtomicMarkableReference object with an initial value of "SnailClimb" and an initial mark of false +AtomicMarkableReference amr = new AtomicMarkableReference<>("SnailClimb", false); + +//Print initial value and tag +boolean[] initialMark = new boolean[1]; +String initialRef = amr.get(initialMark); +System.out.println("Initial Reference: " + initialRef + ", Initial Mark: " + initialMark[0]); + +// update values and tags +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); + +//Print updated value and tag +boolean[] updatedMark = new boolean[1]; +String updatedRef = amr.get(updatedMark); +System.out.println("Updated Reference: " + updatedRef + ", Updated Mark: " + updatedMark[0]); + +// Attempt to update with wrong tag +boolean isUpdatedWithWrongMark = amr.compareAndSet(newRef, "John", oldMark, !newMark); +System.out.println("Update with Wrong Mark Success: " + isUpdatedWithWrongMark); + +//Print the final value and token +boolean[] finalMark = new boolean[1]; +String finalRef = amr.get(finalMark); +System.out.println("Final Reference: " + finalRef + ", Final Mark: " + finalMark[0]); +``` + +The output is as follows: + +```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 +``` + +## Object attribute modification type atomic class + +If you need to atomically update a field in a class, you need to use the object's attribute modification type atomic class. + +- `AtomicIntegerFieldUpdater`: updater for atomically updating integer fields +- `AtomicLongFieldUpdater`: updater for atomically updating long fields +- `AtomicReferenceFieldUpdater`: an updater that atomically updates fields in reference types + +Updating an object's properties atomically requires two steps. The first step is that because the object's attribute modification type atomic class is an abstract class, you must use the static method newUpdater() to create an updater every time you use it, and you need to set the class and attributes you want to update. In the second step, the updated object properties must use the volatile int modifier. + +The methods provided by the above three classes are almost the same, so we use `AtomicIntegerFieldUpdater` as an example to introduce them here. + +**`AtomicIntegerFieldUpdater` class usage example**: + +```java +// Person class +class Person { + private String name; + // To use AtomicIntegerFieldUpdater, the field must be volatile int + volatile int age; + //Omit getter/setter and toString +} + +//Create AtomicIntegerFieldUpdater object +AtomicIntegerFieldUpdater ageUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age"); + +//Create Person object +Person person = new Person("SnailClimb", 22); + +// print initial value +System.out.println("Initial Person: " + person); + +//Update age field +ageUpdater.incrementAndGet(person); // self-increment +System.out.println("After Increment: " + person); + +ageUpdater.addAndGet(person, 5); // Add 5 +System.out.println("After Adding 5: " + person); + +ageUpdater.compareAndSet(person, 28, 30); // If the current value is 28, set it to 30 +System.out.println("After Compare and Set (28 to 30): " + person); + +// Attempt to update with wrong comparison value +boolean isUpdated = ageUpdater.compareAndSet(person, 28, 35); // This time it should fail +System.out.println("Compare and Set (28 to 35) Success: " + isUpdated); +System.out.println("Final Person: " + person); +``` + +Output result: + +```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 +``` + +## Reference + +- "The Art of Concurrent Programming in Java" + + \ No newline at end of file diff --git a/docs_en/java/concurrent/cas.en.md b/docs_en/java/concurrent/cas.en.md new file mode 100644 index 00000000000..d72f3550363 --- /dev/null +++ b/docs_en/java/concurrent/cas.en.md @@ -0,0 +1,167 @@ +--- +title: CAS detailed explanation +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: CAS, Compare-And-Swap, Unsafe, atomic operation, ABA problem, spin, optimistic locking, atomic class + - - meta + - name: description + content: Analyze the implementation and principles of CAS in Java, covering the atomic operations provided by Unsafe, common issues such as ABA, and comparison with locks. +--- + +For an introduction to optimistic locks and pessimistic locks and common implementation methods of optimistic locks, you can read this article written by the author: [Detailed explanation of optimistic locks and pessimistic locks] (https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html). + +This article mainly introduces: the implementation of CAS in Java and some problems of CAS. + +## How is CAS implemented in Java? + +In Java, a key class that implements CAS (Compare-And-Swap, compare and swap) operations is `Unsafe`. + +The `Unsafe` class is located under the `sun.misc` package and is a class that provides low-level, unsafe operations. Due to its powerful functions and potential dangers, it is usually used inside the JVM or in some libraries that require extremely high performance and low-level access, and is not recommended for use by ordinary 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). + +The `Unsafe` class under the `sun.misc` package provides `compareAndSwapObject`, `compareAndSwapInt` and `compareAndSwapLong` methods to implement CAS operations on `Object`, `int` and `long` types: + +```java +/** + * Atomicly update the value of an object field. + * + * @param o The object to be operated on + * @param offset The memory offset of the object field + * @param expected The expected old value + * @param x the new value to be set + * @return Returns true if the value is successfully updated; otherwise returns false + */ +boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); + +/** + * Atomicly update the value of an object field of type int. + */ +boolean compareAndSwapInt(Object o, long offset, int expected, int x); + +/** + * Atomicly update the value of an object field of type long. + */ +boolean compareAndSwapLong(Object o, long offset, long expected, long x); +``` + +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 underlying hardware instructions to implement atomic operations. In other words, the Java language does not directly implement CAS in Java. + +To be more precise, CAS in Java is implemented in the form of C++ inline assembly and is called through JNI (Java Native Interface). Therefore, the specific implementation of CAS is closely related to the operating system and CPU. + +The `java.util.concurrent.atomic` package provides classes for atomic operations. + +![JUC Atomic Class Overview](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +For the introduction and use of these Atomic atomic classes, you can read this article: [Atomic Atomic Class Summary](https://javaguide.cn/java/concurrent/atomic-classes.html). + +Atomic classes rely on CAS optimistic locking to guarantee the atomicity of their methods without using traditional locking mechanisms (such as `synchronized` blocks or `ReentrantLock`). + +`AtomicInteger` is one of Java's atomic classes. It is mainly used to perform atomic operations on variables of type `int`. It uses the low-level atomic operation methods provided by the `Unsafe` class to achieve lock-free thread safety. + +Below, we explain how Java uses the methods of the `Unsafe` class to implement atomic operations by interpreting the core source code of `AtomicInteger` (JDK1.8). + +The core source code of `AtomicInteger` is as follows: + +```java +// Get Unsafe instance +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + // 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); } +} +// Ensure visibility of "value" field +private volatile int value; + +// If the current value is equal to the expected value, atomically set the value to newValue +// Use the Unsafe#compareAndSwapInt method to perform CAS operations +public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} + +// Atomicly add delta to the current value and return the old value +public final int getAndAdd(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta); +} + +// Atomicly increase the current value by 1 and return the value before addition (old value) +// Use the Unsafe#getAndAddInt method to perform CAS operations. +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +// Atomicly decrement the current value by 1 and return the value before decrement (old value) +public final int getAndDecrement() { + return unsafe.getAndAddInt(this, valueOffset, -1); +} +``` + +`Unsafe#getAndAddInt` source code: + +```java +// Atomically get and increment an integer value +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + // Get the integer value of object o at memory offset offset in volatile mode + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + //return old value + return v; +} +``` + +As you can see, `getAndAddInt` uses a `do-while` loop: when the `compareAndSwapInt` operation fails, it will be retried until it succeeds. That is to say, the `getAndAddInt` method will try to update the value of `value` through the `compareAndSwapInt` method. If the update fails (the current value is modified by other threads during this period), it will re-obtain the current value and try to update again until the operation is successful. + +Since CAS operations may fail due to concurrency conflicts, they are usually used with a while loop to retry after failure until the operation succeeds. This is the **Spin Lock Mechanism**. + +## What are the problems with the CAS algorithm? + +The ABA problem is the most common problem with CAS algorithms. + +### ABA Questions + +If a variable V has the value A when it is first read, and it is checked that it is still the value A when preparing to assign it, can we prove that its value has not been modified by other threads? Obviously it cannot, because during this period its value may be changed to other values, and then changed back to A, then the CAS operation will mistakenly think that it has never been modified. This problem is known as the "ABA" problem of CAS operations. **The solution to the ABA problem is to append a version number or timestamp in front of the variable. The `AtomicStampedReference` class after JDK 1.5 is used to solve the ABA problem. The `compareAndSet()` method is to first check whether the current reference is equal to the expected reference, and whether the current flag is equal to the expected flag. If all are equal, the value of the reference and the flag is atomically set to the given update value. + +```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))); +} +``` + +### Long cycle time and high overhead + +CAS often uses spin operations to retry, that is, if it fails, it will continue to loop until it succeeds. If it is unsuccessful for a long time, it will bring very large execution overhead to the CPU. + +If the JVM can support the `pause` instruction provided by the processor, the efficiency of the spin operation will be improved. The `pause` directive has two important functions: + +1. **Delay pipeline execution instructions**: The `pause` instruction can delay the execution of instructions, thereby reducing CPU resource consumption. The exact latency depends on the processor implementation and may be zero on some processors. +2. **Avoid memory order conflicts**: When exiting the loop, the `pause` instruction can avoid the CPU pipeline being cleared due to memory order conflicts, thereby improving the CPU execution efficiency. + +### Only atomic operations on a shared variable can be guaranteed + +CAS operations are only valid on a single shared variable. CAS is powerless when multiple shared variables need to be manipulated. However, starting from JDK 1.5, Java provides the `AtomicReference` class, which allows us to ensure atomicity between reference objects. By encapsulating multiple variables in a single object, we can use `AtomicReference` to perform CAS operations. + +In addition to `AtomicReference`, locking can also be used to ensure this. + +## Summary + +In Java, CAS is implemented through `native` methods in the `Unsafe` class, which call underlying 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. + +Although CAS has efficient lock-free features, it also needs to pay attention to issues such as ABA, long cycle time and high overhead. \ No newline at end of file diff --git a/docs_en/java/concurrent/completablefuture-intro.en.md b/docs_en/java/concurrent/completablefuture-intro.en.md new file mode 100644 index 00000000000..7cd0a454512 --- /dev/null +++ b/docs_en/java/concurrent/completablefuture-intro.en.md @@ -0,0 +1,735 @@ +--- +title: CompletableFuture 详解 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: CompletableFuture,异步编排,并行任务,thenCompose,thenCombine,allOf,anyOf,线程池,Future + - - meta + - name: description + content: 介绍 CompletableFuture 的核心概念与常用 API,涵盖并行执行、任务编排与结果聚合,助力高性能接口设计。 +--- + +实际项目中,一个接口可能需要同时获取多种不同的数据,然后再汇总返回,这种场景还是挺常见的。举个例子:用户请求获取订单信息,可能需要同时获取用户信息、商品详情、物流信息、商品推荐等数据。 + +如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速度会非常慢。考虑到这些任务之间有大部分都是 **无前后顺序关联** 的,可以 **并行执行** ,就比如说调用获取商品详情的时候,可以同时调用获取物流信息。通过并行执行多个任务的方式,接口的响应速度会得到大幅优化。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel.png) + +对于存在前后调用顺序关系的任务,可以进行任务编排。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel2.png) + +1. 获取用户信息之后,才能调用商品详情和物流信息接口。 +2. 成功获取商品详情和物流信息之后,才能调用商品推荐接口。 + +可能会用到多线程异步任务编排的场景(这里只是举例,数据不一定是一次返回,可能会对接口进行拆分): + +1. 首页:例如技术社区的首页可能需要同时获取文章推荐列表、广告栏、文章排行榜、热门话题等信息。 +2. 详情页:例如技术社区的文章详情页可能需要同时获取作者信息、文章详情、文章评论等信息。 +3. 统计模块:例如技术社区的后台统计模块可能需要同时获取粉丝数汇总、文章数据(阅读量、评论量、收藏量)汇总等信息。 + +对于 Java 程序来说,Java 8 才被引入的 `CompletableFuture` 可以帮助我们来做多个任务的编排,功能非常强大。 + +这篇文章是 `CompletableFuture` 的简单入门,带大家看看 `CompletableFuture` 常用的 API。 + +## 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` 那里直接取出任务执行结果。 + +## 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)`:多了一个超时时间。 + +`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 + +`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。 + +![](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<>(); +``` + +Suppose that at some point in the future, we get the final result. At this time, we can call the `complete()` method to pass in the result, which means that `resultFuture` has been completed. + +```java +// The complete() method can only be called once, subsequent calls will be ignored. +resultFuture.complete(rpcResponse); +``` + +You can check if it's done with the `isDone()` method. + +```java +public boolean isDone() { + return result != null; +} +``` + +Obtaining the results of asynchronous calculation is also very simple, just call the `get()` method directly. The thread calling the `get()` method will block until the `CompletableFuture` completes the operation. + +```java +rpcResponse = completableFuture.get(); +``` + +If you already know the result of the calculation, you can use the static method `completedFuture()` to create a `CompletableFuture`. + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!"); +assertEquals("hello!", future.get()); +``` + +The underlying method of `completedFuture()` calls the new method with parameters, but this method is not exposed to the outside world. + +```java +public static CompletableFuture completedFuture(U value) { + return new CompletableFuture((value == null) ? NIL : value); +} +``` + +#### Static factory method + +These two methods can help us encapsulate calculation logic. + +```java +static CompletableFuture supplyAsync(Supplier supplier); +// Use a custom thread pool (recommended) +static CompletableFuture supplyAsync(Supplier supplier, Executor executor); +static CompletableFuture runAsync(Runnable runnable); +// Use a custom thread pool (recommended) +static CompletableFuture runAsync(Runnable runnable, Executor executor); +``` + +The parameter accepted by the `runAsync()` method is `Runnable`, which is a functional interface and does not allow return values. You can use the `runAsync()` method when you need asynchronous operations and don't care about the return result. + +```java +@FunctionalInterface +public interface Runnable { + public abstract void run(); +} +``` + +The parameter accepted by the `supplyAsync()` method is `Supplier`, which is also a functional interface. `U` is the type of the returned result value. + +```java +@FunctionalInterface +public interface Supplier { + + /** + * Gets a result. + * + * @return a result + */ + T get(); +} +``` + +When you need asynchronous operations and care about the return result, you can use the `supplyAsync()` method. + +```java +CompletableFuture future = CompletableFuture.runAsync(() -> System.out.println("hello!")); +future.get();// Output "hello!" +CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "hello!"); +assertEquals("hello!", future2.get()); +``` + +### Processing the results of asynchronous settlement + +After we obtain the results of asynchronous calculation, we can further process them. The more commonly used methods are as follows: + +- `thenApply()` +- `thenAccept()` +- `thenRun()` +- `whenComplete()` + +The `thenApply()` method accepts a `Function` instance and uses it to process the result. + +```java +//Inherit the thread pool of the previous task +public CompletableFuture thenApply( + Function fn) { + return uniApplyStage(null, fn); +} + +//Use the default ForkJoinPool thread pool (not recommended) +public CompletableFuture thenApplyAsync( + Function fn) { + return uniApplyStage(defaultExecutor(), fn); +} +// Use a custom thread pool (recommended) +public CompletableFuture thenApplyAsync( + Function fn, Executor executor) { + return uniApplyStage(screenExecutor(executor), fn); +} +``` + +Examples of using the `thenApply()` method are as follows: + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!"); +assertEquals("hello!world!", future.get()); +// This call will be ignored. +future.thenApply(s -> s + "nice!"); +assertEquals("hello!world!", future.get()); +``` + +You can also make **streaming calls**: + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!").thenApply(s -> s + "nice!"); +assertEquals("hello!world!nice!", future.get()); +``` + +**If you don’t need to get the return result from the callback function, you can use `thenAccept()` or `thenRun()`. The difference between these two methods is that `thenRun()` cannot access the results of asynchronous calculations. ** + +The parameter of the `thenAccept()` method is `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); +} +``` + +As the name suggests, `Consumer` is a consumer interface, which can receive an input object and then "consume" it. + +```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); +}``` + +Examples of usage of the `thenCompose()` method are as follows: + +```java +CompletableFuture future + = CompletableFuture.supplyAsync(() -> "hello!") + .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!")); +assertEquals("hello!world!", future.get()); +``` + +In actual development, this method is still very useful. For example, task1 and task2 are both executed asynchronously, but task1 must be executed before task2 can be started (task2 depends on the execution result of task1). + +Similar to the `thenCompose()` method, there is the `thenCombine()` method, which can also combine two `CompletableFuture` objects. + +```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()); +``` + +**What is the difference between `thenCompose()` and `thenCombine()`? ** + +- `thenCompose()` can link two `CompletableFuture` objects and use the return result of the previous task as the parameter of the next task. There is a sequence between them. +- `thenCombine()` will combine the results of the two tasks after both tasks are executed. The two tasks are executed in parallel, and there is no sequential dependency between them. + +In addition to `thenCompose()` and `thenCombine()`, there are some other methods of combining `CompletableFuture` to achieve different effects and meet different business needs. + +For example, if we want to execute task3 after any one of task1 and task2 is executed, we can use `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); +} +``` + +Just give a simple example: + +```java +CompletableFuture task = CompletableFuture.supplyAsync(() -> { + System.out.println("Task 1 starts execution, current time: " + System.currentTimeMillis()); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Task 1 has been executed, current time: " + System.currentTimeMillis()); + return "task1"; +}); + +CompletableFuture task2 = CompletableFuture.supplyAsync(() -> { + System.out.println("Task 2 starts execution, current time: " + System.currentTimeMillis()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Task 2 has been executed, current time: " + System.currentTimeMillis()); + return "task2"; +}); + +task.acceptEitherAsync(task2, (res) -> { + System.out.println("Task 3 starts execution, current time: " + System.currentTimeMillis()); + System.out.println("The result of the previous task is: " + res); +}); + +//Add some delay time to ensure that the asynchronous task has enough time to complete +try { + Thread.sleep(2000); +} catch (InterruptedException e) { + e.printStackTrace(); +} +``` + +Output: + +```plain +Task 1 starts execution, current time: 1695088058520 +Task 2 starts execution, current time: 1695088058521 +Task 1 has been executed. Current time: 1695088059023 +Task 3 starts execution, current time: 1695088059023 +The result of the previous task is: task1 +Task 2 has been executed. Current time: 1695088059523 +``` + +The task combination operation `acceptEitherAsync()` will trigger the execution of task 3 when either asynchronous task 1 or asynchronous task 2 is completed, but it should be noted that this triggering time is uncertain. If both task 1 and task 2 have not been completed, then task 3 cannot be executed. + +### Run multiple CompletableFutures in parallel + +You can run multiple `CompletableFuture` in parallel through the `allOf()` static method of `CompletableFuture`. + +In actual projects, we often need to run multiple unrelated tasks in parallel. There is no dependency between these tasks and they can run independently of each other. + +For example, let's say we have to read and process 6 files. These 6 tasks are all tasks that have no execution order dependence, but we need to statistically organize the results of processing these files when returning them to the user. In this case, we can use multiple `CompletableFuture` to run in parallel to handle it. + +The sample code is as follows: + +```java +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //Customized business operations + }); +... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //Customized business operations + }); +... + CompletableFuture headerFuture=CompletableFuture.allOf(task1,....,task6); + + try { + headerFuture.join(); + } catch (Exception ex) { + ... + } +System.out.println("all done. "); +``` + +The `anyOf()` method is often compared to the `allOf()` method. + +**The `allOf()` method will wait until all `CompletableFuture` are completed before returning** + +```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"; +});``` + +Calling `join()` allows the program to wait for `future1` and `future2` to finish running before continuing. + +```java +CompletableFuture completableFuture = CompletableFuture.allOf(future1, future2); +completableFuture.join(); +assertTrue(completableFuture.isDone()); +System.out.println("all futures done..."); +``` + +Output: + +```plain +future1 done... +future2 done... +all futures done... +``` + +The **`anyOf()` method will not wait for all `CompletableFuture` to be executed before returning, as long as one execution is completed! ** + +```java +CompletableFuture f = CompletableFuture.anyOf(future1, future2); +System.out.println(f.get()); +``` + +The output may be: + +```plain +future2 done... +efg +``` + +It could also be: + +```plain +future1 done... +abc +``` + +## CompletableFuture usage suggestions + +### Use custom thread pool + +In our code examples above, for convenience, we did not choose to customize the thread pool. In actual projects, this is not advisable. + +`CompletableFuture` uses the globally shared `ForkJoinPool.commonPool()` as the executor by default, and all asynchronous tasks that do not specify an executor will use this thread pool. This means that if an application, multiple libraries or frameworks (such as Spring, third-party libraries) all depend on `CompletableFuture`, they will all share the same thread pool by default. + +Although `ForkJoinPool` is very efficient, when a large number of tasks are submitted at the same time, it may cause resource contention and thread starvation, thereby affecting system performance. + +To avoid these problems, it is recommended to provide a custom thread pool for `CompletableFuture`, which brings the following advantages: + +- **Isolation**: Allocate independent thread pools for different tasks to avoid global thread pool resource contention. +- **Resource Control**: Adjust the thread pool size and queue type according to task characteristics to optimize performance. +- **Exception Handling**: Better handle exceptions in threads by customizing `ThreadFactory`. + +```java +private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + +CompletableFuture.runAsync(() -> { + //... +}, executor); +``` + +### Try to avoid using get() + +The `get()` method of `CompletableFuture` is blocking, so try to avoid using it. If you must use it, you need to add a timeout, otherwise it may cause the main thread to wait forever and be unable to perform other tasks. + +```java + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(10_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "Hello, world!"; + }); + + // Get the return value of the asynchronous task and set the timeout to 5 seconds + try { + String result = future.get(5, TimeUnit.SECONDS); + System.out.println(result); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + // Handle exceptions + e.printStackTrace(); + } +} +``` + +The above code throws a `TimeoutException` exception when calling `get()`. In this way, we can perform corresponding operations in exception handling, such as canceling tasks, retrying tasks, recording logs, etc. + +### Correct exception handling + +When using `CompletableFuture`, exceptions must be handled in the correct way to avoid exceptions being lost or uncontrollable problems. + +Here are some suggestions: + +- Use the `whenComplete` method to trigger the callback function when the task is completed and handle the exception correctly instead of letting the exception be swallowed or lost. +- Use the `exceptionally` method to handle exceptions and rethrow them so that they propagate to subsequent stages rather than having them ignored or terminated. +- Use the `handle` method to handle normal return results and exceptions, and return a new result instead of letting exceptions affect normal business logic. +- Use the `CompletableFuture.allOf` method to combine multiple `CompletableFuture` and handle exceptions for all tasks uniformly, instead of making exception handling too lengthy or repetitive. +-… + +### Reasonably combine multiple asynchronous tasks + +Correctly use `thenCompose()`, `thenCombine()`, `acceptEither()`, `allOf()`, `anyOf()` and other methods to combine multiple asynchronous tasks to meet actual business needs and improve program execution efficiency. + +In actual use, we can also make use of or refer to ready-made asynchronous task orchestration frameworks, such as JD.com's [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool). + +![asyncTool README document](https://oss.javaguide.cn/github/javaguide/java/concurrent/asyncTool-readme.png) + +## Postscript + +This article only briefly introduces the core concepts of `CompletableFuture` and some of the more commonly used APIs. If you want to study in depth, you can also find more books and blogs to read. For example, the following articles are quite good: + +- [CompletableFuture Principles and Practices - Asynchronousization of Takeaway Merchant APIs - Meituan Technical Team](https://tech.meituan.com/2022/05/12/principles-and-practices-of-completablefuture.html): This article details the use of `CompletableFuture` in actual projects. By referring to this article, you can optimize similar scenarios in the project, which is also a small highlight. This performance optimization method is relatively simple and the effect is pretty good! +- [Read RocketMQ source code and learn the three major artifacts of concurrent programming - Yong Ge's java practical sharing] (https://mp.weixin.qq.com/s/32Ak-WFLynQfpn0Cg0N-0A): This article introduces RocketMQ's application of `CompletableFuture`. Specifically, starting from RocketMQ 4.7, RocketMQ introduced `CompletableFuture` to implement asynchronous message processing. + +In addition, it is recommended that G friends take a look at JD.com’s [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) concurrency framework, which uses `CompletableFuture` extensively. + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-concurrent-collections.en.md b/docs_en/java/concurrent/java-concurrent-collections.en.md new file mode 100644 index 00000000000..a61f73d8ae7 --- /dev/null +++ b/docs_en/java/concurrent/java-concurrent-collections.en.md @@ -0,0 +1,168 @@ +--- +title: Java 常见并发容器总结 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: 并发容器,ConcurrentHashMap,CopyOnWriteArrayList,ConcurrentLinkedQueue,BlockingQueue,ConcurrentSkipListMap,JUC + - - meta + - name: description + content: 总览 JUC 并发容器及特性,涵盖线程安全 Map、读多写少 List、非阻塞队列与阻塞队列、跳表等常用数据结构。 +--- + +JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 + +- **`ConcurrentHashMap`** : 线程安全的 `HashMap` +- **`CopyOnWriteArrayList`** : 线程安全的 `List`,在读多写少的场合性能非常好,远远好于 `Vector`。 +- **`ConcurrentLinkedQueue`** : 高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。 +- **`BlockingQueue`** : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 +- **`ConcurrentSkipListMap`** : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 + +## ConcurrentHashMap + +我们知道,`HashMap` 是线程不安全的,如果在并发场景下使用,一种常见的解决方式是通过 `Collections.synchronizedMap()` 方法对 `HashMap` 进行包装,使其变为线程安全。不过,这种方式是通过一个全局锁来同步不同线程间的并发访问,会导致严重的性能瓶颈,尤其是在高并发场景下。 + +为了解决这一问题,`ConcurrentHashMap` 应运而生,作为 `HashMap` 的线程安全版本,它提供了更高效的并发处理能力。 + +在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 + +![Java7 ConcurrentHashMap 存储结构](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)))。 + +Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 + +![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) + +关于 `ConcurrentHashMap` 的详细介绍,请看我写的这篇文章:[`ConcurrentHashMap` 源码分析](./../collection/concurrent-hash-map-source-code.md)。 + +## CopyOnWriteArrayList + +在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 + +JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。 + +对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 + +这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 + +`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 + +当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 + +关于 `CopyOnWriteArrayList` 的详细介绍,请看我写的这篇文章:[`CopyOnWriteArrayList` 源码分析](./../collection/copyonwritearraylist-source-code.md)。 + +## ConcurrentLinkedQueue + +Java 提供的线程安全的 `Queue` 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 `BlockingQueue`,非阻塞队列的典型例子是 `ConcurrentLinkedQueue`,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** + +从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.`ConcurrentLinkedQueue` 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 + +`ConcurrentLinkedQueue` 内部代码我们就不分析了,大家知道 `ConcurrentLinkedQueue` 主要使用 CAS 非阻塞算法来实现线程安全就好了。 + +`ConcurrentLinkedQueue` 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 `ConcurrentLinkedQueue` 来替代。 + +## BlockingQueue + +### BlockingQueue 简介 + +上面我们己经提到了 `ConcurrentLinkedQueue` 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——`BlockingQueue`。阻塞队列(`BlockingQueue`)被广泛使用在“生产者-消费者”问题中,其原因是 `BlockingQueue` 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 + +`BlockingQueue` 是一个接口,继承自 `Queue`,所以其实现类也可以作为 `Queue` 的实现来使用,而 `Queue` 又继承自 `Collection` 接口。下面是 `BlockingQueue` 的相关实现类: + +![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/51622268.jpg) + +下面主要介绍一下 3 个常见的 `BlockingQueue` 的实现类:`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue` 。 + +### ArrayBlockingQueue + +`ArrayBlockingQueue` 是 `BlockingQueue` 接口的有界队列实现类,底层采用数组来实现。 + +```java +public class ArrayBlockingQueue +extends AbstractQueue +implements BlockingQueue, Serializable{} +``` + +Once an `ArrayBlockingQueue` is created, its capacity cannot be changed. Its concurrency control uses the reentrant lock `ReentrantLock`. Whether it is an insertion operation or a read operation, the lock needs to be obtained before the operation can be performed. When the queue is full, trying to put an element into the queue will cause the operation to block; trying to take an element from an empty queue will also block. + +By default, `ArrayBlockingQueue` cannot guarantee the fairness of thread access to the queue. The so-called fairness means strictly following the absolute time order of thread waiting, that is, the thread waiting first can access `ArrayBlockingQueue` first. Unfairness means that the order of accessing `ArrayBlockingQueue` does not follow strict time order. It is possible that when `ArrayBlockingQueue` can be accessed, threads blocked for a long time still cannot access `ArrayBlockingQueue`. If fairness is guaranteed, throughput will usually be reduced. If you need to obtain a fair `ArrayBlockingQueue`, you can use the following code: + +```java +private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); +``` + +### LinkedBlockingQueue + +The underlying blocking queue of `LinkedBlockingQueue` is implemented based on **one-way linked list**. It can be used as an unbounded queue or a bounded queue. It also meets the characteristics of FIFO and has higher throughput than `ArrayBlockingQueue`. In order to prevent the capacity of `LinkedBlockingQueue` from rapidly increasing and consuming a large amount of memory. Usually when creating a `LinkedBlockingQueue` object, its size is specified. If not specified, the capacity is equal to `Integer.MAX_VALUE`. + +**Related construction methods:** + +```java + /** + *Unbounded queue in a sense + * Creates a {@code LinkedBlockingQueue} with a capacity of + * {@link Integer#MAX_VALUE}. + */ + public LinkedBlockingQueue() { + this(Integer.MAX_VALUE); + } + + /** + *bounded queue + * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. + * + * @param capacity the capacity of this queue + * @throws IllegalArgumentException if {@code capacity} is not greater + * than zero + */ + public LinkedBlockingQueue(int capacity) { + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + last = head = new Node(null); + } +``` + +### PriorityBlockingQueue + +`PriorityBlockingQueue` is an unbounded blocking queue that supports priority. By default, elements are sorted in natural order. You can also implement the `compareTo()` method in a custom class to specify the sorting rules for elements, or pass the constructor parameter `Comparator` to specify the sorting rules during initialization. + +`PriorityBlockingQueue` concurrency control uses a reentrant lock `ReentrantLock`, the queue is an unbounded queue (`ArrayBlockingQueue` is a bounded queue, `LinkedBlockingQueue` can also be passed in `capacity` in the constructor to specify the maximum capacity of the queue, but `PriorityBlockingQueue` You can only specify the initial queue size. When elements are inserted later, **it will automatically expand if there is not enough space**). + +Simply put, it is a thread-safe version of `PriorityQueue`. Null values ​​cannot be inserted. At the same time, the object inserted into the queue must be of comparable size (comparable), otherwise a `ClassCastException` exception will be reported. Its insertion operation put method will not block because it is an unbounded queue (take method will block when the queue is empty). + +**Recommended article:** ["Interpretation of Java Concurrent Queue BlockingQueue"](https://javadoop.com/post/java-concurrent-queue) + +## ConcurrentSkipListMap + +> The following content refers to the Geek Time column ["The Beauty of Data Structure and Algorithm"](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster ""The Beauty of Data Structure and Algorithm") and "Practical Java High Concurrency Programming". + +In order to introduce `ConcurrentSkipListMap`, let me first give you a brief understanding of skip lists. + +For a singly linked list, even if the linked list is ordered, if we want to find certain data in it, we can only traverse the linked list from beginning to end, so the efficiency will naturally be very low, and skipping the list is different. A skip table is a data structure that can be used for quick lookups, somewhat similar to a balanced tree. Both of them can quickly find elements. But an important difference is: insertion and deletion of the balanced tree is often likely to cause a global adjustment of the balanced tree. Insertion and deletion of jump tables only require partial operations on the entire data structure. The advantage of this is: in the case of high concurrency, you will need a global lock to ensure the thread safety of the entire balanced tree. For skip tables, you only need partial locks. In this way, you can have better performance in a high-concurrency environment. In terms of query performance, the time complexity of skip tables is also **O(logn)**. Therefore, in concurrent data structures, JDK uses skip tables to implement a Map. + +The essence of a skip list is that multiple linked lists are maintained at the same time, and the linked lists are hierarchical. + +![Level 2 index skip table](https://oss.javaguide.cn/github/javaguide/java/93666217.jpg) + +The lowest level linked list maintains all the elements in the jump list, and each upper level linked list is a subset of the lower level. + +All linked list elements in the jump list are sorted. When searching, you can start from the top-level linked list. Once it is found that the element being searched is smaller than the successor node of the currently visited node (or the successor node is empty), it will be transferred to the next level of the linked list to continue searching. This means that during the search process, the search is jumping. As shown above, find element 18 in the skip list. + +![Find elements in skip list 18](https://oss.javaguide.cn/github/javaguide/java/32005738.jpg) + +When searching for 18, it originally needed to be traversed 18 times, but now it only needs 7 times. When the length of the linked list is relatively large, the improvement in index search efficiency will be very obvious. + +It is easy to see from the above that the jump table is an algorithm that uses space to exchange time. ** + +Another difference between using a skip list to implement `Map` and using a hash algorithm to implement `Map` is that hashing does not preserve the order of elements, while all elements in a skip list are sorted. So when traversing the jump list, you will get an ordered result. Therefore, if your application requires orderliness, then skip lists are your best choice. The class in the JDK that implements this data structure is `ConcurrentSkipListMap`. + +## Reference + +- "Practical Java High Concurrency Programming" +- +- + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-concurrent-questions-01.en.md b/docs_en/java/concurrent/java-concurrent-questions-01.en.md new file mode 100644 index 00000000000..e71cafbef58 --- /dev/null +++ b/docs_en/java/concurrent/java-concurrent-questions-01.en.md @@ -0,0 +1,404 @@ +--- +title: Java并发常见面试题总结(上) +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: 线程和进程,并发和并行,多线程,死锁,线程的生命周期 + - - meta + - name: description + content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助! +--- + + + +## 线程 + +### ⭐️什么是线程和进程? + +#### 何为进程? + +进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 + +在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 + +如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(`.exe` 文件的运行)。 + +![进程示例图片-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) + +#### 何为线程? + +线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 + +Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。 + +```java +public class MultiThread { + public static void main(String[] args) { + // 获取 Java 线程管理 MXBean + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 + ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); + // 遍历线程信息,仅打印线程 ID 和线程名称信息 + for (ThreadInfo threadInfo : threadInfos) { + System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); + } + } +} +``` + +上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): + +```plain +[5] Attach Listener //添加事件 +[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 +[3] Finalizer //调用对象 finalize 方法的线程 +[2] Reference Handler //清除 reference 线程 +[1] main //main 线程,程序入口 +``` + +从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 + +### Java 线程和操作系统的线程有啥区别? + +JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。 + +我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下: + +- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。 +- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。 + +顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。 + +一句话概括 Java 线程和操作系统线程的关系:**现在的 Java 线程的本质其实就是操作系统的线程**。 + +线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种: + +1. 一对一(一个用户线程对应一个内核线程) +2. 多对一(多个用户线程映射到一个内核线程) +3. 多对多(多个用户线程映射到多个内核线程) + +![常见的三种线程模型](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)。 + +### ⭐️请简要描述线程与进程的关系,区别及优缺点? + +下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。 + +![Java 运行时数据区域(JDK1.8 之后)](https://oss.javaguide.cn/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png) + +从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 + +**总结:** 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。 + +下面是该知识点的扩展内容! + +下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? + +#### 程序计数器为什么是私有的? + +程序计数器主要有下面两个作用: + +1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 + +所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 + +#### 虚拟机栈和本地方法栈为什么是私有的? + +- **虚拟机栈:** 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 + +所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 + +#### 一句话简单了解堆和方法区 + +堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 + +### 如何创建线程? + +一般来说,创建线程有很多种方式,例如继承`Thread`类、实现`Runnable`接口、实现`Callable`接口、使用线程池、使用`CompletableFuture`类等等。 + +不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。 + +严格来说,Java 就只有一种方式可以创建线程,那就是通过`new Thread().start()`创建。不管是哪种方式,最终还是依赖于`new Thread().start()`。 + +### ⭐️说说线程的生命周期和状态? + +Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态: + +- NEW: 初始状态,线程被创建出来但没有被调用 `start()` 。 +- RUNNABLE: 运行状态,线程被调用了 `start()`等待运行的状态。 +- BLOCKED:阻塞状态,需要等待锁释放。 +- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。 +- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。 +- TERMINATED:终止状态,表示该线程已经运行完毕。 + +线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。 + +Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误](https://mp.weixin.qq.com/s/0UTyrJpRKaKhkhHcQtXAiA)): + +![Java 线程状态变迁图](https://oss.javaguide.cn/github/javaguide/java/concurrent/640.png) + +由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 + +> 在操作系统层面,线程有 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(运行中)** 状态 。 +> +> **为什么 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 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。 + +![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(终止)** 状态。 + +### 什么是线程上下文切换? + +线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。 + +- 主动让出 CPU,比如调用了 `sleep()`, `wait()` 等。 +- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。 +- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。 +- 被终止或结束运行 + +这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 **上下文切换**。 + +上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 + +### Thread#sleep() 方法和 Object#wait() 方法对比 + +**共同点**:两者都可以暂停线程的执行。 + +**区别**: + +- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 +- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。 +- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 +- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢?下一个问题就会聊到。 + +### 为什么 wait() 方法不定义在 Thread 中? + +`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。 + +类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?** + +因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。 + +### 可以直接调用 Thread 类的 run 方法吗? + +这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个普通方法在调用该方法的线程去执行,所以这并不是多线程工作。 + +**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** + +## 多线程 + +### 并发与并行的区别 + +- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。 +- **并行**:两个及两个以上的作业在同一 **时刻** 执行。 + +最关键的点是:是否是 **同时** 执行。 + +### 同步和异步的区别 + +- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。 +- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。 + +### ⭐️为什么要使用多线程? + +先从总体上来说: + +- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + +再深入到计算机底层来探讨: + +- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 +- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 + +### ⭐️单核 CPU 支持 Java 多线程吗? + +单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。 + +这里顺带提一下 Java 使用的线程调度方式。 + +操作系统主要通过两种线程调度方式来管理多线程的执行: + +- **抢占式调度(Preemptive Scheduling)**:操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。 +- **协同式调度(Cooperative Scheduling)**:线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。 + +Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。 + +### ⭐️单核 CPU 上运行多个线程效率一定会高吗? + +单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程: + +1. **CPU 密集型**:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。 +2. **IO 密集型**:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。 + +在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。 + +因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。 + +### 使用多线程可能带来什么问题? + +并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 + +### 如何理解线程安全和不安全? + +线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。 + +- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。 +- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。 + +## ⭐️死锁 + +### 什么是线程死锁? + +线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 + +如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![线程死锁示意图 ](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811.png) + +下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): + +```java +public class DeadLockDemo { + private static Object resource1 = new Object();//资源 1 + private static Object resource2 = new Object();//资源 2 + + public static void main(String[] args) { + new Thread(() -> { + synchronized (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"); + synchronized (resource2) { + System.out.println(Thread.currentThread() + "get resource2"); + } + } + }, "线程 1").start(); + + new Thread(() -> { + synchronized (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"); + synchronized (resource1) { + System.out.println(Thread.currentThread() + "get resource1"); + } + } + }, "线程 2").start(); + } +} +``` + +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 +``` + +线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过 `Thread.sleep(1000);` 让线程 A 休眠 1s,为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 + +上面的例子符合产生死锁的四个必要条件: + +1. **互斥条件**:该资源任意一个时刻只由一个线程占用。 +2. **请求与保持条件**:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 +3. **不剥夺条件**:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +4. **循环等待条件**:若干线程之间形成一种头尾相接的循环等待资源关系。 + +### 如何检测死锁? + +- Use `jmap`, `jstack` and other commands to view the JVM thread stack and heap memory. If there is a deadlock, the output of `jstack` will usually have the words `Found one Java-level deadlock:`, followed by deadlock-related thread information. In addition, in actual projects, you can also use `top`, `df`, `free` and other commands to check the basic situation of the operating system. Deadlocks may cause excessive consumption of resources such as CPU and memory. +- Use VisualVM, JConsole and other tools for troubleshooting. + +Here we take the JConsole tool as an example for demonstration. + +First, we need to find the bin directory of the JDK, find jconsole and double-click to open it. + +![jconsole](https://oss.javaguide.cn/github/javaguide/java/concurrent/jdk-home-bin-jconsole.png) + +For MAC users, you can view the JDK installation directory through `/usr/libexec/java_home -V`, and then open it through `open . + folder address`. For example, the path to one of my local JDKs is: + +```bash + open ./Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home +``` + +After opening jconsole, connect to the corresponding program, then enter the thread interface and select detect deadlock! + +![jconsole detects deadlock](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock.png) + +![jconsole detected deadlock](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock-done.png) + +### How to prevent and avoid thread deadlock? + +**How to prevent deadlock? ** Just destroy the necessary conditions for the occurrence of deadlock: + +1. **Destroy request and retention conditions**: Apply for all resources at once. +2. **Destruction and non-deprivation conditions**: When a thread that occupies some resources further applies for other resources, if it cannot apply, it can actively release the resources it occupies. +3. **Destruction of loop waiting conditions**: Prevented by applying for resources in order. Resources are requested in a certain order, and resources are released in reverse order. Break the loop wait condition. + +**How ​​to avoid deadlock? ** + +Avoiding deadlock is to use algorithms (such as banker's algorithm) to calculate and evaluate resource allocation when allocating resources to bring them into a safe state. + +> **Safe state** means that the system can allocate the required resources to each thread according to a certain thread advancement sequence (P1, P2, P3...Pn) until the maximum demand for resources of each thread is met, so that each thread can complete successfully. The sequence `` is called a safe sequence. + +We modify the code of thread 2 to the following so that deadlock will not occur. + +```java +new Thread(() -> { + synchronized (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"); + synchronized (resource2) { + System.out.println(Thread.currentThread() + "get resource2"); + } + } + }, "Thread 2").start(); +``` + +Output: + +```plain +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 +``` + +Let’s analyze why the above code avoids deadlock? + +Thread 1 first obtains the monitor lock of resource1. At this time, thread 2 cannot obtain it. Then thread 1 acquires the monitor lock of resource2 and can obtain it. Then thread 1 releases the monitor lock occupation of resource1 and resource2, and thread 2 can execute after acquiring it. This breaks the loop wait condition and therefore avoids deadlock. + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-concurrent-questions-02.en.md b/docs_en/java/concurrent/java-concurrent-questions-02.en.md new file mode 100644 index 00000000000..0e893a6dbd4 --- /dev/null +++ b/docs_en/java/concurrent/java-concurrent-questions-02.en.md @@ -0,0 +1,915 @@ +--- +title: Summary of Common Java Concurrency Interview Questions (Part 2) +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: multi-threading, deadlock, synchronized, ReentrantLock, volatile, ThreadLocal, thread pool, CAS, AQS + - - meta + - name: description + content: Summary of common Java concurrency knowledge points and interview questions (including detailed answers). +--- + + + +## ⭐️JMM (Java Memory Model) + +There are many and important issues related to JMM (Java Memory Model), so I extracted a separate article to summarize the knowledge points and issues related to JMM: [JMM (Java Memory Model) Detailed Explanation](https://javaguide.cn/java/concurrent/jmm.html). + +## ⭐️volatile keyword + +### How to ensure the visibility of variables? + +In Java, the `volatile` keyword can ensure the visibility of a variable. If we declare a variable as **`volatile`**, this indicates to the JVM that this variable is shared and unstable and will be read from the main memory every time it is used. + +![JMM(Java Memory Model)](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) + +![JMM (Java Memory Model) forces reading in main memory](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm2.png) + +The `volatile` keyword is actually not unique to the Java language, it is also found in the C language. Its original meaning is to disable the CPU cache. If we use `volatile` to modify a variable, this indicates to the compiler that this variable is shared and unstable, and will be read from main memory every time it is used. + +The `volatile` keyword guarantees the visibility of the data, but does not guarantee the atomicity of the data. The `synchronized` keyword guarantees both. + +### How to disable instruction reordering? + +**In Java, in addition to ensuring the visibility of variables, the `volatile` keyword also plays an important role in preventing JVM instruction reordering. ** If we declare a variable as **`volatile`**, when reading and writing this variable, a specific **memory barrier** will be inserted to prevent instruction reordering. + +In Java, the `Unsafe` class provides three memory barrier-related methods out of the box, shielding the underlying differences in the operating system: + +```java +public native void loadFence(); +public native void storeFence(); +public native void fullFence(); +``` + +Theoretically, you can use these three methods to achieve the same effect as `volatile` prohibiting reordering, but it will be more troublesome. + +Let me take a common interview question as an example to explain the effect of the `volatile` keyword in prohibiting instruction reordering. + +During interviews, interviewers often say: "Do you understand the singleton pattern? Come and write it down for me! Explain to me the principle of double-check locking to implement the singleton pattern!" + +**Double verification lock implements object singleton (thread safety)**: + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + //First determine whether the object has been instantiated, and then enter the locking code if it has not been instantiated. + if (uniqueInstance == null) { + //Class object lock + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +It is also necessary to modify `uniqueInstance` with the `volatile` keyword. `uniqueInstance = new Singleton();` This code is actually divided into three steps: + +1. Allocate memory space for `uniqueInstance` +2. Initialize `uniqueInstance` +3. Point `uniqueInstance` to the allocated memory address + +However, due to the instruction rearrangement feature of the JVM, the execution order may become 1->3->2. Instruction reordering does not cause problems in a single-threaded environment, but in a multi-threaded environment it will cause a thread to obtain an instance that has not yet been initialized. For example, thread T1 executes 1 and 3. At this time, T2 calls `getUniqueInstance`() and finds that `uniqueInstance` is not empty, so it returns `uniqueInstance`, but `uniqueInstance` has not been initialized yet. + +### Can volatile guarantee atomicity? + +**The `volatile` keyword can guarantee the visibility of variables, but it does not guarantee that operations on variables are atomic. ** + +We can prove it with the following code: + +```java +/** + * Search JavaGuide on WeChat and reply to "Interview Assault" to get your own original Java interview manual for free + * + * @author Guide brother + * @date 2022/08/03 13:40 + **/ +public class VolatileAtomicDemo { + 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(); + } + }); + } + // Wait 1.5 seconds to ensure that the above program execution is completed + Thread.sleep(1500); + System.out.println(inc); + threadPool.shutdown(); + } +} +``` + +Under normal circumstances, running the above code should output `2500`. But after you actually run the above code, you will find that the output result is less than `2500` every time. + +Why does this happen? Isn’t it said that `volatile` can guarantee the visibility of variables? + +That is, if `volatile` can guarantee the atomicity of `inc++` operations. After the `inc` variable is incremented in each thread, other threads can immediately see the modified value. Five threads performed 500 operations respectively, so the final value of inc should be 5\*500=2500. + +Many people mistakenly think that the increment operation `inc++` is atomic. In fact, `inc++` is actually a compound operation, including three steps: + +1. Read the value of inc. +2. Add 1 to inc. +3. Write the value of inc back to memory. + +`volatile` cannot guarantee that these three operations are atomic, which may lead to the following situation: + +1. After thread 1 reads `inc`, it has not modified it. Thread 2 reads the value of `inc` again, modifies it (+1), and then writes the value of `inc` back to memory. +2. After thread 2 completes the operation, thread 1 modifies the value of `inc` (+1), and then writes the value of `inc` back to the memory. + +This also leads to the fact that after two threads perform an auto-increment operation on `inc`, `inc` actually only increases by 1. + +In fact, if you want to ensure that the above code runs correctly, it is very simple, you can use `synchronized`, `Lock` or `AtomicInteger`. + +Improved using `synchronized`: + +```java +public synchronized void increase() { + inc++; +} +``` + +Improved using `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`。 + +The `Unsafe` class is located under the `sun.misc` package and is a class that provides low-level, unsafe operations. Due to its powerful functions and potential dangers, it is usually used inside the JVM or in some libraries that require extremely high performance and low-level access, and is not recommended for use by ordinary 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). + +The `Unsafe` class under the `sun.misc` package provides `compareAndSwapObject`, `compareAndSwapInt` and `compareAndSwapLong` methods to implement CAS operations on `Object`, `int` and `long` types: + +```java +/** + * Atomicly update the value of an object field. + * + * @param o The object to be operated on + * @param offset The memory offset of the object field + * @param expected The expected old value + * @param x the new value to be set + * @return Returns true if the value is successfully updated; otherwise returns false + */ +boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); + +/** + * Atomicly update the value of an object field of type int. + */ +boolean compareAndSwapInt(Object o, long offset, int expected, int x); + +/** + * Atomicly update the value of an object field of type long. + */ +boolean compareAndSwapLong(Object o, long offset, long expected, long x); +``` + +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 underlying hardware instructions to implement atomic operations. In other words, the Java language does not directly implement CAS in Java, but in the form of C++ inline assembly (through JNI calls). Therefore, the specific implementation of CAS is closely related to the operating system and CPU. + +The `java.util.concurrent.atomic` package provides classes for atomic operations. These classes utilize low-level atomic instructions to ensure that operations in a multi-threaded environment are thread-safe. + +![JUC Atomic Class Overview](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +For the introduction and use of these Atomic atomic classes, you can read this article: [Atomic Atomic Class Summary](https://javaguide.cn/java/concurrent/atomic-classes.html). + +`AtomicInteger` is one of Java's atomic classes. It is mainly used to perform atomic operations on variables of type `int`. It uses the low-level atomic operation methods provided by the `Unsafe` class to achieve lock-free thread safety. + +Below, we explain how Java uses the methods of the `Unsafe` class to implement atomic operations by interpreting the core source code of `AtomicInteger` (JDK1.8). + +The core source code of `AtomicInteger` is as follows: + +```java +// Get Unsafe instance +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + // 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); } +} +// Ensure visibility of "value" field +private volatile int value; + +// If the current value is equal to the expected value, atomically set the value to newValue +// Use the Unsafe#compareAndSwapInt method to perform CAS operations +public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} + +// Atomicly add delta to the current value and return the old value +public final int getAndAdd(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta); +} + +// Atomicly increase the current value by 1 and return the value before addition (old value) +// Use the Unsafe#getAndAddInt method to perform CAS operations. +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +// Atomicly decrement the current value by 1 and return the value before decrement (old value) +public final int getAndDecrement() { + return unsafe.getAndAddInt(this, valueOffset, -1); +} +``` + +`Unsafe#getAndAddInt` source code: + +```java +// Atomically get and increment an integer value +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + // Get the integer value of object o at memory offset offset in volatile mode + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + //return old value + return v; +} +``` + +As you can see, `getAndAddInt` uses a `do-while` loop: when the `compareAndSwapInt` operation fails, it will be retried until it succeeds. That is to say, the `getAndAddInt` method will try to update the value of `value` through the `compareAndSwapInt` method. If the update fails (the current value is modified by other threads during this period), it will re-obtain the current value and try to update again until the operation is successful. + +Since CAS operations may fail due to concurrency conflicts, they are usually used with a while loop to retry after failure until the operation succeeds. This is the **Spin Lock Mechanism**. + +### What are the problems with the CAS algorithm? + +The ABA problem is the most common problem with CAS algorithms. + +#### ABA Questions + +If a variable V has the value A when it is first read, and it is checked that it is still the value A when preparing to assign it, can we prove that its value has not been modified by other threads? Obviously it cannot, because during this period its value may be changed to other values, and then changed back to A, then the CAS operation will mistakenly think that it has never been modified. This problem is known as the "ABA" problem of CAS operations. ** + +The solution to the ABA problem is to append a version number or timestamp in front of the variable. The `AtomicStampedReference` class after JDK 1.5 is used to solve the ABA problem. The `compareAndSet()` method is to first check whether the current reference is equal to the expected reference, and whether the current flag is equal to the expected flag. If all are equal, the value of the reference and the flag is atomically set to the given update value. + +```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))); +} +``` + +#### Long cycle time and high overheadCAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 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 depends on JVM and ReentrantLock depends on API + +`synchronized` relies on JVM implementation. As we mentioned earlier, the virtual machine team made a lot of optimizations for the `synchronized` keyword in JDK1.6, but these optimizations were implemented at the virtual machine level and were not directly exposed to us. + +`ReentrantLock` is implemented at the JDK level (that is, at the API level, which requires the `lock()` and `unlock()` methods together with the `try/finally` statement block), so we can see how it is implemented by looking at its source code. + +#### ReentrantLock adds some advanced features than synchronized + +Compared with `synchronized`, `ReentrantLock` adds some advanced functions. Mainly speaking, there are three main points: + +- **Waiting for Interruptible**: `ReentrantLock` provides a mechanism to interrupt threads waiting for the lock. This mechanism is implemented through `lock.lockInterruptibly()`. That is to say, while the current thread is waiting to acquire the lock, if other threads interrupt the current thread "`interrupt()`", the current thread will throw an `InterruptedException` exception, which can be caught and processed accordingly. +- **Achievable fair lock**: `ReentrantLock` can specify whether it is a fair lock or an unfair lock. And `synchronized` can only be an unfair lock. The so-called fair lock means that the thread waiting first obtains the lock first. `ReentrantLock` is unfair by default, and you can specify whether it is fair through the `ReentrantLock(boolean fair)` constructor of the `ReentrantLock` class. +- **Notification mechanism is more powerful**: `ReentrantLock` can achieve group wake-up and selective notification by binding multiple `Condition` objects. This solves the efficiency problem of `synchronized` which can only wake up randomly or all of them, and provides powerful support for complex thread collaboration scenarios. +- **Support timeout**: `ReentrantLock` provides the `tryLock(timeout)` method, which can specify the maximum waiting time to acquire the lock. If the waiting time is exceeded, the lock acquisition will fail and will not wait forever. + +If you want to use the above features, then choosing `ReentrantLock` is a good choice. + +Additional information about the `Condition` interface: + +> `Condition` is only available after JDK1.5. It has good flexibility. For example, it can implement multi-channel notification function, that is, multiple `Condition` instances (i.e. object monitors) can be created in a `Lock` object. **Thread objects can be registered in the specified `Condition`, so that thread notification can be selectively carried out and it is more flexible in scheduling threads. When using the `notify()/notifyAll()` method to notify, the thread to be notified is selected by the JVM. Using the `ReentrantLock` class combined with the `Condition` instance can implement "selective notification"**. This function is very important and is provided by the `Condition` interface by default. The `synchronized` keyword is equivalent to only one `Condition` instance in the entire `Lock` object, and all threads are registered in it. If the `notifyAll()` method is executed, all threads in the waiting state will be notified, which will cause great efficiency problems. The `signalAll()` method of a `Condition` instance will only wake up all waiting threads registered in the `Condition` instance. + +Additional information about **Waiting for interruptible**: + +> `lockInterruptibly()` will allow the thread acquiring the lock to respond to interrupts while blocking and waiting. That is, when the current thread acquires the lock and finds that the lock is held by another thread, it will block and wait. +> +> During the blocking and waiting process, if other threads interrupt the current thread `interrupt()`, an `InterruptedException` exception will be thrown. You can catch the exception and do some processing operations. +> +> In order to better understand this method, borrow a case from Stack Overflow to better understand that `lockInterruptibly()` can respond to interrupts: +> +> ```JAVA +> public class MyRentrantlock { +> Thread t = new Thread() { +> @Override +> public void run() { +> ReentrantLock r = new ReentrantLock(); +> // 1.1. The first attempt to acquire the lock can be successful. +> r.lock(); +> +> // 1.2. The number of reentrants of the lock at this time is 1 +> System.out.println("lock() : lock count :" + r.getHoldCount()); +> +> // 2. Interrupt the current thread. Through Thread.currentThread().isInterrupted() you can see that the interruption status of the current thread is true. +> interrupt(); +> System.out.println("Current thread is interrupted"); +> +> // 3.1. Try to acquire the lock and you can acquire it successfully. +> r.tryLock(); +> // 3.2. The number of reentrants of the lock at this time is 2 +> System.out.println("tryLock() on interrupted thread lock count :" + r.getHoldCount()); +> try { +> // 4. If the interrupt status of the printing thread is true, then calling the lockInterruptibly() method will throw an InterruptedException exception. +> 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. Print the number of reentrants of the lock. You can find that the lockInterruptibly() method did not successfully acquire the lock. +> 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(); +> } +> } +> ``` +> +> Output: +> +> ``BASH +> lock() : lock count :1 +> Current thread is interrupted +> tryLock() on interrupted thread lock count :2 +> Current Thread isInterrupted:true +> Error +> lockInterruptibly() not able to Acqurie lock: lock count :2 +> lock count :1 +> lock count :0 +> ``` + +Additional information about **Support Timeout**: + +> **Why do you need the `tryLock(timeout)` function? ** +>> `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 适合什么场景? + +Like `ReentrantReadWriteLock`, `StampedLock` is also suitable for business scenarios with more reading and less writing. It can be used as a substitute for `ReentrantReadWriteLock` with better performance. + +However, it should be noted that `StampedLock` is not reentrant, does not support condition variables `Condition`, and is not friendly to interrupt operations (improper use can easily cause CPU spikes). If you need to use some of the advanced features of `ReentrantLock`, it is not recommended to use `StampedLock`. + +In addition, although `StampedLock` has good performance, it is relatively troublesome to use. Once used improperly, production problems will occur. It is strongly recommended that you read the cases in [StampedLock official documentation](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html) before using `StampedLock`. + +### Do you understand the underlying principle of StampedLock? + +`StampedLock` does not directly implement the `Lock` or `ReadWriteLock` interface, but is implemented based on **CLH lock** (AQS is also based on this thing). CLH lock is an improvement on spin lock and is an implicit linked list queue. `StampedLock` manages threads through the CLH queue, and uses the synchronization state value `state` to represent the status and type of the lock. + +The principle of `StampedLock` is similar to the AQS principle, so I won’t introduce it in detail here. If you are interested, you can read the following two articles: + +- [AQS detailed explanation](https://javaguide.cn/java/concurrent/aqs.html) +- [Analysis of the underlying principles of StampedLock](https://segmentfault.com/a/1190000015808032) + +If you are just preparing for an interview, it is recommended that you spend more effort to understand the AQS principle. The probability of encountering the underlying principle of `StampedLock` in an interview is very small. + +## Atomic atomic class + +I wrote a separate article to summarize the content of the Atomic atomic class: [Atomic Atomic Class Summary](./atomic-classes.md). + +## Reference + +- "In-depth Understanding of Java Virtual Machine" +- "Practical Java High Concurrency Programming" +- Guide to the Volatile Keyword in Java - Baeldung: +- Things that must be said about Java "lock" - Meituan technical team: +- Why can't the read lock in the ReadWriteLock class be upgraded to a write lock? : +- StampedLock, a high-performance tool to solve thread hunger: +- Understanding ThreadLocal in Java - Technical Black Room: +- ThreadLocal (Java Platform SE 8) - Oracle Help Center: + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-concurrent-questions-03.en.md b/docs_en/java/concurrent/java-concurrent-questions-03.en.md new file mode 100644 index 00000000000..3d814f7c573 --- /dev/null +++ b/docs_en/java/concurrent/java-concurrent-questions-03.en.md @@ -0,0 +1,1358 @@ +--- +title: Summary of common Java concurrency interview questions (Part 2) +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: multi-threading, deadlock, thread pool, CAS, AQS + - - meta + - name: description + content: Summary of common Java concurrency knowledge points and interview questions (including detailed answers), I hope it will be helpful to you! +--- + + + +##ThreadLocal + +### What is ThreadLocal used for? + +Normally, the variables we create can be accessed and modified by any thread. This can lead to data races and thread safety issues in multi-threaded environments. So, if you want each thread to have its own exclusive local variable, how to implement it? ** + +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 vividly compared to a "box that stores data". Each thread has its own independent box for storing private data to ensure that data between different threads do not interfere with each other. + +When you create a `ThreadLocal` variable, each thread that accesses the variable will have an independent copy. This is where the name `ThreadLocal` comes from. A thread can obtain a local copy of its own thread through the `get()` method, or modify the value of the copy through the `set()` method, thereby avoiding thread safety issues. + +To give a simple example: Suppose two people go to the treasure house to collect treasures. If they share a bag, there will inevitably be arguments; but if everyone has an independent bag, there will be no such problem. If these two people are compared to threads, then `ThreadLocal` is a method used to prevent these two threads from competing for the same resource. + +```java +public class ThreadLocalExample { + private static ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); + + public static void main(String[] args) { + Runnable task = () -> { + int value = threadLocal.get(); + value += 1; + threadLocal.set(value); + System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get()); + }; + + Thread thread1 = new Thread(task, "Thread-1"); + Thread thread2 = new Thread(task, "Thread-2"); + + thread1.start(); // Output: Thread-1 Value: 1 + thread2.start(); // Output: Thread-2 Value: 1 + } +} +``` + +### ⭐️Do you understand the principle of ThreadLocal? + +Start with the `Thread` class source code. + +```java +public class Thread implements Runnable { + //...... + //ThreadLocal value related to this thread. Maintained by ThreadLocal class + ThreadLocal.ThreadLocalMap threadLocals = null; + + //InheritableThreadLocal value related to this thread. Maintained by InheritableThreadLocal class + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; + //...... +} +``` + +From the source code of the `Thread` class above, we can see that there is a `threadLocals` and an `inheritableThreadLocals` variable in the `Thread` class. They are both variables of the `ThreadLocalMap` type. We can understand `ThreadLocalMap` as a customized `HashMap` implemented by the `ThreadLocal` class. By default, these two variables are null. They are created only when the current thread calls the `set` or `get` method of the `ThreadLocal` class. In fact, when calling these two methods, we call the `get()` and `set()` methods corresponding to the `ThreadLocalMap` class. + +`set()` method of `ThreadLocal` class + +```java +public void set(T value) { + //Get the thread of the current request + Thread t = Thread.currentThread(); + //Get the threadLocals variable (hash table structure) inside the Thread class + ThreadLocalMap map = getMap(t); + if (map != null) + //Put the value to be stored into this hash table + map.set(this, value); + else + createMap(t, value); +} +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} +``` + +Through the above content, we can conclude by guessing: **The final variable is placed in the `ThreadLocalMap` of the current thread, and does not exist on `ThreadLocal`. `ThreadLocal` can be understood as just a package of `ThreadLocalMap`, passing the variable value. ** In the `ThrealLocal` class, after you can obtain the current thread object through `Thread.currentThread()`, you can directly access the `ThreadLocalMap` object of the thread through `getMap(Thread t)`. + +**Each `Thread` has a `ThreadLocalMap`, and `ThreadLocalMap` can store key-value pairs with `ThreadLocal` as the key and the Object object as the value. ** + +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + //...... +} +``` + +For example, if we declare two `ThreadLocal` objects in the same thread, `Thread` internally uses the only `ThreadLocal` to store data. The key of `ThreadLocalMap` is the `ThreadLocal` object, and the value is the value set by calling the `set` method of the `ThreadLocal` object. + +The `ThreadLocal` data structure is shown in the figure below: + +![ThreadLocal data structure](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadlocal-data-structure.png) + +`ThreadLocalMap` is a static inner class of `ThreadLocal`. + +![ThreadLocal inner class](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-local-inner-class.png) + +### ⭐️What causes ThreadLocal memory leak problem? + +The root cause of `ThreadLocal` memory leaks lies in its internal implementation mechanism. + +From the above content, we already know: each thread maintains a map named `ThreadLocalMap`. When you use `ThreadLocal` to store a value, you are actually storing the value in the current thread's `ThreadLocalMap`, with the `ThreadLocal` instance itself as the key and the value you want to store as the value. + +The source code of `set()` method of `ThreadLocal` is as follows: + +```java +public void set(T value) { + Thread t = Thread.currentThread(); // Get the current thread + ThreadLocalMap map = getMap(t); // Get the ThreadLocalMap of the current thread + if (map != null) { + map.set(this, value); // Set value + } else { + createMap(t, value); // Create a new ThreadLocalMap + } +} +```In the `set()` and `createMap()` methods of `ThreadLocalMap`, the `ThreadLocal` object itself is not directly stored. Instead, the hash value of `ThreadLocal` is used to calculate the array index, and is finally stored in an array of type `static class Entry extends WeakReference>`. + +```java +int i = key.threadLocalHashCode & (len-1); +``` + +`Entry` of `ThreadLocalMap` is defined as follows: + +```java +static class Entry extends WeakReference> { + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +`ThreadLocalMap`’s `key` and `value` reference mechanism: + +- **key is a weak reference**: The key in `ThreadLocalMap` is a weak reference to `ThreadLocal` (`WeakReference>`). This means that if a `ThreadLocal` instance is no longer pointed to by any strong reference, the garbage collector will recycle the instance during the next GC, causing the corresponding key in `ThreadLocalMap` to become `null`. +- **value is a strong reference**: Even if `key` is recycled by GC, `value` is still strongly referenced by `ThreadLocalMap.Entry` and cannot be recycled by GC. + +When a `ThreadLocal` instance loses its strong reference, its corresponding value still exists in `ThreadLocalMap` because the `Entry` object has a strong reference to it. If the thread continues to survive (such as a thread in a thread pool), `ThreadLocalMap` will always exist, causing the entry with key `null` to not be garbage collected, which will cause a memory leak. + +In other words, for a memory leak to occur, two conditions need to be met at the same time: + +1. The `ThreadLocal` instance is no longer strongly referenced; +2. The thread continues to survive, causing `ThreadLocalMap` to exist for a long time. + +Although `ThreadLocalMap` will try to clean up entries with null keys during `get()`, `set()` and `remove()` operations, this cleaning mechanism is passive and not completely reliable. + +**How ​​to avoid memory leaks? ** + +1. After using `ThreadLocal`, be sure to call the `remove()` method. This is the safest and most recommended approach. The `remove()` method will explicitly remove the corresponding entry from `ThreadLocalMap`, completely eliminating the risk of memory leaks. Even if `ThreadLocal` is defined as `static final`, it is strongly recommended to call `remove()` after each use. +2. In thread pool and other thread reuse scenarios, using the `try-finally` block can ensure that the `remove()` method will be executed even if an exception occurs. + +### ⭐️How to pass the value of ThreadLocal across threads? + +Because the variable value of `ThreadLocal` is stored in `Thread`, and the parent and child threads belong to different `Thread`. Therefore, in an asynchronous scenario, the `ThreadLocal` value of the parent and child threads cannot be transferred. + +If you want to pass `ThreadLocal` value in asynchronous scenario, there are two solutions: + +- `InheritableThreadLocal`: `InheritableThreadLocal` is a tool provided by JDK1.2 and inherits from `ThreadLocal`. When using `InheritableThreadLocal`, when creating a child thread, the child thread will inherit the `ThreadLocal` value in the parent thread, but the `ThreadLocal` value transfer in the thread pool scenario cannot be supported. +- `TransmittableThreadLocal`: `TransmittableThreadLocal` (referred to as TTL) is Alibaba's open source tool class. It inherits and enhances the `InheritableThreadLocal` class and can support `ThreadLocal` value transfer in a thread pool scenario. Project address: . + +#### InheritableThreadLocal principle + +`InheritableThreadLocal` implements the function of inheriting the `ThreadLocal` value of the parent thread when creating an asynchronous thread. This class is provided by the JDK team. It implements the transfer of the `ThreadLocal` value when creating a thread by transforming the `Thread` class in the JDK source code package. + +**Where is the value of `InheritableThreadLocal` stored? ** + +A new `ThreadLocalMap` was added to the `Thread` class, named `inheritableThreadLocals`. This variable is used to store `ThreadLocal` values that need to be passed across threads. As follows: + +```JAVA +class Thread implements Runnable { + ThreadLocal.ThreadLocalMap threadLocals = null; + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; +} +``` + +**How to complete the passing of `ThreadLocal` value? ** + +This is achieved by transforming the constructor of the `Thread` class. When creating a `Thread` thread, just get the `inheritableThreadLocals` variable of the parent thread and assign it to the child thread. The relevant code is as follows: + +```JAVA +// The constructor of Thread will call the init() method +private void init(/* ... */) { + // 1. Get the parent thread + Thread parent = currentThread(); + // 2. Assign the inheritableThreadLocals of the parent thread to the child thread + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); +} +``` + +#### TransmittableThreadLocal principle + +By default, JDK does not support the function of `ThreadLocal` value transfer in thread pool scenarios, so Alibaba open sourced a set of tools `TransmittableThreadLocal` to implement this function. + +Alibaba cannot change the source code of the JDK, so it uses the **decorator mode** internally to enhance the original functions to achieve the `ThreadLocal` value transfer in the thread pool scenario. + +There are two places where TTL has been transformed: + +- Implement a custom `Thread` and perform the assignment operation of the `ThreadLocal` variable inside the `run()` method. + +- Decoration based on **Thread Pool**, in the `execute()` method, do not submit the JDK internal `Thread`, but submit the custom `Thread`. + +If you want to view the relevant source code, you can introduce Maven dependencies for download. + +```XML + + com.alibaba + transmittable-thread-local + 2.12.0 + +``` + +#### Application scenarios + +1. **Pressure test flow mark**: In the pressure test scenario, use `ThreadLocal` to store the stress test mark, which is used to distinguish the pressure test flow and the real flow. If the tag is missing, the stress test traffic may be mistakenly treated as online traffic. +2. **Context delivery**: In a distributed system, transfer link tracking information (such as Trace ID) or user context information. + +## Thread pool + +### What is a thread pool? + +As the name suggests, a thread pool is a resource pool that manages a series of threads. When there is a task to be processed, the thread is directly obtained from the thread pool for processing. After processing, the thread will not be destroyed immediately, but will wait for the next task. + +### ⭐️Why use thread pool? + +Pooling technology must be familiar to everyone. Thread pools, database connection pools, HTTP connection pools, etc. are all applications of this idea. The idea of ​​pooling technology is mainly to reduce the consumption of resources each time and improve the utilization of resources. + +Thread pools provide a way to limit and manage resources, including executing a task. Each thread pool also maintains some basic statistics, such as the number of completed tasks. Using a thread pool mainly brings the following benefits: + +1. **Reduce resource consumption**: Threads in the thread pool can be reused. Once a thread completes a task, it is not destroyed immediately, but returns to the pool to wait for the next task. This avoids the overhead caused by frequently creating and destroying threads.2. **Improve response speed**: Because the thread pool usually maintains a certain number of core threads (or "resident workers"), when tasks come, they can be directly handed over to these existing and idle threads for execution, saving the time of creating threads, and tasks can be processed faster. +3. **Improve thread manageability**: The thread pool allows us to uniformly manage the threads in the pool. We can configure the size of the thread pool (number of core threads, maximum number of threads), type and size of task queue, rejection policy, etc. This can control the total number of concurrent threads, prevent resource exhaustion, and ensure system stability. At the same time, the thread pool usually also provides a monitoring interface to facilitate us to understand the running status of the thread pool (such as how many active threads there are, how many tasks are queued, etc.) and to facilitate tuning. + +### How to create a thread pool? + +In Java, there are two main ways to create a thread pool: + +**Method 1: Create directly 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 and have more granular control over the running behavior of the thread pool, thus avoiding the risk of resource exhaustion. + +**Method 2: Create through `Executors` tool class (not recommended for production environment)** + +The method of creating a thread pool provided by the `Executors` tool class is shown in the figure below: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-new-thread-pool-methods.png) + +It can be seen that multiple types of thread pools can be created through the `Executors` tool class, including: + +- `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, if there are idle threads in the thread pool, it will be executed immediately. If not, the new task will be temporarily stored in a task queue, and when a thread is idle, the task in the task queue will be processed. +- `SingleThreadExecutor`: Thread pool with only one thread. If more than one task is submitted to the thread pool, the task will be saved in a task queue. When the thread is idle, the tasks in the queue will be executed in first-in, first-out order. +- `CachedThreadPool`: A thread pool that can adjust the number of threads according to actual conditions. The number of threads in the thread pool is uncertain, but if there are idle threads that can be reused, the reusable threads will be used first. If all threads are working and a new task is submitted, a new thread will be created to handle the task. After all threads complete the execution of the current task, they will be returned to the thread pool for reuse. +- `ScheduledThreadPool`: A thread pool that runs tasks after a given delay or executes tasks periodically. + +### ⭐️Why is it not recommended to use the built-in thread pool? + +In the "Concurrency Processing" chapter of the "Alibaba Java Development Manual", it is clearly stated that thread resources must be provided through the thread pool, and explicit creation of threads in the application is not allowed. + +**Why? ** + +> The advantage of using a thread pool is to reduce the time spent on creating and destroying threads and system resource overhead, and solve the problem of insufficient resources. If the thread pool is not used, it may cause the system to create a large number of similar threads, leading to memory consumption or "excessive switching" problems. + +In addition, the mandatory thread pool in the "Alibaba Java Development Manual" does not allow the use of `Executors` to create, but through the `ThreadPoolExecutor` constructor. This processing method allows the writing students to be more clear about the operating rules of the thread pool and avoid the risk of resource exhaustion. + +The disadvantages of `Executors` returning thread pool objects are as follows (will be introduced in detail later): + +- `FixedThreadPool` and `SingleThreadExecutor`: use the blocking queue `LinkedBlockingQueue`. The maximum length of the task queue is `Integer.MAX_VALUE`, which can be regarded as unbounded and may accumulate a large number of requests, resulting in OOM. +- `CachedThreadPool`: uses a synchronized queue `SynchronousQueue`, and the number of threads allowed to be created is `Integer.MAX_VALUE`. If there are too many tasks and the execution speed is slow, a large number of threads may be created, resulting in OOM. +- `ScheduledThreadPool` and `SingleThreadScheduledExecutor`: use the unbounded delay blocking queue `DelayedWorkQueue`, the maximum length of the task queue is `Integer.MAX_VALUE`, which may accumulate a large number of requests, resulting in OOM. + +```java +public static ExecutorService newFixedThreadPool(int nThreads) { + // The default length of LinkedBlockingQueue is Integer.MAX_VALUE, which can be regarded as unbounded + return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); + +} + +public static ExecutorService newSingleThreadExecutor() { + // The default length of LinkedBlockingQueue is Integer.MAX_VALUE, which can be regarded as unbounded + return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue())); + +} + +// Synchronous queue SynchronousQueue, no capacity, the maximum number of threads is Integer.MAX_VALUE` +public static ExecutorService newCachedThreadPool() { + + return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue()); + +} + +// DelayedWorkQueue (delayed 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()); +} +``` + +### ⭐️What are the common parameters of thread pool? How to explain? + +```java + /** + * Create a new ThreadPoolExecutor with the given initial parameters. + */ + public ThreadPoolExecutor(int corePoolSize,//The number of core threads in the thread pool + int maximumPoolSize,//The maximum number of threads in the thread pool + long keepAliveTime,//When the number of threads is greater than the number of core threads, the maximum time for the excess idle threads to survive + TimeUnit unit,//time unit + BlockingQueue workQueue,//task queue, used to store queues waiting for execution of tasks + ThreadFactory threadFactory,//Thread factory, used to create threads, generally the default is enough + RejectedExecutionHandler handler//Rejection strategy, when too many tasks are submitted and cannot be processed in time, we can customize the strategy to handle the tasks + ) { + 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 most important parameters: + +- `corePoolSize`: When the task queue does not reach the queue capacity, the maximum number of threads that can run simultaneously. +- `maximumPoolSize`: When the tasks stored in the task queue reach the queue capacity, the number of threads that can currently run simultaneously becomes the maximum number of threads. +- `workQueue`: When a new task comes, it will first determine whether the number of currently running threads reaches the number of core threads. If so, the new task will be stored in the queue. + +`ThreadPoolExecutor`Other common parameters: + +- `keepAliveTime`: When the number of threads in the thread pool is greater than `corePoolSize`, that is, when there are non-core threads (threads other than core threads in the thread pool), these non-core threads will not be destroyed immediately after becoming idle, but will wait until the waiting time exceeds `keepAliveTime` before they will be recycled and destroyed. +- `unit` : The time unit of the `keepAliveTime` parameter. +- `threadFactory` :executor is used when creating a new thread. +- `handler`: rejection strategy (will be introduced in detail later). + +The picture below can deepen your understanding of the relationship between various parameters in the thread pool (picture source: "Java Performance Tuning Practice"): + +![Relationship between thread pool parameters](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) + +### Will the core threads of the thread pool be recycled? + +`ThreadPoolExecutor` will not recycle core threads by default, even if they are idle. This is to reduce the overhead of creating threads, because core threads are usually kept active for a long time. However, if the thread pool is used for cyclic use scenarios and the frequency is not high (there is obvious idle time between cycles), you can consider setting the parameter of the `allowCoreThreadTimeOut(boolean value)` method to `true`, so that the idle (time interval is specified by `keepAliveTime`) core threads will be recycled. + +```java +public void allowCoreThreadTimeOut(boolean value) { + // The keepAliveTime of the core thread must be greater than 0 to enable the timeout mechanism + if (value && keepAliveTime <= 0) { + throw new IllegalArgumentException("Core threads must have nonzero keep alive times"); + } + //Set the value of allowCoreThreadTimeOut + if (value != allowCoreThreadTimeOut) { + allowCoreThreadTimeOut = value; + // If the timeout mechanism is enabled, clean up all idle threads, including core threads + if (value) { + interruptIdleWorkers(); + } + } +} +``` + +### What state is the core thread in when it is idle? + +When the core thread is idle, its status is divided into the following two situations: + +- **Set the survival time of the core thread**: When the core thread is idle, it will be in the `WAITING` state, waiting to obtain tasks. If the blocking waiting time exceeds the core thread survival time, the thread will exit the work, the thread will be removed from the worker thread collection of the thread pool, and the thread status will change to `TERMINATED` state. +- **The survival time of the core thread is not set**: When the core thread is idle, it will always be in the `WAITING` state, waiting to obtain tasks, and the core thread will always survive in the thread pool. + +When there are available tasks in the queue, the blocked thread will be awakened, and the thread's state will change from `WAITING` state to `RUNNABLE` state, and then the corresponding task will be executed. + +Next, learn about how the thread pool is done internally through the relevant source code. + +Threads are abstracted into `Worker` inside the thread pool. When `Worker` is started, it will continue to obtain tasks from the task queue. + +When acquiring tasks, the behavior of acquiring tasks from the task queue (`BlockingQueue`) will be determined based on the `timed` value. + +If "the survival time of the core thread is set" or "the number of threads exceeds the number of core threads", then `timed` is marked as `true`, indicating that `poll()` needs to be used to specify the timeout when acquiring the task. + +- `timed == true`: Use `poll(timeout, unit)` to get the task. If you use the `poll(timeout, unit)` method to obtain the task timeout, the current thread will exit execution (`TERMINATED`) and the thread will be removed from the thread pool. +- `timed == false`: Use `take()` to get the task. Using the `take()` method to obtain a task will cause the current thread to be blocked and waiting (`WAITING`). + +The source code is as follows: + +```JAVA +// ThreadPoolExecutor +private Runnable getTask() { + boolean timedOut = false; + for (;;) { + // ... + + // 1. If "the survival time of the core thread is set" or "the number of threads exceeds the number of core threads", then timed is true. + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + // 2. Decrease the number of threads. + // wc > maximuimPoolSize: The number of threads in the thread pool exceeds the maximum number of threads. Where wc is the number of threads in the thread pool. + // timed && timeOut: timeOut indicates that the acquisition task has timed out. + // Divided into two situations: the core thread has set a survival time && if the acquisition task times out, the number of threads will be deducted; the number of threads exceeds the number of core threads && if the acquisition task times out, the number of threads will be deducted. + if ((wc > maximumPoolSize || (timed && timedOut)) + && (wc > 1 || workQueue.isEmpty())) { + if (compareAndDecrementWorkerCount(c)) + return null; + continue; + } + try { + // 3. If timed is true, use poll() to obtain the task; otherwise, use take() to obtain the task. + Runnable r = timed? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS): + workQueue.take(); + // 4. Return after obtaining the task. + if (r != null) + return r; + timedOut = true; + } catch (InterruptedException retry) { + timedOut = false; + } + } +} +``` + +### ⭐️What are the rejection strategies of the thread pool? + +If the number of threads currently running simultaneously reaches the maximum number of threads and the queue is full of tasks, `ThreadPoolExecutor` defines some strategies: + +- `ThreadPoolExecutor.AbortPolicy`: Throws `RejectedExecutionException` to reject the processing of new tasks. +- `ThreadPoolExecutor.CallerRunsPolicy`: Call the executor's own thread to run the task, that is, run (`run`) the rejected task directly in the thread that calls the `execute` method. If the executor has been closed, the task will be discarded. Therefore, this strategy will reduce the speed of new task submission and affect the overall performance of the program. You can choose this strategy if your application can tolerate this delay and you require that every task request must be executed. +- `ThreadPoolExecutor.DiscardPolicy`: New tasks are not processed and discarded directly. +- `ThreadPoolExecutor.DiscardOldestPolicy`: This policy will discard the oldest unhandled task request.For example: When Spring creates a thread pool through `ThreadPoolTaskExecutor` or we directly use the constructor of `ThreadPoolExecutor`, when we do not specify the `RejectedExecutionHandler` rejection policy to configure the thread pool, the default is `AbortPolicy`. Under this rejection strategy, if the queue is full, `ThreadPoolExecutor` will throw a `RejectedExecutionException` exception to reject the incoming task, which means that you will lose the processing of this task. If you don't want to discard tasks, you can use `CallerRunsPolicy`. `CallerRunsPolicy` is different from several other policies in that it neither abandons the task nor throws an exception. Instead, it returns the task to the caller and uses the caller's thread to execute the task. + +```java +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + // Directly execute the main thread instead of thread execution in the thread pool + r.run(); + } + } + } +``` + +### If dropping tasks is not allowed, which rejection policy should be chosen? + +Based on the above introduction to the thread pool rejection policy, I believe everyone can easily come to the answer: `CallerRunsPolicy`. + +Here we will take a look at the source code of `CallerRunsPolicy`: + +```java +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + //As long as the current program is not closed, use the thread that executes the execute method to perform the task. + if (!e.isShutdown()) { + + r.run(); + } + } + } +``` + +It can be seen from the source code that as long as the current program is not closed, the thread that executes the `execute` method will be used to perform the task. + +### What are the risks of the CallerRunsPolicy deny policy? How to solve it? + +We also mentioned above: If you want to ensure that any task request will be executed, it is more appropriate to choose the `CallerRunsPolicy` rejection policy. + +However, if the task of reaching `CallerRunsPolicy` is a very time-consuming task, and the thread that handles the submitted task is the main thread, it may cause the main thread to be blocked and affect the normal operation of the program. + +Here is a simple example. The thread pool limits the maximum number of threads to 2 and the blocking queue size to 1 (which means that the fourth task will reach the rejection policy). `ThreadUtil` is a tool class provided by Hutool: + +```java +public class ThreadPoolTest { + + private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class); + + public static void main(String[] args) { + //Create a thread pool with the number of core threads being 1 and the maximum number of threads being 2 + // When the number of threads is greater than the number of core threads, the maximum time that the excess idle threads can survive is 60 seconds. + // The task queue is an ArrayBlockingQueue with a capacity of 1, and the saturation policy is CallerRunsPolicy. + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, + 2, + 60, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(1), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // Submit the first task to be executed by the core thread + threadPoolExecutor.execute(() -> { + log.info("Core thread executes the first task"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + //Submit the second task. Since the core thread is occupied, the task will enter the queue and wait. + threadPoolExecutor.execute(() -> { + log.info("Non-core thread processes the second task enqueued"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + //Submit the third task. Since the core thread is occupied and the queue is full, a non-core thread is created for processing. + threadPoolExecutor.execute(() -> { + log.info("Non-core thread processing third task"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + //Submit the fourth task. Since both core threads and non-core threads are occupied and the queue is full, according to the CallerRunsPolicy policy, the task will be executed by the thread that submitted the task (i.e. the main thread). + threadPoolExecutor.execute(() -> { + log.info("The main thread processes the fourth task"); + ThreadUtil.sleep(2, TimeUnit.MINUTES); + }); + + //Submit the fifth task. The main thread is stuck by the fourth task. The task must wait until the main thread finishes executing before it can be submitted. + threadPoolExecutor.execute(() -> { + log.info("Core thread executes fifth task"); + }); + + // Close the thread pool + threadPoolExecutor.shutdown(); + } +} + +``` + +Output: + +```bash +18:19:48.203 INFO [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - The core thread executes the first task +18:19:48.203 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - Non-core thread processing third task +18:19:48.203 INFO [main] c.j.concurrent.ThreadPoolTest - The main thread processes the fourth task +18:20:48.212 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - Non-core thread handles second enqueued task +18:21:48.219 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - The core thread executes the fifth task +``` + +It can be seen from the output results that because of the rejection policy of `CallerRunsPolicy`, time-consuming tasks are executed on the main thread, causing the thread pool to be blocked, which in turn causes subsequent tasks to be unable to be executed in time, and in serious cases may lead to OOM. + +Let's start with the essence of the problem. The caller uses `CallerRunsPolicy` in the hope that all tasks can be executed, and tasks that cannot be processed temporarily are stored in the blocking queue `BlockingQueue`. In this case, if memory permits, we can increase the size of the blocking queue `BlockingQueue` and adjust the heap memory to accommodate more tasks to ensure that tasks can be executed accurately. + +In order to make full use of the CPU, we can also adjust the `maximumPoolSize` (maximum number of threads) parameter of the thread pool, which can increase the task processing speed and avoid running out of memory due to too many tasks accumulated in the `BlockingQueue`. + +![Adjust the blocking queue size and maximum number of threads](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` 是一个无界队列。其底层虽然是数组,但当数组容量不足时,它会自动进行扩容,因此队列永远不会被填满。当任务不断提交时,它们会全部被添加到队列中。这意味着线程池的线程数量永远不会超过其核心线程数,最大线程数参数对于使用该队列的线程池来说是无效的。 +- `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)中详细介绍了如何设计一个动态线程池,这也是面试中常问的一道系统设计题。 + +![《后端面试高频系统设计&场景题》](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 详解](https://javaguide.cn/java/concurrent/completablefuture-intro.html)。 + +### 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` not only implements the `Future` interface, but also implements the `Runnable` interface, so it can be directly executed by a thread as a task. + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) + +`FutureTask` has two constructors, which can pass in `Callable` or `Runnable` objects. In fact, passing in a `Runnable` object is also converted to a `Callable` object inside the method. + +```java +public FutureTask(Callable callable) { + if (callable == null) + throw new NullPointerException(); + this.callable = callable; + this.state = NEW; +} +public FutureTask(Runnable runnable, V result) { + // Convert the Runnable object runnable into a Callable object through the adapter RunnableAdapter + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +``` + +`FutureTask` is equivalent to encapsulating `Callable`, managing the task execution, and storing the task execution results of `Callable`'s `call` method. + +For more source code details of `Future`, you can read this 10,000-word analysis, which is very clear: [How does Java implement the Future pattern? Detailed explanation of 10,000 words! ](https://juejin.cn/post/6844904199625375757). + +### What is the use of the CompletableFuture class? + +`Future` has some limitations in actual use. For example, it does not support the orchestration and combination of asynchronous tasks, and the `get()` method to obtain calculation results is a blocking call. + +The `CompletableFuture` class was introduced in Java 8 to solve these shortcomings of `Future`. In addition to providing more easy-to-use and powerful `Future` features, `CompletableFuture` also provides functional programming, asynchronous task orchestration and combination (multiple asynchronous tasks can be connected in series to form a complete chain call) and other capabilities. + +Let's take a brief look at the definition of the `CompletableFuture` class. + +```java +public class CompletableFuture implements Future, CompletionStage { +} +``` + +As you can see, `CompletableFuture` implements both the `Future` and `CompletionStage` interfaces. + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) + +The `CompletionStage` interface describes an asynchronous computation stage. Many calculations can be divided into multiple stages or steps. At this time, it can be used to combine all steps to form an asynchronous calculation pipeline. + +There are many methods in the `CompletionStage` interface, and the functional capabilities of `CompletableFuture` are given by this interface. From the method parameters of this interface, you can find that it makes extensive use of functional programming introduced in Java8. + +![](https://oss.javaguide.cn/javaguide/image-20210902093026059.png) + +### ⭐️A task needs to depend on two other tasks before it is executed. How to design it? + +This task orchestration scenario is very suitable to be implemented through `CompletableFuture`. It is assumed here that T3 is executed after T2 and T1 are executed. + +The code is as follows (in order to simplify the code, Hutool’s thread tool class `ThreadUtil` and date and time tool class `DateUtil` are used): + +```java +// T1 +CompletableFuture futureT1 = CompletableFuture.runAsync(() -> { + System.out.println("T1 is executing. Current time: " + DateUtil.now()); + // Simulate time-consuming operations + ThreadUtil.sleep(1000); +}); +//T2 +CompletableFuture futureT2 = CompletableFuture.runAsync(() -> { + System.out.println("T2 is executing. Current time: " + DateUtil.now()); + ThreadUtil.sleep(1000); +}); + +// Use the allOf() method to merge the CompletableFutures of T1 and T2 and wait for them to complete +CompletableFuture bothCompleted = CompletableFuture.allOf(futureT1, futureT2); +//When both T1 and T2 are completed, execute T3 +bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time: " + DateUtil.now())); +// Wait for all tasks to be completed and verify the effect +ThreadUtil.sleep(3000); +``` + +Run T1 and T2 in parallel through the `allOf()` static method of `CompletableFuture`. When both T1 and T2 are completed, execute T3. + +### ⭐️Using CompletableFuture, a task fails, how to handle the exception? + +When using `CompletableFuture`, exceptions must be handled in the correct way to avoid exceptions being lost or uncontrollable problems. + +Here are some suggestions: + +- Use the `whenComplete` method to trigger the callback function when the task is completed and handle the exception correctly instead of letting the exception be swallowed or lost. +- Use the `exceptionally` method to handle exceptions and rethrow them so that they propagate to subsequent stages rather than having them ignored or terminated. +- Use the `handle` method to handle normal return results and exceptions, and return a new result instead of letting exceptions affect normal business logic. +- Use the `CompletableFuture.allOf` method to combine multiple `CompletableFuture` and handle exceptions for all tasks uniformly, instead of making exception handling too lengthy or repetitive. +-… + +### ⭐️Why do you need to customize the thread pool when using CompletableFuture? + +`CompletableFuture` uses the globally shared `ForkJoinPool.commonPool()` as the executor by default, and all asynchronous tasks that do not specify an executor will use this thread pool. This means that if an application, multiple libraries or frameworks (such as Spring, third-party libraries) all depend on `CompletableFuture`, they will all share the same thread pool by default. + +Although `ForkJoinPool` is very efficient, when a large number of tasks are submitted at the same time, it may cause resource contention and thread starvation, thereby affecting system performance. + +To avoid these problems, it is recommended to provide a custom thread pool for `CompletableFuture`, which brings the following advantages: + +-Isolation: Allocate independent thread pools to different tasks to avoid global thread pool resource contention. +- Resource control: Adjust thread pool size and queue type according to task characteristics to optimize performance. +- Exception handling: Better handle exceptions in threads by customizing `ThreadFactory`. + +```java +private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + +CompletableFuture.runAsync(() -> { + //... +}, executor); +``` + +## AQS + +For a detailed analysis of the AQS source code, you can read this article: [AQS Detailed Explanation](https://javaguide.cn/java/concurrent/aqs.html). + +### What is AQS? + +AQS (`AbstractQueuedSynchronizer`, abstract queue synchronizer) is a Java concurrency core component provided starting from JDK1.5. + +AQS solves the complexity problem for developers when implementing synchronizers. It provides a general framework for implementing various synchronizers, such as ReentrantLock, Semaphore and CountDownLatch. By encapsulating the underlying thread synchronization mechanism, AQS hides complex thread management logic, allowing developers to only focus on specific synchronization logic.简单来说,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); +} +``` + +Calling `semaphore.release();`, the thread attempts to release the license and uses the CAS operation to modify the value of `state` to `state=state+1`. After the license is released successfully, a thread in the synchronization queue will be awakened at the same time. The awakened thread will retry to modify the value of `state` to `state=state-1`. If `state>=0`, the token is obtained successfully, otherwise it will re-enter the blocking queue and suspend the thread. + +```java +// Release a license +public void release() { + sync.releaseShared(1); +} + +// Release the shared lock and wake up a thread in the synchronization queue. +public final boolean releaseShared(int arg) { + //Release shared lock + if (tryReleaseShared(arg)) { + //Wake up a thread in the synchronization queue + doReleaseShared(); + return true; + } + return false; +} +``` + +### What is the use of CountDownLatch? + +`CountDownLatch` allows `count` threads to block in one place until all threads' tasks are completed. + +`CountDownLatch` is one-time use. The value of the counter can only be initialized once in the constructor. There is no mechanism to set its value again afterwards. When `CountDownLatch` is used, it cannot be used again. + +### What is the principle of CountDownLatch? + +`CountDownLatch` is an implementation of shared locks. It constructs the `state` value of AQS by default as `count`. When the thread uses the `countDown()` method, it actually uses the `tryReleaseShared` method to reduce `state` with CAS operations until `state` is 0. When calling the `await()` method, if `state` is not 0, it proves that the task has not been completed, and the `await()` method will always block, which means that the statements after the `await()` method will not be executed. Until `count` threads call `countDown()` so that the state value is reduced to 0, or the thread calling `await()` is interrupted, the thread will be awakened from blocking, and the statements after the `await()` method will be executed. + +### Have you ever used CountDownLatch? In what scenario is it used? + +The function of `CountDownLatch` is to allow count threads to block in one place until all threads' tasks are completed. In the previous project, there was a scenario where multi-threading was used to read and process multiple files. I used `CountDownLatch`. The specific scenario is as follows: + +We need to read and process 6 files. These 6 tasks are all tasks that have no execution order dependency, but we need to statistically organize the results of processing these files when returning them to the user. + +For this we define a thread pool and a `CountDownLatch` object with a count of 6. Use the thread pool to handle the reading task. After each thread is processed, it will count-1 and call the `await()` method of the `CountDownLatch` object. The subsequent logic will not be executed until all files have been read. + +The pseudocode is as follows: + +```java +public class CountDownLatchExample1 { + //Number of files to process + private static final int threadCount = 6; + + public static void main(String[] args) throws InterruptedException { + // Create a thread pool object with a fixed number of threads (it is recommended to use the constructor method to create it) + 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 { + //Business operations for processing files + //...... + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + //Indicates that a file has been completed + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(); + threadPool.shutdown(); + System.out.println("finish"); + } +} +``` + +**Is there anything that can be improved? ** + +This can be improved using the `CompletableFuture` class! Java8's `CompletableFuture` provides many multi-thread-friendly methods. It can be used to easily write multi-threaded programs for us. It is very convenient to be asynchronous, serial, parallel or wait for all threads to finish executing tasks. + +```java +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //Customized business operations + }); +... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //Customized business operations + }); +... +CompletableFuture headerFuture=CompletableFuture.allOf(task1,....,task6); + +try { + headerFuture.join(); +} catch (Exception ex) { + //...... +} +System.out.println("all done. "); +``` + +The above code can continue to be optimized. When there are too many tasks, it is not practical to list every task. You can consider adding tasks through a loop. + +```java +//Folder location +List filePaths = Arrays.asList(...) +// Process all files asynchronously +List> fileFutures = filePaths.stream() + .map(filePath -> doSomeThing(filePath)) + .collect(Collectors.toList()); +// merge them +CompletableFuture allFutures = CompletableFuture.allOf( + fileFutures.toArray(new CompletableFuture[fileFutures.size()]) +); +``` + +### What is the use of CyclicBarrier? + +`CyclicBarrier` is very similar to `CountDownLatch`. It can also implement technical waiting between threads, but its function is more complex and powerful than `CountDownLatch`. The main application scenarios are similar to `CountDownLatch`. + +> The implementation of `CountDownLatch` is based on AQS, while `CyclicBarrier` is based on `ReentrantLock` (`ReentrantLock` also belongs to the AQS synchronizer) and `Condition`. + +`CyclicBarrier` literally means cyclic barrier. What it does is: let a group of threads be blocked when they reach a barrier (also called a synchronization point). The barrier will not open until the last thread reaches the barrier, and all threads intercepted by the barrier will continue to work. + +### What is the principle of CyclicBarrier? + +`CyclicBarrier` internally uses a `count` variable as a counter. The initial value of `count` is the initialization value of the `parties` attribute. Whenever a thread reaches the barrier, the counter is decremented by 1. If the count value is 0, it means that this is the last thread of this generation to reach the fence, and it will try to execute the task entered in our constructor. + +```java +//The number of threads intercepted each time +private final int parties; +//Counter +private int count; +``` + +Let’s take a brief look at the source code below.1. The default constructor of `CyclicBarrier` is `CyclicBarrier(int parties)`, whose parameters represent the number of threads intercepted by the barrier. Each thread calls the `await()` method to tell `CyclicBarrier` that I have reached the barrier, and then the current thread is blocked. + +```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; +} +``` + +Among them, `parties` represents the number of intercepted threads. When the number of intercepted threads reaches this value, the fence will be opened to allow all threads to pass. + +2. When calling the `await()` method of the `CyclicBarrier` object, the `dowait(false, 0L)` method is actually called. The `await()` method acts like erecting a fence, blocking threads. When the number of blocked threads reaches the value of `parties`, the fence will open and the threads can pass for execution. + +```java +public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } +} +``` + +The source code analysis of `dowait(false, 0L)` method is as follows: + +```java + // When the number of threads or the number of requests reaches count, the method after await will be executed. In the above example, the value of count is 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.lock(); + try { + final Generation g = generation; + + if (g.broken) + throw new BrokenBarrierException(); + + //If the thread is interrupted, throw an exception + if (Thread.interrupted()) { + breakBarrier(); + throw new InterruptedException(); + } + // cout decreases by 1 + int index = --count; + // When the count is reduced to 0, it means that the last thread has reached the fence, that is, it has reached the condition after which the await method can be executed. + if (index == 0) { // tripped + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + command.run(); + ranAction = true; + //Reset count to the initialization value of the parties property + // Wake up the previously waiting thread + //The next wave of execution begins + 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(); + } + } +``` + +## Virtual thread + +Virtual threads were officially released in Java 21, which is a major update. Although there are not many questions asked in interviews at present, it is still recommended that everyone take a brief look. I wrote an article to summarize common problems with virtual threads: [Summary of common problems with virtual threads](https://javaguide.cn/java/concurrent/virtual-thread.html), including the following questions: + +1. What is a virtual thread? +2. What is the relationship between virtual threads and platform threads? +3. What are the advantages and disadvantages of virtual threads? +4. How to create a virtual thread? +5. What is the underlying principle of virtual threads? + +## Reference + +- "In-depth Understanding of Java Virtual Machine" +- "Practical Java High Concurrency Programming" +- The implementation principle of Java thread pool and its best practices in business: Alibaba Cloud Developer: +- Let you know about SynchronousQueue (concurrent queue topic): - Blocking queue — DelayedWorkQueue source code analysis: +- Java multi-threading (3) - FutureTask/CompletableFuture: +- Detailed explanation of Java concurrency AQS: +- Cornerstone of Java concurrency package-AQS detailed explanation: + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-thread-pool-best-practices.en.md b/docs_en/java/concurrent/java-thread-pool-best-practices.en.md new file mode 100644 index 00000000000..e7cd2f64337 --- /dev/null +++ b/docs_en/java/concurrent/java-thread-pool-best-practices.en.md @@ -0,0 +1,308 @@ +--- +title: Java Thread Pool Best Practices +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: Thread pool best practices, ThreadPoolExecutor, Executors risks, bounded queue, OOM, rejection policy, monitoring, thread naming, parameter configuration + - - meta + - name: description + content: Summarizes the key practices and pitfall avoidance guidelines for using thread pools, emphasizing important matters such as manual configuration, avoiding Executors OOM risks, monitoring and naming, etc. +--- + +Let me briefly summarize what I know about what you should pay attention to when using a thread pool. There seems to be no article specifically written about this on the Internet. + +## 1. Correctly declare the thread pool + +**The thread pool must be declared manually through the constructor of `ThreadPoolExecutor` to avoid using the `Executors` class to create a thread pool, which may cause OOM risks. ** + +The disadvantages of `Executors` returning thread pool objects are as follows (will be introduced in detail later): + +- **`FixedThreadPool` and `SingleThreadExecutor`**: The blocking queue `LinkedBlockingQueue` is used. The default length and maximum length of the task queue are `Integer.MAX_VALUE`, which can be regarded as an unbounded queue, which may accumulate a large number of requests, resulting in OOM. +- **`CachedThreadPool`**: The synchronized queue `SynchronousQueue` is used, and the number of threads allowed to be created is `Integer.MAX_VALUE`. A large number of threads may be created, resulting in OOM. +- **`ScheduledThreadPool` and `SingleThreadScheduledExecutor`**: The unbounded delay blocking queue `DelayedWorkQueue` used, the maximum length of the task queue is `Integer.MAX_VALUE`, may accumulate a large number of requests, resulting in OOM. + +To put it bluntly: **Use a bounded queue to control the number of threads created. ** + +In addition to avoiding OOM, there are also reasons why using the two quick thread pools provided by `Executors` is not recommended: + +- In actual use, you need to manually configure the parameters of the thread pool such as the number of core threads, the task queue used, the saturation strategy, etc. according to the performance of your own machine and business scenarios. +- We should name our thread pool explicitly to help us locate the problem. + +## 2. Monitor the running status of the thread pool + +You can detect the running status of the thread pool through some means, such as the Actuator component in SpringBoot. + +In addition, we can also use the related API of `ThreadPoolExecutor` to do a simple monitoring. As can be seen from the figure below, `ThreadPoolExecutor` provides the ability to obtain the current number of threads and active threads in the thread pool, the number of completed tasks, the number of tasks in the queue, etc. + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-methods-information.png) + +Below is a simple demo. `printThreadPoolStatus()` will print out the number of threads in the thread pool, the number of active threads, the number of completed tasks, and the number of tasks in the queue every second. + +```java +/** + * Print the status of the thread pool + * + * @param threadPool thread pool object + */ +public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false)); + scheduledExecutorService.scheduleAtFixedRate(() -> { + log.info("========================="); + log.info("ThreadPool Size: [{}]", threadPool.getPoolSize()); + log.info("Active Threads: {}", threadPool.getActiveCount()); + log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount()); + log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size()); + log.info("========================="); + }, 0, 1, TimeUnit.SECONDS); +} +``` + +## 3. It is recommended to use different thread pools for different types of businesses. + +Many people will have questions similar to this in actual projects: **Multiple businesses in my project need to use thread pools. Should I define one for each thread pool or define a common thread pool? ** + +It is generally recommended that different businesses use different thread pools. When configuring the thread pool, configure the current thread pool according to the current business situation. Because the concurrency and resource usage of different businesses are different, focus on optimizing the business related to system performance bottlenecks. + +**Let’s look at a real accident case again! ** (This case comes from: ["An online accident involving improper use of thread pools"](https://heapdump.cn/article/646639), a very exciting case) + +![Case code overview](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-example.png) + +The above code may cause a deadlock. Why? Let me draw a picture to help you understand. + +Imagine such an extreme situation: Suppose the number of core threads in our thread pool is **n**, the number of parent tasks (deduction tasks) is **n**, and there are two subtasks (subtasks under the deduction task) under the parent task, one of which has been executed and the other is placed in the task queue. Since the parent task has used up the core thread resources of the thread pool, the subtask cannot execute normally because it cannot obtain the thread resources and has been blocked in the queue. The parent task waits for the child task to complete execution, and the child task waits for the parent task to release thread pool resources, which also causes **"deadlock"**. + +![Improper use of thread pool leads to deadlock](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-deadlock.png) + +The solution is also very simple, which is to add a new thread pool for executing subtasks specifically to serve it. + +## 4. Don’t forget to name the thread pool + +When initializing the thread pool, you need to display the naming (set the thread pool name prefix), which is helpful for locating problems. + +The name of the thread created by default is similar to `pool-1-thread-n`, which has no business meaning and is not conducive to us locating the problem. + +There are usually two ways to name threads in the thread pool: + +**1. Using 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) +``` + +**2. Implement `ThreadFactory` yourself. ** + +```java +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Thread factory, which sets the thread name, helps us locate problems. + */ +public final class NamingThreadFactory implements ThreadFactory { + + private final AtomicInteger threadNum = new AtomicInteger(); + private final String name; + + /** + * Create a named thread pool production factory + */ + 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; + } +}``` + +## 5、正确配置线程池参数 + +说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)! + +我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考。 + +### 常规操作 + +很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换** 成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 + +> 上下文切换: +> +> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 +> +> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 +> +> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 + +- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 +- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 + +有一个简单并且适用面比较广的公式: + +- **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 密集型任务不再推荐 N+1,原因如下: + +- "N+1" 的初衷是希望预留线程处理突发暂停,但实际上,处理缺页中断等情况仍然需要占用 CPU 核心。 +- CPU 密集场景下,CPU 始终是瓶颈,预留线程并不能凭空增加 CPU 处理能力,反而可能加剧竞争。 + +**如何判断是 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`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +**为什么是这三个参数?** + +我在这篇[《新手也能看懂的线程池学习总结》](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` 最重要的参数,它们基本决定了线程池对于任务的处理策略。 + +**如何支持参数动态配置?** 且看 `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) + +如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: + +- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 +- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 + +## 6、别忘记关闭线程池 + +当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。 + +线程池提供了两个关闭方法: + +- **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 +- **`shutdownNow()`**: Shut down the thread pool and the status of the thread pool changes to `STOP`. The thread pool terminates the currently running tasks, stops processing queued tasks and returns the List waiting to be executed. + +After calling the `shutdownNow` and `shuwdown` methods, it does not mean that the thread pool has completed the shutdown operation. It just asynchronously notifies the thread pool for shutdown processing. If you want to wait synchronously for the thread pool to be completely closed before continuing execution, you need to call the `awaitTermination` method to wait synchronously. + +When calling the `awaitTermination()` method, a reasonable timeout should be set to avoid the program blocking for a long time and causing performance problems. in addition. Since tasks in the thread pool may be canceled or throw exceptions, exception handling is also required when using the `awaitTermination()` method. The `awaitTermination()` method will throw an `InterruptedException` exception, which needs to be caught and handled to avoid the program crashing or failing to exit normally. + +```java +// ... +// Close the thread pool +executor.shutdown(); +try { + // Wait for the thread pool to close, up to 5 minutes + if (!executor.awaitTermination(5, TimeUnit.MINUTES)) { + //If the wait times out, print the log + System.err.println("The thread pool failed to be completely closed within 5 minutes"); + } +} catch (InterruptedException e) { + //Exception handling +} +``` + +## 7. Try not to put time-consuming tasks in the thread pool. + +The purpose of the thread pool itself is to improve task execution efficiency and avoid performance overhead caused by frequent creation and destruction of threads. If time-consuming tasks are submitted to the thread pool for execution, the threads in the thread pool may be occupied for a long time, unable to respond to other tasks in time, and may even cause the thread pool to crash or the program to freeze. + +Therefore, when using the thread pool, we should try to avoid submitting time-consuming tasks to the thread pool for execution. For some time-consuming operations, such as network requests, file reading and writing, etc., you can use `CompletableFuture` and other asynchronous operations to handle them to avoid blocking threads in the thread pool. + +## 8. Some pitfalls in the use of thread pools + +### Pitfalls of repeatedly creating thread pools + +Thread pools can be reused. Be sure not to create thread pools frequently. For example, when a user request arrives, create a separate thread pool. + +```java +@GetMapping("wrong") +public String wrong() throws InterruptedException { + // Custom thread pool + ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy()); + + // Process tasks + executor.execute(() -> { + //...... + } + return "OK"; +} +``` + +The reason for this problem is still insufficient understanding of the thread pool, and it is necessary to strengthen the basic knowledge of the thread pool. + +### The Pitfalls of Spring’s Internal Thread Pool + +When using Spring's internal thread pool, you must manually customize the thread pool and configure reasonable parameters, otherwise production problems will occur (one request creates a thread). + +```java +@Configuration +@EnableAsync +public class ThreadPoolExecutorConfig { + + @Bean(name="threadPoolExecutor") + public Executor threadPoolExecutor(){ + ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor(); + int processNum = Runtime.getRuntime().availableProcessors(); // Returns the number of available processors in the Java virtual machine + int corePoolSize = (int) (processNum / (1 - 0.2)); + int maxPoolSize = (int) (processNum / (1 - 0.5)); + threadPoolExecutor.setCorePoolSize(corePoolSize); // Core pool size + threadPoolExecutor.setMaxPoolSize(maxPoolSize); // Maximum number of threads + threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // Queue length + threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY); + threadPoolExecutor.setDaemon(false); + threadPoolExecutor.setKeepAliveSeconds(300);//Thread idle time + threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // Thread name prefix + return threadPoolExecutor; + } +} +``` + +### Pitfalls shared by thread pool and ThreadLocal + +The thread pool and `ThreadLocal` are shared, which may cause the thread to obtain old values/dirty data from `ThreadLocal`. This is because the thread pool will reuse the thread object, and the static attribute `ThreadLocal` variable of the class bound to the thread object will also be reused, which results in one thread possibly obtaining the `ThreadLocal` value of other threads. + +Don't think that the thread pool does not exist if the thread pool is not shown in the code. For example, in order to increase the concurrency of the commonly used Web server Tomcat processing tasks, the thread pool is used, and a custom thread pool based on the improvement of the native Java thread pool is used. + +Of course, you can set up Tomcat to handle tasks in a single thread. However, this is not suitable and will seriously affect the speed of its processing tasks. + +```properties +server.tomcat.max-threads=1 +``` + +The recommended way to solve the above problem is to use Alibaba's open source `TransmittableThreadLocal` (`TTL`). The `TransmittableThreadLocal` class inherits and enhances the JDK's built-in `InheritableThreadLocal` class. When using thread pools and other execution components that pool multiplexed threads, it provides a `ThreadLocal` value transfer function to solve the problem of context transfer during asynchronous execution. + +`TransmittableThreadLocal` project address: . + + \ No newline at end of file diff --git a/docs_en/java/concurrent/java-thread-pool-summary.en.md b/docs_en/java/concurrent/java-thread-pool-summary.en.md new file mode 100644 index 00000000000..b6a0ff12b0f --- /dev/null +++ b/docs_en/java/concurrent/java-thread-pool-summary.en.md @@ -0,0 +1,832 @@ +--- +title: Java 线程池详解 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: 线程池,ThreadPoolExecutor,Executor,核心线程数,最大线程数,任务队列,拒绝策略,池化技术,ScheduledThreadPoolExecutor + - - meta + - name: description + content: 系统梳理 Java 线程池的原理与架构,包含 Executor 框架、关键参数与队列、常见实现及配置要点。 +--- + + + +池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 + +这篇文章我会详细介绍一下线程池的基本概念以及核心原理。 + +## 线程池介绍 + +池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 + +线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处: + +1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。 +2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。 +3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。 + +## Executor 框架介绍 + +`Executor` 框架是 Java5 之后引进的,在 Java 5 之后,通过 `Executor` 来启动线程比使用 `Thread` 的 `start` 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 + +> this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 + +`Executor` 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,`Executor` 框架让并发编程变得更加简单。 + +`Executor` 框架结构主要由三大部分组成: + +**1、任务(`Runnable` /`Callable`)** + +执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。 + +**2、任务的执行(`Executor`)** + +如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **`ExecutorService`** 接口。 + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executor-class-diagram.png) + +这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。 + +**注意:** 通过查看 `ScheduledThreadPoolExecutor` 源代码我们发现 `ScheduledThreadPoolExecutor` 实际上是继承了 `ThreadPoolExecutor` 并实现了 `ScheduledExecutorService` ,而 `ScheduledExecutorService` 又实现了 `ExecutorService`,正如我们上面给出的类关系图显示的一样。 + +`ThreadPoolExecutor` 类描述: + +```java +//AbstractExecutorService实现了ExecutorService接口 +public class ThreadPoolExecutor extends AbstractExecutorService +``` + +`ScheduledThreadPoolExecutor` 类描述: + +```java +//ScheduledExecutorService继承ExecutorService接口 +public class ScheduledThreadPoolExecutor + extends ThreadPoolExecutor + implements ScheduledExecutorService +``` + +**3、异步计算的结果(`Future`)** + +**`Future`** 接口以及 `Future` 接口的实现类 **`FutureTask`** 类都可以代表异步计算的结果。 + +当我们把 **`Runnable`接口** 或 **`Callable` 接口** 的实现类提交给 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。(调用 `submit()` 方法时会返回一个 **`FutureTask`** 对象) + +**`Executor` 框架的使用示意图**: + +![Executor 框架的使用示意图](./images/java-thread-pool-summary/Executor框架的使用示意图.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)`来取消此任务的执行。 + +## ThreadPoolExecutor 类介绍(重要) + +线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。 + +### 线程池参数分析 + +`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。 + +```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` 拒绝策略定义:** + +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`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(); + } + } + } +``` + +### 线程池创建的两种方式 + +在 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 开发手册》强制线程池不允许使用 `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()); +} +``` + +### Summary of commonly used blocking queues in thread pools + +When a new task comes, it will first determine whether the number of currently running threads reaches the number of core threads. If so, the new task will be stored in the queue. + +Different thread pools will use different blocking queues, which we can analyze with the built-in thread pool. + +- `LinkedBlockingQueue` (unbounded queue) with capacity `Integer.MAX_VALUE`: `FixedThreadPool` and `SingleThreadExector`. `FixedThreadPool` can only create threads with a maximum number of core threads (the number of core threads and the maximum number of threads are equal), and `SingleThreadExector` can only create one thread (the number of core threads and the maximum number of threads are both 1). The task queues of both will never be full. +- `SynchronousQueue`: `CachedThreadPool`. `SynchronousQueue` has no capacity and does not store elements. The purpose is to ensure that for submitted tasks, if there is an idle thread, the idle thread will be used to process it; otherwise, a new thread will be created to process the task. In other words, the maximum number of threads of `CachedThreadPool` is `Integer.MAX_VALUE`, which can be understood as the number of threads can be infinitely expanded, and a large number of threads may be created, resulting in OOM. +- `DelayedWorkQueue` (delayed blocking queue): `ScheduledThreadPool` and `SingleThreadScheduledExecutor`. The internal elements of `DelayedWorkQueue` are not sorted according to the time they are put in, but the tasks are sorted according to the length of delay. The internal "heap" data structure is used to ensure that each task dequeued is the one with the longest execution time in the current queue. `DelayedWorkQueue` will automatically expand to 1/2 of its original capacity after the added elements are full, that is, it will never block. The maximum expansion can reach `Integer.MAX_VALUE`, so only the number of core threads can be created at most. + +## Analysis of thread pool principle (important) + +We have explained the `Executor` framework and the `ThreadPoolExecutor` class above. Let us practice it and review the above content by writing a small Demo of `ThreadPoolExecutor`. + +### Thread pool sample code + +First create an implementation class of the `Runnable` interface (of course it can also be the `Callable` interface, we will introduce the difference between the two later.) + +`MyRunnable.java` + +```java +import java.util.Date; + +/** + * This is a simple Runnable class that takes about 5 seconds to perform its task. + * @author shuang.kou + */ +public class MyRunnable implements Runnable { + + private String command; + + public MyRunnable(String s) { + this.command = s; + } + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); + processCommand(); + System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); + } + + private void processCommand() { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public String toString() { + return this.command; + } +} + +``` + +To write a test program, we use the method recommended by Alibaba to use the `ThreadPoolExecutor` constructor custom parameters to create a thread pool. + +`ThreadPoolExecutorDemo.java` + +```java +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorDemo { + + private static final int CORE_POOL_SIZE = 5; + 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) { + + //Use the method recommended by Alibaba to create a thread pool + //Created through custom parameters of ThreadPoolExecutor constructor + ThreadPoolExecutor executor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for (int i = 0; i < 10; i++) { + //Create WorkerThread object (WorkerThread class implements Runnable interface) + Runnable worker = new MyRunnable("" + i); + //Execute Runnable + executor.execute(worker); + } + //terminate thread pool + executor.shutdown(); + while (!executor.isTerminated()) { + } + System.out.println("Finished all threads"); + } +} + +``` + +You can see that our code above specifies: + +- `corePoolSize`: The number of core threads is 5. +- `maximumPoolSize`: Maximum number of threads 10 +- `keepAliveTime` : The waiting time is 1L. +- `unit`: The unit of waiting time is TimeUnit.SECONDS. +- `workQueue`: The task queue is `ArrayBlockingQueue` and the capacity is 100; +- `handler`: Deny policy is `CallerRunsPolicy`. + +**Output structure**: + +```plain +pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020 +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 // The tasks will not jump out until all tasks are executed, because executor.isTerminated() will jump out of the while loop when it is judged to be true. If and only if the shutdown() method is called, and all submitted tasks are completed, it will return to true.``` + +### 线程池原理分析 + +我们通过前面的代码输出结果可以看出:**线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) + +现在,我们就分析上面的输出内容来简单分析一下线程池原理。 + +为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。 在示例代码中,我们使用 `executor.execute(worker)`来提交一个任务到线程池中去。 + +这个方法非常重要,下面我们来看看它的源码: + +```java + // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + + private static int workerCountOf(int c) { + return c & CAPACITY; + } + //任务队列 + private final BlockingQueue workQueue; + + public void execute(Runnable command) { + // 如果任务为null,则抛出异常。 + if (command == null) + throw new NullPointerException(); + // ctl 中保存的线程池当前的一些状态信息 + int c = ctl.get(); + + // 下面会涉及到 3 步 操作 + // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize + // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里,表明创建新的线程失败。 + // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 + if (!isRunning(recheck) && remove(command)) + reject(command); + // 如果当前工作线程数量为0,新创建一个线程并执行。 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + // 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize + //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 + else if (!addWorker(command, false)) + reject(command); + } +``` + +这里简单分析一下整个流程(对整个逻辑进行了简化,方便理解): + +1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 +2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 +3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 +4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 + +![图解线程池实现原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) + +在 `execute` 方法中,多次调用 `addWorker` 方法。`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。 + +```java + // 全局锁,并发操作必备 + private final ReentrantLock mainLock = new ReentrantLock(); + // 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合 + private int largestPoolSize; + // 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合 + private final HashSet workers = new HashSet<>(); + //获取线程池状态 + private static int runStateOf(int c) { return c & ~CAPACITY; } + //判断线程池的状态是否为 Running + private static boolean isRunning(int c) { + return c < SHUTDOWN; + } + + + /** + * 添加新的工作线程到线程池 + * @param firstTask 要执行 + * @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小 + * @return 添加成功就返回true否则返回false + */ + private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + //这两句用来获取线程池的状态 + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + //获取线程池中工作的线程的数量 + int wc = workerCountOf(c); + // core参数为false的话表明队列也满了,线程池大小变为 maximumPoolSize + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + //原子操作将workcount的数量加1 + if (compareAndIncrementWorkerCount(c)) + break retry; + // 如果线程的状态改变了就再次执行上述操作 + c = ctl.get(); + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + // 标记工作线程是否启动成功 + boolean workerStarted = false; + // 标记工作线程是否创建成功 + boolean workerAdded = false; + Worker w = null; + try { + + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + // 加锁 + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + //获取线程池状态 + int rs = runStateOf(ctl.get()); + //rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中 + //(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker + // firstTask == null证明只新建线程而不执行任务 + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + //更新当前工作线程的最大容量 + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + // 工作线程是否启动成功 + workerAdded = true; + } + } finally { + // 释放锁 + mainLock.unlock(); + } + //// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例 + if (workerAdded) { + t.start(); + /// 标记线程启动成功 + workerStarted = true; + } + } + } finally { + // 线程启动失败,需要从工作线程中移除对应的Worker + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; + } +``` + +For more content on thread pool source code analysis, I recommend this article: Hardcore dry stuff: [4W words to analyze the implementation principle of JUC thread pool ThreadPoolExecutor from the source code] (https://www.cnblogs.com/throwable/p/13574306.html). + +Now, let's go back to the sample code. Shouldn't it be easy to understand how it works? + +If you don’t understand it, it doesn’t matter. You can take a look at my analysis: + +> We simulated 10 tasks in the code. We configured the number of core threads to be 5 and the waiting queue capacity to be 100, so only 5 tasks may be executed at the same time each time, and the remaining 5 tasks will be placed in the waiting queue. If any of the current five tasks has been executed, the thread pool will get a new task for execution. + +### Several common comparisons + +#### `Runnable` vs `Callable` + +`Runnable` has been around since Java 1.0, but `Callable` was only 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, but the `Callable` interface does. Therefore, if the task does not need to return results or throw exceptions, it is recommended to use the `Runnable` interface, so that the code will look more concise. + +The utility class `Executors` can convert `Runnable` objects into `Callable` objects. (`Executors.callable(Runnable task)` or `Executors.callable(Runnable task, Object result)`). + +`Runnable.java` + +```java +@FunctionalInterface +public interface Runnable { + /** + * Executed by a thread, there is no return value and no exception can be thrown + */ + public abstract void run(); +} +``` + +`Callable.java` + +```java +@FunctionalInterface +public interface Callable { + /** + * Compute the result, or throw an exception if this cannot be done. + * @return calculated result + * @throws throws an exception if the result cannot be calculated + */ + V call() throws Exception; +} + +``` + +#### `execute()` vs `submit()` + +`execute()` and `submit()` are two methods of submitting tasks to the thread pool. There are some differences: + +- **Return value**: The `execute()` method is used to submit tasks that do not require a return value. Usually used to execute `Runnable` tasks. It is impossible to determine whether the task is successfully executed by the thread pool. The `submit()` method is used to submit tasks that require return values. Can submit `Runnable` or `Callable` tasks. The `submit()` method returns a `Future` object, through which the `Future` object can be used to determine whether the task is executed successfully and obtain the return value of the task (the `get()` method will block the current thread until the task is completed, `get(long timeout, TimeUnit unit)` has an additional timeout period, if the task has not been executed within the `timeout` time, a `java.util.concurrent.TimeoutException` will be thrown). +- **Exception handling**: When using the `submit()` method, you can use the `Future` object to handle exceptions thrown during task execution; when using the `execute()` method, exception handling needs to be done through a custom `ThreadFactory` (the `UncaughtExceptionHandler` object is set when the thread factory creates the thread to handle exceptions) or `ThreadPoolExecutor`'s `afterExecute()` method to deal with + +Example 1: Use the `get()` method to get the return value. + +```java +// This is just for demonstration purposes. It is recommended to use the `ThreadPoolExecutor` constructor to create a thread pool. +ExecutorService executorService = Executors.newFixedThreadPool(3); + +Future submit = executorService.submit(() -> { + try { + Thread.sleep(5000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "abc"; +}); + +String s = submit.get(); +System.out.println(s); +executorService.shutdown(); +``` + +Output: + +```plain +abc +``` + +Example 2: Use the `get(long timeout, TimeUnit unit)` method to get the return value. + +```java +ExecutorService executorService = Executors.newFixedThreadPool(3); + +Future submit = executorService.submit(() -> { + try { + Thread.sleep(5000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "abc"; +}); + +String s = submit.get(3, TimeUnit.SECONDS); +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()`**: Shut down the thread pool and the status of the thread pool changes to `SHUTDOWN`. The thread pool no longer accepts new tasks, but the tasks in the queue must be completed. +- **`shutdownNow()`**: Shut down the thread pool and the status of the thread pool changes to `STOP`. The thread pool terminates the currently running tasks, stops processing queued tasks and returns the List waiting to be executed. + +#### `isTerminated()` VS `isShutdown()` + +- **`isShutDown`** Returns true when calling the `shutdown()` method. +- **`isTerminated`** returns true when the `shutdown()` method is called and all submitted tasks are completed + +## Several common built-in thread pools + +### FixedThreadPool + +#### Introduction + +`FixedThreadPool` is known as a thread pool that can reuse a fixed number of threads. Let’s take a look at the relevant implementation through the relevant source code in the `Executors` class: + +```java + /** + * Create a thread pool that can reuse a fixed number of threads + */ + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory); + } +``` + +There is also an implementation method of `FixedThreadPool`, which is similar to the above, so I won’t elaborate on it here: + +```java + public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + } +```From the source code above, we can see that the `corePoolSize` and `maximumPoolSize` of the newly created `FixedThreadPool` are both set to `nThreads`. This `nThreads` parameter is passed by ourselves when we use it. + +Even if the value of `maximumPoolSize` is greater than `corePoolSize`, at most `corePoolSize` threads will be created. This is because `FixedThreadPool` uses a `LinkedBlockingQueue` (unbounded queue) with a capacity of `Integer.MAX_VALUE`, and the queue will never be filled. + +#### Introduction to the task execution process + +Schematic diagram of the `execute()` method of `FixedThreadPool` (source of this picture: "The Art of Java Concurrent Programming"): + +![FixedThreadPool’s execute() method operation diagram](./images/java-thread-pool-summary/FixedThreadPool.png) + +**Description of the above picture:** + +1. If the number of currently running threads is less than `corePoolSize`, if a new task comes again, a new thread will be created to execute the task; +2. After the number of currently running threads equals `corePoolSize`, if a new task comes, the task will be added to `LinkedBlockingQueue`; +3. After the thread in the thread pool completes the task at hand, it will repeatedly obtain the task from `LinkedBlockingQueue` in the loop for execution; + +#### Why is `FixedThreadPool` not recommended? + +`FixedThreadPool` uses the unbounded queue `LinkedBlockingQueue` (the capacity of the queue is Integer.MAX_VALUE) as the work queue of the thread pool, which will have the following effects on the thread pool: + +1. When the number of threads in the thread pool reaches `corePoolSize`, new tasks will wait in the unbounded queue, so the number of threads in the thread pool will not exceed `corePoolSize`; +2. `maximumPoolSize` will be an invalid parameter when using an unbounded queue, because it is impossible for the task queue to be full. Therefore, by creating the source code of `FixedThreadPool`, we can see that the `corePoolSize` and `maximumPoolSize` of the created `FixedThreadPool` are set to the same value. +3. Due to 1 and 2, `keepAliveTime` will be an invalid parameter when using unbounded queue; +4. The running `FixedThreadPool` (without executing `shutdown()` or `shutdownNow()`) will not reject tasks, and will cause OOM (memory overflow) when there are many tasks. + +### SingleThreadExecutor + +#### Introduction + +`SingleThreadExecutor` is a thread pool with only one thread. Let’s take a look at the implementation of **SingleThreadExecutor:** + +```java + /** + *Returns a thread pool with only one thread + */ + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory)); + } +``` + +```java + public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); + } +``` + +From the source code above, we can see that the `corePoolSize` and `maximumPoolSize` of the newly created `SingleThreadExecutor` are both set to 1, and the other parameters are the same as `FixedThreadPool`. + +#### Introduction to the task execution process + +Operation diagram of `SingleThreadExecutor` (source of this picture: "The Art of Java Concurrent Programming"): + +![SingleThreadExecutor operation diagram](./images/java-thread-pool-summary/SingleThreadExecutor.png) + +**Description of the above picture**: + +1. If the number of currently running threads is less than `corePoolSize`, create a new thread to perform the task; +2. After there is a running thread in the current thread pool, add the task to `LinkedBlockingQueue` +3. After the thread completes the current task, it will repeatedly obtain tasks from `LinkedBlockingQueue` in the loop for execution; + +#### Why is `SingleThreadExecutor` not recommended? + +`SingleThreadExecutor`, like `FixedThreadPool`, uses `LinkedBlockingQueue` (unbounded queue) with a capacity of `Integer.MAX_VALUE` as the work queue of the thread pool. `SingleThreadExecutor` using an unbounded queue as the thread pool's work queue will have the same impact on the thread pool as `FixedThreadPool`. To put it simply, it may cause OOM. + +### CachedThreadPool + +#### Introduction + +`CachedThreadPool` is a thread pool that creates new threads as needed. Let’s take a look at the implementation of `CachedThreadPool` through the source code: + +```java + /** + * Create a thread pool that creates new threads as needed, but reuses previously built threads when they become available. + */ + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory); + } + +``` + +```java + public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); + } +``` + +The `corePoolSize` of `CachedThreadPool` is set to empty (0), and the `maximumPoolSize` is set to `Integer.MAX.VALUE`, that is, it is unbounded. This means that if the speed of the main thread submitting tasks is higher than the speed of thread processing tasks in `maximumPool`, `CachedThreadPool` will continue to create new threads. In extreme cases, this can lead to exhaustion of cpu and memory resources. + +#### Introduction to the task execution process + +Schematic diagram of the execution of the `execute()` method of `CachedThreadPool` (source of this picture: "The Art of Java Concurrent Programming"): + +![Execution diagram of the execute() method of CachedThreadPool](./images/java-thread-pool-summary/CachedThreadPool-execute.png) + +**Description of the above picture:**1. First execute `SynchronousQueue.offer(Runnable task)` to submit the task to the task queue. If there is an idle thread in the current `maximumPool` that is executing `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`, then the main thread executes the offer operation and the `poll` operation executed by the idle thread is paired successfully, the main thread hands the task to the idle thread for execution, and the execution of the `execute()` method is completed, otherwise proceed to step 2 below; +2. When the initial `maximumPool` is empty, or there are no idle threads in `maximumPool`, no thread will execute `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`. In this case, step 1 will fail. At this time, `CachedThreadPool` will create a new thread to perform the task, and the execute method will be executed; + +#### Why is `CachedThreadPool` not recommended? + +`CachedThreadPool` uses a synchronized queue `SynchronousQueue`, and the number of threads allowed to be created is `Integer.MAX_VALUE`. A large number of threads may be created, causing OOM. + +### ScheduledThreadPool + +#### Introduction + +`ScheduledThreadPool` is used to run tasks after a given delay or execute tasks periodically. This is basically not used in actual projects and is not recommended. You just need to have a brief understanding of it. + +```java +public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); +} +public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); +} +``` + +`ScheduledThreadPool` is created through `ScheduledThreadPoolExecutor` and uses `DelayedWorkQueue` (delayed blocking queue) as the task queue of the thread pool. + +The internal elements of `DelayedWorkQueue` are not sorted according to the time they are put in, but the tasks are sorted according to the length of delay. The internal "heap" data structure is used to ensure that each task dequeued is the one with the longest execution time in the current queue. `DelayedWorkQueue` will automatically expand to 1/2 of its original capacity after the added elements are full, that is, it will never block. The maximum expansion can reach `Integer.MAX_VALUE`, so only the number of core threads can be created at most. + +`ScheduledThreadPoolExecutor` inherits `ThreadPoolExecutor`, so creating `ScheduledThreadExecutor` is essentially creating a `ThreadPoolExecutor` thread pool, but the parameters passed in are different. + +```java +public class ScheduledThreadPoolExecutor + extends ThreadPoolExecutor + implements ScheduledExecutorService +``` + +#### Comparison between ScheduledThreadPoolExecutor and Timer + +- `Timer` is sensitive to changes in the system clock, `ScheduledThreadPoolExecutor` is not; +- `Timer` has only one thread of execution, so long-running tasks can delay other tasks. `ScheduledThreadPoolExecutor` can be configured with any number of threads. Furthermore, you can have full control over the created threads if you want (by providing a `ThreadFactory`); +- A runtime exception thrown in `TimerTask` will kill a thread, causing `Timer` to freeze, i.e. the scheduled task will no longer run. `ScheduledThreadExecutor` not only catches runtime exceptions, but also allows you to handle them if needed (by overriding the `afterExecute` method of `ThreadPoolExecutor`). The task that throws the exception will be canceled, but other tasks will continue to run. + +For a detailed introduction to scheduled tasks, you can read this article: [Java scheduled tasks detailed explanation](https://javaguide.cn/system-design/schedule-task.html). + +## Thread pool best practices + +[Java Thread Pool Best Practices](https://javaguide.cn/java/concurrent/java-thread-pool-best-practices.html) This article summarizes some things that should be paid attention to when using thread pools. You can take a look before using thread pools in actual projects. + +## Reference + +- "The Art of Concurrent Programming in Java" +- [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") + + \ No newline at end of file diff --git a/docs_en/java/concurrent/jmm.en.md b/docs_en/java/concurrent/jmm.en.md new file mode 100644 index 00000000000..7067c7cd852 --- /dev/null +++ b/docs_en/java/concurrent/jmm.en.md @@ -0,0 +1,244 @@ +--- +title: JMM(Java 内存模型)详解 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: CPU 缓存模型,指令重排序,Java 内存模型(JMM),happens-before + - - meta + - name: description + content: 对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。 +--- + +JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。 + +要想理解透彻 JMM(Java 内存模型),我们先要从 **CPU 缓存模型和指令重排序** 说起! + +## 从 CPU 缓存模型说起 + +**为什么要弄一个 CPU 高速缓存呢?** 类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。** + +我们甚至可以把 **内存看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。 + +总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** + +为了更好地理解,我画了一个简单的 CPU Cache 示意图如下所示。 + +> **🐛 修正(参见:[issue#1848](https://github.com/Snailclimb/JavaGuide/issues/1848))**:对 CPU 缓存模型绘图不严谨的地方进行完善。 + +![CPU 缓存模型示意图](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) + +现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。有些 CPU 可能还有 L4 Cache,这里不做讨论,并不常见 + +**CPU Cache 的工作方式:** 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 + +**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 [MESI 协议](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE))或者其他手段来解决。** 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。 + +![缓存一致性协议](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache-protocol.png) + +我们的程序运行在操作系统之上,操作系统屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化。于是,操作系统也就同样需要解决内存缓存不一致性问题。 + +操作系统通过 **内存模型(Memory Model)** 定义一系列规范来解决这个问题。无论是 Windows 系统,还是 Linux 系统,它们都有特定的内存模型。 + +## 指令重排序 + +说完了 CPU 缓存模型,我们再来看看另外一个比较重要的概念 **指令重排序** 。 + +为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 + +**什么是指令重排序?** 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。 + +常见的指令重排序有下面 2 种情况: + +- **编译器优化重排**:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。 +- **指令并行重排**:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 + +另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 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 variable rule**: A write operation to a volatile variable happens-before a subsequent read operation to the volatile variable. To put it bluntly, the result of a write operation to a volatile variable is visible to any subsequent operations. +4. **Transitive Rule**: If A happens-before B, and B happens-before C, then A happens-before C; +5. **Thread startup rules**: The `start()` method of the Thread object happens-before every action of this thread. + +If two operations do not satisfy any of the above happens-before rules, then the order of the two operations is not guaranteed, and the JVM can reorder the two operations. + +### What is the relationship between happens-before and JMM? + +The relationship between happens-before and JMM can be explained very clearly with a picture in the book "The Art of Java Concurrent Programming". + +![The relationship between happenings-before and JMM](https://oss.javaguide.cn/github/javaguide/java/concurrent/image-20220731084604667.png) + +## Let’s look at three important features of concurrent programming + +### Atomicity + +For one operation or multiple operations, either all operations are executed and will not be interrupted by any factors, or none are executed. + +In Java, atomicity can be achieved with the help of `synchronized`, various `Lock` and various atomic classes. + +`synchronized` and various `Lock` can ensure that only one thread accesses the code block at any time, thus ensuring atomicity. Various atomic classes use CAS (compare and swap) operations (the `volatile` or `final` keywords may also be used) to ensure atomic operations. + +### Visibility + +When a thread modifies a shared variable, other threads can immediately see the latest modified value. + +In Java, visibility can be achieved with the help of `synchronized`, `volatile` and various `Lock`. + +If we declare a variable as `volatile`, this indicates to the JVM that this variable is shared and volatile and will be read from main memory every time it is used. + +### Orderliness + +Due to instruction reordering, the order in which the code is executed is not necessarily the order in which the code was written. + +We also mentioned this when we talked about reordering above: + +> **Instruction reordering can ensure consistent serial semantics, but there is no obligation to ensure consistent semantics across multiple threads**, so under multi-threading, instruction reordering may cause some problems. + +In Java, the `volatile` keyword can disable instruction reordering optimization. + +## Summary + +- Java is the first language to try to provide a memory model. Its main purpose is to simplify multi-threaded programming and enhance program portability. +- The CPU can solve the memory cache inconsistency problem by developing a cache coherence protocol (such as the [MESI protocol](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE)). +- In order to improve execution speed/performance, the computer will reorder instructions when executing program code. To put it simply, when the system executes the code, it does not necessarily execute it in the order of the code you wrote. **Instruction reordering can ensure consistent serial semantics, but there is no obligation to ensure consistent semantics across multiple threads**, so under multi-threading, instruction reordering may cause some problems. +- 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 which concurrency-related principles and specifications must be followed during the conversion process from Java source code to CPU executable instructions. Its main purpose is to simplify multi-threaded programming and enhance program portability. +- JSR 133 introduces the concept of happens-before to describe memory visibility between two operations. + +## Reference + +- Chapter 3 of "The Art of Concurrent Programming in Java" Java Memory Model +- "In-depth introduction to Java multi-threading": +- Research on Java memory access reordering: +- Hey, classmate, the Java Memory Model (JMM) you want is here: +- JSR 133 (Java Memory Model) FAQ: + + \ No newline at end of file diff --git a/docs_en/java/concurrent/optimistic-lock-and-pessimistic-lock.en.md b/docs_en/java/concurrent/optimistic-lock-and-pessimistic-lock.en.md new file mode 100644 index 00000000000..a0dfdc78417 --- /dev/null +++ b/docs_en/java/concurrent/optimistic-lock-and-pessimistic-lock.en.md @@ -0,0 +1,121 @@ +--- +title: 乐观锁和悲观锁详解 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: 乐观锁,悲观锁,synchronized,ReentrantLock,CAS,版本号,并发控制,死锁,性能 + - - meta + - name: description + content: 对比乐观锁与悲观锁的思想与实现,结合 synchronized、ReentrantLock 与 CAS 的应用场景与优劣分析。 +--- + +如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 + +## 什么是悲观锁? + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +像 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 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 + +关于 CAS 的进一步介绍,可以阅读读者写的这篇文章:[CAS 详解](./cas.md),其中详细提到了 Java 中 CAS 的实现以及 CAS 存在的一些问题。 + +## 总结 + +本文详细介绍了乐观锁和悲观锁的概念以及乐观锁常见实现方式: + +- 悲观锁基于悲观的假设,认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。Java 中的 `synchronized` 和 `ReentrantLock` 是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。 +- 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。Java 中的 `AtomicInteger` 和 `LongAdder` 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。 +- 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。 + +悲观锁和乐观锁各有优缺点,适用于不同的应用场景。在实际开发中,选择合适的锁机制能够有效提升系统的并发性能和稳定性。 + +## 参考 + +- 《Java 并发编程核心 78 讲》 +- Easy to understand pessimistic lock, optimistic lock, reentrant lock, spin lock, bias lock, lightweight/heavyweight lock, read-write lock, various locks and their Java implementation! : + + \ No newline at end of file diff --git a/docs_en/java/concurrent/reentrantlock.en.md b/docs_en/java/concurrent/reentrantlock.en.md new file mode 100644 index 00000000000..f25db5d8775 --- /dev/null +++ b/docs_en/java/concurrent/reentrantlock.en.md @@ -0,0 +1,1023 @@ +--- +title: 从ReentrantLock的实现看AQS的原理及应用 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: ReentrantLock,AQS,公平锁,非公平锁,可重入,lock/unlock,Sync Queue,独占锁,compareAndSetState,acquire + - - meta + - name: description + content: 结合 ReentrantLock 的实现剖析 AQS 工作原理,比较公平与非公平锁、与 synchronized 的差异以及独占锁的加解锁流程。 +--- + +> 本文转载自: +> +> 作者:美团技术团队 + +Java 中的大部分同步类(Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。 + +本文会从应用层逐渐深入到原理层,并通过 ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点,同时采取问答的模式来帮助大家理解 AQS。由于篇幅原因,本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue,不讲述包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。 + +## 1 ReentrantLock + +### 1.1 ReentrantLock 特性概览 + +ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点): + +![](https://p0.meituan.net/travelcube/412d294ff5535bbcddc0d979b2a339e6102264.png) + +下面通过伪代码,进行更加直观的比较: + +```java +// **************************Synchronized的使用方式************************** +// 1.用于代码块 +synchronized (this) {} +// 2.用于对象 +synchronized (object) {} +// 3.用于方法 +public synchronized void test () {} +// 4.可重入 +for (int i = 0; i < 100; i++) { + synchronized (this) {} +} +// **************************ReentrantLock的使用方式************************** +public void test () throw Exception { + // 1.初始化选择公平锁、非公平锁 + ReentrantLock lock = new ReentrantLock(true); + // 2.可用于代码块 + lock.lock(); + try { + try { + // 3.支持多种加锁方式,比较灵活; 具有可重入特性 + if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } + } finally { + // 4.手动释放锁 + lock.unlock() + } + } finally { + lock.unlock(); + } +} +``` + +### 1.2 ReentrantLock 与 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 的关联比较明显,解锁流程后续会介绍)。 + +非公平锁源码中的加锁流程如下: + +```java +// java.util.concurrent.locks.ReentrantLock#NonfairSync + +// 非公平锁 +static final class NonfairSync extends Sync { + ... + final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } + ... +} +``` + +这块代码的含义为: + +- 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。 +- 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入 Acquire 方法进行后续处理。 + +第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考: + +- 某个线程获取锁失败的后续流程是什么呢?有以下两种可能: + +(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 Principle Overview + +The core idea of AQS is that if the requested shared resource is idle, then the thread currently requesting the resource is set as a valid working thread and the shared resource is set to the locked state; if the shared resource is occupied, a certain blocking and waiting wake-up mechanism is required to ensure lock allocation. This mechanism is mainly implemented using a variant of the CLH queue, and threads that cannot temporarily obtain the lock are added to the queue. + +CLH: Craig, Landin and Hagersten queue is a one-way linked list. The queue in AQS is a virtual two-way queue (FIFO) of CLH variant. AQS implements lock allocation by encapsulating each thread requesting shared resources into a node. + +The main schematic diagram is as follows: + +![](https://p0.meituan.net/travelcube/7132e4cef44c26f62835b197b239147b18062.png) + +AQS uses a Volatile int type member variable to represent the synchronization state, completes the queuing work of resource acquisition through the built-in FIFO queue, and completes the modification of the State value through CAS. + +#### 2.1.1 AQS data structure + +Let’s first look at the most basic data structure in AQS - Node. Node is the node in the CLH variant queue above. + +![](https://p1.meituan.net/travelcube/960271cf2b5c8a185eed23e98b72c75538637.png) + +Explain the meaning of several methods and attribute values: + +| Method and property values | Meaning | +| :---------- | :--------------------------------------------------------------------------------------------- | +| waitStatus | The status of the current node in the queue | +| thread | represents the thread at this node | +| prev | Precursor pointer | +| predecessor | Returns the predecessor node, if not, throws npe | +| nextWaiter | Points to the next node in the CONDITION state (since this article does not describe the Condition Queue queue, this pointer will not be introduced in detail) | +| next | successor pointer | + +Two lock modes for threads: + +| Pattern | Meaning | +| :-------- | :-------------------------------- | +| SHARED | Indicates that the thread is waiting for the lock in shared mode | +| EXCLUSIVE | Indicates that the thread is waiting for the lock exclusively | + +waitStatus has the following enumeration values: + +| Enumeration | Meaning | +| :-------- | :----------------------------------------------- | +| 0 | The default value when a Node is initialized | +| CANCELLED | is 1, indicating that the thread's request to acquire the lock has been canceled | +| CONDITION | is -2, indicating that the node is in the waiting queue and the node thread is waiting to wake up | +| PROPAGATE | is -3, this field will only be used when the current thread is in SHARED | +| SIGNAL | is -1, indicating that the thread is ready and is waiting for the resource to be released | + +#### 2.1.2 Synchronization state State + +After understanding the data structure, let’s learn about the synchronization state of AQS - State. AQS maintains a field named state, which means synchronization status. It is modified by Volatile and is used to display the current lock status of critical resources. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private volatile int state; +``` + +Here are several ways to access this field: + +| Method name | Description | +| :------------------------------------------------------------------ | :----------------------- | +| protected final int getState() | Get the value of State | +| protected final void setState(int newState) | Set the value of State | +| protected final boolean compareAndSetState(int expect, int update) | Use CAS to update State | + +These methods are all modified with Final, which means they cannot be overridden in subclasses. We can implement multi-threaded exclusive mode and shared mode (locking process) by modifying the synchronization state represented by the State field. + +![](https://p0.meituan.net/travelcube/27605d483e8935da683a93be015713f331378.png) + +![](https://p0.meituan.net/travelcube/3f1e1a44f5b7d77000ba4f9476189b2e32806.png) + +For our customized synchronization tool, we need to customize the way to obtain synchronization status and release status, which is the first layer in the AQS architecture diagram: API layer. + +### 2.2 Association between AQS important methods and ReentrantLock + +As can be seen from the architecture diagram, AQS provides a large number of Protected methods for custom synchronizer implementation. The related methods of custom synchronizer implementation are only to implement multi-threaded exclusive mode or shared mode by modifying the State field. The custom synchronizer needs to implement the following methods (the methods that ReentrantLock needs to implement are as follows, not all): + +| Method name | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------- | +| protected boolean isHeldExclusively() | Whether this thread is holding resources exclusively. You only need to implement it if you use Condition. | +| protected boolean tryAcquire(int arg) | Exclusive mode. arg is the number of times to acquire the lock and try to acquire the resource. True will be returned if successful and False if failed. || protected boolean tryRelease(int arg) | Exclusive mode. arg is the number of times to release the lock. Try to release the resource. True will be returned if successful, False if failed. | +| protected int tryAcquireShared(int arg) | Sharing method. arg is the number of times to acquire the lock and try to acquire the resource. A negative number indicates failure; 0 indicates success, but no resources remain available; a positive number indicates success, and there are remaining resources. | +| protected boolean tryReleaseShared(int arg) | Sharing method. arg is the number of times to release the lock. Try to release the resource. If it is allowed to wake up the subsequent waiting node after the release, it returns True, otherwise it returns False. | + +Generally speaking, custom synchronizers are either exclusive or shared, and they only need to implement one of tryAcquire-tryRelease and tryAcquireShared-tryReleaseShared. AQS also supports custom synchronizers to implement both exclusive and shared methods, such as ReentrantReadWriteLock. ReentrantLock is an exclusive lock, so tryAcquire-tryRelease is implemented. + +Taking unfair locks as an example, here we mainly explain the relationship between unfair locks and AQS. The specific role of each core method will be explained in detail later in the article. + +![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png) + +> 🐛 Correction (see: [issue#1761](https://github.com/Snailclimb/JavaGuide/issues/1761)): A small error in the picture, (AQS)CAS should successfully acquire the lock after successfully modifying the shared resource State (unfair lock). +> +> The corresponding source code is as follows: +> +> ```java +> final boolean nonfairTryAcquire(int acquires) { +> final Thread current = Thread.currentThread();//Get the current thread +> int c = getState(); +> if (c == 0) { +> if (compareAndSetState(0, acquires)) {//CAS lock grabbing +> setExclusiveOwnerThread(current);//Set the current thread as an exclusive thread +> return true;//Lock grabbing successful +> } +> } +> 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; +> } +> ``` + +In order to help everyone understand the interaction process between ReentrantLock and AQS, taking unfair lock as an example, we will highlight the interaction process of locking and unlocking separately to facilitate understanding of the subsequent content. + +![](https://p1.meituan.net/travelcube/7aadb272069d871bdee8bf3a218eed8136919.png) + +Lock: + +- Perform locking operations through the locking method Lock of ReentrantLock. +- The Lock method of the internal class Sync will be called. Since Sync#lock is an abstract method, executing the Lock method of the relevant internal class based on the fair lock and unfair lock selected by ReentrantLock initialization will essentially execute the Acquire method of AQS. +- The Acquire method of AQS will execute the tryAcquire method, but since tryAcquire requires a custom synchronizer implementation, the tryAcquire method in ReentrantLock is executed. Since ReentrantLock is a tryAcquire method implemented through fair lock and unfair lock internal classes, different tryAcquire will be executed depending on the lock type. +- tryAcquire is the lock acquisition logic. After the acquisition fails, the subsequent logic of the framework AQS will be executed, which has nothing to do with the ReentrantLock custom synchronizer. + +Unlock: + +- Unlock through the Unlock method of ReentrantLock. +- Unlock will call the Release method of the internal class Sync, which is inherited from AQS. +- The tryRelease method will be called in Release. tryRelease requires a custom synchronizer implementation. tryRelease is only implemented in Sync in ReentrantLock. Therefore, it can be seen that the process of releasing the lock does not distinguish whether it is a fair lock. +- After the release is successful, all processing is completed by the AQS framework and has nothing to do with the custom synchronizer. + +Through the above description, we can probably summarize the mapping relationship of the core methods of the API layer when locking and unlocking ReentrantLock. + +![](https://p0.meituan.net/travelcube/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png) + +## 3 Understanding AQS through ReentrantLock + +Fair locks and unfair locks in ReentrantLock are the same at the bottom level. Here we take unfair locks as an example for analysis. + +In unfair lock, there is a piece of code like this: + +```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); + } + ... +} +``` + +Take a look at how this Acquire is written: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final void acquire(int arg) { + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +Take another look at the tryAcquire method: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); +} +``` + +It can be seen that this is just a simple implementation of AQS. The specific implementation method of acquiring locks is implemented separately by respective fair locks and unfair locks (taking ReentrantLock as an example). If this method returns True, it means that the current thread has successfully acquired the lock, and there is no need to execute it further; if the acquisition fails, it needs to be added to the waiting queue. The following will explain in detail when and how threads are added to the waiting queue. + +### 3.1 Thread joins waiting queue + +#### 3.1.1 Timing to join the queue + +When Acquire(1) is executed, the lock is acquired through tryAcquire. In this case, if the lock acquisition fails, addWaiter will be called to join the waiting queue. + +#### 3.1.2 How to join the queue + +After failing to acquire the lock, addWaiter(Node.EXCLUSIVE) will be executed to join the waiting queue. The specific implementation method is as follows: + +```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); + } +} +``` + +Note: The setHead method sets the current node as a virtual node, but does not modify waitStatus because it is data that is always needed. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void setHead(Node node) { + head = node; + node.thread = null; + node.prev = null; +} + +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +// Determine whether the current thread should be blocked based on the predecessor node +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + // Get the node status of the predecessor node + int ws = pred.waitStatus; + // Indicates that the predecessor node is in the awake state + if (ws == Node.SIGNAL) + return true; + // Through the enumeration value, we know that waitStatus>0 is the cancellation status + if (ws > 0) { + do { + // Loop forward to find the cancellation node and remove the cancellation node from the queue + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + //Set the waiting status of the predecessor node to SIGNAL + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} +``` + +parkAndCheckInterrupt is mainly used to suspend the current thread, block the call stack, and return the interrupt status of the current thread. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +``` + +The flow chart of the above method is as follows: + +![](https://p0.meituan.net/travelcube/c124b76dcbefb9bdc778458064703d1135485.png) + +As can be seen from the above figure, the condition for jumping out of the current loop is when "the preceding node is the head node and the current thread acquires the lock successfully." In order to prevent CPU resources from being wasted due to endless loops, we will judge the status of the previous node to decide whether to suspend the current thread. The specific suspension process is represented by a flow chart as follows (shouldParkAfterFailedAcquire process): + +![](https://p0.meituan.net/travelcube/9af16e2481ad85f38ca322a225ae737535740.png) + +Now that the doubts about releasing nodes from the queue have been dispelled, there are new questions: + +- How is the cancellation node generated in shouldParkAfterFailedAcquire? When is a node's waitStatus set to -1? +- At what time is the node released notified to the suspended thread? + +### 3.2 CANCELLED status node generation + +Finally code in acquireQueued method: + +```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); + } +} +``` + +Mark the Node's status as CANCELLED through the cancelAcquire method. Next, we analyze the principle of this method line by line: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void cancelAcquire(Node node) { + // Filter out invalid nodes + if (node == null) + return; + //Set this node not to be associated with any thread, that is, a virtual node + node.thread = null; + Node pred = node.prev; + // Use the predecessor node to skip the canceled node. + while (pred.waitStatus > 0) + node.prev = pred = pred.prev; + // Get the successor node of the filtered predecessor node + Node predNext = pred.next; + //Set the status of the current node to CANCELLED + node.waitStatus = Node.CANCELLED; + // If the current node is the tail node, set the first non-cancelled node from the back to the front as the tail node + // If the update fails, enter else. If the update is successful, set the successor node of tail to null. + if (node == tail && compareAndSetTail(node, pred)) { + compareAndSetNext(pred, predNext, null); + } else { + int ws; + // If the current node is not the successor node of head, 1: Determine whether the predecessor node of the current node is SIGNAL, 2: If not, set the predecessor node to SIGNAL to see if it is successful. + // If either 1 or 2 is true, then determine whether the thread of the current node is null. + // If the above conditions are met, point the successor pointer of the current node's predecessor node to the current node's successor node + 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 { + // If the current node is the successor node of head, or the above conditions are not met, then wake up the successor node of the current node + unparkSuccessor(node); + } + node.next = node; // help GC + } +} +``` + +Current process: + +- Get the predecessor node of the current node. If the status of the predecessor node is CANCELLED, then traverse forward to find the first node with waitStatus <= 0, associate the found Pred node with the current Node, and set the current Node to CANCELLED. +- Based on the position of the current node, consider the following three situations: + +(1) The current node is the tail node. + +(2) The current node is the successor node of Head. + +(3) The current node is not the successor node of Head, nor is it the tail node. + +According to the second item above, let’s analyze the process of each situation. + +The current node is the tail node. + +![](https://p1.meituan.net/travelcube/b845211ced57561c24f79d56194949e822049.png) + +The current node is the successor node of Head. + +![](https://p1.meituan.net/travelcube/ab89bfec875846e5028a4f8fead32b7117975.png) + +The current node is not the successor node of Head, nor is it the tail node. + +![](https://p0.meituan.net/travelcube/45d0d9e4a6897eddadc4397cf53d6cd522452.png) + +Through the above process, we already have a general understanding of the generation and changes of the CANCELLED node status, but why do all changes operate on the Next pointer and not on the Prev pointer? Under what circumstances will the Prev pointer be operated?> When cancelAcquire is executed, the previous node of the current node may have been removed from the queue (the shouldParkAfterFailedAcquire method in the Try code block has been executed). If the Prev pointer is modified at this time, it may cause Prev to point to another Node that has been removed from the queue, so this change of the Prev pointer is unsafe. In the shouldParkAfterFailedAcquire method, the following code will be executed, which is actually processing the Prev pointer. shouldParkAfterFailedAcquire will only be executed when the lock acquisition fails. After entering this method, it means that the shared resource has been acquired, and the nodes before the current node will not change, so it is safer to change the Prev pointer at this time. +> +> ```java +> do { +> node.prev = pred = pred.prev; +> } while (pred.waitStatus > 0); +> ``` + +### 3.3 How to unlock + +We have analyzed the basic process in the locking process, and then we will analyze the basic process of unlocking. Since ReentrantLock does not distinguish between fair locks and unfair locks when unlocking, we directly look at the unlocking source code: + +```java +// java.util.concurrent.locks.ReentrantLock + +public void unlock() { + sync.release(1); +} +``` + +It can be seen that the essence of releasing the lock is done through the framework. + +```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; +} +``` + +Sync, the parent class of fair locks and unfair locks in ReentrantLock, defines the lock release mechanism for reentrant locks. + +```java +// java.util.concurrent.locks.ReentrantLock.Sync + +//The method returns whether the current lock is not held by the thread +protected final boolean tryRelease(int releases) { + // Reduce the number of reentrants + int c = getState() - releases; + //The current thread is not the thread holding the lock, and an exception is thrown. + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + // If all holding threads are released, set all threads of the current exclusive lock to null and update state + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; +} +``` + +Let's explain the following source code: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final boolean release(int arg) { + // If the custom tryRelease above returns true, it means that the lock is not held by any thread. + if (tryRelease(arg)) { + // Get the head node + Node h = head; + // If the head node is not empty and the waitStatus of the head node is not an initialization node, release the thread suspension state. + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +Why is the judgment condition here h != null && h.waitStatus != 0? + +> h == null Head has not been initialized yet. In the initial case, head == null, the first node is added to the queue, and Head will be initialized as a virtual node. Therefore, if there is no time to join the team here, head == null will occur. +> +> h != null && waitStatus == 0 indicates that the thread corresponding to the successor node is still running and does not need to be awakened. +> +> h != null && waitStatus < 0 indicates that the successor node may be blocked and needs to be woken up. + +Take another look at the unparkSuccessor method: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void unparkSuccessor(Node node) { + // Get the head node waitStatus + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + // Get the next node of the current node + Node s = node.next; + // If the next node is null or the next node is canceled, find the non-cancelled node at the beginning of the queue + if (s == null || s.waitStatus > 0) { + s = null; + // Start searching from the tail node, go to the head of the queue, and find the first node in the queue with waitStatus<0. + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + // If the next node of the current node is not empty and the status is <= 0, unpark the current node + if (s != null) + LockSupport.unpark(s.thread); +} +``` + +Why do we need to find the first non-Canceled node from back to front? Here’s why. + +Previous addWaiter method: + +```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; +} +``` + +We can see from here that joining the node into the queue is not an atomic operation. In other words, node.prev = pred; compareAndSetTail(pred, node) can be regarded as the atomic operation of Tail joining the queue. However, pred.next = node; has not been executed yet. If the unparkSuccessor method is executed at this time, there is no way to search from front to back, so you need to search from back to front. There is another reason. When the CANCELLED status node is generated, the Next pointer is disconnected first, and the Prev pointer is not disconnected. Therefore, all Nodes must be traversed from back to front. + +To sum up, if you search from front to back, due to the non-atomic operation of enqueuing in extreme cases and the operation of disconnecting the Next pointer during the generation of CANCELLED nodes, it may not be possible to traverse all nodes. Therefore, after waking up the corresponding thread, the corresponding thread will continue to execute. How to handle interruption after continuing to execute acquireQueued method? + +### 3.4 Execution process after interruption and recovery + +After waking up, return Thread.interrupted(); will be executed. This function returns the interrupt status of the current execution thread and clears it. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +``` + +Returning to the acquireQueued code, when parkAndCheckInterrupt returns True or False, the value of interrupted is different, but the next loop will be executed. If the lock is acquired successfully at this time, the current interrupted will be returned. + +```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); + } +}``` + +If acquireQueued is True, the selfInterrupt method will be executed. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +static void selfInterrupt() { + Thread.currentThread().interrupt(); +} +``` + +This method is actually to interrupt the thread. But why do we need to interrupt the thread after acquiring the lock? This part belongs to the collaborative interrupt knowledge content provided by Java. Interested students can check it out. Here is a brief introduction: + +1. When the interrupt thread is awakened, the reason for the awakening is not known. It may be that the current thread was interrupted while waiting, or it may be awakened after the lock is released. Therefore, we check the interrupt flag through the Thread.interrupted() method (this method returns the interrupt status of the current thread and sets the interrupt flag of the current thread to False), and records it. If it is found that the thread has been interrupted, interrupt it again. +2. The thread is awakened while waiting for resources. After waking up, it will continue to try to acquire the lock until it grabs the lock. That is to say, during the entire process, it does not respond to interrupts, but only records interrupt records. Finally, the lock is grabbed and returned. If it has been interrupted, another interruption needs to be added. + +The processing method here mainly uses the runWorker in the basic operating unit Worder in the thread pool, and performs additional judgment processing through Thread.interrupted(). Interested students can take a look at the ThreadPoolExecutor source code. + +### 3.5 Summary + +We raised some questions in Section 1.3 and now answer them. + +> Q: What is the follow-up process if a thread fails to acquire a lock? +> +> A: There is some kind of queuing mechanism. The thread continues to wait, and the possibility of acquiring the lock remains, and the process of acquiring the lock continues. +> +> Q: Since we are talking about the queuing waiting mechanism, there must be some kind of queue formed. What is the data structure of such a queue? +> +> A: A FIFO deque that is a CLH variant. +> +> Q: When can a thread in the queue waiting mechanism have the opportunity to acquire a lock? +> +> A: You can read section 2.3.1.3 in detail. +> +> Q: If a thread in the queue waiting mechanism has been unable to acquire the lock, does it need to wait forever? Or are there other strategies to solve this problem? +> +> A: The status of the node where the thread is located will change to the canceled status, and the node in the canceled status will be released from the queue. See section 2.3.2 for details. +> +> Q: The Lock function locks through the Acquire method, but how exactly is it locked? +> +> A: AQS's Acquire will call the tryAcquire method. tryAcquire is implemented by each custom synchronizer, and the locking process is completed through tryAcquire. + +## 4 AQS Application + +### 4.1 Reentrant application of ReentrantLock + +ReentrantLock's reentrancy is one of the good applications of AQS. After understanding the above knowledge points, we can easily know how to implement ReentrantLock's reentrancy. In ReentrantLock, whether it is a fair lock or an unfair lock, there is a logic. + +Fair lock: + +```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; +} +``` + +Unfair lock: + +```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; +} +``` + +As you can see from the above two paragraphs, there is a synchronization state State to control the overall reentrant situation. State is modified with Volatile to ensure certain visibility and orderliness. + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private volatile int state; +``` + +Next, let’s look at the main process of the State field: + +1. State is 0 when initialized, indicating that no thread holds the lock. +2. When a thread holds the lock, the value will be +1 based on the original value. If the same thread obtains the lock multiple times, it will be +1 multiple times. This is the concept of reentrancy. +3. Unlocking also changes this field from -1 to 0, and this thread releases the lock. + +### 4.2 Application scenarios in JUC + +In addition to the reentrant application of ReentrantLock above, AQS, as a concurrent programming framework, provides good solutions for many other synchronization tools. The following lists several synchronization tools in JUC, and gives a general introduction to the application scenarios of AQS: + +| Synchronization tool | Association between synchronization tool and AQS | +| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ReentrantLock | Use AQS to save the number of times a lock is repeatedly held. When a thread acquires a lock, ReentrantLock records the identity of the thread currently acquiring the lock, which is used to detect repeated acquisitions and handle exceptions when the wrong thread attempts to unlock the operation. | +| Semaphore | Use AQS sync state to hold the current count of a semaphore. tryRelease increments the count and acquireShared decrements it. | +| CountDownLatch | Uses AQS sync status to represent counts. When the count is 0, all Acquire operations (await method of CountDownLatch) can pass. | +| ReentrantReadWriteLock | Use 16 bits in the AQS synchronization state to save the number of times the write lock is held, and the remaining 16 bits are used to save the number of times the read lock is held. | +| ThreadPoolExecutor | Worker uses AQS synchronization status to set exclusive thread variables (tryAcquire and tryRelease). | + +### 4.3 Custom synchronization tool + +After understanding the basic principles of AQS, implement a synchronization tool yourself according to the AQS knowledge points mentioned above. + +```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); + } +}``` + +Complete certain synchronization functions through our own defined 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); + } +} +``` + +The result of running the above code will be 20000 every time. The synchronization function can be achieved with a few simple lines of code. This is the power of AQS. + +## 5 Summary + +There are too many scenarios where we use concurrency in our daily development, but not many people understand the basic framework principles within concurrency. Due to space reasons, this article only introduces the principle of reentrant lock ReentrantLock and the principle of AQS. I hope it can be a stepping stone for everyone to understand synchronizers such as AQS and ReentrantLock. + +## References + +- Lea D. The java. util. concurrent synchronizer framework\[J]. Science of Computer Programming, 2005, 58(3): 293-309. +- "Java Concurrent Programming in Practice" +- [Must-talk about Java “lock”](https://tech.meituan.com/2018/11/15/java-lock.html) + + \ No newline at end of file diff --git a/docs_en/java/concurrent/threadlocal.en.md b/docs_en/java/concurrent/threadlocal.en.md new file mode 100644 index 00000000000..24b1205a210 --- /dev/null +++ b/docs_en/java/concurrent/threadlocal.en.md @@ -0,0 +1,914 @@ +--- +title: ThreadLocal detailed explanation +category: Java +tag: + - Java concurrency +head: + - - meta + - name: keywords + content: ThreadLocal, thread variable copy, ThreadLocalMap, weak reference, hash conflict, expansion, cleanup mechanism, memory leak + - - meta + - name: description + content: An in-depth analysis of the design and implementation of ThreadLocal, covering the structure, weak reference and cleanup mechanism of ThreadLocalMap, as well as common usage pitfalls and avoidance methods. +--- + +> This article comes from the submission of Is a Flower a Romance? The original address: [https://juejin.cn/post/6844904151567040519](https://juejin.cn/post/6844904151567040519). + +### Preface + +![](./images/thread-local/1.png) + +**The full text has a total of 10,000+ words and 31 pictures. This article also took a lot of time and energy to create. It is not easy to be original. Please pay attention and read it. Thank you. ** + +Regarding `ThreadLocal`, everyone's first reaction may be that it is very simple, a copy of the thread's variables, and each thread is isolated. So here are a few questions you can think about: + +- The key of `ThreadLocal` is a **weak reference**, so when `ThreadLocal.get()` occurs, after **GC** occurs, is the key **null**? +- What is the **data structure** of `ThreadLocalMap` in `ThreadLocal`? +- **Hash algorithm** of `ThreadLocalMap`? +- How to solve the **Hash conflict** in `ThreadLocalMap`? +- What is the **expansion mechanism** of `ThreadLocalMap`? +- What is the cleaning mechanism for expired keys in `ThreadLocalMap`? **Detective Cleaning** and **Heuristic Cleaning** processes? +- What is the implementation principle of `ThreadLocalMap.set()` method? +- What is the implementation principle of `ThreadLocalMap.get()` method? +- How is `ThreadLocal` used in the project? Encountered a pitfall? +-… + +Do you already have a clear grasp of some of the above issues? This article will focus on these issues using graphics and text to analyze the **bits and pieces** of `ThreadLocal`. + +### Directory + +**Note:** The source code of this article is based on `JDK 1.8` + +### `ThreadLocal` code demonstration + +Let’s first look at the usage example of `ThreadLocal`: + +```java +public class ThreadLocalTest { + private List messages = Lists.newArrayList(); + + public static final ThreadLocal holder = ThreadLocal.withInitial(ThreadLocalTest::new); + + public static void add(String message) { + holder.get().messages.add(message); + } + + public static List clear() { + List messages = holder.get().messages; + holder.remove(); + + System.out.println("size: " + holder.get().messages.size()); + return messages; + } + + public static void main(String[] args) { + ThreadLocalTest.add("Is a flower considered romantic?"); + System.out.println(holder.get().messages); + ThreadLocalTest.clear(); + } +} +``` + +Print the result: + +```java +[Is a flower considered romantic?] +size: 0 +``` + +The `ThreadLocal` object can provide thread local variables. Each thread `Thread` has its own **copy variable**, and multiple threads do not interfere with each other. + +### Data structure of `ThreadLocal` + +![](./images/thread-local/2.png) + +The `Thread` class has an instance variable `threadLocals` of type `ThreadLocal.ThreadLocalMap`, which means that each thread has its own `ThreadLocalMap`. + +`ThreadLocalMap` has its own independent implementation. Its `key` can be simply regarded as `ThreadLocal`, and `value` is the value put in the code (in fact, `key` is not `ThreadLocal` itself, but a **weak reference** of it). + +When each thread puts a value into `ThreadLocal`, it will store it in its own `ThreadLocalMap`. When reading, it also uses `ThreadLocal` as a reference and finds the corresponding `key` in its own `map`, thus achieving **thread isolation**. + +`ThreadLocalMap` is somewhat similar to the structure of `HashMap`, except that `HashMap` is implemented by **array + linked list**, and there is no **linked list** structure in `ThreadLocalMap`. + +We also need to pay attention to `Entry`, its `key` is `ThreadLocal k`, inherited from `WeakReference`, which is what we often call a weak reference type. + +### Is key null after GC? + +In response to the question at the beginning, the `key` of `ThreadLocal` is a weak reference, so when `ThreadLocal.get()` occurs, after `GC` occurs, is `key` `null`? + +In order to clarify this problem, we need to understand the **four reference types** of `Java`: + +- **Strong reference**: The object we often new is a strong reference type. As long as the strong reference exists, the garbage collector will never recycle the referenced object, even when there is insufficient memory. +- **Soft Reference**: Objects modified with SoftReference are called soft references. The objects pointed to by soft references are recycled when the memory is about to overflow. +- **Weak Reference**: Objects modified with WeakReference are called weak references. As long as garbage collection occurs, if the object is only pointed to by weak references, it will be recycled. +- **Virtual reference**: A virtual reference is the weakest reference and is defined using PhantomReference in Java. The only function of virtual references is to use the queue to receive notifications that the object is about to die. + +Next, let’s look at the code. We use reflection to see the data in `ThreadLocal` after `GC`: (The following code comes from: Local running demonstration GC recycling scenario) + +```java +public class ThreadLocalDemo { + + public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException { + Thread t = new Thread(()->test("abc",false)); + t.start(); + t.join(); + System.out.println("--after gc--"); + Thread t2 = new Thread(() -> test("def", true)); + t2.start(); + t2.join(); + } + + private static void test(String s,boolean isGC) { + try { + new ThreadLocal<>().set(s); + if (isGC) { + System.gc(); + } + Thread t = Thread.currentThread(); + Class clz = t.getClass(); + Field field = clz.getDeclaredField("threadLocals"); + field.setAccessible(true); + Object ThreadLocalMap = field.get(t); + Class tlmClass = ThreadLocalMap.getClass(); + Field tableField = tlmClass.getDeclaredField("table"); + tableField.setAccessible(true); + Object[] arr = (Object[]) tableField.get(ThreadLocalMap); + for (Object o : arr) { + if (o != null) { + Class entryClass = o.getClass(); + Field valueField = entryClass.getDeclaredField("value"); + Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); + valueField.setAccessible(true); + referenceField.setAccessible(true); + System.out.println(String.format("Weak reference key:%s, value:%s", referenceField.get(o), valueField.get(o))); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +}``` + +The result is as follows: + +```java +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) + +As shown in the figure, because the `ThreadLocal` created here does not point to any value, that is, there is no reference: + +```java +new ThreadLocal<>().set(s); +``` + +So here after `GC`, `key` will be recycled. We see `referent=null` in `debug` above. If **change the code:** + +![](./images/thread-local/4.png) + +When you first look at this problem, if you don’t think too much about **weak references** and **garbage collection**, then you will definitely think it is `null`. + +In fact, it is wrong, because the question is about doing `ThreadLocal.get()` operation, which proves that there is still a strong reference, so `key` is not `null`. As shown in the figure below, the **strong reference** of `ThreadLocal` still exists. + +![](./images/thread-local/5.png) + +If our **strong reference** does not exist, then `key` will be recycled, which means that our `value` will not be recycled, and `key` will be recycled, causing `value` to exist forever, causing a memory leak. + +### `ThreadLocal.set()` method source code detailed explanation + +![](./images/thread-local/6.png) + +The principle of the `set` method in `ThreadLocal` is as shown in the figure above. It is very simple. It mainly determines whether `ThreadLocalMap` exists, and then uses the `set` method in `ThreadLocal` for data processing. + +The code is as follows: + +```java +public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); +} + +void createMap(Thread t, T firstValue) { + t.threadLocals = new ThreadLocalMap(this, firstValue); +} +``` + +The main core logic is still in `ThreadLocalMap`. Read on step by step. There will be a more detailed analysis later. + +### `ThreadLocalMap` Hash algorithm + +Since it is a `Map` structure, `ThreadLocalMap` must also implement its own `hash` algorithm to solve the hash table array conflict problem. + +```java +int i = key.threadLocalHashCode & (len-1); +``` + +The `hash` algorithm in `ThreadLocalMap` is very simple, where `i` is the array subscript position corresponding to the current key in the hash table. + +The most critical thing here is the calculation of `threadLocalHashCode` value. There is an attribute in `ThreadLocal` which is `HASH_INCREMENT = 0x61c88647` + +```java +public class ThreadLocal { + private final int threadLocalHashCode = nextHashCode(); + + private static AtomicInteger nextHashCode = new AtomicInteger(); + + private static final int HASH_INCREMENT = 0x61c88647; + + private static int nextHashCode() { + return nextHashCode.getAndAdd(HASH_INCREMENT); + } + + static class ThreadLocalMap { + ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + table = new Entry[INITIAL_CAPACITY]; + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + + table[i] = new Entry(firstKey, firstValue); + size = 1; + setThreshold(INITIAL_CAPACITY); + } + } +} +``` + +Whenever a `ThreadLocal` object is created, the value of `ThreadLocal.nextHashCode` will be incremented by `0x61c88647`. + +This value is very special, it is **Fibonacci number** also called **golden section number**. The increment of `hash` is this number, and the advantage is that `hash` is **distributed very evenly**. + +We can try it ourselves: + +![](./images/thread-local/8.png) + +It can be seen that the generated hash codes are evenly distributed. We will not go into details about the specific algorithm of **Fibonacci** here. Those who are interested can check the relevant information by themselves. + +### `ThreadLocalMap` Hash conflict + +> **Note:** In all the example images below, the **green block**`Entry` represents **normal data**, and the **grey block** represents the `key` value of `Entry` which is `null` and **has been garbage collected**. **White block** indicates that `Entry` is `null`. + +Although `ThreadLocalMap` uses the **golden section** as the `hash` calculation factor, which greatly reduces the probability of `Hash` conflicts, conflicts still exist. + +The method to resolve conflicts in `HashMap` is to construct a **linked list** structure on the array, and the conflicting data is mounted on the linked list. If the length of the linked list exceeds a certain number, it will be converted into a **red-black tree**. + +There is no linked list structure in `ThreadLocalMap`, so `HashMap` cannot be used to resolve conflicts here. + +![](./images/thread-local/7.png) + +As shown in the figure above, if we insert a `value=27` data, it should fall into slot 4 after `hash` calculation, and slot 4 already has `Entry` data. + +At this time, the search will be linearly backward, and the search will not stop until the slot where `Entry` is `null` is found, and the current element will be placed in this slot. Of course, there are other situations during the iteration process, such as the situation where `Entry` is not `null` and the `key` values ​​are equal, and the situation where the `key` value in `Entry` is `null`, etc. will be handled differently, which will be explained in detail later. + +Here we also draw a data where the `key` in `Entry` is `null` (**Entry=2 gray block data**), because the `key` value is a **weak reference** type, so this kind of data exists. During the `set` process, if `Entry` data with expired `key` is encountered, a round of **detective cleaning** operation will actually be performed. The specific operation method will be discussed later. + +### Detailed explanation of `ThreadLocalMap.set()` + +#### `ThreadLocalMap.set()` principle illustration + +After reading `ThreadLocal` **hash algorithm**, let's look at how `set` is implemented. + +There are several situations for `set` data (**new** or **update** data) in `ThreadLocalMap`. We draw pictures to illustrate different situations. + +**First case:** The `Entry` data corresponding to the slot calculated through `hash` is empty: + +![](./images/thread-local/9.png) + +Here you can directly put the data into the slot. + +**Second case:** The slot data is not empty, and the `key` value is consistent with the `key` value obtained by the current `ThreadLocal` through `hash` calculation: + +![](./images/thread-local/10.png) + +The data of this slot is directly updated here. + +**Third case:** The slot data is not empty. During the subsequent traversal process, no `Entry` with expired `key` is encountered before finding the slot with `Entry` as `null`: + +![](./images/thread-local/11.png) + +Traverse the hash array and search linearly backwards. If you find a slot with `Entry` as `null`, put the data into the slot, or during the subsequent traversal, if you encounter data with the same key value, you can update it directly. + +**Fourth case:** The slot data is not empty. During the subsequent traversal process, before finding the slot whose `Entry` is `null`, an `Entry` with an expired `key` is encountered, as shown below. During the subsequent traversal process, an `key=null` of the `Entry` of the slot data `index=7` is encountered: + +![](./images/thread-local/12.png)The `Entry` data `key` corresponding to position 7 in the hash array is `null`, indicating that the `key` value of this data has been garbage collected. At this time, the `replaceStaleEntry()` method will be executed. The meaning of this method is **the logic of replacing expired data**, and starts traversing from the starting point of **index=7** to perform detection data cleaning. + +Initialize the starting position of exploratory cleaning of expired data scanning: `slotToExpunge = staleSlot = 7` + +Starting from the current `staleSlot`, iteratively search forward to find other expired data, and then update the starting scan index of the expired data `slotToExpunge`. The `for` loop iterates until `Entry` is `null`. + +If expired data is found, continue to iterate forward until it encounters a slot with `Entry=null`. As shown in the figure below, **slotToExpunge is updated to 0**: + +![](./images/thread-local/13.png) + +Iterate forward with the current node (`index=7`), check whether there is expired `Entry` data, and update the `slotToExpunge` value if there is. When `null` is encountered, the detection ends. Taking the above picture as an example, `slotToExpunge` is updated to 0. + +The above forward iteration operation is to update the value of the starting index `slotToExpunge` for detecting and cleaning expired data. This value will be explained later. It is used to determine whether there are expired elements before the current expired slot `staleSlot`. + +Then start iterating backwards from the `staleSlot` position (`index=7`), **If Entry data with the same key value is found:** + +![](./images/thread-local/14.png) + +Search backwards from the current node `staleSlot` for the `Entry` element with the same `key` value. After finding it, update the value of `Entry` and exchange the position of the `staleSlot` element (the position of `staleSlot` is the expired element), update the `Entry` data, and then start cleaning up the expired `Entry`, as shown in the following figure: + +![](https://oss.javaguide.cn/java-guide-blog/view.png) During backward traversal, if Entry data with the same key value is not found: + +![](./images/thread-local/15.png) + +Search backwards from the current node `staleSlot` for `Entry` elements with equal `key` values, and stop searching until `Entry` is `null`. As can be seen from the above figure, there is no `Entry` with the same `key` value in `table` at this time. + +Create a new `Entry` and replace the `table[stableSlot]` position: + +![](./images/thread-local/16.png) + +After the replacement is completed, expired elements are also cleaned up. There are two main methods for cleaning: `expungeStaleEntry()` and `cleanSomeSlots()`. The specific details will be discussed later, please continue to read later. + +#### Detailed explanation of `ThreadLocalMap.set()` source code + +The principle of `set()` implementation has been analyzed above in the form of a diagram. In fact, it is very clear. Let's take a look at the source code: + +`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); + + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + + if (k == key) { + e.value = value; + return; + } + + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + } + + tab[i] = new Entry(key, value); + int sz = ++size; + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +} +``` + +Here, `key` is used to calculate the corresponding position in the hash table, and then the position of the bucket corresponding to the current `key` is searched backward to find the bucket that can be used. + +```java +Entry[] tab = table; +int len = tab.length; +int i = key.threadLocalHashCode & (len-1); +``` + +Under what circumstances can a bucket be used? + +1. `k = key` indicates that it is a replacement operation and can be used +2. When encountering an expired bucket, execute replacement logic and occupy the expired bucket. +3. During the search process, if you encounter `Entry=null` in the bucket, use it directly + +The next step is to execute `for` loop traversal and search backwards. Let’s first look at the implementation of `nextIndex()` and `prevIndex()` methods: + +![](./images/thread-local/17.png) + +```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); +} +``` + +Then look at the remaining logic in the `for` loop: + +1. Traverse the `Entry` data in the bucket corresponding to the current `key` value. This means that there is no data conflict in the hash array. Jump out of the `for` loop and directly `set` the data into the corresponding bucket. +2. If the `Entry` data in the bucket corresponding to the `key` value is not empty + 2.1 If `k = key`, it means that the current `set` operation is a replacement operation, do the replacement logic and return directly + 2.2 If `key = null`, it means that the `Entry` in the current bucket position is expired data, execute the `replaceStaleEntry()` method (core method), and then return +3. The execution of the `for` loop is completed and the execution continues, indicating that the `entry` is `null` during the backward iteration process. + 3.1 Create a new `Entry` object in the bucket where `Entry` is `null` + 3.2 Perform `++size` operation +4. Call `cleanSomeSlots()` to do a heuristic cleaning job and clean up the expired data of `key` of `Entry` in the hash array + 4.1 If no data is recovered after the cleaning work is completed, and `size` exceeds the threshold (2/3 of the array length), perform the `rehash()` operation + 4.2 `rehash()` will first perform a round of detection cleaning to clean up the expired `key`. After the cleaning is completed, if **size >= threshold - threshold / 4**, the real expansion logic will be executed (the expansion logic will be looked at later) + +Next, focus on the `replaceStaleEntry()` method. The `replaceStaleEntry()` method provides the function of replacing expired data. We can review it corresponding to the schematic diagram of the **fourth case** above. The specific code is as follows: + +`java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`: + +```java +private void replaceStaleEntry(ThreadLocal key, Object value, + int staleSlot) { + Entry[] tab = table; + int len = tab.length; + Entry e; + + int slotToExpunge = staleSlot; + for (int i = prevIndex(staleSlot, len); + (e = tab[i]) != null; + i = prevIndex(i, len)) + + if (e.get() == null) + slotToExpunge = i; + + for (int i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + + ThreadLocal k = e.get(); + + if (k == key) { + e.value = value; + + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + if (slotToExpunge == staleSlot) + slotToExpunge = i; + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; + } + + if (k == null && slotToExpunge == staleSlot) + slotToExpunge = i; + } + + tab[staleSlot].value = null; + tab[staleSlot] = new Entry(key, value); + + if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); +}``` + +`slotToExpunge` indicates the starting index to start exploratory cleaning of expired data. By default, it starts from the current `staleSlot`. Starting from the current `staleSlot`, iterate forward to find the data that has not expired. The `for` loop will not end until `Entry` is `null`. If expired data is found forward, the start index of the update probe to clean up the expired data is i, that is, `slotToExpunge=i` + +```java +for (int i = prevIndex(staleSlot, len); + (e = tab[i]) != null; + i = prevIndex(i, len)){ + + if (e.get() == null){ + slotToExpunge = i; + } +} +``` + +Then it starts searching backward from `staleSlot`, and ends when it encounters the bucket where `Entry` is `null`. +If k == key is encountered during the iteration process, it means that the replacement logic is here, replacing the new data and exchanging the current `staleSlot` position. If `slotToExpunge == staleSlot`, this means that `replaceStaleEntry()` did not find expired `Entry` data when it initially searched for expired data forward, and then did not find expired data during the backward search process. Modify the subscript where the expiration-type cleaning of expired data begins to be the index of the current cycle, that is, `slotToExpunge = i`. Finally, call `cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);` to perform heuristic expiration data cleaning. + +```java +if (k == key) { + e.value = value; + + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + if (slotToExpunge == staleSlot) + slotToExpunge = i; + + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; +} +``` + +The `cleanSomeSlots()` and `expungeStaleEntry()` methods will be discussed in detail later. These two methods are related to cleaning. One is the heuristic cleaning of the `Entry` related to the expired `key` (`Heuristically scan`), and the other is the detection cleaning of the `Entry` related to the expired `key`. + +**If k != key**, it will continue to go down. `k == null` means that the currently traversed `Entry` is expired data. `slotToExpunge == staleSlot` means that the initial forward search of data did not find the expired `Entry`. If the condition is true, update `slotToExpunge` to the current position. This premise is that no expired data is found during the predecessor node scan. + +```java +if (k == null && slotToExpunge == staleSlot) + slotToExpunge = i; +``` + +During subsequent iterations, if no data with `k == key` is found and data with `Entry` as `null` is encountered, the current iteration operation will end. At this time, it means that this is an added logic, adding new data to the `slot` corresponding to `table[staleSlot]`. + +```java +tab[staleSlot].value = null; +tab[staleSlot] = new Entry(key, value); +``` + +Finally, it was judged that in addition to `staleSlot`, other expired `slot` data was also found, so it was necessary to start the logic of cleaning the data: + +```java +if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); +``` + +### Detective cleaning process of `ThreadLocalMap` expired keys + +Above we mentioned two methods of cleaning expired `key` data of `ThreadLocalMap`: **detective cleaning** and **heuristic cleaning**. + +Let's first talk about detection cleaning, that is, the `expungeStaleEntry` method, which traverses the hash array, detects and cleans expired data from the starting position backward, sets the `Entry` of the expired data to `null`, and if it encounters unexpired data along the way, this data `reh Re-position in the `table` array after ash`. If there is already data at the positioned position, the unexpired data will be placed in the `Entry=null` bucket closest to this position, so that the `Entry` data after `rehash` is closer to the correct bucket position. The operation logic is as follows: + +![](./images/thread-local/18.png) + +As shown in the figure above, `set(27)` should fall into the bucket of `index=4` after hash calculation. Since the `index=4` bucket already has data, the final data of subsequent iterations is put into the bucket of `index=7`. After a while, the `Entry` data `key` in `index=5` becomes `null` + +![](./images/thread-local/19.png) + +If there is any other data `set` into `map`, the **detective cleaning** operation will be triggered. + +As shown in the picture above, after executing **detective cleaning**, the data of `index=5` is cleared, and iteration continues. When the element of `index=7` is reached, the correct `index=4` of the element is found after `rehash`, and there is already data at this position. Then, the node with `Entry=null` closest to `index=4` is searched (the data just cleaned by detection: `index=5`), and after finding it, move `index= The data of 7` is in `index=5`. At this time, the position of the bucket is closer to the correct position `index=4`. + +After a round of detection cleaning, the expired data of `key` will be cleared. The bucket position of the unexpired data after `rehash` relocation is theoretically closer to the position of `i= key.hashCode & (tab.len - 1)`. This optimization will improve overall hash table query performance. + +Next, let’s take a look at the specific process of `expungeStaleEntry()`. Let’s sort it out step by step by first explaining the schematic diagram and then the source code: + +![](./images/thread-local/20.png) + +We assume that `expungeStaleEntry(3)` is used to call this method. As shown in the figure above, we can see the data of `table` in `ThreadLocalMap`, and then perform the cleaning operation: + +![](./images/thread-local/21.png) + +The first step is to clear the data at the current `staleSlot` position. The `Entry` at the `index=3` position becomes `null`. Then continue to probe backward: + +![](./images/thread-local/22.png) + +After executing the second step, the element with index=4 is moved to the slot with index=3. + +Continue to iterate and check later. When normal data is encountered, calculate whether the position of the data is offset. If it is offset, recalculate the `slot` position. The purpose is to store the normal data in the correct position or as close to the correct position as possible. + +![](./images/thread-local/23.png) + +In the process of subsequent iterations, when an empty slot is encountered, the detection is terminated. In this way, a round of detection cleaning work is completed. Then we continue to look at the specific **implementation source code**: + +```java +private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + tab[staleSlot].value = null; + tab[staleSlot] = null; + size--; + + Entry e; + int i; + for (i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + tab[i] = null; + size--; + } else { + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + tab[i] = null; + + while (tab[h] != null) + h = nextIndex(h, len); + tab[h] = e; + } + } + } + return i; +} +``` + +Here we still use `staleSlot=3` as an example. First, clear the data in the `tab[staleSlot]` slot, and then set `size--` +Then iterate backwards based on the `staleSlot` position. If expired data with `k==null` is encountered, the slot data is also cleared, and then `size--` + +```java +ThreadLocal k = e.get(); + +if (k == null) { + e.value = null; + tab[i] = null; + size--; +}``` + +If `key` has not expired, recalculate whether the subscript position of the current `key` is the current slot subscript position. If not, then it means that a `hash` conflict has occurred. At this time, iterate backwards with the newly calculated correct slot position to find the latest position where `entry` can be stored. + +```java +int h = k.threadLocalHashCode & (len - 1); +if (h != i) { + tab[i] = null; + + while (tab[h] != null) + h = nextIndex(h, len); + + tab[h] = e; +} +``` + +Here is the processing of normal data that generates `Hash` conflicts. After iteration, the `Entry` position of the `Hash` conflict data will be closer to the correct position. In this case, the query efficiency will be higher. + +### `ThreadLocalMap` expansion mechanism + +At the end of the `ThreadLocalMap.set()` method, if no data is cleared after performing the heuristic cleaning work, and the number of `Entry` in the current hash array has reached the list expansion threshold `(len*2/3)`, the `rehash()` logic will be executed: + +```java +if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +``` + +Next, let’s take a look at the specific implementation of `rehash()`: + +```java +private void rehash() { + expungeStaleEntries(); + + if (size >= threshold - threshold / 4) + resize(); +} + +private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) + expungeStaleEntry(j); + } +} +``` + +The first step here is to perform detection cleaning work, starting from the starting position of `table` and going back. The detailed process of analysis and cleaning is provided above. After the cleaning is completed, there may be some `Entry` data with `key` as `null` in the `table`, so at this time, it is determined whether to expand the capacity by judging `size >= threshold - threshold / 4`, that is, `size >= threshold * 3/4`. + +We still remember that the threshold for `rehash()` above is `size >= threshold`, so when the interviewer tells us about the `ThreadLocalMap` expansion mechanism, we must explain these two steps clearly: + +![](./images/thread-local/24.png) + +Next, let’s take a look at the specific `resize()` method. For the convenience of demonstration, we use `oldTab.len=8` as an example: + +![](./images/thread-local/25.png) + +The size of the expanded `tab` is `oldLen * 2`, and then the old hash table is traversed, the `hash` position is recalculated, and then placed in the new `tab` array. If a `hash` conflict occurs, the nearest `entry` is `null` slot. After the traversal is completed, all the `entry` data in `oldTab` has been put into the new `tab`. Recalculate the **threshold** for the next expansion of `tab`. The specific code is as follows: + +```java +private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + int count = 0; + + for (int j = 0; j < oldLen; ++j) { + Entry e = oldTab[j]; + if (e != null) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + } else { + int h = k.threadLocalHashCode & (newLen - 1); + while (newTab[h] != null) + h = nextIndex(h, newLen); + newTab[h] = e; + count++; + } + } + } + + setThreshold(newLen); + size = count; + table = newTab; +} +``` + +### Detailed explanation of `ThreadLocalMap.get()` + +We have read the source code of the `set()` method above, which includes operations such as `set` data, cleaning data, optimizing the position of the data bucket, etc. Let's take a look at the principle of the `get()` operation. + +#### `ThreadLocalMap.get()` diagram + +**First case:** Calculate the `slot` position in the hash table by searching for the `key` value, and then the `Entry.key` in the `slot` position is consistent with the searched `key`, then return directly: + +![](./images/thread-local/26.png) + +**Second case:** The `Entry.key` in the `slot` position is inconsistent with the `key` to be found: + +![](./images/thread-local/27.png) + +Let's take `get(ThreadLocal1)` as an example. After calculation through `hash`, the correct `slot` position should be 4, and the slot with `index=4` already has data, and the `key` value is not equal to `ThreadLocal1`, so we need to continue to iteratively search. + +When iterating to the data of `index=5`, `Entry.key=null` triggers a detection data recycling operation and executes the `expungeStaleEntry()` method. After the execution, the data of `index 5 and 8` will be recycled, and the data of `index 6 and 7` will be moved forward. After `index 6,7` is moved forward, it continues to iterate from `index=5` backward, so the `Entry` data with the same `key` value is found at `index=6`, as shown in the following figure: + +![](./images/thread-local/28.png) + +#### Detailed explanation of `ThreadLocalMap.get()` source code + +`java.lang.ThreadLocal.ThreadLocalMap.getEntry()`: + +```java +private Entry getEntry(ThreadLocal key) { + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + if (e != null && e.get() == key) + return e; + else + return getEntryAfterMiss(key, i, e); +} + +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + while (e != null) { + ThreadLocal k = e.get(); + if (k == key) + return e; + if(k==null) + expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; +} +``` + +### `ThreadLocalMap` heuristic cleaning process for expired keys + +Two cleaning methods for `ThreadLocalMap` expired keys have been mentioned many times above: **detective cleaning (expungeStaleEntry())**, **heuristic cleaning (cleanSomeSlots())** + +Detection cleaning is based on the current `Entry` and will end the cleaning when the value is `null`. It belongs to **Linear Detection Cleaning**. + +Heuristic cleaning is defined by the author 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) { + boolean removed = false; + Entry[] tab = table; + int len = tab.length; + do { + i = nextIndex(i, len); + Entry e = tab[i]; + if (e != null && e.get() == null) { + n = len; + removed = true; + i = expungeStaleEntry(i); + } + } while ( (n >>>= 1) != 0); + return removed; +}``` + +### `InheritableThreadLocal` + +When we use `ThreadLocal`, in an asynchronous scenario, the thread copy data created in the parent thread cannot be shared with the child thread. + +In order to solve this problem, there is also an `InheritableThreadLocal` class in the JDK. Let's take a look at an example: + +```java +public class InheritableThreadLocalDemo { + public static void main(String[] args) { + ThreadLocal ThreadLocal = new ThreadLocal<>(); + ThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>(); + ThreadLocal.set("parent class data: threadLocal"); + inheritableThreadLocal.set("parent class data: inheritableThreadLocal"); + + new Thread(new Runnable() { + @Override + public void run() { + System.out.println("The child thread obtains the parent class ThreadLocal data: " + ThreadLocal.get()); + System.out.println("The child thread obtains the parent class inheritableThreadLocal data: " + inheritableThreadLocal.get()); + } + }).start(); + } +} +``` + +Print the result: + +```java +The child thread obtains the parent class ThreadLocal data: null +The child thread obtains the parent class inheritableThreadLocal data: parent class data: inheritableThreadLocal +``` + +The implementation principle is that the child thread is created by calling the `new Thread()` method in the parent thread, and the `Thread#init` method is called in the constructor of `Thread`. Copy the parent thread data to the child thread in the `init` method: + +```java +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc, + boolean inheritThreadLocals) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + this.stackSize = stackSize; + tid = nextThreadID(); +} +``` + +But `InheritableThreadLocal` still has flaws. Generally, we use a thread pool for asynchronous processing, and `InheritableThreadLocal` is assigned in the `init()` method in `new Thread`, and the thread pool is the logic of thread reuse, so there will be problems here. + +Of course, when problems arise, there will be solutions to solve them. Alibaba has open sourced a `TransmittableThreadLocal` component to solve this problem. It will not be extended here. Those who are interested can check the information by themselves. + +### Practical use in `ThreadLocal` project + +#### `ThreadLocal` usage scenarios + +In our current project, we use `ELK+Logstash` for logging, and finally display and retrieve it in `Kibana`. + +Nowadays, distributed systems provide unified services to the outside world. The calling relationship between projects can be related through `traceId`, but how to pass `traceId` between different projects? + +Here we use `org.slf4j.MDC` to implement this function, which is implemented internally through `ThreadLocal`. The specific implementation is as follows: + +When the front end sends a request to **Service A**, **Service A** will generate a `traceId` string similar to `UUID`, put this string into the `ThreadLocal` of the current thread, and when calling **Service B**, write `traceId` into the `Header` of the request, **Service When B** receives a request, it will first determine whether there is a `traceId` in the `Header` of the request. If it exists, it will be written to the `ThreadLocal` of its own thread. + +![](./images/thread-local/30.png) + +The `requestId` in the picture is the `traceId` associated with each of our system links. The systems call each other and the corresponding link can be found through this `requestId`. There are some other scenarios here: + +![](./images/thread-local/31.png) + +For these scenarios, we can have corresponding solutions, as shown below + +#### Feign remote calling solution + +**Service Send Request:** + +```java +@Component +@Slf4j +public class FeignInvokeInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + String requestId = MDC.get("requestId"); + if (StringUtils.isNotBlank(requestId)) { + template.header("requestId", requestId); + } + } +} +``` + +**Service receives request:** + +```java +@Slf4j +@Component +public class LogInterceptor extends HandlerInterceptorAdapter { + + @Override + public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) { + MDC.remove("requestId"); + } + + @Override + public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) { + } + + @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("-", ""); + } + MDC.put("requestId", requestId); + return true; + } +} +``` + +#### Thread pool asynchronous call, requestId passed + +Because `MDC` is implemented based on `ThreadLocal`, during the asynchronous process, the child thread has no way to obtain the data stored in the parent thread `ThreadLocal`, so you can customize the thread pool executor and modify the `run()` method: + +```java +public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { + + @Override + public void execute(Runnable runnable) { + Map context = MDC.getCopyOfContextMap(); + super.execute(() -> run(runnable, context)); + } + + @Override + private void run(Runnable runnable, Map context) { + if (context != null) { + MDC.setContextMap(context); + } + try { + runnable.run(); + } finally { + MDC.remove(); + } + } +}``` + +#### Use MQ to send messages to third-party systems + +Customize the attribute `requestId` in the message body sent by MQ. After the receiver consumes the message, it can parse `requestId` by itself. + + \ No newline at end of file diff --git a/docs_en/java/concurrent/virtual-thread.en.md b/docs_en/java/concurrent/virtual-thread.en.md new file mode 100644 index 00000000000..c2d0c2bdc23 --- /dev/null +++ b/docs_en/java/concurrent/virtual-thread.en.md @@ -0,0 +1,243 @@ +--- +title: 虚拟线程常见问题总结 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: 虚拟线程,Virtual Threads,Project Loom,Java 21,平台线程,轻量级线程,并发,I/O 密集型,兼容性 + - - meta + - name: description + content: 总结 Java 21 虚拟线程的概念与实践,解析与平台线程关系、适用场景、优势与限制以及常见问题。 +--- + +> 本文部分内容来自 [Lorin](https://github.com/Lorin-github) 的[PR](https://github.com/Snailclimb/JavaGuide/pull/2190)。 + +虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 + +## 什么是虚拟线程? + +虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。 + +## 虚拟线程和平台线程有什么关系? + +在引入虚拟线程之前,`java.lang.Thread` 包已经支持所谓的平台线程(Platform Thread),也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。 + +虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:[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) + +关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 + +## 虚拟线程有什么优点和缺点? + +### 优点 + +- **非常轻量级**:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。 +- **简化异步编程**: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。 +- **减少资源开销**: 由于虚拟线程是由 JVM 实现的,它能够更高效地利用底层资源,例如 CPU 和内存。虚拟线程的上下文切换比平台线程更轻量,因此能够更好地支持高并发场景。 + +### 缺点 + +- **不适用于计算密集型任务**: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。 +- **与某些第三方库不兼容**: 虽然虚拟线程设计时考虑了与现有代码的兼容性,但某些依赖平台线程特性的第三方库可能不完全兼容虚拟线程。 + +## 如何创建虚拟线程? + +官方提供了以下四种方式创建虚拟线程: + +1. 使用 `Thread.startVirtualThread()` 创建 +2. 使用 `Thread.ofVirtual()` 创建 +3. 使用 `ThreadFactory` 创建 +4. 使用 `Executors.newVirtualThreadPerTaskExecutor()`创建 + +**1、使用 `Thread.startVirtualThread()` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + Thread.startVirtualThread(customThread); + } +} + +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +**2、使用 `Thread.ofVirtual()` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + // 创建不启动 + Thread unStarted = Thread.ofVirtual().unstarted(customThread); + unStarted.start(); + // 创建直接启动 + Thread.ofVirtual().start(customThread); + } +} +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +**3、使用 `ThreadFactory` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + ThreadFactory factory = Thread.ofVirtual().factory(); + Thread thread = factory.newThread(customThread); + thread.start(); + } +} + +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); + } + } + } +} +``` + +**Number of requests: 10,000, single request takes 1s**: + +```plain +//Virtual Thread +max: 22 platform thread/os thread +totalMillis:1806ms + +//Platform Thread thread number 200 +max: 209 platform thread/os thread +totalMillis:50578ms + +//Platform Thread thread number 500 +max: 509 platform thread/os thread +totalMillis:20254ms + +//Platform Thread thread number 1000 +max: 1009 platform thread/os thread +totalMillis:10214ms + +//Platform Thread thread number 2000 +max:2009 platform thread/os thread +totalMillis:5358ms +``` + +**Number of requests: 10,000, single request takes 0.5s**: + +```plain +//Virtual Thread +max: 22 platform thread/os thread +totalMillis:1316ms + +//Platform Thread thread number 200 +max: 209 platform thread/os thread +totalMillis:25619ms + +//Platform Thread thread number 500 +max: 509 platform thread/os thread +totalMillis:10277ms + +//Platform Thread thread number 1000 +max: 1009 platform thread/os thread +totalMillis:5197ms + +//Platform Thread thread number 2000 +max:2009 platform thread/os thread +totalMillis:2865ms +``` + +- It can be seen that in intensive IO scenarios, a large number of platform threads need to be created for asynchronous processing to achieve the processing speed of virtual threads. +- Therefore, in intensive IO scenarios, virtual threads can greatly improve thread execution efficiency and reduce thread resource creation and context switching. + +**Note**: For some time, JDK has been committed to Reactor reactive programming to improve Java performance, but reactive programming is difficult to understand, debug, and use, and eventually returned to synchronous programming, and finally virtual threads were born. + +## What is the underlying principle of virtual threads? + +If you want to learn more about the implementation principles of virtual threads, I recommend an article: [Virtual Threads - VirtualThread Source Code Perspective](https://www.cnblogs.com/throwable/p/16758997.html). + +This question is generally not asked in interviews, and is only for students who are capable of learning to study further. \ No newline at end of file diff --git a/docs_en/java/io/io-basis.en.md b/docs_en/java/io/io-basis.en.md new file mode 100644 index 00000000000..ce7ae035108 --- /dev/null +++ b/docs_en/java/io/io-basis.en.md @@ -0,0 +1,553 @@ +--- +title: Summary of Java IO basic knowledge +category: Java +tag: + -JavaIO + - Java basics +head: + - - meta + - name: keywords + content: IO basics, byte stream, character stream, buffering, file operation, InputStream, Reader, OutputStream, Writer + - - meta + - name: description + content: Overview of the basic concepts and core classes of Java IO, understanding byte/character streams, buffering, and file reading and writing. +--- + + + +## Introduction to IO streams + +IO is `Input/Output`, input and output. The process of inputting data into computer memory is called input, and the process of outputting data to external storage (such as database, file, remote host) is called output. The data transfer process is similar to water flow, so it is called IO flow. IO streams are divided into input streams and output streams in Java, and are further divided into byte streams and character streams according to the way data is processed. + +More than 40 classes of Java IO streams are derived from the following 4 abstract class base classes. + +- `InputStream`/`Reader`: The base class of all input streams, the former is a byte input stream, and the latter is a character input stream. +- `OutputStream`/`Writer`: The base class of all output streams, the former is a byte output stream, and the latter is a character output stream. + +## Byte stream + +### InputStream (byte input stream) + +`InputStream` is used to read data (byte information) from the source (usually a file) into memory. The `java.io.InputStream` abstract class is the parent class of all byte input streams. + +`InputStream` common methods: + +- `read()`: Returns the next byte of data in the input stream. The value returned is between 0 and 255. If no bytes were read, the code returns `-1`, indicating end of file. +- `read(byte b[ ])` : Read some bytes from the input stream and store them in the array `b`. If the length of array `b` is zero, it is not read. If there are no bytes available to read, return `-1`. If there are bytes available to read, up to a maximum number of bytes read equal to `b.length`, the number of bytes read is returned. This method is equivalent to `read(b, 0, b.length)`. +- `read(byte b[], int off, int len)`: Based on the `read(byte b[ ])` method, the `off` parameter (offset) and the `len` parameter (the maximum number of bytes to be read) are added. +- `skip(long n)`: Ignore n bytes in the input stream and return the actual number of ignored bytes. +- `available()`: Returns the number of bytes that can be read in the input stream. +- `close()`: Close the input stream to release related system resources. + +Starting from Java 9, `InputStream` has added several new practical methods: + +- `readAllBytes()`: Read all bytes in the input stream and return a byte array. +- `readNBytes(byte[] b, int off, int len)`: Block until `len` bytes are read. +- `transferTo(OutputStream out)`: Transfer all bytes from an input stream to an output stream. + +`FileInputStream` is a commonly used byte input stream object. It can directly specify the file path, read single-byte data directly, or read it into a byte array. + +`FileInputStream` code example: + +```java +try (InputStream fis = new FileInputStream("input.txt")) { + System.out.println("Number of remaining bytes:" + + fis.available()); + int content; + long skip = fis.skip(2); + System.out.println("The actual number of bytes skipped:" + skip); + System.out.print("The content read from file:"); + while ((content = fis.read()) != -1) { + System.out.print((char) content); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +`input.txt` file content: + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155214614.png) + +Output: + +```plain +Number of remaining bytes:11 +The actual number of bytes skipped:2 +The content read from file:JavaGuide +``` + +However, generally we do not use `FileInputStream` directly alone, but usually use it in conjunction with `BufferedInputStream` (byte buffered input stream, which will be discussed later). + +Code like the following is relatively common in our projects. We read all bytes of the input stream through `readAllBytes()` and assign them directly to a `String` object. + +```java +//Create a new BufferedInputStream object +BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); +// Read the contents of the file and copy it to the String object +String result = new String(bufferedInputStream.readAllBytes()); +System.out.println(result); +``` + +`DataInputStream` is used to read data of specified types. It cannot be used alone and must be combined with other streams, such as `FileInputStream`. + +```java +FileInputStream fileInputStream = new FileInputStream("input.txt"); +//fileInputStream must be used as a construction parameter to use +DataInputStream dataInputStream = new DataInputStream(fileInputStream); +//Can read any specific type of data +dataInputStream.readBoolean(); +dataInputStream.readInt(); +dataInputStream.readUTF(); +``` + +`ObjectInputStream` is used to read Java objects from the input stream (deserialization), and `ObjectOutputStream` is used to write objects to the output stream (serialization). + +```java +ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data")); +MyClass object = (MyClass) input.readObject(); +input.close(); +``` + +In addition, the class used for serialization and deserialization must implement the `Serializable` interface. If there are properties in the object that do not want to be serialized, use `transient` to modify them. + +### OutputStream (byte output stream) + +`OutputStream` is used to write data (byte information) to a destination (usually a file). The `java.io.OutputStream` abstract class is the parent class of all byte output streams. + +`OutputStream` common methods: + +- `write(int b)`: Write specific bytes to the output stream. +- `write(byte b[ ])` : Write array `b` to the output stream, equivalent to `write(b, 0, b.length)`. +- `write(byte[] b, int off, int len)`: Based on the `write(byte b[ ])` method, the `off` parameter (offset) and the `len` parameter (the maximum number of bytes to be read) are added. +- `flush()`: Flushes this output stream and forces all buffered output bytes to be written out. +- `close()`: Close the output stream to release related system resources. + +`FileOutputStream` is the most commonly used byte output stream object. It can directly specify the file path, directly output single-byte data, or output a specified byte array. + +`FileOutputStream` code example: + +```java +try (FileOutputStream output = new FileOutputStream("output.txt")) { + byte[] array = "JavaGuide".getBytes(); + output.write(array); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +Running results:![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155514392.png) + +Similar to `FileInputStream`, `FileOutputStream` is usually used with `BufferedOutputStream` (byte buffered output stream, discussed later). + +```java +FileOutputStream fileOutputStream = new FileOutputStream("output.txt"); +BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream) +``` + +**`DataOutputStream`** is used to write specified types of data. It cannot be used alone and must be combined with other streams, such as `FileOutputStream`. + +```java +//output stream +FileOutputStream fileOutputStream = new FileOutputStream("out.txt"); +DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); +// Output any data type +dataOutputStream.writeBoolean(true); +dataOutputStream.writeByte(1); +``` + +`ObjectInputStream` is used to read Java objects from the input stream (`ObjectInputStream`, deserialization), and `ObjectOutputStream` writes objects to the output stream (`ObjectOutputStream`, serialization). + +```java +ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt") +Person person = new Person("Guide brother", "JavaGuide author"); +output.writeObject(person); +``` + +## Character stream + +Whether it is file reading and writing or network sending and receiving, the smallest storage unit of information is bytes. **Then why are I/O stream operations divided into byte stream operations and character stream operations? ** + +Personally, I think there are two main reasons: + +- The character stream is obtained by converting bytes by the Java virtual machine. This process is quite time-consuming. +- If we don't know the encoding type, it is easy to have garbled characters. + +The garbled code problem can be easily reproduced. We only need to change the content of the `input.txt` file in the `FileInputStream` code example mentioned above to Chinese. The original code does not need to be changed. + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220419154632551.png) + +Output: + +```java +Number of remaining bytes:9 +The actual number of bytes skipped:2 +The content read from file:§å®¶å¥½ +``` + +It can be clearly seen that the read content has become garbled. + +Therefore, the I/O stream simply provides an interface for directly operating characters, which facilitates our usual stream operations on characters. If audio files, pictures and other media files are better, it is better to use byte stream. If characters are involved, it is better to use character stream. + +The character stream uses `Unicode` encoding by default, and we can customize the encoding through the construction method. + +Unicode itself is just a character set that assigns a unique numerical number to each character and does not specify a specific storage method. UTF-8, UTF-16, and UTF-32 are all Unicode encoding methods, and they use different numbers of bytes to represent Unicode characters. For example, UTF-8: English occupies 1 byte, Chinese occupies 3 bytes. + +### Reader (character input stream) + +`Reader` is used to read data (character information) from the source (usually a file) into memory. The `java.io.Reader` abstract class is the parent class of all character input streams. + +`Reader` is used to read text and `InputStream` is used to read raw bytes. + +`Reader` common methods: + +- `read()` : Read a character from the input stream. +- `read(char[] cbuf)` : Read some characters from the input stream and store them into the character array `cbuf`, equivalent to `read(cbuf, 0, cbuf.length)`. +- `read(char[] cbuf, int off, int len)`: Based on the `read(char[] cbuf)` method, the `off` parameter (offset) and the `len` parameter (the maximum number of characters to be read) are added. +- `skip(long n)`: Ignore n characters in the input stream and return the actual number of ignored characters. +- `close()` : Close the input stream and release related system resources. + +`InputStreamReader` is a bridge that converts byte streams into character streams. Its subclass `FileReader` is an encapsulation based on this basis and can directly operate character files. + +```java +//The bridge that converts byte stream to character stream +public class InputStreamReader extends Reader { +} +// Used to read character files +public class FileReader extends InputStreamReader { +} +``` + +`FileReader` code example: + +```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` file content: + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220419154632551.png) + +Output: + +```plain +The actual number of bytes skipped:3 +The content read from file:I am Guide. +``` + +### Writer (character output stream) + +`Writer` is used to write data (character information) to a destination (usually a file). The `java.io.Writer` abstract class is the parent class of all character output streams. + +`Writer` common methods: + +- `write(int c)` : Write a single character. +- `write(char[] cbuf)`: Write character array `cbuf`, equivalent to `write(cbuf, 0, cbuf.length)`. +- `write(char[] cbuf, int off, int len)`: Based on the `write(char[] cbuf)` method, the `off` parameter (offset) and the `len` parameter (the maximum number of characters to be read) are added. +- `write(String str)`: Write a string, equivalent to `write(str, 0, str.length())`. +- `write(String str, int off, int len)`: Based on the `write(String str)` method, the `off` parameter (offset) and the `len` parameter (the maximum number of characters to be read) are added. +- `append(CharSequence csq)`: Appends the specified character sequence to the specified `Writer` object and returns the `Writer` object. +- `append(char c)`: Appends the specified character to the specified `Writer` object and returns the `Writer` object. +- `flush()`: Flushes this output stream and forces all buffered output characters to be written out. +- `close()`: Close the output stream to release related system resources. + +`OutputStreamWriter` is a bridge that converts character streams into byte streams. Its subclass `FileWriter` is an encapsulation based on this basis and can directly write characters to files. + +```java +// Bridge that converts character stream to byte stream +public class OutputStreamWriter extends Writer { +} +// Used to write characters to the file +public class FileWriter extends OutputStreamWriter { +} +``` + +`FileWriter` code example: + +```java +try (Writer output = new FileWriter("output.txt")) { + output.write("Hello, I am Guide."); +} catch (IOException e) { + e.printStackTrace(); +}``` + +Output result: + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220419155802288.png) + +## Byte buffer stream + +IO operations are very performance-consuming. Buffered streams load data into buffers and read/write multiple bytes at one time, thereby avoiding frequent IO operations and improving stream transmission efficiency. + +The byte buffer stream uses the decorator pattern to enhance the functionality of `InputStream` and `OutputStream` subclass objects. + +For example, we can enhance the functionality of `FileInputStream` through `BufferedInputStream` (byte buffered input stream). + +```java +//Create a new BufferedInputStream object +BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); +``` + +The performance difference between byte streams and byte buffer streams is mainly reflected in the fact that when we use both, we call `write(int b)` and `read()`, which only read one byte at a time. Since there is a buffer (byte array) inside the byte buffer stream, the byte buffer stream will first store the read bytes in the cache area, greatly reducing the number of IO times and improving reading efficiency. + +I use the `write(int b)` and `read()` methods to copy a `524.9 mb` PDF file through the byte stream and the byte buffer stream respectively. The time-consuming comparison is as follows: + +```plain +Total time taken to copy PDF files using buffered stream: 15428 milliseconds +Total time taken to copy PDF files using ordinary byte stream: 2555062 milliseconds +``` + +The time consumption difference between the two is very big. The time consumed by buffering stream is 1/165 of that of byte stream. + +The test code is as follows: + +```java +@Test +void copy_pdf_to_another_pdf_buffer_stream() { + // Record start time + long start = System.currentTimeMillis(); + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("In-depth understanding of computer operating systems.pdf")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("In-depth understanding of computer operating systems-copy.pdf"))) { + int content; + while ((content = bis.read()) != -1) { + bos.write(content); + } + } catch (IOException e) { + e.printStackTrace(); + } + //record end time + long end = System.currentTimeMillis(); + System.out.println("Total time taken to copy PDF files using buffered stream:" + (end - start) + " milliseconds"); +} + +@Test +void copy_pdf_to_another_pdf_stream() { + // Record start time + long start = System.currentTimeMillis(); + try (FileInputStream fis = new FileInputStream("In-depth understanding of computer operating systems.pdf"); + FileOutputStream fos = new FileOutputStream("In-depth understanding of computer operating systems-copy.pdf")) { + int content; + while ((content = fis.read()) != -1) { + fos.write(content); + } + } catch (IOException e) { + e.printStackTrace(); + } + //record end time + long end = System.currentTimeMillis(); + System.out.println("Total time taken to copy PDF files using normal stream:" + (end - start) + " milliseconds"); +} +``` + +If you call `read(byte b[])` and `write(byte b[], int off, int len)`, which are two methods of writing a byte array, as long as the size of the byte array is appropriate, the performance gap between the two is actually not big and can basically be ignored. + +This time we use the `read(byte b[])` and `write(byte b[], int off, int len)` methods to copy a 524.9 MB PDF file through the byte stream and the byte buffer stream respectively. The time-consuming comparison is as follows: + +```plain +Total time taken to copy PDF files using buffered streaming: 695 milliseconds +Total time taken to copy PDF files using normal byte stream: 989 milliseconds +``` + +The time-consuming difference between the two is not very big, and the performance of buffered streaming is slightly better. + +The test code is as follows: + +```java +@Test +void copy_pdf_to_another_pdf_with_byte_array_buffer_stream() { + // Record start time + long start = System.currentTimeMillis(); + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("In-depth understanding of computer operating systems.pdf")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("In-depth understanding of computer operating systems-copy.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(); + } + //record end time + long end = System.currentTimeMillis(); + System.out.println("Total time taken to copy PDF files using buffered stream:" + (end - start) + " milliseconds"); +} + +@Test +void copy_pdf_to_another_pdf_with_byte_array_stream() { + // Record start time + long start = System.currentTimeMillis(); + try (FileInputStream fis = new FileInputStream("In-depth understanding of computer operating systems.pdf"); + FileOutputStream fos = new FileOutputStream("In-depth understanding of computer operating systems-copy.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(); + } + //record end time + long end = System.currentTimeMillis(); + System.out.println("Total time taken to copy PDF files using normal stream:" + (end - start) + " milliseconds"); +} +``` + +### BufferedInputStream (byte buffered input stream) + +When `BufferedInputStream` reads data (byte information) from the source (usually a file) to the memory, it will not read byte by byte. Instead, it will first store the read bytes in the buffer area and read the bytes separately from the internal buffer. This greatly reduces the number of IOs and improves reading efficiency. + +`BufferedInputStream` maintains a buffer internally, which is actually a byte array. You can get this conclusion by reading the `BufferedInputStream` source code. + +```java +public +class BufferedInputStream extends FilterInputStream { + // Internal buffer array + protected volatile byte buf[]; + //Default size of buffer + private static int DEFAULT_BUFFER_SIZE = 8192; + // Use default buffer size + public BufferedInputStream(InputStream in) { + this(in, DEFAULT_BUFFER_SIZE); + } + // Custom buffer size + public BufferedInputStream(InputStream in, int size) { + super(in); + if (size <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + buf = new byte[size]; + } +}``` + +The buffer size defaults to **8192** bytes. Of course, you can also specify the buffer size through the `BufferedInputStream(InputStream in, int size)` constructor. + +### BufferedOutputStream (byte buffered output stream) + +When `BufferedOutputStream` writes data (byte information) to the destination (usually a file), it will not write byte by byte. Instead, it will first store the bytes to be written in the buffer area and write the bytes individually from the internal buffer. This greatly reduces the number of IOs and improves efficiency. + +```java +try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { + byte[] array = "JavaGuide".getBytes(); + bos.write(array); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +Similar to `BufferedInputStream`, `BufferedOutputStream` also maintains a buffer internally, and the size of this buffer is also **8192** bytes. + +## Character buffer stream + +`BufferedReader` (character buffered input stream) and `BufferedWriter` (character buffered output stream) are similar to `BufferedInputStream` (byte buffered input stream) and `BufferedOutputStream` (byte buffered input stream), and internally maintain a byte array as a buffer. However, the former is mainly used to operate character information. + +## Print stream + +Do you use the following code often? + +```java +System.out.print("Hello!"); +System.out.println("Hello!"); +``` + +`System.out` is actually used to obtain a `PrintStream` object, and the `print` method actually calls the `write` method of the `PrintStream` object. + +`PrintStream` belongs to the byte printing stream, corresponding to `PrintWriter` (character printing stream). `PrintStream` is a subclass of `OutputStream`, and `PrintWriter` is a subclass of `Writer`. + +```java +public class PrintStream extends FilterOutputStream + implements Appendable, Closeable { +} +public class PrintWriter extends Writer { +} +``` + +## Random access stream + +The random access stream to be introduced here refers to `RandomAccessFile` that supports jumping to any location in the file for reading and writing. + +The construction method of `RandomAccessFile` is as follows, we can specify `mode` (read and write mode). + +```java +// The openAndDelete parameter defaults to false, which means the file is opened and the file will not be deleted. +public RandomAccessFile(File file, String mode) + throws FileNotFoundException { + this(file, mode, false); +} +// private method +private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{ + // Omit most of the code +} +``` + +There are four main reading and writing modes: + +- `r` : read-only mode. +- `rw`: read and write mode +- `rws`: Relative to `rw`, `rws` synchronously updates the modifications to the "file content" or "metadata" to the external storage device. +- `rwd`: Relative to `rw`, `rwd` synchronously updates the modifications to the "content of the file" to the external storage device. + +File content refers to the data actually saved in the file, and metadata is used to describe file attributes such as file size information, creation and modification time. + +There is a file pointer in `RandomAccessFile` used to indicate the location of the next byte to be written or read. We can set the offset of the file pointer (poss bytes from the beginning of the file) through the `seek(long pos)` method of `RandomAccessFile`. If you want to get the current position of the file pointer, you can use the `getFilePointer()` method. + +`RandomAccessFile` code example: + +```java +RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); +System.out.println("Offset before reading: " + randomAccessFile.getFilePointer() + ", currently read character " + (char) randomAccessFile.read() + ", offset after reading: " + randomAccessFile.getFilePointer()); +//The current offset of the pointer is 6 +randomAccessFile.seek(6); +System.out.println("Offset before reading: " + randomAccessFile.getFilePointer() + ", currently read character " + (char) randomAccessFile.read() + ", offset after reading: " + randomAccessFile.getFilePointer()); +//Write byte data backward starting from offset 7 +randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); +//The current offset of the pointer is 0, return to the starting position +randomAccessFile.seek(0); +System.out.println("Offset before reading: " + randomAccessFile.getFilePointer() + ", currently read character " + (char) randomAccessFile.read() + ", offset after reading: " + randomAccessFile.getFilePointer()); +``` + +`input.txt` file content: + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220421162050158.png) + +Output: + +```plain +Offset before reading: 0, currently read character A, offset after reading: 1 +Offset before reading: 6, currently read character G, offset after reading: 7 +Offset before reading: 0, currently read character A, offset after reading: 1 +``` + +The content of the `input.txt` file becomes `ABCDEFGHIJK`. + +The `write` method of `RandomAccessFile` will overwrite it if there is data in the corresponding location when writing the object. + +```java +RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); +randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); +``` + +Assume that the content of the `input.txt` file changes to `ABCD` before running the above program, and changes to `HIJK` after running it. + +One of the more common applications of `RandomAccessFile` is to implement **resumable upload** of large files. What is resume transfer? To put it simply, after the file upload is suspended or fails (for example, if you encounter a network problem), you do not need to re-upload it. You only need to upload the file fragments that were not successfully uploaded. Segmented upload (first split the file into multiple file segments) is the basis for resumed upload. + +`RandomAccessFile` can help us merge file fragments. The sample code is as follows: + +![](https://oss.javaguide.cn/github/javaguide/java/io/20210609164749122.png) + +I introduced the problem of uploading large files in detail in ["Java Interview Guide"](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html). + +![](https://oss.javaguide.cn/github/javaguide/java/image-20220428104115362.png) + +The implementation of `RandomAccessFile` depends on `FileDescriptor` (file descriptor) and `FileChannel` (memory mapped file). + + \ No newline at end of file diff --git a/docs_en/java/io/io-design-patterns.en.md b/docs_en/java/io/io-design-patterns.en.md new file mode 100644 index 00000000000..97216ca88c7 --- /dev/null +++ b/docs_en/java/io/io-design-patterns.en.md @@ -0,0 +1,326 @@ +--- +title: Summary of Java IO design patterns +category: Java +tag: + -JavaIO + - Java basics +head: + - - meta + - name: keywords + content: IO design pattern, decorator, adapter, chain of responsibility, streaming processing, FilterInputStream + - - meta + - name: description + content: Combine design patterns to understand the class structure and extension methods of Java IO, and master the typical usage of stream processing. +--- + +In this article, we will briefly take a look at the applications of design patterns that we can learn from IO. + +## Decorator pattern + +**Decorator pattern** can expand the functionality of the original object without changing it. + +The decorator pattern extends the functionality of the original class through combination instead of inheritance, and is more practical in some scenarios where the inheritance relationship is more complex (the inheritance relationship of various classes in the IO scenario is more complex). + +For byte streams, `FilterInputStream` (corresponding to the input stream) and `FilterOutputStream` (corresponding to the output stream) are the core of the decorator pattern, used to enhance the functions of `InputStream` and `OutputStream` subclass objects respectively. + +Our common `BufferedInputStream` (byte buffered input stream), `DataInputStream`, etc. are all subclasses of `FilterInputStream`, `BufferedOutputStream` (byte buffered output stream), `DataOutputStream`, etc. are all subclasses of `FilterOutputStream`. + +For example, we can enhance the functionality of `FileInputStream` through `BufferedInputStream` (byte buffered input stream). + +The `BufferedInputStream` constructor is as follows: + +```java +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]; +} +``` + +It can be seen that one of the parameters of the constructor of `BufferedInputStream` is `InputStream`. + +`BufferedInputStream` code example: + +```java +try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) { + int content; + long skip = bis.skip(2); + while ((content = bis.read()) != -1) { + System.out.print((char) content); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +At this time, you may be thinking: **Why don’t we just create a `BufferedFileInputStream` (character buffered file input stream)? ** + +```java +BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt"); +``` + +This is fine if there are only a few subclasses of `InputStream`. However, there are too many subclasses of `InputStream`, and the inheritance relationship is too complicated. Wouldn't it be too troublesome if we customized a corresponding buffered input stream for each subclass? + +If you are familiar with IO streams, you will find that `ZipInputStream` and `ZipOutputStream` can also enhance the capabilities of `BufferedInputStream` and `BufferedOutputStream` respectively. + +```java +BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName)); +ZipInputStream zis = new ZipInputStream(bis); + +BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName)); +ZipOutputStream zipOut = new ZipOutputStream(bos); +``` + +`ZipInputStream` and `ZipOutputStream` inherit from `InflaterInputStream` and `DeflaterOutputStream` respectively. + +```java +public +class InflaterInputStream extends FilterInputStream { +} + +public +class DeflaterOutputStream extends FilterOutputStream { +} + +``` + +This is also a very important feature of the decorator pattern, that is, multiple decorators can be nested on the original class. + +In order to achieve this effect, the decorator class needs to inherit the same abstract class or implement the same interface as the original class. The common parent classes of these IO-related decoration classes and original classes introduced above are `InputStream` and `OutputStream`. + +For character streams, `BufferedReader` can be used to increase the functionality of the `Reader` (character input stream) subclass, and `BufferedWriter` can be used to increase the functionality of the `Writer` (character output stream) subclass. + +```java +BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); +``` + +There are so many examples of decorator pattern applications in IO streams that you don’t need to remember them specially. It’s completely unnecessary! After understanding the core of the decorator pattern, you will naturally know where the decorator pattern is used when using it. + +## Adapter mode + +**Adapter Pattern** is mainly used for coordination of classes with incompatible interfaces. You can think of it like the power adapter we often use every day. + +The object or class that exists in the adapter pattern is called Adaptee, and the object or class that acts on the adapter is called Adapter. Adapters are divided into object adapters and class adapters. Class adapters are implemented using inheritance relationships, and object adapters are implemented using composition relationships. + +The character stream and byte stream in the IO stream have different interfaces. The coordination between them is based on the adapter mode, more precisely, it is an object adapter. Through the adapter, we can adapt the byte stream object into a character stream object, so that we can read or write character data directly through the byte stream object. + +`InputStreamReader` and `OutputStreamWriter` are two adapters (Adapter). At the same time, they are also the bridge between byte stream and character stream. `InputStreamReader` uses `StreamDecoder` (stream decoder) to decode bytes, ** realize the conversion of byte stream to character stream, ** `OutputStreamWriter` uses `StreamEncoder` (stream encoder) to encode characters, and realize the conversion of character stream to byte stream. + +Subclasses of `InputStream` and `OutputStream` are adaptees, and `InputStreamReader` and `OutputStreamWriter` are adapters. + +```java +// InputStreamReader is the adapter, FileInputStream is the adapted class +InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8"); +// BufferedReader enhances the functionality of InputStreamReader (decorator mode) +BufferedReader bufferedReader = new BufferedReader(isr); +``` + +`java.io.InputStreamReader` Part of the source code: + +```java +public class InputStreamReader extends Reader { + //Object used for decoding + private final StreamDecoder sd; + public InputStreamReader(InputStream in) { + super(in); + try { + // Get the StreamDecoder object + sd = StreamDecoder.forInputStreamReader(in, this, (String)null); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + //Use the StreamDecoder object to do specific reading work + public int read() throws IOException { + return sd.read(); + } +}``` + +`java.io.OutputStreamWriter` Part of the source code: + +```java +public class OutputStreamWriter extends Writer { + // Object used for encoding + private final StreamEncoder se; + public OutputStreamWriter(OutputStream out) { + super(out); + try { + // Get the StreamEncoder object + se = StreamEncoder.forOutputStreamWriter(out, this, (String)null); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + //Use the StreamEncoder object to do specific writing work + public void write(int c) throws IOException { + se.write(c); + } +} +``` + +**What is the difference between adapter mode and decorator mode? ** + +**Decorator Pattern** focuses more on dynamically enhancing the functionality of the original class. The decorator class needs to inherit the same abstract class or implement the same interface as the original class. Moreover, the decorator pattern supports the nested use of multiple decorators on the original class. + +**Adapter pattern** focuses more on allowing classes whose interfaces are incompatible and cannot interact to work together. When we call the corresponding method of the adapter, the adapter class will internally call the method of the adapter class or the class related to the adapter class. This process is transparent. For example, `StreamDecoder` (stream decoder) and `StreamEncoder` (stream encoder) obtain the `FileChannel` object based on `InputStream` and `OutputStream` respectively and call the corresponding `read` method and `write` method to read and write byte data. + +```java +StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { + // Omit most of the code + // Get the FileChannel object based on the InputStream object + ch = getChannel((FileInputStream)in); +} +``` + +Both adapters and adapters do not need to inherit the same abstract class or implement the same interface. + +In addition, the `FutureTask` class uses the adapter pattern. The implementation of the `RunnableAdapter` internal class of `Executors` belongs to the adapter and is used to adapt `Runnable` to `Callable`. + +The `FutureTask` parameter contains a constructor of `Runnable`: + +```java +public FutureTask(Runnable runnable, V result) { + // Call the callable method of the Executors class + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +``` + +Corresponding methods and adapters in `Executors`: + +```java +// What is actually called is the constructor of the internal class RunnableAdapter of Executors +public static Callable callable(Runnable task, T result) { + if (task == null) + throw new NullPointerException(); + return new RunnableAdapter(task, result); +} +// adapter +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; + } +} +``` + +## Factory pattern + +Factory mode is used to create objects. Factory mode is widely used in NIO. For example, the `newInputStream` method of the `Files` class is used to create an `InputStream` object (static factory), the `get` method of the `Paths` class creates a `Path` object (static factory), and the `ZipFileSystem` class (a class under the `sun.nio` package, belonging to some internal implementations related to `java.nio`) The `getPath` method creates a `Path` object (simple factory). + +```java +InputStream is = Files.newInputStream(Paths.get(generatorLogoPath)) +``` + +## Observer pattern + +The file directory listening service in NIO uses the observer mode. + +The file directory listening service in NIO is based on the `WatchService` interface and the `Watchable` interface. `WatchService` belongs to the observer, and `Watchable` belongs to the observed. + +The `Watchable` interface defines a method `register` for registering objects to `WatchService` (monitoring service) and binding listening events. + +```java +public interface Path + extends Comparable, Iterable, Watchable{ +} + +public interface Watchable { + WatchKey register(WatchService watcher, + WatchEvent.Kind[] events, + WatchEvent.Modifier... modifiers) + throws IOException; +} +``` + +`WatchService` is used to monitor changes in file directories. The same `WatchService` object can monitor multiple file directories. + +```java +// Create WatchService object +WatchService watchService = FileSystems.getDefault().newWatchService(); + +//Initialize the Path class of a monitored folder: +Path path = Paths.get("workingDirectory"); +//Register this path object to WatchService (monitoring service) +WatchKey watchKey = path.register( +watchService, StandardWatchEventKinds...); +``` + +The second parameter `events` (the events that need to be monitored) of the `register` method of the `Path` class is a variable-length parameter, which means that we can listen to multiple events at the same time. + +```java +WatchKey register(WatchService watcher, + WatchEvent.Kind... events) + throws IOException; +``` + +There are three commonly used listening events: + +- `StandardWatchEventKinds.ENTRY_CREATE`: File creation. +- `StandardWatchEventKinds.ENTRY_DELETE` : File deletion. +- `StandardWatchEventKinds.ENTRY_MODIFY` : File modification. + +The `register` method returns a `WatchKey` object. Through the `WatchKey` object, you can obtain the specific information of the event, such as whether the file was created, deleted or modified in the file directory, and what is the specific name of the file that was created, deleted or modified. + +```java +WatchKey key; +while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + // You can call the method of the WatchEvent object to do some things, such as output the specific context information of the event. + } + key.reset(); +} +``` + +`WatchService` internally uses a daemon thread to detect file changes in a regular polling manner. The simplified source code is as follows. + +```java +classPollingWatchService + extends AbstractWatchService +{ + //Define a daemon thread to poll and detect file changes + 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) { + //Update listening events + this.events = events; + + // Start regular polling + Runnable thunk = new Runnable() { public void run() { poll(); }}; + this.poller = scheduledExecutor + .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS); + } + } +}``` + +## Reference + +- Patterns in Java APIs: +- Decorator pattern: Learn the decorator pattern by analyzing the Java IO class library source code: +- What is the sun.nio package? Is it java code? - RednaxelaFX + + \ No newline at end of file diff --git a/docs_en/java/io/io-model.en.md b/docs_en/java/io/io-model.en.md new file mode 100644 index 00000000000..85ab8f8e24f --- /dev/null +++ b/docs_en/java/io/io-model.en.md @@ -0,0 +1,142 @@ +--- +title: Java IO 模型详解 +category: Java +tag: + - Java IO + - Java基础 +head: + - - meta + - name: keywords + content: IO 模型,阻塞IO,非阻塞IO,同步异步,多路复用,Reactor,Proactor + - - meta + - name: description + content: 总结常见 IO 模型与并发处理方式,理解阻塞/非阻塞与同步/异步差异。 +--- + +IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收获!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~ + +_个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!_ + +## 前言 + +I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。 + +## I/O + +### 何为 I/O? + +I/O(**I**nput/**O**utput) 即**输入/输出** 。 + +**我们先从计算机结构的角度来解读一下 I/O。** + +根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。 + +![冯诺依曼体系结构](https://oss.javaguide.cn/github/javaguide/java/io/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9pcy1jbG91ZC5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70.jpeg) + +输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。 + +输入设备向计算机输入数据,输出设备接收计算机输出的数据。 + +**从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。** + +**我们再先从应用程序的角度来解读一下 I/O。** + +根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 **用户空间(User space)** 和 **内核空间(Kernel space )** 。 + +像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。 + +并且,用户空间的程序不能直接访问内核空间。 + +当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。 + +因此,用户进程想要执行 IO 操作的话,必须通过 **系统调用** 来间接访问内核空间 + +我们在平常开发过程中接触最多的就是 **磁盘 IO(读写文件)** 和 **网络 IO(网络请求和响应)**。 + +**从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。** + +当应用程序发起 I/O 调用后,会经历两个步骤: + +1. 内核等待 I/O 设备准备好数据 +2. 内核将数据从内核空间拷贝到用户空间。 + +### 有哪些常见的 IO 模型? + +UNIX 系统下, IO 模型一共有 5 种:**同步阻塞 I/O**、**同步非阻塞 I/O**、**I/O 多路复用**、**信号驱动 I/O** 和**异步 I/O**。 + +这也是我们经常提到的 5 种 IO 模型。 + +## Java 中 3 种常见 IO 模型 + +### BIO (Blocking I/O) + +**BIO 属于同步阻塞 IO 模型** 。 + +同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。 + +![图源:《深入拆解Tomcat & Jetty》](https://oss.javaguide.cn/p3-juejin/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.png) + +在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + +### 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,发起一个 read 调用,如果数据没有准备好,这个时候应用程序可以不阻塞等待,而是切换去做一些小的计算任务,然后很快回来继续发起 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) + +## 参考 + +- "In-depth Teardown of Tomcat & Jetty" +- How to complete an IO: +- Programmers should understand IO like this: [https://www.jianshu.com/p/fa7bdc4f3de7](https://www.jianshu.com/p/fa7bdc4f3de7) +- 10 minutes to understand, the underlying principles of Java NIO: +- How much you know about IO models | Theory: +- "UNIX Network Programming Volume 1; Socket Networking API" Section 6.2 IO Model + + \ No newline at end of file diff --git a/docs_en/java/io/nio-basis.en.md b/docs_en/java/io/nio-basis.en.md new file mode 100644 index 00000000000..da323229d60 --- /dev/null +++ b/docs_en/java/io/nio-basis.en.md @@ -0,0 +1,399 @@ +--- +title: Java NIO 核心知识总结 +category: Java +tag: + - Java IO + - Java基础 +head: + - - meta + - name: keywords + content: NIO,Channel,Buffer,Selector,非阻塞IO,零拷贝,文件与网络 + - - meta + - name: description + content: 介绍 Java NIO 的核心组件与使用方式,理解 Channel/Buffer/Selector 的协作与性能优势。 +--- + +在学习 NIO 之前,需要先了解一下计算机 I/O 模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。 + +## NIO 简介 + +在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。 + +为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — **NIO** (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。 + +下图是 BIO、NIO 和 AIO 处理客户端请求的简单对比图(关于 AIO 的介绍,可以看我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html),不是重点,了解即可)。 + +![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) + +⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。 + +## NIO 核心组件 + +NIO 主要包括以下三个核心组件: + +- **Buffer(缓冲区)**:NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 +- **Channel(通道)**:Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。 +- **Selector(选择器)**:允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。 + +三者的关系如下图所示(暂时不理解没关系,后文会详细介绍): + +![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) + +下面详细介绍一下这三个组件。 + +### Buffer(缓冲区) + +在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。 + +在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。 + +`Buffer` 的子类如下图所示。其中,最常用的是 `ByteBuffer`,它可以用来存储和操作字节数据。 + +![Buffer 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/buffer-subclasses.png) + +你可以将 Buffer 理解为一个数组,`IntBuffer`、`FloatBuffer`、`CharBuffer` 等分别对应 `int[]`、`float[]`、`char[]` 等。 + +为了更清晰地认识缓冲区,我们来简单看看`Buffer` 类中定义的四个成员变量: + +```java +public abstract class Buffer { + // Invariants: mark <= position <= limit <= capacity + private int mark = -1; + private int position = 0; + private int limit; + private int capacity; +} +``` + +这四个成员变量的具体含义如下: + +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(); + } + } +} +``` + +In the example, we create a simple server that listens on port 8080 and uses a Selector to handle connection, read, and write events. When data from the client is received, the server reads the data and prints it to the console, then replies "Hello, Client!" to the client. + +## NIO zero copy + +Zero copy is a common method to improve the performance of IO operations. Top open source projects such as ActiveMQ, Kafka, RocketMQ, QMQ, and Netty all use zero copy. + +Zero copy means that when the computer performs IO operations, the CPU does not need to copy data from one storage area to another, thereby reducing context switching and CPU copy time. In other words, zero copy mainly solves the problem of frequent copying of data by the operating system when processing I/O operations. Common zero-copy implementation technologies include: `mmap+write`, `sendfile` and `sendfile + DMA gather copy`. + +The following figure shows a comparison of various zero-copy technologies: + +| | CPU copy | DMA copy | System call | Context switch | +| -------------------------- | -------- | -------- | ---------- | ---------- | +| Traditional method | 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 | + +It can be seen that whether it is the traditional I/O method or the introduction of zero copy, two DMA (Direct Memory Access) copies are indispensable. Because both DMAs rely on hardware. Zero copy mainly reduces CPU copying and context switching. + +Java’s support for zero-copy: + +- `MappedByteBuffer` is an implementation provided by NIO based on the zero-copy method of memory mapping (`mmap`). The underlying layer actually calls the `mmap` system call of the Linux kernel. It can map a file or part of a file into memory to form a virtual memory file, so that the data in the memory can be directly manipulated without the need to read and write files through system calls. +- `transferTo()/transferFrom()` of `FileChannel` is an implementation provided by NIO based on the zero-copy method of sending files (`sendfile`). The bottom layer actually calls the `sendfile` system call of the Linux kernel. It can send file data directly from disk to the network without going through user space buffers. Regarding the usage of `FileChannel`, you can read this article: [Java NIO File Channel FileChannel Usage](https://www.cnblogs.com/robothy/p/14235598.html). + +Code example: + +```java +private void loadFileIntoMemory(File xmlFile) throws IOException { + FileInputStream fis = new FileInputStream(xmlFile); + //Create FileChannel object + FileChannel fc = fis.getChannel(); + // FileChannel.map() maps the file to direct memory and returns a MappedByteBuffer object + MappedByteBuffer mmb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + xmlFileBuffer = new byte[(int)fc.size()]; + mmb.get(xmlFileBuffer); + fis.close(); +} +``` + +## Summary + +In this article, we mainly introduce the core knowledge points of NIO, including NIO's core components and zero copy. + +If we need to use NIO to build a network program, it is not recommended to use native NIO directly. The programming is complex and the functionality is too weak. It is recommended to use some mature NIO-based network programming frameworks such as Netty. Netty has made some optimizations and extensions based on NIO, such as supporting multiple protocols, supporting SSL/TLS, etc. + +## Reference + +- A brief analysis of Java NIO: + +- Interviewer: Do you know Java NIO? + +- Java NIO: Buffer, Channel and Selector: + + \ No newline at end of file diff --git a/docs_en/java/jvm/class-file-structure.en.md b/docs_en/java/jvm/class-file-structure.en.md new file mode 100644 index 00000000000..038dbed1b63 --- /dev/null +++ b/docs_en/java/jvm/class-file-structure.en.md @@ -0,0 +1,222 @@ +--- +title: Detailed explanation of class file structure +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: Class file, constant pool, magic number, version, fields, methods, properties + - - meta + - name: description + content: Introduces core components such as Java bytecode Class file structure and constant pool to assist in understanding compiled products. +--- + +## Review the bytecode + +In Java, the code that the JVM can understand is called bytecode (that is, a file with the extension `.class`). It is not oriented to any specific processor, only to the virtual machine. The Java language solves the problem of low execution efficiency of traditional interpreted languages ​​to a certain extent through bytecode, while retaining the portability characteristics of interpreted languages. Therefore, Java programs run more efficiently, and because the bytecode is not specific to a specific machine, Java programs can run on computers with many different operating systems without recompiling. + +Clojure (a dialect of the Lisp language), Groovy, Scala, JRuby, Kotlin and other languages ​​all run on the Java virtual machine. The figure below shows that different languages ​​are compiled into `.class` files by different compilers and finally run on the Java virtual machine. The binary format of `.class` files can be viewed using [WinHex](https://www.x-ways.net/winhex/). + +![Programming language running on the Java virtual machine](https://oss.javaguide.cn/github/javaguide/java/basis/java-virtual-machine-program-language-os.png) + +It can be said that the `.class` file is an important bridge between different languages ​​in the Java virtual machine, and it is also an important reason to support Java cross-platform. + +## Class file structure summary + +According to the Java virtual machine specification, Class files are defined through `ClassFile`, which is somewhat similar to the C language structure. + +The structure of `ClassFile` is as follows: + +```java +ClassFile { + u4 magic; //Mark of Class file + u2 minor_version;//Minor version number of Class + u2 major_version;//The major version number of Class + u2 constant_pool_count;//The number of constant pools + cp_info constant_pool[constant_pool_count-1];//Constant pool + u2 access_flags;//Access flag of 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;//The number of attributes in this type of attribute table + attribute_info attributes[attributes_count];//Attribute table collection +} +``` + +By analyzing the contents of `ClassFile`, we can know the composition of the class file. + +![ClassFile content analysis](https://oss.javaguide.cn/java-guide-blog/16d5ec47609818fc.jpeg) + +The picture below is viewed through the IDEA plug-in `jclasslib`. You can see the Class file structure more intuitively. + +![](https://oss.javaguide.cn/java-guide-blog/image-20210401170711475.png) + +Using `jclasslib`, you can not only visually view the bytecode file corresponding to a certain class, but also view the basic information of the class, constant pool, interface, properties, functions and other information. + +The following is a detailed introduction to some components involved in the Class file structure. + +###Magic Number + +```java + u4 magic; //Mark of Class file +``` + +The first 4 bytes of each Class file are called the Magic Number. Its only function is to determine whether this file is a Class file that can be received by the virtual machine. The Java specification specifies that the magic number is a fixed value: 0xCAFEBABE. If the file read does not begin with this magic number, the Java virtual machine will refuse to load it. + +### Class file version number (Minor&Major Version) + +```java + u2 minor_version;//Minor version number of Class + u2 major_version;//The major version number of Class +``` + +The four bytes immediately following the magic number store the version number 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**. + +Whenever Java releases a major version (such as Java 8, Java 9), the major version number will be increased by 1. You can use the `javap -v` command to quickly view the version number information of the Class file. + +A higher version of the Java virtual machine can execute Class files generated by a lower version of the compiler, but a lower version of the Java virtual machine cannot execute Class files generated by a higher version of the compiler. Therefore, during actual development, we must ensure that the JDK version developed is consistent with the JDK version in the production environment. + +### Constant Pool + +```java + u2 constant_pool_count;//The number of constant pools + cp_info constant_pool[constant_pool_count-1];//Constant pool +``` + +Immediately after the major and minor version numbers is the constant pool. The number of constant pools is `constant_pool_count-1` (**The constant pool counter starts counting from 1. There are special considerations for emptying the 0th constant. An index value of 0 means "do not reference any constant pool item"**). + +The constant pool mainly stores two major constants: literals and symbolic references. Literals are closer to the concept of constants at the Java language level, such as text strings, constant values ​​declared as final, etc. Symbol reference is a concept related to compilation principles. Includes the following three types of constants: + +- Fully qualified names of classes and interfaces +- Name and descriptor of the field +- the name and descriptor of the method + +Each constant in the constant pool is a table. These 14 tables have a common feature: the first bit at the beginning is a u1 type flag bit -tag to identify the type of the constant, representing which constant type the current constant belongs to. ** + +| Type | Tag | Description | +| :----------------------------------: | :----------: | :----------------------------------: | +| CONSTANT_utf8_info | 1 | UTF-8 encoded string | +| CONSTANT_Integer_info | 3 | Integer literal | +| CONSTANT_Float_info | 4 | Floating point literal | +| CONSTANT_Long_info | 5 | Long integer literal | +| CONSTANT_Double_info | 6 | Double precision floating point literal | +| CONSTANT_Class_info | 7 | Symbolic reference to a class or interface | +| CONSTANT_String_info | 8 | String type literal | +| CONSTANT_FieldRef_info | 9 | Symbolic reference of field | +| CONSTANT_MethodRef_info | 10 | Symbolic reference to a method in a class | +| CONSTANT_InterfaceMethodRef_info | 11 | Symbolic reference to a method in an interface || CONSTANT_NameAndType_info | 12 | Symbolic reference to a field or method | +| CONSTANT_MethodType_info | 16 | Flag method type | +| CONSTANT_MethodHandle_info | 15 | Represents method handle | +| CONSTANT_InvokeDynamic_info | 18 | Represents a dynamic method call point | + +`.class` file can use the `javap -v class class name` command to view the information in its constant pool (`javap -v class class name-> temp.txt`: output the results to the temp.txt file). + +### Access Flags + +```java + u2 access_flags;//Access flag of Class +``` + +After the constant pool ends, the next two bytes represent the access flag. This flag is used to identify some class or interface level access information, including: whether the Class is a class or an interface, whether it is a `public` or `abstract` type, if it is a class, whether it is declared as `final`, etc. + +Class access and property modifiers: + +![Class access and attribute modifiers](https://oss.javaguide.cn/github/javaguide/java/%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png) + +We define an `Employee` class + +```java +package top.snailclimb.bean; +public class Employee { + ... +} +``` + +Use the `javap -v class class name` command to look at the access flags of the class. + +![View the access flag of the 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) + +### Index collection of current class (This Class), parent class (Super Class), and interfaces (Interfaces) + +```java + 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 +``` + +The inheritance relationship of Java classes is determined by three items: class index, parent class index and interface index set. The class index, parent class index and interface index set are arranged in order after the access flag. + +The class index is used to determine the fully qualified name of this class, and the parent class index is used to determine the fully qualified name of the parent class of this class. Due to the single inheritance of the Java language, there is only one parent class index. Except for `java.lang.Object`, all Java classes have parent classes, so except for `java.lang.Object`, the parent class index of all Java classes is not 0. + +The interface index collection is used to describe which interfaces this class implements. These implemented interfaces will be arranged in the interface index collection from left to right in the order of the interfaces after `implements` (or `extends` if the class itself is an interface). + +### Field table collection (Fields) + +```java + u2 fields_count;//Number of fields + field_info fields[fields_count];//A class can have a field +``` + +Field info is used to describe variables declared in an interface or class. Fields include class-level variables and instance variables, but do not include local variables declared within methods. + +**Structure of field info (field table):** + +![Structure of field table](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:** The scope of the field (`public`, `private`, `protected` modifiers), whether it is an instance variable or a class variable (`static` modifier), whether it can be serialized (transient modifier), variability (final), visibility (volatile modifier, whether to force reading and writing from main memory). +- **name_index:** A reference to the constant pool, representing the name of the field; +- **descriptor_index:** A reference to the constant pool, representing the descriptors of fields and methods; +- **attributes_count:** A field will also have some additional attributes, attributes_count stores the number of attributes; +- **attributes[attributes_count]:** stores the specific content of specific attributes. + +In the above information, each modifier is a Boolean value, either with a certain modifier or without, which is very suitable to be represented by flag bits. The name of the field and the data type for which the field is defined cannot be fixed and can only be described by referring to the constants in the constant pool. + +**Value of access_flag of field:** + +![The value of the access_flag of the field](https://oss.javaguide.cn/github/javaguide/java/jvm/class-file-fields-access_flag.png) + +### Method table collection (Methods) + +```java + u2 methods_count;//Number of methods + method_info methods[methods_count];//A class can have multiple methods +``` + +methods_count represents the number of methods, while method_info represents the method table. + +The description of methods in the Class file storage format is almost identical to the description of fields. The structure of the method table is the same as the field table, including access flags, name indexes, descriptor indexes, and attribute table collections. + +**method_info (method table) structure:** + +![Structure of method table](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 value of method table:** + +![Access_flag value of method table](https://oss.javaguide.cn/github/javaguide/java/jvm/class-file-methods-access_flag.png) + +Note: Because the `volatile` modifier and the `transient` modifier cannot modify methods, there are no corresponding flags in the access flags of the method table. However, keyword modification methods such as `synchronized`, `native`, and `abstract` have been added, so there are more flags corresponding to these keywords. + +### Attribute table collection (Attributes) + +```java + u2 attributes_count;//The number of attributes in this type of attribute table + attribute_info attributes[attributes_count];//Attribute table collection +``` + +Class files, field tables, and method tables can all carry their own set of attribute tables to describe information specific to certain scenarios. Different from the order, length and content required by other data items in the Class file, the restrictions on the attribute table collection are slightly looser. Each attribute table is no longer required to have a strict order, and as long as it does not duplicate existing attribute names, anyone can implement a compiler to write self-defined attribute information into the attribute table. The Java virtual machine will ignore attributes it does not recognize when running. + +## Reference + +- "Practical Java Virtual Machine" +- Chapter 4. The class File Format - Java Virtual Machine Specification: +- Example analysis of the file structure of JAVA CLASS: +- "Java Virtual Machine Principle Illustration" 1.2.2, detailed explanation of the constant pool in the Class file (Part 1): + + \ No newline at end of file diff --git a/docs_en/java/jvm/class-loading-process.en.md b/docs_en/java/jvm/class-loading-process.en.md new file mode 100644 index 00000000000..aa0fead1723 --- /dev/null +++ b/docs_en/java/jvm/class-loading-process.en.md @@ -0,0 +1,151 @@ +--- +title: 类加载过程详解 +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: 类加载,加载,验证,准备,解析,初始化,clinit,常量池 + - - meta + - name: description + content: 拆解 JVM 类加载的各阶段与关键细节,理解验证、准备、解析与初始化的具体行为。 +--- + +## 类的生命周期 + +类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。 + +这 7 个阶段的顺序如下图所示: + +![一个类的完整生命周期](https://oss.javaguide.cn/github/javaguide/java/jvm/lifecycle-of-a-class.png) + +## 类加载过程 + +**Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?** + +系统加载 Class 类型的文件主要三步:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 + +![类加载过程](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")。 + +### 加载 + +类加载过程的第一步,主要完成下面 3 件事情: + +1. 通过全类名获取定义此类的二进制字节流。 +2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。 +3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口。 + +虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取( `ZIP`、 `JAR`、`EAR`、`WAR`、网络、动态代理技术运行时动态生成、其他文件生成比如 `JSP`...)、怎样获取。 + +加载这一步主要是通过我们后面要讲到的 **类加载器** 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 **双亲委派模型** 决定(不过,我们也能打破双亲委派模型)。 + +> 类加载器、双亲委派模型也是非常重要的知识点,这部分内容在[类加载器详解](https://javaguide.cn/java/jvm/classloader.html "类加载器详解")这篇文章中有详细介绍到。阅读本篇文章的时候,大家知道有这么个东西就可以了。 + +每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的。 + +一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。 + +加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。 + +### 验证 + +**验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。** + +验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。 + +不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。但是需要注意的是 `-Xverify:none` 和 `-noverify` 在 JDK 13 中被标记为 deprecated ,在未来版本的 JDK 中可能会被移除。 + +验证阶段主要由四个检验阶段组成: + +1. 文件格式验证(Class 文件格式检查) +2. 元数据验证(字节码语义检查) +3. 字节码验证(程序语义检查) +4. 符号引用验证(类的正确性检查) + +![验证阶段示意图](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-process-verification.png) + +文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。 + +> 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 **类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据**。 +> +> 关于方法区的详细介绍,推荐阅读 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html "Java 内存区域详解") 这篇文章。 + +符号引用验证发生在类加载过程中的解析阶段,具体点说是 JVM 将符号引用转化为直接引用的时候(解析阶段会介绍符号引用和直接引用)。 + +符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如: + +- `java.lang.IllegalAccessError`:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。 +- `java.lang.NoSuchFieldError`:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。 +- `java.lang.NoSuchMethodError`:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。 +- …… + +### 准备 + +**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: + +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。 + +**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 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) + +### Analysis + +**The parsing phase is the process in which the virtual machine replaces symbol references in the constant pool with direct references. ** The parsing action is mainly performed on class or interface, field, class method, interface method, method type, method handle and call qualifier 7 class symbol references. + +The explanation of symbolic references and direct references in the third edition of Section 7.3.4 of "In-depth Understanding of the Java Virtual Machine" is as follows: + +![Symbol reference and direct reference](https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png) + +For example: when the program executes a method, the system needs to know exactly where the method is located. The Java virtual machine prepares a method table for each class to store all methods in the class. When you need to call a method of a class, you can call the method directly as long as you know the offset of the method in the method table. By parsing the operation symbol reference, it can be directly converted into the location of the target method in the method table of the class, so that the method can be called. + +In summary, the parsing phase is the process in which the virtual machine replaces the symbolic reference in the constant pool with a direct reference, that is, obtains the pointer or offset of the class, field, or method in memory. + +### Initialization + +**The initialization phase is the process of executing the initialization method ` ()` method. It is the last step of class loading. At this step, the JVM begins to actually execute the Java program code (bytecode) defined in the class. ** + +> Note: The ` ()` method is automatically generated after compilation. + +For calls to the ` ()` method, the virtual machine itself ensures its safety in a multi-threaded environment. Because the ` ()` method is thread-safe with locks, class initialization in a multi-threaded environment may cause multiple threads to block, and this blocking is difficult to detect. + +For the initialization phase, the virtual machine strictly regulates that there are only 6 situations in which a class must be initialized (a class will only be initialized if it is actively used): + +1. When encountering the four bytecode instructions `new`, `getstatic`, `putstatic` or `invokestatic`: + - `new`: Create an instance object of a class. + - `getstatic`, `putstatic`: Read or set static fields of a type (except static fields modified by `final` and the results have been put into the constant pool at compile time). + - `invokestatic`: Invoke the static method of the class. +2. Use the methods of the `java.lang.reflect` package to make reflection calls to the class, such as `Class.forName("...")`, `newInstance()`, etc. If the class is not initialized, its initialization needs to be triggered. +3. Initialize a class. If its parent class has not been initialized yet, trigger the initialization of the parent class first. +4. When the virtual machine starts, the user needs to define a main class to be executed (the class containing the `main` method), and the virtual machine will initialize this class first. +5. `MethodHandle` and `VarHandle` can be regarded as lightweight reflection calling mechanisms. If you want to use these two calls, you must first use `findStaticVarHandle` to initialize the class to be called. +6. **"Additional, from [issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")"** When an interface defines a new default method added by JDK8 (an interface method modified by the default keyword), if the implementation class of this interface is initialized, the interface must be initialized before it. + +## Class unloading + +> This part of uninstalling comes from [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662 "issue#662") and is supplemented by **[guang19](https://github.com/guang19 "guang19")**. + +**Unloading a class means that the Class object of this class is GC. ** + +Uninstalling classes needs to meet 3 requirements: + +1. All instance objects of this class have been GCed, which means that there are no instance objects of this class in the heap. +2. The class is not referenced anywhere else +3. The instance of the class loader of this class has been GC + +Therefore, during the JVM life cycle, classes loaded by the class loader that comes with the jvm will not be unloaded. But classes loaded by our custom class loader may be unloaded. + +Just think about it, the `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` that comes with the JDK are responsible for loading the classes provided by the JDK, so they (instances of the class loader) will definitely not be recycled. Instances of our custom class loader can be recycled, so classes loaded using our custom loader can be unloaded. + +**Reference** + +- "In-depth Understanding of Java Virtual Machine" +- "Practical Java Virtual Machine" +- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification: + + \ No newline at end of file diff --git a/docs_en/java/jvm/classloader.en.md b/docs_en/java/jvm/classloader.en.md new file mode 100644 index 00000000000..bb3cd02f4cc --- /dev/null +++ b/docs_en/java/jvm/classloader.en.md @@ -0,0 +1,412 @@ +--- +title: Detailed explanation of class loader (key points) +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: Class loader, parent delegation, loading link initialization, custom ClassLoader, ClassPath + - - meta + - name: description + content: An in-depth explanation of the JVM class loading mechanism and parent delegation model, including the loading process and common practices. +--- + +## Review the class loading process + +Before we start introducing class loaders and the parent delegation model, let's briefly review the class loading process. + +- Class loading process: **Loading->Connection->Initialization**. +- The connection process can be divided into three steps: **Verification->Preparation->Analysis**. + +![Class loading process](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png) + +Loading is the first step in the class loading process, which mainly completes the following three things: + +1. Get the binary byte stream that defines this class through the full class name +2. Convert the static storage structure represented by the byte stream into the runtime data structure of the method area +3. Generate a `Class` object representing the class in memory as the access point for these data in the method area. + +## Class loader + +### Class loader introduction + +Class loaders have appeared since JDK 1.0, originally just to meet the needs of Java Applets (which have been obsolete). Later, it gradually became an important part of Java programs, giving Java classes the ability to be dynamically loaded into the JVM and executed. + +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. +> +> Every Class object contains a reference to the ClassLoader that defined it. +> +> 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. + +Translated, it roughly means: + +> A class loader is an object responsible for loading classes. `ClassLoader` is an abstract class. Given the binary name of a class, the class loader should attempt to locate or generate the data that constitutes the class definition. A typical strategy is to convert the name to a filename and then read a "class file" of that name from the file system. +> +> Every Java class has a reference to the `ClassLoader` that loaded it. However, the array class is not created through `ClassLoader`, but is automatically created by the JVM when needed. When the array class obtains `ClassLoader` through the `getClassLoader()` method, it is consistent with the `ClassLoader` of the element type of the array. + +It can be seen from the above introduction: + +- A class loader is an object responsible for loading classes and is used to implement the loading step in the class loading process. +- Every Java class has a reference to the `ClassLoader` that loaded it. +- The array class is not created through `ClassLoader` (the array class does not have a corresponding binary byte stream), but is directly generated by the JVM. + +```java +class Class { + ... + private final ClassLoader classLoader; + @CallerSensitive + public ClassLoader getClassLoader() { + //... + } + ... +} +``` + +Simply put, the main function of the class loader is to dynamically load the bytecode of the Java class (`.class` file) into the JVM (generate a `Class` object representing the class in memory). ** Bytecode can be a Java source program (`.java` file) compiled by `javac`, or it can be dynamically generated by a tool or downloaded from the Internet. + +In fact, in addition to loading classes, the class loader can also load resources required by Java applications such as text, images, configuration files, videos, and other file resources. This article only discusses its core function: loading classes. + +### Class loader loading rules + +When the JVM starts, it does not load all classes at once, but dynamically loads them as needed. In other words, most classes will only be loaded when they are specifically used, which is more memory-friendly. + +Loaded classes will be placed in `ClassLoader`. When loading a class, the system will first determine whether the current class has been loaded. Classes that have been loaded will be returned directly, otherwise loading will be attempted. In other words, for a class loader, classes with the same binary name will only be loaded once. + +```java +public abstract class ClassLoader { + ... + private final ClassLoader parent; + // Classes loaded by this classloader. + private final Vector> classes = new Vector<>(); + // Called by the VM to record each loaded class with this class loader. + void addClass(Class c) { + classes.addElement(c); + } + ... +} +``` + +### Class loader summary + +There are three important `ClassLoaders` built into the JVM: + +1. **`BootstrapClassLoader`**: The top-level loading class, implemented by C++, usually expressed as null, and has no parent. It is mainly used to load the core class library inside the JDK (`rt.jar`, `resources.jar`, `charsets.jar` and other jar packages and classes in the `%JAVA_HOME%/lib` directory) as well as the All classes under the path specified by the `-Xbootclasspath` parameter. +2. **`ExtensionClassLoader` (Extension Class Loader)**: Mainly responsible for loading jar packages and classes in the `%JRE_HOME%/lib/ext` directory and all classes in the path specified by the `java.ext.dirs` system variable. +3. **`AppClassLoader` (Application Class Loader)**: A loader for our users, responsible for loading all jar packages and classes under the current application classpath. + +> 🌈 Expand it: +> +> - **`rt.jar`**: rt stands for "RunTime", `rt.jar` is the Java basic class library, containing class files of all classes seen in Java doc. In other words, our commonly used built-in libraries `java.xxx.*` are all in it, such as `java.util.*`, `java.io.*`, `java.nio.*`, `java.lang.*`, `java.sql.*`, `java.math.*`. +> - Java 9 introduced the module system and slightly changed the class loader mentioned above. The extension class loader was renamed the platform class loader. Except for a few key modules in Java SE, such as `java.base`, which are loaded by the startup class loader, other modules are loaded by the platform class loader. + +In addition to these three class loaders, users can also add custom class loaders for expansion to meet their own special needs. For example, we can encrypt the bytecode of a Java class (`.class` file) and then use a custom class loader to decrypt it when loading. + +![Class loader hierarchy diagram](https://oss.javaguide.cn/github/javaguide/java/jvm/class-loader-parents-delegation-model.png) + +Except for `BootstrapClassLoader` which is part of the JVM itself, all other class loaders are implemented outside the JVM and all inherit from the `ClassLoader` abstract class. The advantage of this is that users can customize the class loader so that the application can decide how to obtain the required classes.每个 `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 应用之间的类隔。 + +单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。 + +For example, in SPI, the SPI interface (such as `java.sql.Driver`) is provided by the Java core library and loaded by `BootstrapClassLoader`. SPI implementations (such as `com.mysql.cj.jdbc.Driver`) are provided by third-party vendors, and they are loaded by application class loaders or custom class loaders. By default, a class and its dependent classes are loaded by the same class loader. Therefore, the class loader that loads the SPI interface (`BootstrapClassLoader`) will also be used to load the SPI implementation. According to the parent delegation model, `BootstrapClassLoader` cannot find the SPI implementation class because it cannot delegate to the subclass loader to try to load it. + +It should be noted here: Modularity was introduced after JDK 9+, and the JDBC API was split into the `java.sql` module. It is no longer loaded directly by `BootstrapClassLoader`, but loaded by `PlatformClassLoader`. + +```java +public class ClassLoaderTest { + public static void main(String[] args) throws ClassNotFoundException { + Class clazz = Class.forName("java.sql.Driver"); + ClassLoader loader = clazz.getClassLoader(); + System.out.println("Loader for java.sql.Driver: " + loader); + + // Loader for java.sql.Driver: null in .jdks/corretto-1.8.0_442/bin/java environment + + // In the .jdks/jbr-17.0.12/bin/java environment, it is Loader for java.sql.Driver: jdk.internal.loader.ClassLoaders$PlatformClassLoader@30f39991 + } +} +``` + +For another example, assume that there is a Spring jar package in our project. Since it is shared between web applications, it will be loaded by `SharedClassLoader` (the web server is Tomcat). Some of Spring's business classes are used in our project, such as implementing the interfaces provided by Spring and using the annotations provided by Spring. Therefore, the class loader that loads Spring (that is, `SharedClassLoader`) will also be used to load these business classes. However, the business classes are in the Web application directory and not in the loading path of `SharedClassLoader`, so `SharedClassLoader` cannot find the business classes and cannot load them. + +How to solve this problem? At this time, you need to use **Thread Context Class Loader (`ThreadContextClassLoader`)**. + +Take Spring as an example. When Spring needs to load a business class, it does not use its own class loader, but the context class loader of the current thread. Remember what I said above? Each web application will create a separate `WebAppClassLoader` and set the thread context class loader to `WebAppClassLoader` in the thread that starts the web application. This allows the high-level class loader (`SharedClassLoader`) to use the subclass loader (`WebAppClassLoader`) to load business classes, destroying Java's class loading delegation mechanism and allowing applications to use the class loader in reverse. + +The principle of the thread context class loader is to save a class loader in thread private data, bind it to the thread, and then take it out and use it when needed. This class loader is usually set by the application or container (such as Tomcat). + +`getContextClassLoader()` and `setContextClassLoader(ClassLoader cl)` in `Java.lang.Thread` are used to obtain and set the context class loader of the thread respectively. If not set via `setContextClassLoader(ClassLoader cl)`, the thread will inherit the context class loader of its parent thread. + +Spring's code to obtain the thread context class loader is as follows: + +```java +cl = Thread.currentThread().getContextClassLoader(); +``` + +Interested friends can delve into the principles of Tomcat breaking the parental delegation model. Recommended information: ["In-depth Disassembly of Tomcat & Jetty"](http://gk.link/a/10Egr). + +## Recommended reading + +- "In-depth Disassembly of Java Virtual Machine" +- In-depth analysis of Java ClassLoader principles: +- Java class loader (ClassLoader): +- Class Loaders in Java: +- Class ClassLoader - Oracle official documentation: +- The old and difficult Java ClassLoader will be old if you don’t understand it anymore: + + \ No newline at end of file diff --git a/docs_en/java/jvm/jdk-monitoring-and-troubleshooting-tools.en.md b/docs_en/java/jvm/jdk-monitoring-and-troubleshooting-tools.en.md new file mode 100644 index 00000000000..d3b6f1febf8 --- /dev/null +++ b/docs_en/java/jvm/jdk-monitoring-and-troubleshooting-tools.en.md @@ -0,0 +1,330 @@ +--- +title: Summary of JDK monitoring and troubleshooting tools +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: JDK tools, jps, jstat, jmap, jstack, jvisualvm, diagnosis, monitoring + - - meta + - name: description + content: Summarizes common JDK monitoring and debugging tools and usage examples to assist in locating and analyzing JVM problems. +--- + +## JDK command line tools + +These commands are in the bin directory of the JDK installation directory: + +- **`jps`** (JVM Process Status): Similar to the UNIX `ps` command. Used to view information such as startup classes, incoming parameters, and Java virtual machine parameters of all Java processes; +- **`jstat`** (JVM Statistics Monitoring Tool): used to collect various aspects of running data of the HotSpot virtual machine; +- **`jinfo`** (Configuration Info for Java): Configuration Info for Java, displays virtual machine configuration information; +- **`jmap`** (Memory Map for Java): generate a heap dump snapshot; +- **`jhat`** (JVM Heap Dump Browser): Used to analyze heapdump files. It will establish an HTTP/HTML server so that users can view the analysis results on the browser. JDK9 removed jhat; +- **`jstack`** (Stack Trace for Java): Generate a thread snapshot of the virtual machine at the current moment. The thread snapshot is the collection of method stacks being executed by each thread in the current virtual machine. + +### `jps`: View all Java processes + +The `jps` (JVM Process Status) command is similar to the UNIX `ps` command. + +`jps`: Displays the virtual machine execution main class name and the local virtual machine unique ID (Local Virtual Machine Identifier, LVMID) of these processes. `jps -q`: Only output the unique ID of the local virtual machine of the process. + +```powershell +C:\Users\SnailClimb>jps +7360 NettyClient2 +17396 +7972 Launcher +16504Jps +17340 NettyServer +``` + +`jps -l`: Output the full name of the main class. If the process executes a Jar package, output the Jar path. + +```powershell +C:\Users\SnailClimb>jps -l +7360 firstNettyDemo.NettyClient2 +17396 +7972 org.jetbrains.jps.cmdline.Launcher +16492 sun.tools.jps.Jps +17340 firstNettyDemo.NettyServer +``` + +`jps -v`: Output the JVM parameters when the virtual machine process starts. + +`jps -m`: Output the parameters passed to the main() function of the Java process. + +### `jstat`: Monitor various running status information of virtual machines + +jstat (JVM Statistics Monitoring Tool) is a command line tool used to monitor various running status information of virtual machines. It can display class information, memory, garbage collection, JIT compilation and other running data in the virtual machine process locally or remotely (requires RMI support from the remote host). On servers that do not have a GUI and only provide a plain text console environment, it will be the first choice tool to locate virtual machine performance problems during operation. + +**`jstat` command usage format:** + +```powershell +jstat -