diff --git a/README.md b/README.md index 03271e4..8a31679 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,49 @@ -> ## 目录结构 - - +> # 目录结构 | 🏖 | 🌁| 📮 | 🌈 | 🚀 |⚔️ | 🖥 | 🚏 | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | | [Java](#Java) | [数据库](#数据库)|[计算机网络](#计算机网络) | [算法](#算法) |[操作系统](#操作系统)|[设计模式](#设计模式)| [系统设计](#系统设计)|[框架原理](#框架原理)| +> # 框架原理 +| 🧐 | 🥇 | 🏄 | 🌈 | ⛱ |️ 🚀 | 🎯 | 🍻 | 🦄| 🏖 | +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------: | :-------: | +| [Spring](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Spring.md) | [MyBatis](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/MyBatis.md) |[Solr](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Solr.md) | [Dubbo](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Dubbo.md) | [Netty](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/netty.md)|[Kafka](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Kafka.md)|[Zookeeper](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Zookeeper.md)|[Nginx](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Nginx.md)|[Tomcat](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Tomcat.md)| [Redis](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/database/Redis.md)| + -### Java +## Java - [JAVA基础](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/java/Java%E5%9F%BA%E7%A1%80.md) - [JAVA虚拟机](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/java/Java%E8%99%9A%E6%8B%9F%E6%9C%BA.md) -- [JAVA并发](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/java/Java%E5%B9%B6%E5%8F%91.md) +- [JAVA并发编程](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/java/Java%E5%B9%B6%E5%8F%91.md) - [JAVA容器类](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/java/Java%E5%AE%B9%E5%99%A8.md) -- [锁汇总](https://github.com/zaiyunduan123/Java_ecosystem/blob/master/doc/lock.md) +- [Java锁汇总](https://github.com/zaiyunduan123/Java_ecosystem/blob/master/doc/lock.md) -### 数据库 +## 数据库 - [MySQL](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/database/MySQL.md) -- [Redis](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/database/Redis.md) - - [MySQL数据库开发规范](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/database/DataBaseDesign.md) -### 计算机网络 +## 计算机网络 - [计算机网络](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/network/Computer-Network.md) -### 算法 +## 算法 - [数据结构与算法](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/algorithms/DataStructures-Algorithms.md) - [LeetCode解题总结](https://github.com/zaiyunduan123/leetcode-java) - [海量数据处理总结](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/algorithms/Big-Data.md) -### 操作系统 +## 操作系统 - [操作系统](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/os/Operating-System.md) - [Linux](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/os/Linux.md) -### 设计模式 +## 设计模式 - [设计模式](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/design/Design-Patterns.md) -### 系统设计 -- [系统设计](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/scene/Scene-Design.md) - -### 框架原理 -- [Spring实现原理](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Spring.md) - -- [MyBatis实现原理](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/MyBatis.md) - -- [Solr实现原理](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Solr.md) - -- [Dubbo实现原理](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/framework/Dubbo.md) - - +## 系统设计 +- [系统与架构设计](https://github.com/zaiyunduan123/Java-Interview/blob/master/notes/scene/Scene-Design.md) diff --git a/image/Java-17.png b/image/Java-17.png new file mode 100644 index 0000000..f1a898a Binary files /dev/null and b/image/Java-17.png differ diff --git a/image/Java-18.png b/image/Java-18.png new file mode 100644 index 0000000..a13391a Binary files /dev/null and b/image/Java-18.png differ diff --git a/image/Java-19.png b/image/Java-19.png new file mode 100644 index 0000000..4740bff Binary files /dev/null and b/image/Java-19.png differ diff --git a/image/Java-20.png b/image/Java-20.png new file mode 100644 index 0000000..c86fd26 Binary files /dev/null and b/image/Java-20.png differ diff --git a/image/Java-21.png b/image/Java-21.png new file mode 100644 index 0000000..3d2bcae Binary files /dev/null and b/image/Java-21.png differ diff --git a/image/Java-22.png b/image/Java-22.png new file mode 100644 index 0000000..4b24137 Binary files /dev/null and b/image/Java-22.png differ diff --git a/image/Java-23.png b/image/Java-23.png new file mode 100644 index 0000000..be23470 Binary files /dev/null and b/image/Java-23.png differ diff --git a/image/Java-24.png b/image/Java-24.png new file mode 100644 index 0000000..7d24d8a Binary files /dev/null and b/image/Java-24.png differ diff --git a/image/Java-25.png b/image/Java-25.png new file mode 100644 index 0000000..5acf019 Binary files /dev/null and b/image/Java-25.png differ diff --git a/image/Kafka-1.png b/image/Kafka-1.png new file mode 100644 index 0000000..097dce0 Binary files /dev/null and b/image/Kafka-1.png differ diff --git a/image/Kafka-2.png b/image/Kafka-2.png new file mode 100644 index 0000000..0353802 Binary files /dev/null and b/image/Kafka-2.png differ diff --git a/image/Kafka-3.jpeg b/image/Kafka-3.jpeg new file mode 100644 index 0000000..579c65c Binary files /dev/null and b/image/Kafka-3.jpeg differ diff --git a/image/Kafka-4.png b/image/Kafka-4.png new file mode 100644 index 0000000..5e0f501 Binary files /dev/null and b/image/Kafka-4.png differ diff --git a/image/Kafka-5.png b/image/Kafka-5.png new file mode 100644 index 0000000..d4f1703 Binary files /dev/null and b/image/Kafka-5.png differ diff --git a/image/Kafka-6.png b/image/Kafka-6.png new file mode 100644 index 0000000..8a38c21 Binary files /dev/null and b/image/Kafka-6.png differ diff --git a/image/Kafka-7.png b/image/Kafka-7.png new file mode 100644 index 0000000..0b95633 Binary files /dev/null and b/image/Kafka-7.png differ diff --git a/image/Redis-5.png b/image/Redis-5.png new file mode 100644 index 0000000..f789c21 Binary files /dev/null and b/image/Redis-5.png differ diff --git a/image/Zookeeper-1.png b/image/Zookeeper-1.png new file mode 100644 index 0000000..82148cc Binary files /dev/null and b/image/Zookeeper-1.png differ diff --git a/image/Zookeeper-2.png b/image/Zookeeper-2.png new file mode 100644 index 0000000..258ccdb Binary files /dev/null and b/image/Zookeeper-2.png differ diff --git a/image/Zookeeper-3.png b/image/Zookeeper-3.png new file mode 100644 index 0000000..a6309f5 Binary files /dev/null and b/image/Zookeeper-3.png differ diff --git a/image/frame-4.png b/image/frame-4.png new file mode 100644 index 0000000..bb662e3 Binary files /dev/null and b/image/frame-4.png differ diff --git a/image/netty-1.png b/image/netty-1.png new file mode 100644 index 0000000..c9249ad Binary files /dev/null and b/image/netty-1.png differ diff --git a/image/netty-10.png b/image/netty-10.png new file mode 100644 index 0000000..6f8cfbe Binary files /dev/null and b/image/netty-10.png differ diff --git a/image/netty-11.png b/image/netty-11.png new file mode 100644 index 0000000..ca8cc0a Binary files /dev/null and b/image/netty-11.png differ diff --git a/image/netty-2.png b/image/netty-2.png new file mode 100644 index 0000000..145a947 Binary files /dev/null and b/image/netty-2.png differ diff --git a/image/netty-3.png b/image/netty-3.png new file mode 100644 index 0000000..fd1981f Binary files /dev/null and b/image/netty-3.png differ diff --git a/image/netty-4.png b/image/netty-4.png new file mode 100644 index 0000000..adccf6a Binary files /dev/null and b/image/netty-4.png differ diff --git a/image/netty-5.png b/image/netty-5.png new file mode 100644 index 0000000..53656c2 Binary files /dev/null and b/image/netty-5.png differ diff --git a/image/netty-6.png b/image/netty-6.png new file mode 100644 index 0000000..a796542 Binary files /dev/null and b/image/netty-6.png differ diff --git a/image/netty-7.png b/image/netty-7.png new file mode 100644 index 0000000..4e0992a Binary files /dev/null and b/image/netty-7.png differ diff --git a/image/netty-8.png b/image/netty-8.png new file mode 100644 index 0000000..ad121d7 Binary files /dev/null and b/image/netty-8.png differ diff --git a/image/netty-9.png b/image/netty-9.png new file mode 100644 index 0000000..c737801 Binary files /dev/null and b/image/netty-9.png differ diff --git a/image/os-3.png b/image/os-3.png new file mode 100644 index 0000000..9b8bdd9 Binary files /dev/null and b/image/os-3.png differ diff --git a/image/scene-5.png b/image/scene-5.png new file mode 100644 index 0000000..f41d5f2 Binary files /dev/null and b/image/scene-5.png differ diff --git a/image/spring-5.png b/image/spring-5.png new file mode 100644 index 0000000..7e66ec7 Binary files /dev/null and b/image/spring-5.png differ diff --git a/notes/database/MySQL.md b/notes/database/MySQL.md index 33f7f97..ea9b35c 100644 --- a/notes/database/MySQL.md +++ b/notes/database/MySQL.md @@ -2,18 +2,32 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [数据库的定义](#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%AE%9A%E4%B9%89) -- [MySQL 的架构](#mysql-%E7%9A%84%E6%9E%B6%E6%9E%84) +- [数据库基础知识](#%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86) + - [数据库的定义](#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%AE%9A%E4%B9%89) + - [什么是SQL?](#%E4%BB%80%E4%B9%88%E6%98%AFsql) + - [什么是MySQL?](#%E4%BB%80%E4%B9%88%E6%98%AFmysql) + - [MySQL的架构](#mysql%E7%9A%84%E6%9E%B6%E6%9E%84) + - [MySQL的binlog有有几种录入格式?分别有什么区别?](#mysql%E7%9A%84binlog%E6%9C%89%E6%9C%89%E5%87%A0%E7%A7%8D%E5%BD%95%E5%85%A5%E6%A0%BC%E5%BC%8F%E5%88%86%E5%88%AB%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [MySQL常用函数](#mysql%E5%B8%B8%E7%94%A8%E5%87%BD%E6%95%B0) + - [聚合函数](#%E8%81%9A%E5%90%88%E5%87%BD%E6%95%B0) + - [数学函数](#%E6%95%B0%E5%AD%A6%E5%87%BD%E6%95%B0) + - [字符串函数](#%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%87%BD%E6%95%B0) + - [日期和时间函数](#%E6%97%A5%E6%9C%9F%E5%92%8C%E6%97%B6%E9%97%B4%E5%87%BD%E6%95%B0) + - [条件判断函数](#%E6%9D%A1%E4%BB%B6%E5%88%A4%E6%96%AD%E5%87%BD%E6%95%B0) + - [系统信息函数](#%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF%E5%87%BD%E6%95%B0) + - [加密函数](#%E5%8A%A0%E5%AF%86%E5%87%BD%E6%95%B0) - [索引](#%E7%B4%A2%E5%BC%95) - [B+树索引和哈希索引的区别](#b%E6%A0%91%E7%B4%A2%E5%BC%95%E5%92%8C%E5%93%88%E5%B8%8C%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8C%BA%E5%88%AB) - [B树和B+树的区别](#b%E6%A0%91%E5%92%8Cb%E6%A0%91%E7%9A%84%E5%8C%BA%E5%88%AB) - [聚集索引和辅助索引](#%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95) - [聚集索引](#%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95) - [辅助索引](#%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95) - - [mysql联合索引](#mysql%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95) + - [联合索引](#%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95) - [什么情况下应不建或少建索引](#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E5%BA%94%E4%B8%8D%E5%BB%BA%E6%88%96%E5%B0%91%E5%BB%BA%E7%B4%A2%E5%BC%95) - - [导致索引失效的一些情况](#%E5%AF%BC%E8%87%B4%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E7%9A%84%E4%B8%80%E4%BA%9B%E6%83%85%E5%86%B5) + - [哪些情况会导致索引失效?](#%E5%93%AA%E4%BA%9B%E6%83%85%E5%86%B5%E4%BC%9A%E5%AF%BC%E8%87%B4%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88) - [key和index的区别](#key%E5%92%8Cindex%E7%9A%84%E5%8C%BA%E5%88%AB) + - [如果索引值为null,走不走索引?](#%E5%A6%82%E6%9E%9C%E7%B4%A2%E5%BC%95%E5%80%BC%E4%B8%BAnull%E8%B5%B0%E4%B8%8D%E8%B5%B0%E7%B4%A2%E5%BC%95) + - [判断列是否要添加索引的标准](#%E5%88%A4%E6%96%AD%E5%88%97%E6%98%AF%E5%90%A6%E8%A6%81%E6%B7%BB%E5%8A%A0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%A0%87%E5%87%86) - [锁](#%E9%94%81) - [行锁](#%E8%A1%8C%E9%94%81) - [优点](#%E4%BC%98%E7%82%B9) @@ -23,6 +37,10 @@ - [Gap Lock](#gap-lock) - [Next-Key Lock](#next-key-lock) - [锁选择](#%E9%94%81%E9%80%89%E6%8B%A9) + - [悲观锁和乐观锁](#%E6%82%B2%E8%A7%82%E9%94%81%E5%92%8C%E4%B9%90%E8%A7%82%E9%94%81) + - [悲观锁和乐观锁使用在哪些场景?](#%E6%82%B2%E8%A7%82%E9%94%81%E5%92%8C%E4%B9%90%E8%A7%82%E9%94%81%E4%BD%BF%E7%94%A8%E5%9C%A8%E5%93%AA%E4%BA%9B%E5%9C%BA%E6%99%AF) + - [乐观锁重试](#%E4%B9%90%E8%A7%82%E9%94%81%E9%87%8D%E8%AF%95) + - [数据库死锁的产生原因及解决办法](#%E6%95%B0%E6%8D%AE%E5%BA%93%E6%AD%BB%E9%94%81%E7%9A%84%E4%BA%A7%E7%94%9F%E5%8E%9F%E5%9B%A0%E5%8F%8A%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95) - [MySQL分区](#mysql%E5%88%86%E5%8C%BA) - [什么是分区?](#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%8C%BA) - [分区与分表的区别](#%E5%88%86%E5%8C%BA%E4%B8%8E%E5%88%86%E8%A1%A8%E7%9A%84%E5%8C%BA%E5%88%AB) @@ -35,19 +53,23 @@ - [binlog(归档日志)](#binlog%E5%BD%92%E6%A1%A3%E6%97%A5%E5%BF%97) - [两种日志区别](#%E4%B8%A4%E7%A7%8D%E6%97%A5%E5%BF%97%E5%8C%BA%E5%88%AB) - [四种隔离级别](#%E5%9B%9B%E7%A7%8D%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB) + - [MySQL的事务隔离级别的底层实现原理](#mysql%E7%9A%84%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) - [对于脏读,不可重复读,幻读的理解](#%E5%AF%B9%E4%BA%8E%E8%84%8F%E8%AF%BB%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E5%B9%BB%E8%AF%BB%E7%9A%84%E7%90%86%E8%A7%A3) - [脏读](#%E8%84%8F%E8%AF%BB) - [不可重复读](#%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB) - [幻读](#%E5%B9%BB%E8%AF%BB) - [不可重复读和幻读区别](#%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E5%8C%BA%E5%88%AB) + - [InnoDB的RR级别如何避免幻读?](#innodb%E7%9A%84rr%E7%BA%A7%E5%88%AB%E5%A6%82%E4%BD%95%E9%81%BF%E5%85%8D%E5%B9%BB%E8%AF%BB) - [关于MVVC](#%E5%85%B3%E4%BA%8Emvvc) - [存储过程](#%E5%AD%98%E5%82%A8%E8%BF%87%E7%A8%8B) - [Mysql存储引擎](#mysql%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E) - [MyISAM和InnoDB的区别](#myisam%E5%92%8Cinnodb%E7%9A%84%E5%8C%BA%E5%88%AB) - [如何选择](#%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9) - [mysql主从同步](#mysql%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) + - [MySQL主从复制的两种方式](#mysql%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E5%BC%8F) - [使用主从同步的好处](#%E4%BD%BF%E7%94%A8%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E7%9A%84%E5%A5%BD%E5%A4%84) - [主从复制原理](#%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86) + - [主从复制延迟及解决](#%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%BB%B6%E8%BF%9F%E5%8F%8A%E8%A7%A3%E5%86%B3) - [MySQL事务原理](#mysql%E4%BA%8B%E5%8A%A1%E5%8E%9F%E7%90%86) - [undo 日志文件](#undo-%E6%97%A5%E5%BF%97%E6%96%87%E4%BB%B6) - [redo/undo日志文件](#redoundo%E6%97%A5%E5%BF%97%E6%96%87%E4%BB%B6) @@ -55,11 +77,20 @@ - [Join的实现原理](#join%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) - [优化](#%E4%BC%98%E5%8C%96) - [MySQL优化](#mysql%E4%BC%98%E5%8C%96) + - [一条sql执行过长的时间,你如何优化,从哪些方面?](#%E4%B8%80%E6%9D%A1sql%E6%89%A7%E8%A1%8C%E8%BF%87%E9%95%BF%E7%9A%84%E6%97%B6%E9%97%B4%E4%BD%A0%E5%A6%82%E4%BD%95%E4%BC%98%E5%8C%96%E4%BB%8E%E5%93%AA%E4%BA%9B%E6%96%B9%E9%9D%A2) - [MySQL中binary log和redo log顺序一致性问题](#mysql%E4%B8%ADbinary-log%E5%92%8Credo-log%E9%A1%BA%E5%BA%8F%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98) +- [一个表有3000万记录,假如有一列占8位字节的字段,根据这一列建索引的话索引树的高度是多少?](#%E4%B8%80%E4%B8%AA%E8%A1%A8%E6%9C%893000%E4%B8%87%E8%AE%B0%E5%BD%95%E5%81%87%E5%A6%82%E6%9C%89%E4%B8%80%E5%88%97%E5%8D%A08%E4%BD%8D%E5%AD%97%E8%8A%82%E7%9A%84%E5%AD%97%E6%AE%B5%E6%A0%B9%E6%8D%AE%E8%BF%99%E4%B8%80%E5%88%97%E5%BB%BA%E7%B4%A2%E5%BC%95%E7%9A%84%E8%AF%9D%E7%B4%A2%E5%BC%95%E6%A0%91%E7%9A%84%E9%AB%98%E5%BA%A6%E6%98%AF%E5%A4%9A%E5%B0%91) +- [b+树一般多高,能存储多少数据?](#b%E6%A0%91%E4%B8%80%E8%88%AC%E5%A4%9A%E9%AB%98%E8%83%BD%E5%AD%98%E5%82%A8%E5%A4%9A%E5%B0%91%E6%95%B0%E6%8D%AE) +- [为什么要给表加上主键?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%BB%99%E8%A1%A8%E5%8A%A0%E4%B8%8A%E4%B8%BB%E9%94%AE) +- [为何要使用自增int作为主键?](#%E4%B8%BA%E4%BD%95%E8%A6%81%E4%BD%BF%E7%94%A8%E8%87%AA%E5%A2%9Eint%E4%BD%9C%E4%B8%BA%E4%B8%BB%E9%94%AE) +- [MySQL中sql注入是什么,如何避免?](#mysql%E4%B8%ADsql%E6%B3%A8%E5%85%A5%E6%98%AF%E4%BB%80%E4%B9%88%E5%A6%82%E4%BD%95%E9%81%BF%E5%85%8D) +- [SQL中where、having、group by、order by执行和书写顺序?](#sql%E4%B8%ADwherehavinggroup-byorder-by%E6%89%A7%E8%A1%8C%E5%92%8C%E4%B9%A6%E5%86%99%E9%A1%BA%E5%BA%8F) +- [水平分库后查询如何排序](#%E6%B0%B4%E5%B9%B3%E5%88%86%E5%BA%93%E5%90%8E%E6%9F%A5%E8%AF%A2%E5%A6%82%E4%BD%95%E6%8E%92%E5%BA%8F) -# 数据库的定义 +# 数据库基础知识 +## 数据库的定义 数据库:物理操作文件系统或其他形式文件类型的集合; @@ -68,14 +99,72 @@ 在 MySQL 中,实例和数据库往往都是一一对应的,而我们也无法直接操作数据库,而是要通过数据库实例来操作数据库文件,可以理解为数据库实例是数据库为上层提供的一个专门用于操作的接口。 在 Unix 上,启动一个 MySQL 实例往往会产生两个进程,mysqld 就是真正的数据库服务守护进程,而 mysqld_safe 是一个用于检查和设置 mysqld 启动的控制程序,它负责监控 MySQL 进程的执行,当 mysqld 发生错误时,mysqld_safe 会对其状态进行检查并在合适的条件下重启。 +## 什么是SQL? +结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。 -# MySQL 的架构 +作用:用于存取数据、查询、更新和管理关系数据库系统。 +## 什么是MySQL? +MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。 + +## MySQL的架构 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/MySQL-8.png) ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/MySQL-9.png) +## MySQL的binlog有有几种录入格式?分别有什么区别? +有三种格式,statement,row和mixed。 +1. statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql +的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。 +2. row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。 +3. mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。 +此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。 + + +# MySQL常用函数 +## 聚合函数 +- count():求数据表的行数 +- max():求某列的最大数值 +- min():求某列的最小值 +- sum():对数据表的某列进行求和操作 +- avg():对数据表的某列进行求平均值操作 + +## 数学函数 +- abs(X):返回X的绝对值 +- mod(N,M)或%:返回N被M除的余数 +- floor(X):返回不大于X的最大整数值 +- round(X) :返回参数X的四舍五入的一个整数 + +## 字符串函数 +- concat(str1,str2,...):返回来自于参数连结的字符串 +- length(str):返回字符串str的长度 +- substring(str,pos):从字符串str的起始位置pos返回一个子串 +- replace(str,from_str,to_str):返回字符串str,其字符串from_str的所有出现由字符串to_str代替 + +## 日期和时间函数 +- dayofweek(date):返回日期date的星期索引(1=星期天,2=星期一, …7=星期六) +- dayofmonth(date):返回date的月份中的日期,在1到31范围内 +- month(date):返回date的月份,范围1到12 +- now():以‘YYYY-MM-DD HH:MM:SS’或YYYYMMDDHHMMSS格式返回当前的日期和时间 + +## 条件判断函数 +- CASE value WHEN [compare-value] THEN result [WHEN [compare-value] THEN result ... +如:SELECT CASE WHEN 1>0 THEN 'true' ELSE 'false' END; +- IF(expr1,expr2,expr3):如果 expr1 是TRUE,则 IF()的返回值为expr2; 否则返回值则为 expr3 + +## 系统信息函数 +- version():函数返回数据库的版本号 +- connection_ID():函数返回服务器的连接数 +- database()和schema()返回当前数据库名。 + +## 加密函数 +- password(str):函数可以对字符串str进行加密 +- MD5(str)加密函数 + + + + # 索引 索引是数据库中非常非常重要的概念,它是存储引擎能够快速定位记录的秘密武器,对于提升数据库的性能、减轻数据库服务器的负担有着非常重要的作用;索引优化是对查询性能优化的最有效手段,它能够轻松地将查询的性能提高几个数量级。 @@ -167,7 +256,7 @@ B+的内部结点并没有指向关键字具体信息的指针。因此其内部 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/MySQL-7.png) -## mysql联合索引 +## 联合索引 1. 联合索引是两个或更多个列上的索引。对于联合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a 、 a,b 、 a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效。 2. 利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知 道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。 @@ -177,42 +266,47 @@ B+的内部结点并没有指向关键字具体信息的指针。因此其内部 3. 数据重复且分布平均的表字段,假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。 4. 经常和主字段一块查询但主字段索引值比较多的表字段 -## 导致索引失效的一些情况 -1.隐式转换导致索引失效.这一点应当引起重视.也是开发中经常会犯的错误. - - 由于表的字段tu_mdn定义为varchar2(20),但在查询时把该字段作为number类型以where条件传给Oracle,这样会导致索引失效. - 错误的例子:select * from test where tu_mdn=13333333333; - 正确的例子:select * from test where tu_mdn='13333333333'; - -2. 对索引列进行运算导致索引失效,我所指的对索引列进行运算包括(+,-,*,/,! 等) +## 哪些情况会导致索引失效? +1. 左模糊 +2. 不符合最左原则 +3. or +4. is null +5. !=或<> +6. in 和 not in +7. 表达式操作:where num/2=100 +8. 函数:where substring(name,1,3)='abc' - 错误的例子:select * from test where id-1=9; - 正确的例子:select * from test where id=10; - -3. 以下使用会使索引失效,应避免使用; - -a. 使用 <> 、not in 、not exist、!= -b. like "%_" 百分号在前(可采用在建立索引时用reverse(columnName)这种方法处理) -c. 单独引用复合索引里非第一位置的索引列.应总是使用索引的第一个列,如果索引是建立在多个列上, 只有在它的第一个列被where子句引用时,优化器才会选择使用该索引。 -d. 字符型字段为数字时在where条件里不添加引号. -e. 当变量采用的是times变量,而表的字段采用的是date变量时.或相反情况。 +## key和index的区别 +1. key 是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key, unique key, foreign key 等 +2. index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引等; -4. 不要将空的变量值直接与比较运算符(符号)比较。 -如果变量可能为空,应使用 IS NULL 或 IS NOT NULL 进行比较,或者使用 ISNULL 函数。 +## 如果索引值为null,走不走索引? +答案:走 -5. 不要在 SQL 代码中使用双引号。 +MySQL难以优化引用了可空列的查询,它会使索引、索引统计和值更加复杂。可空列需要更多的储存空间,还需要在MySQL内部进行特殊处理。当可空列被索引的时候, -因为字符常量使用单引号。如果没有必要限定对象名称,可以使用(非 ANSI SQL 标准)括号将名称括起来。 - -6. 将索引所在表空间和数据所在表空间分别设于不同的磁盘chunk上,有助于提高索引查询的效率。 - +每条记录都需要一个额外的字节,还可能导致 MyISAM 中固定大小的索引(例如一个整数列上的索引)变成可变大小的索引。 -## key和index的区别 -1. key 是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key, unique key, foreign key 等 -2. index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引等; +即使要在表中储存「没有值」的字段,还是有可能不使用 NULL 的,考虑使用 0、特殊值或空字符串来代替它。 +把 NULL 列改为 NOT NULL 带来的性能提升很小,所以除非确定它引入了问题,否则就不要把它当作优先的优化措施。 +然后,如果计划对列进行索引,就要尽量避免把它设置为可空,**虽然在mysql里 Null值的列也是走索引的**。对MySQL来说,null是一个特殊的值。 + +##为什么MySQL的索引结构,采用了B+树,没有使用跳跃表呢? +1. 首先,跳跃表不适用于磁盘读取的场景,B+树的页天生就和磁盘块对应 +2. 其二,跳跃表的查找效率不如B+树效率高,也不如B+树稳定。 + + +## 判断列是否要添加索引的标准 +有时候由数据库的查询优化器自动判断是否使用索引; +- 1、较频繁的作为查询条件的字段应该创建索引,且唯一性较好的 +- 2、show index from table_name ## 查看该表的索引信息 + +Cardinality 值非常重要,优化器会根据这个值来判断是否使用这个索引,但是这个值并不是实时更新的,即并非索引的更新都会更新该值,因为代价太大,只是一个大概值 + +Cardinality :非常关键的值,标识索引中唯一值的数目的估计值,Cardinality/表的记录数应尽可能的接近1,如果非常小,那用户需要考虑是否还有必要创建这个索引。故在访问高选择性属性的字段并从表中取出很少一部分数据时,对于字段添加B+树索引是非常有必要的 # 锁 @@ -291,6 +385,46 @@ Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,其设置 简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并没有gap,所以并不会阻塞其他session在gap间隙里插入记录。不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。想象一下,如果一个表有一个索引idx_test,表中有记录1和8,那么每个事务都可以在2和7之间插入任何记录,只会对当前插入的记录加record lock,并不会阻塞其他session插入与自己不同的记录,因为他们并没有任何冲突。 +## 悲观锁和乐观锁 +### 悲观锁和乐观锁使用在哪些场景? +悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,楼主列举的例子在select ... for update前加个事务就可以防止更新丢失。悲观锁和乐观锁大部分场景下差异不大,一些独特场景下有一些差别,一般我们可以从如下几个方面来判断: +1. 响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁 +2. 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大 +3. 重试代价:如果重试代价大,建议采用悲观锁 + +### 乐观锁重试 +并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。 + + +## 数据库死锁的产生原因及解决办法 +**死锁的第一种情况** +一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样 +用户B要等用户A释放表A才能继续,这就死锁就产生了。 + +解决方法: +- 对于数据库的多表操作时,尽量按照相同的顺序进 行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该**按照相同的顺序**来锁定资源。 + +**死锁的第二种情况** +用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,**这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁**,而用户B里的独占锁由于A 有共享锁存在所以必须等A释放掉共享锁,而**A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁**,于是出现了死锁。 + +解决方法: +1. 对于按钮等控件,点击后使其立刻失效,不让用户重复点击,避免对同时对同一条记录操作。 +2. 使用乐观锁进行控制。 +3. 使用悲观锁进行控制。 + +**死锁的第三种情况** +如果在事务中执行了一条不满足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情 况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。 + +解决方法: +- SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。 + + + + + + + + # MySQL分区 ## 什么是分区? 表分区,是指根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分。从逻辑上看,只有一张表,但是底层却是由多个物理分区组成。 @@ -371,6 +505,35 @@ MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的 3. Read committed (读已提交):可避免脏读的发生。 4. Read uncommitted (读未提交):最低级别,任何情况都无法保证。 +## MySQL的事务隔离级别的底层实现原理 +底层实现原理主要在读,写都会加排他锁 +1. 不会加锁 +2. MVCC 每次select +3. MVCC 第一次select +4. 加共享锁,读写互斥 + +**1、 READ UNCOMMITTED(你读了别人正在处理的数据)** + +读不会加任何锁。而写会加排他锁,并到事务结束之后释放。 + +**2、READ COMMITTED(两次读的都是真的(不脏读) 可是却存在不可重复,你读的期间别人插进来对数据操作了)** + +读取数据不加锁而是使用了MVCC机制,写数据时,使用排它锁。 + +该级别下还是遗留了不可重复读和幻读问题: MVCC版本的生成时机: 是**每次select时**。这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读,即:重复读时,会出现数据不一致问题,后面我们会讲解超支现象,就是这种引起的。 + +读时通过mvcc,访问的是创建版本最大&&删除版本为空的记录 + +**3、REPEATABLE READ(事务是多次读取,得到的相同的值)** +READ COMMITTED级别不同的是MVCC版本的生成时机,即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。 + +读时通过mvcc,访问的是创建版本小于等于当前版本&&(删除版本大于当前版本 || 删除版本为空)的记录 + +**4、SERIALISABLE(你读的时候别人看不到,隔离开,单独执行)** +该级别下,会自动将所有普通select转化为select ... lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。 + + + # 对于脏读,不可重复读,幻读的理解 ## 脏读 @@ -389,8 +552,20 @@ MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。 ## 不可重复读和幻读区别 -1. 不可重复读的重点是修改: 同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 -2. 幻读的重点在于新增或者删除 (数据条数变化),同样的条件, 第1次和第2次读出来的记录数不一样 +- 不可重复读:同样的条件,你读取过的数据,再次读取出来发现值不一样了。 +- 幻读:同样的条件,第1次和第2次读出来的记录数不一样 + +不可重复读重点在于只锁住update和delete,而幻读的重点还在于锁住insert。 + +如果使用锁机制来实现这两种隔离级别,**在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读**,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。 + +所以说不可重复读和幻读最大的区别,有没有锁住insert。 + + +## InnoDB的RR级别如何避免幻读? +避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。 + +在RR的隔离级别下,Innodb使用MVVC和next-key locks解决幻读,MVVC解决的是普通读(快照读)的幻读,next-key locks解决的是当前读情况下的幻读。 # 关于MVVC MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,现阶段几乎所有的RDBMS,都支持了MVCC。 @@ -456,6 +631,11 @@ ps:存储过程跟触发器有点类似,都是一组SQL集,但是存储过 主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。 + +## MySQL主从复制的两种方式 +1. 通过binary log配置主从 +2. 通过GTID配置主从 + ## 使用主从同步的好处 1. 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。 @@ -476,6 +656,17 @@ ps:存储过程跟触发器有点类似,都是一组SQL集,但是存储过 从库的io线程会实时依据master.info信息的去主库的binlog日志里面读取更新的内容,将更新的内容取回到自己的中继日志中,同时会更新master.info信息,此时sql线程实时会从中继日志中读取并执行里面的sql语句。 +## 主从复制延迟及解决 +原因:主库并发量大,而从库复制是单线程,从库过多,主从系统配置不当,cpu,内存等,慢sql过大多,大的事物,网络延迟,跨公网的主从复制很容易导致主从复制延迟 + +解决方法 +1. 适当数量的从库,3-5个,从库配置更好的硬件,网络配置等 +2. 将大事物拆分成多个小事物进行提交,表加主键,否在会全表扫描 +3. mysql 5.7.19 + 版本支持并行复制 +4. 如果对实时性要求高的系统,从服务器只当备份使用,数据从缓存返回,降低主服务器压力。 + +主机与从机之间的物理延迟是无法避免的,既然无法避免就可以考虑尝试通过缓存等方式,降低新修改数据被立即读取的概率。 + # MySQL事务原理 ACID是通过redo 和 undo 日志文件实现的,不管是redo还是undo文件都会有一个缓存我们称之为redo_buf和undo_buf。同样,数据库文件也会有缓存称之为data_buf。 @@ -567,6 +758,15 @@ Prepared Statements很像存储过程,是一种运行在后台的SQL语句集 2. adaptive hash index(自适应哈希索引)作用:Innodb 存储引擎会监控对表上索引的查找,如果观察到建立哈希索引可以带来速度上的提升,则建立哈希索引。读写速度上也有所提高。 3. insert buffer (插入缓冲)作用:针对普通索引的插入把随机 IO 变成顺序 IO,并合并插入磁盘 +## 一条sql执行过长的时间,你如何优化,从哪些方面? +1. 查看sql是否涉及多表的联表或者子查询,如果有,看是**否能进行业务拆分**,相关字段冗余或者合并成临时表(业务和算法的优化) +2. 涉及链表的查询,是否能进行**分表查询**,单表查询之后的结果进行字段整合 +3. 如果以上两种都不能操作,非要链表查询,那么考虑对相对应的查询条件做索引。加快查询速度 +4. 针对数量大的表进行**历史表分离**(如交易流水表) +5. 数据库**主从分离**,读写分离,降低读写针对同一表同时的压力,至于主从同步,mysql有自带的binlog实现 主从同步 +6. explain分析sql语句,查看执行计划,**分析索引是否用上,分析扫描行数**等等 +7. 查看**mysql执行日志**,看看是否有其他方面的问题 + # MySQL中binary log和redo log顺序一致性问题 MySQL是多存储引擎的,不管使用那种存储引擎,都会有binary log,而不一定有redo log,简单的说,binlog是MySQL Server层的,redo log是InnoDB层的 @@ -609,3 +809,74 @@ PS:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后, 由上面的二阶段提交流程可以看出,一旦步骤2中的操作完成,就确保了事务的提交,即使在执行步骤3时数据库发送了宕机。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。步骤2的fsync参数由sync_binlog=1控制,步骤3的fsync由参数innodb_flush_log_at_trx_commit=1控制,俗称“双1”,是保证CrashSafe的根本。 +# 一个表有3000万记录,假如有一列占8位字节的字段,根据这一列建索引的话索引树的高度是多少? +表的记录数是N,每一个BTREE节点平均有B个索引KEY,那么B+TREE索引树的高度就是logNB(等价于logN/logB) + +现在我们假设表3000W条记录(因为2^25=33554432),如果每个节点保存64个索引KEY,那么索引的高度就是(log2^25)/log64≈ 25/6 ≈ 4.17 + +节点保存的KEY的数量为pagesize/(keysize+pointsize) + +假设平均指针大小是8个字节,那么索引树的每个节点可以存储16k/((8+8)*8)≈128。那么:一个拥有3000w数据,且主键是BIGINT类型的表的主键索引树的高度就是(log2^25)/log128 ≈ 25/7 ≈ 3.57 + +由上面的计算可知:一个千万量级,且存储引擎是MyISAM或者InnoDB的表,其索引树的高度在3~5之间。 + +# b+树一般多高,能存储多少数据? +**一般3到4层** 能存储到2千万到25亿数据 + +Innodb 的所有数据文件(后缀为 ibd 的文件),他的大小始终都是 16384(16k)的整数倍。 + +我们假设主键 ID 为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节,我们一个页中能存放多少这样的单元,其实就代表有多少指针,即 16384/14=1170 + +那么可以算出一棵高度为 2 的 B+ 树,能存放 1170*16=18720 (两万)条这样的数据记录。 + +根据同样的原理我们可以算出一个高度为 3 的 B+ 树可以存放: 1170*1170*16=21902400 (两千万)条这样的记录。 + +根据同样的原理我们可以算出一个高度为 4 的 B+ 树可以存放: 1170*1170*1170*16=25 625 808 000 (25亿)条这样的记录。 + + + + + + + + +# 为什么要给表加上主键? +1. 一个没加主键的表,它的数据无序的放置在磁盘存储器上,一行一行的排列的很整齐. +2. 一个加了主键的表,并不能被称之为「表」。如果给表上了主键,那么表在磁盘上的存储结构就由整齐排列的结构转变成了树状结构,并且是「平衡树」结构,换句话说,就是整个表就变成了一个索引。没错,再说一遍,**整个表变成了一个索引,也就是所谓的「聚集索引」**。 这就是为什么一个表只能有一个主键,**一个表只能有一个「聚集索引」**,因为主键的作用就是把「表」的数据格式转换成「索引(平衡树)」的格式放置。 + + +# 为何要使用自增int作为主键? +1. **int 相比varchar、char、text使用更少的存储空间**,而且数据类型简单,可以节约CPU的开销,更便于表结构的维护 +2. 默认都会在主键上建立主键索引,**使用整形作为主键可以将更多的索引载入内存,提高查询性能** +3. 对于InnoDB存储引擎而言,**每个二级索引都会使用主键作为索引值的后缀,使用自增主键可以减少索引的长度(大小)**,方便更多的索引数据载入内存 +4. 可以使索引数据更加紧凑,在数据插入、删除、更新时可以做到索引数据尽可能少的移动、分裂页,减少碎片的产生(可以通过optimize table 来重建表),减少维护开销 +5. 在数据插入时,可以保证逻辑相邻的元素物理也相邻,便于范围查找 + +当然,使用自增int作为主键也不是百利无一害,在高并发的情况下也可能会造成锁的争用问题。 + +# MySQL中sql注入是什么,如何避免? +sql注入分为平台层注入和代码层注入,前者由不安全的数据库配置或数据库平台的漏洞所致,后者主要由于程序员对输入未进行细致地过滤。 + +如何避免 +1. PreparedStatement +2. 使用正则表达式过滤传入的参数 +3. JSP中调用该函数检查是否包函非法字符 +4. JSP页面判断代码 +5. 字符串过滤 + +# SQL中where、having、group by、order by执行和书写顺序? +当一个查询语句同时出现了where,group by,having,order by的时候,执行顺序和编写顺序是: +1. 执行where xx对全表数据做筛选,返回第1个结果集。 +2. 针对第1个结果集使用group by分组,返回第2个结果集。 +3. 针对第2个结果集中的每1组数据执行select xx,有几组就执行几次,返回第3个结果集。 +4. 针对第3个结集执行having xx进行筛选,返回第4个结果集。 +5. 针对第4个结果集排序。 + +from --> where --[result 1]--> group by --[result 2]--> select (x N) --[result 3]--> having --[result 4]--> order by --> OUTPUT + +# 水平分库后查询如何排序 +**跨分片的排序分页** +一般来讲,分页时需要按照指定字段进行排序。 +- 当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片, +- 而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。 + diff --git a/notes/database/Redis.md b/notes/database/Redis.md index dc4a1d9..d3da173 100644 --- a/notes/database/Redis.md +++ b/notes/database/Redis.md @@ -11,12 +11,14 @@ - [压缩列表](#%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8) - [快速列表](#%E5%BF%AB%E9%80%9F%E5%88%97%E8%A1%A8) - [跳跃列表](#%E8%B7%B3%E8%B7%83%E5%88%97%E8%A1%A8) + - [为什么Redis选择使用跳表而不是红黑树来实现有序集合?](#%E4%B8%BA%E4%BB%80%E4%B9%88redis%E9%80%89%E6%8B%A9%E4%BD%BF%E7%94%A8%E8%B7%B3%E8%A1%A8%E8%80%8C%E4%B8%8D%E6%98%AF%E7%BA%A2%E9%BB%91%E6%A0%91%E6%9D%A5%E5%AE%9E%E7%8E%B0%E6%9C%89%E5%BA%8F%E9%9B%86%E5%90%88) - [Redis应用](#redis%E5%BA%94%E7%94%A8) - [分布式锁](#%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81) - [延时队列](#%E5%BB%B6%E6%97%B6%E9%98%9F%E5%88%97) - [位图](#%E4%BD%8D%E5%9B%BE) - [HyperLogLog](#hyperloglog) - [布隆过滤器](#%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8) +- [Gossip协议](#gossip%E5%8D%8F%E8%AE%AE) - [Redis单进程单线程方式](#redis%E5%8D%95%E8%BF%9B%E7%A8%8B%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%96%B9%E5%BC%8F) - [单进程单线程好处](#%E5%8D%95%E8%BF%9B%E7%A8%8B%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%A5%BD%E5%A4%84) - [单进程单线程弊端](#%E5%8D%95%E8%BF%9B%E7%A8%8B%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%BC%8A%E7%AB%AF) @@ -24,23 +26,50 @@ - [多路I/O复用模型](#%E5%A4%9A%E8%B7%AFio%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B) - [Redis快的主要原因](#redis%E5%BF%AB%E7%9A%84%E4%B8%BB%E8%A6%81%E5%8E%9F%E5%9B%A0) - [Redis主从复制](#redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6) -- [Redis两种持久化方式优缺点](#redis%E4%B8%A4%E7%A7%8D%E6%8C%81%E4%B9%85%E5%8C%96%E6%96%B9%E5%BC%8F%E4%BC%98%E7%BC%BA%E7%82%B9) +- [Redis持久化](#redis%E6%8C%81%E4%B9%85%E5%8C%96) + - [Redis RDB和AOF的优缺点对比以及如何选择](#redis-rdb%E5%92%8Caof%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9%E5%AF%B9%E6%AF%94%E4%BB%A5%E5%8F%8A%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9) + - [RDB和AOF到底该如何选择?](#rdb%E5%92%8Caof%E5%88%B0%E5%BA%95%E8%AF%A5%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9) + - [为什么恢复的时候RDB比AOF快?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%81%A2%E5%A4%8D%E7%9A%84%E6%97%B6%E5%80%99rdb%E6%AF%94aof%E5%BF%AB) - [Redis常见的性能问题都有哪些?如何解决?](#redis%E5%B8%B8%E8%A7%81%E7%9A%84%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3) -- [Redis提供6种数据淘汰策略](#redis%E6%8F%90%E4%BE%9B6%E7%A7%8D%E6%95%B0%E6%8D%AE%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5) -- [Redis设置过期时间的原理](#redis%E8%AE%BE%E7%BD%AE%E8%BF%87%E6%9C%9F%E6%97%B6%E9%97%B4%E7%9A%84%E5%8E%9F%E7%90%86) - - [定时扫描策略](#%E5%AE%9A%E6%97%B6%E6%89%AB%E6%8F%8F%E7%AD%96%E7%95%A5) +- [Redis六种淘汰key策略(默认时no-eviction)](#redis%E5%85%AD%E7%A7%8D%E6%B7%98%E6%B1%B0key%E7%AD%96%E7%95%A5%E9%BB%98%E8%AE%A4%E6%97%B6no-eviction) +- [Redis三种删除过期键策略](#redis%E4%B8%89%E7%A7%8D%E5%88%A0%E9%99%A4%E8%BF%87%E6%9C%9F%E9%94%AE%E7%AD%96%E7%95%A5) + - [定时删除(并没有用到)](#%E5%AE%9A%E6%97%B6%E5%88%A0%E9%99%A4%E5%B9%B6%E6%B2%A1%E6%9C%89%E7%94%A8%E5%88%B0) + - [惰性删除](#%E6%83%B0%E6%80%A7%E5%88%A0%E9%99%A4) + - [定期删除](#%E5%AE%9A%E6%9C%9F%E5%88%A0%E9%99%A4) +- [Redis的内存优化](#redis%E7%9A%84%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96) +- [Redis4.0新特性](#redis40%E6%96%B0%E7%89%B9%E6%80%A7) + - [一、主从数据同步机制](#%E4%B8%80%E4%B8%BB%E4%BB%8E%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6) + - [二、命令优化](#%E4%BA%8C%E5%91%BD%E4%BB%A4%E4%BC%98%E5%8C%96) + - [三、慢日志记录客户端来源IP地址](#%E4%B8%89%E6%85%A2%E6%97%A5%E5%BF%97%E8%AE%B0%E5%BD%95%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%9D%A5%E6%BA%90ip%E5%9C%B0%E5%9D%80) + - [四、混合RDB + AOF格式](#%E5%9B%9B%E6%B7%B7%E5%90%88rdb--aof%E6%A0%BC%E5%BC%8F) + - [五、内存使用和性能改进](#%E4%BA%94%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E5%92%8C%E6%80%A7%E8%83%BD%E6%94%B9%E8%BF%9B) - [缓存解决方案分析](#%E7%BC%93%E5%AD%98%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E5%88%86%E6%9E%90) - [缓存雪崩](#%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9) - [缓存穿透](#%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F) - [缓存击穿](#%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF) - [缓存预热](#%E7%BC%93%E5%AD%98%E9%A2%84%E7%83%AD) - [缓存更新](#%E7%BC%93%E5%AD%98%E6%9B%B4%E6%96%B0) +- [Redis Sentinel 与 Redis Cluster区别和各自适用场景](#redis-sentinel-%E4%B8%8E-redis-cluster%E5%8C%BA%E5%88%AB%E5%92%8C%E5%90%84%E8%87%AA%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF) + - [Redis Sentinel](#redis-sentinel) + - [Redis Cluster](#redis-cluster) +- [为什么lua脚本结合Redis命令可以实现原子性?](#%E4%B8%BA%E4%BB%80%E4%B9%88lua%E8%84%9A%E6%9C%AC%E7%BB%93%E5%90%88redis%E5%91%BD%E4%BB%A4%E5%8F%AF%E4%BB%A5%E5%AE%9E%E7%8E%B0%E5%8E%9F%E5%AD%90%E6%80%A7) +- [Redis分布式锁会导致什么问题?](#redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E4%BC%9A%E5%AF%BC%E8%87%B4%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98) +- [Redis有1000万个key,找出前缀为aaa的key的命令是什么?](#redis%E6%9C%891000%E4%B8%87%E4%B8%AAkey%E6%89%BE%E5%87%BA%E5%89%8D%E7%BC%80%E4%B8%BAaaa%E7%9A%84key%E7%9A%84%E5%91%BD%E4%BB%A4%E6%98%AF%E4%BB%80%E4%B9%88) +- [热key问题](#%E7%83%ADkey%E9%97%AE%E9%A2%98) + - [怎么发现热key](#%E6%80%8E%E4%B9%88%E5%8F%91%E7%8E%B0%E7%83%ADkey) + - [方法一:凭借业务经验,进行预估哪些是热key](#%E6%96%B9%E6%B3%95%E4%B8%80%E5%87%AD%E5%80%9F%E4%B8%9A%E5%8A%A1%E7%BB%8F%E9%AA%8C%E8%BF%9B%E8%A1%8C%E9%A2%84%E4%BC%B0%E5%93%AA%E4%BA%9B%E6%98%AF%E7%83%ADkey) + - [方法二:在客户端进行收集](#%E6%96%B9%E6%B3%95%E4%BA%8C%E5%9C%A8%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9B%E8%A1%8C%E6%94%B6%E9%9B%86) + - [方法三:在Proxy层做收集](#%E6%96%B9%E6%B3%95%E4%B8%89%E5%9C%A8proxy%E5%B1%82%E5%81%9A%E6%94%B6%E9%9B%86) + - [方法四:用redis自带命令](#%E6%96%B9%E6%B3%95%E5%9B%9B%E7%94%A8redis%E8%87%AA%E5%B8%A6%E5%91%BD%E4%BB%A4) + - [方法五:自己抓包评估](#%E6%96%B9%E6%B3%95%E4%BA%94%E8%87%AA%E5%B7%B1%E6%8A%93%E5%8C%85%E8%AF%84%E4%BC%B0) + - [怎么设置redis失效时间、怎么设置永久有效?](#%E6%80%8E%E4%B9%88%E8%AE%BE%E7%BD%AEredis%E5%A4%B1%E6%95%88%E6%97%B6%E9%97%B4%E6%80%8E%E4%B9%88%E8%AE%BE%E7%BD%AE%E6%B0%B8%E4%B9%85%E6%9C%89%E6%95%88) +- [Redis事务和MySQL事务有什么区别?](#redis%E4%BA%8B%E5%8A%A1%E5%92%8Cmysql%E4%BA%8B%E5%8A%A1%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) -## Redis介绍 +# Redis介绍 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。 Redis 与其他 key - value 缓存产品有以下三个特点: @@ -50,13 +79,13 @@ Redis 与其他 key - value 缓存产品有以下三个特点: -### Redis特点 +## Redis特点 1. 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) 2. 支持丰富数据类型,支持string,list,set,sorted set,hash 3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除 -### Redis和Memcached区别 +## Redis和Memcached区别 1. memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 2. redis的速度比memcached快很多 3. redis可以持久化其数据 @@ -65,8 +94,8 @@ Redis 与其他 key - value 缓存产品有以下三个特点: 6. value大小:redis最大可以达到1GB,而memcache只有1MB -## Redis内部数据结构 -### 字符串 +# Redis内部数据结构 +## 字符串 Redis的字符串叫做[SDS],即Simple Dynamic String。它的结构是一个带长度信息的字节数组 @@ -86,7 +115,7 @@ byte[] content; // 数组内容 4. 字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。 -### 字典 +## 字典 字典,是一种用于保存键值对的抽象数据结构,Redis中的hash结构、zset中value和score值的映射关系、Redis所有的key和value、带过期时间的key都是使用字典(dict)这个数据结构。 @@ -112,7 +141,7 @@ SAVE和BGSAVE的区别: 2. BGSAVE 是fork一个save的子进程,在执行save过程中,不影响主进程,客户端可以正常链接redis,等子进程fork执行save完成后,通知主进程,子进程关闭。很明显BGSAVE方式比较适合线上的维护操作,两种方式的使用一定要了解清楚在谨慎选择。 -### 压缩列表 +## 压缩列表 zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。 @@ -139,7 +168,7 @@ zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (z -### 快速列表 +## 快速列表 考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。 @@ -165,18 +194,26 @@ quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切 2. quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数list-compress-depth决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。 -### 跳跃列表 +## 跳跃列表 Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获取 value 列表的功能,这就需要另外一个结构「跳跃列表」。 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/redis-4.png) 图中只画了四层,Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64 次方个元素。每一个 kv 块对应的结构如下面的代码中的zslnode结构,kv header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是 Double.MIN_VALUE,用来垫底的。kv 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。 +### 为什么Redis选择使用跳表而不是红黑树来实现有序集合? +Redis 中的有序集合(zset) 支持的操作: +1. 插入一个元素 +2. 删除一个元素 +3. 查找一个元素 +4. 有序输出所有元素 +5. 按照范围区间查找元素(比如查找值在 [100, 356] 之间的数据) +其中,前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。按照区间查找数据时,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效。 -## Redis应用 -### 分布式锁 +# Redis应用 +## 分布式锁 Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。 分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。 @@ -205,15 +242,15 @@ GETSET lock.foo 通过GETSET,C1拿到的时间戳如果是超时的,那就说明中间锁超时并且中间没有被其他客户端抢先获得锁,因此C1拿到锁。 如果在C1之前,有个叫C2的客户端比C1快一步执行了上面的操作,那么C1拿到的时间戳是个未超时的值,这时C1没有如期获得锁,需要再次等待或重试。尽管C1没拿到锁,但它改写了C2设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。 -### 延时队列 +## 延时队列 延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset 的value,这个消息的到期处理时间作为score,然后用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。 -### 位图 +## 位图 Redis 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位置范围内 1 的个数,bitpos 用来查找指定范围内出现的第一个 0 或 1。 比如我们可以通过 bitcount 统计用户一共签到了多少天,通过 bitpos 指令查找用户从哪一天开始第一次签到。如果指定了范围参数[start, end],就可以统计在某个时间范围内用户签到了多少天,用户自某天以后的哪天开始签到。 -### HyperLogLog +## HyperLogLog HyperLogLog 数据结构是 Redis 的高级数据结构,HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了 -### 布隆过滤器 +## 布隆过滤器 布隆过滤器是一个神奇的数据结构,可以用来判断一个元素是否在一个集合中。很常用的一个功能是用来去重。 redis 在 4.0 的版本中加入了 module 功能,布隆过滤器可以通过 module 的形式添加到 redis 中,所以使用 redis 4.0 以上的版本可以通过加载 module 来使用 redis 中的布隆过滤器。但是这不是最简单的方式,使用 docker 可以直接在 redis 中体验布隆过滤器。 @@ -244,9 +281,20 @@ redis 中有一个命令可以来设置这两个值: +# Gossip协议 +Gossip协议是一个通信协议,一种传播消息的方式,灵感来自于:瘟疫、社交网络等。使用Gossip协议的有:Redis Cluster、Consul、Apache Cassandra等。 +Gossip协议基本思想就是:**一个节点想要分享一些信息给网络中的其他的一些节点**。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。**这些收到信息的节点接下来会做同样的事情**,即把这些信息传递给其他一些随机选择的节点。一般而言,信息会周期性的传递给N个目标节点,而不只是一个。这个N被称为fanout(这个单词的本意是扇出) -## Redis单进程单线程方式 +Gossip协议的主要用途就是**信息传播和扩散**:即把一些发生的事件传播到全世界。它们也被用于数据库复制,信息扩散,集群成员身份确认,故障探测等 + +特点: +1. **可扩展性**:即使某条消息传播过程中丢失,它也不需要做任何补偿措施 +2. **失败容错**:因为一个节点会多次分享某个需要传播的信息,即使不能连通某个节点,其他被感染的节点也会尝试向这个节点传播信息。 +3. **健壮性**:没有任何扮演特殊角色的节点(比如leader等)。任何一个节点无论什么时候下线或者加入,并不会破坏整个系统的服务质量。 + + +# Redis单进程单线程方式 注意:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行(具体是子线程还是子进程待读者深入研究);例如我在测试服务器上查看Redis进程,然后找到该进程下的线程 @@ -254,20 +302,20 @@ redis 中有一个命令可以来设置这两个值: -### 单进程单线程好处 +## 单进程单线程好处 1. 代码更清晰,处理逻辑更简单 2. 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗 3. 不存在多进程或者多线程导致的切换而消耗CPU -### 单进程单线程弊端 +## 单进程单线程弊端 1. 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善; -### 其他一些优秀的开源软件采用的模型 +## 其他一些优秀的开源软件采用的模型 1. 多进程单线程模型:Nginx (Nginx有两类进程,一类称为Master进程(相当于管理进程),另一类称为Worker进程(实际工作进程)) 2. 单进程多线程模型:MySQL、Memcached、Oracle( Windows版本); -### 多路I/O复用模型 +## 多路I/O复用模型 1. 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 2. 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。 @@ -275,7 +323,7 @@ redis 中有一个命令可以来设置这两个值: -## Redis快的主要原因 +# Redis快的主要原因 1. 完全基于内存 2. 采用单线程,避免了不必要的上下文切换和竞争条件 3. 数据结构简单,对数据操作也简单 @@ -285,7 +333,7 @@ redis 中有一个命令可以来设置这两个值: -## Redis主从复制 +# Redis主从复制 过程原理: 1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令 @@ -296,65 +344,131 @@ redis 中有一个命令可以来设置这两个值: 缺点:所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决 +# Redis持久化 + +## Redis RDB和AOF的优缺点对比以及如何选择 +**RDB的优点** +- RDB文件是紧凑的二进制文件,比较适合做冷备,全量复制的场景。 +- 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速; +- RDB对Redis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可; +- RDB使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了Redis的高性能 ; + +**RDB的缺点** +- 如果想要在Redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。 +- RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒; +- RDB无法实现实时或者秒级持久化 + +**AOF的优点** +- AOF可以更好的保护数据不丢失(每隔1秒,后台线程执行一次fsync操作) +- AOF日志文件以append-only模式写入,写入性能比较高 +- AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。 +- 适合做灾难性的误删除紧急恢复 + +**AOF的缺点** +- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大,恢复速度慢; +- AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的; + + +## RDB和AOF到底该如何选择? +重启Redis时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志重写, + +但是AOF重写性能相对rdb来说要慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。 + +Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。 + +AOF在进行文件重写时将重写这一刻之前的内存rdb快照文件的内容和增量的AOF修改内存数据的命令日志文件存在一起,都写入新的aof文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。 -## Redis两种持久化方式优缺点 -1. RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot) -2. AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 -3. Redis 还可以同时使用 AOF 持久化和 RDB 持久化。当redis重启时,它会有限使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更加完整 -**RDB的优点:** -1. RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。 +## 为什么恢复的时候RDB比AOF快? +- AOF,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的; +- RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可; -2. RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。 -3. RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。 -4. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快 -## Redis常见的性能问题都有哪些?如何解决? +# Redis常见的性能问题都有哪些?如何解决? 1. Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。 2. Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。 3. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。 4. Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内 -## Redis提供6种数据淘汰策略 -1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 -2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 -3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 -4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 -5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 -6. no-enviction(驱逐):禁止驱逐数据 +# Redis六种淘汰key策略(默认时no-eviction) +- volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key +- allkeys-lru:移除最近最少使用的key +- volatile-random:在设置了过期时间的键空间中,随机移除一个key +- allkey-random:随机移除一个key +- volatile-ttl:在设置了过期时间的键空间中,移除将要过期的key +- no-eviction:在内存使用达到阈值的时候,所有引起申请内存的命令会报错 + + + + + + +# Redis三种删除过期键策略 +- 定时删除: 在设置键的过期时间的同时,创建一个定时器,让定时器执行对键的删除操作 +- 惰性删除: 每次取的时候先判断 expires 对象里面的键是否已经过期,如果过期,则删除键,否则,返回该键 +- 定期删除: 每隔一段时间,程序对数据库遍历检查一遍,然后删除过期的键 + +## 定时删除(并没有用到) +在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作; + +定时删除操作对于内存来说是友好的,内存不需要操作,而是通过使用定时器,可以保证尽快的将过期键删除,但是对于CPU来说不是友好的,如果过期键比较多的话,起的定时器也会比较多,删除的这个操作会占用到CPU的资源; + +## 惰性删除 +放任键过期不管,但是每次从键空间中获取键是,都检查取得的键的过期时间,如果过期的话,删除即可; + +惰性操作对于CPU来说是友好的,过期键只有在程序读取时判断是否过期才删除掉,而且也只会删除这一个过期键,但是对于内存来说是不友好的,如果多个键都已经过期了,而这些键又恰好没有被访问,那么这部分的内存就都不会被释放出来; + +## 定期删除 +每隔一段时间,程序就对数据库进行一次检查,删除掉过期键; -## Redis设置过期时间的原理 -Redis 提供的诸多命令中,EXPIRE、EXPIREAT、PEXPIRE、PEXPIREAT 以及 SETEX 和 PSETEX 均可以用来设置一条 Key-Value 对的过期时间。 +定期删除是上面两种方案的折中方案,每隔一段时间来删除过期键,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响,除此之外,还有效的减少内存的浪费;但是该策略的难点在于间隔时长,这个需要根据自身业务情况来进行设置; -Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,过期键的处理就是把过期键删除,这里的操作主要是针对过期字段处理的。 +>目前,Redis采用的是惰性删除+定期删除的方案,通过配合使用,服务器可以很好的平衡 CPU 和内存。 -Redis中有三种处理策略:定时删除、惰性删除和定期删除 -1. 定时删除:在设置键的过期时间的时候创建一个定时器,当过期时间到的时候立马执行删除操作。不过这种处理方式是即时的,不管这个时间内有多少过期键,不管服务器现在的运行状况,都会立马执行,所以对CPU不是很友好。但是这在最大程度上释放了内存,所以这种方式算是一种内存优先优化策略。 -2. 惰性删除:惰性删除策略不会在键过期的时候立马删除,而是当外部指令获取这个键的时候才会主动删除。处理过程为:接收get执行、判断是否过期、执行删除操作、返回nil(空)。 -3. 定期删除:定期删除是设置一个时间间隔,每个时间段都会检测是否有过期键,如果有执行删除操作。 +# Redis的内存优化 +1. redisObject对象 +2. 缩减键值对象 +3. 共享对象池 +4. 字符串优化 +5. 编码优化 +6. 控制key的数量 -### 定时扫描策略 -Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。 -1. 从过期字典中随机 20 个 key; -2. 删除这 20 个 key 中已经过期的 key; -3. 如果过期的 key 比率超过 1/4,那就重复步骤 1; +# Redis4.0新特性 +包含几个重大改进:更好的复制(PSYNC2),线程DEL / FLUSH,混合RDB + AOF格式,活动内存碎片整理,内存使用和性能改进。 -同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。 +## 一、主从数据同步机制 +- PSYNC2: 新的一种主从复制同步机制。 +- SYNC1:2.8~4.0之前版本的同步为PSYNC1 +- psync1因为网络中断或者阻塞导致主从中断,恢复后必须重新到主节点dump一份全量数据同步到从节点。psync2再中断恢复后只需要同步复制延迟的那部分数据。 +## 二、命令优化 +线程DEL / FLUSH 优化 -## 缓存解决方案分析 +Redis现在可以在不同的线程中删除后台的key而不会阻塞服务器。 +## 三、慢日志记录客户端来源IP地址 +## 四、混合RDB + AOF格式 +混合RDB + AOF格式: 混合的RDB-AOF格式。 如果启用,则在重写AOF文件时使用新格式:重写使用更紧凑和更快的方式来生成RDB格式,并将AOF流附加到文件。 这允许在使用AOF持久性时更快地重写和重新加载。 +## 五、内存使用和性能改进 +1. Redis现在使用更少的内存来存储相同数量的数据。 +2. Redis现在可以对使用的内存进行碎片整理,并逐渐回收空间(这个功能依然是试用阶段,可以通过参数不开启即可) -### 缓存雪崩 + + + + +# 缓存解决方案分析 + +## 缓存雪崩 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。从而形成一系列连锁反应,造成整个系统崩溃。 一般有三种处理办法: 1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。 2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。 3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。 -### 缓存穿透 +## 缓存穿透 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。 解决方案: @@ -362,14 +476,14 @@ Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期 2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击 3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力 -### 缓存击穿 +## 缓存击穿 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 解决方案: 1. 设置热点数据永远不过期。 3. 加互斥锁,互斥锁 -### 缓存预热 +## 缓存预热 缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! 缓存预热解决方案: @@ -377,8 +491,74 @@ Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期 2. 数据量不大,可以在项目启动的时候自动进行加载; 3. 定时刷新缓存; -### 缓存更新 +## 缓存更新 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: 1. 定时去清理过期的缓存; 2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 + +# Redis Sentinel 与 Redis Cluster区别和各自适用场景 +## Redis Sentinel +Redis-Sentinel(哨兵模式)是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自懂切换。它的主要功能有以下几点: +- 不时地监控redis是否按照预期良好地运行; +- 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端); +- 能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。 + +## Redis Cluster +- Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。**当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的**。分布式集群首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集。 +- Redis Cluster采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是**为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽**。 +- Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383,计算公式:slot = CRC16(key)&16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。下图展现一个五个节点构成的集群,每个节点平均大约负责3276个槽,以及通过计算公式映射到对应节点的对应槽的过程。 + + + +# 为什么lua脚本结合Redis命令可以实现原子性? +Redis 提供了非常丰富的指令集,但是用户依然不满足,希望可以自定义扩充若干指令来完成一些特定领域的问题。Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。 + +redis会为lua脚本执行创建伪客户端模拟客户端调用redis执行命令,伪客户端执行lua脚本是排他的,再加上redis是原子性的。 + + +# Redis分布式锁会导致什么问题? +- 互斥性。在任意时刻,只有一个客户端能持有锁。 +- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 +- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。 +- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。 + +业务逻辑执行太慢,比锁失效时间还长怎么办,redisson的WatchDog 会每10秒轮询,延长锁。 + +如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,可以考虑 redlock。 + +# Redis有1000万个key,找出前缀为aaa的key的命令是什么? +- SCAN cursor [MATCH pattern] [COUNT count],指令指定返回条数 +- SCAN 0 MATCH aaa* COUNT 5 表示从游标0开始查询aaa开头的key,每次返回5条,但是这个5条不一定,只是给Redis打了个招呼,具体返回数量看Redis心情。 + + +# 热key问题 +热Key问题 +- 所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。 + +## 怎么发现热key +### 方法一:凭借业务经验,进行预估哪些是热key +其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。 +### 方法二:在客户端进行收集 +这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。 +### 方法三:在Proxy层做收集 +有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。 +### 方法四:用redis自带命令 +1. monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。 +2.hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。 +### 方法五:自己抓包评估 +Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。缺点就是开发成本高,维护困难,有丢包可能性。 + +## 怎么设置redis失效时间、怎么设置永久有效? +**设置redis失效时间** +- expire : 将键的生存时间设为 ttl 秒 +- pExpire :将键的生存时间设为 ttl 毫秒 +- expireAt :将键的过期时间设为 timestamp 所指定的秒数时间戳 +- pExpireAt : 将键的过期时间设为 timestamp 所指定的毫秒数时间戳。 + +**redis设置了数据永不过期 ** +- persist key持久化key,不设置失效时间,并用noeviction:默认回收策略,不淘汰,如果内存已满,添加数据是报错。 + + +# Redis事务和MySQL事务有什么区别? +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Redis-5.png) \ No newline at end of file diff --git a/notes/framework/Dubbo.md b/notes/framework/Dubbo.md index bac54d9..4fc3a8a 100644 --- a/notes/framework/Dubbo.md +++ b/notes/framework/Dubbo.md @@ -42,6 +42,7 @@ - [cluster集群](#cluster%E9%9B%86%E7%BE%A4) - [loadbalance负载均衡](#loadbalance%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1) - [dubbo实现SOA的服务降级](#dubbo%E5%AE%9E%E7%8E%B0soa%E7%9A%84%E6%9C%8D%E5%8A%A1%E9%99%8D%E7%BA%A7) + - [Dubbo的限流与降级怎么实现的?](#dubbo%E7%9A%84%E9%99%90%E6%B5%81%E4%B8%8E%E9%99%8D%E7%BA%A7%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E7%9A%84) - [9、Dubbo把网络通信的IO异步变同步](#9dubbo%E6%8A%8A%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E7%9A%84io%E5%BC%82%E6%AD%A5%E5%8F%98%E5%90%8C%E6%AD%A5) - [1. 异步,无返回值](#1-%E5%BC%82%E6%AD%A5%E6%97%A0%E8%BF%94%E5%9B%9E%E5%80%BC) - [2. 异步,有返回值](#2-%E5%BC%82%E6%AD%A5%E6%9C%89%E8%BF%94%E5%9B%9E%E5%80%BC) @@ -71,6 +72,7 @@ - [4. 修饰器模式](#4-%E4%BF%AE%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F) - [5. 代理模式](#5-%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F) - [13、Dubbo优雅关机](#13dubbo%E4%BC%98%E9%9B%85%E5%85%B3%E6%9C%BA) +- [14、扩展:如何自己设计一个类似dubbo的rpc框架?](#14%E6%89%A9%E5%B1%95%E5%A6%82%E4%BD%95%E8%87%AA%E5%B7%B1%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E7%B1%BB%E4%BC%BCdubbo%E7%9A%84rpc%E6%A1%86%E6%9E%B6) @@ -1315,7 +1317,10 @@ mock=fail:return null mock=force:return null ``` - +### Dubbo的限流与降级怎么实现的? +1. dubbo的服务者与消费者 service的配置,最大连接数 与 请求数目配置 +2. dubbo的超时设置 + 配置mock 类。 请求超时后会执行mock,并返回 +3. dubbo可以通过扩展Filter的方式引入Hystrix,具体代码如下:https://github.com/yskgood/dubbo-hystrix-support ## 9、Dubbo把网络通信的IO异步变同步 先讲一下单工、全双工 、半双工 区别 @@ -2273,4 +2278,16 @@ public void close(int timeout) { logger.warn(e.getMessage(), e); } } -``` \ No newline at end of file +``` + +## 14、扩展:如何自己设计一个类似dubbo的rpc框架? +可以从几方面去思考: +1. 服务订阅发布(注册中心) +2. 服务路由 +3. 负载均衡(随机、轮询、最少活跃调用数、一致性哈希负载均衡) +4. 集群容错(失败重试、限流降级) +5. 服务调用(同步调用、异步调用、参数回调、事件通知) +6. 多协议 +7. 序列化方式 +8. 统一配置 +9. 动态代理 \ No newline at end of file diff --git a/notes/framework/Kafka.md b/notes/framework/Kafka.md new file mode 100644 index 0000000..ff37f63 --- /dev/null +++ b/notes/framework/Kafka.md @@ -0,0 +1,296 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Kafka介绍](#kafka%E4%BB%8B%E7%BB%8D) +- [Kafka特性](#kafka%E7%89%B9%E6%80%A7) +- [Kafka使用场景](#kafka%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) +- [Kafka术语](#kafka%E6%9C%AF%E8%AF%AD) +- [Kafka集群](#kafka%E9%9B%86%E7%BE%A4) + - [线上集群部署方案](#%E7%BA%BF%E4%B8%8A%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88) + - [操作系统](#%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F) + - [磁盘](#%E7%A3%81%E7%9B%98) + - [磁盘容量](#%E7%A3%81%E7%9B%98%E5%AE%B9%E9%87%8F) + - [带宽](#%E5%B8%A6%E5%AE%BD) + - [集群参数配置](#%E9%9B%86%E7%BE%A4%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE) + - [Broker 端参数](#broker-%E7%AB%AF%E5%8F%82%E6%95%B0) + - [ZooKeeper 相关参数](#zookeeper-%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0) + - [Broker连接相关](#broker%E8%BF%9E%E6%8E%A5%E7%9B%B8%E5%85%B3) + - [Topic管理](#topic%E7%AE%A1%E7%90%86) + - [数据留存](#%E6%95%B0%E6%8D%AE%E7%95%99%E5%AD%98) +- [Kafka分区机制](#kafka%E5%88%86%E5%8C%BA%E6%9C%BA%E5%88%B6) + - [为什么分区?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%88%86%E5%8C%BA) + - [分区策略](#%E5%88%86%E5%8C%BA%E7%AD%96%E7%95%A5) + - [轮询策略](#%E8%BD%AE%E8%AF%A2%E7%AD%96%E7%95%A5) + - [随机策略](#%E9%9A%8F%E6%9C%BA%E7%AD%96%E7%95%A5) + - [按消息键保序策略](#%E6%8C%89%E6%B6%88%E6%81%AF%E9%94%AE%E4%BF%9D%E5%BA%8F%E7%AD%96%E7%95%A5) +- [Kafka副本机制](#kafka%E5%89%AF%E6%9C%AC%E6%9C%BA%E5%88%B6) + - [如何确保副本中所有的数据都是一致的呢?](#%E5%A6%82%E4%BD%95%E7%A1%AE%E4%BF%9D%E5%89%AF%E6%9C%AC%E4%B8%AD%E6%89%80%E6%9C%89%E7%9A%84%E6%95%B0%E6%8D%AE%E9%83%BD%E6%98%AF%E4%B8%80%E8%87%B4%E7%9A%84%E5%91%A2) + - [为什么追随者副本是不对外提供服务的呢?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%BD%E9%9A%8F%E8%80%85%E5%89%AF%E6%9C%AC%E6%98%AF%E4%B8%8D%E5%AF%B9%E5%A4%96%E6%8F%90%E4%BE%9B%E6%9C%8D%E5%8A%A1%E7%9A%84%E5%91%A2) + - [1. 方便实现“Read-your-writes”](#1-%E6%96%B9%E4%BE%BF%E5%AE%9E%E7%8E%B0read-your-writes) + - [2. 方便实现单调读(Monotonic Reads)](#2-%E6%96%B9%E4%BE%BF%E5%AE%9E%E7%8E%B0%E5%8D%95%E8%B0%83%E8%AF%BBmonotonic-reads) +- [无消息丢失配置怎么实现?](#%E6%97%A0%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E9%85%8D%E7%BD%AE%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0) + - [生产者程序丢失数据](#%E7%94%9F%E4%BA%A7%E8%80%85%E7%A8%8B%E5%BA%8F%E4%B8%A2%E5%A4%B1%E6%95%B0%E6%8D%AE) + - [消费者程序丢失数据](#%E6%B6%88%E8%B4%B9%E8%80%85%E7%A8%8B%E5%BA%8F%E4%B8%A2%E5%A4%B1%E6%95%B0%E6%8D%AE) + - [Kafka 无消息丢失的配置](#kafka-%E6%97%A0%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%85%8D%E7%BD%AE) +- [Kafka的通信](#kafka%E7%9A%84%E9%80%9A%E4%BF%A1) +- [消费者组](#%E6%B6%88%E8%B4%B9%E8%80%85%E7%BB%84) + - [Rebalance重平衡](#rebalance%E9%87%8D%E5%B9%B3%E8%A1%A1) + - [重平衡全流程](#%E9%87%8D%E5%B9%B3%E8%A1%A1%E5%85%A8%E6%B5%81%E7%A8%8B) + - [JoinGroup 请求的处理过程](#joingroup-%E8%AF%B7%E6%B1%82%E7%9A%84%E5%A4%84%E7%90%86%E8%BF%87%E7%A8%8B) + - [SyncGroup 请求的处理流程](#syncgroup-%E8%AF%B7%E6%B1%82%E7%9A%84%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B) + - [位移提交](#%E4%BD%8D%E7%A7%BB%E6%8F%90%E4%BA%A4) + - [消费进度监控](#%E6%B6%88%E8%B4%B9%E8%BF%9B%E5%BA%A6%E7%9B%91%E6%8E%A7) + + + + + +# Kafka介绍 +Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编写,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。 + +# Kafka特性 +- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。 +- 可扩展性:kafka集群支持热扩展 +- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失 +- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败) +- 高并发:支持数千个客户端同时读写 + +# Kafka使用场景 +- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。 +- 消息系统:解耦和生产者和消费者、缓存消息等。 +- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。 +- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 +- 流式处理:比如spark streaming和storm +- 事件源 + +# Kafka术语 +- 消息:Record。Kafka 是消息引擎嘛,这里的消息就是指 Kafka 处理的主要对象。 +- 主题:Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。 +- 分区:Partition。一个有序不变的消息序列。每个主题下可以有多个分区。 +- 消息位移:Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。 +- 副本:Replica。Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 +- 生产者:Producer。向主题发布新消息的应用程序。 +- 消费者:Consumer。从主题订阅新消息的应用程序。 +- 消费者位移:Consumer Offset。表征消费者消费进度,每个消费者都有自己的消费者位移。 +- 消费者组:Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。 +- 重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-1.png) + +# Kafka集群 +## 线上集群部署方案 +从操作系统、磁盘、磁盘容量和带宽等方面来讨论一下 +### 操作系统 +目前常见的操作系统有 3 种:Linux、Windows 和 macOS。应该说部署在 Linux 上的生产环境是最多的。 +如果考虑操作系统与 Kafka 的适配性,Linux 系统显然要比其他两个特别是 Windows 系统更加适合部署 Kafka。 + +Kafka 客户端底层使用了 Java 的 selector,selector 在 Linux 上的实现机制是 epoll,而在 Windows 平台上的实现机制是 select。因此在这一点上将 Kafka 部署在 Linux 上是有优势的,因为能够获得更高效的 I/O 性能。 + +Linux 平台实现了零拷贝机制,就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速地数据传输。在 Linux 部署 Kafka 能够享受到零拷贝技术所带来的快速数据传输特性。 + +### 磁盘 +应该选择普通的机械磁盘还是固态硬盘?前者成本低且容量大,但易损坏;后者性能优势大,不过单价高。 + +Kafka 大量使用磁盘,可它使用的方式多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写操作慢。 + +就 Kafka 而言,一方面 Kafka 自己实现了冗余机制来提供高可靠性;另一方面通过分区的概念,所以使用机械磁盘完全能够胜任 Kafka 线上环境。 + +### 磁盘容量 +我们来计算一下:每天 1 亿条 1KB 大小的消息,保存两份且留存两周的时间,那么总的空间大小就等于 1 亿 * 1KB * 2 / 1000 / 1000 = 200GB。一般情况下 Kafka 集群除了消息数据还有其他类型的数据,比如索引数据等,故我们再为这些数据预留出 10% 的磁盘空间,因此总的存储容量就是 220GB。既然要保存两周,那么整体容量即为 220GB * 14,大约 3TB 左右。Kafka 支持数据的压缩,假设压缩比是 0.75,那么最后你需要规划的存储空间就是 0.75 * 3 = 2.25TB。 + +总之在规划磁盘容量时你需要考虑下面这几个元素: +- 新增消息数 +- 消息留存时间 +- 平均消息大小 +- 备份数 +- 是否启用压缩 + +### 带宽 +在 1 小时内处理 1TB 的业务数据。需要多少台 Kafka 服务器来完成这个业务呢? + +通常情况下你只能假设 Kafka 会用到 70% 的带宽资源,因为总要为其他应用或进程留一些资源。 + +根据这个目标,我们每秒需要处理 2336Mb 的数据,除以 240,约等于 10 台服务器。如果消息还需要额外复制两份,那么总的服务器台数还要乘以 3,即 30 台。 + +## 集群参数配置 +### Broker 端参数 +- log.dirs:指定了 Broker 需要使用的若干个文件目录路径。要知道这个参数是没有默认值的,它必须由你亲自指定。 +- log.dir:注意这是 dir,结尾没有 s,说明它只能表示单个路径,它是补充上一个参数用的。 + +在线上生产环境中一定要为log.dirs配置多个路径。这样做有两个好处: +- 提升读写性能:比起单块磁盘,多块物理磁盘同时读写数据有更高的吞吐量。 +- 能够实现故障转移:即 Failover。这是 Kafka 1.1 版本新引入的强大功能。坏掉的磁盘上的数据会自动地转移到其他正常的磁盘上,而且 Broker 还能正常工作。 + +### ZooKeeper 相关参数 +- zookeeper.connect + +### Broker连接相关 +- listeners:监听器,告诉外部连接者要通过什么协议访问指定主机名和端口开放的 Kafka 服务。 +- advertised.listeners:和 listeners 相比多了个 advertised。Advertised 的含义表示宣称的、公布的,就是说这组监听器是 Broker 用于对外发布的。 + +### Topic管理 +- auto.create.topics.enable:是否允许自动创建 Topic。 +- unclean.leader.election.enable:是否允许 Unclean Leader 选举。 +- auto.leader.rebalance.enable:是否允许定期进行 Leader 选举。 + + +auto.create.topics.enable参数我建议最好设置成 false,即不允许自动创建 Topic。在线上环境里面有很多名字稀奇古怪的 Topic,大概都是因为该参数被设置成了 true 的缘故。 + +### 数据留存 +- log.retention.{hour|minutes|ms}:都是控制一条消息数据被保存多长时间。从优先级上来说 ms 设置最高、minutes 次之、hour 最低。 +- log.retention.bytes:这是指定 Broker 为消息保存的总磁盘容量大小。 +- message.max.bytes:控制 Broker 能够接收的最大消息大小。 +- retention.ms:规定了该 Topic 消息被保存的时长。默认是 7 天,即该 Topic 只保存最近 7 天的消息。一旦设置了这个值,它会覆盖掉 Broker 端的全局参数值。 +- retention.bytes:规定了要为该 Topic 预留多大的磁盘空间。和全局参数作用相似,这个值通常在多租户的 Kafka 集群中会有用武之地。当前默认值是 -1,表示可以无限使用磁盘空间。 + +# Kafka分区机制 +## 为什么分区? +分区的作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(Scalability)。不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量。 + +Kafka 的三层消息架构: +- 第一层是主题层,每个主题可以配置 M 个分区,而每个分区又可以配置 N 个副本。 +- 第二层是分区层,每个分区的 N 个副本中只能有一个充当领导者角色,对外提供服务;其他 N-1 个副本是追随者副本,只是提供数据冗余之用。 +- 第三层是消息层,分区中包含若干条消息,每条消息的位移从 0 开始,依次递增。 +- 最后,客户端程序只能与分区的领导者副本进行交互 + +## 分区策略 +所谓分区策略是决定生产者将消息发送到哪个分区的算法 +### 轮询策略 +也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0 + +轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。 + +### 随机策略 +所谓随机就是我们随意地将消息放置到任意一个分区上 +### 按消息键保序策略 +Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。 + +# Kafka副本机制 +所谓的副本机制(Replication),也可以称之为备份机制,通常是指分布式系统在多台网络互联的机器上保存有相同的数据拷贝。副本机制有什么好处呢? +1. 提供数据冗余。即使系统部分组件失效,系统依然能够继续运转,因而增加了整体可用性以及数据持久性。 +2. 提供高伸缩性。支持横向扩展,能够通过增加机器的方式来提升读性能,进而提高读操作吞吐量。 +3. 改善数据局部性。允许将数据放入与用户地理位置相近的地方,从而降低系统延时。 + +Kafka 是有主题概念的,而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。 + +所谓副本(Replica),本质就是一个只能追加写消息的提交日志。 + +## 如何确保副本中所有的数据都是一致的呢? +最常见的解决方案就是采用基于领导者(Leader-based)的副本机制。Apache Kafka 就是这样的设计。 + +基于领导者的副本机制的工作原理如下图所示 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-4.png) + +1. 在 Kafka 中,副本分成两类:领导者副本(Leader Replica)和追随者副本(Follower Replica)。每个分区在创建时都要选举一个副本,称为领导者副本,其余的副本自动称为追随者副本。 +2. Kafka 的副本机制比其他分布式系统要更严格一些。在 Kafka 中,追随者副本是不对外提供服务的。这就是说,任何一个追随者副本都不能响应消费者和生产者的读写请求。所有的请求都必须由领导者副本来处理,或者说,所有的读写请求都必须发往领导者副本所在的 Broker,由该 Broker 负责处理。追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步。 +3. 当领导者副本挂掉了,或者说领导者副本所在的 Broker 宕机时,Kafka 依托于 ZooKeeper 提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。老 Leader 副本重启回来后,只能作为追随者副本加入到集群中。 + +### 为什么追随者副本是不对外提供服务的呢? +#### 1. 方便实现“Read-your-writes” +顾名思义就是,当你使用生产者 API 向 Kafka 成功写入消息后,马上使用消费者 API 去读取刚才生产的消息。 + +#### 2. 方便实现单调读(Monotonic Reads) +就是对于一个消费者用户而言,在多次消费消息时,它不会看到某条消息一会儿存在一会儿不存在。 + + +# 无消息丢失配置怎么实现? +Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证。 + +## 生产者程序丢失数据 +Producer 永远要使用带有回调通知的发送 API,也就是说不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。 + +它能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况,你就可以有针对性地进行处理。 + +## 消费者程序丢失数据 +Consumer 端丢失数据主要体现在 Consumer 端要消费的消息不见了。Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。下面这张图来自于官网,它清晰地展示了 Consumer 端的位移数据。 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-2.png) + +比如对于 Consumer A 而言,它当前的位移值就是 9;Consumer B 的位移值是 11。 + +要对抗这种消息丢失,办法很简单:维持先消费消息,再更新位移的顺序即可。 + +如果是多线程异步处理消费消息,Consumer 程序不要开启自动提交位移,而是要应用程序手动提交位移。 + +## Kafka 无消息丢失的配置 +1. 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。 +2. 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。 +3. 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。 +4. 设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。 +5. 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。 +6. 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。 +7. 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。 +8. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。 + + +# Kafka的通信 +Apache Kafka 的所有通信都是基于 TCP 的,而不是基于 HTTP 或其他协议。无论是生产者、消费者,还是 Broker 之间的通信都是如此。 + +目的是利用 TCP 本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力 + +所谓的多路复用请求,即 multiplexing request,是指将两个或多个数据流合并到底层单一物理连接中的过程。TCP 的多路复用请求会在一条物理连接上创建若干个虚拟连接,每个虚拟连接负责流转各自对应的数据流。其实严格来说,TCP 并不能多路复用,它只是提供可靠的消息交付语义保证,比如自动重传丢失的报文。 + +更严谨地说,作为一个基于报文的协议,TCP 能够被用于多路复用连接场景的前提是,上层的应用协议(比如 HTTP)允许发送多条消息。 + +# 消费者组 +Consumer Group 是 Kafka 提供的可扩展且具有容错性的消费者机制。既然是一个组,那么组内必然可以有多个消费者或消费者实例(Consumer Instance),它们共享一个公共的 ID,这个 ID 被称为 Group ID。 + +组内的所有消费者协调在一起来消费订阅主题(Subscribed Topics)的所有分区(Partition)。当然,每个分区只能由同一个消费者组内的一个 Consumer 实例来消费。 + +理解 Consumer Group 记住下面这三个特性: +1. Consumer Group 下可以有一个或多个 Consumer 实例。这里的实例可以是一个单独的进程,也可以是同一进程下的线程。在实际场景中,使用进程更为常见一些。 +2. Group ID 是一个字符串,在一个 Kafka 集群中,它标识唯一的一个 Consumer Group。 +3. Consumer Group 下所有实例订阅的主题的单个分区,只能分配给组内的某个 Consumer 实例消费。这个分区当然也可以被其他的 Group 消费。 + +Kafka 仅仅使用 Consumer Group 这一种机制,却同时实现了传统消息引擎系统的两大模型:如果所有实例都属于同一个 Group,那么它实现的就是消息队列模型;如果所有实例分别属于不同的 Group,那么它实现的就是发布 / 订阅模型。 + +理想情况下,Consumer 实例的数量应该等于该 Group 订阅主题的分区总数。 + +## Rebalance重平衡 +Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区。 + +Rebalance 的触发条件: +1. 组成员数发生变更。比如有新的 Consumer 实例加入组或者离开组,抑或是有 Consumer 实例崩溃被“踢出”组。 +2. 订阅主题数发生变更。Consumer Group 可以使用正则表达式的方式订阅主题,比如 consumer.subscribe(Pattern.compile(“t.*c”)) 就表明该 Group 订阅所有以字母 t 开头、字母 c 结尾的主题。在 Consumer Group 的运行过程中,你新创建了一个满足这样条件的主题,那么该 Group 就会发生 Rebalance。 +3. 订阅主题的分区数发生变更。Kafka 当前只能允许增加一个主题的分区数。当分区数增加时,就会触发订阅该主题的所有 Group 开启 Rebalance。 + +### 重平衡全流程 +消费者组状态机的各个状态流转 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-5.png) + +一个消费者组最开始是 Empty 状态,当重平衡过程开启后,它会被置于 PreparingRebalance 状态等待成员加入,之后变更到 CompletingRebalance 状态等待分配方案,最后流转到 Stable 状态完成重平衡。 + +当有新成员加入或已有成员退出时,消费者组的状态从 Stable 直接跳到 PreparingRebalance 状态,此时,所有现存成员就必须重新申请加入组。当所有成员都退出组后,消费者组状态变更为 Empty。Kafka 定期自动删除过期位移的条件就是,组要处于 Empty 状态。因此,如果你的消费者组停掉了很长时间(超过 7 天),那么 Kafka 很可能就把该组的位移数据删除了。 + +在消费者端,重平衡分为两个步骤:分别是加入组和等待领导者消费者(Leader Consumer)分配方案。这两个步骤分别对应两类特定的请求:JoinGroup 请求和 SyncGroup 请求。 +1. 第一个发送 JoinGroup 请求的成员自动成为领导者,领导者消费者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案。 +2. 领导者向协调者发送 SyncGroup 请求,将刚刚做出的分配方案发给协调者。 + +#### JoinGroup 请求的处理过程 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-6.png) + +#### SyncGroup 请求的处理流程 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-7.png) + +## 位移提交 +Consumer 需要向 Kafka 汇报自己的位移数据,这个汇报过程被称为提交位移。因为 Consumer 能够同时消费多个分区的数据,所以位移的提交实际上是在分区粒度上进行的,即Consumer 需要为分配给它的每个分区提交各自的位移数据。 + +位移提交的语义保障是由你来负责的,Kafka 只会“无脑”地接受你提交的位移。你对位移提交的管理直接影响了你的 Consumer 所能提供的消息语义保障。 + +从用户的角度来说,位移提交分为自动提交和手动提交;从 Consumer 端的角度来说,位移提交分为同步提交和异步提交。 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Kafka-3.jpeg) + +我们先来说说自动提交和手动提交。所谓自动提交,就是指 Kafka Consumer 在后台默默地为你提交位移,作为用户的你完全不必操心这些事;自动提交位移的一个问题在于,它可能会出现重复消费。而手动提交,则是指你要自己提交位移,Kafka Consumer 压根不管。 + +反观手动提交位移,它的好处就在于更加灵活,你完全能够把控位移提交的时机和频率。但是,它也有一个缺陷,就是在调用 commitSync() 时,Consumer 程序会处于阻塞状态,直到远端的 Broker 返回提交结果,这个状态才会结束。 + + + +## 消费进度监控 +1. 使用 Kafka 自带的命令行工具 kafka-consumer-groups 脚本。 +2. 使用 Kafka Java Consumer API 编程。 +3. 使用 Kafka 自带的 JMX 监控指标。 \ No newline at end of file diff --git a/notes/framework/MyBatis.md b/notes/framework/MyBatis.md index 0104251..72d84f4 100644 --- a/notes/framework/MyBatis.md +++ b/notes/framework/MyBatis.md @@ -2,12 +2,50 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [MyBatis介绍](#mybatis%E4%BB%8B%E7%BB%8D) + - [ORM](#orm) + - [JPA](#jpa) + - [MyBatis概念](#mybatis%E6%A6%82%E5%BF%B5) + - [MyBatis优点](#mybatis%E4%BC%98%E7%82%B9) - [MyBatis原理](#mybatis%E5%8E%9F%E7%90%86) -- [MyBatis缓存](#mybatis%E7%BC%93%E5%AD%98) + - [MyBatis的设计思想](#mybatis%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%80%9D%E6%83%B3) +- [一级缓存和二级缓存](#%E4%B8%80%E7%BA%A7%E7%BC%93%E5%AD%98%E5%92%8C%E4%BA%8C%E7%BA%A7%E7%BC%93%E5%AD%98) + - [区别](#%E5%8C%BA%E5%88%AB) + - [为什么不推荐使用二级缓存?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8%E4%BA%8C%E7%BA%A7%E7%BC%93%E5%AD%98) + - [二级缓存的使用场景](#%E4%BA%8C%E7%BA%A7%E7%BC%93%E5%AD%98%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) +- [MyBatis使用的设计模式](#mybatis%E4%BD%BF%E7%94%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F) +- [MyBatis插件](#mybatis%E6%8F%92%E4%BB%B6) + - [插件的构建](#%E6%8F%92%E4%BB%B6%E7%9A%84%E6%9E%84%E5%BB%BA) + - [插件链是何时构建的](#%E6%8F%92%E4%BB%B6%E9%93%BE%E6%98%AF%E4%BD%95%E6%97%B6%E6%9E%84%E5%BB%BA%E7%9A%84) + - [插件如何执行](#%E6%8F%92%E4%BB%B6%E5%A6%82%E4%BD%95%E6%89%A7%E8%A1%8C) +- [数据库预编译为什么能防止SQL注入呢?](#%E6%95%B0%E6%8D%AE%E5%BA%93%E9%A2%84%E7%BC%96%E8%AF%91%E4%B8%BA%E4%BB%80%E4%B9%88%E8%83%BD%E9%98%B2%E6%AD%A2sql%E6%B3%A8%E5%85%A5%E5%91%A2) -# MyBatis原理 +## MyBatis介绍 +在介绍MyBatis之前先简单了解几个概念:ORM,JPA。 + +### ORM +ORM(Object-Relationship-Mapping):是对象关系映射的意思,它是一种思想,是指将数据库中的每一行数据用对象的形式表现出来。 + +### JPA +JPA(Java-Persistence-API):是Java持久化接口的意思,它是JavaEE关于ORM思想的一套标准接口,仅仅是一套接口,不是具体的实现。 + +### MyBatis概念 +MyBatis是一个实现了JPA规范的用来连接数据库并对其进行增删改查操作的开源框架 (就和传统的JDBC一样,就是个连接数据库的东西),其实,它底层就是一个JDBC封装的组件。MyBatis的前身是Ibatis,Ibatis创建与2002年最初为Apache下面的一个开源项目,2010迁移到google code下面并改名为MyBatis。 + +MyBatis虽然实现了JPA但是它并不是一个完完全全的ORM组件,而是一个基于SQL开发的半ORM组件。 + +而Hibernate是一个完完全全的ORM组件,它是完全基于对象来操作数据库中的记录,并不和MyBatis一样是一个假把式。 + +### MyBatis优点 +- 简单易学,容易上手(相比于Hibernate) ---- 基于SQL编程 +- 消除了JDBC大量冗余的代码,不需要手动开关连接 +- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持,而JDB提供了可扩展性,所以只要这个数据库有针对Java的jar包就可以就可以与MyBatis兼容),开发人员不需要考虑数据库的差异性。 +- 提供了很多第三方插件(分页插件 / 逆向工程) +- 能够与Spring很好的集成 + +## MyBatis原理 **MyBatis完成2件事情** 1. 封装JDBC操作 @@ -29,10 +67,52 @@ ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/frame-2.jpg) +### MyBatis的设计思想 +如果让我们自己设计一个MyBatis,那么最核心的思想是什么呢? + +答案:JDK动态代理和反射 -# MyBatis缓存 + +MyBatis的作用就是**调用一个Mapper接口的方法就相当于执行一条sql** + +1、MyBatis在SqlSession为给Mapper接口通过动态代理实现一个代理 +2、在代理方法里面通过反射获取接口名称、方法名称、参数,拿这些数据后执行Executor的jdbc与sql交互的方法(这个才是真正去执行sql) +3、执行sql的结果集通过反射设置到Bean对象里面返回 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/frame-4.png) + +注意**InvocationHandler**接口,这是proxy代理实例的调用处理程序实现的一个接口 + +mapper映射器其实就是一个动态代理对象,进入到MapperMethod的方法就能找到SqlSession的删除、更新、查询、选择方法,从底层实现来说:通过动态代理技术,让接口跑起来 + +``` +public class MyMapperProxy implements InvocationHandler{ + private MySqlSession sqlSession; + public MyMapperProxy(){} + public MyMapperProxy(MySqlSession sqlSession){ + this.sqlSession = sqlSession; + } +//在代理方法里面通过反射获取接口名称、方法名称、参数,拿这些数据后执行Executor的jdbc与sql交互的方法(这个才是真正去执行sql) + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String mapperClass = method.getDeclaringClass().getName(); + System.out.println("mapperClass"+mapperClass); + if (UserMapperXML.namespace.equals(mapperClass)){ + String methodName = method.getName(); + String originsql = UserMapperXML.getMethodSql(methodName); + String formatSql = String.format(originsql,String.valueOf(args[0])); + return sqlSession.selectOne(formatSql); + } + return null; + } +} +``` +首先根据命名空间,找出与mapper接口方法名相同的sql语句,然后交给sqlSession来执行。(这里用到反射) + +## 一级缓存和二级缓存 MyBatis提供查询缓存,用于减轻数据库压力,提高性能。MyBatis提供了一级缓存和二级缓存。 + ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/frame-3.jpg) 1. 一级缓存是SqlSession级别的缓存,每个SqlSession对象都有一个哈希表用于缓存数据,不同SqlSession对象之间缓存不共享。同一个SqlSession对象对象执行2遍相同的SQL查询,在第一次查询执行完毕后将结果缓存起来,这样第二遍查询就不用向数据库查询了,直接返回缓存结果即可。一级缓存是MyBatis内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利 @@ -42,4 +122,75 @@ MyBatis提供查询缓存,用于减轻数据库压力,提高性能。MyBatis -``` \ No newline at end of file +``` + +### 区别 +MyBatis 的一级缓存与二级缓存,是针对短时间内重复查询而做的优化: +* 一级缓存 + * Mybatis 默认只是开启一级缓存,一级缓存只是相对于同一个 SqlSession 而言。 + * 只有在参数和SQL完全一样的情况下,并且使用同一个 SqlSession 的情况下,Mybatis 才会将第一次的查询结果缓存起来,后续同一个SqlSession的再查询,就会命中缓存,而不是去直接查库 +* 二级缓存 + * 一级缓存对于使用不同的 SqlSession 并不会命中缓存,即一级缓存必须 SqlSession,参数与Sql必须完全一致 + * 二级缓存需要手动配置,使得缓存在SqlSessionFactory层面上能够提供给各个Sql Session 共享 + * 二级缓存能够对同样参数,同样Sql语句,当时不同 SqlSession的查询提供命中 + +### 为什么不推荐使用二级缓存? +MyBatis 的二级缓存是和命名空间绑定的,所以通常情况下每一个 Mapper 映射文件都拥有 自己的二级缓存,不同 Mapper 的二级缓存互不影响。 + +在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计, 使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们 的命名空间不同, 因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。 + +### 二级缓存的使用场景 +1. 以查询为主的应用中,只有尽可能少的增、删、改操作; +2. 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。 + +## MyBatis使用的设计模式 +1. Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder; +2. 工厂模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory; +3. 单例模式,例如ErrorContext和LogFactory; +4. 代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果; +5. 组合模式,例如SqlNode和各个子类ChooseSqlNode等; +6. 模板方法模式,例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler; +7. 适配器模式,例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现; +8. 装饰者模式,例如Cache包中的cache.decorators子包中等各个装饰者的实现; +9. 迭代器模式,例如迭代器模式PropertyTokenizer; + + +## MyBatis插件 +Mybatis插件又称拦截器,Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为(诸如SQL重写之类的),MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的Mybatis四大接口方法调用包括: +```java +Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) + +ParameterHandler (getParameterObject, setParameters) + +ResultSetHandler (handleResultSets, handleOutputParameters) + +StatementHandler (prepare, parameterize, batch, update, query) +``` +总体概括为: +1. 拦截执行器的方法 +2. 拦截参数的处理 +3. 拦截结果集的处理 +4. 拦截SQL语法构建的处理 + +### 插件的构建 +谈原理首先要知道StatementHandler,ParameterHandler,Result Handler都是代理,他们是Configuration创建,在创建过程中会调用interceptorChain.pluginAll()方法,为四大组件组装插件(再底层是通过Plugin.wrap(target,XX, new Plugin( interceptor))来来创建的)。 + +### 插件链是何时构建的 +在执行SqlSession的query或者update方法时,SqlSession会通过Configuration创建Executor代理,在创建过程中就调用interceptor的pluginAll方法组装插件。然后executor在调用doQuery()方法的时候,也会调用Configuration的newStatementHandler方法创建StatemenHandler(和上面描述的一样,这个handler就是个代理,也是通过interceptorChain的pluginAll方法构建插件) + +### 插件如何执行 +以statementhandler的prepare方法的插件为例,正如前面所说,statementhandler是一个proxy,执行他的prepare方法,将调用invokeHandler的invoke方法,而invokeHandler就是Plugin.wrap(target, xxx, new Plugin(interceptor))中的第三个参数,所以很自然invokeHanlder的invoke的方法最终就会调用interceptor对象的intercept方法。 + +PageHelper分页的实现原来是在我们执行SQL语句之前动态的将SQL语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。 + +## 数据库预编译为什么能防止SQL注入呢? +所谓SQL注入,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力。 + +因为SQL语句在程序运行前已经进行了预编译,在程序运行时第一次操作数据库之前,SQL语句已经被数据库分析,编译和优化,对应的执行计划也会缓存下来并允许数据库已参数化的形式进行查询,当运行时动态地把参数传给PreprareStatement时,**即使参数里有敏感字符如 or '1=1’也数据库会作为一个参数一个字段的属性值来处理而不会作为一个SQL指令**,如此,就起到了SQL注入的作用了! + +如果我们想防止SQL注入,理所当然地要在输入参数上下功夫。上面代码中黄色高亮即输入参数在SQL中拼接的部分,传入参数后,打印出执行的SQL语句,会看到SQL是这样的: +``` +SELECT id,title,author,content FROM blog WHERE id = ? +``` +不管输入什么参数,打印出的SQL都是这样的。这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。 + diff --git a/notes/framework/Nginx.md b/notes/framework/Nginx.md new file mode 100644 index 0000000..612d7d8 --- /dev/null +++ b/notes/framework/Nginx.md @@ -0,0 +1,235 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nginx是什么?](#nginx%E6%98%AF%E4%BB%80%E4%B9%88) +- [我们为什么选择Nginx?](#%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9nginx) + - [1. IO多路复用epoll](#1-io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8epoll) + - [2. 轻量级](#2-%E8%BD%BB%E9%87%8F%E7%BA%A7) + - [3. CPU亲和](#3-cpu%E4%BA%B2%E5%92%8C) + - [4. sendfile](#4-sendfile) +- [模块](#%E6%A8%A1%E5%9D%97) +- [使用场景](#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) + - [1、静态资源WEB服务](#1%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90web%E6%9C%8D%E5%8A%A1) + - [2、浏览器缓存](#2%E6%B5%8F%E8%A7%88%E5%99%A8%E7%BC%93%E5%AD%98) + - [3、跨站访问](#3%E8%B7%A8%E7%AB%99%E8%AE%BF%E9%97%AE) + - [4、防盗链](#4%E9%98%B2%E7%9B%97%E9%93%BE) + - [基于http_refer防盗链配置模块](#%E5%9F%BA%E4%BA%8Ehttp_refer%E9%98%B2%E7%9B%97%E9%93%BE%E9%85%8D%E7%BD%AE%E6%A8%A1%E5%9D%97) + - [valid_referers](#valid_referers) + - [5、HTTP代理服务](#5http%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1) + + + +## Nginx是什么? +Nginx是一个开源且高性能、可靠的HTTP中间件、代理服务 + +其他的HTTP服务: +1. HTTPD-Apache基金会 +2. IIS-微软 +3. GWS-Google(不对外开放) + +## 我们为什么选择Nginx? +### 1. IO多路复用epoll +### 2. 轻量级 +- 功能模块少 - Nginx仅保留了HTTP需要的模块,其他都用插件的方式,后天添加 +- 代码模块化 - 更适合二次开发,如阿里巴巴Tengine +### 3. CPU亲和 +把CPU核心和Nginx工作进程绑定,把每个worker进程固定在一个CPU上执行,减少切换CPU的cache miss,从而提高性能。 +### 4. sendfile +sendfile: 设置为on表示启动高效传输文件的模式。 + +sendfile可以让Nginx在传输文件时直接在磁盘和tcp socket之间传输数据。如果这个参数不开启,会先在用户空间(Nginx进程空间)申请一个buffer,用read函数把数据从磁盘读到cache,再从cache读取到用户空间的buffer,再用write函数把数据从用户空间的buffer写入到内核的buffer,最后到tcp socket。开启这个参数后可以让数据不用经过用户buffer。 + +## 模块 + +Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。 + +Nginx的模块从结构上分为核心模块、基础模块和第三方模块: +1. 核心模块:HTTP模块、EVENT模块和MAIL模块 +2. 基础模块:HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块 +3. 第三方模块:HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块 + +用户根据自己的需要开发的模块都属于第三方模块。正是有了这么多模块的支撑,Nginx的功能才会如此强大。 + +Nginx的模块从功能上分为如下三类: +- Handlers(处理器模块)。此类模块直接处理请求,并进行输出内容和修改headers信息等操作。Handlers处理器模块一般只能有一个。 +- Filters (过滤器模块)。此类模块主要对其他处理器模块输出的内容进行修改操作,最后由Nginx输出。 +- Proxies (代理类模块)。此类模块是Nginx的HTTP Upstream之类的模块,这些模块主要与后端一些服务比如FastCGI等进行交互,实现服务代理和负载均衡等功能。 + + +Nginx内置模块 +- http_auth_basic_module HTTP基本认证 +- http_stub_status_module 状态信息 +- http_gzip_module 压缩资源 +- http_gzip_static_module 支持.gz资源 +- http_sub_module 字符串替换 +- http_addition_module 追加内容 +- http_realip_module 获取实际IP +- http_image_filter_module 图片处理 +- http_geoip_module 支持GeoIP +- http_auth_request_module 第三方auth支持 +- http_flv_module 流媒体点播 + +## 使用场景 +### 1、静态资源WEB服务 +nginx静态资源配置 + ```lombok.config +配置域:http、server、location +#文件高速读取 +http { + sendfile on; +} +#在 sendfile 开启的情况下,开启 tcp_nopush 提高网络包传输效率 +#tcp_nopush 将文件一次性一起传输给客户端,就好像你有十个包裹,快递员一次送一个,来回十趟,开启后,快递员讲等待你十个包裹都派件,一趟一起送给你 +http { + sendfile on; + tcp_nopush on; +} +#tcp_nodelay 开启实时传输,传输方式与 tcp_nopush 相反,追求实时性,但是它只有在长连接下才生效 +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; +} + +#将访问的文件压缩传输 (减少文件资源大小,提高传输速度) +#当访问内容以gif或jpg结尾的资源时 +location ~ .*\.(gif|jpg)$ { + gzip on; #开启 + gzip_http_version 1.1; #服务器传输版本 + gzip_comp_level 2; #压缩比,越高压缩越多,压缩越高可能会消耗服务器性能 + gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/xml+rss image/jpeg image/gif image/png; #压缩文件类型 + root /opt/app/code; #对应目录(去该目录下寻找对应文件) +} + +#直接访问已压缩文件 +#当访问路径以download开头时,如www.baidu.com/download/test.img +#去/opt/app/code目录下寻找test.img.gz文件,返回到前端时已是可以浏览的img文件 +location ~ load^/download { + gzip_static on #开启; + tcp_nopush on; + root /opt/app/code; +} +``` +### 2、浏览器缓存 +HTTP协议定义的缓存机制(如:Expires; Cache-control等 ) +减少服务端的消耗,降低延迟 + +1.浏览器无缓存 + +浏览器请求 -> 无缓存 -> 请求WEB服务器 -> 请求相应 -> 呈现 + +在呈现阶段会根据缓存的设置在浏览器中生成缓存 + +2.浏览器有缓存 + +浏览器请求 -> 有缓存 -> 校验本地缓存时间是否过期 -> 没有过期 -> 呈现 + +若过期从新请求WEB服务器 + +3.语法配置 + ```lombok.config +location ~ .*\.(html|htm)$ { + expires 12h; #缓存12小时 +} +``` + +服务器响应静态文件时,请求头信息会带上 etag 和 last_modified_since 2个标签值,浏览器下次去请求时,头信息发送这两个标签,服务器检测文件有没有发生变化,如无,直接头信息返 etag 和last_modified_since,状态码为 304 ,浏览器知道内容无改变,于是直接调用本地缓存,这个过程也请求了服务,但是传着的内容极少 +### 3、跨站访问 + +为什么浏览器禁止跨站访问? +- 不安全,容易出现CSRF攻击 + +为什么还需要nginx打开跨域访问? +- nginx配置文件通过使用add_header指令来设置response header。使用ngx_http_headers_module中的add_header 指令,在响应头中添加允许跨域。 + +开发nginx跨站访问设置 +```lombok.config +location ~ .*\.(html|htm)$ { + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS; + #Access-Control-Allow-Credentials true #允许cookie跨域 +} +``` + +在响应中指定 Access-Control-Allow-Credentials 为 true 时,Access-Control-Allow-Origin 不能指定为 *,需要设置为允许访问的域名,比如http://abc.com + +### 4、防盗链 +目的:防止服务器内的静态资源被其他网站所套用 + + +防盗链设置思路:区别哪些请求是非正常的用户请求 + +#### 基于http_refer防盗链配置模块 +当浏览器向web服务器发送请求的时候,一般会在头信息中带上Referer字段,告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理。基于头信息的Referer字段,nginx识别指定的Referer,在客户端请求时,通过匹配referer头域与配置,对于指定放行,对于其他referer视为盗链。 + +nginx模块ngx_http_referer_module通常用于阻挡来源非法的域名请。 + +#### valid_referers +valid_referers配置项是属于ngx_http_referer_module模块传送门{:target="_blank"} + +>Syntax: valid_referers none | blocked | server_names | string ...; +Default: — +Context: server, location + + +示例配置 + +```lombok.config +location ~ .*\.(jpg|gif|png)$ { + root /opt/app/code/images; + gzip on; + gzip_http_version 1.1; + gzip_comp_level 2; + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; + + valid_referers none blocked 116.62.113.218 walidream.com; + if ($invalid_referer) { + return 403; + } +} + +``` +允许116.62.113.218 walidream.com访问服务器图片 + + +因为HTTPReferer头信息是可以通过程序来伪装生成的,所以通过Referer信息防盗链并非100%可靠,但是,它能够限制大部分的盗链 + +### 5、HTTP代理服务 + +代理区别 +- 正向代理代理的对象是客户端 +- 反向代理代理的对象是服务端 + +反向代理 +```lombok.config +语法:proxy_pass URL +默认:—— +位置:loaction + +#代理端口 +#场景:服务器80端口开放,8080端口对外关闭,客户端需要访问到8080 +#在nginx中配置proxy_pass代理转发时,如果在proxy_pass后面的url加/,表示绝对根路径;如果没有/,表示相对路径,把匹配的路径部分也给代理走 +server { + listen 80; + location / { + proxy_pass http://127.0.0.1:8080/; + proxy_redirect default; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; #获取客户端真实IP + + proxy_connect_timeout 30; #超时时间 + proxy_send_timeout 60; + proxy_read_timeout 60; + + proxy_buffer_size 32k; + proxy_buffering on; #开启缓冲区,减少磁盘io + proxy_buffers 4 128k; + proxy_busy_buffers_size 256k; + proxy_max_temp_file_size 256k; #当超过内存允许储蓄大小,存到文件 + } +} +``` + + diff --git a/notes/framework/Spring.md b/notes/framework/Spring.md index 8c88b81..8d6dbd1 100644 --- a/notes/framework/Spring.md +++ b/notes/framework/Spring.md @@ -2,17 +2,32 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [Spring 介绍](#spring-%E4%BB%8B%E7%BB%8D) + - [什么是spring?](#%E4%BB%80%E4%B9%88%E6%98%AFspring) + - [Spring框架的设计目标,设计理念,和核心是什么?](#spring%E6%A1%86%E6%9E%B6%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%9B%AE%E6%A0%87%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E5%92%8C%E6%A0%B8%E5%BF%83%E6%98%AF%E4%BB%80%E4%B9%88) + - [Spring和SpringMVC的关系](#spring%E5%92%8Cspringmvc%E7%9A%84%E5%85%B3%E7%B3%BB) + - [Spring中使用的设计模式](#spring%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F) - [Spring IOC](#spring-ioc) - [IOC原理](#ioc%E5%8E%9F%E7%90%86) - [Spring单例Bean与单例模式的区别](#spring%E5%8D%95%E4%BE%8Bbean%E4%B8%8E%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%8C%BA%E5%88%AB) + - [构造器依赖注入和 Setter方法注入的区别](#%E6%9E%84%E9%80%A0%E5%99%A8%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E5%92%8C-setter%E6%96%B9%E6%B3%95%E6%B3%A8%E5%85%A5%E7%9A%84%E5%8C%BA%E5%88%AB) - [Spring AOP](#spring-aop) - [实现AOP的技术](#%E5%AE%9E%E7%8E%B0aop%E7%9A%84%E6%8A%80%E6%9C%AF) - [Spring实现AOP](#spring%E5%AE%9E%E7%8E%B0aop) - [AOP使用场景](#aop%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) + - [过滤器filter、拦截器interceptor、和AOP的区别与联系](#%E8%BF%87%E6%BB%A4%E5%99%A8filter%E6%8B%A6%E6%88%AA%E5%99%A8interceptor%E5%92%8Caop%E7%9A%84%E5%8C%BA%E5%88%AB%E4%B8%8E%E8%81%94%E7%B3%BB) + - [filter过滤器](#filter%E8%BF%87%E6%BB%A4%E5%99%A8) + - [Interceptor拦截器](#interceptor%E6%8B%A6%E6%88%AA%E5%99%A8) + - [Spring AOP拦截器](#spring-aop%E6%8B%A6%E6%88%AA%E5%99%A8) - [Spring Bean生命周期](#spring-bean%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) -- [Spring @Transactional工作原理](#spring-transactional%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) +- [Spring @Transactional](#spring-transactional) + - [工作原理](#%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) + - [参数配置](#%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE) + - [Spring事务什么情况下回滚?](#spring%E4%BA%8B%E5%8A%A1%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E5%9B%9E%E6%BB%9A) + - [Spring事务trycatch会回滚吗?](#spring%E4%BA%8B%E5%8A%A1trycatch%E4%BC%9A%E5%9B%9E%E6%BB%9A%E5%90%97) - [SpringMVC的工作原理](#springmvc%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) - [两种bean的实例化](#%E4%B8%A4%E7%A7%8Dbean%E7%9A%84%E5%AE%9E%E4%BE%8B%E5%8C%96) + - [Spring与SpringMVC父子容器的区别和联系](#spring%E4%B8%8Espringmvc%E7%88%B6%E5%AD%90%E5%AE%B9%E5%99%A8%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E8%81%94%E7%B3%BB) - [SpringMVC拦截器](#springmvc%E6%8B%A6%E6%88%AA%E5%99%A8) - [常见应用场景](#%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF) - [拦截器接口](#%E6%8B%A6%E6%88%AA%E5%99%A8%E6%8E%A5%E5%8F%A3) @@ -34,12 +49,49 @@ - [7、寻找依赖](#7%E5%AF%BB%E6%89%BE%E4%BE%9D%E8%B5%96) - [8、针对不同的scope进行bean的创建](#8%E9%92%88%E5%AF%B9%E4%B8%8D%E5%90%8C%E7%9A%84scope%E8%BF%9B%E8%A1%8Cbean%E7%9A%84%E5%88%9B%E5%BB%BA) - [9、类型转换](#9%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2) +- [BeanFactory 和 ApplicationContext有什么区别?](#beanfactory-%E5%92%8C-applicationcontext%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) + - [依赖关系](#%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB) + - [加载方式](#%E5%8A%A0%E8%BD%BD%E6%96%B9%E5%BC%8F) + - [创建方式](#%E5%88%9B%E5%BB%BA%E6%96%B9%E5%BC%8F) + - [注册方式](#%E6%B3%A8%E5%86%8C%E6%96%B9%E5%BC%8F) +- [前后端分离跨域解决](#%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E8%B7%A8%E5%9F%9F%E8%A7%A3%E5%86%B3) +- [Spring 注解](#spring-%E6%B3%A8%E8%A7%A3) + - [@Component, @Controller, @Repository, @Service](#component-controller-repository-service) + - [@service和@component的区别](#service%E5%92%8Ccomponent%E7%9A%84%E5%8C%BA%E5%88%AB) + - [@Autowired和@Resource之间的区别](#autowired%E5%92%8Cresource%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8C%BA%E5%88%AB) + - [@Conditional的使用](#conditional%E7%9A%84%E4%BD%BF%E7%94%A8) +# Spring 介绍 +## 什么是spring? +Spring是一个轻量级Java开发框架,最早有Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。 +Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。 +## Spring框架的设计目标,设计理念,和核心是什么? +Spring设计目标:Spring为开发者提供一个一站式轻量级应用开发平台; +Spring设计理念:在JavaEE开发中,支持POJO和JavaBean开发方式,使应用面向接口开发,充分支持OO(面向对象)设计方法;Spring通过IoC容器实现对象耦合关系的管理,并实现依赖反转,将对象之间的依赖关系交给IoC容器,实现解耦; +Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系;通过AOP以动态非侵入的方式增强服务。 + +IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。 + + +## Spring和SpringMVC的关系 +Spring是IOC和AOP的容器框架,SpringMVC是基于Spring功能之上添加的Web框架,想用SpringMVC必须先依赖Spring。 + +Spring可以说是一个管理bean的容器,也可以说是包括很多开源项目的总称,spring mvc是其中一个开源项目 + +## Spring中使用的设计模式 +- 简单工厂模式:spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象 +- 单例模式:Spring下默认的bean均为singleton,可以通过singleton=“true|false” 或者 scope="?"来指定。 +- 代理模式:AOP +- 适配器模式:AOP的处理中有Adapter模式,由于Advisor链需要的是MethodInterceptor对象,所以每一个Advisor中的Advice都要适配成对应的MethodInterceptor对象。 +- 包装器模式: +- 观察者模式:listener的实现。如ApplicationListener +- 策略模式:spring中在实例化对象的时候用到Strategy模式 +- 模板方法模式:spring中的JdbcTemplate # Spring IOC java程序中的每个业务逻辑至少需要两个或以上的对象来协作完成。通常,每个对象在使用他的合作对象时,自己均要使用像new object() 这样的语法来完成合作对象的申请工作。这样对象间的耦合度高了。 @@ -67,6 +119,17 @@ IOC: 与此相比,Spring的单例Bean是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个Spring容器,即使是单例bean,也一定会创建多个实例 + +## 构造器依赖注入和 Setter方法注入的区别 +构造函数注入 | setter 注入 +-|- +没有部分注入 | 有部分注入 +不会覆盖 setter 属性 | 会覆盖 setter 属性 +任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 +适用于设置很多属性 | 适用于设置少量属性 + + +两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。 # Spring AOP 1. AOP利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了 多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。 @@ -112,6 +175,31 @@ spring事务其实是一种特殊的aop方式。在spring配置文件中配置 11. Synchronization 同步 12. Transactions 事务管理 + +## 过滤器filter、拦截器interceptor、和AOP的区别与联系 + +### filter过滤器 +> * **过滤器拦截web访问url地址**。 严格意义上讲,filter只是适用于web中,依赖于Servlet容器,利用**Java的回调机制**进行实现。 +> * Filter**过滤器**:和框架无关,可以控制最初的http请求,但是更细一点的类和方法控制不了。 +> * **过滤器可以拦截到方法的请求和响应(ServletRequest request, ServletResponse response)**,并对请求响应做出像响应的过滤操作, +> * 比如**设置字符编码,鉴权操作**等 + +### Interceptor拦截器 + +> * **拦截器拦截以 .action结尾的url,拦截Action的访问**。 Interfactor是基于**Java的反射机制**(APO思想)进行实现,不依赖Servlet容器。 +> * **拦截器可以在方法执行之前(preHandle)和方法执行之后(afterCompletion)进行操作,回调操作(postHandle)**,**可以获取执行的方法的名称**,请求(HttpServletRequest) +> * Interceptor:**可以控制请求的控制器和方法**,但**控制不了请求方法里的参数(只能获取参数的名称,不能获取到参数的值)** +> * **(**用于处理页面提交的请求响应并进行处理,例如做国际化,做主题更换,过滤等)。 + +### Spring AOP拦截器 +> * **只能拦截Spring管理Bean的访问(业务层Service)**。 具体AOP详情参照 [Spring AOP:原理、 通知、连接点、切点、切面、表达式](https://blog.csdn.net/fly910905/article/details/84025425) +> * 实际开发中,AOP常和事务结合:[Spring的事务管理:声明式事务管理(切面)](https://blog.csdn.net/fly910905/article/details/83547744) +> * **AOP操作可以对操作进行横向的拦截**,最大的优势在于他可**以获取执行方法的参数( ProceedingJoinPoint.getArgs() )**,对方法进行统一的处理。 +> * Aspect : 可以自定义切入的点,有方法的参数,**但是拿不到http请求,可以通过其他方式如RequestContextHolder**获得( +> ServletRequestAttributes servletRequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); +> )。 +> * 常见**使用日志,事务,请求参数安全验证 + # Spring Bean生命周期 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/spring-4.png) - Bean的建立, 由BeanFactory读取Bean定义文件,并生成各个实例 @@ -126,7 +214,8 @@ spring事务其实是一种特殊的aop方式。在spring配置文件中配置 - Bean定义文件中定义destroy-method,在容器关闭时,可以在Bean定义文件中使用“destory-method”定义的方法 -# Spring @Transactional工作原理 +# Spring @Transactional +## 工作原理 1. 当spring遍历容器中所有的切面,查找与当前实例化bean匹配的切面,这里就是获取事务属性切面,查找@Transactional注解及其属性值,然后根据得到的切面进入createProxy方法,创建一个AOP代理。 2. 默认是使用JDK动态代理创建代理,如果目标类是接口,则使用JDK动态代理,否则使用Cglib。 3. 获取的是当前目标方法对应的拦截器,里面是根据之前获取到的切面来获取相对应拦截器,这时候会得到TransactionInterceptor实例。如果获取不到拦截器,则不会创建MethodInvocation,直接调用目标方法。 @@ -137,8 +226,56 @@ spring事务其实是一种特殊的aop方式。在spring配置文件中配置 1. A类的a1方法没有标注@Transactional,a2方法标注@Transactional,在a1里面调用a2。a1方法是目标类A的原生方法,调用a1的时候即直接进入目标类A进行调用,在目标类A里面只有a2的原生方法,在a1里调用a2,即直接执行a2的原生方法,并不通过创建代理对象进行调用,所以并不会进入TransactionInterceptor的invoke方法,不会开启事务。 2. 将@Transactional注解标注在非public方法上。内部使用AOP,所以必须是public修饰的方法才可以被代理 +## 参数配置 +1. propagation参数,Propagation类型(枚举),默认值为Propogation.REQUIRED,支持的值有REQUIRED、MANDATORY、NESTED、NEVER、NOT_SUPPORTED、REQUIRE_NEW、SUPPORTS。关于这个问题的详细说明将在以后的文章中展开。 +2. isolation参数,Isolation类型(枚举),默认值为Isolation.DEFAULT,支持的值有DEFAULT、READ_COMMITTED、READ_UNCOMMITTED、REPEATABLE_READ、SERIALIZABLE。关于这个问题的详细说明将在以后的文章中展开。 +3. timeout参数,int类型,事务的超时时间,默认值为-1,即不会超时。 +4. readOnly参数,boolean类型,true表示事务为只读,默认值为false。 +5. rollbackFor参数,Class[]类型,默认为空数组。 +6. rollbackForClassName参数,String[]类型,默认为空数组。 +7. noRollbackFor参数,Class[]类型,默认为空数组。 +8. noRollbackForClassName参数,String[]类型,默认为空数组。 + +最后四个参数都与回滚有关,首先,一般不推荐使用rollbackForClassName和noRollbackForClassName两个参数,而用另外两个参数来代替,从参数的类型上就可以看出区别,使用字符串的缺点在于:如果不是用类的完整路径,就可能导致回滚设置对位于不同包中的同名类都生效;且如果类名写错,也无法得到IDE的动态提示。 + +但是,如果不配置任何与回滚有关的参数,不代表事务不会进行回滚,如果没有配置这四个选项,那么DefaultTransactionAttribute配置将会生效,具体的行为是,抛掷任何unchecked Exception都会触发回滚,当然包括所有的RuntimeException。 +## Spring事务什么情况下回滚? +Spring事务回滚机制是这样的:当所拦截的方法有指定异常抛出,事务才会自动进行回滚。 + +默认配置下,事务只会对Error与RuntimeException及其子类这些UNChecked异常,做出回滚。一般的Exception这些Checked异常不会发生回滚(如果一般Exception想回滚要做出配置); +## Spring事务trycatch会回滚吗? +依赖spring事物时,当service层进行try catch异常捕获时,事物不会产生回滚,代码如下 +``` + public void insertMsg(ConversationBean conversationBean){ + try{ + for(int i=0;i<100;i++){ + if(i!=10){ + testDao.insert2(i); + }else{ + testDao.insert1(i); + } + } + }catch(Exception e){ + } + } +``` +此时异常被捕获,这种业务方法也就等于脱离了spring事务的管理,因为没有任何异常会从业务方法中抛出,全被捕获,导致spring异常抛出触发事务回滚策略失效。 - +解决此类问题时,需要在try catch中显示的抛出异常RuntimeException 然后在Controller层捕获异常并编写返回值,代码如下: +``` + public void insertMsg(ConversationBean conversationBean){ + try{ + for(int i=0;i<100;i++){ + if(i!=10){ + testDao.insert2(i); + }else{ + testDao.insert1(i); + } + } + }catch(Exception e){ + throw new RuntimeException(); + } +``` # SpringMVC的工作原理 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/frame-1.jpg) SpringMVC流程 @@ -169,6 +306,11 @@ Spring提供了两种类型的IOC容器实现(两种类型的配置方式是 1. BeanFactory:是Spring框架的基础设施,面向Spring本身 2. ApplicationContext: 面向使用 Spring 框架的开发者,几乎所有的应用场合都直接使用 ApplicationContext 而非底层的 +## Spring与SpringMVC父子容器的区别和联系 +1. Spring 与SpringMVC 两个都是容器,存在父子关系(包含和被包含的关系) +2. Spring容器中存放着mapper代理对象,service对象,SpringMVC存放着Controller对象。**子容器SpringMVC中可以访问父容器中的对象。但父容器Spring不能访问子容器SpringMVC的对象**(存在领域作用域的原因,子容器可以访问父容器中的成员,而子容器的成员则只能被自己使用)。如:Service对象可以在Controller层中注入,反之则不行。 +3. Spring容器导入的properties配置文件,只能在Spring容器中用而在SpringMVC容器中不能读取到。 需要在SpringMVC 的配置文件中重新进行导入properties文件,并且同样在父容器Spring中不能被使用,导入后使用@Value("${key}")在java类中进行读取。 + # SpringMVC拦截器 ## 常见应用场景 @@ -538,3 +680,107 @@ protected T doGetBean(String name, Class requiredType, final Object[] arg 程序到这里返回bean后已经基本结束了,通常对该方法的调用参数requiredType是为空的,但是可能会存在这样的情况,返回的bean其实是个Spring,但是requiredType却传入Integer类型,那么这时候本步骤就会起作用了,它的功能是将返回的bean转换为requiredType所指定的类型,当然,Spring转换为Integer是最简单的一种转换,在Spring中提供了各种各样的转换器,用户也可以自己扩展转换器来满足需求 + + +# BeanFactory 和 ApplicationContext有什么区别? +BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。 + +可以从依赖关系、加载方式、创建方式、注册方式这四方面去讲。 + +## 依赖关系 +BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。 + +ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能: +- 继承MessageSource,因此支持国际化。 +- 统一的资源文件访问方式。 +- 提供在监听器中注册bean的事件。 +- 同时加载多个配置文件。 + +载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。 + +## 加载方式 +BeanFactroy采用的是**延迟加载**形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。 + +ApplicationContext,它是在容器启动时,**一次性创建了所有的Bean**。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。 + +相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。 + +## 创建方式 +BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。 + +## 注册方式 +BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。 + +BeanFactory 简单粗暴,可以理解为就是个 HashMap,Key 是 BeanName,Value 是 Bean 实例。通常只提供注册(put),获取(get)这两个功能。我们可以称之为 “低级容器”。 + +ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口。因此具备了更多的功能。例如资源的获取,支持多种消息(例如 JSP tag 的支持),对 BeanFactory 多了工具级别的支持等待。所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”, 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean。 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/spring-5.png) + + +# 前后端分离跨域解决 +我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。 +``` +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin("*"); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); + urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); + return new CorsFilter(urlBasedCorsConfigurationSource); + } +} +``` +现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。 +``` + @Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowCredentials(true) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .maxAge(3600); + } + +} +``` + + + +# Spring 注解 +## @Component, @Controller, @Repository, @Service +@Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。 + +@Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。 + +@Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。 + +@Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。 + +## @service和@component的区别 +**@Component** +- Component 用于将所标注的类加载到 Spring 环境中,需要搭配 component-scan 使用 +- 泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。 + +**@service** +- @service和@controller引用来了@component注解,而@service是对@component进一步拓展,也就是component注解实现的功能@service都能实现,被@service注解标注的百类会被spring认定是业务逻辑层 + +## @Autowired和@Resource之间的区别 +@Autowired可用于:构造函数、成员变量、Setter方法 + +@Autowired和@Resource之间的区别 +- @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。 +- @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。 + +## @Conditional的使用 +作用:根据条件,决定类是否加载到Spring Ioc容器中,在SpringBoot中有大量的运用 + +应用场景:在一些需要条件满足才是实例化的类中,使用此注解,我曾经在项目中需要根据不同的场景使用不同的mq中间件的时候使用过,在mq的实例化bean上,加上此注解,根据配置文件的不同,来决定这个bean是否加载至ioc容器中。 \ No newline at end of file diff --git a/notes/framework/Tomcat.md b/notes/framework/Tomcat.md new file mode 100644 index 0000000..6d97992 --- /dev/null +++ b/notes/framework/Tomcat.md @@ -0,0 +1,41 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Tomcat是什么?](#tomcat%E6%98%AF%E4%BB%80%E4%B9%88) +- [Tomcat 有哪几种Connector 运行模式(优化)?](#tomcat-%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8Dconnector-%E8%BF%90%E8%A1%8C%E6%A8%A1%E5%BC%8F%E4%BC%98%E5%8C%96) + - [BIO:同步并阻塞](#bio%E5%90%8C%E6%AD%A5%E5%B9%B6%E9%98%BB%E5%A1%9E) + - [NIO:同步非阻塞IO](#nio%E5%90%8C%E6%AD%A5%E9%9D%9E%E9%98%BB%E5%A1%9Eio) + - [APR:即Apache Portable Runtime](#apr%E5%8D%B3apache-portable-runtime) +- [Tomcat顶层架构](#tomcat%E9%A1%B6%E5%B1%82%E6%9E%B6%E6%9E%84) + + + + +## Tomcat是什么? +Tomcat 服务器Apache软件基金会项目中的一个核心项目,是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。 +## Tomcat 有哪几种Connector 运行模式(优化)? +### BIO:同步并阻塞 +一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。Tomcat7或以下,在Linux系统中默认使用这种方式。 +### NIO:同步非阻塞IO +利用Java的异步IO处理,可以通过少量的线程处理大量的请求,可以复用同一个线程处理多个connection(多路复用)。Tomcat8在Linux系统中默认使用这种方式。 +### APR:即Apache Portable Runtime +从操作系统层面解决io阻塞问题。 + +Tomcat有几种部署方式? +Tomcat容器是如何创建servlet类实例?用到了什么原理? +1. 当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对 xml文件进行解析,并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过 反射的方式实例化。(有时候也是在第一次请求时实例化) +2. 在servlet注册时加上1如果为正数,则在一开始就实例化,如果不写或为负数,则第一次请求实例化。 + + +## Tomcat顶层架构 +Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,即可以包含多个Service,用于具体提供服务。 + +Service主要包含两个部分:Connector和Container。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下: +- Connector用于处理连接相关的事情,并提供Socket与Request请求和Response响应相关的转化; +- Container用于封装和管理Servlet,以及具体处理Request请求; + +一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接 + +多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。 + diff --git a/notes/framework/Zookeeper.md b/notes/framework/Zookeeper.md new file mode 100644 index 0000000..7766f79 --- /dev/null +++ b/notes/framework/Zookeeper.md @@ -0,0 +1,285 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [ZooKeeper 是什么?](#zookeeper-%E6%98%AF%E4%BB%80%E4%B9%88) +- [ZooKeeper 设计目标](#zookeeper-%E8%AE%BE%E8%AE%A1%E7%9B%AE%E6%A0%87) + - [简单的数据模型](#%E7%AE%80%E5%8D%95%E7%9A%84%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B) + - [可构建集群](#%E5%8F%AF%E6%9E%84%E5%BB%BA%E9%9B%86%E7%BE%A4) + - [顺序访问](#%E9%A1%BA%E5%BA%8F%E8%AE%BF%E9%97%AE) + - [高性能](#%E9%AB%98%E6%80%A7%E8%83%BD) +- [ZooKeeper 提供了什么?](#zookeeper-%E6%8F%90%E4%BE%9B%E4%BA%86%E4%BB%80%E4%B9%88) +- [Zookeeper 文件系统](#zookeeper-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F) +- [Zookeeper 怎么保证主从节点的状态同步?](#zookeeper-%E6%80%8E%E4%B9%88%E4%BF%9D%E8%AF%81%E4%B8%BB%E4%BB%8E%E8%8A%82%E7%82%B9%E7%9A%84%E7%8A%B6%E6%80%81%E5%90%8C%E6%AD%A5) + - [恢复模式](#%E6%81%A2%E5%A4%8D%E6%A8%A1%E5%BC%8F) + - [广播模式](#%E5%B9%BF%E6%92%AD%E6%A8%A1%E5%BC%8F) +- [四种类型的数据节点 Znode](#%E5%9B%9B%E7%A7%8D%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%95%B0%E6%8D%AE%E8%8A%82%E7%82%B9-znode) + - [持久节点](#%E6%8C%81%E4%B9%85%E8%8A%82%E7%82%B9) + - [临时节点](#%E4%B8%B4%E6%97%B6%E8%8A%82%E7%82%B9) + - [持久顺序节点](#%E6%8C%81%E4%B9%85%E9%A1%BA%E5%BA%8F%E8%8A%82%E7%82%B9) + - [临时顺序节点](#%E4%B8%B4%E6%97%B6%E9%A1%BA%E5%BA%8F%E8%8A%82%E7%82%B9) +- [Zookeeper 的典型应用场景](#zookeeper-%E7%9A%84%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF) +- [Zookeeper的通知机制](#zookeeper%E7%9A%84%E9%80%9A%E7%9F%A5%E6%9C%BA%E5%88%B6) +- [Zookeeper的分布式锁实现方式](#zookeeper%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F) +- [Zookeeper 采用的哪种分布式一致性协议? 还有哪些分布式一致性协议](#zookeeper-%E9%87%87%E7%94%A8%E7%9A%84%E5%93%AA%E7%A7%8D%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE-%E8%BF%98%E6%9C%89%E5%93%AA%E4%BA%9B%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE) + - [ZAD和Paxos算法的联系和区别](#zad%E5%92%8Cpaxos%E7%AE%97%E6%B3%95%E7%9A%84%E8%81%94%E7%B3%BB%E5%92%8C%E5%8C%BA%E5%88%AB) +- [Zookeeper 是怎样保证主从节点的状态同步](#zookeeper-%E6%98%AF%E6%80%8E%E6%A0%B7%E4%BF%9D%E8%AF%81%E4%B8%BB%E4%BB%8E%E8%8A%82%E7%82%B9%E7%9A%84%E7%8A%B6%E6%80%81%E5%90%8C%E6%AD%A5) +- [集群中为什么要有主节点](#%E9%9B%86%E7%BE%A4%E4%B8%AD%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89%E4%B8%BB%E8%8A%82%E7%82%B9) +- [leader 选举过程](#leader-%E9%80%89%E4%B8%BE%E8%BF%87%E7%A8%8B) +- [Zookeeper与Eureka的区别](#zookeeper%E4%B8%8Eeureka%E7%9A%84%E5%8C%BA%E5%88%AB) + - [结构上](#%E7%BB%93%E6%9E%84%E4%B8%8A) + - [高可用与强一致性](#%E9%AB%98%E5%8F%AF%E7%94%A8%E4%B8%8E%E5%BC%BA%E4%B8%80%E8%87%B4%E6%80%A7) + - [Eureka的自我保护机制](#eureka%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6) + + + + +# ZooKeeper 是什么? +ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 + +ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 + +Zookeeper 保证了如下分布式一致性特性: +1. 顺序一致性 +2. 原子性 +3. 单一视图 +4. 可靠性 +5. 实时性(最终一致性) + +客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。 + +有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper 最新的 zxid。 + +# ZooKeeper 设计目标 +## 简单的数据模型 +ZooKeeper 允许分布式进程通过共享的层次结构命名空间进行相互协调,这与标准文件系统类似。 名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Zookeeper-1.png) + +## 可构建集群 +为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。 + +ZooKeeper 官方提供的架构图: + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Zookeeper-2.png) + +上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic Broadcast)来保持数据的一致性。 + +## 顺序访问 +对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper 这个特性来实现更高层次的同步原语。 这个编号也叫做时间戳——zxid(Zookeeper Transaction Id) + +## 高性能 +ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。) + + +# ZooKeeper 提供了什么? +- 文件系统 +- 通知机制 + +# Zookeeper 文件系统 +Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。 + +Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。 + +# Zookeeper 怎么保证主从节点的状态同步? +Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。 + +## 恢复模式 +当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。 + +## 广播模式 +一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。 + + +# 四种类型的数据节点 Znode +## 持久节点 +除非手动删除,否则节点一直存在于 Zookeeper 上 + +## 临时节点 +临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。 + +## 持久顺序节点 +基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。 + +## 临时顺序节点 +基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。 + +# Zookeeper 的典型应用场景 +Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。 + +通过对 Zookeeper 中丰富的数据节点进行交叉使用,配合 Watcher 事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如: +1. 数据发布/订阅 +2. 负载均衡 +3. 命名服务 +4. 分布式协调/通知 +5. 集群管理 +6. Master 选举 +7. 分布式锁 +8. 分布式队列 + + +# Zookeeper的通知机制 +客户端端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些客户端会收到 zookeeper 的通知,然后客户端可以根据 znode 变化来做出业务上的改变。 + +# Zookeeper的分布式锁实现方式 +在讲zk分布锁之前,先看下zookeeper中几个关于节点的有趣的性质: +1. 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。 +2. 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。 +3. 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件: +1、节点创建;2、节点删除;3、节点数据修改;4、子节点变更。 + +下面描述使用zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock: + +1. 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。 +2. 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁; +3. 执行业务代码; +4. 完成业务流程后,删除对应的子节点释放锁。 + +# Zookeeper 采用的哪种分布式一致性协议? 还有哪些分布式一致性协议 + +zab协议是zookeeper专门设计的支持崩溃恢复的原子广播协议。目的是实现分布式zoopkeeper各个节点数据一致性。 + +zab协议约定zk节点有两种角色leader和follower,zk客户端会随机的链接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向 Leader 提交事务,Leader 接收到事务提交,会广播该事务,只要超过半数节点写入成功,该事务就会被提交。 + +ZAB协议包括两种基本的模式:消息广播和崩溃恢复。 + +整个 Zookeeper 就是在这两个模式之间切换。 简而言之,当 Leader 服务可以正常使用,就进入消息广播模式,当 Leader 不可用时,则进入崩溃恢复模式。 + +以上其实大致经历了三个步骤: +1. 崩溃恢复:主要就是Leader选举过程。 +2. 数据同步:Leader服务器与其他服务器进行数据同步。 +3. 消息广播:Leader服务器将数据发送给其他服务器。 + +支持崩溃恢复后数据准确性的就是数据同步了,数据同步基于事务的 ZXID 的唯一性来保证。通过 + 1 操作可以辨别事务的先后顺序。 + +## ZAD和Paxos算法的联系和区别 +共同点: +1. 两者都存在一个类似于Leader进程的角色,由其负责协调多个Follow进程的运行。 +2. Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。 +3. 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前Leader周期,在Paxos算法中,同样存在这样一个标识,只是名字变成了Ballot。 +不同点: + +Paxos算法中,一个新的选举产生的主进程会进行两个阶段的工作 +1. 读阶段,新的主进程会通过和所有其他进程进行通信的方式来搜集上一个主进程提出的提案,并将它们提交。 +2. 写阶段,当前主进程开始提出它自己的提案。 +3. ZAB在Paxos基础上额外添加一个同步阶段。同步阶段之前,ZAB协议存在一个和Paxos读阶段类似的发现(Discovery)阶段 + +同步阶段中,新的Leader会确保存在过半的Follower已经提交了之前Leader周期中的所有事务Proposal +- 发现阶段的存在,确保所有进程都已经完成对之前所有事物Proposal的提交 +- ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper,Paxos算法则是用于构建一个分布式的一致性状态机系统 + +# Zookeeper 是怎样保证主从节点的状态同步 +Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。 + +恢复模式:当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。 + +因此,选主得到的leader保证了同步状态的进行,状态同步又保证了leader和Server具有相同的系统状态,当leader失去主权后可以在其他follower中选主新的leader。 + +# 集群中为什么要有主节点 +在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行, + +其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,所以就需要主节点 + + +# leader 选举过程 +有两种情况会发起Leader选举: +1. 服务器启动的时候 +2. 服务器运行的时候当Leader宕机 + + +在讲解流程之前,先说明一下选举流程中涉及到的角色: + +- LOOKING:寻找Leader状态,处于该状态需要进入选举流程(只有该节点才可以投票) +- LEADING:领导者状态,处于该状态的节点说明是角色已经是Leader +- FOLLOWING:跟随者状态,表示Leader已经选举出来,当前节点角色是follower +- OBSERVER:观察者状态,表明当前节点角色是observer(该节点不参与竞选) + + + +三个核心选举原则: + +1. Zookeeper集群中只有超过半数以上的服务器启动,集群才能正常工作; +2. 在集群正常工作之前,myid小的服务器给myid大的服务器投票,直到集群正常工作,选出Leader; +3. 选出Leader之后,之前的服务器状态由Looking改变为Following,以后的服务器都是Follower。 + + + +下面以一个简单的例子来说明整个选举的过程: + +假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Zookeeper-2.png) + +假设这些服务器从id1-5,依序启动: + +因为一共5台服务器,只有超过半数以上,即最少启动3台服务器,集群才能正常工作。 + +**(1)服务器1启动,发起一次选举。** +服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成; + +服务器1状态保持为LOOKING; + +**(2)服务器2启动,再发起一次选举。** + +服务器1和2分别投自己一票,此时服务器1发现服务器2的id比自己大,更改选票投给服务器2; + +此时服务器1票数0票,服务器2票数2票,不够半数以上(3票),选举无法完成; + +服务器1,2状态保持LOOKING; + +**(3)服务器3启动,发起一次选举。** + +与上面过程一样,服务器1和2先投自己一票,然后因为服务器3id最大,两者更改选票投给为服务器3; + +此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数(3票),服务器3当选Leader。 + +服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING; + +**(4)服务器4启动,发起一次选举。** + +此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。 + +此时服务器4服从多数,更改选票信息为服务器3; + +服务器4并更改状态为FOLLOWING; + +**(5)服务器5启动,同4一样投票给3,此时服务器3一共5票,服务器5为0票;** + +服务器5并更改状态为FOLLOWING; + +**(6)选举结果** + +最终Leader是服务器3,状态为LEADING; + +其余服务器是Follower,状态为FOLLOWING。 + +# Zookeeper与Eureka的区别 +## 结构上 +1、Zookeeper是主从架构 +2、Eureka是点对点,节点都是平级的 + +## 高可用与强一致性 +1、Zookeeper当master挂了,会在30-120s进行leader选举,这点类似于redis的哨兵机制,在选举期间Zookeeper是不可用的,这么长时间不能进行服务注册,是无法忍受的,别说30s,5s都不能忍受。这时Zookeeper集群会瘫痪,这也是Zookeeper的CP,保持节点的一致性,牺牲了A/高可用。而Eureka不会,,这就是AP,牺牲了C/一致性。 + +2、即使Eureka有部分挂掉,还有其他节点可以使用的,他们保持平级的关系,只不过信息有可能不一致。 + +当坏掉的服务恢复的时候,会自动加入到节点上,也是高可用的一种。然后退出自我保护机制,这也是应对网络异常的一种机制 + +## Eureka的自我保护机制 +在默认配置中,Eureka Server在默认90s没有得到客户端的心跳,则注销该实例,但是往往因为微服务跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,但是因为网络分区故障时,Eureka Server注销服务实例则会让大部分微服务不可用,这很危险,因为服务明明没有问题。 + +为了解决这个问题,Eureka 有自我保护机制,通过在Eureka Server配置如下参数,可启动保护机制 +``` +eureka.server.enable-self-preservation=true +``` + +如果在**15分钟内超过85%的节点**都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了**网络故障**,此时会出现以下几种情况: +1. Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务 +2. Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用) +3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中 + +它的原理是,**当Eureka Server节点在短时间内丢失过多的客户端时(可能发送了网络故障),那么这个节点将进入自我保护模式,不再注销任何微服务,当网络故障恢复后,该节点会自动退出自我保护模式**。 + +自我保护模式的架构哲学是宁可放过一个,决不可错杀一千 + + diff --git a/notes/framework/netty.md b/notes/framework/netty.md new file mode 100644 index 0000000..36e55c5 --- /dev/null +++ b/notes/framework/netty.md @@ -0,0 +1,423 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [1、IO和NIO](#1io%E5%92%8Cnio) + - [面向流和面向Buffer](#%E9%9D%A2%E5%90%91%E6%B5%81%E5%92%8C%E9%9D%A2%E5%90%91buffer) + - [选择器](#%E9%80%89%E6%8B%A9%E5%99%A8) + - [区别](#%E5%8C%BA%E5%88%AB) +- [2、JDK原生NIO程序的问题](#2jdk%E5%8E%9F%E7%94%9Fnio%E7%A8%8B%E5%BA%8F%E7%9A%84%E9%97%AE%E9%A2%98) +- [3、Netty的介绍](#3netty%E7%9A%84%E4%BB%8B%E7%BB%8D) + - [Netty的主要特点](#netty%E7%9A%84%E4%B8%BB%E8%A6%81%E7%89%B9%E7%82%B9) + - [1、高性能](#1%E9%AB%98%E6%80%A7%E8%83%BD) + - [2、可靠性](#2%E5%8F%AF%E9%9D%A0%E6%80%A7) + - [3、可定制性](#3%E5%8F%AF%E5%AE%9A%E5%88%B6%E6%80%A7) + - [4、可扩展性](#4%E5%8F%AF%E6%89%A9%E5%B1%95%E6%80%A7) +- [4、Netty的线程模型](#4netty%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B) + - [1、串行化处理模型](#1%E4%B8%B2%E8%A1%8C%E5%8C%96%E5%A4%84%E7%90%86%E6%A8%A1%E5%9E%8B) + - [2、并行化处理模型](#2%E5%B9%B6%E8%A1%8C%E5%8C%96%E5%A4%84%E7%90%86%E6%A8%A1%E5%9E%8B) + - [3、Netty具体线程模型](#3netty%E5%85%B7%E4%BD%93%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B) +- [5、Netty工作原理](#5netty%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) + - [1、server端工作原理](#1server%E7%AB%AF%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) + - [2、client端工作原理](#2client%E7%AB%AF%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) +- [6、netty的启动](#6netty%E7%9A%84%E5%90%AF%E5%8A%A8) + - [1、服务端启动流程](#1%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B) + - [2、客户端启动流程](#2%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B) +- [7、ByteBuf](#7bytebuf) + - [ByteBuf和ByteBuffer的区别](#bytebuf%E5%92%8Cbytebuffer%E7%9A%84%E5%8C%BA%E5%88%AB) + - [ByteBuf和设计模式](#bytebuf%E5%92%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F) + - [1、ByteBufAllocator - 抽象工厂模式](#1bytebufallocator---%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F) + - [2、CompositeByteBuf - 组合模式](#2compositebytebuf---%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F) + - [3、ByteBufInputStream - 适配器模式](#3bytebufinputstream---%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F) + - [4、ReadOnlyByteBuf - 装饰器模式](#4readonlybytebuf---%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F) + - [5、ByteBuf - 工厂方法模式](#5bytebuf---%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F) +- [9、channelHandler](#9channelhandler) + - [事件的传播](#%E4%BA%8B%E4%BB%B6%E7%9A%84%E4%BC%A0%E6%92%AD) +- [10、NioEventLoop](#10nioeventloop) + - [Selector BUG出现的原因](#selector-bug%E5%87%BA%E7%8E%B0%E7%9A%84%E5%8E%9F%E5%9B%A0) + - [Netty的解决办法](#netty%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95) +- [11、通信协议编解码](#11%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE%E7%BC%96%E8%A7%A3%E7%A0%81) +- [12、Netty内存池和对象池](#12netty%E5%86%85%E5%AD%98%E6%B1%A0%E5%92%8C%E5%AF%B9%E8%B1%A1%E6%B1%A0) +- [13、心跳与空闲检测](#13%E5%BF%83%E8%B7%B3%E4%B8%8E%E7%A9%BA%E9%97%B2%E6%A3%80%E6%B5%8B) + - [服务端空闲检测](#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%A9%BA%E9%97%B2%E6%A3%80%E6%B5%8B) + - [客户端定时心跳](#%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%AE%9A%E6%97%B6%E5%BF%83%E8%B7%B3) +- [14、拆包粘包理论与解决](#14%E6%8B%86%E5%8C%85%E7%B2%98%E5%8C%85%E7%90%86%E8%AE%BA%E4%B8%8E%E8%A7%A3%E5%86%B3) +- [15、Netty 自带的拆包器](#15netty-%E8%87%AA%E5%B8%A6%E7%9A%84%E6%8B%86%E5%8C%85%E5%99%A8) + - [1、固定长度的拆包器 FixedLengthFrameDecoder](#1%E5%9B%BA%E5%AE%9A%E9%95%BF%E5%BA%A6%E7%9A%84%E6%8B%86%E5%8C%85%E5%99%A8-fixedlengthframedecoder) + - [2、行拆包器 LineBasedFrameDecoder](#2%E8%A1%8C%E6%8B%86%E5%8C%85%E5%99%A8-linebasedframedecoder) + - [3、分隔符拆包器 DelimiterBasedFrameDecoder](#3%E5%88%86%E9%9A%94%E7%AC%A6%E6%8B%86%E5%8C%85%E5%99%A8-delimiterbasedframedecoder) + - [4、基于长度域拆包器 LengthFieldBasedFrameDecoder](#4%E5%9F%BA%E4%BA%8E%E9%95%BF%E5%BA%A6%E5%9F%9F%E6%8B%86%E5%8C%85%E5%99%A8-lengthfieldbasedframedecoder) +- [16、预留问题](#16%E9%A2%84%E7%95%99%E9%97%AE%E9%A2%98) + + + +## 1、IO和NIO +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-1.png) +### 面向流和面向Buffer +传统IO和Java NIO最大的区别是传统的IO是面向流,NIO是面向Buffer + +Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 + +Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 +### 选择器 +Java NIO的选择器允许一个单独的线程来 监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道,这些通道里已经有可以处理的输入,或者选择已经准备写入的通道,这种选择机制,使得一个单独的线程很容易来管理多个通道 + +### 区别 +传统的IO +- socketServer的accept方法是阻塞的; +- 获得连接的顺序是和客户端请求到达服务器的先后顺序相关; +- 适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的; +- 适合需要管理同时打开不太多的连接,这些连接会发送大量的数据 + +NIO +- 基于事件驱动,当有连接请求,会将此连接注册到多路复用器上(selector); +- 在多路复用器上可以注册监听事件,比如监听accept、read; +- 通过监听,当真正有请求数据时,才来处理数据; +- 会不停的轮询是否有就绪的事件,所以处理顺序和连接请求先后顺序无关,与请求数据到来的先后顺序有关; +- 优势在于一个线程管理多个通道;但是数据的处理将会变得复杂; +- 适合需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据 + + +## 2、JDK原生NIO程序的问题 +JDK 原生也有一套网络应用程序 API,但是存在一系列问题,主要如下: +1. NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。 +2. 需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。 +3. 可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。 +4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。 +## 3、Netty的介绍 +Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。 + +Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。 + +### Netty的主要特点 + +#### 1、高性能 +1. 采用异步非阻塞的IO类库,基于Reactor模式实现,解决了传统同步阻塞IO模式 +2. TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了IO读取和写入的性能 +3. 支持内存池的方式循环利用ByteBuf,避免了频繁插件和销毁ByteBuf带来的性能消耗 +4. 可配置的IO线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景 +5. 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全或锁。 +6. 合理使用线程安全容器,原子类,提升系统的并发处理能力 +7. 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和cpu资源消耗 +8. 通过引用计数法及时地申请释放不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗 +#### 2、可靠性 +1、 链路有效监测(心跳和空闲检测) +- 读空闲超时机制 +- 写空闲超时机制 + +2、内存保护机制 +- 通过对象引用计数法对Netty的ByteBuf等内置对象进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护 +- 通过内存池来重用ByteBuf,节省内存 +- 可设置的内存容量上限,包括ByteBuf、线程池线程数 +3、优雅停机 +- 优雅停机需要设置最大超时时间,如果达到该时间系统还没退出,则通过Kill -9 pid强杀当前线程。 +- JVM通过注册的Shutdown Hook拦截到退出信号量,然后执行退出操作 +#### 3、可定制性 +1. 责任链模式:channelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展 +2. 基于接口的开发:关键的类库都提供了接口或者抽象类,用户可以自定义实现相关接口 +3. 提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象 +4. 提供大量的系统参数供用户按需设置,增强系统的场景定制 +#### 4、可扩展性 +可以方便进行应用层协议定制,比如Dubbo、RocketMQ + + + +## 4、Netty的线程模型 +对于网络请求一般可以分为两个处理阶段,一是接收请求任务,二是处理网络请求。根据不同阶段处理方式分为以下几种线程模型: +### 1、串行化处理模型 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-2.png) +这个模型中用一个线程来处理网络请求连接和任务处理,当worker接受到一个任务之后,就立刻进行处理,也就是说任务接受和任务处理是在同一个worker线程中进行的,没有进行区分。这样做存在一个很大的问题是,必须要等待某个task处理完成之后,才能接受处理下一个task。 + +因此可以把接收任务和处理任务两个阶段分开处理,一个线程接收任务,放入任务队列,另外的线程异步处理任务队列中的任务。 +### 2、并行化处理模型 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-3.png) + +由于任务处理一般比较缓慢,会导致任务队列中任务积压长时间得不到处理,这时可以使用线程池来处理。可以通过为每个线程维护一个任务队列来改进这种模型。 +### 3、Netty具体线程模型 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-4.png) + +1、如何理解NioEventLoop和NioEventLoopGroup +- NioEventLoop实际上就是工作线程,可以直接理解为一个线程。NioEventLoopGroup是一个线程池,线程池中的线程就是NioEventLoop。 +- 实际上bossGroup中有多个NioEventLoop线程,每个NioEventLoop绑定一个端口,也就是说,如果程序只需要监听1个端口的话,bossGroup里面只需要有一个NioEventLoop线程就行了。 + +2、每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selecotr在监听IO就绪事件。而Channel注册到Selector。 + +3、一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。 + +4、一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行检查。 + +## 5、Netty工作原理 +### 1、server端工作原理 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-5.png) +server端启动时绑定本地某个端口,将自己NioServerSocketChannel注册到某个boss NioEventLoop的selector上。 + +server端包含1个boss NioEventLoopGroup和1个worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。 + +每个boss NioEventLoop循环执行的任务包含3步: +1. 轮询accept事件; +2. 处理io任务,即accept事件,与client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个worker NioEventLoop的selector上; +3. 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。 + +每个worker NioEventLoop循环执行的任务包含3步: +1. 轮询read、write事件; +2. 处理io任务,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理; +3. 处理任务队列中的任务,runAllTasks。 + + +### 2、client端工作原理 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-6.png) +client端启动时connect到server,建立NioSocketChannel,并注册到某个NioEventLoop的selector上。 + +client端只包含1个NioEventLoopGroup,每个NioEventLoop循环执行的任务包含3步: +1. 轮询connect、read、write事件; +2. 处理io任务,即connect、read、write事件,在NioSocketChannel连接建立、可读、可写事件发生时进行处理; +3. 处理非io任务,runAllTasks。 + +## 6、netty的启动 +### 1、服务端启动流程 +```java +public class NettyServer { + public static void main(String[] args) { + NioEventLoopGroup bossGroup = new NioEventLoopGroup(); + NioEventLoopGroup workerGroup = new NioEventLoopGroup(); + + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap + .group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + protected void initChannel(NioSocketChannel ch) { + } + }); + + serverBootstrap.bind(8000); + } +} +``` +1. 首先创建了两个NioEventLoopGroup,这两个对象可以看做是传统IO编程模型的两大线程组,bossGroup表示监听端口,accept 新连接的线程组,workerGroup表示处理每一条连接的数据读写的线程组。 +2. 接下来创建了一个引导类 ServerBootstrap,这个类将引导我们进行服务端的启动工作,直接new出来开搞。 +3. 通过.group(bossGroup, workerGroup)给引导类配置两大线程组,这个引导类的线程模型也就定型了。 +4. 然后指定服务端的 IO 模型为NIO,我们通过.channel(NioServerSocketChannel.class)来指定 IO 模型。 +5. 最后我们调用childHandler()方法,给这个引导类创建一个ChannelInitializer,这里主要就是定义后续每条连接的数据读写,业务处理逻辑。ChannelInitializer这个类中,我们注意到有一个泛型参数NioSocketChannel,这个类是 Netty 对 NIO 类型的连接的抽象,而我们前面NioServerSocketChannel也是对 NIO 类型的连接的抽象,NioServerSocketChannel和NioSocketChannel的概念可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上 + +总结:创建一个引导类,然后给他指定线程模型,IO模型,连接读写处理逻辑,绑定端口之后,服务端就启动起来了。 + +### 2、客户端启动流程 +对于客户端的启动来说,和服务端的启动类似,依然需要线程模型、IO 模型,以及 IO 业务处理逻辑三大参数 +```java +public class NettyClient { + public static void main(String[] args) { + NioEventLoopGroup workerGroup = new NioEventLoopGroup(); + + Bootstrap bootstrap = new Bootstrap(); + bootstrap + // 1.指定线程模型 + .group(workerGroup) + // 2.指定 IO 类型为 NIO + .channel(NioSocketChannel.class) + // 3.IO 处理逻辑 + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + } + }); + // 4.建立连接 + bootstrap.connect("juejin.im", 80).addListener(future -> { + if (future.isSuccess()) { + System.out.println("连接成功!"); + } else { + System.err.println("连接失败!"); + } + + }); + } +} +``` +1. 首先,与服务端的启动一样,需要给它指定线程模型,驱动着连接的数据读写 +2. 然后指定 IO 模型为 NioSocketChannel,表示 IO 模型为 NIO +3. 接着给引导类指定一个 handler,这里主要就是定义连接的业务处理逻辑 +4. 配置完线程模型、IO 模型、业务处理逻辑之后,调用 connect 方法进行连接,可以看到 connect 方法有两个参数,第一个参数可以填写 IP 或者域名,第二个参数填写的是端口号,由于 connect 方法返回的是一个 Future,也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息 + +总结:创建一个引导类,然后给他指定线程模型,IO 模型,连接读写处理逻辑,连接上特定主机和端口,客户端就启动起来了 +## 7、ByteBuf +ByteBuf是一个节点容器,里面数据包括三部分: +1. 已经丢弃的数据,这部分数据是无效的 +2. 可读字节,这部分数据是ByteBuf的主体 +3. 可写字节 +这三段数据被两个指针给划分出来,读指针、写指针。 + +ByteBuf 本质上就是,它引用了一段内存,这段内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对 ByteBuf 的读写,可以理解为是外观模式的一种使用 + +基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法,要注意 read/write 与 get/set 的区别 + +多个 ByteBuf 可以引用同一段内存,通过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则 +### ByteBuf和ByteBuffer的区别 +1. 可扩展到用户定义的buffer类型中 +2. 通过内置的复合buffer类型实现透明的零拷贝(zero-copy) +3. 容量可以根据需要扩展 +4. 切换读写模式不需要调用ByteBuffer.flip()方法 +5. 读写采用不同的索引 +6. 支持方法链接调用 +7. 支持引用计数 +8. 支持池技术(比如:线程池、数据库连接池) +### ByteBuf和设计模式 +#### 1、ByteBufAllocator - 抽象工厂模式 +在Netty的世界里,ByteBuf实例通常应该由ByteBufAllocator来创建。 +#### 2、CompositeByteBuf - 组合模式 +CompositeByteBuf可以让我们把多个ByteBuf当成一个大Buf来处理,ByteBufAllocator提供了compositeBuffer()工厂方法来创建CompositeByteBuf。CompositeByteBuf的实现使用了组合模式 +#### 3、ByteBufInputStream - 适配器模式 +ByteBufInputStream使用适配器模式,使我们可以把ByteBuf当做Java的InputStream来使用。同理,ByteBufOutputStream允许我们把ByteBuf当做OutputStream来使用。 +#### 4、ReadOnlyByteBuf - 装饰器模式 +ReadOnlyByteBuf用适配器模式把一个ByteBuf变为只读,ReadOnlyByteBuf通过调用Unpooled.unmodifiableBuffer(ByteBuf)方法获得: +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-9.png) +#### 5、ByteBuf - 工厂方法模式 +我们很少需要直接通过构造函数来创建ByteBuf实例,而是通过Allocator来创建。从装饰器模式可以看出另外一种获得ByteBuf的方式是调用ByteBuf的工厂方法,比如: +- ByteBuf#duplicate() +- ByteBuf#slice() + + +## 9、channelHandler +channelHandler在只会对感兴趣的事件进行拦截和处理,Servlet的Filter过滤器,负责对IO事件或者IO操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。 + +pipeline与channelHandler它们通过责任链设计模式来组织代码逻辑,并且支持逻辑的动态添加和删除。 + +ChannelHandler 有两大子接口: +- 第一个子接口是 ChannelInboundHandler,从字面意思也可以猜到,他是处理读数据的逻辑 +- 第二个子接口 ChannelOutBoundHandler 是处理写数据的逻辑 + +这两个子接口分别有对应的默认实现,ChannelInboundHandlerAdapter,和 ChanneloutBoundHandlerAdapter,它们分别实现了两大接口的所有功能,默认情况下会把读写事件传播到下一个 handler。 + +### 事件的传播 +AbstractChannel直接调用了Pipeline的write()方法,因为write是个outbound事件,所以DefaultChannelPipeline直接找到tail部分的context,调用其write()方法: + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-10.png) + +context的write()方法沿着context链往前找,直至找到一个outbound类型的context为止,然后调用其invokeWrite()方法: + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-11.png) +## 10、NioEventLoop +NioEventLoop除了要处理IO事件,还有主要: +1. 非IO操作的系统Task +2. 定时任务 +非IO操作和IO操作各占默认值50%,底层使用Selector(多路复用器) + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-8.png) + + +### Selector BUG出现的原因 +若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%, +### Netty的解决办法 +- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数, +- 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。 +- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。 + + +## 11、通信协议编解码 +通信协议是为了服务端与客户端交互,双方协商出来的满足一定规则的二进制格式 +1. 客户端把一个Java对象按照通信协议转换成二进制数据包 +2. 把二进制数据包发送到服务端,数据的传输油TCP/IP协议负责 +3. 服务端收到二进制数据包后,按照通信协议,包装成Java对象。 + +通信协议的设计 +1. 魔数,作用:能够在第一时间识别出这个数据包是不是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。 +2. 版本号 +3. 序列化算法 +4. 指令 +5. 数据长度 +6. 数据 + + +## 12、Netty内存池和对象池 +1、内存池是指为了实现内存池的功能,设计一个内存结构Chunk,其内部管理着一个大块的连续内存区域,将这个内存区域切分成均等的大小,每一个大小称之为一个Page。将从内存池中申请内存的动作映射为从Chunk中申请一定数量Page。为了方便计算和申请Page,Chunk内部采用完全二叉树的方式对Page进行管理。 + +2、对象池是指Recycler整个对象池的核心实现由ThreadLocal和Stack及WrakOrderQueue构成,接着来看Stack和WrakOrderQueue的具体实现,最后概括整体实现。 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/netty-7.png) +整个设计上核心的几点: +1. Stack相当于是一级缓存,同一个线程内的使用和回收都将使用一个Stack +2. 每个线程都会有一个自己对应的Stack,如果回收的线程不是Stack的线程,将元素放入到Queue中 +3. 所有的Queue组合成一个链表,Stack可以从这些链表中回收元素(实现了多线程之间共享回收的实例) + +## 13、心跳与空闲检测 +连接假死的现象是:在某一端(服务端或者客户端)看来,底层的 TCP 连接已经断开了,但是应用程序并没有捕获到,因此会认为这条连接仍然是存在的,从 TCP 层面来说,只有收到四次握手数据包或者一个 RST 数据包,连接的状态才表示已断开。 + +假死导致两个问题 +1. 对于服务端,每条连接都会耗费cpu和内存资源,大量假死的连接会耗光服务器的资源 +2. 对于客户端,假死会造成发送数据超时,影响用户体验 + +通常,连接假死由以下几个原因造成的 +1. 应用程序出现线程堵塞,无法进行数据的读写。 +2. 客户端或者服务端网络相关的设备出现故障,比如网卡,机房故障。 +3. 公网丢包。公网环境相对内网而言,非常容易出现丢包,网络抖动等现象,如果在一段时间内用户接入的网络连续出现丢包现象,那么对客户端来说数据一直发送不出去,而服务端也是一直收不到客户端来的数据,连接就一直耗着 + +### 服务端空闲检测 +1. 如果能一直收到客户端发来的数据,那么可以说明这条连接还是活的,因此,服务端对于连接假死的应对策略就是空闲检测。 +2. 简化一下,我们的服务端只需要检测一段时间内,是否收到过客户端发来的数据即可,Netty 自带的 IdleStateHandler 就可以实现这个功能。 + +IdleStateHandler 的构造函数有四个参数,其中第一个表示读空闲时间,指的是在这段时间内如果没有数据读到,就表示连接假死;第二个是写空闲时间,指的是 在这段时间如果没有写数据,就表示连接假死;第三个参数是读写空闲时间,表示在这段时间内如果没有产生数据读或者写,就表示连接假死。写空闲和读写空闲为0,表示我们不关心者两类条件;最后一个参数表示时间单位。在我们的例子中,表示的是:如果 15 秒内没有读到数据,就表示连接假死。 + +在一段时间之内没有读到客户端的数据,是否一定能判断连接假死呢?并不能为了防止服务端误判,我们还需要在客户端做点什么。 + +### 客户端定时心跳 +服务端在一段时间内没有收到客户端的数据有两种情况 +1. 连接假死 +2. 非假死确实没数据发 + +所以我们要排除第二种情况就能保证连接自然就是假死的,定期发送心跳到服务端 + +实现了每隔 5 秒,向服务端发送一个心跳数据包,这个时间段通常要比服务端的空闲检测时间的一半要短一些,我们这里直接定义为空闲检测时间的三分之一,主要是为了排除公网偶发的秒级抖动。 + +为了排除是否是因为服务端在非假死状态下确实没有发送数据,服务端也要定期发送心跳给客户端。 + +## 14、拆包粘包理论与解决 +TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包的问题。 + +解决方法 +1. 解决思路是在封装自己的包协议:包=包内容长度(4byte)+包内容 +2. 对于粘包问题先读出包头即包体长度n,然后再读取长度为n的包内容,这样数据包之间的边界就清楚了。 +3. 对于断包问题先读出包头即包体长度n,由于此次读取的缓存区长度小于n,这时候就需要先缓存这部分的内容,等待下次read事件来时拼接起来形成完整的数据包。 + +## 15、Netty 自带的拆包器 +### 1、固定长度的拆包器 FixedLengthFrameDecoder +如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。 +### 2、行拆包器 LineBasedFrameDecoder +从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。 + +### 3、分隔符拆包器 DelimiterBasedFrameDecoder +DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。 +### 4、基于长度域拆包器 LengthFieldBasedFrameDecoder +这种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。 + +## 16、预留问题 +默认情况下,Netty服务端启动多少个线程?何时启动? + +Netty是如何解决jdk空轮询bug? + +Netty如何保证异步串行无锁? + +Netty是在哪里检测有新连接接入的? + +答:Boss线程通过服务端Channel绑定的Selector轮询OP_ACCEPT事件,通过JDK底层Channel的accept()方法获取JDK底层SocketChannel创建新连接 + +新连接是怎样注册到NioEventLoop线程的? + +答:Worker线程调用Chooser的next()方法选择获取NioEventLoop绑定到客户端Channel,使用doRegister()方法将新连接注册到NioEventLoop的Selector上面 + +Netty是如何判断ChannelHandler类型的? + +对于ChannelHandler的添加应遵循什么顺序? + +用户手动触发事件传播,不同触发方式的区别? + +Netty内存类别有哪些? + +如何减少多线程内存分配之间的竞争? + +不同大小的内存是如何进行分配的? + +解码器抽象的解码过程是什么样的? + +Netty里面有哪些拆箱即用的解码器? + +如何把对象变成字节流,最终写到Socket底层? + +Netty内存泄漏问题怎么解决? diff --git "a/notes/java/Java\345\237\272\347\241\200.md" "b/notes/java/Java\345\237\272\347\241\200.md" index 5b1aeee..0c8691b 100644 --- "a/notes/java/Java\345\237\272\347\241\200.md" +++ "b/notes/java/Java\345\237\272\347\241\200.md" @@ -2,8 +2,25 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [JAVA开发六大原则](#java%E5%BC%80%E5%8F%91%E5%85%AD%E5%A4%A7%E5%8E%9F%E5%88%99) - [抽象类和接口的对比](#%E6%8A%BD%E8%B1%A1%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%AF%B9%E6%AF%94) +- [如何去设计类和接口(Effective Java)](#%E5%A6%82%E4%BD%95%E5%8E%BB%E8%AE%BE%E8%AE%A1%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3effective-java) + - [1、使类和成员的可访问性最小化](#1%E4%BD%BF%E7%B1%BB%E5%92%8C%E6%88%90%E5%91%98%E7%9A%84%E5%8F%AF%E8%AE%BF%E9%97%AE%E6%80%A7%E6%9C%80%E5%B0%8F%E5%8C%96) + - [2、复合优先于继承](#2%E5%A4%8D%E5%90%88%E4%BC%98%E5%85%88%E4%BA%8E%E7%BB%A7%E6%89%BF) + - [3、接口优于抽象类](#3%E6%8E%A5%E5%8F%A3%E4%BC%98%E4%BA%8E%E6%8A%BD%E8%B1%A1%E7%B1%BB) + - [4、优先考虑静态成员类](#4%E4%BC%98%E5%85%88%E8%80%83%E8%99%91%E9%9D%99%E6%80%81%E6%88%90%E5%91%98%E7%B1%BB) +- [三大特性](#%E4%B8%89%E5%A4%A7%E7%89%B9%E6%80%A7) + - [多态](#%E5%A4%9A%E6%80%81) + - [重载](#%E9%87%8D%E8%BD%BD) +- [使用final的意义](#%E4%BD%BF%E7%94%A8final%E7%9A%84%E6%84%8F%E4%B9%89) +- [四大引用](#%E5%9B%9B%E5%A4%A7%E5%BC%95%E7%94%A8) +- [值传递和引用传递的区别?](#%E5%80%BC%E4%BC%A0%E9%80%92%E5%92%8C%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92%E7%9A%84%E5%8C%BA%E5%88%AB) + - [值传递](#%E5%80%BC%E4%BC%A0%E9%80%92) + - [引用传递](#%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92) - [equals()](#equals) + - [==和equals的区别?实现equals要注意哪些东西?](#%E5%92%8Cequals%E7%9A%84%E5%8C%BA%E5%88%AB%E5%AE%9E%E7%8E%B0equals%E8%A6%81%E6%B3%A8%E6%84%8F%E5%93%AA%E4%BA%9B%E4%B8%9C%E8%A5%BF) + - [equals()与hashCode()之间的关系](#equals%E4%B8%8Ehashcode%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB) +- [一个字符(英文字母)占多少个字节,一个中文占多少字节?](#%E4%B8%80%E4%B8%AA%E5%AD%97%E7%AC%A6%E8%8B%B1%E6%96%87%E5%AD%97%E6%AF%8D%E5%8D%A0%E5%A4%9A%E5%B0%91%E4%B8%AA%E5%AD%97%E8%8A%82%E4%B8%80%E4%B8%AA%E4%B8%AD%E6%96%87%E5%8D%A0%E5%A4%9A%E5%B0%91%E5%AD%97%E8%8A%82) - [java中double和float精度丢失问题及解决方法](#java%E4%B8%ADdouble%E5%92%8Cfloat%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E9%97%AE%E9%A2%98%E5%8F%8A%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95) - [BigDecimal](#bigdecimal) - [注解](#%E6%B3%A8%E8%A7%A3) @@ -15,8 +32,10 @@ - [创建一个类的几种方法?](#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%B1%BB%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E6%B3%95) - [Redirect和forward](#redirect%E5%92%8Cforward) - [Object跟这些标记符代表的java类型有啥区别呢?](#object%E8%B7%9F%E8%BF%99%E4%BA%9B%E6%A0%87%E8%AE%B0%E7%AC%A6%E4%BB%A3%E8%A1%A8%E7%9A%84java%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB%E5%91%A2) -- [Java 异常的体系结构](#java-%E5%BC%82%E5%B8%B8%E7%9A%84%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84) -- [throw和throws区别](#throw%E5%92%8Cthrows%E5%8C%BA%E5%88%AB) +- [Java 异常](#java-%E5%BC%82%E5%B8%B8) + - [throw和throws区别](#throw%E5%92%8Cthrows%E5%8C%BA%E5%88%AB) + - [什么情况finally不会执行](#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5finally%E4%B8%8D%E4%BC%9A%E6%89%A7%E8%A1%8C) + - [finally方法一定会被执行么?](#finally%E6%96%B9%E6%B3%95%E4%B8%80%E5%AE%9A%E4%BC%9A%E8%A2%AB%E6%89%A7%E8%A1%8C%E4%B9%88) - [.class 文件是什么类型文件](#class-%E6%96%87%E4%BB%B6%E6%98%AF%E4%BB%80%E4%B9%88%E7%B1%BB%E5%9E%8B%E6%96%87%E4%BB%B6) - [java中序列化之子类继承父类序列化](#java%E4%B8%AD%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B%E5%AD%90%E7%B1%BB%E7%BB%A7%E6%89%BF%E7%88%B6%E7%B1%BB%E5%BA%8F%E5%88%97%E5%8C%96) - [标识符](#%E6%A0%87%E8%AF%86%E7%AC%A6) @@ -26,20 +45,30 @@ - [Java中wait 和sleep 方法比较](#java%E4%B8%ADwait-%E5%92%8Csleep-%E6%96%B9%E6%B3%95%E6%AF%94%E8%BE%83) - [hashCode和equals方法的关系](#hashcode%E5%92%8Cequals%E6%96%B9%E6%B3%95%E7%9A%84%E5%85%B3%E7%B3%BB) - [Object类中有哪些方法](#object%E7%B1%BB%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E6%B3%95) -- [String s=new String("xyz")究竟创建String Object分为两种情况](#string-snew-stringxyz%E7%A9%B6%E7%AB%9F%E5%88%9B%E5%BB%BAstring-object%E5%88%86%E4%B8%BA%E4%B8%A4%E7%A7%8D%E6%83%85%E5%86%B5) +- [String](#string) + - [String s=new String("xyz")究竟创建String Object分为两种情况](#string-snew-stringxyz%E7%A9%B6%E7%AB%9F%E5%88%9B%E5%BB%BAstring-object%E5%88%86%E4%B8%BA%E4%B8%A4%E7%A7%8D%E6%83%85%E5%86%B5) + - [Java中由substring方法引发的内存泄漏](#java%E4%B8%AD%E7%94%B1substring%E6%96%B9%E6%B3%95%E5%BC%95%E5%8F%91%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F) - [什么是值传递和引用传递](#%E4%BB%80%E4%B9%88%E6%98%AF%E5%80%BC%E4%BC%A0%E9%80%92%E5%92%8C%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92) - [什么是泛型,为什么要使用以及类型擦除](#%E4%BB%80%E4%B9%88%E6%98%AF%E6%B3%9B%E5%9E%8B%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E4%BB%A5%E5%8F%8A%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4) - [什么是序列化?为什么要序列化?](#%E4%BB%80%E4%B9%88%E6%98%AF%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%BA%8F%E5%88%97%E5%8C%96) - [反射](#%E5%8F%8D%E5%B0%84) - [优点](#%E4%BC%98%E7%82%B9) - [缺点](#%E7%BC%BA%E7%82%B9) -- [Java各个版本的主要新特性](#java%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC%E7%9A%84%E4%B8%BB%E8%A6%81%E6%96%B0%E7%89%B9%E6%80%A7) +- [Java1.0-1.12各个版本的新特性](#java10-112%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC%E7%9A%84%E6%96%B0%E7%89%B9%E6%80%A7) +## JAVA开发六大原则 +1. 单一原则 : 一个类或一个方法只负责一件事情 +2. 里斯替换原则: 子类不应该重写父类已实现的方法,重载不应该比父类的参数更少 +3. 依赖倒置原则: 面向接口编程.(面向接口更能添加程序的可扩展性) +4. 接口隔离原则: 接口中的方法应该细分,要合理的隔离开不同的功能到不同的接口中. +5. 迪米特原则: 高内聚低耦合 +6. 开闭原则: 对修改关闭,对扩展开放 +总结: 用抽象构建框架,用实现扩展细节 ## 抽象类和接口的对比 | 参数 | 抽象类 | 接口 | @@ -55,6 +84,126 @@ |添加新方法 |如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。| 如果你往接口中添加方法,那么你必须改变实现该接口的类。| +## 如何去设计类和接口(Effective Java) +### 1、使类和成员的可访问性最小化 +尽可能地使每个类或者成员不被外界访问,尽可能最小的访问级别。 +### 2、复合优先于继承 +与方法调用不同的是,继承打破了封装性。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。 + +建议新的类中增加一个私有域,它引现有类的一个实例。这种设计被称做“复合(composition) +### 3、接口优于抽象类 +如果你希望让两个类扩展同一个抽象类,就必须把抽象类放到类型层次结构的高处,以便这两个类的一个祖先成为它的子类。遗憾的是这样做会间接到伤害到类层次,迫使这个公共祖先到所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。 +### 4、优先考虑静态成员类 +非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。 + +## 三大特性 +### 多态 +**好处** +1. 提高了代码的维护性(继承保证) +2. 提高了代码的扩展性(由多态保证) +多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在复运行时,可以通过指向基类的指针,来调用实现派生类中的方法。 + +**实现原理** +多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。 + +Java 对于方法调用**动态绑定**的实现主要依赖于**方法表**,但通过**类引用调**用(invokevitual)和**接口引用调用**(invokeinterface)的实现则有所不同 + +### 重载 +在编译器眼里,方法名称+参数类型+参数个数,组成一个唯一键,称为方法签名。返回值并不是方法签名的一部分,会导致编译出错。 + +**一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。** + +## 使用final的意义 +1. 为方法“上锁”,防止任何继承类改变它的本来含义和实现。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。 +2. 提高程序执行的效率,将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里(内嵌机制) +3. 如果一个数据既是static又是final,那么它会拥有一块无法改变的存储空间 + + +## 四大引用 +| 引用类型 | 回收时机 | 使用场景 | +|--|--|--| +|强引用 | 不回收| 创建对象实例 | +|软引用 | 内存不足时 | 图片缓存 | +| 弱引用 | 垃圾回收 | WeakHashMap,维护一种非强制的映射关系 | +| 虚引用 | Unknow | 跟踪对象垃圾回收的活动 | + +## 值传递和引用传递的区别? +### 值传递 +在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,**都仅仅是对这个副本的操作**,不影响原始值的内容。 +``` + public static void valueCrossTest(int age, float weight){ + System.out.println("传入的age:" + age); + System.out.println("传入的weight:" + weight); + age = 33; + weight = 89.5f; + System.out.println("方法内重新赋值后的age:" + age); + System.out.println("方法内重新赋值后的weight:" + weight); + } + + public static void main(String[] args) { + int a = 25; + float w = 77.5f; + valueCrossTest(a, w); + System.out.println("方法执行后的age:" + a); + System.out.println("方法执行后的weight:"+w); + } +``` +``` +传入的age:25 +传入的weight:77.5 +方法内重新赋值后的age:33 +方法内重新赋值后的weight:89.5 +方法执行后的age:25 +方法执行后的weight:77.5 +``` +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-25.png) + +只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。 + +**值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。** + + +### 引用传递 +**”引用”也就是指向真实内容的地址值**,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,**形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容**。 +``` + public static void PersonCrossTest(Person person){ + System.out.println("传入的person的name:"+person.getName()); + person.setName("我是张小龙"); + System.out.println("方法内重新赋值后的name:"+person.getName()); + } + + public static void main(String[] args) { + Person p = new Person(); + p.setName("我是马化腾"); + p.setAge(45); + PersonCrossTest(p); + System.out.println("方法执行后的name:"+p.getName()); + } +``` +``` +传入的person的name:我是马化腾 +方法内重新赋值后的name:我是张小龙 +方法执行后的name:我是张小龙 +``` +可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。 + +修改一下 +``` + public static void PersonCrossTest(Person person){ + System.out.println("传入的person的name:"+person.getName()); + person=new Person();//加多此行代码 + person.setName("我是张小龙"); + System.out.println("方法内重新赋值后的name:"+person.getName()); + } +``` +``` +传入的person的name:我是马化腾 +方法内重新赋值后的name:我是张小龙 +方法执行后的name:我是马化腾 +``` +JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。 + + ## equals() 1. Obejct的equals()源码 ```java @@ -100,6 +249,19 @@ public boolean equals(Object obj) { return false; } ``` + +### ==和equals的区别?实现equals要注意哪些东西? +**==和equals的区别** +- ==:判断两个字符串在内存中首地址是否相同,即判断两者是否是同一个字符串对象 +- equles():如果没有重写equals()方法比较的是对象的地址,因为对Object来说对象没有什么属性可以比较,只能比较最底层的地址。 +而如果重写equals()方法时,该方法的对象因为是Object的子类,所以调用时会调用子类对象里面的方法.所以只有重写equals()方法后,两者比较的才是内容.或者说重写可以使自己定义比较的规则,不想按照地址去比较. + +**实现equals要注意哪些东西?** +1、自反性:对于任何非空引用x,x.equals(x)应该返回true。 +2、对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。 +3、传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。 +4、一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果。 +5、非空性:对于任意非空引用x,x.equals(null)应该返回false。 compareTo() ```java public int compareTo(Integer anotherInteger) { @@ -111,6 +273,16 @@ compareTo() } ``` +### equals()与hashCode()之间的关系 +- 如果两个对象equals()方法相等则它们的hashCode返回值一定要相同,如果两个对象的hashCode返回值相同,但它们的equals()方法不一定相等。 +- hashCode()的作用是为了提高在散列结构存储中查找的效率 +- Java中重写equals()方法时尽量要重写hashCode()方法的原因:声明相等对象必须具有相等的哈希码,包括 HashMap、HashSet、Hashtable 等 + +## 一个字符(英文字母)占多少个字节,一个中文占多少字节? +- 一个字符占1个字节(GBK、ASCII、UTF-8) +- 一个中文占 2 个字节(GBK、ASCII) +- 一个中文占 3 个字节(UTF-8) + ## java中double和float精度丢失问题及解决方法 ```java System.out.println(0.11+2001299.32); @@ -135,6 +307,7 @@ public BigDecimal(double val) 2. 另一方面,String 构造方法是完全可预知的:写入 new BigDecimal("0.1") 将创建一个 BigDecimal,它正好 等于预期的 0.1。因此,比较而言,通常建议优先使用 String 构造方法。 3. 当 double 必须用作 BigDecimal 的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用 Double.toString(double) 方法,然后使用 BigDecimal(String) 构造方法,将 double 转换为 String。要获取该结果,请使用 static valueOf(double) 方法。 + ## 注解 ### 元注解(4个) @@ -265,7 +438,7 @@ Object是所有类的根类,任何类的对象都可以设置给该Object引 -## Java 异常的体系结构 +## Java 异常 Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。 @@ -278,7 +451,7 @@ Java异常层次结构图如下图所示: Error:Error类对象由 Java 虚拟机生成并抛出,Error表示编译时和系统错误,通常不能预期和恢复,比如硬件故障、JVM崩溃、内存不足等 。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。 Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。 -## throw和throws区别 +### throw和throws区别 **throw:(针对对象的做法)** 抛出一个异常,可以是系统定义的,也可以是自己定义的 @@ -301,6 +474,57 @@ public void yichang() throws NumberFormatException{ 2. throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。 3. 两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。 +### 什么情况finally不会执行 +1、没有进入try代码块。 +2、进入try代码块 , 但是代码运行中出现了死循环或死锁状态。 +3、进入try代码块, 但是执行了 System.exit()操作。 + +注意, finally 是在 return 表达式运行后执行的 , 此时将要 return 的结果 已 经被暂 存起来 , 待 finally 代码块执行结束后再将之前暂存的结果返回 + +``` + private static int test1() { + int tmp = 10000; + try { + throw new Exception(); + } catch (Exception e) { + return ++tmp; + } finally { + tmp = 99999; + } + } + +``` + +此方法最终的返回值是 10001 ,而不是 99999。 + +相对在 finally 代码块中赋值,更加危险的做法是在 finally块中使用 return 操作,这样的代码会使返回值变得非常不可控。 +``` + private static int test1() { + int x = 1; + int y = 10; + int z = 100; + try { + return ++x; + } catch (Exception e) { + return ++y; + } finally { + return ++z; + } + } + +``` +( 1 )最后 return 的功件是由 finally 代码块巾的 return ++z 完成的,所以为法返 回的结果是 101。 +( 2 )语旬 return ++x 中的++x 被成功执行,所以运行结果是x=2。 +( 3 ) 如果有异常抛出 ,那么运行结果将会是 y =11,而 x=1; + + +finally代码块中使用 return语旬,使返回值的判断变得复杂,所以避免返回值不 +可控,我们不要在 finally代码块中使用 return语句。 + +### finally方法一定会被执行么? +java中,如果想要执行try中的代码之后,不允许再执行finally中的代码,有以下两种方式: +- 使用System.exit(1)来退出虚拟机 +- 把当前执行trycatchfinally代码的线程设置为守护线程 ## .class 文件是什么类型文件 class文件是一种8位字节的二进制流文件 @@ -458,11 +682,33 @@ public class Object { protected void finalize() throws Throwable { } } ``` - -## String s=new String("xyz")究竟创建String Object分为两种情况 +## String +### String s=new String("xyz")究竟创建String Object分为两种情况 1. 如果String常理池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String("xyz"); 2. 如果String常理池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String("xyz")。 +### Java中由substring方法引发的内存泄漏 +- 内存溢出(out of memory ):通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出。 +- 内存泄漏(leak of memory):是指为一个对象分配内存之后,在对象已经不在使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样。 + +substring(int beginIndex, int endndex )是String类的一个方法,但是这个方法在JDK6和JDK7中的实现是完全不同的(虽然它们都达到了同样的效果)。在JDK1.6中不当使用substring会导致严重的内存泄漏问题。 +``` +String str = "abcdefghijklmnopqrst"; +String sub = str.substring(1, 3); +str = null; +``` +这段简单的程序有两个字符串变量str、sub。sub字符串是由父字符串str截取得到的,假如上述这段程序在JDK1.6中运行,我们知道数组的内存空间分配是在堆上进行的,那么sub和str的内部char数组value是公用了同一个,也就是上述有字符a~字符t组成的char数组,str和sub唯一的差别就是在数组中其实beginIndex和字符长度count的不同。在第三句,我们使str引用为空,本意是释放str占用的空间,但是这个时候,GC是无法回收这个大的char数组的,因为还在被sub字符串内部引用着,虽然sub只截取这个大数组的一小部分。当str是一个非常大字符串的时候,这种浪费是非常明显的,甚至会带来性能问题,解决这个问题可以是通过以下的方法: +``` +String str = "abcdefghijklmnopqrst"; +String sub = str.substring(1, 3) + ""; +str = null; +``` +利用的就是字符串的拼接技术,它会创建一个新的字符串,这个新的字符串会使用一个新的内部char数组存储自己实际需要的字符,这样父数组的char数组就不会被其他引用,令str=null,在下一次GC回收的时候会回收整个str占用的空间。但是这样书写很明显是不好看的,所以在JDK7中,substring 被重新实现了。 + +在JDK7中改进了substring的实现,它实际是为截取的子字符串在堆中创建了一个新的char数组用于保存子字符串的字符。这样子字符串和父字符串也就没有什么必然的联系了,当父字符串的引用失效的时候,GC就会适时的回收父字符串占用的内存空间。 + + + ## 什么是值传递和引用传递 @@ -727,3 +973,4 @@ Java 语言真正走向成熟,提供了非常完备的语言特性,如 NIO + diff --git "a/notes/java/Java\345\256\271\345\231\250.md" "b/notes/java/Java\345\256\271\345\231\250.md" index fe006a7..d828d62 100644 --- "a/notes/java/Java\345\256\271\345\231\250.md" +++ "b/notes/java/Java\345\256\271\345\231\250.md" @@ -3,7 +3,9 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Java集合类框架图](#java%E9%9B%86%E5%90%88%E7%B1%BB%E6%A1%86%E6%9E%B6%E5%9B%BE) + - [Java集合类框架的基本接口有哪些?](#java%E9%9B%86%E5%90%88%E7%B1%BB%E6%A1%86%E6%9E%B6%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%8E%A5%E5%8F%A3%E6%9C%89%E5%93%AA%E4%BA%9B) - [HashSet和TreeSet区别](#hashset%E5%92%8Ctreeset%E5%8C%BA%E5%88%AB) +- [ArrayList和LinkedList的区别](#arraylist%E5%92%8Clinkedlist%E7%9A%84%E5%8C%BA%E5%88%AB) - [讲一下LinkedHashMap](#%E8%AE%B2%E4%B8%80%E4%B8%8Blinkedhashmap) - [Java8 中HashMap的优化(引入红黑树的数据结构和扩容的优化)](#java8-%E4%B8%ADhashmap%E7%9A%84%E4%BC%98%E5%8C%96%E5%BC%95%E5%85%A5%E7%BA%A2%E9%BB%91%E6%A0%91%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E6%89%A9%E5%AE%B9%E7%9A%84%E4%BC%98%E5%8C%96) - [Map遍历的keySet()和entrySet()性能差异原因](#map%E9%81%8D%E5%8E%86%E7%9A%84keyset%E5%92%8Centryset%E6%80%A7%E8%83%BD%E5%B7%AE%E5%BC%82%E5%8E%9F%E5%9B%A0) @@ -14,9 +16,12 @@ - [HashMap原理](#hashmap%E5%8E%9F%E7%90%86) - [HashMap特性](#hashmap%E7%89%B9%E6%80%A7) - [HashMap的原理,内部数据结构](#hashmap%E7%9A%84%E5%8E%9F%E7%90%86%E5%86%85%E9%83%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) + - [HashMap的hash函数原理](#hashmap%E7%9A%84hash%E5%87%BD%E6%95%B0%E5%8E%9F%E7%90%86) - [讲一下 HashMap 中 put 方法过程](#%E8%AE%B2%E4%B8%80%E4%B8%8B-hashmap-%E4%B8%AD-put-%E6%96%B9%E6%B3%95%E8%BF%87%E7%A8%8B) - [get()方法的工作原理](#get%E6%96%B9%E6%B3%95%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) + - [HashMap的put()方法流程](#hashmap%E7%9A%84put%E6%96%B9%E6%B3%95%E6%B5%81%E7%A8%8B) - [HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?](#hashmap%E4%B8%ADhash%E5%87%BD%E6%95%B0%E6%80%8E%E4%B9%88%E6%98%AF%E6%98%AF%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%BF%98%E6%9C%89%E5%93%AA%E4%BA%9B-hash-%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F) + - [Java8 HashMap扩容时为什么不需要重新hash?](#java8-hashmap%E6%89%A9%E5%AE%B9%E6%97%B6%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E9%9C%80%E8%A6%81%E9%87%8D%E6%96%B0hash) - [HashMap 怎样解决冲突?](#hashmap-%E6%80%8E%E6%A0%B7%E8%A7%A3%E5%86%B3%E5%86%B2%E7%AA%81) - [扩展问题1:当两个对象的hashcode相同会发生什么?](#%E6%89%A9%E5%B1%95%E9%97%AE%E9%A2%981%E5%BD%93%E4%B8%A4%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84hashcode%E7%9B%B8%E5%90%8C%E4%BC%9A%E5%8F%91%E7%94%9F%E4%BB%80%E4%B9%88) - [扩展问题2:抛开 HashMap,hash 冲突有那些解决办法?](#%E6%89%A9%E5%B1%95%E9%97%AE%E9%A2%982%E6%8A%9B%E5%BC%80-hashmaphash-%E5%86%B2%E7%AA%81%E6%9C%89%E9%82%A3%E4%BA%9B%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95) @@ -27,6 +32,7 @@ - [HashMap与HashTable区别](#hashmap%E4%B8%8Ehashtable%E5%8C%BA%E5%88%AB) - [区别](#%E5%8C%BA%E5%88%AB) - [能否让HashMap同步?](#%E8%83%BD%E5%90%A6%E8%AE%A9hashmap%E5%90%8C%E6%AD%A5) +- [求两个list的并集、交集、差集?](#%E6%B1%82%E4%B8%A4%E4%B8%AAlist%E7%9A%84%E5%B9%B6%E9%9B%86%E4%BA%A4%E9%9B%86%E5%B7%AE%E9%9B%86) @@ -34,6 +40,9 @@ ## Java集合类框架图 ![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-16.png) +### Java集合类框架的基本接口有哪些? +总共有两大接口:Collection 和Map ,一个元素集合,一个是键值对集合; 其中List和Set接口继承了Collection接口,一个是有序元素集合,一个是无序元素集合; 而ArrayList和 LinkedList 实现了List接口,HashSet实现了Set接口,这几个都比较常用; HashMap 和HashTable实现了Map接口,并且HashTable是线程安全的,但是HashMap性能更好; + ## HashSet和TreeSet区别 **HashSet** @@ -47,6 +56,12 @@ 1. TreeSet是SortedSet接口的唯一实现类 2. TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象 +## ArrayList和LinkedList的区别 +- **底层实现**:ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构,ArrayList需要扩容、LinkedList不需要 +- **时间复杂度**:对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针 +- **使用场景**:LinkedList是个双向链表,它同样可以被当作栈、队列或双端队列来使用。 + + ## 讲一下LinkedHashMap @@ -220,6 +235,17 @@ HashMap是基于hashing的原理,底层使用哈希表(数组 + 链表)实 存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。 +### HashMap的hash函数原理 +``` +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` +Java 8中这一步做了优化,只做一次16位右位移异或混合,而不是四次,但原理是不变的。 + +优化了高位运算的算法,**通过hashCode()的高16位异或低16位实现的**,主要是从速度、功效、质量来考虑的 + ### 讲一下 HashMap 中 put 方法过程 1. 对key的hashCode做hash操作,然后再计算在bucket中的index(1.5 HashMap的哈希函数); 2. 如果没碰撞直接放到bucket里; @@ -230,11 +256,49 @@ HashMap是基于hashing的原理,底层使用哈希表(数组 + 链表)实 ### get()方法的工作原理 通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表中查找对应的节点。 +### HashMap的put()方法流程 + +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-23.png) ### HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式? 1. 对key的hashCode做hash操作(高16bit不变,低16bit和高16bit做了一个异或); 2. h & (length-1); //通过位操作得到下标index。 还有数字分析法、平方取中法、分段叠加法、 除留余数法、 伪随机数法。 +### Java8 HashMap扩容时为什么不需要重新hash? +``` + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + +``` +可以看到它是通过将数据的hash与扩容前的长度进行与操作,根据`e.hash & oldCap`的结果来判断,如果是0,说明位置没有发生变化,如果不为0,说明位置发生了变化,而且新的位置=老的位置+老的数组长度。 + + +比如数据B它经过hash之后的值为 1111,在扩容之前数组长度是8,数据B的位置是: +``` +(n-1)&hash = (8-1) & 1111 = 111 & 1111 = 0111 +``` +扩容之后,数组长度是16,重新计算hash位置是: +``` +(n-1)&hash = (16-1) & 1111 = 1111 & 1111 = 1111 +``` +可见数据B的位置发生了变化,同时新的位置和原来的位置关系是: +**新的位置(1111)= 1000+原来的位置(0111)=原来的长度(8)+原来的位置(0111)** +继续看一下e.hash & oldCap的结果 +``` +e.hash & oldCap = 1111 & 8 = 1111 & 1000 = 1000 (!=0) +``` ### HashMap 怎样解决冲突? HashMap中处理冲突的方法实际就是链地址法,内部数据结构是数组+单链表。 @@ -272,7 +336,7 @@ HashMap中处理冲突的方法实际就是链地址法,内部数据结构是   2.因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。 ## HashMap与HashTable区别 -  Hashtable可以看做是线程安全版的HashMap,两者几乎“等价”(当然还是有很多不同)。Hashtable几乎在每个方法上都加上synchronized(同步锁),实现线程安全。 +Hashtable可以看做是线程安全版的HashMap,两者几乎“等价”(当然还是有很多不同)。Hashtable几乎在每个方法上都加上synchronized(同步锁),实现线程安全。 ### 区别 1. HashMap继承于AbstractMap,而Hashtable继承于Dictionary; @@ -284,4 +348,9 @@ HashMap中处理冲突的方法实际就是链地址法,内部数据结构是 7. 速度。由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。 ### 能否让HashMap同步? -  HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap); \ No newline at end of file +HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap); + +## 求两个list的并集、交集、差集? +- 并集:list1.addAll(list2); +- 交集:list1.retainAll(list2); +- 差集:list1.removeAll(list2); diff --git "a/notes/java/Java\345\271\266\345\217\221.md" "b/notes/java/Java\345\271\266\345\217\221.md" index 890df69..e169f0a 100644 --- "a/notes/java/Java\345\271\266\345\217\221.md" +++ "b/notes/java/Java\345\271\266\345\217\221.md" @@ -2,52 +2,84 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [什么是线程安全,怎么保证线程安全?](#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E6%80%8E%E4%B9%88%E4%BF%9D%E8%AF%81%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8) +- [如何保证线程安全](#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8) - [JAVA 线程状态转换图示](#java-%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%9B%BE%E7%A4%BA) - [线程为什么调用start()而不是直接调用run()](#%E7%BA%BF%E7%A8%8B%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B0%83%E7%94%A8start%E8%80%8C%E4%B8%8D%E6%98%AF%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8run) -- [synchronized 的底层怎么实现](#synchronized-%E7%9A%84%E5%BA%95%E5%B1%82%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0) -- [讲一下CAS](#%E8%AE%B2%E4%B8%80%E4%B8%8Bcas) -- [线程池](#%E7%BA%BF%E7%A8%8B%E6%B1%A0) - - [ThreadPoolExecutor执行的策略](#threadpoolexecutor%E6%89%A7%E8%A1%8C%E7%9A%84%E7%AD%96%E7%95%A5) - - [常见四种线程池](#%E5%B8%B8%E8%A7%81%E5%9B%9B%E7%A7%8D%E7%BA%BF%E7%A8%8B%E6%B1%A0) - - [四种线程池使用场景](#%E5%9B%9B%E7%A7%8D%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) - - [四种拒绝策略](#%E5%9B%9B%E7%A7%8D%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5) - - [为什么要用线程池](#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0) - - [线程池ThreadPoolExecutor参数设置](#%E7%BA%BF%E7%A8%8B%E6%B1%A0threadpoolexecutor%E5%8F%82%E6%95%B0%E8%AE%BE%E7%BD%AE) - - [corePoolSize](#corepoolsize) - - [maxPoolSize](#maxpoolsize) - - [queueCapacity](#queuecapacity) - - [keepAliveTime](#keepalivetime) - - [allowCoreThreadTimeout](#allowcorethreadtimeout) -- [Executorshe和ThreaPoolExecutor创建线程池的区别](#executorshe%E5%92%8Cthreapoolexecutor%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%8C%BA%E5%88%AB) + - [阻塞,等待,挂起,休眠的区别](#%E9%98%BB%E5%A1%9E%E7%AD%89%E5%BE%85%E6%8C%82%E8%B5%B7%E4%BC%91%E7%9C%A0%E7%9A%84%E5%8C%BA%E5%88%AB) +- [多线程上下文切换的影响](#%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2%E7%9A%84%E5%BD%B1%E5%93%8D) +- [synchronized](#synchronized) + - [synchronized 的底层怎么实现](#synchronized-%E7%9A%84%E5%BA%95%E5%B1%82%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0) + - [为什么notify和wait方法必须在synchronized方法中使用?](#%E4%B8%BA%E4%BB%80%E4%B9%88notify%E5%92%8Cwait%E6%96%B9%E6%B3%95%E5%BF%85%E9%A1%BB%E5%9C%A8synchronized%E6%96%B9%E6%B3%95%E4%B8%AD%E4%BD%BF%E7%94%A8) + - [1、依赖锁对象的监视器monitor](#1%E4%BE%9D%E8%B5%96%E9%94%81%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%9B%91%E8%A7%86%E5%99%A8monitor) + - [2、避免lost wake up问题](#2%E9%81%BF%E5%85%8Dlost-wake-up%E9%97%AE%E9%A2%98) + - [jdk1.6以后对synchronized锁做了哪些优化](#jdk16%E4%BB%A5%E5%90%8E%E5%AF%B9synchronized%E9%94%81%E5%81%9A%E4%BA%86%E5%93%AA%E4%BA%9B%E4%BC%98%E5%8C%96) +- [Java有哪些锁?](#java%E6%9C%89%E5%93%AA%E4%BA%9B%E9%94%81) +- [CAS](#cas) + - [CAS 介绍](#cas-%E4%BB%8B%E7%BB%8D) + - [CAS到底最后加没加锁](#cas%E5%88%B0%E5%BA%95%E6%9C%80%E5%90%8E%E5%8A%A0%E6%B2%A1%E5%8A%A0%E9%94%81) - [CountDownLatch与CyclicBarrier的比较](#countdownlatch%E4%B8%8Ecyclicbarrier%E7%9A%84%E6%AF%94%E8%BE%83) + - [源码上的区别](#%E6%BA%90%E7%A0%81%E4%B8%8A%E7%9A%84%E5%8C%BA%E5%88%AB) - [对象锁和静态锁之间的区别](#%E5%AF%B9%E8%B1%A1%E9%94%81%E5%92%8C%E9%9D%99%E6%80%81%E9%94%81%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8C%BA%E5%88%AB) - [简述volatile字](#%E7%AE%80%E8%BF%B0volatile%E5%AD%97) + - [volatile为什么不能保证原子性?](#volatile%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E4%BF%9D%E8%AF%81%E5%8E%9F%E5%AD%90%E6%80%A7) + - [synchronized 和 volatile 的区别是什么?](#synchronized-%E5%92%8C-volatile-%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88) - [happens-before 原则(先行发生原则)](#happens-before-%E5%8E%9F%E5%88%99%E5%85%88%E8%A1%8C%E5%8F%91%E7%94%9F%E5%8E%9F%E5%88%99) + - [as-if-serial规则和happens-before规则的区别](#as-if-serial%E8%A7%84%E5%88%99%E5%92%8Chappens-before%E8%A7%84%E5%88%99%E7%9A%84%E5%8C%BA%E5%88%AB) - [Lock 和synchronized 的区别](#lock-%E5%92%8Csynchronized-%E7%9A%84%E5%8C%BA%E5%88%AB) + - [什么情况下可以使用 ReentrantLock](#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8-reentrantlock) +- [什么时候用重入锁,什么时候用非重入锁?](#%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E7%94%A8%E9%87%8D%E5%85%A5%E9%94%81%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E7%94%A8%E9%9D%9E%E9%87%8D%E5%85%A5%E9%94%81) +- [AQS是如何唤醒下一个线程的?](#aqs%E6%98%AF%E5%A6%82%E4%BD%95%E5%94%A4%E9%86%92%E4%B8%8B%E4%B8%80%E4%B8%AA%E7%BA%BF%E7%A8%8B%E7%9A%84) - [ThreadLocal(线程变量副本)](#threadlocal%E7%BA%BF%E7%A8%8B%E5%8F%98%E9%87%8F%E5%89%AF%E6%9C%AC) - [Threadlocal和run方法的局部变量的区别](#threadlocal%E5%92%8Crun%E6%96%B9%E6%B3%95%E7%9A%84%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB) - [ThreadLocal 适用于如下两种场景](#threadlocal-%E9%80%82%E7%94%A8%E4%BA%8E%E5%A6%82%E4%B8%8B%E4%B8%A4%E7%A7%8D%E5%9C%BA%E6%99%AF) - [ThreadLocal内存泄露](#threadlocal%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2) + - [父子线程传递Threadlcoal值的问题](#%E7%88%B6%E5%AD%90%E7%BA%BF%E7%A8%8B%E4%BC%A0%E9%80%92threadlcoal%E5%80%BC%E7%9A%84%E9%97%AE%E9%A2%98) - [通过Callable和Future创建线程](#%E9%80%9A%E8%BF%87callable%E5%92%8Cfuture%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B) - [什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)](#%E4%BB%80%E4%B9%88%E5%8F%AB%E5%AE%88%E6%8A%A4%E7%BA%BF%E7%A8%8B%E7%94%A8%E4%BB%80%E4%B9%88%E6%96%B9%E6%B3%95%E5%AE%9E%E7%8E%B0%E5%AE%88%E6%8A%A4%E7%BA%BF%E7%A8%8Bthreadsetdeamon%E7%9A%84%E5%90%AB%E4%B9%89) - [如何停止一个线程?](#%E5%A6%82%E4%BD%95%E5%81%9C%E6%AD%A2%E4%B8%80%E4%B8%AA%E7%BA%BF%E7%A8%8B) -- [什么是线程安全?什么是线程不安全?](#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E4%B8%8D%E5%AE%89%E5%85%A8) -- [I/O多路复用](#io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8) -- [讲一下netty](#%E8%AE%B2%E4%B8%80%E4%B8%8Bnetty) -- [Nio的原理(同步非阻塞)](#nio%E7%9A%84%E5%8E%9F%E7%90%86%E5%90%8C%E6%AD%A5%E9%9D%9E%E9%98%BB%E5%A1%9E) -- [缓冲区Buffer、通道Channel、选择器Selector](#%E7%BC%93%E5%86%B2%E5%8C%BAbuffer%E9%80%9A%E9%81%93channel%E9%80%89%E6%8B%A9%E5%99%A8selector) -- [BIO和NIO的区别](#bio%E5%92%8Cnio%E7%9A%84%E5%8C%BA%E5%88%AB) -- [NIO的selector作用](#nio%E7%9A%84selector%E4%BD%9C%E7%94%A8) +- [Java 中 interrupted 和 isInterrupted 方法的区别?](#java-%E4%B8%AD-interrupted-%E5%92%8C-isinterrupted-%E6%96%B9%E6%B3%95%E7%9A%84%E5%8C%BA%E5%88%AB) +- [线程的 sleep()方法和 yield()方法有什么区别?](#%E7%BA%BF%E7%A8%8B%E7%9A%84-sleep%E6%96%B9%E6%B3%95%E5%92%8C-yield%E6%96%B9%E6%B3%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) - [final域的内存语义](#final%E5%9F%9F%E7%9A%84%E5%86%85%E5%AD%98%E8%AF%AD%E4%B9%89) - [写final域的重排序规则](#%E5%86%99final%E5%9F%9F%E7%9A%84%E9%87%8D%E6%8E%92%E5%BA%8F%E8%A7%84%E5%88%99) - [读final域的重排序规则](#%E8%AF%BBfinal%E5%9F%9F%E7%9A%84%E9%87%8D%E6%8E%92%E5%BA%8F%E8%A7%84%E5%88%99) - [notify和notifyAll的区别](#notify%E5%92%8Cnotifyall%E7%9A%84%E5%8C%BA%E5%88%AB) +- [JUC](#juc) + - [ConcurrentHashMap是如何在保证并发安全的同时提高性能](#concurrenthashmap%E6%98%AF%E5%A6%82%E4%BD%95%E5%9C%A8%E4%BF%9D%E8%AF%81%E5%B9%B6%E5%8F%91%E5%AE%89%E5%85%A8%E7%9A%84%E5%90%8C%E6%97%B6%E6%8F%90%E9%AB%98%E6%80%A7%E8%83%BD) + - [为什么java.util.concurrent 包里没有并发的ArrayList实现?](#%E4%B8%BA%E4%BB%80%E4%B9%88javautilconcurrent-%E5%8C%85%E9%87%8C%E6%B2%A1%E6%9C%89%E5%B9%B6%E5%8F%91%E7%9A%84arraylist%E5%AE%9E%E7%8E%B0) + - [ConcurrentModificationException异常出现的原因](#concurrentmodificationexception%E5%BC%82%E5%B8%B8%E5%87%BA%E7%8E%B0%E7%9A%84%E5%8E%9F%E5%9B%A0) + - [fail-fast机制](#fail-fast%E6%9C%BA%E5%88%B6) + - [1、在单线程环境下的解决办法](#1%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E7%8E%AF%E5%A2%83%E4%B8%8B%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95) + - [2、在多线程环境下的解决方法](#2%E5%9C%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%8E%AF%E5%A2%83%E4%B8%8B%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95) + - [比AtomicLong更高性能的LongAdder](#%E6%AF%94atomiclong%E6%9B%B4%E9%AB%98%E6%80%A7%E8%83%BD%E7%9A%84longadder) +- [两个线程同时执行i++100次,结果是多少](#%E4%B8%A4%E4%B8%AA%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%97%B6%E6%89%A7%E8%A1%8Ci100%E6%AC%A1%E7%BB%93%E6%9E%9C%E6%98%AF%E5%A4%9A%E5%B0%91) +- [如何排查死锁?](#%E5%A6%82%E4%BD%95%E6%8E%92%E6%9F%A5%E6%AD%BB%E9%94%81) +- [线程池](#%E7%BA%BF%E7%A8%8B%E6%B1%A0) + - [ThreadPoolExecutor执行的策略](#threadpoolexecutor%E6%89%A7%E8%A1%8C%E7%9A%84%E7%AD%96%E7%95%A5) + - [常见四种线程池](#%E5%B8%B8%E8%A7%81%E5%9B%9B%E7%A7%8D%E7%BA%BF%E7%A8%8B%E6%B1%A0) + - [四种线程池使用场景](#%E5%9B%9B%E7%A7%8D%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) + - [四种拒绝策略](#%E5%9B%9B%E7%A7%8D%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5) + - [为什么要用线程池](#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0) + - [线程池的非核心线程什么时候会被释放](#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E9%9D%9E%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E8%A2%AB%E9%87%8A%E6%94%BE) + - [Executorshe和ThreaPoolExecutor创建线程池的区别](#executorshe%E5%92%8Cthreapoolexecutor%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%8C%BA%E5%88%AB) + - [线程池ThreadPoolExecutor参数设置](#%E7%BA%BF%E7%A8%8B%E6%B1%A0threadpoolexecutor%E5%8F%82%E6%95%B0%E8%AE%BE%E7%BD%AE) + - [corePoolSize](#corepoolsize) + - [maxPoolSize](#maxpoolsize) + - [queueCapacity](#queuecapacity) + - [keepAliveTime](#keepalivetime) + - [allowCoreThreadTimeout](#allowcorethreadtimeout) +## 什么是线程安全,怎么保证线程安全? +线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题 + +## 如何保证线程安全 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-3.jpg) + ## JAVA 线程状态转换图示 -![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-3.jpg) +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-19.png) 线程共包括以下5种状态。 1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。 @@ -100,10 +132,30 @@ thread.run(); start方法其实是在一个新的操作系统线程上面去调用run方法。换句话说,直接调用run方法而不是调用start方法的话,它并不会开启新的线程,而是在调用run的当前的线程当中执行你的操作。 +### 阻塞,等待,挂起,休眠的区别 +阻塞是线程的状态,等待、挂起和休眠是让线程进入阻塞状态的不同行为。等待是线程因为需要等待外部某个条件而进入阻塞,等条件满足后再继续运行(比如等待IO信号)。挂起线程主动让出CPU,等别的线程去唤醒它(比如如join)。休眠是线程主动让出CPU一段时间而进入阻塞状态,等时间到之后再继续运行(比如sleep(time))。 + +## 多线程上下文切换的影响 +**多线程上下文切换的影响** +- 切换带来的性能损耗 +**引起上下文切换的原因** +1. 时间片用完,CPU正常调度下一个任务 +2. 被其他优先级更高的任务抢占 +3. 执行任务碰到IO阻塞,调度器挂起当前任务,切换执行下一个任务 +4. 用户代码主动挂起当前任务让出CPU时间 +5. 多任务抢占资源,由于没有抢到被挂起 +6. 硬件中断 -## synchronized 的底层怎么实现 +**如何减少上下文切换** +1. 无锁并发编程。多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据,队列实现异步串型无锁化。 +2. CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁 +3. 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态 +4. 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换 + +## synchronized +### synchronized 的底层怎么实现 1. **同步代码块**(Synchronization)基于进入和退出管程(Monitor)对象实现。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: - 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 @@ -112,141 +164,118 @@ start方法其实是在一个新的操作系统线程上面去调用run方法。 - 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 2. **被 synchronized 修饰的同步方法**并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成 -## 讲一下CAS -CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试。 - -原理: - -1. CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 - -JDK文档说cas同时具有volatile读和volatile写的内存语义。 - -缺点: - -1. ABA问题。 -因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化 -2. 循环时间长开销大。 -自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 -3. 只能保证一个共享变量的原子操作。 -对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。 +### 为什么notify和wait方法必须在synchronized方法中使用? +#### 1、依赖锁对象的监视器monitor +这是因为调用这三个方法之前必须拿要到当前锁对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,又因为monitor存在于对象头的Mark Word中(存储monitor引用指针),而synchronized关键字可以获取monitor ,所以,notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法中调用。 +#### 2、避免lost wake up问题 +因为会导致lost wake up问题,说白了就唤不醒消费者 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-24.png) +为了避免出现这种lost wake up问题,Java强制我们的wait()/notify()调用必须要在一个同步块中。 -## 线程池 -Executor线程池框架是一个根据一组**执行策略调用,调度,执行和控制**的异步任务的框架。 -### ThreadPoolExecutor执行的策略 +### jdk1.6以后对synchronized锁做了哪些优化 +**锁的级别从低到高:** +无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-18.png) +**锁分级别原因:** -1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务 -2. 线程数量达到了corePools,则将任务移入队列等待 -3. 队列已满,新建线程(非核心线程)执行任务 -4. 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常 +没有优化以前,sychronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 sychronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。 -新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略 +**无锁**:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。 +**偏向锁**:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。 -### 常见四种线程池 +**偏向锁的撤销**,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁; +如果线程处于活动状态,升级为轻量级锁的状态。 -1. CachedThreadPool():可缓存线程池。 - - 线程数无限制 - - 有空闲线程则复用空闲线程,若无空闲线程则新建线程 - - 一定程序减少频繁创建/销毁线程,减少系统开销 +**轻量级锁**:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 -2. FixedThreadPool():定长线程池。 -- 可控制线程最大并发数(同时执行的线程数) -- 超出的线程会在队列中等待 +当前只有一个等待线程,则该线程将通过自旋进行等待。 +**两种情况轻量锁会升级到重量锁:** +1. 当自旋超过一定的次数时 +2. 第三个线程来访时 -3. ScheduledThreadPool():定时线程池。 -- 支持定时及周期性任务执行。 +**重量级锁**:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。 -4. SingleThreadExecutor():单线程化的线程池。 -- 有且仅有一个工作线程执行任务 -- 所有任务按照指定顺序执行,即遵循队列的入队出队规则 +重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 **Mutex Lock**实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。 -### 四种线程池使用场景 - -1. newSingleThreadExecutor:适用于串行执行任务的场景 -2. newFixedThreadExecutor:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般Ncpu + 1 -3. newCachedThreadExecutor:适用于北方执行大量短期的小任务 -4. newScheduledThreadExecutor:适用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景 -### 四种拒绝策略 - -1. AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略 -2. DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式。 -3. DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行 -4. CallerRunPolicy:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用 - +## Java有哪些锁? +- 公平锁/非公平锁 +- 可重入锁 +- 独享锁/共享锁 +- 互斥锁/读写锁 +- 乐观锁/悲观锁 +- 分段锁 +- 偏向锁/轻量级锁/重量级锁 +- 自旋锁 -### 为什么要用线程池 +## CAS -1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 -2. 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 -3. 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现 +### CAS 介绍 +CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试。 +原理: -### 线程池ThreadPoolExecutor参数设置 -参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数: -- tasks,每秒需要处理的的任务数 -- tasktime,处理每个任务花费的时间 -- responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒。 +1. CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 -#### corePoolSize -每个任务需要tasktime秒处理,则每个线程每钞可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime),即tasks*tasktime个线程数。 +JDK文档说cas同时具有volatile读和volatile写的内存语义。 -假设系统每秒任务数为100 ~ 1000,每个任务耗时0.1秒,则需要100 * 0.1至1000 * 0.1,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数小于200,最多时为1000,则corePoolSize可设置为20。 +缺点: -#### maxPoolSize +1. ABA问题。 +因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化 -当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60。 +2. 循环时间长开销大。 +自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 +3. 只能保证一个共享变量的原子操作。 +对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。 -#### queueCapacity +### CAS到底最后加没加锁 +首先使用Unsafe类中的compareAndSwapInt方法实现。 +LOCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀 +1. 如果是多处理器,为cmpxchg指令添加lock前缀。 +2. 反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果) -任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为(corePoolSize/tasktime)*responsetime: (20/0.1)*2=400,即队列长度可设置为400。 -队列长度设置过大,会导致任务响应时间过长,切忌以下写法: -```java -LinkedBlockingQueue queue = new LinkedBlockingQueue(); -``` -这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。 +## CountDownLatch与CyclicBarrier的比较 +CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的: -#### keepAliveTime +1. CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 +2. 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行; +3. CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能; +4. CountDownLatch是不能复用的,而CyclicLatch是可以复用的。 -当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程。 +### 源码上的区别 -#### allowCoreThreadTimeout +**CountDownLatch底层是使用AQS** +- 当我们调用CountDownLatch countDownLatch=new CountDownLatch(4) 时候,此时会创建一个AQS的同步队列,并把创建CountDownLatch 传进来的计数器赋值给AQS队列的 state,所以state的值也代表CountDownLatch所剩余的计数次数;(state:同步状态,多少线程获取锁) +- 当我们调用countDownLatch.wait()的时候,会创建一个节点,加入到AQS阻塞队列,并同时把当前线程挂起。 +- 当执行 CountDownLatch 的 countDown()方法,将计数器减一,也就是state减一,当减到0的时候,等待队列中的线程被释放。是调用 AQS 的 releaseShared 方法来实现的。(tryreleaseshared:通过设置同步状态尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false) +- 因为这是共享型的,当计数器为 0 后,会唤醒等待队列里的所有线程,所有调用了 await() 方法的线程都被唤醒,并发执行。这种情况对应到的场景是,有多个线程需要等待一些动作完成。 -默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。 +**CyclicBarrier底层是使用ReentrantLock(独占锁)和Condition** +- 每当线程执行await,内部变量count减1,如果count!= 0,说明有线程还未到屏障处,则在锁条件变量trip上等待。 +- 当count == 0时,说明所有线程都已经到屏障处,执行条件变量的signalAll方法唤醒等待的线程。 +- 其中 nextGeneration方法可以实现屏障的循环使用:重新生成Generation对象,恢复count值,如果generation.broken为true的话,说明这个屏障已经损坏,当某个线程await的时候,直接抛出异常 +- 在CyclicBarrier中,同一批的线程属于同一代,即同一个Generation;CyclicBarrier中通过generation对象,记录属于哪一代。当有parties个线程到达barrier,generation就会被更新换代。达到了循环使用 -如果涉及到有突发流量的场景,又该如何设置? -## Executorshe和ThreaPoolExecutor创建线程池的区别 -- Executors 各个方法的弊端: -1. newFixedThreadPool 和 newSingleThreadExecutor: -主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。 -2. newCachedThreadPool 和 newScheduledThreadPool: - 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。 -- ThreaPoolExecutor -1. 创建线程池方式只有一种,就是走它的构造函数,参数自己指定 -## CountDownLatch与CyclicBarrier的比较 -CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的: -1. CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 -2. 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行; -3. CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能; -4. CountDownLatch是不能复用的,而CyclicLatch是可以复用的。 ## 对象锁和静态锁之间的区别 1. 对象锁用于对象实例方法, @@ -266,6 +295,29 @@ CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以 **要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性** +### volatile为什么不能保证原子性? +修改volatile变量分为四步: +1. 读取volatile变量到local +2. 修改变量值 +3. local值写回 +4. 插入内存屏障,即lock指令,让其他线程可见这样就很容易看出来 + +前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。 + +并发编程中得了解的三个问题,可见性,原子性,有序性。volatile 原本的语义是禁用cpu缓存,也就是导致可见性的源头。原子性一般通过锁机制解决。 + +volatile 关键字通过内存屏障禁止了指令的重排序,并在单个核心中,强制数据的更新及时更新到缓存。在此基础上,依靠多核心处理器的缓存一致性协议等机制,保证了变量的可见性。 + +这里介绍几个状态协议,先从最简单的开始,MESI协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。 + +MESI 这种协议在数据更新后,会标记其它共享的CPU缓存的数据拷贝为Invalid状态,然后当其它CPU再次read的时候,就会出现 cache miss 的问题,此时再从内存中更新数据。 + + +### synchronized 和 volatile 的区别是什么? +- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 +- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 +- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 +- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 ## happens-before 原则(先行发生原则) @@ -286,26 +338,48 @@ CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以 hread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始 +### as-if-serial规则和happens-before规则的区别 +as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 +as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 + +as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 ## Lock 和synchronized 的区别 -1. Lock 是一个 接口,而 synchronized 是 Java 中的 关键字, -synchronized 是 内置的语言实现; -2. synchronized 在 发生异常时,会 自动释放线程占有的锁,因此 不会导 -致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放 -锁,则很 可能造成死锁现象,因此用 使用 Lock 时需要在 finally 块中释放锁; +- Synchronized是关键字,是JVM层面的底层实现,而Lock是个接口,是JDK层面的有丰富API +- Synchronized会自动释放锁,而Lock必须手动释放锁 +- Synchronized不可中断,Synchronized可以中断也可以不中断。 +- 通过Lock可以知道线程没有拿到锁,而Synchronized不可以 +- Synchronized可以锁住方法和代码块,而Lock只能锁住代码块 +- Synchronized是非公平锁,Lock是可以控制是否公平锁 + + +### 什么情况下可以使用 ReentrantLock +- 使用synchronized 的一些限制: +- 无法中断正在等候获取一个锁的线程; +- 无法通过投票得到一个锁; +- 释放锁的操作只能与获得锁所在的代码块中进行,无法在别的代码块中释放锁; +- ReentrantLock 没有以上的这些限制,且必须是手工释放锁。 + +## 什么时候用重入锁,什么时候用非重入锁? +可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 + +不可重入锁,也可以叫非递归锁,就是拿不到锁的情况会不停自旋循环检测来等待,不进入内核态沉睡,而是在用户态自旋尝试。 +- 可重入锁的作用就是为了避免死锁 +- 非重入锁(自旋锁)比较适用于锁使用者保持锁时间比较短的情况,这种情况下自旋锁的效率要远高于互斥锁 + + + +## AQS是如何唤醒下一个线程的? +看出当前线程是否需要阻塞: +1. 如果当前线程节点的前驱节点为SINGAL状态,则表明当前线程处于等待状态,返回true,当前线程阻塞 +2. 如果当前线程节点的前驱节点状态为CANCELLED(值为1),则表明前驱节点线程已经等待超时或者被中断,此时需要将该节点从同步队列中移除掉。最后返回false +3. 如果当前节点节点前驱节点非SINGAL,CANCELLED状态,则通过CAS将其前驱节点的等待状态设置为SINGAL,返回false。 + +当线程释放同步状态后,则需要唤醒该线程的后继节点: + +可能会存在当前线程的后继节点为null,超时、被中断的情况,如果遇到这种情况了,则需要跳过该节点,但是为何是从tail尾节点开始,而不是从node.next开始呢?原因在于node.next仍然可能会存在null或者取消了,所以采用tail回溯办法找第一个可用的线程。最后调用LockSupport的unpark(Thread thread)方法唤醒该线程。 -3. Lock 可以让 等待锁的线程响应中断 (可中断锁),而 synchronized -却不行,使用 synchronized 时,等待的线程会一直等待下去, 不能够响应中 -断 (不可中断锁); -4. 通过 Lock 可以知道 有没有成功获取锁 (tryLock ( ) 方法 : 如果获取 -了锁 ,回 则返回 true ;回 否则返回 false e, , 也就说这个方法无论如何都会立即返回 。 -在拿不到锁时不会一直在那等待。 ),而 synchronized 却无法办到。 -5. Lock 可以提高 多个线程进行读操作的效率( 读写锁)。 -6. Lock 可以实现 公平锁,synchronized 不保证公平性。 -在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 -当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 -于 synchronized。所以说,在具体使用时要根据适当情况选择。 @@ -344,6 +418,44 @@ ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要 ThreadLocal.ThreadLocalMap.Entry中的key是弱引用的,也即是当某个ThreadLocal对象不存在强引用时,就会被GC回收,但是value是基于强引用的,所以当key被回收,但是value还存在其他强引用时,就会出现内存的泄露情况,在最新的ThreadLocal中已经做出了修改,即在调用set、get、remove方法时,会清除key为null的Entry,但是如果不调用这些方法,仍然还是会出现内存泄漏 :),所以要养成用完ThreadLocal对象之后及时remove的习惯。 +### 父子线程传递Threadlcoal值的问题 +InheritableThreadLocal为什么能解决父子线程传递Threadlcoal值的问题。 +- 在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量 +- 在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去 +- 因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值 + +``` +public class TestInheritableThreadLocal implements Runnable { + private static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>(); + + public static void main(String[] args) { + System.out.println("----主线程设置值为\"主线程\""); + threadLocal.set("主线程"); + System.out.println("----主线程设置后获取值:" + threadLocal.get()); + Thread tt = new Thread(new TestInheritableThreadLocal()); + tt.start(); + System.out.println("----主线程结束"); + + } + + @Override + public void run() { + System.out.println("----子线程设置值前获取:" + threadLocal.get()); + System.out.println("----新开线程设置值为\"子线程\""); + threadLocal.set("子线程"); + System.out.println("----新开的线程设置值后获取:" + threadLocal.get()); + } +} +``` + +- InheritableThreadLocal的源码非常简单,继承自ThreadLocal,重写其中三个方法。 +- InheritableThreadLocal本身并没做什么操作,唯一的可能就是Thread里做了手脚。**目前的需求是要求将当前线程里的ThreadLocalMap共享到新开的线程**,那么,因为不知道用户何时使用这个数据,所以**新开的线程创建好后就必须能访问到这些数据**。 +- 如果当前线程的inheritableThreadLocals != null,新线程:this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals) +传入当前线程的inheritableThreadLocals 。 + + + + ## 通过Callable和Future创建线程 Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。 @@ -394,73 +506,25 @@ thread.stop(); 注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。 +## Java 中 interrupted 和 isInterrupted 方法的区别? +- interrupt:将被置为”中断”状态 -## 什么是线程安全?什么是线程不安全? -1. 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 -2. 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据 -在多线程的情况下,由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 - -## I/O多路复用 -单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。尽量多的提高服务器的吞吐能力 - -select, poll, epoll 都是I/O多路复用的具体的实现 - - -## 讲一下netty -netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件 - - -## Nio的原理(同步非阻塞) -服务端和客户端各自维护一个管理通道的对象,我们称之为 selector,该对 -象能检测一个或多个通道(channel)上的事件。我们以服务端为例,如果服务 -端的 selector 上注册了读事件,某时刻客户端给服务端送了一些数据,阻塞 I/O -这时会调用 read()方法阻塞地读取数据,而 NIO 的服务端会在 selector 中添加 -一个读事件。服务端的处理线程会轮询地访问 selector,如果访问 selector 时发 -现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处 -理线程会一直阻塞直到感兴趣的事件到达为止。 -![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-4.jpg) +注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处。**支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。 -## 缓冲区Buffer、通道Channel、选择器Selector -缓冲区Buffer +- interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。 -- 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。 +- isInterrupted:查看当前中断信号是true还是false -通道Channel -- 通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于 通道是双向 -的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 -OutputStream 的子类,比如 InputStream 只能进行读取操作,OutputStream -只能进行写操作),而通道是双向的,可以用于读、写或者同时用于读写。 -选择器(Selector ) -- NIO 有一个主要的类 Selector,这个类似一个观察者,只要我们把需要探知 -的 socketchannel 告诉 Selector,我们接着做别的事情, 当有事件发生时,他会 -通知我们,传回一组 SelectionKey, 我们读取这些 Key, 就会获得我们刚刚注册 -过的 socketchannel, 然后,我们从这个 Channel 中读取数据,放心,包准能 -够读到,接着我们可以处理这些数据。 -- Selector 内部原理实际是在做一个 对所注册的 channel 的轮询访问,不断 -地轮询,一旦轮询到一个 channel 有所注册的事情发生,比如数据来了,他就 -会站起来报告, 交出一把钥匙,让我们 通过这把钥匙来读取这个 channel 的内 -容。 -## BIO和NIO的区别 -1. BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 -2. NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 +## 线程的 sleep()方法和 yield()方法有什么区别? +1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; +2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; +3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; +4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 -## NIO的selector作用 -Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。 - -为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件: - -1. connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8) -2. accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16) -3. read:读事件,对应值为SelectionKey.OP_READ(1) -4. write:写事件,对应值为SelectionKey.OP_WRITE(4) - -每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。 - -所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。 ## final域的内存语义 @@ -494,6 +558,190 @@ reader() 方法包含三个操作: 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。 - 尽量使用 notifyAll(),notify()可能会导致死锁 +## JUC +### ConcurrentHashMap是如何在保证并发安全的同时提高性能 +其实就是要控制锁的粒度,尽量避免锁的发生 + +ConcurrentHashMap使用了一些技巧来获取高的并发性能,同时避免了锁。这些技巧包括: +1. 使用CAS乐观锁和volatile代替RentrantLock +2. spread二次哈希进行segment分段。 +3. stream提高并行处理能力。 + +### 为什么java.util.concurrent 包里没有并发的ArrayList实现? + +我认为在java.util.concurrent包中没有加入并发的ArrayList实现的主要原因是:**很难去开发一个通用并且没有并发瓶颈的线程安全的List。** + +像ConcurrentHashMap这样的类的真正价值(The real point / value of classes)并不是它们保证了线程安全。而在于它们在**保证线程安全的同时不存在并发瓶颈**。举个例子,ConcurrentHashMap采用了锁分段技术和弱一致性的Map迭代器去规避并发瓶颈。 + +所以问题在于,像“Array List”这样的数据结构,你不知道如何去规避并发的瓶颈。拿contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list? + +另一方面,Queue 和Deque (基于Linked List)有并发的实现是因为他们的接口相比List的接口有更多的限制,这些限制使得实现并发成为可能。 + +CopyOnWriteArrayList是一个有趣的例子,它规避了只读操作(如get/contains)并发的瓶颈,但是它为了做到这点,在修改操作中做了很多工作和修改可见性规则。 此外,修改操作还会锁住整个List,因此这也是一个并发瓶颈。所以从理论上来说,CopyOnWriteArrayList并不算是一个通用的并发List。 + +### ConcurrentModificationException异常出现的原因 +原因:如果modCount不等于expectedModCount,则抛出ConcurrentModificationException异常。 +关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。 + +### fail-fast机制 +这种机制经常出现在多线程环境下 , 当前线程会维护一个计数比较器, 即 expectedModCount, 记录已经修改的次数。在进入遍历前, 会把实时修改次数 modCount 赋值给 expectedModCount,如果这两个数据不相等 , 则抛出异常。 + +Iterator、COW(Copy-on-write)是 fail-safe机制的 + +#### 1、在单线程环境下的解决办法 +使用iterator删除,并且调用iterator的remove方法,不是list的remove方法 +#### 2、在多线程环境下的解决方法 +1、在使用iterator迭代的时候使用synchronized或者Lock进行同步; +2、使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。 + +### 比AtomicLong更高性能的LongAdder +LongAdder在高并发的场景下会比它的前辈————AtomicLong 具有更好的性能,代价是消耗更多的内存空间 + +AtomicLong在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况。 + +LongAdder的基本思路就是**分散热点**,将value值分散到一个数组中,不同线程会命中到数组的不同槽中**,各个线程只对自己槽中的那个值进行CAS操作**,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。 + +ConcurrentHashMap中的“分段锁”其实就是类似的思路。 + + + + +## 两个线程同时执行i++100次,结果是多少 +可能的结果:最小为2,最大为200 + +i++这种操作并不是原子性的, 实际上它的操作是首先从内存中取出数据放在cpu寄存器中进行计算, 然后再将计算好的结果返回到内存中。 + +**最小值2的分析:** +- 假设两个线程a,b +- 首先a执行99次,i为99,在未被写入内存时,b取i=0时执行1次,写入内存后i=1,此时覆盖掉了i=99的值; +- 然后a取i=1执行1次,b取i=1执行99次,当a比b后写入内存时,a覆盖掉b,此时i=2 + + + + +## 如何排查死锁? +使用 jps + jstack +- jps -l +- jstack -l 12316 + + + + + + + +## 线程池 +Executor线程池框架是一个根据一组**执行策略调用,调度,执行和控制**的异步任务的框架。 + +### ThreadPoolExecutor执行的策略 + +1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务 +2. 线程数量达到了corePools,则将任务移入队列等待 +3. 队列已满,新建线程(非核心线程)执行任务 +4. 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常 + +新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略 + + +### 常见四种线程池 + + +1. CachedThreadPool():可缓存线程池。 + - 线程数无限制 + - 有空闲线程则复用空闲线程,若无空闲线程则新建线程 + - 一定程序减少频繁创建/销毁线程,减少系统开销 + +2. FixedThreadPool():定长线程池。 +- 可控制线程最大并发数(同时执行的线程数) +- 超出的线程会在队列中等待 + +3. ScheduledThreadPool():定时线程池。 +- 支持定时及周期性任务执行。 + +4. SingleThreadExecutor():单线程化的线程池。 +- 有且仅有一个工作线程执行任务 +- 所有任务按照指定顺序执行,即遵循队列的入队出队规则 + +### 四种线程池使用场景 + +1. newSingleThreadExecutor:适用于串行执行任务的场景 +2. newFixedThreadExecutor:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般Ncpu + 1 +3. newCachedThreadExecutor:适用于北方执行大量短期的小任务 +4. newScheduledThreadExecutor:适用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景 + + + +### 四种拒绝策略 + +1. AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略 +2. DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式。 +3. DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行 +4. CallerRunPolicy:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用 + + + +### 为什么要用线程池 + +1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 +2. 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 +3. 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现 + +### 线程池的非核心线程什么时候会被释放 +当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过 keepAliveTime。 + + +### Executorshe和ThreaPoolExecutor创建线程池的区别 +- Executors 各个方法的弊端: +1. newFixedThreadPool 和 newSingleThreadExecutor: +主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。 +2. newCachedThreadPool 和 newScheduledThreadPool: + 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。 + +- ThreaPoolExecutor +1. 创建线程池方式只有一种,就是走它的构造函数,参数自己指定 + +### 线程池ThreadPoolExecutor参数设置 +参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数: +- tasks,每秒需要处理的的任务数 +- tasktime,处理每个任务花费的时间 +- responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒。 + +#### corePoolSize +每个任务需要tasktime秒处理,则每个线程每钞可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime),即tasks*tasktime个线程数。 + +假设系统每秒任务数为100 ~ 1000,每个任务耗时0.1秒,则需要100 * 0.1至1000 * 0.1,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数小于200,最多时为1000,则corePoolSize可设置为20。 + +#### maxPoolSize + +当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60。 + +#### queueCapacity + +任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为(corePoolSize/tasktime)*responsetime: (20/0.1)*2=400,即队列长度可设置为400。 + +队列长度设置过大,会导致任务响应时间过长,切忌以下写法: +```java +LinkedBlockingQueue queue = new LinkedBlockingQueue(); +``` +这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。 + + +#### keepAliveTime + +当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程。 + + +#### allowCoreThreadTimeout + +默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。 + + +如果涉及到有突发流量的场景,又该如何设置? + + + + + diff --git "a/notes/java/Java\350\231\232\346\213\237\346\234\272.md" "b/notes/java/Java\350\231\232\346\213\237\346\234\272.md" index 45dc7ed..5912769 100644 --- "a/notes/java/Java\350\231\232\346\213\237\346\234\272.md" +++ "b/notes/java/Java\350\231\232\346\213\237\346\234\272.md" @@ -13,46 +13,62 @@ - [三者区别](#%E4%B8%89%E8%80%85%E5%8C%BA%E5%88%AB) - [垃圾回收](#%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6) - [GC垃圾收集器](#gc%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8) - - [Serial垃圾收集器(单线程、复制算法)](#serial%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95) - - [ParNew垃圾收集器(Serial + 多线程)](#parnew%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8serial--%E5%A4%9A%E7%BA%BF%E7%A8%8B) - - [Parllel Scavenge收集器 (多线程、复制算法)](#parllel-scavenge%E6%94%B6%E9%9B%86%E5%99%A8-%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95) - - [Serial Old收集器(单线程标记整理算法)](#serial-old%E6%94%B6%E9%9B%86%E5%99%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95) - - [Parallel Old收集器(多线程标记整理算法)](#parallel-old%E6%94%B6%E9%9B%86%E5%99%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95) + - [JVM 有哪些垃圾回收器?](#jvm-%E6%9C%89%E5%93%AA%E4%BA%9B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8) + - [新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?](#%E6%96%B0%E7%94%9F%E4%BB%A3%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E5%92%8C%E8%80%81%E5%B9%B4%E4%BB%A3%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) - [CMS收集器(多线程标记清除算法)](#cms%E6%94%B6%E9%9B%86%E5%99%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%A0%87%E8%AE%B0%E6%B8%85%E9%99%A4%E7%AE%97%E6%B3%95) - [CMS的缺点](#cms%E7%9A%84%E7%BC%BA%E7%82%B9) - [CMS的使用场景](#cms%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) - [G1收集器](#g1%E6%94%B6%E9%9B%86%E5%99%A8) - - [G1对比CMS的区别](#g1%E5%AF%B9%E6%AF%94cms%E7%9A%84%E5%8C%BA%E5%88%AB) + - [G1对比CMS的区别](#g1%E5%AF%B9%E6%AF%94cms%E7%9A%84%E5%8C%BA%E5%88%AB) - [Major GC和Full GC的区别是什么?触发条件呢?](#major-gc%E5%92%8Cfull-gc%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6%E5%91%A2) - [什么时候会触发full gc](#%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E8%A7%A6%E5%8F%91full-gc) - [可以作为root的对象](#%E5%8F%AF%E4%BB%A5%E4%BD%9C%E4%B8%BAroot%E7%9A%84%E5%AF%B9%E8%B1%A1) - [新生代转移到老年代的触发条件](#%E6%96%B0%E7%94%9F%E4%BB%A3%E8%BD%AC%E7%A7%BB%E5%88%B0%E8%80%81%E5%B9%B4%E4%BB%A3%E7%9A%84%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6) - - [G1和CMS的区别](#g1%E5%92%8Ccms%E7%9A%84%E5%8C%BA%E5%88%AB) + - [什么情况对象直接在老年代分配](#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E5%AF%B9%E8%B1%A1%E7%9B%B4%E6%8E%A5%E5%9C%A8%E8%80%81%E5%B9%B4%E4%BB%A3%E5%88%86%E9%85%8D) + - [高吞吐量的话用哪种gc算法](#%E9%AB%98%E5%90%9E%E5%90%90%E9%87%8F%E7%9A%84%E8%AF%9D%E7%94%A8%E5%93%AA%E7%A7%8Dgc%E7%AE%97%E6%B3%95) + - [GC分代年龄为什么最大为15?](#gc%E5%88%86%E4%BB%A3%E5%B9%B4%E9%BE%84%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%80%E5%A4%A7%E4%B8%BA15) + - [Minor GC ,Full GC 触发条件是什么?](#minor-gc-full-gc-%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6%E6%98%AF%E4%BB%80%E4%B9%88) + - [Minor GC触发条件](#minor-gc%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6) + - [Full GC触发条件](#full-gc%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6) + - [动态对象年龄判定](#%E5%8A%A8%E6%80%81%E5%AF%B9%E8%B1%A1%E5%B9%B4%E9%BE%84%E5%88%A4%E5%AE%9A) - [类加载](#%E7%B1%BB%E5%8A%A0%E8%BD%BD) - - [双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制](#%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E6%B3%95%E7%94%A8%E6%88%B7%E5%A6%82%E4%BD%95%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8-%E6%80%8E%E4%B9%88%E6%89%93%E7%A0%B4%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%89%98%E6%9C%BA%E5%88%B6) + - [Java类加载机制](#java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6) + - [JVM加载Class文件的原理机制](#jvm%E5%8A%A0%E8%BD%BDclass%E6%96%87%E4%BB%B6%E7%9A%84%E5%8E%9F%E7%90%86%E6%9C%BA%E5%88%B6) + - [什么是类加载器,类加载器有哪些?](#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E6%9C%89%E5%93%AA%E4%BA%9B) + - [类装载的执行过程](#%E7%B1%BB%E8%A3%85%E8%BD%BD%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) + - [双亲委派模型](#%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B) + - [双亲委派机制的作用](#%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%E7%9A%84%E4%BD%9C%E7%94%A8) + - [双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制](#%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E6%B3%95%E7%94%A8%E6%88%B7%E5%A6%82%E4%BD%95%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8-%E6%80%8E%E4%B9%88%E6%89%93%E7%A0%B4%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%89%98%E6%9C%BA%E5%88%B6) + - [在什么情况下需要自定义类加载器呢?](#%E5%9C%A8%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%91%A2) - [内存溢出](#%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA) - [原因](#%E5%8E%9F%E5%9B%A0) - - [解决方法](#%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95) + - [OOM可能发生在哪,怎么查看,怎么调优?](#oom%E5%8F%AF%E8%83%BD%E5%8F%91%E7%94%9F%E5%9C%A8%E5%93%AA%E6%80%8E%E4%B9%88%E6%9F%A5%E7%9C%8B%E6%80%8E%E4%B9%88%E8%B0%83%E4%BC%98) - [栈溢出](#%E6%A0%88%E6%BA%A2%E5%87%BA) - [解决办法](#%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95) - [java应用系统运行速度慢的解决方法](#java%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E8%BF%90%E8%A1%8C%E9%80%9F%E5%BA%A6%E6%85%A2%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95) - [逃逸分析](#%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90) +- [泛型和类型擦除的关系](#%E6%B3%9B%E5%9E%8B%E5%92%8C%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E7%9A%84%E5%85%B3%E7%B3%BB) - [编译](#%E7%BC%96%E8%AF%91) - [即时编译器的优化方法](#%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91%E5%99%A8%E7%9A%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95) - [编译过程的五个阶段](#%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E7%9A%84%E4%BA%94%E4%B8%AA%E9%98%B6%E6%AE%B5) - [JVM、Java编译器和Java解释器](#jvmjava%E7%BC%96%E8%AF%91%E5%99%A8%E5%92%8Cjava%E8%A7%A3%E9%87%8A%E5%99%A8) - [JIT 编译过程](#jit-%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B) - - [Graal 的实现](#graal-%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [GraalVM 中的 Ahead-Of-Time(AOT)](#graalvm-%E4%B8%AD%E7%9A%84-ahead-of-timeaot) -- [JVM的Intrinsics方法](#jvm%E7%9A%84intrinsics%E6%96%B9%E6%B3%95) -- [JVM的invokedynamic方法](#jvm%E7%9A%84invokedynamic%E6%96%B9%E6%B3%95) -- [方法句柄](#%E6%96%B9%E6%B3%95%E5%8F%A5%E6%9F%84) + - [Java 源代码是怎么被机器识别并执行的呢?](#java-%E6%BA%90%E4%BB%A3%E7%A0%81%E6%98%AF%E6%80%8E%E4%B9%88%E8%A2%AB%E6%9C%BA%E5%99%A8%E8%AF%86%E5%88%AB%E5%B9%B6%E6%89%A7%E8%A1%8C%E7%9A%84%E5%91%A2) + - [机器码和字节码区别](#%E6%9C%BA%E5%99%A8%E7%A0%81%E5%92%8C%E5%AD%97%E8%8A%82%E7%A0%81%E5%8C%BA%E5%88%AB) +- [JVM调优](#jvm%E8%B0%83%E4%BC%98) + - [如何进行JVM调优?](#%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8Cjvm%E8%B0%83%E4%BC%98) + - [JVM性能调优的6大步骤](#jvm%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98%E7%9A%846%E5%A4%A7%E6%AD%A5%E9%AA%A4) + - [JVM参数](#jvm%E5%8F%82%E6%95%B0) + - [如何查看Dump日志?怎么产生的?命令有哪些?](#%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8Bdump%E6%97%A5%E5%BF%97%E6%80%8E%E4%B9%88%E4%BA%A7%E7%94%9F%E7%9A%84%E5%91%BD%E4%BB%A4%E6%9C%89%E5%93%AA%E4%BA%9B) + - [如何生成java dump文件](#%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90java-dump%E6%96%87%E4%BB%B6) - [栈上分配和TLAB](#%E6%A0%88%E4%B8%8A%E5%88%86%E9%85%8D%E5%92%8Ctlab) - [栈上分配](#%E6%A0%88%E4%B8%8A%E5%88%86%E9%85%8D) - [线程私有分配区TLAB](#%E7%BA%BF%E7%A8%8B%E7%A7%81%E6%9C%89%E5%88%86%E9%85%8D%E5%8C%BAtlab) - [总体流程](#%E6%80%BB%E4%BD%93%E6%B5%81%E7%A8%8B) - [对象分配流程图](#%E5%AF%B9%E8%B1%A1%E5%88%86%E9%85%8D%E6%B5%81%E7%A8%8B%E5%9B%BE) - [Java 8: 从永久代(PermGen)到元空间(Metaspace)](#java-8-%E4%BB%8E%E6%B0%B8%E4%B9%85%E4%BB%A3permgen%E5%88%B0%E5%85%83%E7%A9%BA%E9%97%B4metaspace) + - [方法区和元空间是什么关系?](#%E6%96%B9%E6%B3%95%E5%8C%BA%E5%92%8C%E5%85%83%E7%A9%BA%E9%97%B4%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB) + - [为什么用元空间代替永久代?](#%E4%B8%BA%E4%BB%80%E4%B9%88%E7%94%A8%E5%85%83%E7%A9%BA%E9%97%B4%E4%BB%A3%E6%9B%BF%E6%B0%B8%E4%B9%85%E4%BB%A3) @@ -115,19 +131,22 @@ Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一 # 垃圾回收 ## GC垃圾收集器 -### Serial垃圾收集器(单线程、复制算法) -Serial(英文:连续)是最基本垃圾收集器,使用复制算法,曾经是 JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 +### JVM 有哪些垃圾回收器? +- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效; +- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; +- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景; +- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本; +- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; +- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 +- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。 + +### 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别? +- 新生代回收器:Serial、ParNew、Parallel Scavenge +- 老年代回收器:Serial Old、Parallel Old、CMS +- 整堆回收器:G1 + +新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 -Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。 -### ParNew垃圾收集器(Serial + 多线程) -ParNew(Parallel:平行的) 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃 圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。 - -ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。 - -ParNew 虽然是除了多线程外和 Serial 收集器几乎完全一样,但是 ParNew 垃圾收集器是很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。 -### Parllel Scavenge收集器 (多线程、复制算法) -### Serial Old收集器(单线程标记整理算法) -### Parallel Old收集器(多线程标记整理算法) ### CMS收集器(多线程标记清除算法) CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。 @@ -171,15 +190,18 @@ G1有三个明显特点:1、压缩空间强,避免碎片 2、空间使用更 3. 想要更可控、可预期的GC停顿周期:防止高并发应用雪崩现象 -#### G1对比CMS的区别 -1. G1在压缩空间方面有优势 -2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题 -3. Eden,Survivor,Old区不再固定、在内存使用效率上来说更灵活 -4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象,可驾驭度,G1 是可以设定GC 暂停的 target 时间的,根据预测模型选取性价比收益更高,且一定数目的 Region 作为 +### G1对比CMS的区别 +1. G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-8.jpg) +2. CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。 +3. G1在压缩空间方面有优势 +4. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题 +5. Eden,Survivor,Old区不再固定、在内存使用效率上来说更灵活 +6. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象,可驾驭度,G1 是可以设定GC 暂停的 target 时间的,根据预测模型选取性价比收益更高,且一定数目的 Region 作为 CSet,能回收多少便是多少。 -5. G1在回收内存后会马上同时做,合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做 -6. G1会在Young GC中使用、而CMS只能在O区使用 -7. SATB 算法在 remark 阶段延迟极低以及借助 RSet 的实现可以不做全堆扫描(G1 对大堆更友好)以外,最重要的是可驾驭度 +7. G1在回收内存后会马上同时做,合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做 +8. G1会在Young GC中使用、而CMS只能在O区使用 +9. SATB 算法在 remark 阶段延迟极低以及借助 RSet 的实现可以不做全堆扫描(G1 对大堆更友好)以外,最重要的是可驾驭度 ## Major GC和Full GC的区别是什么?触发条件呢? 针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种: @@ -228,21 +250,102 @@ public void doSomething(Object A){ } } ``` + + + ## 新生代转移到老年代的触发条件 1. 长期存活的对象 2. 大对象直接进入老年代 3. minor gc后,survivor仍然放不下 4. 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代 -## G1和CMS的区别 -1. G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。 -![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-8.jpg) -2. CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。 +## 什么情况对象直接在老年代分配 +1. 分配的对象大小大于eden space。适合所有收集器。 +2. eden space剩余空间不足分配,且需要分配对象内存大小不小于eden space总空间的一半,直接分配到老年代,不触发Minor GC。适合-XX:+UseParallelGC、-XX:+UseParallelOldGC,即适合Parallel Scavenge。 +3. 大对象直接进入老年代,使用-XX:PretenureSizeThreshold参数控制,适合-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,即适合Serial和ParNew收集器。 + +## 高吞吐量的话用哪种gc算法 +复制清除 + +## GC分代年龄为什么最大为15? +因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15 + +## Minor GC ,Full GC 触发条件是什么? +Minor GC ,Full GC 触发条件 +- 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC; +- 对老年代GC称为Major GC; +- 而Full GC是对整个堆来说的; + +在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略。 + +### Minor GC触发条件 +- 当Eden区满时,触发Minor GC。 + +### Full GC触发条件 +(1) **System.gc()方法的调用** + +此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。 + +(2) **老年代空间不足** + +旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 + + +(3) **通过Minor GC后进入老年代的平均大小大于老年代的可用内存** + +如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC + +(4) **对象太大,年轻代容不下** + +## 动态对象年龄判定 +虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold(jdk1.7默认是15)中要求的年龄。 # 类加载 +## Java类加载机制 +虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 + +## JVM加载Class文件的原理机制 +Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 + +类装载方式,有两种 : +1. 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, +2. 显式装载, 通过class.forname()等方法,显式加载需要的类 + +Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。 + +## 什么是类加载器,类加载器有哪些? +实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 + +主要有一下四种类加载器: +- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 +- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 +- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 +- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。 + +## 类装载的执行过程 +类装载分为以下 5 个步骤: +1. 加载:根据查找路径找到相应的 class 文件然后导入; +2. 验证:检查加载的 class 文件的正确性; +3. 准备:给类中的静态变量分配内存空间; +4. 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址; +5. 初始化:对静态变量和静态代码块执行初始化工作。 + + +## 双亲委派模型 +在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。 -## 双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-22.png) +双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 + +当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。 + +### 双亲委派机制的作用 +1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 +2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。 + + +### 双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制 1. 双亲委派模型中用到的方法: - findLoadedClass(), - loadClass() @@ -263,7 +366,11 @@ public void doSomething(Object A){ - +## 在什么情况下需要自定义类加载器呢? +1. 隔离加载类。 在某些框架内进行中间件与应用的模块隔离 , 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar包不会影响到中间件运行时使用的 jar 包。 +2. 修改类加载方式。 类的加载模型并非强制 ,除Bootstrap 外 , 其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。 +3. 扩展加载源。 比如从数据库、网络,甚至是电视机机顶盒进行加载。 +4. 防止源码泄露。 Java代码容易被编译和篡改,可以进行编译加密。 那么类加载器也需要自定义,还原加密的字节码。 @@ -280,30 +387,21 @@ public void doSomething(Object A){ 4. 使用的第三方软件中的BUG; 5. 启动参数内存值设定的过小; +## OOM可能发生在哪,怎么查看,怎么调优? +**原因不外乎有两点** +1. 分配的少了:比如虚拟机本身可使用的内存太少。 +2. 应用用的太多,并且用完没释放 -## 解决方法 -内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。 - -第一步,就是修改JVM启动参数,直接增加内存。JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“OutOfMemory”错误。因此,-Xms,-Xmx参数一定不要忘记加。 - -第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。 - -第三步,找出可能发生内存溢出的位置。重点排查以下几点: +**处理方法** +1. 先把内存镜像dump出来,有两种方法 + - 设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息 + - 使用jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取 +2.得到内存信息文件后就使用工具去分析,也有两个工具 + - mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具 + - jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等 -1. 检查代码中是否有死循环或递归调用。 - -2. 检查是否有大循环重复产生新对象实体。 - -3. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。 - -4. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。 - - -第四步,使用内存查看工具动态查看内存使用情况。 - -内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。 @@ -333,7 +431,19 @@ public void doSomething(Object A){ 逃逸分析可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配,由于该对象一定是局部的,所以栈上分配不会有问题。 +# 泛型和类型擦除的关系 +**Java泛型的实现方法:类型擦除** +**Java的泛型是伪泛型**。因为,在编译期间,所有的泛型信息都会被擦除掉。 + +Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。 + +Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型。 +**类型擦除引起的问题**: +- 1、先检查,在编译,以及检查编译的对象和引用传递的问题,java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的 +- 2、类型擦除与多态的冲突和解决方法:桥方法 +- 3、泛型类型变量不能是基本数据类型 +- 4、泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数 # 编译 @@ -398,85 +508,80 @@ Java字节码的执行有两种方式: 2. 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的 profile; 3. 将生成的二进制码部署至代码缓存(code cache)里。 +## Java 源代码是怎么被机器识别并执行的呢? +是目前 OpenJDK 使用的主流 NM, 它采用解释与编译混合执行的模式, 其 JIT 技术采用分层编译, 极大地提升了 Java的执行速度。 -许多开发者会觉得用 C++ 写的 C2 肯定要比 Graal 快。实际上,在充分预热的情况下,Java 程序中的热点代码早已经通过即时编译转换为二进制码,在执行速度上并不亚于静态编译的 C++ 程序。 +## 机器码和字节码区别 +高级程序代码 ---(编译器)---> 字节码 ---(解释器)---> 机器码 -## Graal 的实现 -HotSpot集成了两个JIT compiler — C1及C2(或称为Client及Server)。两者的区别在于,前者没有应用激进的优化技术,因为这些优化往往伴随着耗时较长的代码分析。因此,C1的编译速度较快,而C2所编译的方法运行速度较快。 +机器码:是电脑CPU直接读取运行的机器指令,运行速度最快。 +字节码:是一种中间状态(中间码)的二进制代码(文件)。需解释器(也叫直译器)转译后才能成为机器码 -Java 7引入了tiered compilation的概念,综合了C1的高启动性能及C2的高峰值性能。这两个JIT compiler以及interpreter将HotSpot的执行方式划分为五个级别: -1. level 0:interpreter解释执行 -2. level 1:C1编译,无profiling -3. level 2:C1编译,仅方法及循环back-edge执行次数的profiling -4. level 3:C1编译,除level 2中的profiling外还包括branch(针对分支跳转字节码)及receiver type(针对成员方法调用或类检测,如checkcast,instnaceof,aastore字节码)的profiling -5. level 4:C2编译 +**字节码在运行时通过JVM(JAVA虚拟机)做一次转换生成机器指令,也就是说虚拟机执行字节码指令时是通过生成机器码交付给硬件执行** -Graal可替换C2成为HotSpot的顶层JIT compiler,即上述level 4。与C2相比,Graal采用更加激进的优化方式,因此当程序达到稳定状态后,其执行效率(峰值性能)将更有优势。 -Graal 和 C2 最为明显的一个区别是:Graal 是用 Java 写的,许多C2中实现的优化均被移植到Graal中.而 C2 是用 C++ 写的。相对来说,Graal 更加模块化,也更容易开发与维护。在充分预热的情况下,Java 程序中的热点代码早已经通过即时编译转换为二进制码,在执行速度上并不亚于静态编译的 C++ 程序。即便是解释执行 Graal,也仅是会减慢编译效率,而并不影响编译结果的性能。Graal 和 C2 另一个优化上的分歧则是方法内联算法。相对来说,Graal 的内联算法对新语法、新语言更加友好,例如 Java 8 的 lambda 表达式以及 Scala 语言。 +Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令(机器码)执行。这就是Java的能够“一次编译,到处运行”的原因。 -Graal 编译器将编译过程分为前端和后端两大部分。前端用于实现平台无关的优化(如方法内联),以及小部分平台相关的优化;而后端则负责大部分的平台相关优化(如寄存器分配),以及机器码的生成。 -Graal 和 C2 都采用了 Sea-of-Nodes IR。严格来说,这里指的是 Graal 的前端,而后端采用的是另一种非 Sea-of-Nodes 的 IR。通常,我们将前端的 IR 称之为 High-level IR,或者 HIR;后端的 IR 则称之为 Low-level IR,或者 LIR。 +字节码必须通过类加载过程加载到 JVM 环境后,才可以执行。执行有三种模式 第一,解释执行,第二, JIT编译执行 第三, JIT编译与解释混合执行(主流JVM 默认执行模式) -Graal 是一个用 Java 写就的、并能够将 Java 字节码转换成二进制码的即时编译器。它通过 JVMCI 与 Java 虚拟机交互,响应由后者发出的编译请求、完成编译并部署编译结果。 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-21.png) -对 Java 程序而言,Graal 编译结果的性能略优于 OpenJDK 中的 C2;对 Scala 程序而言,它的性能优势可达到 10%(企业版甚至可以达到 20%!)。这背后离不开 Graal 所采用的激进优化方式。 +# JVM调优 +## 如何进行JVM调优? -这种基于假设的优化手段。在编译过程中,Graal 支持自定义假设,并且直接与去优化节点相关联。 +**何时进行JVM调优** +- Heap内存(老年代)持续上涨达到设置的最大内存值; +- Full GC 次数频繁; +- GC 停顿时间过长(超过1秒); +- 应用出现OutOfMemory 等内存异常; +- 应用中有使用本地缓存且占用大量内存空间; +- 系统吞吐量与响应性能不高或下降。 -## GraalVM 中的 Ahead-Of-Time(AOT) -GraalVM 是一个高性能的、支持多种编程语言的执行环境。它既可以在传统的 OpenJDK 上运行,也可以通过 AOT(Ahead-Of-Time)编译成可执行文件单独运行,甚至可以集成至数据库中运行。 -˚ -即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。 +**JVM调优目标**: +- 延迟:GC低停顿和GC低频率; +- 低内存占用; +- 高吞吐量; -而AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。它的成果可以是需要链接至托管环境中的动态共享库,也可以是独立运行的可执行文件。 +**JVM调优的步骤**: +- 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点; +- 确定JVM调优量化目标; +- 确定JVM调优参数(根据历史JVM参数来调整); +- 依次调优内存、延迟、吞吐量等指标; +- 对比观察调优前后的差异; +- 不断的分析和调整,直到找到合适的JVM参数配置; +- 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。 -AOT 编译的优点:无须在运行过程中耗费 CPU 资源来进行即时编译,而程序也能够在启动伊始就达到理想的性能。 +## JVM性能调优的6大步骤 +1. 监控GC的状态 +2、生成堆的dump文件 +3、分析dump文件 +4、分析结果,判断是否需要优化 +5、调整GC类型和内存分配 +6、不断的分析和调整 -AOT 编译的缺点:AOT 编译无法得知程序运行时的信息,因此也(1)无法进行基于类层次分析的完全虚方法内联,或者(2)基于程序 profile 的投机性优化(并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行的程序 profile 来绕开这两个限制)。这两者都会影响程序的峰值性能。 +## JVM参数 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/Java-20.png) +## 如何查看Dump日志?怎么产生的?命令有哪些? -Java 9 引入了实验性 AOT 编译工具jaotc。它借助了 Graal 编译器,将所输入的 Java 类文件(class字节码文件)转换为机器码,并存放至生成的动态共享库之中 +Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中。主要是用来在系统中出现异常或者崩溃的时候来生成dump文件,然后用调试器进行调试。 -源文件就是程序员们所编写出来的文件 程序员们能看懂的文件 -类文件则是利用java虚拟机生成的编译文件 是用来给机器看的机器语言 +如何dump出jvm日志。 +1. 在jvm启动的参数中,新增-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs/java.hprof jvm参数。这样在发生jvm 内存溢出时,就会直接dump出java.hprof 文件了。 +2. 直接导出jvm内存信息。 -# JVM的Intrinsics方法 +jmap -dump:format=b,file=/home/admin/logs/heap.hprof  javapid +推荐使用Eclipse插件Memory Analyzer Tool来打开heap.hprof文件。 +## 如何生成java dump文件 +1、JVM的配置文件中配置: +- 在应用启动时配置相关的参数 -XX:+HeapDumpOnOutOfMemoryError,当应用抛出OutOfMemoryError时生成dump文件 -在hotspot jvm里会定义一些intrinsic的方法,从而可以定义自己独有的一些编译的算法,根据不同的架构使用不同的指令集,比如Math.sin,Math.cos之类. +2、通过jmap执行指令,直接生成当前JVM的dmp文件 +- jmap -dump:file=文件名.dump [pid] -对每个方法hotspot jvm都会定义一个instrinisics id, 这个id可以用于区分java 里自己定义的lib类的方法还是用户自己定义的java的类的方法,用户自己写的类会用 vmIntrinsics::_none 来表示. - -CallGenerator是在hotspot jvm中方法调用的核心,不同运行方式是由不同的call generator决定的,而instrinsic_id又是决定不同的call generator的key. - -对Java自定义的lib库的方法,jvm 用了LibraryIntrinsic 作为lib库的CallGenerator, 在generate 函数的时候,初始化了LibraryCallKit,里面inline了很多lib的方法 - - - -# JVM的invokedynamic方法 -我们常用的JavaScript, Python, Ruby都可以归为动态语言,而Java, Bytecode都可以认为是静态语言。这两种语言最大的差别是变量和函数的类型是不是在程序运行中确定的。 - -invokedynamic 是 Java 7 引入的一条新指令,用以支持动态语言的方法调用。具体来说,它将调用点(CallSite)抽象成一个 Java 类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序。在运行过程中,每一条 invokedynamic 指令将捆绑一个调用点,并且会调用该调用点所链接的方法句柄。 - -在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成前面提到的调用点,并且将之绑定至该 invokedynamic 指令中。在之后的运行过程中,Java 虚拟机则会直接调用绑定的调用点所链接的方法句柄。 - -在 Java 8 中,Lambda 表达式也是借助 invokedynamic 来实现的。 -Lambda 表达式到函数式接口的转换是通过 invokedynamic 指令来实现的。该 invokedynamic 指令对应的启动方法将通过 ASM 生成一个适配器类。 -对于没有捕获其他变量的 Lambda 表达式,该 invokedynamic 指令始终返回同一个适配器类的实例。对于捕获了其他变量的 Lambda 表达式,每次执行 invokedynamic 指令将新建一个适配器类实例。 - -不管是捕获型的还是未捕获型的 Lambda 表达式,它们的性能上限皆可以达到直接调用的性能。其中,捕获型 Lambda 表达式借助了即时编译器中的逃逸分析,来避免实际的新建适配器类实例的操作。 - -# 方法句柄 -invokedynamic 底层机制的基石:方法句柄。 - -方法句柄是一个强类型的、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。 - -方法句柄可以通过 invokeExact 以及 invoke 来调用。其中,invokeExact 要求传入的参数和所指向方法的描述符严格匹配。方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。 - -方法句柄的调用和反射调用一样,都是间接调用,同样会面临无法内联的问题。 # 栈上分配和TLAB @@ -521,3 +626,13 @@ TLAB空间主要有3个指针:_start、_top、_end。_start指针表示TLAB空 由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。 +## 方法区和元空间是什么关系? +1. 首先,方法区是JVM规范的一个概念定义,并不是一个具体的实现,每一个JVM的实现都可以有各自的实现; +2. 然后,在Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区; +3. 也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义; + +然后多说一句,这个**元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的**,所以可以理论上物理机器还有多个内存就可以分配,而不用再受限于JVM本身分配的内存了。 +## 为什么用元空间代替永久代? +类的元数据信息(metadata)转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。 + +由于**类的元数据可以在本地内存(native memory)**之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。 diff --git a/notes/network/Computer-Network.md b/notes/network/Computer-Network.md index 9be35bc..847c17c 100644 --- a/notes/network/Computer-Network.md +++ b/notes/network/Computer-Network.md @@ -4,6 +4,7 @@ - [TCP](#tcp) - [TCP三次握手](#tcp%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B) + - [syn洪水攻击](#syn%E6%B4%AA%E6%B0%B4%E6%94%BB%E5%87%BB) - [TCP四次挥手](#tcp%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B) - [TCP如何保证可靠传输?](#tcp%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93) - [滑动窗口](#%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3) @@ -14,8 +15,12 @@ - [快速重传](#%E5%BF%AB%E9%80%9F%E9%87%8D%E4%BC%A0) - [快速恢复](#%E5%BF%AB%E9%80%9F%E6%81%A2%E5%A4%8D) - [tcp怎么保证包的顺序](#tcp%E6%80%8E%E4%B9%88%E4%BF%9D%E8%AF%81%E5%8C%85%E7%9A%84%E9%A1%BA%E5%BA%8F) + - [TCP协议规定2MSL等待的原因](#tcp%E5%8D%8F%E8%AE%AE%E8%A7%84%E5%AE%9A2msl%E7%AD%89%E5%BE%85%E7%9A%84%E5%8E%9F%E5%9B%A0) - [TCP粘包和拆包](#tcp%E7%B2%98%E5%8C%85%E5%92%8C%E6%8B%86%E5%8C%85) - [解决方法](#%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95) + - [TCP_NODELAY与Nagle算法](#tcp_nodelay%E4%B8%8Enagle%E7%AE%97%E6%B3%95) + - [为什么UDP不会粘包](#%E4%B8%BA%E4%BB%80%E4%B9%88udp%E4%B8%8D%E4%BC%9A%E7%B2%98%E5%8C%85) + - [TCP Keepalive](#tcp-keepalive) - [HTTP](#http) - [特点](#%E7%89%B9%E7%82%B9) - [HTTP/2.0 相比1.0的改进](#http20-%E7%9B%B8%E6%AF%9410%E7%9A%84%E6%94%B9%E8%BF%9B) @@ -63,6 +68,11 @@ TCP通过校验和、序列号、确认应答、重发控制、连接管理以 完成三次握手,客户端与服务器开始传送数据 +### syn洪水攻击 +在S返回一个确认的SYN-ACK包的时候,S可能由于各种原因不会接到C回应的ACK包。这个也就是所谓的半开放连接,S需要 耗费一定的数量的系统内存来等待这个未决的连接,虽然这个数量是受限,但是恶意者可以通过创建很多的半开放式连接来发动SYN洪水攻击 。 + + + ## TCP四次挥手 ![](https://user-gold-cdn.xitu.io/2018/5/2/1631fb807f2c6c1b?w=640&h=512&f=png&s=31059) @@ -174,7 +184,18 @@ TCP 利用滑动窗口实现流量控制。 2. 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。 ### tcp怎么保证包的顺序 -主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。 +1. 主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认, +2. 如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。 +3. 接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等, +4. 接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。 + + +### TCP协议规定2MSL等待的原因 +1、保证TCP协议的全双工连接能够可靠关闭 +2、保证这次连接的重复数据段从网络中消失 + + + @@ -192,6 +213,42 @@ TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP 3、其它复杂的协议,如RTMP协议等。 +### TCP_NODELAY与Nagle算法 +传统的传输模式很容易将窄窄的带宽挤满而丢包,再重传、再丢包的恶性循环。 + +于是Nagle算法通过先把数据缓存起来再一起发送,减少需要传输的数据包,提高效率,唯一的不足是,可能会有一些延迟。 + +启动TCP_NODELAY,就意味着禁用了Nagle算法,允许小包的发送,数据传输非常少,延时较低。 + +### 为什么UDP不会粘包 +1. TCP协议是面向流的协议,UDP是面向消息的协议 +2. UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据 +3. UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端口等信息) + + + + +## TCP Keepalive +**作用** +1. 探测连接的对端是否存活 +2. 防止中间设备因超时删除连接相关的连接表 + +定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序 + +上述的可定义变量,分别被称为保活时间、保活时间间隔和保活探测次数,默认设置是 7200 秒(2 小时)、75 秒和 9 次探测。 + +**如果开启了 TCP 保活,需要考虑以下几种情况:** +1. 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。 +2. 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。 +3. 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。 + +**问题:** +1. 在短暂的故障期间,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接 +2. 需要消耗额外的宽带和流量 +3. 在以流量计费的互联网环境中增加了费用开销 + +HTTP协议的Keep-Alive意图在于TCP连接复用,同一个连接上串行方式传递请求-响应数据;TCP的Keepalive机制意图在于探测连接的对端是否存活。 + # HTTP HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。 diff --git a/notes/os/Linux.md b/notes/os/Linux.md index b730609..a8f6ed4 100644 --- a/notes/os/Linux.md +++ b/notes/os/Linux.md @@ -2,37 +2,23 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Linux 系统的顶层目录结构](#linux-%E7%B3%BB%E7%BB%9F%E7%9A%84%E9%A1%B6%E5%B1%82%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84) +- [CPU 与内存](#cpu-%E4%B8%8E%E5%86%85%E5%AD%98) - [linux内核map图](#linux%E5%86%85%E6%A0%B8map%E5%9B%BE) - [Linux中软链接和硬链接的区别](#linux%E4%B8%AD%E8%BD%AF%E9%93%BE%E6%8E%A5%E5%92%8C%E7%A1%AC%E9%93%BE%E6%8E%A5%E7%9A%84%E5%8C%BA%E5%88%AB) - [kill进程杀不掉的原因](#kill%E8%BF%9B%E7%A8%8B%E6%9D%80%E4%B8%8D%E6%8E%89%E7%9A%84%E5%8E%9F%E5%9B%A0) - [swap分区的作用](#swap%E5%88%86%E5%8C%BA%E7%9A%84%E4%BD%9C%E7%94%A8) +- [Linux命令查找出日志文件中访问量最大的10个ip](#linux%E5%91%BD%E4%BB%A4%E6%9F%A5%E6%89%BE%E5%87%BA%E6%97%A5%E5%BF%97%E6%96%87%E4%BB%B6%E4%B8%AD%E8%AE%BF%E9%97%AE%E9%87%8F%E6%9C%80%E5%A4%A7%E7%9A%8410%E4%B8%AAip) +- [Linux常见命令](#linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4) +- [top详细](#top%E8%AF%A6%E7%BB%86) -## Linux 系统的顶层目录结构 -```java -/ 根目录 -├── bin 存放用户二进制文件 -├── boot 存放内核引导配置文件 -├── dev 存放设备文件 -├── etc 存放系统配置文件 -├── home 用户主目录 -├── lib 动态共享库 -├── lost+found 文件系统恢复时的恢复文件 -├── media 可卸载存储介质挂载点 -├── mnt 文件系统临时挂载点 -├── opt 附加的应用程序包 -├── proc 系统内存的映射目录,提供内核与进程信息 -├── root root 用户主目录 -├── sbin 存放系统二进制文件 -├── srv 存放服务相关数据 -├── sys sys 虚拟文件系统挂载点 -├── tmp 存放临时文件 -├── usr 存放用户应用程序 -└── var 存放邮件、系统日志等变化文件 -``` -Linux 与其他类 UNIX 系统一样并不区分文件与目录:目录是记录了其他文件名的文件。使用命 令 mkdir 创建目录时,若期望创建的目录的名称与现有的文件名(或目录名)重复,则会创建失败。 + +## CPU 与内存 +CPU ( Central Processing Unit )是一块超大规模的集成电路板,是计算机的核心部件,承载着计算机的主要运算和控制功能,是计算机指令的最终解释模块和执行模块。 +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/os-3.png) + + ## linux内核map图 @@ -85,4 +71,71 @@ Swap分区,即交换区,系统在物理内存不够时,与Swap进行交换 Swap配置对性能的影响 1. 分配太多的Swap空间会浪费磁盘空间,而Swap空间太少,则系统会发生错误 2. Swap空间应大于或等于物理内存的大小,最小不应小于64M,通常Swap空间的大小应是物理内存的2-2.5倍 -3. 如果有多个Swap交换区,Swap空间的分配会以轮流的方式操作于所有的Swap,这样会大大均衡IO的负载,加快Swap交换的速度。如果只有一个交换区,所有的交换操作会使交换区变得很忙,使系统大多数时间处于等待状态,效率很低。 \ No newline at end of file +3. 如果有多个Swap交换区,Swap空间的分配会以轮流的方式操作于所有的Swap,这样会大大均衡IO的负载,加快Swap交换的速度。如果只有一个交换区,所有的交换操作会使交换区变得很忙,使系统大多数时间处于等待状态,效率很低。 + + + +## Linux命令查找出日志文件中访问量最大的10个ip +linux 命令如下: +``` +cat test.log|awk -F" " '{print $2}'|sort|uniq -c|sort -nrk 1 -t' '|awk -F" " '{print $2}'|head -10 +``` + +问题剖析: +1. cat *.log将文本内容打印到屏幕 +2. 使用awk命令可以按照分割符将一行分割为多个列,第一列用$1表示,第二列用$2表示,依次类推awk -F" " '{print $2} //表示用空格作为分隔符进行分割,打印出第2列 +3. sort 进行排序,默认是按照ascii码进行排序的 +4. uniq -c 统计相邻的行的重复数量,结果是类似 3 127.13.13.13,前面的数字代码重复的行数sort|uniq -c //统计重复的行数 +5. sort -n是按照数值进行由小到大进行排序, -r是表示逆序,-t是指定分割符,-k是执行按照第几列进行排序 +sort -nrk 1 -t' ' +6. 使用awk命令可以按照分割符将一行分割为多个列,第一列用$1表示,第二列用$2表示,依次类推awk -F" " '{print $2}' //表示用空格作为分隔符进行分割,打印出第2列 +7. head -n表示取前n个head -10 + + + +## Linux常见命令 +- iftop:linux网络流量查看命令 +- top:查看cpu +- ps -le:查看所有正在运行的进程;ps aux|grep 筛选条件 +- tail -f:查看日志 +- free:查看内存 +- uptime:查看系统负载 + + + + +## top详细 +系统负载(三个数分别代表1分钟、5分钟、15分钟的平均值,数值越小负载越低) + +序号 列名 含义 +a PID 进程id +b PPID 父进程id +c RUSER Real user name +d UID 进程所有者的用户id +e USER 进程所有者的用户名 +f GROUP 进程所有者的组名 +g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ? +h PR 优先级 +i NI nice值。负值表示高优先级,正值表示低优先级 +j P 最后使用的CPU,仅在多CPU环境下有意义 +k %CPU 上次更新到现在的CPU时间占用百分比 +l TIME 进程使用的CPU时间总计,单位秒 +m TIME+ 进程使用的CPU时间总计,单位1/100秒 +n %MEM 进程使用的物理内存百分比 +o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES +p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。 +q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA +r CODE 可执行代码占用的物理内存大小,单位kb +s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb +t SHR 共享内存大小,单位kb +u nFLT 页面错误次数 +v nDRT 最后一次写入到现在,被修改过的页面数。 +w S 进程状态。 + D=不可中断的睡眠状态 + R=运行 + S=睡眠 + T=跟踪/停止 + Z=僵尸进程 +x COMMAND 命令名/命令行 +y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名 +z Flags 任务标志,参考 sched.h \ No newline at end of file diff --git a/notes/scene/Scene-Design.md b/notes/scene/Scene-Design.md index 14cc53e..4b70a4b 100644 --- a/notes/scene/Scene-Design.md +++ b/notes/scene/Scene-Design.md @@ -2,6 +2,12 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [三高](#%E4%B8%89%E9%AB%98) + - [高并发](#%E9%AB%98%E5%B9%B6%E5%8F%91) + - [高性能](#%E9%AB%98%E6%80%A7%E8%83%BD) + - [高可用](#%E9%AB%98%E5%8F%AF%E7%94%A8) +- [网站统计IP PV UV实现原理](#%E7%BD%91%E7%AB%99%E7%BB%9F%E8%AE%A1ip-pv-uv%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) +- [如何进行系统拆分?](#%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E7%B3%BB%E7%BB%9F%E6%8B%86%E5%88%86) - [场景题:设计判断论文抄袭的系统](#%E5%9C%BA%E6%99%AF%E9%A2%98%E8%AE%BE%E8%AE%A1%E5%88%A4%E6%96%AD%E8%AE%BA%E6%96%87%E6%8A%84%E8%A2%AD%E7%9A%84%E7%B3%BB%E7%BB%9F) - [设计一个即时聊天的系统](#%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E5%8D%B3%E6%97%B6%E8%81%8A%E5%A4%A9%E7%9A%84%E7%B3%BB%E7%BB%9F) - [分布式系统事务一致性解决方案](#%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%BA%8B%E5%8A%A1%E4%B8%80%E8%87%B4%E6%80%A7%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88) @@ -12,7 +18,10 @@ - [分布式与集群的区别是什么](#%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%8E%E9%9B%86%E7%BE%A4%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88) - [实时展现热门文章,比如近8小时点击量最大的文章前100名](#%E5%AE%9E%E6%97%B6%E5%B1%95%E7%8E%B0%E7%83%AD%E9%97%A8%E6%96%87%E7%AB%A0%E6%AF%94%E5%A6%82%E8%BF%918%E5%B0%8F%E6%97%B6%E7%82%B9%E5%87%BB%E9%87%8F%E6%9C%80%E5%A4%A7%E7%9A%84%E6%96%87%E7%AB%A0%E5%89%8D100%E5%90%8D) - [如何解决电商网站超卖现象](#%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E7%94%B5%E5%95%86%E7%BD%91%E7%AB%99%E8%B6%85%E5%8D%96%E7%8E%B0%E8%B1%A1) +- [如何避免下重复订单](#%E5%A6%82%E4%BD%95%E9%81%BF%E5%85%8D%E4%B8%8B%E9%87%8D%E5%A4%8D%E8%AE%A2%E5%8D%95) +- [如何实现下订单后一个小时后未付款的订单自动取消](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%8B%E8%AE%A2%E5%8D%95%E5%90%8E%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%97%B6%E5%90%8E%E6%9C%AA%E4%BB%98%E6%AC%BE%E7%9A%84%E8%AE%A2%E5%8D%95%E8%87%AA%E5%8A%A8%E5%8F%96%E6%B6%88) - [有关微服务拆分思想?如何进行微服务的拆分?](#%E6%9C%89%E5%85%B3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%8B%86%E5%88%86%E6%80%9D%E6%83%B3%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%8B%86%E5%88%86) +- [分布式集群怎么生成唯一id](#%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E7%BE%A4%E6%80%8E%E4%B9%88%E7%94%9F%E6%88%90%E5%94%AF%E4%B8%80id) - [如何保证接口的幂等性?](#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%B9%82%E7%AD%89%E6%80%A7) - [全局唯一ID](#%E5%85%A8%E5%B1%80%E5%94%AF%E4%B8%80id) - [去重表](#%E5%8E%BB%E9%87%8D%E8%A1%A8) @@ -59,9 +68,62 @@ - [一致性哈希算法](#%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95) - [解决了什么问题?](#%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98) - [RabbitMQ消息丢失解决](#rabbitmq%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E8%A7%A3%E5%86%B3) +- [如何去设计分布式监控中心?](#%E5%A6%82%E4%BD%95%E5%8E%BB%E8%AE%BE%E8%AE%A1%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%91%E6%8E%A7%E4%B8%AD%E5%BF%83) +- [令牌桶和漏桶使用场景?](#%E4%BB%A4%E7%89%8C%E6%A1%B6%E5%92%8C%E6%BC%8F%E6%A1%B6%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) +- [如何设计一个接口?](#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E6%8E%A5%E5%8F%A3) +## 三高 +### 高并发 + +高并发是现在互联网分布式框架设计必须要考虑的因素之一,它是可以保证系统能被同时并行处理很多请求,对于高并发来说,它的指标有: +- 响应时间:系统对进来的请求反应的时间,比如你打开一个页面需要1秒,那么这1秒就是响应时间。 +- 吞吐量:吞吐量是指每秒能处理多少请求数量,好比你吃饭,每秒能吃下多少颗米饭。 +- 秒查询率:秒查询率是指每秒响应请求数,和吞吐量差不多。 +- 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。 + + +### 高性能 + +什么是高性能呢?**高性能是指程序处理速度非常快,所占内存少,cpu占用率低。**高性能的指标经常和高并发的指标紧密相关,想要提高性能,那么就要提高系统发并发能力,两者互相捆绑在一起。应用性能优化的时候,对于**计算密集型和IO密集型**还是有很大差别,需要分开来考虑。还有可以增加服务器的数量,内存,IO等参数提升系统的并发能力和性能,但不要浪费资源,要考虑硬件的使用率最高才能发挥到极致。 + +怎么样提高性能呢? +1. 避免因为IO阻塞让CPU闲置,导致CPU的浪费 +2. 避免多线程间增加锁来保证同步,导致并行系统串行化 +3. 避免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上 + +### 高可用 + +高可用通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。高可用注意如果使用单机,一旦挂机将导致服务不可用,可以使用集群来代替单机,一台服务器挂了,还有其他后备服务器能够顶上。或者使用分布式部署项。比如现在redis的高可用的集群方案有: Redis单副本,Redis多副本(主从),Redis Sentinel(哨兵),Redis Cluster,Redis自研。 + +## 网站统计IP PV UV实现原理 +- PV(访问量):Page View, 即页面浏览量或点击量,用户每次刷新即被计算一次。 +- UV(独立访客):Unique Visitor,一般使用cookie标记,访问您网站的一台电脑客户端(比如一台电脑开多个浏览器访问则为多个UV)为一个访客,00:00-24:00内相同的客户端只会被计算一次。 +- IP(独立IP):指独立IP数。00:00-24:00内相同IP地址之被计算一次(多台电脑可能共用一个ip)。 + +**ip、pv、uv的区别:** +- IP(独立IP):某IP地址的计算机访问网站的次数。这种统计方式很容易实现,具有真实性。所以是衡量网站流量的重要指标。 +- PV(访问量):PV反映的是浏览某网站的页面数,所以每刷新一次也算一次。就是说PV与来访者的数量成正比,但PV并不是页面的来访者数量,而是网站被访问的页面数量。 +- UV(独立访客):可以理解成访问某网站的电脑的数量。网站判断来访电脑的身份是通过来访电脑的cookies实现的。如果更换了IP后但不清除cookies,再访问相同网站,该网站的统计中UV数是不变的。 + +**工作流程:** +- 1、编写监控javascript和提供接口。这个接口返回的是监控网站对应的javascript文件,这个文件可以再客户端可以标记和采集访客的信息。 +- 2、网站调用接口。只需将引入javascript到要监控的站点即可,访客访问该站点时,javascript文件就会被加载。 +- 3、标记和采集数据。监控js被加载后就会往浏览器写入cookie标记访客,比如新访客生产一个新cookie和标记访问次数,若是老用户则,读取 cookie信息,计算访问次数和最后访问时间等,这些客户端的信息处理完后,则向指定的服务器发送数据。 +- 4、最后服务器接收javascript提交过来的数据处理入库和后续的数据处理了。 + +## 如何进行系统拆分? +可以从纵向和横向来拆分: +- 纵向拆分:从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。 +- 横向拆分:从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。 + +1. 单一职责、高内聚低耦合; +2. 服务粒度适中; +3. 考虑团队结构:(康威定律:设计系统的组织其产生系统的设计和架构等价于组织间的沟通结构。就是指每个团队开发设计和测试发布自己团队的微服务时,要互不干扰系统效率才能得到提升,) +4.  以业务模型切入:(领域模型:对具体某个边界领域的抽象,反应了领域内用户业务需求的本质。领域模型只反应业务与任何技术是现实没有关系的,不仅反应领域间的实体概念还能反应领域间的过程概念) +5. 演进式的拆分; +6. 避免环形依赖与双向依赖:(如:商品服务于订单服务相互依赖) ## 场景题:设计判断论文抄袭的系统 @@ -233,6 +295,17 @@ RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二 5. 悲观锁,就是直接在数据库层面将数据锁死,类似于oralce中使用select xxxxx from xxxx where xx=xx for update,这样其他线程将无法提交数据。 6. 使用消息队列异步下单 +## 如何避免下重复订单 +1. 当进入商品详情页时,去生成一个全局唯一ID(可用雪花算法); +2. 将这个全局唯一ID和订单信息传给服务器; +3. 判断这个ID对应的订单号存在,则直接返回; +4. 生成订单号,保存订单信息; + +## 如何实现下订单后一个小时后未付款的订单自动取消 +为了避免轮询并且在服务端主动取消订单,可以使用类似于消息队列的方式,比如 redis 的 pub/sub 服务。 +1. DelayQueue **延时队列**,此队列放入的数据需要实现java.util.concurrent.Delayed接口,用户存放待取消订单 +2. redis 分布式缓存,用于存放待取消订单,数据可以长久存放,不受服务启停影响 +3. 监听器,监听某一事件,执行一定的逻辑 ## 有关微服务拆分思想?如何进行微服务的拆分? 1. 根据业务能力拆分 @@ -247,6 +320,15 @@ RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二 - 支持: 与业务是做什么的相关,但不是主要区分点。这个可以自己做或者外包。 - 通用: 不特定于业务,理想情况下使用现成的软件来实现 +## 分布式集群怎么生成唯一id +1. 数据库自增长序列或字段 +2. UUID +3. UUID的变种 +4. Redis生成ID:主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现 +5. Twitter的snowflake算法:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。 +6. 利用zookeeper生成唯一ID:zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。 +7. MongoDB的ObjectId:MongoDB的ObjectId和snowflake算法类似。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求。使其在分片环境中要容易生成得多。 + ## 如何保证接口的幂等性? ### 全局唯一ID 如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。 @@ -607,3 +689,60 @@ RabbitMQ消息丢失分为三种情况 关闭RabbitMQ自动ACK,确保完成再ACK,否则不要ACK +## 如何去设计分布式监控中心? +**监控中心最基本四个功能:** +1. 数据采集 +2. 实时绘图 +3. 告警 +4. 数据存储 + + +**大概原理:** +- 监控中心的agent需要安装到被监控的主机上,它负责定期收集各项数据,并发送到监控中心server,监控中心server将数据存储到数据库,监控中心web在前端进行绘图。 + + +**agent收集数据分为主动和被动两种模式:** +- 主动:agent请求server获取主动的监控项列表,并主动将监控项内需要检测的数据提交给server/proxy +- server向agent请求获取监控项的数据,agent返回数据。 + + +**一般需要监控哪些数据:** +- 可以自己埋点获取监控信息 +- 代码运行时间统计、次数、错误次数 +- 服务器系统信息、JVM 内存/GC信息、线程信息 +- SQL语句执行时长 +- 各个进程的CPU和内存使用情况 + + +**现在市面比较流行的监控组件:** +按照功能划分: +![](https://github.com/zaiyunduan123/Java-Interview/blob/master/image/scene-5.png) +- 日志:肯定是ELKB +- 调用链APM:OpenTracing、Cat、Pinpoint、zipkin + + + +## 令牌桶和漏桶使用场景? +记住一条,令牌桶会比漏桶更优就行了,99%选用令牌桶。如果请求产生的速率恒定,令牌桶和漏桶效果一样。如果请求分布不均匀,令牌桶比漏桶好。 + + +## 如何设计一个接口? +接口设计需要考虑哪些方面 +1. 接口的命名。 +2. 请求参数。 +3. 支持的协议。 +4. TPS、并发数、响应时长。 +5. 数据存储。DB选型、缓存选型。 +6. 是否需要依赖于第三方。 +7. 接口是否拆分。 +8. 接口是否需要幂等。 +9. 防刷。 +10. 接口限流、降级。 +11. 负载均衡器支持。 +12. 如何部署。 +13. 是否需要服务治理。 +14. 是否存在单点。 +15. 接口是否资源包、预加载还是内置。 +16. 是否需要本地缓存。 +17. 是否需要分布式缓存、缓存穿透怎么办。 +18. 是否需要白名单。