commit f87f455f3d98fdd5b8ebdc795997c6fb8c08d2be Author: 吴尤 <2213093478@qq.com> Date: Thu Nov 4 12:30:34 2021 +0800 docs-lemon diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.md b/404.md new file mode 100644 index 0000000..5ee743f --- /dev/null +++ b/404.md @@ -0,0 +1 @@ +> 迷路了,如果你想要编辑,点击右上角Gitee挂件。 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd06536 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +收纳了 操作系统、JAVA、算法、数据库、中间件、解决方案、架构、DevOps 和 大数据 等技术栈总结!其内容有来源笔者个人总结的内容,也有来源于互联网各种经典场景或案例的总结(感谢对应的笔者),目的在于把常用的技术内容进行归纳整理记录。 + +同时提供一个微信交流群:请加笔者个人微信(echo-lry),备注拉群,笔者通过后,会发送邀请入群。 + +>用最少的图文来讲解清楚每一个知识点,如果欢迎大家一起加入! + +开源地址: + +Gitee:https://gitee.com/yu120/lemon-guide + +Github:https://github.com/yu120/lemon-guide + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/_coverpage.md b/_coverpage.md new file mode 100644 index 0000000..3f2eb7a --- /dev/null +++ b/_coverpage.md @@ -0,0 +1,10 @@ +![](lemon.png) +# lemon-guide 1.0 +> 一款优秀的开源笔记 + +- 操作系统、算法、解决方案 +- JAVA、数据库、中间件 +- 架构、DevOps、大数据 + +[Gitee](https://gitee.com/yu120/lemon-guide) +[点击进入](./README.md) \ No newline at end of file diff --git a/_sidebar.md b/_sidebar.md new file mode 100644 index 0000000..830c05b --- /dev/null +++ b/_sidebar.md @@ -0,0 +1,6 @@ +* 🏁分类 + * [✍操作系统](src/OS/README) + * [✍算法](src/Algorithm/README) + * [✍JAVA](src/JAVA/README) + * [✍数据库](src/Database/README) + * [✍中间件](src/Middleware/README) \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c4ef2d0 --- /dev/null +++ b/index.html @@ -0,0 +1,102 @@ + + + + + lemon-guide + + + + + + + + + +
正在玩命加载中...
+ + + + + + + + + + + + + + diff --git a/lemon.png b/lemon.png new file mode 100644 index 0000000..d01db01 Binary files /dev/null and b/lemon.png differ diff --git a/src/Algorithm/1.md b/src/Algorithm/1.md new file mode 100644 index 0000000..25b38bb --- /dev/null +++ b/src/Algorithm/1.md @@ -0,0 +1,156 @@ +- **左移( << )**:操作数的非0位左移n位,低位补0 + +- **右移( >> )**:操作数的非0位右移n位,高位补0 + +- **无符号右移( >>> )**:正数右移,高位用0补,负数右移,高位用1补,当负数使用无符号右移时,用0进行补位 + +- **位非( ~ )**:操作数为1则为0,否则为1 + +- **位与( & )**:第一个数和第二个数,都为1则为1,否则为0 + +- **位或( | )**:第一个数和第二个数,有一个为1则为1,否则为0 + +- **位异或( ^ )**:第一个数和第二个数,有一个不相同则为1,否则为0 + + ```java + a^a=0; // 自己和自己异或等于0 + a^0=a; // 任何数字和0异或还等于他自己 + a^b^c=a^c^b; // 异或运算具有交换律 + ``` + + + + + +### 左移( << ) + +案例如下: + +`5<<2=20` + +首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位): +0000 0000 0000 0000 0000 0000 0000 0`101` 然后左移2位后,低位补0: +0000 0000 0000 0000 0000 0000 000`101` `00` 换算成10进制为20 + + + +### 右移( >> ) + +案例如下: + +`5>>2=1` + +还是先将5转为2进制表示形式: + +0000 0000 0000 0000 0000 0000 0000 0`101` 然后右移2位,高位补0 + +`00`00 0000 0000 0000 0000 0000 0000 000`1` + + + +### 无符号右移( >>> ) + +我们知道在Java中int类型占32位,可以表示一个正数,也可以表示一个负数。正数换算成二进制后的最高位为0,负数的二进制最高为为1。**正数右移,高位用0补,负数右移,高位用1补,当负数使用无符号右移时,用0进行补位**。 + +案例如下: + +`5>>3=1` + +`-5>>-1` + +`-5>>>536870911` + +我们来看看它的移位过程(可以通过其结果换算成二进制进行对比): + +5换算成二进制: 0000 0000 0000 0000 0000 0000 0000 0101 + +5右移3位后结果为0,0的二进制为: 0000 0000 0000 0000 0000 0000 0000 0000 // (用0进行补位) + +-5换算成二进制: 1111 1111 1111 1111 1111 1111 1111 1011 + +-5右移3位后结果为-1,-1的二进制为: 1111 1111 1111 1111 1111 1111 1111 1111 // (用1进行补位) + +-5无符号右移3位后的结果 536870911 换算成二进制: 0001 1111 1111 1111 1111 1111 1111 1111 // (用0进行补位) + + + +### 位非( ~ ) + +操作数的第n位为1,那么结果的第n位为0,反之。 + +案例如下: + +`~5=-6` + +5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101 + +-6转换为二进制:1111 1111 1111 1111 1111 1111 1111 1010 + + + +### 位与( & ) + +**都为1则为1,否则为0**。第一个操作数的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0。 + +案例如下: + +`5&3=1` + +将2个操作数和结果都转换为二进制进行比较: +5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0`101` + +3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0`011` + +1转换为二进制:0000 0000 0000 0000 0000 0000 0000 0`001` + +### 位或( | ) + +第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0。 + +案例如下: + +`5|3=7` + +5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101 + +3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011 + +7转换为二进制:0000 0000 0000 0000 0000 0000 0000 0111 + + + +### 位异或( ^ ) + +第一个操作数的的第n位于第二个操作数的第n位 相反,那么结果的第n为也为1,否则为0。 + +`a^0=a` + +案例如下: + +`5^3=6` + +5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101 + +3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011 + +6转换为二进制:0000 0000 0000 0000 0000 0000 0000 0110 + + + +### 衍生 + +由位运算操作符衍生而来的有: + +`&=` 按位与赋值 + +`|=` 按位或赋值 + +`^=` 按位非赋值 + +`>>=` 右移赋值 + +`>>=` 无符号右移赋值 + +`<<=` 赋值左移 + +和 += 一个概念而已。 \ No newline at end of file diff --git a/src/Algorithm/101.md b/src/Algorithm/101.md new file mode 100644 index 0000000..1feeeca --- /dev/null +++ b/src/Algorithm/101.md @@ -0,0 +1,70 @@ +![SortAlgorithm](images/Algorithm/SortAlgorithm.png) + +**相关概念** + +- **稳定**:如果a原本在b前面,而a=b,排序之后a仍然在b的前面 +- **不稳定**:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面 +- **时间复杂度**:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律 +- **空间复杂度:**是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数 + + + +**时间复杂度与时间效率**:O(1) < O(log2N) < O(n) < O(N \* log2N) < O(N2) < O(N3) < 2N < 3N < N! + +一般来说,前四个效率比较高,中间两个差强人意,后三个比较差(只要N比较大,这个算法就动不了了)。 + + + +### 常数阶 + +```java +int sum = 0,n = 100; //执行一次 +sum = (1+n)*n/2; //执行一次 +System.out.println (sum); //执行一次 +``` + +上面算法的运行的次数的函数为 f(n)=3,根据推导大 O 阶的规则 1,我们需要将常数 3 改为 1,则这个算法的时间复杂度为 O(1)。如果 sum=(1+n)*n/2 这条语句再执行 10 遍,因为这与问题大小 n 的值并没有关系,所以这个算法的时间复杂度仍旧是 O(1),我们可以称之为常数阶。 + + + +### 线性阶 + +线性阶主要要分析循环结构的运行情况,如下所示: + +```java +for (int i = 0; i < n; i++) { + //时间复杂度为O(1)的算法 + ... +} +``` + +上面算法循环体中的代码执行了n次,因此时间复杂度为O(n)。 + + + +### 对数阶 + +接着看如下代码: + +```java +int number = 1; +while (number < n) { + number = number * 2; + //时间复杂度为O(1)的算法 + ... +} +``` + +可以看出上面的代码,随着 number 每次乘以 2 后,都会越来越接近 n,当 number 不小于 n 时就会退出循环。假设循环的次数为 X,则由 2^x=n 得出 x=log₂n,因此得出这个算法的时间复杂度为 O(logn)。 + + + +### 平方阶 + +下面的代码是循环嵌套: + +```java +for (int i = 0; i < n; i++) { for(int j = 0; j < n; i++) { //复杂度为O(1)的算法 ... }} +``` + +内层循环的时间复杂度在讲到线性阶时就已经得知是O(n),现在经过外层循环n次,那么这段算法的时间复杂度则为O(n²)。 \ No newline at end of file diff --git a/src/Algorithm/102.md b/src/Algorithm/102.md new file mode 100644 index 0000000..6a01487 --- /dev/null +++ b/src/Algorithm/102.md @@ -0,0 +1,46 @@ +![算法思想](images/Algorithm/算法思想.jpg) + +### 分治(Divide and Conquer) + +分治算法思想很大程度上是基于递归的,也比较适合用递归来实现。顾名思义,分而治之。一般分为以下三个过程: + +- **分解**:将原问题分解成一系列子问题 +- **解决**:递归求解各个子问题,若子问题足够小,则直接求解 +- **合并**:将子问题的结果合并成原问题 + +比较经典的应用就是`归并排序 (Merge Sort)` 以及`快速排序 (Quick Sort)` 等。我们来从归并排序理解分治思想,归并排序就是将待排序数组不断二分为规模更小的子问题处理,再将处理好的子问题合并起来。 + + + +### 贪心(Greedy) + +`贪心算法`是`动态规划`算法的一个子集,可以更高效解决一部分更特殊的问题。实际上,用贪心算法解决问题的思路,并不总能给出最优解。因为它在每一步的决策中,选择目前最优策略,不考虑全局是不是最优。 + +**贪心算法+双指针求解** + +- 给一个孩子的饼干应当尽量小并且能满足孩子,大的留来满足胃口大的孩子 +- 因为胃口小的孩子最容易得到满足,所以优先满足胃口小的孩子需求 +- 按照从小到大的顺序使用饼干尝试是否可满足某个孩子 +- 当饼干 j >= 胃口 i 时,饼干满足胃口,更新满足的孩子数并移动指针 `i++ j++ res++` +- 当饼干 j < 胃口 i 时,饼干不能满足胃口,需要换大的 `j++` + + + +### 回溯(Backtracking) + +使用回溯法进行求解,回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。究其本质,其实就是枚举。 + +- 如果没有更多的数字需要被输入,说明当前的组合已经产生 +- 如果还有数字需要被输入: + - 遍历下一个数字所对应的所有映射的字母 + - 将当前的字母添加到组合最后,也就是 `str + tmp[r]` + + + +### 动态规划(Dynamic Programming) + +虽然动态规划的最终版本 (降维再去维) 大都不是递归,但解题的过程还是离开不递归的。新手可能会觉得动态规划思想接受起来比较难,确实,动态规划求解问题的过程不太符合人类常规的思维方式,我们需要切换成机器思维。使用动态规划思想解题,首先要明确动态规划的三要素。动态规划三要素: + +- `重叠子问题`:切换机器思维,自底向上思考 +- `最优子结构`:子问题的最优解能够推出原问题的优解 +- `状态转移方程`:dp[n] = dp[n-1] + dp[n-2] \ No newline at end of file diff --git a/src/Algorithm/103.md b/src/Algorithm/103.md new file mode 100644 index 0000000..66dc853 --- /dev/null +++ b/src/Algorithm/103.md @@ -0,0 +1,107 @@ +### 递归模板 + +```java +public void recur(int level, int param) { + // terminator + if (level > MAX_LEVEL) { + // process result + return; + } + + // process current logic + process(level, param); + + // drill down + recur(level + 1, newParam); + + // restore current status +} +``` + +**List转树形结构** + +- **方案一:两层循环实现建树** +- **方案二:使用递归方法建树** + +```java +public class TreeNode { + private String id; + private String parentId; + private String name; + private List children; + + /** + * 方案一:两层循环实现建树 + * + * @param treeNodes 传入的树节点列表 + * @return + */ + public static List bulid(List treeNodes) { + List trees = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + if ("0".equals(treeNode.getParentId())) { + trees.add(treeNode); + } + + for (TreeNode it : treeNodes) { + if (it.getParentId() == treeNode.getId()) { + if (treeNode.getChildren() == null) { + treeNode.setChildren(new ArrayList()); + } + treeNode.getChildren().add(it); + } + } + } + + return trees; + } + + /** + * 方案二:使用递归方法建树 + * + * @param treeNodes + * @return + */ + public static List buildByRecursive(List treeNodes) { + List trees = new ArrayList<>(); + for (TreeNode treeNode : treeNodes) { + if ("0".equals(treeNode.getParentId())) { + trees.add(findChildren(treeNode, treeNodes)); + } + } + + return trees; + } + + /** + * 递归查找子节点 + * + * @param treeNodes + * @return + */ + private static TreeNode findChildren(TreeNode treeNode, List treeNodes) { + for (TreeNode it : treeNodes) { + if (treeNode.getId().equals(it.getParentId())) { + if (treeNode.getChildren() == null) { + treeNode.setChildren(new ArrayList<>()); + } + treeNode.getChildren().add(findChildren(it, treeNodes)); + } + } + + return treeNode; + } +} +``` + + + +### 回溯模板 + + + +### DFS模板 + + + +### BFS模板 \ No newline at end of file diff --git a/src/Algorithm/104.md b/src/Algorithm/104.md new file mode 100644 index 0000000..d31ee08 --- /dev/null +++ b/src/Algorithm/104.md @@ -0,0 +1,211 @@ +### 顺序查找 + +就是一个一个依次查找。 + + + +### 二分查找 + +二分查找又叫折半查找,从有序列表的初始候选区`li[0:n]`开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。如果待查值小于候选区中间值,则只需比较中间值左边的元素,减半查找范围。依次类推依次减半。 + +- 二分查找的前提:**列表有序** +- 二分查找的有点:**查找速度快** +- 二分查找的时间复杂度为:**O(logn)** + +![二分查找](images/Algorithm/二分查找.gif) + +JAVA代码如下: + +```java +/** + * 执行递归二分查找,返回第一次出现该值的位置 + * + * @param array 已排序的数组 + * @param start 开始位置,如:0 + * @param end 结束位置,如:array.length-1 + * @param findValue 需要找的值 + * @return 值在数组中的位置,从0开始。找不到返回-1 + */ +public static int searchRecursive(int[] array, int start, int end, int findValue) { + // 如果数组为空,直接返回-1,即查找失败 + if (array == null) { + return -1; + } + + if (start <= end) { + // 中间位置 + int middle = (start + end) / 1; + // 中值 + int middleValue = array[middle]; + if (findValue == middleValue) { + // 等于中值直接返回 + return middle; + } else if (findValue < middleValue) { + // 小于中值时在中值前面找 + return searchRecursive(array, start, middle - 1, findValue); + } else { + // 大于中值在中值后面找 + return searchRecursive(array, middle + 1, end, findValue); + } + } else { + // 返回-1,即查找失败 + return -1; + } +} + +/** + * 循环二分查找,返回第一次出现该值的位置 + * + * @param array 已排序的数组 + * @param findValue 需要找的值 + * @return 值在数组中的位置,从0开始。找不到返回-1 + */ +public static int searchLoop(int[] array, int findValue) { + // 如果数组为空,直接返回-1,即查找失败 + if (array == null) { + return -1; + } + + // 起始位置 + int start = 0; + // 结束位置 + int end = array.length - 1; + while (start <= end) { + // 中间位置 + int middle = (start + end) / 2; + // 中值 + int middleValue = array[middle]; + if (findValue == middleValue) { + // 等于中值直接返回 + return middle; + } else if (findValue < middleValue) { + // 小于中值时在中值前面找 + end = middle - 1; + } else { + // 大于中值在中值后面找 + start = middle + 1; + } + } + + // 返回-1,即查找失败 + return -1; +} +``` + + + +### 插值查找 + +插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。将折半查找中的求mid索引的公式,low表示左边索引left,high表示右边索引right,key就是前面我们讲的findVal。 + +![二分查找mid](images/Algorithm/二分查找mid.png) **改为** ![插值查找mid](images/Algorithm/插值查找mid.png) + +**注意事项** + +- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快 +- 关键字分布不均匀的情况下,该方法不一定比折半查找要好 + +```java +/** + * 插值查找 + * + * @param arr 已排序的数组 + * @param left 开始位置,如:0 + * @param right 结束位置,如:array.length-1 + * @param findValue + * @return + */ +public static int insertValueSearch(int[] arr, int left, int right, int findValue) { + //注意:findVal < arr[0] 和 findVal > arr[arr.length - 1] 必须需要, 否则我们得到的 mid 可能越界 + if (left > right || findValue < arr[0] || findValue > arr[arr.length - 1]) { + return -1; + } + + // 求出mid, 自适应 + int mid = left + (right - left) * (findValue - arr[left]) / (arr[right] - arr[left]); + int midValue = arr[mid]; + if (findValue > midValue) { + // 向右递归 + return insertValueSearch(arr, mid + 1, right, findValue); + } else if (findValue < midValue) { + // 向左递归 + return insertValueSearch(arr, left, mid - 1, findValue); + } else { + return mid; + } +} +``` + + + +### 斐波那契查找 + +黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。斐波那契数列{1, 1,2, 3, 5, 8, 13,21, 34, 55 }发现斐波那契数列的两个相邻数的比例,无限接近黄金分割值0.618。 +斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示: + +![斐波那契查找](images/Algorithm/斐波那契查找.png) + +JAVA代码如下: + +```java +/** + * 因为后面我们mid=low+F(k-1)-1,需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列 + *

+ * 非递归方法得到一个斐波那契数列 + * + * @return + */ +private static int[] getFibonacci() { + int[] fibonacci = new int[20]; + fibonacci[0] = 1; + fibonacci[1] = 1; + for (int i = 2; i < fibonacci.length; i++) { + fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]; + } + return fibonacci; +} + +/** + * 编写斐波那契查找算法 + *

+ * 使用非递归的方式编写算法 + * + * @param arr 数组 + * @param findValue 我们需要查找的关键码(值) + * @return 返回对应的下标,如果没有-1 + */ +public static int fibonacciSearch(int[] arr, int findValue) { + int low = 0; + int high = arr.length - 1; + int k = 0;// 表示斐波那契分割数值的下标 + int mid = 0;// 存放mid值 + int[] fibonacci = getFibonacci();// 获取到斐波那契数列 + // 获取到斐波那契分割数值的下标 + while (high > fibonacci[k] - 1) { + k++; + } + + // 因为 fibonacci[k] 值可能大于 arr 的 长度,因此我们需要使用Arrays类,构造一个新的数组 + int[] temp = Arrays.copyOf(arr, fibonacci[k]); + // 实际上需求使用arr数组最后的数填充 temp + for (int i = high + 1; i < temp.length; i++) { + temp[i] = arr[high]; + } + + // 使用while来循环处理,找到我们的数 findValue + while (low <= high) { + mid = low + fibonacci[k] - 1; + if (findValue < temp[mid]) { + high = mid - 1; + k--; + } else if (findValue > temp[mid]) { + low = mid + 1; + k++; + } else { + return Math.min(mid, high); + } + } + + return -1; +} +``` \ No newline at end of file diff --git a/src/Algorithm/105.md b/src/Algorithm/105.md new file mode 100644 index 0000000..3d59fe7 --- /dev/null +++ b/src/Algorithm/105.md @@ -0,0 +1,484 @@ +### 深度优先搜索(DFS) + +深度优先搜索(Depth-First Search / DFS)是一种**优先遍历子节点**而不是回溯的算法。 + +![深度优先搜索](images/Algorithm/深度优先搜索.jpg) + +**DFS解决的是连通性的问题**。即给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能从起点连接到终点。起点和终点,也可以指的是某种起始状态和最终的状态。问题的要求并不在乎路径是长还是短,只在乎有还是没有。 + + + +**代码案例** + +```java +/** + * Depth-First Search(DFS) + *

+ * 从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。 + *

+ * 数据结构:栈 + * 父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点即可 + * + * @author lry + */ +public class DepthFirstSearch { + + /** + * 树节点 + * + * @param + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TreeNode { + private V value; + private List> childList; + + // 二叉树节点支持如下 + + public TreeNode getLeft() { + if (childList == null || childList.isEmpty()) { + return null; + } + + return childList.get(0); + + } + + public TreeNode getRight() { + if (childList == null || childList.isEmpty()) { + return null; + } + + return childList.get(1); + } + } + + /** + * 模型: + * .......A + * ...../ \ + * ....B C + * .../ \ / \ + * ..D E F G + * ./ \ / \ + * H I J K + */ + public static void main(String[] args) { + TreeNode treeNodeA = new TreeNode<>("A", new ArrayList<>()); + TreeNode treeNodeB = new TreeNode<>("B", new ArrayList<>()); + TreeNode treeNodeC = new TreeNode<>("C", new ArrayList<>()); + TreeNode treeNodeD = new TreeNode<>("D", new ArrayList<>()); + TreeNode treeNodeE = new TreeNode<>("E", new ArrayList<>()); + TreeNode treeNodeF = new TreeNode<>("F", new ArrayList<>()); + TreeNode treeNodeG = new TreeNode<>("G", new ArrayList<>()); + TreeNode treeNodeH = new TreeNode<>("H", new ArrayList<>()); + TreeNode treeNodeI = new TreeNode<>("I", new ArrayList<>()); + TreeNode treeNodeJ = new TreeNode<>("J", new ArrayList<>()); + TreeNode treeNodeK = new TreeNode<>("K", new ArrayList<>()); + // A->B,C + treeNodeA.getChildList().add(treeNodeB); + treeNodeA.getChildList().add(treeNodeC); + // B->D,E + treeNodeB.getChildList().add(treeNodeD); + treeNodeB.getChildList().add(treeNodeE); + // C->F,G + treeNodeC.getChildList().add(treeNodeF); + treeNodeC.getChildList().add(treeNodeG); + // D->H,I + treeNodeD.getChildList().add(treeNodeH); + treeNodeD.getChildList().add(treeNodeI); + // G->J,K + treeNodeG.getChildList().add(treeNodeJ); + treeNodeG.getChildList().add(treeNodeK); + + System.out.println("非递归方式"); + dfsNotRecursive(treeNodeA); + System.out.println(); + System.out.println("前续遍历"); + dfsPreOrderTraversal(treeNodeA, 0); + System.out.println(); + System.out.println("后续遍历"); + dfsPostOrderTraversal(treeNodeA, 0); + System.out.println(); + System.out.println("中续遍历"); + dfsInOrderTraversal(treeNodeA, 0); + } + + /** + * 非递归方式 + * + * @param tree + * @param + */ + public static void dfsNotRecursive(TreeNode tree) { + if (tree != null) { + // 次数之所以用 Map 只是为了保存节点的深度,如果没有这个需求可以改为 Stack> + Stack, Integer>> stack = new Stack<>(); + Map, Integer> root = new HashMap<>(); + root.put(tree, 0); + stack.push(root); + + while (!stack.isEmpty()) { + Map, Integer> item = stack.pop(); + TreeNode node = item.keySet().iterator().next(); + int depth = item.get(node); + + // 打印节点值以及深度 + System.out.print("-->[" + node.getValue().toString() + "," + depth + "]"); + + if (node.getChildList() != null && !node.getChildList().isEmpty()) { + for (TreeNode treeNode : node.getChildList()) { + Map, Integer> map = new HashMap<>(); + map.put(treeNode, depth + 1); + stack.push(map); + } + } + } + } + } + + /** + * 递归前序遍历方式 + *

+ * 前序遍历(Pre-Order Traversal) :指先访问根,然后访问子树的遍历方式,二叉树则为:根->左->右 + * + * @param tree + * @param depth + * @param + */ + public static void dfsPreOrderTraversal(TreeNode tree, int depth) { + if (tree != null) { + // 打印节点值以及深度 + System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]"); + + if (tree.getChildList() != null && !tree.getChildList().isEmpty()) { + for (TreeNode item : tree.getChildList()) { + dfsPreOrderTraversal(item, depth + 1); + } + } + } + } + + /** + * 递归后序遍历方式 + *

+ * 后序遍历(Post-Order Traversal):指先访问子树,然后访问根的遍历方式,二叉树则为:左->右->根 + * + * @param tree + * @param depth + * @param + */ + public static void dfsPostOrderTraversal(TreeNode tree, int depth) { + if (tree != null) { + if (tree.getChildList() != null && !tree.getChildList().isEmpty()) { + for (TreeNode item : tree.getChildList()) { + dfsPostOrderTraversal(item, depth + 1); + } + } + + // 打印节点值以及深度 + System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]"); + } + } + + /** + * 递归中序遍历方式 + *

+ * 中序遍历(In-Order Traversal):指先访问左(右)子树,然后访问根,最后访问右(左)子树的遍历方式,二叉树则为:左->根->右 + * + * @param tree + * @param depth + * @param + */ + public static void dfsInOrderTraversal(TreeNode tree, int depth) { + if (tree.getLeft() != null) { + dfsInOrderTraversal(tree.getLeft(), depth + 1); + } + + // 打印节点值以及深度 + System.out.print("-->[" + tree.getValue().toString() + "," + depth + "]"); + + if (tree.getRight() != null) { + dfsInOrderTraversal(tree.getRight(), depth + 1); + } + } + +} +``` + + + +### 广度优先搜索(BFS) + +广度优先搜索(Breadth-First Search / BFS)是**优先遍历邻居节点**而不是子节点的图遍历算法。 + +![广度优先搜索](images/Algorithm/广度优先搜索.jpg) + +**BFS一般用来解决最短路径的问题**。和深度优先搜索不同,广度优先的搜索是从起始点出发,一层一层地进行,每层当中的点距离起始点的步数都是相同的,当找到了目的地之后就可以立即结束。广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS。这种算法往往可以大大地提高搜索的效率。 + + + +**代码案例** + +```java +/** + * Breadth-First Search(BFS) + *

+ * 从根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。 + *

+ * 数据结构:队列 + * 父节点入队,父节点出队列,先左子节点入队,后右子节点入队。递归遍历全部节点即可 + * + * @author lry + */ +public class BreadthFirstSearch { + + /** + * 树节点 + * + * @param + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TreeNode { + private V value; + private List> childList; + } + + /** + * 模型: + * .......A + * ...../ \ + * ....B C + * .../ \ / \ + * ..D E F G + * ./ \ / \ + * H I J K + */ + public static void main(String[] args) { + TreeNode treeNodeA = new TreeNode<>("A", new ArrayList<>()); + TreeNode treeNodeB = new TreeNode<>("B", new ArrayList<>()); + TreeNode treeNodeC = new TreeNode<>("C", new ArrayList<>()); + TreeNode treeNodeD = new TreeNode<>("D", new ArrayList<>()); + TreeNode treeNodeE = new TreeNode<>("E", new ArrayList<>()); + TreeNode treeNodeF = new TreeNode<>("F", new ArrayList<>()); + TreeNode treeNodeG = new TreeNode<>("G", new ArrayList<>()); + TreeNode treeNodeH = new TreeNode<>("H", new ArrayList<>()); + TreeNode treeNodeI = new TreeNode<>("I", new ArrayList<>()); + TreeNode treeNodeJ = new TreeNode<>("J", new ArrayList<>()); + TreeNode treeNodeK = new TreeNode<>("K", new ArrayList<>()); + // A->B,C + treeNodeA.getChildList().add(treeNodeB); + treeNodeA.getChildList().add(treeNodeC); + // B->D,E + treeNodeB.getChildList().add(treeNodeD); + treeNodeB.getChildList().add(treeNodeE); + // C->F,G + treeNodeC.getChildList().add(treeNodeF); + treeNodeC.getChildList().add(treeNodeG); + // D->H,I + treeNodeD.getChildList().add(treeNodeH); + treeNodeD.getChildList().add(treeNodeI); + // G->J,K + treeNodeG.getChildList().add(treeNodeJ); + treeNodeG.getChildList().add(treeNodeK); + + System.out.println("递归方式"); + bfsRecursive(Arrays.asList(treeNodeA), 0); + System.out.println(); + System.out.println("非递归方式"); + bfsNotRecursive(treeNodeA); + } + + /** + * 递归遍历 + * + * @param children + * @param depth + * @param + */ + public static void bfsRecursive(List> children, int depth) { + List> thisChildren, allChildren = new ArrayList<>(); + for (TreeNode child : children) { + // 打印节点值以及深度 + System.out.print("-->[" + child.getValue().toString() + "," + depth + "]"); + + thisChildren = child.getChildList(); + if (thisChildren != null && thisChildren.size() > 0) { + allChildren.addAll(thisChildren); + } + } + + if (allChildren.size() > 0) { + bfsRecursive(allChildren, depth + 1); + } + } + + /** + * 非递归遍历 + * + * @param tree + * @param + */ + public static void bfsNotRecursive(TreeNode tree) { + if (tree != null) { + // 跟上面一样,使用 Map 也只是为了保存树的深度,没这个需要可以不用 Map + Queue, Integer>> queue = new ArrayDeque<>(); + Map, Integer> root = new HashMap<>(); + root.put(tree, 0); + queue.offer(root); + + while (!queue.isEmpty()) { + Map, Integer> itemMap = queue.poll(); + TreeNode node = itemMap.keySet().iterator().next(); + int depth = itemMap.get(node); + + //打印节点值以及深度 + System.out.print("-->[" + node.getValue().toString() + "," + depth + "]"); + + if (node.getChildList() != null && !node.getChildList().isEmpty()) { + for (TreeNode child : node.getChildList()) { + Map, Integer> map = new HashMap<>(); + map.put(child, depth + 1); + queue.offer(map); + } + } + } + } + } + +} +``` + + + +### 迪杰斯特拉算法(Dijkstra) + +**迪杰斯特拉(Dijkstra)算法** 是典型最短路径算法,用于计算一个节点到其他节点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。 + + + +**基本思想** + +通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。 + +此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。 + +初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是"起点s到该顶点的路径"。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 ... 重复该操作,直到遍历完所有顶点。 + + +**操作步骤** + +- 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为"起点s到该顶点的距离"[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞] +- U中选出"距离最短的顶点k",并将顶点k加入到S中;同时,从U中移除顶点k +- 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离 +- 重复步骤(2)和(3),直到遍历完所有顶点 + + + +**迪杰斯特拉算法图解** + +![img](images/Algorithm/1117043-20170407105053816-427306966.png) + +以上图G4为例,来对迪杰斯特拉进行算法演示(以第4个顶点D为起点): + +![img](images/Algorithm/1117043-20170407105111300-518814658.png) + + **代码案例** + +```java +public class Dijkstra { + // 代表正无穷 + public static final int M = 10000; + public static String[] names = new String[]{"A", "B", "C", "D", "E", "F", "G",}; + + public static void main(String[] args) { + // 二维数组每一行分别是 A、B、C、D、E 各点到其余点的距离, + // A -> A 距离为0, 常量M 为正无穷 + int[][] weight1 = { + {0, 12, M, M, M, 16, 14}, + {12, 0, 10, M, M, 7, M}, + {M, 10, 0, 3, 5, 6, M}, + {M, M, 3, 0, 4, M, M}, + {M, M, 5, 4, 0, 2, 8}, + {16, 7, 6, M, 2, 0, 9}, + {14, M, M, M, 8, 9, 0} + }; + + int start = 0; + int[] shortPath = dijkstra(weight1, start); + System.out.println("==============="); + for (int i = 0; i < shortPath.length; i++) { + System.out.println("从" + names[start] + "出发到" + names[i] + "的最短距离为:" + shortPath[i]); + } + } + + /** + * Dijkstra算法 + * + * @param weight 图的权重矩阵 + * @param start 起点编号start(从0编号,顶点存在数组中) + * @return 返回一个int[] 数组,表示从start到它的最短路径长度 + */ + public static int[] dijkstra(int[][] weight, int start) { + // 顶点个数 + int n = weight.length; + // 标记当前该顶点的最短路径是否已经求出,1表示已求出 + int[] visited = new int[n]; + // 保存start到其他各点的最短路径 + int[] shortPath = new int[n]; + + // 保存start到其他各点最短路径的字符串表示 + String[] path = new String[n]; + for (int i = 0; i < n; i++) { + path[i] = names[start] + "-->" + names[i]; + } + + // 初始化,第一个顶点已经求出 + shortPath[start] = 0; + visited[start] = 1; + + // 要加入n-1个顶点 + for (int count = 1; count < n; count++) { + // 选出一个距离初始顶点start最近的未标记顶点 + int k = -1; + int dMin = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + if (visited[i] == 0 && weight[start][i] < dMin) { + dMin = weight[start][i]; + k = i; + } + } + + // 将新选出的顶点标记为已求出最短路径,且到start的最短路径就是dmin + shortPath[k] = dMin; + visited[k] = 1; + + // 以k为中间点,修正从start到未访问各点的距离 + for (int i = 0; i < n; i++) { + // 如果 '起始点到当前点距离' + '当前点到某点距离' < '起始点到某点距离', 则更新 + if (visited[i] == 0 && weight[start][k] + weight[k][i] < weight[start][i]) { + weight[start][i] = weight[start][k] + weight[k][i]; + path[i] = path[k] + "-->" + names[i]; + } + } + } + + for (int i = 0; i < n; i++) { + System.out.println("从" + names[start] + "出发到" + names[i] + "的最短路径为:" + path[i]); + } + + return shortPath; + } + +} +``` + + + +### kruskal(克鲁斯卡尔)算法 + +https://www.cnblogs.com/skywang12345/category/508186.html \ No newline at end of file diff --git a/src/Algorithm/106.md b/src/Algorithm/106.md new file mode 100644 index 0000000..f0e11fa --- /dev/null +++ b/src/Algorithm/106.md @@ -0,0 +1,841 @@ +十种常见排序算法可以分为两大类: + +- **非线性时间比较类排序** + + 通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。 + +- **线性时间非比较类排序** + + 不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 + +![排序算法](images/Algorithm/排序算法.png) + +### 冒泡排序(Bubble Sort) + +**循环遍历多次每次从前往后把大元素往后调,每次确定一个最大(最小)元素,多次后达到排序序列。**这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 + +![冒泡排序](images/Algorithm/冒泡排序.jpg) + +![冒泡排序](images/Algorithm/冒泡排序.gif) + +**算法步骤** + +- 比较相邻的元素。如果第一个比第二个大,就交换它们两个 +- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数 +- 针对所有的元素重复以上的步骤,除了最后一个 +- 重复步骤1~3,直到排序完成 + +**代码实现** + +```java +/** + * 冒泡排序 + *

+ * 描述:每轮连续比较相邻的两个数,前数大于后数,则进行替换。每轮完成后,本轮最大值已被移至最后 + * + * @param arr 待排序数组 + */ +public static int[] bubbleSort(int[] arr) { + for (int i = 0; i < arr.length - 1; i++) { + for (int j = 0; j < arr.length - 1 - i; j++) { + // 每次比较2个相邻的数,前一个小于后一个 + if (arr[j] > arr[j + 1]) { + int tmp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = tmp; + } + } + } + + return arr; +} +``` + +以下是冒泡排序算法复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :------- | :------- | :--------- | +| O(n²) | O(n) | O(n²) | O(1) | + +冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n)。平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1)。 + +Tips: 由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。 + + + +### 选择排序(Selection Sort) + +选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 + +![选择排序](images/Algorithm/选择排序.jpg) + +![选择排序](images/Algorithm/选择排序.gif) + +**算法描述** + +n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下: + +- 初始状态:无序区为R[1..n],有序区为空 +- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区 +- n-1趟结束,数组有序化了 + +**代码实现** + +```java + /** + * 选择排序 + *

+ * 描述:每轮选择出最小值,然后依次放置最前面 + * + * @param arr 待排序数组 + */ + public static int[] selectSort(int[] arr) { + for (int i = 0; i < arr.length - 1; i++) { + // 选最小的记录 + int min = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[min] > arr[j]) { + min = j; + } + } + + // 内层循环结束后,即找到本轮循环的最小的数以后,再进行交换:交换a[i]和a[min] + if (min != i) { + int temp = arr[i]; + arr[i] = arr[min]; + arr[min] = temp; + } + } + + return arr; + } +``` + +以下是选择排序复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :------- | :------- | :--------- | +| O(n²) | O(n²) | O(n²) | O(1) | + +选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。 + + + +### 插入排序(Insertion Sort) + +插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序由于操作不尽相同,可分为 `直接插入排序`、`折半插入排序`(又称二分插入排序)、`链表插入排序`、`希尔排序` 。 + +![插入排序](images/Algorithm/插入排序.jpg) + +![插入排序](images/Algorithm/插入排序.gif) + +**算法描述** + +一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下: + +- 从第一个元素开始,该元素可以认为已经被排序 +- 取出下一个元素,在已经排序的元素序列中从后向前扫描 +- 如果该元素(已排序)大于新元素,将该元素移到下一位置 +- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 +- 将新元素插入到该位置后 +- 重复步骤2~5 + +**代码实现** + +```java + /** + * 直接插入排序 + *

+ * 1. 从第一个元素开始,该元素可以认为已经被排序 + * 2. 取出下一个元素,在已经排序的元素序列中从后向前扫描 + * 3. 如果该元素(已排序)大于新元素,将该元素移到下一位置 + * 4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 + * 5. 将新元素插入到该位置后 + * 6. 重复步骤2~5 + * + * @param arr 待排序数组 + */ + public static int[] insertionSort(int[] arr) { + for (int i = 1; i < arr.length; i++) { + // 取出下一个元素,在已经排序的元素序列中从后向前扫描 + int temp = arr[i]; + for (int j = i; j >= 0; j--) { + if (j > 0 && arr[j - 1] > temp) { + // 如果该元素(已排序)大于取出的元素temp,将该元素移到下一位置 + arr[j] = arr[j - 1]; + } else { + // 将新元素插入到该位置后 + arr[j] = temp; + break; + } + } + } + + return arr; + } + + /** + * 折半插入排序 + *

+ * 往前找合适的插入位置时采用二分查找的方式,即折半插入 + *

+ * 交换次数较多的实现 + * + * @param arr 待排序数组 + */ + public static int[] insertionBinarySort(int[] arr) { + for (int i = 1; i < arr.length; i++) { + if (arr[i] < arr[i - 1]) { + int tmp = arr[i]; + + // 记录搜索范围的左边界,右边界 + int low = 0, high = i - 1; + while (low <= high) { + // 记录中间位置Index + int mid = (low + high) / 2; + // 比较中间位置数据和i处数据大小,以缩小搜索范围 + if (arr[mid] < tmp) { + // 左边指针则一只中间位置+1 + low = mid + 1; + } else { + // 右边指针则一只中间位置-1 + high = mid - 1; + } + } + + // 将low~i处数据整体向后移动1位 + for (int j = i; j > low; j--) { + arr[j] = arr[j - 1]; + } + arr[low] = tmp; + } + } + + return arr; + } +``` + +插入排序复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :------- | :------- | :--------- | +| O(n²) | O(n) | O(n²) | O(1) | + +Tips:由于直接插入排序每次只移动一个元素的位, 并不会改变值相同的元素之间的排序, 因此它是一种稳定排序。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 + + + +### 希尔排序(Shell Sort) + +1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫**缩小增量排序**。 + +![希尔排序](images/Algorithm/希尔排序.gif) + +**算法描述** + +先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: + +- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1 +- 按增量序列个数k,对序列进行k 趟排序 +- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度 + +**代码实现** + +```java + /** + * 希尔排序 + *

+ * 1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;(一般初次取数组半长,之后每次再减半,直到增量为1) + * 2. 按增量序列个数k,对序列进行k 趟排序; + * 3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。 + * 仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 + * + * @param arr 待排序数组 + */ + public static int[] shellSort(int[] arr) { + int gap = arr.length / 2; + + // 不断缩小gap,直到1为止 + for (; gap > 0; gap /= 2) { + // 使用当前gap进行组内插入排序 + for (int j = 0; (j + gap) < arr.length; j++) { + for (int k = 0; (k + gap) < arr.length; k += gap) { + if (arr[k] > arr[k + gap]) { + int temp = arr[k + gap]; + arr[k + gap] = arr[k]; + arr[k] = temp; + } + } + } + } + + return arr; + } +``` + +以下是希尔排序复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :--------- | :--------- | :--------- | +| O(nlog2 n) | O(nlog2 n) | O(nlog2 n) | O(1) | + +Tips:希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。  + + + +### 归并排序(Merging Sort) + +**简介** + +**基本思想**:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列**合并**为整体有序序列。 + + + +**场景使用** + +**应用场景**:内存少的时候使用,可以进行**并行计算**的时候使用。 + + + +**步骤**: + +- 选择**相邻**两个数组成一个有序序列 +- 选择相邻的两个有序序列组成一个有序序列 +- 重复第二步,直到全部组成一个**有序**序列 + +![归并排序](images/Algorithm/归并排序.jpg) + +![归并排序](images/Algorithm/归并排序.gif) + +**算法描述** + +**a.递归法**(假设序列共有n个元素) + +①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; +②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; +③. 重复步骤②,直到所有元素排序完毕。 + +**b.迭代法** + +①. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 +②. 设定两个指针,最初位置分别为两个已经排序序列的起始位置 +③. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 +④. 重复步骤③直到某一指针到达序列尾 +⑤. 将另一序列剩下的所有元素直接复制到合并序列尾 + +**代码实现** + +```java +/** + * 归并排序(递归) + *

+ * ①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素; + * ②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素; + * ③. 重复步骤②,直到所有元素排序完毕。 + * + * @param arr 待排序数组 + */ + public static int[] mergeSort(int[] arr) { + return mergeSort(arr, 0, arr.length - 1); + } + + private static int[] mergeSort(int[] arr, int low, int high) { + int center = (high + low) / 2; + if (low < high) { + // 递归,直到low==high,也就是数组已不能再分了, + mergeSort(arr, low, center); + mergeSort(arr, center + 1, high); + + // 当数组不能再分,开始归并排序 + mergeSort(arr, low, center, high); + } + + return arr; + } + + private static void mergeSort(int[] a, int low, int mid, int high) { + int[] temp = new int[high - low + 1]; + int i = low, j = mid + 1, k = 0; + + // 把较小的数先移到新数组中 + while (i <= mid && j <= high) { + if (a[i] < a[j]) { + temp[k++] = a[i++]; + } else { + temp[k++] = a[j++]; + } + } + + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = a[i++]; + } + + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = a[j++]; + } + + // 把新数组中的数覆盖nums数组 + for (int x = 0; x < temp.length; x++) { + a[x + low] = temp[x]; + } + } +``` + +以下是归并排序算法复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :-------- | :-------- | :--------- | +| O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | + +从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。 + +Tips:和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是`O(nlogn)`的时间复杂度。代价是需要额外的内存空间。 + + + +### 快速排序(Quick Sort) + +快速排序(Quicksort)是对冒泡排序的一种改进,借用了分治的思想,由C. A. R. Hoare在1962年提出。基本思想是通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。 + +![快速排序](images/Algorithm/快速排序.gif) + +**算法描述** + +快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下: + +- 从数列中挑出一个元素,称为 “基准”(pivot) +- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作 +- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 + +**代码实现** + +```java +/** + * 快速排序(递归) + *

+ * ①. 从数列中挑出一个元素,称为"基准"(pivot)。 + * ②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 + * ③. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。 + * + * @param arr 待排序数组 + */ + public static int[] quickSort(int[] arr) { + return quickSort(arr, 0, arr.length - 1); + } + + private static int[] quickSort(int[] arr, int low, int high) { + if (arr.length <= 0 || low >= high) { + return arr; + } + + int left = low; + int right = high; + + // 挖坑1:保存基准的值 + int temp = arr[left]; + while (left < right) { + // 坑2:从后向前找到比基准小的元素,插入到基准位置坑1中 + while (left < right && arr[right] >= temp) { + right--; + } + arr[left] = arr[right]; + // 坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中 + while (left < right && arr[left] <= temp) { + left++; + } + arr[right] = arr[left]; + } + // 基准值填补到坑3中,准备分治递归快排 + arr[left] = temp; + quickSort(arr, low, left - 1); + quickSort(arr, left + 1, high); + + return arr; + } + + /** + * 快速排序(非递归) + *

+ * ①. 从数列中挑出一个元素,称为"基准"(pivot)。 + * ②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 + * ③. 把分区之后两个区间的边界(low和high)压入栈保存,并循环①、②步骤 + * + * @param arr 待排序数组 + */ + public static int[] quickSortByStack(int[] arr) { + Stack stack = new Stack<>(); + + // 初始状态的左右指针入栈 + stack.push(0); + stack.push(arr.length - 1); + while (!stack.isEmpty()) { + // 出栈进行划分 + int high = stack.pop(); + int low = stack.pop(); + + int pivotIdx = partition(arr, low, high); + + // 保存中间变量 + if (pivotIdx > low) { + stack.push(low); + stack.push(pivotIdx - 1); + } + if (pivotIdx < high && pivotIdx >= 0) { + stack.push(pivotIdx + 1); + stack.push(high); + } + } + + return arr; + } + + private static int partition(int[] arr, int low, int high) { + if (arr.length <= 0) return -1; + if (low >= high) return -1; + int l = low; + int r = high; + + // 挖坑1:保存基准的值 + int pivot = arr[l]; + while (l < r) { + // 坑2:从后向前找到比基准小的元素,插入到基准位置坑1中 + while (l < r && arr[r] >= pivot) { + r--; + } + arr[l] = arr[r]; + // 坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中 + while (l < r && arr[l] <= pivot) { + l++; + } + arr[r] = arr[l]; + } + + // 基准值填补到坑3中,准备分治递归快排 + arr[l] = pivot; + return l; + } +``` + +以下是快速排序算法复杂度: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :-------- | :------- | :--------------------- | +| O(nlog₂n) | O(nlog₂n) | O(n²) | O(1)(原地分区递归版) | + +快速排序排序效率非常高。 虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常平均来看, 它的时间复杂为O(nlogn), 比同样为O(nlogn)时间复杂度的归并排序还要快. 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高。 + +Tips: 同选择排序相似, 快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 因此, 快速排序并不稳定。 + + + +### 基数排序(Radix Sort) + +基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。 + +![基数排序](images/Algorithm/基数排序.gif) + +**算法描述** + +- 取得数组中的最大数,并取得位数 +- arr为原始数组,从最低位开始取每个位组成radix数组 +- 对radix进行计数排序(利用计数排序适用于小范围数的特点) + +**代码实现** + +```java +/** + * 基数排序(LSD 从低位开始) + *

+ * 基数排序适用于: + * (1)数据范围较小,建议在小于1000 + * (2)每个数值都要大于等于0 + *

+ * ①. 取得数组中的最大数,并取得位数; + * ②. arr为原始数组,从最低位开始取每个位组成radix数组; + * ③. 对radix进行计数排序(利用计数排序适用于小范围数的特点); + * + * @param arr 待排序数组 + */ +public static int[] radixSort(int[] arr) { + // 取得数组中的最大数,并取得位数 + int max = 0; + for (int item : arr) { + if (max < item) { + max = item; + } + } + int maxDigit = 1; + while (max / 10 > 0) { + maxDigit++; + max = max / 10; + } + + // 申请一个桶空间 + int[][] buckets = new int[10][arr.length]; + int base = 10; + + // 从低位到高位,对每一位遍历,将所有元素分配到桶中 + for (int i = 0; i < maxDigit; i++) { + // 存储各个桶中存储元素的数量 + int[] bktLen = new int[10]; + + // 分配:将所有元素分配到桶中 + for (int value : arr) { + int whichBucket = (value % base) / (base / 10); + buckets[whichBucket][bktLen[whichBucket]] = value; + bktLen[whichBucket]++; + } + + // 收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞 + int k = 0; + for (int b = 0; b < buckets.length; b++) { + for (int p = 0; p < bktLen[b]; p++) { + arr[k++] = buckets[b][p]; + } + } + + base *= 10; + } + + return arr; +} +``` + +以下是基数排序算法复杂度,其中k为最大数的位数: + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :--------- | :--------- | :--------- | +| O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) | + +其中,**d 为位数,r 为基数,n 为原数组个数**。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 `O(d*(n + r))`。基数排序更适合用于对时间, 字符串等这些**整体权值未知的数据**进行排序。 + +Tips: 基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。 + + + +### 堆排序(Heap Sort) + +对于堆排序,首先是建立在堆的基础上,堆是一棵完全二叉树,还要先认识下大根堆和小根堆,完全二叉树中所有节点均大于(或小于)它的孩子节点,所以这里就分为两种情况: + +- 如果所有节点**「大于」**孩子节点值,那么这个堆叫做**「大根堆」**,堆的最大值在根节点 +- 如果所有节点**「小于」**孩子节点值,那么这个堆叫做**「小根堆」**,堆的最小值在根节点 + +堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。 + +![大根堆-小根堆](images/Algorithm/大根堆-小根堆.jpg) + +**算法描述** + +- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区 +- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n] +- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成 + +**动图演示** + +![堆排序](images/Algorithm/堆排序.gif) + +**代码实现** + +```java +/** + * 堆排序算法 + * + * @param arr 待排序数组 + */ + public static int[] heapSort(int[] arr) { + // 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆 + for (int i = arr.length / 2 - 1; i >= 0; i--) { + adjustHeap(arr, i, arr.length); + } + + // 将最大的节点放在堆尾,然后从根节点重新调整 + for (int j = arr.length - 1; j > 0; j--) { + // 交换 + int temp = arr[j]; + arr[j] = arr[0]; + arr[0] = temp; + + // 完成将以i对应的非叶子结点的树调整成大顶堆 + adjustHeap(arr, 0, j); + } + + return arr; + } + + /** + * 功能: 完成将以i对应的非叶子结点的树调整成大顶堆 + * + * @param arr 待排序数组 + * @param i 表示非叶子结点在数组中索引 + * @param length 表示对多少个元素继续调整, length 是在逐渐的减少 + */ + private static void adjustHeap(int[] arr, int i, int length) { + // 先取出当前元素的值,保存在临时变量 + int temp = arr[i]; + //开始调整。说明:1. k = i * 2 + 1 k 是 i结点的左子结点 + for (int k = i * 2 + 1; k < length; k = k * 2 + 1) { + // 说明左子结点的值小于右子结点的值 + if (k + 1 < length && arr[k] < arr[k + 1]) { + // k 指向右子结点 + k++; + } + + // 如果子结点大于父结点 + if (arr[k] > temp) { + // 把较大的值赋给当前结点 + arr[i] = arr[k]; + // i 指向 k,继续循环比较 + i = k; + } else { + break; + } + } + + //当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)。将temp值放到调整后的位置 + arr[i] = temp; + } +``` + +| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | +| :------------- | :------------- | :------------- | :--------- | +| O(n \log_{2}n) | O(n \log_{2}n) | O(n \log_{2}n) | O(1) | + +Tips: **由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列.** 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序. + + + +### 计数排序(Counting Sort) + +计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。 + +**算法描述** + +- 找出待排序的数组中最大和最小的元素 +- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项 +- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) +- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1 + +**动图演示** + +![计数排序](images/Algorithm/计数排序.gif) + +**代码实现** + +```java +/** + * 计数排序算法 + * + * @param arr 待排序数组 + */ + public static int[] countingSort(int[] arr) { + // 得到数列的最大值与最小值,并算出差值d + int max = arr[0]; + int min = arr[0]; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + if (arr[i] < min) { + min = arr[i]; + } + } + int d = max - min; + + // 创建统计数组并计算统计对应元素个数 + int[] countArray = new int[d + 1]; + for (int value : arr) { + countArray[value - min]++; + } + + // 统计数组变形,后面的元素等于前面的元素之和 + int sum = 0; + for (int i = 0; i < countArray.length; i++) { + sum += countArray[i]; + countArray[i] = sum; + } + + // 倒序遍历原始数组,从统计数组找到正确位置,输出到结果数组 + int[] sortedArray = new int[arr.length]; + for (int i = arr.length - 1; i >= 0; i--) { + sortedArray[countArray[arr[i] - min] - 1] = arr[i]; + countArray[arr[i] - min]--; + } + + return sortedArray; + } + +``` + +**算法分析** + +计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。 + + + +### 桶排序(Bucket Sort) + +桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。 + +![桶排序](images/Algorithm/桶排序.png) + +![BucketSort](images/Algorithm/BucketSort.gif) + +**算法描述** + +- 设置一个定量的数组当作空桶 +- 遍历输入数据,并且把数据一个一个放到对应的桶里去 +- 对每个不是空的桶进行排序 +- 从不是空的桶里把排好序的数据拼接起来 + +**代码实现** + +```java +/** + * 桶排序算法 + * + * @param arr 待排序数组 + */ + public static int[] bucketSort(int[] arr) { + // 计算最大值与最小值 + int max = Integer.MIN_VALUE; + int min = Integer.MAX_VALUE; + for (int value : arr) { + max = Math.max(max, value); + min = Math.min(min, value); + } + + // 计算桶的数量 + int bucketNum = (max - min) / arr.length + 1; + List> bucketArr = new ArrayList<>(bucketNum); + for (int i = 0; i < bucketNum; i++) { + bucketArr.add(new ArrayList<>()); + } + + // 将每个元素放入桶 + for (int value : arr) { + int num = (value - min) / (arr.length); + bucketArr.get(num).add(value); + } + + // 对每个桶进行排序 + for (List integers : bucketArr) { + Collections.sort(integers); + } + + // 将桶中的元素赋值到原序列 + int index = 0; + for (List integers : bucketArr) { + for (Integer integer : integers) { + arr[index++] = integer; + } + } + + return arr; + } +``` + +**算法分析** + +桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 \ No newline at end of file diff --git a/src/Algorithm/107.md b/src/Algorithm/107.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Algorithm/2.md b/src/Algorithm/2.md new file mode 100644 index 0000000..e1ab84c --- /dev/null +++ b/src/Algorithm/2.md @@ -0,0 +1,843 @@ +### 数组(Array) + +![数据结构-array](images/Algorithm/数据结构-array.png) + +**优点** + +- 构建非常简单 +- 能在 O(1) 的时间里根据数组的下标(index)查询某个元素 + +**缺点** + +- 构建时必须分配一段连续的空间 +- 查询某个元素是否存在时需要遍历整个数组,耗费 O(n) 的时间(其中,n 是元素的个数) +- 删除和添加某个元素时,同样需要耗费 O(n) 的时间 + +**基本操作** + +- **insert**:在某个索引处插入元素 +- **get**:读取某个索引处的元素 +- **delete**:删除某个索引处的元素 +- **size**:获取数组的长度 + + + +**案例一:翻转字符串“algorithm”** + +![翻转字符串algorithm](images/Algorithm/翻转字符串algorithm.gif) + +**解法**:用两个指针,一个指向字符串的第一个字符 a,一个指向它的最后一个字符 m,然后互相交换。交换之后,两个指针向中央一步步地靠拢并相互交换字符,直到两个指针相遇。这是一种比较快速和直观的方法。 + +**案例二:给定两个字符串s和t,编写一个函数来判断t是否是s的字母异位词。** + +说明:你可以假设字符串只包含小写字母。 + +**解题思路**:字母异位词,也就是两个字符串中的相同字符的数量要对应相等。 + +- 可以利用两个长度都为 26 的字符数组来统计每个字符串中小写字母出现的次数,然后再对比是否相等 +- 可以只利用一个长度为 26 的字符数组,将出现在字符串 s 里的字符个数加 1,而出现在字符串 t 里的字符个数减 1,最后判断每个小写字母的个数是否都为 0 + + + +### 链表(Linked List) + +链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。 + +![链表(LinkedList)](images/Algorithm/链表(LinkedList).png) + +**优点** + +- 链表能灵活地分配内存空间 +- 能在 O(1) 时间内删除或者添加元素 + +**缺点** + +- 不像数组能通过下标迅速读取元素,每次都要从链表头开始一个一个读取 +- 查询第 k 个元素需要 O(k) 时间 + +**基本操作** + +- **insertAtEnd**:在链表结尾插入元素 +- **insertAtHead**:在链表开头插入元素 +- **delete** :删除链表的指定元素 +- **deleteAtHead** :删除链表第一个元素 +- **search**:在链表中查询指定元素 +- **isEmpty**:查询链表是否为空 + +**应用场景** + +- 如果要解决的问题里面**需要很多快速查询**,链表可能并不适合 +- 如果遇到的问题中,数据的元素个数不确定,而且需要**经常进行数据的添加和删除**,那么链表会比较合适 +- 如果数据元素大小确定,**删除和插入的操作并不多**,那么数组可能更适合 + + + +**链表实现数据结构** + +**① 用链表实现队列** + +![用链表实现栈](images/Algorithm/用链表实现栈.png) + +**② 用链表实现栈** + +![用链表实现队列](images/Algorithm/用链表实现队列.png) + + + +**链表翻转算法** + +**① 递归翻转** + +```java +/** + * 链表递归翻转模板 + */ +public Node reverseLinkedList(参数0) { + // Step1:终止条件 + if (终止条件) { + return; + } + + // Step2:逻辑处理:可能有,也可能没有,具体问题具体分析 + // Step3:递归调用 + Node reverse = reverseLinkedList(参数1); + // Step4:逻辑处理:可能有,也可能没有,具体问题具体分析 +} + +/** + * 链表递归翻转算法 + */ +public Node reverseLinkedList(Node head) { + // Step1:终止条件 + if (head == null || head.next == null) { + return head; + } + + // Step2:保存当前节点的下一个结点 + Node next = head.next; + // Step3:从当前节点的下一个结点开始递归调用 + Node reverse = reverseLinkedList(next); + // Step4:head挂到next节点的后面就完成了链表的反转 + next.next = head; + // 这里head相当于变成了尾结点,尾结点都是为空的,否则会构成环 + head.next = null; + return reverse; +} +``` + +**② 三指针翻转** + +```java +public static Node reverseLinkedList(Node head) { + // 单链表为空或只有一个节点,直接返回原单链表 + if (head == null || head.getNext() == null) { + return head; + } + + // 前一个节点指针 + Node preNode = null; + // 当前节点指针 + Node curNode = head; + // 下一个节点指针 + Node nextNode = null; + while (curNode != null) { + // nextNode 指向下一个节点 + nextNode = curNode.getNext(); + // 将当前节点next域指向前一个节点 + curNode.setNext(preNode); + // preNode 指针向后移动 + preNode = curNode; + // curNode指针向后移动 + curNode = nextNode; + } + + return preNode; +} +``` + +**③ 利用栈翻转** + +```java +public Node reverseLinkedList(Node node) { + Stack nodeStack = new Stack<>(); + // 存入栈中,模拟递归开始的栈状态 + while (node != null) { + nodeStack.push(node); + node = node.getNode(); + } + + // 特殊处理第一个栈顶元素:反转前的最后一个元素,因为它位于最后,不需要反转 + Node head = null; + if ((!nodeStack.isEmpty())) { + head = nodeStack.pop(); + } + + // 排除以后就可以快乐的循环 + while (!nodeStack.isEmpty()) { + Node tempNode = nodeStack.pop(); + tempNode.getNode().setNode(tempNode); + tempNode.setNode(null); + } + + return head; +} +``` + + + +#### 单向链表 + +![单向链表](images/Algorithm/单向链表.png) + +单向链表包含两个域: + +- **一个数据域**:用于存储数据 +- **一个指针域**:用于指向下一个节点(最后一个节点则指向一个空值): + +单链表的遍历方向单一,只能从链头一直遍历到链尾。它的缺点是当要查询某一个节点的前一个节点时,只能再次从头进行遍历查询,因此效率比较低,而双向链表的出现恰好解决了这个问题。单向链表代码如下: + +```java +public class Node { + private Eitem; + private Node next; +} +``` + + + +#### 双向链表 + +![双向链表](images/Algorithm/双向链表.png) + +双向链表的每个节点由三部分组成: + +- **prev指针**:指向前置节点 +- **item节点**:数据信息 +- **next指针**:指向后置节点 + +双向链表代码如下: + +```kotlin +public class Node { + private E item; + private Node next; + private Node prev; +} +``` + + + +#### 循环链表 + +循环链表又分为单循环链表和双循环链表,也就是将单向链表或双向链表的首尾节点进行连接。 + +- **单循环链表** + + ![单循环链表](images/Algorithm/单循环链表.png) + +- **双循环链表** + + ![双循环链表](images/Algorithm/双循环链表.png) + + + +### 栈(Stack) + +栈是一种**先进后出**(`FILO`,First in last out)或**后进先出**(`LIFO`,Last in first out)的数据结构。 + +![Stack-Push-and-Pop-Operations](images/Algorithm/Stack-Push-and-Pop-Operations.png) + +- **单向链表**:可以利用一个单链表来实现栈的数据结构。而且,因为我们都只针对栈顶元素进行操作,所以借用单链表的头就能让所有栈的操作在 O(1) 的时间内完成。 +- **Stack**:是Vector的子类,比Vector多了几个方法 + +```java +public class Stack extends Vector { + // 把元素压入栈顶 + public E push(E item) { + addElement(item); + return item; + } + + // 弹出栈顶元素 + public synchronized E pop() { + E obj; + int len = size(); + obj = peek(); + removeElementAt(len - 1); + return obj; + } + + // 访问当前栈顶元素,但是不拿走栈顶元素 + public synchronized E peek() { + int len = size(); + if (len == 0) + throw new EmptyStackException(); + return elementAt(len - 1); + } + + // 测试堆栈是否为空 + public boolean empty() { + return size() == 0; + } + + // 返回对象在堆栈中的位置,以1为基数 + public synchronized int search(Object o) { + int i = lastIndexOf(o); + if (i >= 0) { + return size() - i; + } + return -1; + } +} +``` + +**基本操作**(失败时:add/remove/element为抛异常,offer/poll/peek为返回false或null) + +- `E push(E)`:把元素压入栈 +- `E pop()`:把栈顶的元素弹出 +- `E peek()`:取栈顶元素但不弹出 +- `boolean empty()`:堆栈是否为空测试 +- `int search(o)`:返回对象在堆栈中的位置,以 1 为基数 + +**应用场景** + +在解决某个问题的时候,只要求关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作。 + +![Stack](images/Algorithm/Stack.png) + + + +**案例一:判断字符串是否有效** + +给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足: + +- 左括号必须用相同类型的右括号闭合 +- 左括号必须以正确的顺序闭合 +- 空字符串可被认为是有效字符串 + +**解题思路**:利用一个栈,不断地往里压左括号,一旦遇上了一个右括号,我们就把栈顶的左括号弹出来,表示这是一个合法的组合,以此类推,直到最后判断栈里还有没有左括号剩余。 + +![Stack判断字符串是否有效](images/Algorithm/Stack判断字符串是否有效.gif) + +**案例二:每日温度** + +根据每日气温列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。 + +**解题思路** + +- 思路 1:最直观的做法就是针对每个温度值向后进行依次搜索,找到比当前温度更高的值,这样的计算复杂度就是 O(n2)。 + +- 思路 2:可以运用一个堆栈 stack 来快速地知道需要经过多少天就能等到温度升高。从头到尾扫描一遍给定的数组 T,如果当天的温度比堆栈 stack 顶端所记录的那天温度还要高,那么就能得到结果。 + + ![Stack每日温度](images/Algorithm/Stack每日温度.gif) + + + +### 队列(Queue) + +队列(Queue)和栈不同,队列的最大特点是**先进先出**(`FIFO`),就好像按顺序排队一样。对于队列的数据来说,我们只允许在队尾查看和添加数据,在队头查看和删除数据。 + +- **栈**:采用**后进先出**(`LIFO`) +- **队列**:采用 **先进先出**(First in First Out,即`FIFO`) + +![FIFO-Representation-of-Queue](images/Algorithm/FIFO-Representation-of-Queue.png) + +**实现方式** + +可借助**双链表**来实现队列。双链表的头指针允许在队头查看和删除数据,而双链表的尾指针允许在队尾查看和添加数据。 + +**基本操作**(失败时:add/remove/element为抛异常,offer/poll/peek为返回false或null) + +- `int size()`:获取队列长度 +- `boolean add(E)`/`boolean offer(E)`:添加元素到队尾 +- `E remove()`/`E poll()`:获取队首元素并从队列中删除 +- `E element()`/`E peek()`:获取队首元素但并不从队列中删除 + +**应用场景** + +当需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来处理。 + + + +### 双端队列(Deque) + +双端队列和普通队列最大的不同在于,它允许我们在队列的头尾两端都能在 O(1) 的时间内进行数据的查看、添加和删除。 + +**实现方式** + +双端队列(Deque)与队列相似,可以利用一个**双链表实现**双端队列。 + +**应用场景** + +双端队列最常用的地方就是实现一个长度动态变化的窗口或者连续区间,而动态窗口这种数据结构在很多题目里都有运用。 + + + +**案例一:滑动窗口最大值** + +给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字,滑动窗口每次只向右移动一位,就返回当前滑动窗口中的最大值。 + +**解题思路** + +- 思路 1:移动窗口,扫描,获得最大值。假设数组里有 n 个元素,算法复杂度就是 O(n)。这是最直观的做法。 + +- 思路 2:利用一个双端队列来保存当前窗口中最大那个数在数组里的下标,双端队列新的头就是当前窗口中最大的那个数。通过该下标,可以很快知道新窗口是否仍包含原来那个最大的数。如果不再包含,就把旧的数从双端队列的头删除。 + + 因为双端队列能让上面的这两种操作都能在 O(1) 的时间里完成,所以整个算法的复杂度能控制在 O(n)。 + + ![Deque滑动窗口最大值](images/Algorithm/Deque滑动窗口最大值.gif) + + + +### 树(Tree) + +**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成,是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归。各种树解决的问题以及面临的新问题: + +- **二叉查找树(BST)**:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表 +- **平衡二叉树(AVL)**:通过旋转解决了平衡的问题,但是旋转操作效率太低 +- **红黑树**:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多 +- **B树**:通过将二叉树改为多路平衡查找树,解决了树过高的问题 +- **B+树**:在B树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效 + + + +#### 树的遍历 + +##### 前序遍历(Preorder Traversal) + +**实现原理**:`先访问根节点,然后访问左子树,最后访问右子树`。 + +**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。 + +![前序遍历](images/Algorithm/前序遍历.gif) + +```java +// 递归实现 +public void preOrderTraverse1(TreeNode root) { + if (root != null) { + System.out.print(root.val + "->"); + preOrderTraverse1(root.left); + preOrderTraverse1(root.right); + } +} + +// 非递归实现 +public void preOrderTraverse2(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.empty()) { + if (node != null) { + System.out.print(node.val + "->"); + stack.push(node); + node = node.left; + } else { + TreeNode tem = stack.pop(); + node = tem.right; + } + } +} +``` + + + +##### 中序遍历(Inorder Traversal) + +**实现原理**:`先访问左子树,然后访问根节点,最后访问右子树`。 + +**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。 + +![中序遍历](images/Algorithm/中序遍历.gif) + +```java +// 递归实现 +public void inOrderTraverse(TreeNode root) { + if (root != null) { + inOrderTraverse(root.left); + System.out.print(root.val + "->"); + inOrderTraverse(root.right); + } +} + +// 非递归实现 +public void inOrderTraverse(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode node = root; + while (node != null || !stack.isEmpty()) { + if (node != null) { + stack.push(node); + node = node.left; + } else { + TreeNode tem = stack.pop(); + System.out.print(tem.val + "->"); + node = tem.right; + } + } +} +``` + + + +##### 后序遍历(Postorder Traversal) + +**实现原理**:`先访问左子树,然后访问右子树,最后访问根节点`。 + +**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。 + +![后序遍历](images/Algorithm/后序遍历.gif) + +```java +// 递归实现 +public void postOrderTraverse(TreeNode root) { + if (root != null) { + postOrderTraverse(root.left); + postOrderTraverse(root.right); + System.out.print(root.val + "->"); + } +} + +// 非递归实现 +public void postOrderTraverse(TreeNode root) { + TreeNode cur, pre = null; + + Stack stack = new Stack<>(); + stack.push(root); + + while (!stack.empty()) { + cur = stack.peek(); + if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) { + System.out.print(cur.val + "->"); + stack.pop(); + pre = cur; + } else { + if (cur.right != null) + stack.push(cur.right); + if (cur.left != null) + stack.push(cur.left); + } + } +} +``` + + + +##### 层次遍历 + +```java +public void levelOrderTraverse(TreeNode root) { + if (root == null) { + return; + } + + Queue queue = new LinkedList(); + queue.add(root); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); + System.out.print(node.val + "->"); + + if (node.left != null) { + queue.add(node.left); + } + if (node.right != null) { + queue.add(node.right); + } + } +} +``` + + + +#### 二叉树(Binary Tree) + +**二叉树是每个节点最多有两个子节点的树**。二叉树的叶子节点有0个字节点,根节点或内部节点有一个或两个子节点。 + +![BinaryTree](images/Algorithm/BinaryTree.png) + +**存储结构**: + +```java +public class TreeNode { + // 数据域 + private Object data; + // 左孩子指针 + private TreeNode leftChild; + // 右孩子指针 + private TreeNode rightChild; +} +``` + + + +##### 斜树 + +- **左斜树**:所有结点都只有左子树 +- **右斜树**:所有结点都只有右子树 + +![斜树](images/Algorithm/斜树.png) + + + +##### 满二叉树 + +一颗二叉树的所有分支结点都存在左子树和右子树,且所有叶子节点都只存在在最下面一层。 + +![满二叉树](images/Algorithm/满二叉树.png) + + + +##### 完全二叉树 + +若二叉树的深度为k,二叉树的层数从1到k-1层的结点都达到了最大个数,在第k层的所有结点都集中在最左边,这就是完全二叉树。完全二叉树由满二叉树引出,满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。 + +![完全二叉树](images/Algorithm/完全二叉树.png) + + + +#### 二叉搜索树(Binary Search Tree) + +二叉搜索树, 又叫**二叉查找树**,它是一棵空树或是具有下列性质的二叉树: + +- **若左子树不空,则左子树上所有结点的值均小于它的根结点的值** +- **若右子树不空,则右子树上所有结点的值均大于它的根结点的值** +- **它的左、右子树也分别为二叉搜索树** + +![BinarySearchTree](images/Algorithm/BinarySearchTree.png) + +**效率总结** + +- **访问/查找**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)` +- **插入/删除**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)` + + + +#### 平衡二叉树(AVL Tree) + +二叉查找树在最差情况下竟然和顺序查找效率相当,这是无法仍受的。事实也证明,当存储数据足够大的时候,树的结构对某些关键字的查找效率影响很大。当然,造成这种情况的主要原因就是BST不够平衡(左右子树高度差太大)。既然如此,那么我们就需要通过一定的算法,将不平衡树改变成平衡树。因此,AVL树就诞生了。 + +**平衡二叉树全称叫做 `平衡二叉搜索(排序)树`,简称 AVL树**。高度为 `logN`。本质是一颗二叉查找树,AVL树的特性: + +- 它是**一棵空树**或**左右两个子树的高度差**的绝对值不超过 `1` +- 左右两个子树也都是一棵平衡二叉树 + +如下图,根节点左边高度是3,因为左边最多有3条边;右边高度而2,相差1。根节点左边的节点50的左边是1条边,高度为1,右边有两条边,高度为2,相差1。 + +![AVLTree](images/Algorithm/AVLTree.png) + +**效率总结** + +- 查找:时间复杂度维持在`O(logN)`,不会出现最差情况 +- 插入:插入操作时最多需要 `1` 次旋转,其时间复杂度在`O(logN)`左右 +- 删除:删除时代价稍大,执行每个删除操作的时间复杂度需要`O(2logN)` + + + +#### 红黑树(Red-Black Tree) + +二叉平衡树的严格平衡策略以**牺牲建立查找结构(插入,删除操作)的代**价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢? 能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。 + +**红黑树是一种含有红、黑结点,并能自平衡的二叉查找树**。高度为 `logN`。其性质如下: + +- **每个结点或是红色的,或是黑色的** +- **根节点是黑色的** +- **每个叶子节点(NIL)是黑色的** +- **如果一个节点是红色的,则它的两个子节点都是黑色的** +- **任意一结点到每个叶子结点的路径都包含数量相同的黑节点** + +正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logN的高度,从而也就解释了“红黑树的查找、插入、删除的时间复杂度最坏为O(logN)”这一结论成立的原因。 + +![Red-BlackTree](images/Algorithm/Red-BlackTree.jpg) + +**效率总结** + +- **查找**:最好情况下时间复杂度为`O(logN)`,但在最坏情况下比`AVL`要差一些,但也远远好于`BST` + +- **插入/删除**:改变树的平衡性的概率要远远小于`AVL`(RBT不是高度平衡的) + + 因此需要旋转操作的可能性要小,且一旦需要旋转,插入一个结点最多只需旋转`2`次,删除最多只需旋转`3`次(小于`AVL`的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在`O(logN)`,但是实际上,这种操作由于简单所需要的代价很小 + + + +##### 左旋 + +![左旋](images/Algorithm/左旋.jpg) + +- **逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点** +- **对x进行左旋,意味着"将x变成一个左节点"** +- **左旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在右子树** + + + +**情况1**:如果当前节点是右子树,并且父节点是左子树。`形如:(900是新插入节点)` + +![左旋条件情况1](images/Algorithm/左旋条件情况1.png) + +要根据父节点左旋【899】(根据谁左旋,谁就变成子节点): + +![左旋条件情况1流程](images/Algorithm/左旋条件情况1流程.gif) + +【900】的左子树挂到 【899】的右子树上 , 【899】变为子节点 , 【900】变为父节点此时不变色。 + + + +**情况2**:如果当前节点是右子树,并且父节点是右子树。形如:(【100】是当前节点) + +![左旋条件情况2](images/Algorithm/左旋条件情况2.png) + +根据祖父节点左旋【56】(根据谁左旋,谁就变成子节点): + +![左旋条件情况2流程](images/Algorithm/左旋条件情况2流程.gif) + +【56】变为子节点,【89】变为父节点,【89】的左子树 挂到 【56】的右子树。同时: 父节点变黑(【89】变黑),祖父节点变红(【56】变红 )。 + + + +##### 右旋 + + + +![右旋](images/Algorithm/右旋.jpg) + +- **顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点** +- **对x进行左旋,意味着"将x变成一个左节点"** +- **右旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在左子树** + + + +**情况1**:如果当前节点是左子树,并且父节点也是右子树。形如:(【8000】是当前节点) + +![右旋条件情况1](images/Algorithm/右旋条件情况1.png) + +根据父节点右旋【9000】(根据谁右旋,谁就变成子节点): + +![右旋条件情况1流程](images/Algorithm/右旋条件情况1流程.gif) + +【9000】变为子节点,【8000】变为父节点,【8000】的右子树 挂到 【9000】的左子树,此时不变色。 + + + +**情况2**:如果当前节点是左子树,并且父节点也是左子树。形如:(【899】是当前节点) + +![右旋条件情况2](images/Algorithm/右旋条件情况2.png) + +根据祖父节点右旋【1000】(根据谁右旋,谁就变成子节点): + +![右旋条件情况2流程](images/Algorithm/右旋条件情况2流程.gif) + +【1000】变为子节点,【900】变为父节点,【900】的右子树 挂到 【1000】的左子树 +同时: 父节点变黑(【900】变黑),祖父节点变红(【1000】变红)。 + + + +##### 变色 + +如果当前节点的父亲节点和叔叔节点均是红色,那么执行以下变色操作: + +父 --> 黑 +叔 --> 黑 +爷 --> 红 + + + +**无法通过变色而进行旋转的四种场景** + +**场景一:左左节点旋转** +这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。规则如下:以祖父节点【右旋】,搭配【变色】。 + +![左左节点旋转](images/Algorithm/左左节点旋转.png) + +按照规则,步骤如下: + +![左左节点旋转步骤](images/Algorithm/左左节点旋转步骤.jpeg) + +**场景二:左右节点旋转** +这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】。按照规则,步骤如下: + +![左右节点旋转](images/Algorithm/左右节点旋转.png) + +**场景三:右左节点旋转** +这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2)这种情况,我们要插入节点 68。规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】。 + +![右左节点旋转](images/Algorithm/右左节点旋转.png) + +按照规则,步骤如下: + +![右左节点旋转步骤](images/Algorithm/右左节点旋转步骤.jpeg) + +**场景四:右右节点旋转** +这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。规则如下:以祖父节点【左旋】,搭配【变色】。按照规则,步骤如下: + +![右右节点旋转](images/Algorithm/右右节点旋转.png) + + + +#### B-树(Balance Tree) + +![B-tree图解](images/Algorithm/B-tree图解.png) + +对于在内存中的查找结构而言,红黑树的效率已经非常好了(实际上很多实际应用还对RBT进行了优化)。但是如果是数据量非常大的查找呢?将这些数据全部放入内存组织成RBT结构显然是不实际的。实际上,像OS中的文件目录存储,数据库中的文件索引结构的存储…. 都不可能在内存中建立查找结构。必须在磁盘中建立好这个结构。 +在磁盘中组织查找结构,从任何一个结点指向其他结点都有可能读取一次磁盘数据,再将数据写入内存进行比较。大家都知道,频繁的磁盘IO操作,效率是很低下的(机械运动比电子运动要慢不知道多少)。显而易见,所有的二叉树的查找结构在磁盘中都是低效的。因此,B树很好的解决了这一个问题。 + + + +**B树也称B-树、B-Tree,它是一颗多路平衡查找树**。描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。B树的定义: + +- **每个结点最多有m-1个关键字** +- **根结点最少可以只有1个关键字** +- **非根结点至少有Math.ceil(m/2)-1个关键字** +- **每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它** +- **所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同** + + + +**B树的优势** + +- **B树的高度远远小于AVL树和红黑树(B树是一颗“矮胖子”),磁盘IO次数大大减少** +- **对访问局部性原理的利用**。指当一个数据被使用时,其附近的数据有较大概率在短时间内被使用。当访问其中某个数据时,数据库会将该整个节点读到缓存中;当它临近的数据紧接着被访问时,可以直接在缓存中读取,无需进行磁盘IO + + + +**案例分析** + +如下图(B树的内部节点可以存放数据,类似ZK的中间节点一样。B树不是每个节点都有足够多的子节点): + +![BalanceTree](images/Algorithm/BalanceTree.png) + +上图是一颗阶数为4的B树。在实际应用中的B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。**我们将一个key和其对应的data称为一个记录**。**但为了方便描述,除非特别说明,后续文中就用key来代替(key, value)键值对这个整体**。在数据库中我们将B树(和B+树)作为索引结构,可以加快查询速速,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。 + + + +#### B+树(B+Tree) + +![B+tree图解](images/Algorithm/B+tree图解.png) + +**B+树是从B树的变体**。跟B树的不同: + +- **内部节点不保存数据,只用于索引** +- **B+树的每个叶子节点之间存在指针相连,而且是单链表**,叶子节点本身依关键字的大小自小而大顺序链接 + + + +**案例分析** + +如下图,其实B+树上二叉搜索树的扩展,二叉搜索树是每次一分为二,B树是每次一分为多,现代操作系统中,磁盘的存储结构使用的是B+树机制,mysql的innodb引擎的存储方式也是B+树机制: + +![B+Tree](images/Algorithm/B+Tree.png) + +**B+树与B树相比有以下优势** + +- **更少的IO次数**:B+树的非叶节点只包含键,而不包含真实数据,因此每个节点存储的记录个数比B数多很多(即阶m更大),因此B+树的高度更低,访问时所需要的IO次数更少。此外,由于每个节点存储的记录数更多,所以对访问局部性原理的利用更好,缓存命中率更高 +- **更适于范围查询**:在B树中进行范围查询时,首先找到要查找的下限,然后对B树进行中序遍历,直到找到查找的上限;而B+树的范围查询,只需要对链表进行遍历即可 +- **更稳定的查询效率**:B树的查询时间复杂度在1到树高之间(分别对应记录在根节点和叶节点),而B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。 + + + +**B+树劣势** + +由于键会重复出现,因此**会占用更多的空间**。但是与带来的性能优势相比,空间劣势往往可以接受,因此B+树的在数据库中的使用比B树更加广泛。 + + + +#### B*树 + +是B+树的变体,**在B+树的非根和非叶子结点再增加指向兄弟的指针**,且**定义了非叶子结点关键字个数至少为(2/3)×M**,即块的最低使用率为2/3(代替B+树的1/2): + +![Bx树](images/Algorithm/Bx树.jpg) \ No newline at end of file diff --git a/src/Algorithm/201.md b/src/Algorithm/201.md new file mode 100644 index 0000000..b57d274 --- /dev/null +++ b/src/Algorithm/201.md @@ -0,0 +1,53 @@ +### 删除排序数组中的重复项 + +给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + +```java +/** + * 双指针解决 + */ +public int removeDuplicates(int[] A) { + // 边界条件判断 + if (A == null || A.length == 0){ + return 0; + } + + int left = 0; + for (int right = 1; right < A.length; right++){ + // 如果左指针和右指针指向的值一样,说明有重复的, + // 这个时候,左指针不动,右指针继续往右移。如果他俩 + // 指向的值不一样就把右指针指向的值往前挪 + if (A[left] != A[right]){ + A[++left] = A[right]; + } + } + return ++left; +} +``` + + + +### 移动零 + +给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。 + +**示例:**输入: [0,1,0,3,12],输出: [1,3,12,0,0] + +可以参照双指针的思路解决,指针j是一直往后移动的,如果指向的值不等于0才对他进行操作。而i统计的是前面0的个数,我们可以把j-i看做另一个指针,它是指向前面第一个0的位置,然后我们让j指向的值和j-i指向的值交换 + + +```java +public void moveZeroes(int[] nums) { + int i = 0;// 统计前面0的个数 + for (int j = 0; j < nums.length; j++) { + if (nums[j] == 0) {//如果当前数字是0就不操作 + i++; + } else if (i != 0) { + //否则,把当前数字放到最前面那个0的位置,然后再把 + //当前位置设为0 + nums[j - i] = nums[j]; + nums[j] = 0; + } + } +} +``` \ No newline at end of file diff --git a/src/Algorithm/202.md b/src/Algorithm/202.md new file mode 100644 index 0000000..d0bc6e9 --- /dev/null +++ b/src/Algorithm/202.md @@ -0,0 +1,27 @@ +### 旋转数组 + +给定一个数组,将数组中的元素向右移动 `k` 个位置,其中 `k` 是非负数。 + +先反转全部数组,在反转前k个,最后在反转剩余的,如下所示: + +![旋转数组](images/Algorithm/旋转数组.png) + + +```java +public void rotate(int[] nums, int k) { + int length = nums.length; + k %= length; + reverse(nums, 0, length - 1);//先反转全部的元素 + reverse(nums, 0, k - 1);//在反转前k个元素 + reverse(nums, k, length - 1);//接着反转剩余的 +} + +//把数组中从[start,end]之间的元素两两交换,也就是反转 +public void reverse(int[] nums, int start, int end) { + while (start < end) { + int temp = nums[start]; + nums[start++] = nums[end]; + nums[end--] = temp; + } +} +``` \ No newline at end of file diff --git a/src/Algorithm/203.md b/src/Algorithm/203.md new file mode 100644 index 0000000..313eb8a --- /dev/null +++ b/src/Algorithm/203.md @@ -0,0 +1,297 @@ +### 判断字符串括号是否合法 + +字符串中只有字符'('和')'。合法字符串需要括号可以配对。如:(),()(),(())是合法的。)(,()(,(()是非法的。 + +**① 栈** + +![判断字符串括号是否合法](images/Algorithm/判断字符串括号是否合法.gif) + +```java +/** + * 判断字符串括号是否合法 + * + * @param s + * @return + */ +public boolean isValid(String s) { + // 当字符串本来就是空的时候,我们可以快速返回true + if (s == null || s.length() == 0) { + return true; + } + + // 当字符串长度为奇数的时候,不可能是一个有效的合法字符串 + if (s.length() % 2 == 1) { + return false; + } + + // 消除法的主要核心逻辑: + Stack t = new Stack<>(); + for (int i = 0; i < s.length(); i++) { + // 取出字符 + char c = s.charAt(i); + if (c == '(') { + // 如果是'(',那么压栈 + t.push(c); + } else if (c == ')') { + // 如果是')',那么就尝试弹栈 + if (t.empty()) { + // 如果弹栈失败,那么返回false + return false; + } + t.pop(); + } + } + + return t.empty(); +} +``` + +**复杂度分析**:每个字符只入栈一次,出栈一次,所以时间复杂度为 O(N),而空间复杂度为 O(N),因为最差情况下可能会把整个字符串都入栈。 + + + +**② 栈深度扩展** + +如果仔细观察,你会发现,栈中存放的元素是一样的。全部都是左括号'(',除此之外,再也没有别的元素,优化方法如下。**栈中元素都相同时,实际上没有必要使用栈,只需要记录栈中元素个数。** 我们可以通过画图来解决这个问题,如下图所示: + +![leftBraceNumber加减](images/Algorithm/leftBraceNumber加减.gif) + +```java +/** + * 判断字符串括号是否合法 + * + * @param s + * @return + */ +public boolean isValid(String s) { + // 当字符串本来就是空的时候,我们可以快速返回true + if (s == null || s.length() == 0) { + return true; + } + + // 当字符串长度为奇数的时候,不可能是一个有效的合法字符串 + if (s.length() % 2 == 1) { + return false; + } + + // 消除法的主要核心逻辑: + int leftBraceNumber = 0; + for (int i = 0; i < s.length(); i++) { + // 取出字符 + char c = s.charAt(i); + if (c == '(') { + // 如果是'(',那么压栈 + leftBraceNumber++; + } else if (c == ')') { + // 如果是')',那么就尝试弹栈 + if (leftBraceNumber <= 0) { + // 如果弹栈失败,那么返回false + return false; + } + --leftBraceNumber; + } + } + + return leftBraceNumber == 0; +} +``` + +**复杂度分析**:每个字符只入栈一次,出栈一次,所以时间复杂度为 O(N),而空间复杂度为 O(1),因为我们已经只用一个变量来记录栈中的内容。 + + + +**③ 栈广度扩展** + +接下来再来看看如何进行广度扩展。观察题目可以发现,栈中只存放了一个维度的信息:左括号'('和右括号')'。如果栈中的内容变得更加丰富一点,就可以得到下面这道扩展题。给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足: + +- 左括号必须用相同类型的右括号闭合 +- 左括号必须以正确的顺序闭合 +- 注意空字符串可被认为是有效字符串 + +```java +/** + * 判断字符串括号是否合法 + * + * @param s + * @return + */ +public boolean isValid(String s) { + if (s == null || s.length() == 0) { + return true; + } + if (s.length() % 2 == 1) { + return false; + } + + Stack t = new Stack<>(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '{' || c == '(' || c == '[') { + t.push(c); + } else if (c == '}') { + if (t.empty() || t.peek() != '{') { + return false; + } + t.pop(); + } else if (c == ']') { + if (t.empty() || t.peek() != '[') { + return false; + } + t.pop(); + } else if (c == ')') { + if (t.empty() || t.peek() != '(') { + return false; + } + t.pop(); + } else { + return false; + } + } + + return t.empty(); +} +``` + + + +### 大鱼吃小鱼 + +在水中有许多鱼,可以认为这些鱼停放在 x 轴上。再给定两个数组 Size,Dir,Size[i] 表示第 i 条鱼的大小,Dir[i] 表示鱼的方向 (0 表示向左游,1 表示向右游)。这两个数组分别表示鱼的大小和游动的方向,并且两个数组的长度相等。鱼的行为符合以下几个条件: + +- 所有的鱼都同时开始游动,每次按照鱼的方向,都游动一个单位距离 +- 当方向相对时,大鱼会吃掉小鱼; +- 鱼的大小都不一样。 + +输入:Size = [4, 3, 2, 1, 5], Dir = [1, 1, 1, 1, 0],输出:1。请完成以下接口来计算还剩下几条鱼? + +![大鱼吃小鱼](images/Algorithm/大鱼吃小鱼.gif) + +```java +/** + * 大鱼吃小鱼 + * + * @param fishSize + * @param fishDirection + * @return + */ +public int solution(int[] fishSize, int[] fishDirection) { + // 首先拿到鱼的数量: 如果鱼的数量小于等于1,那么直接返回鱼的数量 + final int fishNumber = fishSize.length; + if (fishNumber <= 1) { + return fishNumber; + } + + // 0表示鱼向左游 + final int left = 0; + // 1表示鱼向右游 + final int right = 1; + Stack t = new Stack<>(); + for (int i = 0; i < fishNumber; i++) { + // 当前鱼的情况:1,游动的方向;2,大小 + final int curFishDirection = fishDirection[i]; + final int curFishSize = fishSize[i]; + // 当前的鱼是否被栈中的鱼吃掉了 + boolean hasEat = false; + // 如果栈中还有鱼,并且栈中鱼向右,当前的鱼向左游,那么就会有相遇的可能性 + while (!t.empty() && fishDirection[t.peek()] == right && curFishDirection == left) { + // 如果栈顶的鱼比较大,那么把新来的吃掉 + if (fishSize[t.peek()] > curFishSize) { + hasEat = true; + break; + } + // 如果栈中的鱼较小,那么会把栈中的鱼吃掉,栈中的鱼被消除,所以需要弹栈。 + t.pop(); + } + // 如果新来的鱼,没有被吃掉,那么压入栈中。 + if (!hasEat) { + t.push(i); + } + } + + return t.size(); +} +``` + + + +### 找出数组中右边比我小的元素 + +一个整数数组 A,找到每个元素:右边第一个比我小的下标位置,没有则用 -1 表示。输入:[5, 2],输出:[1, -1]。 + +```java +/** + * 找出数组中右边比我小的元素 + * + * @param A + * @return + */ +public static int[] findRightSmall(int[] A) { + // 结果数组 + int[] ans = new int[A.length]; + // 注意,栈中的元素记录的是下标 + Stack t = new Stack<>(); + for (int i = 0; i < A.length; i++) { + final int x = A[i]; + // 每个元素都向左遍历栈中的元素完成消除动作 + while (!t.empty() && A[t.peek()] > x) { + // 消除的时候,记录一下被谁消除了 + ans[t.peek()] = i; + // 消除时候,值更大的需要从栈中消失 + t.pop(); + } + // 剩下的入栈 + t.push(i); + } + // 栈中剩下的元素,由于没有人能消除他们,因此,只能将结果设置为-1。 + while (!t.empty()) { + ans[t.peek()] = -1; + t.pop(); + } + return ans; +} +``` + + + +### 字典序最小的 k 个数的子序列 + +给定一个正整数数组和 k,要求依次取出 k 个数,输出其中数组的一个子序列,需要满足:1. **长度为 k**;2.**字典序最小**。 + +输入:nums = [3,5,2,6], k = 2,输出:[2,6] + +**解释**:在所有可能的解:{[3,5], [3,2], [3,6], [5,2], [5,6], [2,6]} 中,[2,6] 字典序最小。所谓字典序就是,给定两个数组:x = [x1,x2,x3,x4],y = [y1,y2,y3,y4],如果 0 ≤ p < i,xp == yp 且 xi < yi,那么我们认为 x 的字典序小于 y。 + +```java +/** + * 字典序最小的 k 个数的子序列 + * + * @param nums + * @param k + * @return + */ +public int[] findSmallSeq(int[] nums, int k) { + int[] ans = new int[k]; + Stack s = new Stack(); + // 这里生成单调栈 + for (int i = 0; i < nums.length; i++) { + final int x = nums[i]; + final int left = nums.length - i; + // 注意我们想要提取出k个数,所以注意控制扔掉的数的个数 + while (!s.empty() && (s.size() + left > k) && s.peek() > x) { + s.pop(); + } + s.push(x); + } + // 如果递增栈里面的数太多,那么我们只需要取出前k个就可以了。 + // 多余的栈中的元素需要扔掉。 + while (s.size() > k) { + s.pop(); + } + // 把k个元素取出来,注意这里取的顺序! + for (int i = k - 1; i >= 0; i--) { + ans[i] = s.peek(); + s.pop(); + } + return ans; +} +``` \ No newline at end of file diff --git a/src/Algorithm/204.md b/src/Algorithm/204.md new file mode 100644 index 0000000..2feb9ea --- /dev/null +++ b/src/Algorithm/204.md @@ -0,0 +1,301 @@ +### 二叉树的层次遍历(两种方法) + +从上到下按层打印二叉树,同一层结点按从左到右的顺序打印,每一层打印到一行。输入: + +![二叉树的层次遍历](images/Algorithm/二叉树的层次遍历.png) + +输出:[[3], [9, 8], [6, 7]] + +```java +// 二叉树结点的定义 +public class TreeNode { + // 树结点中的元素值 + int val = 0; + // 二叉树结点的左子结点 + TreeNode left = null; + // 二叉树结点的右子结点 + TreeNode right = null; +} +``` + +**Queue表示FIFO队列解法**: + +![Queue表示FIFO队列解法](images/Algorithm/Queue表示FIFO队列解法.gif) + +```java +public List> levelOrder(TreeNode root) { + // 生成FIFO队列 + Queue Q = new LinkedList<>(); + // 如果结点不为空,那么加入FIFO队列 + if (root != null) { + Q.offer(root); + } + + // ans用于保存层次遍历的结果 + List> ans = new LinkedList<>(); + // 开始利用FIFO队列进行层次遍历 + while (Q.size() > 0) { + // 取出当前层里面元素的个数 + final int qSize = Q.size(); + // 当前层的结果存放于tmp链表中 + List tmp = new LinkedList<>(); + // 遍历当前层的每个结点 + for (int i = 0; i < qSize; i++) { + // 当前层前面的结点先出队 + TreeNode cur = Q.poll(); + // 把结果存放当于当前层中 + tmp.add(cur.val); + // 把下一层的结点入队,注意入队时需要非空才可以入队。 + if (cur.left != null) { + Q.offer(cur.left); + } + if (cur.right != null) { + Q.offer(cur.right); + } + } + + // 把当前层的结果放到返回值里面。 + ans.add(tmp); + } + + return ans; +} +``` + +**ArrayList表示FIFO队列解法**: + +![ArrayList表示FIFO队列解法](images/Algorithm/ArrayList表示FIFO队列解法.gif) + +```java +public List> levelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + // 初始化当前层结点 + List curLevel = new ArrayList<>(); + // 注意:需要root不空的时候才加到里面。 + if (root != null) { + curLevel.add(root); + } + while (curLevel.size() > 0) { + // 准备用来存放下一层的结点 + List nextLevel = new ArrayList<>(); + // 用来存放当前层的结果 + List curResult = new ArrayList<>(); + // 遍历当前层的每个结点 + for (TreeNode cur : curLevel) { + // 把当前层的值存放到当前结果里面 + curResult.add(cur.val); + // 生成下一层 + if (cur.left != null) { + nextLevel.add(cur.left); + } + if (cur.right != null) { + nextLevel.add(cur.right); + } + } + // 注意这里的更迭!滚动前进 + curLevel = nextLevel; + // 把当前层的值放到结果里面 + ans.add(curResult); + } + return ans; +} +``` + + + +### 循环队列 + +设计一个可以容纳 k 个元素的循环队列。需要实现以下接口: + +```java +public class MyCircularQueue { + // 参数k表示这个循环队列最多只能容纳k个元素 + public MyCircularQueue(int k){} + // 将value放到队列中, 成功返回true + public boolean enQueue(int value){return false;} + // 删除队首元素,成功返回true + public boolean deQueue(){return false;} + // 得到队首元素,如果为空,返回-1 + public int Front(){return 0;} + // 得到队尾元素,如果队列为空,返回-1 + public int Rear(){return 0;} + // 看一下循环队列是否为空 + public boolean isEmpty(){return false;} + // 看一下循环队列是否已放满k个元素 + public boolean isFull(){return false;} +} +``` + +循环队列的重点在于**循环使用固定空间**,难点在于**控制好 front/rear 两个首尾指示器**。 + +**方法一:k个元素空间** + +只使用 k 个元素的空间,三个变量 front, rear, used 来控制循环队列。标记 k = 6 时,循环队列的三种情况,如下图所示: + +![循环队列k个元素空间](images/Algorithm/循环队列k个元素空间.png) + +```java +public class MyCircularQueue { + // 已经使用的元素个数 + private int used = 0; + // 第一个元素所在位置 + private int front = 0; + // rear是enQueue可在存放的位置,注意开闭原则, [front, rear) + private int rear = 0; + // 循环队列最多可以存放的元素个数 + private int capacity = 0; + // 循环队列的存储空间 + private int[] a = null; + + public MyCircularQueue(int k) { + // 初始化循环队列 + capacity = k; + a = new int[capacity]; + } + + public boolean enQueue(int value) { + // 如果已经放满了 + if (isFull()) { + return false; + } + + // 如果没有放满,那么a[rear]用来存放新进来的元素 + a[rear] = value; + // rear注意取模 + rear = (rear + 1) % capacity; + // 已经使用的空间 + used++; + // 存放成功! + return true; + } + + public boolean deQueue() { + // 如果是一个空队列,当然不能出队 + if (isEmpty()) { + return false; + } + + // 第一个元素取出 + int ret = a[front]; + // 注意取模 + front = (front + 1) % capacity; + // 已经存放的元素减减 + used--; + // 取出元素成功 + return true; + } + + public int front() { + // 如果为空,不能取出队首元素 + if (isEmpty()) { + return -1; + } + + // 取出队首元素 + return a[front]; + } + + public int rear() { + // 如果为空,不能取出队尾元素 + if (isEmpty()) { + return -1; + } + + // 注意:这里不能使用rear - 1 + // 需要取模 + int tail = (rear - 1 + capacity) % capacity; + return a[tail]; + } + + // 队列是否为空 + public boolean isEmpty() { + return used == 0; + } + + // 队列是否满了 + public boolean isFull() { + return used == capacity; + } +} +``` + +**复杂度分析**:入队操作与出队操作都是 O(1)。 + +**方法二:k+1个元素空间** + +方法 1 利用 used 变量对满队列和空队列进行了区分。实际上,这种区分方式还有另外一种办法,使用 k+1 个元素的空间,两个变量 front, rear 来控制循环队列的使用。具体如下: + +- 在申请数组空间的时候,申请 k + 1 个空间 +- 在放满循环队列的时候,必须要保证 rear 与 front 之间有空隙 + +如下图(此时 k = 5)所示: + +![循环队列k+1个元素空间](images/Algorithm/循环队列k+1个元素空间.png) + +```java +public class MyCircularQueue { + // 队列的头部元素所在位置 + private int front = 0; + // 队列的尾巴,注意我们采用的是前开后闭原则, [front, rear) + private int rear = 0; + private int[] a = null; + private int capacity = 0; + + public MyCircularQueue(int k) { + // 初始化队列,注意此时队列中元素个数为 + // k + 1 + capacity = k + 1; + a = new int[k + 1]; + } + + public boolean enQueue(int value) { + // 如果已经满了,无法入队 + if (isFull()) { + return false; + } + // 把元素放到rear位置 + a[rear] = value; + // rear向后移动 + rear = (rear + 1) % capacity; + return true; + } + + public boolean deQueue() { + // 如果为空,无法出队 + if (isEmpty()) { + return false; + } + // 出队之后,front要向前移 + front = (front + 1) % capacity; + return true; + } + + public int front() { + // 如果能取出第一个元素,取a[front]; + return isEmpty() ? -1 : a[front]; + } + + public int rear() { + // 由于我们使用的是前开后闭原则 + // [front, rear) + // 所以在取最后一个元素时,应该是 + // (rear - 1 + capacity) % capacity; + int tail = (rear - 1 + capacity) % capacity; + return isEmpty() ? -1 : a[tail]; + } + + public boolean isEmpty() { + // 队列是否为空 + return front == rear; + } + + public boolean isFull() { + // rear与front之间至少有一个空格 + // 当rear指向这个最后的一个空格时, + // 队列就已经放满了! + return (rear + 1) % capacity == front; + } +} +``` + +**复杂度分析**:入队与出队操作都是 O(1)。 \ No newline at end of file diff --git a/src/Algorithm/205.md b/src/Algorithm/205.md new file mode 100644 index 0000000..d530154 --- /dev/null +++ b/src/Algorithm/205.md @@ -0,0 +1,108 @@ +单调队列属于**双端队列**的一种。双端队列与 FIFO 队列的区别在于: + +- FIFO 队列只能从尾部添加元素,首部弹出元素 +- 双端队列可以从首尾两端 push/pop 元素 + +### 滑动窗口的最大值 + +给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。输入:nums = [1,3,-1,-3,5,3], k = 3,输出:[3,3,5,5]。 + +![滑动窗口的最大值](images/Algorithm/滑动窗口的最大值.png) + +```java +public class Solution { + + // 单调队列使用双端队列来实现 + private ArrayDeque Q = new ArrayDeque<>(); + + // 入队的时候,last方向入队,但是入队的时候 + // 需要保证整个队列的数值是单调的 + // (在这个题里面我们需要是递减的) + // 并且需要注意,这里是Q.getLast() < val + // 如果写成Q.getLast() <= val就变成了严格单调递增 + private void push(int val) { + while (!Q.isEmpty() && Q.getLast() < val) { + Q.removeLast(); + } + // 将元素入队 + Q.addLast(val); + } + + // 出队的时候,要相等的时候才会出队 + private void pop(int val) { + if (!Q.isEmpty() && Q.getFirst() == val) { + Q.removeFirst(); + } + } + + public int[] maxSlidingWindow(int[] nums, int k) { + List ans = new ArrayList<>(); + for (int i = 0; i < nums.length; i++) { + push(nums[i]); + // 如果队列中的元素还少于k个 + // 那么这个时候,还不能去取最大值 + if (i < k - 1) { + continue; + } + // 队首元素就是最大值 + ans.add(Q.getFirst()); + // 尝试去移除元素 + pop(nums[i - k + 1]); + } + // 将ans转换成为数组返回! + return ans.stream().mapToInt(Integer::valueOf).toArray(); + } + +} +``` + +**复杂度分析**:每个元素都只入队一次,出队一次,每次入队与出队都是 O(1) 的复杂度,因此整个算法的复杂度为 O(n)。 + + + +### 捡金币游戏 + +给定一个数组 A[],每个位置 i 放置了金币 A[i],小明从 A[0] 出发。当小明走到 A[i] 的时候,下一步他可以选择 A[i+1, i+k](当然,不能超出数组边界)。每个位置一旦被选择,将会把那个位置的金币收走(如果为负数,就要交出金币)。请问,最多能收集多少金币? + +输入:[1,-1,-100,-1000,100,3], k = 2,输出:4。 + +**解释**:从 A[0] = 1 出发,收获金币 1。下一步走往 A[2] = -100, 收获金币 -100。再下一步走到 A[4] = 100,收获金币 100,最后走到 A[5] = 3,收获金币 3。最多收获 1 - 100 + 100 + 3 = 4。没有比这个更好的走法了。 + +```java +public class Solution { + public int maxResult(int[] A, int k) { + // 处理掉各种边界条件! + if (A == null || A.length == 0 || k <= 0) { + return 0; + } + final int N = A.length; + // 每个位置可以收集到的金币数目 + int[] get = new int[N]; + // 单调队列,这里并不是严格递减 + ArrayDeque Q = new ArrayDeque(); + for (int i = 0; i < N; i++) { + // 在取最大值之前,需要保证单调队列中都是有效值。 + // 也就是都在区间里面的值 + // 当要求get[i]的时候, + // 单调队列中应该是只能保存[i-k, i-1]这个范围 + if (i - k > 0) { + if (!Q.isEmpty() && Q.getFirst() == get[i - k - 1]) { + Q.removeFirst(); + } + } + // 从单调队列中取得较大值 + int old = Q.isEmpty() ? 0 : Q.getFirst(); + get[i] = old + A[i]; + // 入队的时候,采用单调队列入队 + while (!Q.isEmpty() && Q.getLast() < get[i]) { + Q.removeLast(); + } + Q.addLast(get[i]); + } + return get[N - 1]; + } + +} +``` + +**复杂度分析**:每个元素只入队一次,出队一次,每次入队与出队复杂度都是 O(n)。因此,时间复杂度为 O(n),空间复杂度为 O(n)。 \ No newline at end of file diff --git a/src/Algorithm/3.md b/src/Algorithm/3.md new file mode 100644 index 0000000..b3bb5a9 --- /dev/null +++ b/src/Algorithm/3.md @@ -0,0 +1,238 @@ +### 哈希表(Hash Table) + +In a hash table, a new index is processed using the keys. And, the element corresponding to that key is stored in the index. This process is called **hashing**. + +Let k be a key and h(x) be a hash function. Here, h(k) will give us a new index to store the element linked with k. + +![Hash-table-Representation](images/Algorithm/Hash-table-Representation.png) + + + + + +### 优先队列(Priority Queue) + +能保证每次取出的元素都是队列中优先级别最高的。优先级别可以是自定义的,例如,数据的数值越大,优先级越高;或者数据的数值越小,优先级越高。优先级别甚至可以通过各种复杂的计算得到。 + +**实现方式** +优先队列的本质是一个二叉堆结构。堆在英文里叫 Binary Heap,它是利用一个数组结构来实现的完全二叉树。换句话说,优先队列的本质是一个数组,数组里的每个元素既有可能是其他元素的父节点,也有可能是其他元素的子节点,而且,每个父节点只能有两个子节点,很像一棵二叉树的结构。 + +**优先队列的性质** + +- 数组里的第一个元素 array[0] 拥有最高的优先级别 +- 给定一个下标 i,那么对于元素 array[i] 而言: + - 它的父节点所对应的元素下标是 (i-1)/2 + - 它的左孩子所对应的元素下标是 2×i + 1 + - 它的右孩子所对应的元素下标是 2×i + 2 +- 数组里每个元素的优先级别都要高于它两个孩子的优先级别 + +**应用场景**:从一堆杂乱无章的数据当中按照一定的顺序(或者优先级)逐步地筛选出部分乃至全部的数据。 + + + +**基本操作** + +**① 向上筛选(sift up/bubble up)** + +- 当有新的数据加入到优先队列中,新的数据首先被放置在二叉堆的底部。 +- 不断进行向上筛选的操作,即如果发现该数据的优先级别比父节点的优先级别还要高,那么就和父节点的元素相互交换,再接着往上进行比较,直到无法再继续交换为止。 + +![优先队列-向上筛选](images/Algorithm/优先队列-向上筛选.gif) + +**时间复杂度**:由于二叉堆是一棵完全二叉树,并假设堆的大小为 k,因此整个过程其实就是沿着树的高度往上爬,所以只需要 O(logk) 的时间。 + +**② 向下筛选(sift down/bubble down)** + +- 当堆顶的元素被取出时,要更新堆顶的元素来作为下一次按照优先级顺序被取出的对象,需要将堆底部的元素放置到堆顶,然后不断地对它执行向下筛选的操作。 +- 将该元素和它的两个孩子节点对比优先级,如果优先级最高的是其中一个孩子,就将该元素和那个孩子进行交换,然后反复进行下去,直到无法继续交换为止。 + +![优先队列-向下筛选](images/Algorithm/优先队列-向下筛选.gif) + +**时间复杂度**:整个过程就是沿着树的高度往下爬,所以时间复杂度也是 O(logk)。 + + + +**案例一:[前 K 个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/)** + +给你一个整数数组 `nums` 和一个整数 `k` ,请你返回其中出现频率前 `k` 高的元素。你可以按 **任意顺序** 返回答案。 + +**最小堆解法**:题目最终需要返回的是前 k 个频率最大的元素,可以想到借助堆这种数据结构,对于 k 频率之后的元素不用再去处理,进一步优化时间复杂度。 + +![最小堆-前K个高频元素](images/Algorithm/最小堆-前K个高频元素.jpg) + +```java +public class Solution { + public List topKFrequent(int[] nums, int k) { + // 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值 + HashMap map = new HashMap(); + for(int num : nums){ + if (map.containsKey(num)) { + map.put(num, map.get(num) + 1); + } else { + map.put(num, 1); + } + } + + // 遍历map,用最小堆保存频率最大的k个元素 + PriorityQueue pq = new PriorityQueue<>(new Comparator() { + @Override + public int compare(Integer a, Integer b) { + return map.get(a) - map.get(b); + } + }); + for (Integer key : map.keySet()) { + if (pq.size() < k) { + pq.add(key); + } else if (map.get(key) > map.get(pq.peek())) { + pq.remove(); + pq.add(key); + } + } + + // 取出最小堆中的元素 + List res = new ArrayList<>(); + while (!pq.isEmpty()) { + res.add(pq.remove()); + } + return res; + } +} +``` + + + +### 图(Graph) + +**图(graph)**由多个**节点(vertex)**构成,节点之间阔以互相连接组成一个网络。(x, y)表示一条**边(edge)**,它表示节点 x 与 y 相连。边可能会有**权值(weight/cost)**。 + +![数据结构-graph](images/Algorithm/数据结构-graph.png) + + + +**常见图算法** + +- 图的遍历:深度优先、广度优先 +- 环的检测:有向图、无向图 +- 拓扑排序 +- 最短路径算法:Dijkstra、Bellman-Ford、Floyd Warshall +- 连通性相关算法:Kosaraju、Tarjan、求解孤岛的数量、判断是否为树 +- 图的着色、旅行商问题等 + + + +### 前缀树(Trie) + +假如有一个字典,字典里面有如下词:"A","to","tea","ted","ten","i","in","inn",每个单词还能有自己的一些权重值,那么用前缀树来构建这个字典将会是如下的样子: + +![前缀树](images/Algorithm/前缀树.png) + +**性质** + +- 每个节点至少包含两个基本属性 + + - children:数组或者集合,罗列出每个分支当中包含的所有字符 + - isEnd:布尔值,表示该节点是否为某字符串的结尾 + +- 前缀树的根节点是空的 + + 所谓空,即只利用到这个节点的 children 属性,即只关心在这个字典里,有哪些打头的字符 + +- 除了根节点,其他所有节点都有可能是单词的结尾,叶子节点一定都是单词的结尾 + + + +**实现** + +- 创建 + + - 遍历一遍输入的字符串,对每个字符串的字符进行遍历 + - 从前缀树的根节点开始,将每个字符加入到节点的 children 字符集当中 + - 如果字符集已经包含了这个字符,则跳过 + - 如果当前字符是字符串的最后一个,则把当前节点的 isEnd 标记为真。 + + 由上,创建的方法很直观。前缀树真正强大的地方在于,每个节点还能用来保存额外的信息,比如可以用来记录拥有相同前缀的所有字符串。因此,当用户输入某个前缀时,就能在 O(1) 的时间内给出对应的推荐字符串。 + +- 搜索 + + 与创建方法类似,从前缀树的根节点出发,逐个匹配输入的前缀字符,如果遇到了就继续往下一层搜索,如果没遇到,就立即返回。 + + + +### 线段树(Segment Tree) + +假设有一个数组 array[0 … n-1], 里面有 n 个元素,现在要经常对这个数组做两件事。 + +- 更新数组元素的数值 +- 求数组任意一段区间里元素的总和(或者平均值) + + + +**解法 1:遍历一遍数组** + +- 时间复杂度 O(n)。 + +**解法 2:线段树** + +- 线段树,就是一种按照二叉树的形式存储数据的结构,每个节点保存的都是数组里某一段的总和。 +- 适用于数据很多,而且需要频繁更新并求和的操作。 +- 时间复杂度 O(logn)。 + + + +**实现** +如数组是 [1, 3, 5, 7, 9, 11],那么它的线段树如下。 + +![线段树](images/Algorithm/线段树.png) + +根节点保存的是从下标 0 到下标 5 的所有元素的总和,即 36。左右两个子节点分别保存左右两半元素的总和。按照这样的逻辑不断地切分下去,最终的叶子节点保存的就是每个元素的数值。 + + + +### 树状数组(Fenwick Tree) + +树状数组(Fenwick Tree / Binary Indexed Tree)。 + +**举例**:假设有一个数组 array[0 … n-1], 里面有 n 个元素,现在要经常对这个数组做两件事。 + +- 更新数组元素的数值 +- 求数组前 k 个元素的总和(或者平均值) + + + +**解法 1:线段树** + +- 线段树能在 O(logn) 的时间里更新和求解前 k 个元素的总和 + +**解法 2:树状数** + +- 该问题只要求求解前 k 个元素的总和,并不要求任意一个区间 +- 树状数组可以在 O(logn) 的时间里完成上述的操作 +- 相对于线段树的实现,树状数组显得更简单 + + + +**树状数组特点** + +- 它是利用数组来表示多叉树的结构,在这一点上和优先队列有些类似,只不过,优先队列是用数组来表示完全二叉树,而树状数组是多叉树。 +- 树状数组的第一个元素是空节点。 +- 如果节点 tree[y] 是 tree[x] 的父节点,那么需要满足条件:y = x - (x & (-x))。 + + + +### 散列表(Hash) + +散列表又称为**哈希表**,是将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代表。哈希可以用来实现各种数据结构,其中最常用的就是**哈希表(hash table)**。哈希表通常由数组实现。 + +![数据结构-hash_table](images/Algorithm/数据结构-hash_table.png) + + + +### 二叉堆 + +**最大堆**:任何一个父节点的值,都 **大于** 或 **等于** 它左、右子节点的值,**堆顶** 是整个堆中的 **最大** 元素。 + +![Max-heap](images/Algorithm/Max-heap.png) + +**最小堆**:任何一个父节点的值,都 **小于** 或 **等于** 它左、右孩子节点的值,**堆顶** 是整个堆中的 **最小** 元素。 + +![Min-heap](images/Algorithm/Min-heap.png) \ No newline at end of file diff --git a/src/Algorithm/301.md b/src/Algorithm/301.md new file mode 100644 index 0000000..852d98a --- /dev/null +++ b/src/Algorithm/301.md @@ -0,0 +1,96 @@ +建造类设计模式提供了对创建对象的基本定义和约束条件,以寻求最佳的实例化Java对象解决方案。 + +### 工厂模式(Factory) + +**概念** + +定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的**实例化延迟**到其子类。 + +**使用场景** + +jdbc 连接数据库,硬件访问,降低对象的产生和销毁。 + + + +实现工厂模式需要满足三个条件: + +- 超类(super class):超类是一个抽象类 +- 子类(sub class):子类需继承超类 +- 工厂类(factory class):工厂类根据输入参数实例化子类 + +![工厂模式](images/Algorithm/工厂模式.png) + + + +### 抽象工厂模式(Abstract Factory) + +**概念** + +为创建一组相关或相互依赖的对象提供一个接口,而且**无须指定它们的具体类。** + + + +**使用场景** + +一个对象族(或是一组没有任何关系的对象)都有相同的约束。 + +涉及不同操作系统的时候,都可以考虑使用抽象工厂模式。 + + + +### 单例模式(Singleton) + +**概念** + +确保某一个类只有一个实例,而且**自行实例化**并向整个系统提供这个实例。 + + + +**使用场景** + +- 要求生成唯一序列号的环境 +- 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的 +- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源 +- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式) + + + +![单例模式](images/Algorithm/单例模式.png) + + + +### 建造者模式(Builder) + +**概念** + +将一个复杂对象的构建与它的表示分离,使得**同样的构建过程可以创建不同的表示。** + + + +**使用场景** + +- 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式 +- 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式 +- 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适 + + + +![建造者模式](images/Algorithm/建造者模式.png) + + + +### 原型模式(Prototype) + +**概念** + +用原型实例指定创建对象的种类,并且**通过拷贝这些原型**创建新的对象。 + + + +**使用场景** + +- **资源优化场景:**类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等 + +- **性能和安全要求的场景:**通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式 + +- **一个对象多个修改者的场景:**一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以、考虑使用原型模式拷贝多个对象供调用者使用 \ No newline at end of file diff --git a/src/Algorithm/302.md b/src/Algorithm/302.md new file mode 100644 index 0000000..24b26d8 --- /dev/null +++ b/src/Algorithm/302.md @@ -0,0 +1,135 @@ +结构类设计模式主要解决如何通过多个小对象组合出一个大对象的问题,如使用继承和接口实现将多个类组合在一起。 + +### 适配器模式(Adapter) + +**概念** + +将一个类的接口**变换成客户端所期待的另一种接口,**从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 + + + +**使用场景** + +你有动机修改一个已经投产中的接口时,适配器模式可能是最适合你的模式。比如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接口,怎么办?详细设计阶段不要考虑使用适配器模式,使用主要场景为扩展应用中。 + + + +![适配器模式-默认适配器](images/Algorithm/适配器模式-默认适配器.png) + +![适配器模式-对象适配器](images/Algorithm/适配器模式-对象适配器.png) + +![适配器模式-类继承](images/Algorithm/适配器模式-类继承.png) + + + +### 组合模式(Composite) + +**概念** + +将对象组合成树形结构以表示**“部分-整体”**的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 + + + +**使用场景** + +- 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理 +- 从一个整体中能够独立出部分模块或功能的场景 +- 只要是树形结构,就考虑使用组合模式 + + + +**案例**:每个员工都有姓名、部门、薪水这些属性,同时还有下属员工集合(虽然可能集合为空),而下属员工和自己的结构是一样的,也有姓名、部门这些属性,同时也有他们的下属员工集合。 + +![组合模式](images/Algorithm/组合模式.png) + + + +### 代理模式(Proxy) + +**概念** + +为其他对象**提供一种代理以控制**对这个对象的访问。 + + + +![代理模式](images/Algorithm/代理模式.png) + + + +### 享元模式(Flywight) + +**概念** + +使用共享对象可有效地**支持大量的细粒度的对象。** + +对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。 + +- **内部状态:**内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变 + +- **外部状态:**外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态 + + + +**使用场景** + +- 系统中存在大量的相似对象 +- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份 +- 需要缓冲池的场景 + + + +### 门面模式(Facade) + +**概念** + +要求一个子系统的外部与其内部的通信必须**通过一个统一的对象进行。**门面模式提供一个高层次的接口,使得子系统更易于使用。 + + + +**使用场景** + +- 为一个复杂的模块或子系统提供一个供外界访问的接口 +- 子系统相对独立——外界对子系统的访问只要黑箱操作即可 +- 预防低水平人员带来的风险扩散 + + + +![门面模式](images/Algorithm/门面模式.png) + + + +### 桥接模式(Bridge) + +**概念** + +将**抽象和实现解耦,**使得两者可以独立地变化。 + + + +**使用场景** + +- 不希望或不适用使用继承的场景 +- 接口或抽象类不稳定的场景 +- 重用性要求较高的场景 + + + +![桥梁模式-1](images/Algorithm/桥梁模式-1.png) + +![桥梁模式-2](images/Algorithm/桥梁模式-2.png) + + + +### 装饰器模式(Decorator) + +**概念** + +动态地给一个对象**添加一些额外的职责。**就增加功能来说,装饰模式相比生成子类更为灵活。 + + + +**使用场景** + +- 需要扩展一个类的功能,或给一个类增加附加功能 +- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销 +- 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式 \ No newline at end of file diff --git a/src/Algorithm/303.md b/src/Algorithm/303.md new file mode 100644 index 0000000..ec43971 --- /dev/null +++ b/src/Algorithm/303.md @@ -0,0 +1,180 @@ +行为类设计模式主要用于定义和描述对象之间的交互规则和职责边界,为对象之间更好的交互提供解决方案。 + +### 模板方法模式(Template Method) + +**概念** + +定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以**不改变一个算法的结构**即可重定义该算法的某些特定步骤。 + + + +**使用场景** + +- 多个子类有公有的方法,并且逻辑基本相同时 +- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现 +- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为 + + + + + +![模板方法模式](images/Algorithm/模板方法模式.png) + + + +### 中介者模式(Mediator) + +**概念** + +用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其**耦合松散,**而且可以独立地改变它们之间的交互。 + + + +**使用场景** + +中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中出现了蜘蛛网状结构,即每个类都与其他的类有直接的联系。 + + + +### 责任链模式(Chain) + +**概念** + +使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象**连成一条链,**并沿着这条链传递该请求,直到有对象处理它为止。 + + + +![责任链模式](images/Algorithm/责任链模式.png) + + + +### 观察者模式(Observer) + +**概念** + +定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会**得到通知并被自动更新。** + + + +**使用场景** + +- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系 +- 事件多级触发场景 +- 跨系统的消息交换场景,如消息队列的处理机制 + + + +![观察者模式](images/Algorithm/观察者模式.png) + + + +### 策略模式(Strategy) + +**概念** + +定义一组算法,将**每个算法都封装起来,**并且使它们之间可以互换。 + + + +**使用场景** + +- 多个类只有在算法或行为上稍有不同的场景 +- 算法需要自由切换的场景 +- 需要屏蔽算法规则的场景 +- 具体策略数量超过 4 个,则需要考虑使用混合模式 + + + +![策略模式](images/Algorithm/策略模式.png) + + + +### 命令模式(Command) + +**概念** + +将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对**请求排队或者记录请求日志,**可以提供命令的撤销和恢复功能。 + + + +**使用场景** + +认为是命令的地方就可以采用命令模式,例如,在 GUI 开发中,一个按钮的点击是一个命令,可以采用命令模式;模拟 DOS 命令的时候,当然也要采用命令模式;触发-反馈机制的处理等。 + + + +![命令模式](images/Algorithm/命令模式.png) + + + +### 状态模式(State) + +**概念** + +当一个对象**内在状态改变时允许其改变行为,**这个对象看起来像改变了其类。 + + + +**使用场景** + +- 行为随状态改变而改变的场景,这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式 +- 条件、分支判断语句的替代者 + + + +![状态模式](images/Algorithm/状态模式.png) + + + +### 访问者模式(Visitor) + +**概念** + +封装一些**作用于某种数据结构中的各元素的操作,**它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。 + + + +**使用场景** + +- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景 +- 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类 + + + +### 解释器模式(Interpreter) + +**概念** + +给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器**使用该表示来解释语言中的句子。** + + + +**使用场景** + +- 重复发生的问题可以使用解释器模式 +- 一个简单语法需要解释的场景 + + + +### 迭代器模式(Iterator) + +**概念** + +它提供一种方法**访问一个容器对象中各个元素,**而又不需暴露该对象的内部细节。 + + + +### 备忘录模式(Memento) + +**概念** + +在不破坏封装性的前提下,**捕获一个对象的内部状态,**并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。 + + + +**使用场景** + +- 需要保存和恢复数据的相关状态场景 +- 提供一个可回滚(rollback)的操作 +- 需要监控的副本场景中 +- 数据库连接的事务管理就是用的备忘录模式 \ No newline at end of file diff --git a/src/Algorithm/README.md b/src/Algorithm/README.md new file mode 100644 index 0000000..aa99054 --- /dev/null +++ b/src/Algorithm/README.md @@ -0,0 +1,5 @@ +

Algorithm
+ +Introduction:收纳技术相关的 `Data Structure`、`Algorithm`、`Design Pattern`等总结! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/Algorithm/_sidebar.md b/src/Algorithm/_sidebar.md new file mode 100644 index 0000000..dfeea30 --- /dev/null +++ b/src/Algorithm/_sidebar.md @@ -0,0 +1,21 @@ +* 🏁Data Structure + * [✍位运算](src/Algorithm/1 "位运算") + * [✍常用数据结构](src/Algorithm/2 "常用数据结构") + * [✍高级数据结构](src/Algorithm/3 "高级数据结构") +* 🏁Algorithm + * [✍复杂度](src/Algorithm/101 "复杂度") + * [✍算法思想](src/Algorithm/102 "算法思想") + * [✍算法模板](src/Algorithm/103 "算法模板") + * [✍查找算法](src/Algorithm/104 "查找算法") + * [✍搜素算法](src/Algorithm/105 "搜素算法") + * [✍排序算法](src/Algorithm/106 "排序算法") +* 🏁Case + * [✍双指针](src/Algorithm/201 "双指针") + * [✍反转](src/Algorithm/202 "反转") + * [✍栈](src/Algorithm/203 "栈") + * [✍FIFO队列](src/Algorithm/204 "FIFO队列") + * [✍单调队列](src/Algorithm/205 "单调队列") +* 🏁Design Pattern + * [✍创建型模式](src/Algorithm/301 "创建型模式") + * [✍结构型模式](src/Algorithm/302 "结构型模式") + * [✍行为型模式](src/Algorithm/303 "进程和线程") \ No newline at end of file diff --git a/src/Algorithm/images/Algorithm/1117043-20170407105053816-427306966.png b/src/Algorithm/images/Algorithm/1117043-20170407105053816-427306966.png new file mode 100644 index 0000000..916946d Binary files /dev/null and b/src/Algorithm/images/Algorithm/1117043-20170407105053816-427306966.png differ diff --git a/src/Algorithm/images/Algorithm/1117043-20170407105111300-518814658.png b/src/Algorithm/images/Algorithm/1117043-20170407105111300-518814658.png new file mode 100644 index 0000000..22c3859 Binary files /dev/null and b/src/Algorithm/images/Algorithm/1117043-20170407105111300-518814658.png differ diff --git a/src/Algorithm/images/Algorithm/AVLTree.png b/src/Algorithm/images/Algorithm/AVLTree.png new file mode 100644 index 0000000..da0b84f Binary files /dev/null and b/src/Algorithm/images/Algorithm/AVLTree.png differ diff --git a/src/Algorithm/images/Algorithm/ArrayList表示FIFO队列解法.gif b/src/Algorithm/images/Algorithm/ArrayList表示FIFO队列解法.gif new file mode 100644 index 0000000..ba1f8c4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/ArrayList表示FIFO队列解法.gif differ diff --git a/src/Algorithm/images/Algorithm/B+Tree.png b/src/Algorithm/images/Algorithm/B+Tree.png new file mode 100644 index 0000000..c5e8961 Binary files /dev/null and b/src/Algorithm/images/Algorithm/B+Tree.png differ diff --git a/src/Algorithm/images/Algorithm/B+tree图解.png b/src/Algorithm/images/Algorithm/B+tree图解.png new file mode 100644 index 0000000..7d7cc4e Binary files /dev/null and b/src/Algorithm/images/Algorithm/B+tree图解.png differ diff --git a/src/Algorithm/images/Algorithm/B-tree图解.png b/src/Algorithm/images/Algorithm/B-tree图解.png new file mode 100644 index 0000000..4df54d4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/B-tree图解.png differ diff --git a/src/Algorithm/images/Algorithm/BalanceTree.png b/src/Algorithm/images/Algorithm/BalanceTree.png new file mode 100644 index 0000000..3997c6e Binary files /dev/null and b/src/Algorithm/images/Algorithm/BalanceTree.png differ diff --git a/src/Algorithm/images/Algorithm/BinarySearchTree.png b/src/Algorithm/images/Algorithm/BinarySearchTree.png new file mode 100644 index 0000000..1d5f7e2 Binary files /dev/null and b/src/Algorithm/images/Algorithm/BinarySearchTree.png differ diff --git a/src/Algorithm/images/Algorithm/BinaryTree.png b/src/Algorithm/images/Algorithm/BinaryTree.png new file mode 100644 index 0000000..39a4856 Binary files /dev/null and b/src/Algorithm/images/Algorithm/BinaryTree.png differ diff --git a/src/Algorithm/images/Algorithm/BucketSort.gif b/src/Algorithm/images/Algorithm/BucketSort.gif new file mode 100644 index 0000000..c4770a0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/BucketSort.gif differ diff --git a/src/Algorithm/images/Algorithm/Bx树.jpg b/src/Algorithm/images/Algorithm/Bx树.jpg new file mode 100644 index 0000000..755b316 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Bx树.jpg differ diff --git a/src/Algorithm/images/Algorithm/Deque滑动窗口最大值.gif b/src/Algorithm/images/Algorithm/Deque滑动窗口最大值.gif new file mode 100644 index 0000000..a263ad7 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Deque滑动窗口最大值.gif differ diff --git a/src/Algorithm/images/Algorithm/FIFO-Representation-of-Queue.png b/src/Algorithm/images/Algorithm/FIFO-Representation-of-Queue.png new file mode 100644 index 0000000..1283e9b Binary files /dev/null and b/src/Algorithm/images/Algorithm/FIFO-Representation-of-Queue.png differ diff --git a/src/Algorithm/images/Algorithm/Hash-table-Representation.png b/src/Algorithm/images/Algorithm/Hash-table-Representation.png new file mode 100644 index 0000000..c2f877a Binary files /dev/null and b/src/Algorithm/images/Algorithm/Hash-table-Representation.png differ diff --git a/src/Algorithm/images/Algorithm/Max-heap.png b/src/Algorithm/images/Algorithm/Max-heap.png new file mode 100644 index 0000000..5792a82 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Max-heap.png differ diff --git a/src/Algorithm/images/Algorithm/Min-heap.png b/src/Algorithm/images/Algorithm/Min-heap.png new file mode 100644 index 0000000..e1865a0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Min-heap.png differ diff --git a/src/Algorithm/images/Algorithm/Queue表示FIFO队列解法.gif b/src/Algorithm/images/Algorithm/Queue表示FIFO队列解法.gif new file mode 100644 index 0000000..5c63d0a Binary files /dev/null and b/src/Algorithm/images/Algorithm/Queue表示FIFO队列解法.gif differ diff --git a/src/Algorithm/images/Algorithm/Red-BlackTree.jpg b/src/Algorithm/images/Algorithm/Red-BlackTree.jpg new file mode 100644 index 0000000..01ebfbb Binary files /dev/null and b/src/Algorithm/images/Algorithm/Red-BlackTree.jpg differ diff --git a/src/Algorithm/images/Algorithm/SortAlgorithm.png b/src/Algorithm/images/Algorithm/SortAlgorithm.png new file mode 100644 index 0000000..a3ccb3f Binary files /dev/null and b/src/Algorithm/images/Algorithm/SortAlgorithm.png differ diff --git a/src/Algorithm/images/Algorithm/Stack-Push-and-Pop-Operations.png b/src/Algorithm/images/Algorithm/Stack-Push-and-Pop-Operations.png new file mode 100644 index 0000000..cf8f0e4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Stack-Push-and-Pop-Operations.png differ diff --git a/src/Algorithm/images/Algorithm/Stack.png b/src/Algorithm/images/Algorithm/Stack.png new file mode 100644 index 0000000..c068927 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Stack.png differ diff --git a/src/Algorithm/images/Algorithm/Stack判断字符串是否有效.gif b/src/Algorithm/images/Algorithm/Stack判断字符串是否有效.gif new file mode 100644 index 0000000..303b592 Binary files /dev/null and b/src/Algorithm/images/Algorithm/Stack判断字符串是否有效.gif differ diff --git a/src/Algorithm/images/Algorithm/Stack每日温度.gif b/src/Algorithm/images/Algorithm/Stack每日温度.gif new file mode 100644 index 0000000..db2001b Binary files /dev/null and b/src/Algorithm/images/Algorithm/Stack每日温度.gif differ diff --git a/src/Algorithm/images/Algorithm/leftBraceNumber加减.gif b/src/Algorithm/images/Algorithm/leftBraceNumber加减.gif new file mode 100644 index 0000000..26cafd0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/leftBraceNumber加减.gif differ diff --git a/src/Algorithm/images/Algorithm/中序遍历.gif b/src/Algorithm/images/Algorithm/中序遍历.gif new file mode 100644 index 0000000..89358b3 Binary files /dev/null and b/src/Algorithm/images/Algorithm/中序遍历.gif differ diff --git a/src/Algorithm/images/Algorithm/二分查找.gif b/src/Algorithm/images/Algorithm/二分查找.gif new file mode 100644 index 0000000..f1c00aa Binary files /dev/null and b/src/Algorithm/images/Algorithm/二分查找.gif differ diff --git a/src/Algorithm/images/Algorithm/二分查找mid.png b/src/Algorithm/images/Algorithm/二分查找mid.png new file mode 100644 index 0000000..cef8668 Binary files /dev/null and b/src/Algorithm/images/Algorithm/二分查找mid.png differ diff --git a/src/Algorithm/images/Algorithm/二叉树的层次遍历.png b/src/Algorithm/images/Algorithm/二叉树的层次遍历.png new file mode 100644 index 0000000..f88514a Binary files /dev/null and b/src/Algorithm/images/Algorithm/二叉树的层次遍历.png differ diff --git a/src/Algorithm/images/Algorithm/代理模式.png b/src/Algorithm/images/Algorithm/代理模式.png new file mode 100644 index 0000000..64dc72d Binary files /dev/null and b/src/Algorithm/images/Algorithm/代理模式.png differ diff --git a/src/Algorithm/images/Algorithm/优先队列-向上筛选.gif b/src/Algorithm/images/Algorithm/优先队列-向上筛选.gif new file mode 100644 index 0000000..3ac76ed Binary files /dev/null and b/src/Algorithm/images/Algorithm/优先队列-向上筛选.gif differ diff --git a/src/Algorithm/images/Algorithm/优先队列-向下筛选.gif b/src/Algorithm/images/Algorithm/优先队列-向下筛选.gif new file mode 100644 index 0000000..a066f5f Binary files /dev/null and b/src/Algorithm/images/Algorithm/优先队列-向下筛选.gif differ diff --git a/src/Algorithm/images/Algorithm/冒泡排序.gif b/src/Algorithm/images/Algorithm/冒泡排序.gif new file mode 100644 index 0000000..9dd0ed4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/冒泡排序.gif differ diff --git a/src/Algorithm/images/Algorithm/冒泡排序.jpg b/src/Algorithm/images/Algorithm/冒泡排序.jpg new file mode 100644 index 0000000..7f47a26 Binary files /dev/null and b/src/Algorithm/images/Algorithm/冒泡排序.jpg differ diff --git a/src/Algorithm/images/Algorithm/判断字符串括号是否合法.gif b/src/Algorithm/images/Algorithm/判断字符串括号是否合法.gif new file mode 100644 index 0000000..9d822d8 Binary files /dev/null and b/src/Algorithm/images/Algorithm/判断字符串括号是否合法.gif differ diff --git a/src/Algorithm/images/Algorithm/前序遍历.gif b/src/Algorithm/images/Algorithm/前序遍历.gif new file mode 100644 index 0000000..a4ccb18 Binary files /dev/null and b/src/Algorithm/images/Algorithm/前序遍历.gif differ diff --git a/src/Algorithm/images/Algorithm/前缀树.png b/src/Algorithm/images/Algorithm/前缀树.png new file mode 100644 index 0000000..8992ab3 Binary files /dev/null and b/src/Algorithm/images/Algorithm/前缀树.png differ diff --git a/src/Algorithm/images/Algorithm/单例模式.png b/src/Algorithm/images/Algorithm/单例模式.png new file mode 100644 index 0000000..c26872c Binary files /dev/null and b/src/Algorithm/images/Algorithm/单例模式.png differ diff --git a/src/Algorithm/images/Algorithm/单向链表.png b/src/Algorithm/images/Algorithm/单向链表.png new file mode 100644 index 0000000..a6576ef Binary files /dev/null and b/src/Algorithm/images/Algorithm/单向链表.png differ diff --git a/src/Algorithm/images/Algorithm/单循环链表.png b/src/Algorithm/images/Algorithm/单循环链表.png new file mode 100644 index 0000000..3a7d5ce Binary files /dev/null and b/src/Algorithm/images/Algorithm/单循环链表.png differ diff --git a/src/Algorithm/images/Algorithm/双向链表.png b/src/Algorithm/images/Algorithm/双向链表.png new file mode 100644 index 0000000..15ee411 Binary files /dev/null and b/src/Algorithm/images/Algorithm/双向链表.png differ diff --git a/src/Algorithm/images/Algorithm/双循环链表.png b/src/Algorithm/images/Algorithm/双循环链表.png new file mode 100644 index 0000000..b19b8a5 Binary files /dev/null and b/src/Algorithm/images/Algorithm/双循环链表.png differ diff --git a/src/Algorithm/images/Algorithm/右右节点旋转.png b/src/Algorithm/images/Algorithm/右右节点旋转.png new file mode 100644 index 0000000..85dda7b Binary files /dev/null and b/src/Algorithm/images/Algorithm/右右节点旋转.png differ diff --git a/src/Algorithm/images/Algorithm/右左节点旋转.png b/src/Algorithm/images/Algorithm/右左节点旋转.png new file mode 100644 index 0000000..8c22043 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右左节点旋转.png differ diff --git a/src/Algorithm/images/Algorithm/右左节点旋转步骤.jpeg b/src/Algorithm/images/Algorithm/右左节点旋转步骤.jpeg new file mode 100644 index 0000000..380e8c8 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右左节点旋转步骤.jpeg differ diff --git a/src/Algorithm/images/Algorithm/右旋.jpg b/src/Algorithm/images/Algorithm/右旋.jpg new file mode 100644 index 0000000..0ebdfb5 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右旋.jpg differ diff --git a/src/Algorithm/images/Algorithm/右旋条件情况1.png b/src/Algorithm/images/Algorithm/右旋条件情况1.png new file mode 100644 index 0000000..046f725 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右旋条件情况1.png differ diff --git a/src/Algorithm/images/Algorithm/右旋条件情况1流程.gif b/src/Algorithm/images/Algorithm/右旋条件情况1流程.gif new file mode 100644 index 0000000..a93aa19 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右旋条件情况1流程.gif differ diff --git a/src/Algorithm/images/Algorithm/右旋条件情况2.png b/src/Algorithm/images/Algorithm/右旋条件情况2.png new file mode 100644 index 0000000..00751f4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/右旋条件情况2.png differ diff --git a/src/Algorithm/images/Algorithm/右旋条件情况2流程.gif b/src/Algorithm/images/Algorithm/右旋条件情况2流程.gif new file mode 100644 index 0000000..1280c0d Binary files /dev/null and b/src/Algorithm/images/Algorithm/右旋条件情况2流程.gif differ diff --git a/src/Algorithm/images/Algorithm/后序遍历.gif b/src/Algorithm/images/Algorithm/后序遍历.gif new file mode 100644 index 0000000..6bbf2b4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/后序遍历.gif differ diff --git a/src/Algorithm/images/Algorithm/命令模式.png b/src/Algorithm/images/Algorithm/命令模式.png new file mode 100644 index 0000000..ea80a6e Binary files /dev/null and b/src/Algorithm/images/Algorithm/命令模式.png differ diff --git a/src/Algorithm/images/Algorithm/基数排序.gif b/src/Algorithm/images/Algorithm/基数排序.gif new file mode 100644 index 0000000..2a55695 Binary files /dev/null and b/src/Algorithm/images/Algorithm/基数排序.gif differ diff --git a/src/Algorithm/images/Algorithm/堆排序.gif b/src/Algorithm/images/Algorithm/堆排序.gif new file mode 100644 index 0000000..783010a Binary files /dev/null and b/src/Algorithm/images/Algorithm/堆排序.gif differ diff --git a/src/Algorithm/images/Algorithm/大根堆-小根堆.jpg b/src/Algorithm/images/Algorithm/大根堆-小根堆.jpg new file mode 100644 index 0000000..fba1133 Binary files /dev/null and b/src/Algorithm/images/Algorithm/大根堆-小根堆.jpg differ diff --git a/src/Algorithm/images/Algorithm/大鱼吃小鱼.gif b/src/Algorithm/images/Algorithm/大鱼吃小鱼.gif new file mode 100644 index 0000000..2ba5ddd Binary files /dev/null and b/src/Algorithm/images/Algorithm/大鱼吃小鱼.gif differ diff --git a/src/Algorithm/images/Algorithm/完全二叉树.png b/src/Algorithm/images/Algorithm/完全二叉树.png new file mode 100644 index 0000000..2dd64d7 Binary files /dev/null and b/src/Algorithm/images/Algorithm/完全二叉树.png differ diff --git a/src/Algorithm/images/Algorithm/工厂模式.png b/src/Algorithm/images/Algorithm/工厂模式.png new file mode 100644 index 0000000..09964d1 Binary files /dev/null and b/src/Algorithm/images/Algorithm/工厂模式.png differ diff --git a/src/Algorithm/images/Algorithm/左右节点旋转.png b/src/Algorithm/images/Algorithm/左右节点旋转.png new file mode 100644 index 0000000..c67523d Binary files /dev/null and b/src/Algorithm/images/Algorithm/左右节点旋转.png differ diff --git a/src/Algorithm/images/Algorithm/左左节点旋转.png b/src/Algorithm/images/Algorithm/左左节点旋转.png new file mode 100644 index 0000000..e9f29c8 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左左节点旋转.png differ diff --git a/src/Algorithm/images/Algorithm/左左节点旋转步骤.jpeg b/src/Algorithm/images/Algorithm/左左节点旋转步骤.jpeg new file mode 100644 index 0000000..9296510 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左左节点旋转步骤.jpeg differ diff --git a/src/Algorithm/images/Algorithm/左旋.jpg b/src/Algorithm/images/Algorithm/左旋.jpg new file mode 100644 index 0000000..f10977a Binary files /dev/null and b/src/Algorithm/images/Algorithm/左旋.jpg differ diff --git a/src/Algorithm/images/Algorithm/左旋条件情况1.png b/src/Algorithm/images/Algorithm/左旋条件情况1.png new file mode 100644 index 0000000..646f1a1 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左旋条件情况1.png differ diff --git a/src/Algorithm/images/Algorithm/左旋条件情况1流程.gif b/src/Algorithm/images/Algorithm/左旋条件情况1流程.gif new file mode 100644 index 0000000..6b58833 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左旋条件情况1流程.gif differ diff --git a/src/Algorithm/images/Algorithm/左旋条件情况2.png b/src/Algorithm/images/Algorithm/左旋条件情况2.png new file mode 100644 index 0000000..f69b1d0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左旋条件情况2.png differ diff --git a/src/Algorithm/images/Algorithm/左旋条件情况2流程.gif b/src/Algorithm/images/Algorithm/左旋条件情况2流程.gif new file mode 100644 index 0000000..fd9cc62 Binary files /dev/null and b/src/Algorithm/images/Algorithm/左旋条件情况2流程.gif differ diff --git a/src/Algorithm/images/Algorithm/希尔排序.gif b/src/Algorithm/images/Algorithm/希尔排序.gif new file mode 100644 index 0000000..bbb3bea Binary files /dev/null and b/src/Algorithm/images/Algorithm/希尔排序.gif differ diff --git a/src/Algorithm/images/Algorithm/广度优先搜索.jpg b/src/Algorithm/images/Algorithm/广度优先搜索.jpg new file mode 100644 index 0000000..39f7c2c Binary files /dev/null and b/src/Algorithm/images/Algorithm/广度优先搜索.jpg differ diff --git a/src/Algorithm/images/Algorithm/建造者模式.png b/src/Algorithm/images/Algorithm/建造者模式.png new file mode 100644 index 0000000..093abd6 Binary files /dev/null and b/src/Algorithm/images/Algorithm/建造者模式.png differ diff --git a/src/Algorithm/images/Algorithm/归并排序.gif b/src/Algorithm/images/Algorithm/归并排序.gif new file mode 100644 index 0000000..a29ca19 Binary files /dev/null and b/src/Algorithm/images/Algorithm/归并排序.gif differ diff --git a/src/Algorithm/images/Algorithm/归并排序.jpg b/src/Algorithm/images/Algorithm/归并排序.jpg new file mode 100644 index 0000000..8787d2c Binary files /dev/null and b/src/Algorithm/images/Algorithm/归并排序.jpg differ diff --git a/src/Algorithm/images/Algorithm/循环队列k+1个元素空间.png b/src/Algorithm/images/Algorithm/循环队列k+1个元素空间.png new file mode 100644 index 0000000..a8d9973 Binary files /dev/null and b/src/Algorithm/images/Algorithm/循环队列k+1个元素空间.png differ diff --git a/src/Algorithm/images/Algorithm/循环队列k个元素空间.png b/src/Algorithm/images/Algorithm/循环队列k个元素空间.png new file mode 100644 index 0000000..a8a8c8f Binary files /dev/null and b/src/Algorithm/images/Algorithm/循环队列k个元素空间.png differ diff --git a/src/Algorithm/images/Algorithm/快速排序.gif b/src/Algorithm/images/Algorithm/快速排序.gif new file mode 100644 index 0000000..ad88d35 Binary files /dev/null and b/src/Algorithm/images/Algorithm/快速排序.gif differ diff --git a/src/Algorithm/images/Algorithm/排序算法.png b/src/Algorithm/images/Algorithm/排序算法.png new file mode 100644 index 0000000..d09b5c0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/排序算法.png differ diff --git a/src/Algorithm/images/Algorithm/插值查找mid.png b/src/Algorithm/images/Algorithm/插值查找mid.png new file mode 100644 index 0000000..a21efd4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/插值查找mid.png differ diff --git a/src/Algorithm/images/Algorithm/插入排序.gif b/src/Algorithm/images/Algorithm/插入排序.gif new file mode 100644 index 0000000..2702b14 Binary files /dev/null and b/src/Algorithm/images/Algorithm/插入排序.gif differ diff --git a/src/Algorithm/images/Algorithm/插入排序.jpg b/src/Algorithm/images/Algorithm/插入排序.jpg new file mode 100644 index 0000000..d2df145 Binary files /dev/null and b/src/Algorithm/images/Algorithm/插入排序.jpg differ diff --git a/src/Algorithm/images/Algorithm/数据结构-array.png b/src/Algorithm/images/Algorithm/数据结构-array.png new file mode 100644 index 0000000..6a3b098 Binary files /dev/null and b/src/Algorithm/images/Algorithm/数据结构-array.png differ diff --git a/src/Algorithm/images/Algorithm/数据结构-graph.png b/src/Algorithm/images/Algorithm/数据结构-graph.png new file mode 100644 index 0000000..020f989 Binary files /dev/null and b/src/Algorithm/images/Algorithm/数据结构-graph.png differ diff --git a/src/Algorithm/images/Algorithm/数据结构-hash_table.png b/src/Algorithm/images/Algorithm/数据结构-hash_table.png new file mode 100644 index 0000000..4d03ac9 Binary files /dev/null and b/src/Algorithm/images/Algorithm/数据结构-hash_table.png differ diff --git a/src/Algorithm/images/Algorithm/数据结构-queue.png b/src/Algorithm/images/Algorithm/数据结构-queue.png new file mode 100644 index 0000000..bb9ca34 Binary files /dev/null and b/src/Algorithm/images/Algorithm/数据结构-queue.png differ diff --git a/src/Algorithm/images/Algorithm/数据结构-stack.png b/src/Algorithm/images/Algorithm/数据结构-stack.png new file mode 100644 index 0000000..a7620f6 Binary files /dev/null and b/src/Algorithm/images/Algorithm/数据结构-stack.png differ diff --git a/src/Algorithm/images/Algorithm/斐波那契查找.png b/src/Algorithm/images/Algorithm/斐波那契查找.png new file mode 100644 index 0000000..d6d3ca0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/斐波那契查找.png differ diff --git a/src/Algorithm/images/Algorithm/斜树.png b/src/Algorithm/images/Algorithm/斜树.png new file mode 100644 index 0000000..39d4f98 Binary files /dev/null and b/src/Algorithm/images/Algorithm/斜树.png differ diff --git a/src/Algorithm/images/Algorithm/旋转数组.png b/src/Algorithm/images/Algorithm/旋转数组.png new file mode 100644 index 0000000..395867b Binary files /dev/null and b/src/Algorithm/images/Algorithm/旋转数组.png differ diff --git a/src/Algorithm/images/Algorithm/最小堆-前K个高频元素.jpg b/src/Algorithm/images/Algorithm/最小堆-前K个高频元素.jpg new file mode 100644 index 0000000..a4d0be8 Binary files /dev/null and b/src/Algorithm/images/Algorithm/最小堆-前K个高频元素.jpg differ diff --git a/src/Algorithm/images/Algorithm/桥梁模式-1.png b/src/Algorithm/images/Algorithm/桥梁模式-1.png new file mode 100644 index 0000000..8571cf4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/桥梁模式-1.png differ diff --git a/src/Algorithm/images/Algorithm/桥梁模式-2.png b/src/Algorithm/images/Algorithm/桥梁模式-2.png new file mode 100644 index 0000000..33c9d58 Binary files /dev/null and b/src/Algorithm/images/Algorithm/桥梁模式-2.png differ diff --git a/src/Algorithm/images/Algorithm/桶排序.png b/src/Algorithm/images/Algorithm/桶排序.png new file mode 100644 index 0000000..008bbb4 Binary files /dev/null and b/src/Algorithm/images/Algorithm/桶排序.png differ diff --git a/src/Algorithm/images/Algorithm/模板方法模式.png b/src/Algorithm/images/Algorithm/模板方法模式.png new file mode 100644 index 0000000..c7f1619 Binary files /dev/null and b/src/Algorithm/images/Algorithm/模板方法模式.png differ diff --git a/src/Algorithm/images/Algorithm/深度优先搜索.jpg b/src/Algorithm/images/Algorithm/深度优先搜索.jpg new file mode 100644 index 0000000..39f7c2c Binary files /dev/null and b/src/Algorithm/images/Algorithm/深度优先搜索.jpg differ diff --git a/src/Algorithm/images/Algorithm/滑动窗口的最大值.png b/src/Algorithm/images/Algorithm/滑动窗口的最大值.png new file mode 100644 index 0000000..14fe519 Binary files /dev/null and b/src/Algorithm/images/Algorithm/滑动窗口的最大值.png differ diff --git a/src/Algorithm/images/Algorithm/满二叉树.png b/src/Algorithm/images/Algorithm/满二叉树.png new file mode 100644 index 0000000..128c6c2 Binary files /dev/null and b/src/Algorithm/images/Algorithm/满二叉树.png differ diff --git a/src/Algorithm/images/Algorithm/状态模式.png b/src/Algorithm/images/Algorithm/状态模式.png new file mode 100644 index 0000000..808b386 Binary files /dev/null and b/src/Algorithm/images/Algorithm/状态模式.png differ diff --git a/src/Algorithm/images/Algorithm/用链表实现栈.png b/src/Algorithm/images/Algorithm/用链表实现栈.png new file mode 100644 index 0000000..dd32c28 Binary files /dev/null and b/src/Algorithm/images/Algorithm/用链表实现栈.png differ diff --git a/src/Algorithm/images/Algorithm/用链表实现队列.png b/src/Algorithm/images/Algorithm/用链表实现队列.png new file mode 100644 index 0000000..b4fa119 Binary files /dev/null and b/src/Algorithm/images/Algorithm/用链表实现队列.png differ diff --git a/src/Algorithm/images/Algorithm/策略模式.png b/src/Algorithm/images/Algorithm/策略模式.png new file mode 100644 index 0000000..f0669f2 Binary files /dev/null and b/src/Algorithm/images/Algorithm/策略模式.png differ diff --git a/src/Algorithm/images/Algorithm/算法思想.jpg b/src/Algorithm/images/Algorithm/算法思想.jpg new file mode 100644 index 0000000..10ddca1 Binary files /dev/null and b/src/Algorithm/images/Algorithm/算法思想.jpg differ diff --git a/src/Algorithm/images/Algorithm/线段树.png b/src/Algorithm/images/Algorithm/线段树.png new file mode 100644 index 0000000..82d94a0 Binary files /dev/null and b/src/Algorithm/images/Algorithm/线段树.png differ diff --git a/src/Algorithm/images/Algorithm/组合模式.png b/src/Algorithm/images/Algorithm/组合模式.png new file mode 100644 index 0000000..b51be9f Binary files /dev/null and b/src/Algorithm/images/Algorithm/组合模式.png differ diff --git a/src/Algorithm/images/Algorithm/翻转字符串algorithm.gif b/src/Algorithm/images/Algorithm/翻转字符串algorithm.gif new file mode 100644 index 0000000..84677c2 Binary files /dev/null and b/src/Algorithm/images/Algorithm/翻转字符串algorithm.gif differ diff --git a/src/Algorithm/images/Algorithm/观察者模式.png b/src/Algorithm/images/Algorithm/观察者模式.png new file mode 100644 index 0000000..d313673 Binary files /dev/null and b/src/Algorithm/images/Algorithm/观察者模式.png differ diff --git a/src/Algorithm/images/Algorithm/计数排序.gif b/src/Algorithm/images/Algorithm/计数排序.gif new file mode 100644 index 0000000..2479350 Binary files /dev/null and b/src/Algorithm/images/Algorithm/计数排序.gif differ diff --git a/src/Algorithm/images/Algorithm/责任链模式.png b/src/Algorithm/images/Algorithm/责任链模式.png new file mode 100644 index 0000000..fbbce52 Binary files /dev/null and b/src/Algorithm/images/Algorithm/责任链模式.png differ diff --git a/src/Algorithm/images/Algorithm/适配器模式-对象适配器.png b/src/Algorithm/images/Algorithm/适配器模式-对象适配器.png new file mode 100644 index 0000000..a59284d Binary files /dev/null and b/src/Algorithm/images/Algorithm/适配器模式-对象适配器.png differ diff --git a/src/Algorithm/images/Algorithm/适配器模式-类继承.png b/src/Algorithm/images/Algorithm/适配器模式-类继承.png new file mode 100644 index 0000000..d1ddac1 Binary files /dev/null and b/src/Algorithm/images/Algorithm/适配器模式-类继承.png differ diff --git a/src/Algorithm/images/Algorithm/适配器模式-默认适配器.png b/src/Algorithm/images/Algorithm/适配器模式-默认适配器.png new file mode 100644 index 0000000..59c2f81 Binary files /dev/null and b/src/Algorithm/images/Algorithm/适配器模式-默认适配器.png differ diff --git a/src/Algorithm/images/Algorithm/选择排序.gif b/src/Algorithm/images/Algorithm/选择排序.gif new file mode 100644 index 0000000..353459b Binary files /dev/null and b/src/Algorithm/images/Algorithm/选择排序.gif differ diff --git a/src/Algorithm/images/Algorithm/选择排序.jpg b/src/Algorithm/images/Algorithm/选择排序.jpg new file mode 100644 index 0000000..ea7ed3e Binary files /dev/null and b/src/Algorithm/images/Algorithm/选择排序.jpg differ diff --git a/src/Algorithm/images/Algorithm/链表(LinkedList).png b/src/Algorithm/images/Algorithm/链表(LinkedList).png new file mode 100644 index 0000000..479abef Binary files /dev/null and b/src/Algorithm/images/Algorithm/链表(LinkedList).png differ diff --git a/src/Algorithm/images/Algorithm/门面模式.png b/src/Algorithm/images/Algorithm/门面模式.png new file mode 100644 index 0000000..692e284 Binary files /dev/null and b/src/Algorithm/images/Algorithm/门面模式.png differ diff --git a/src/Database/1.md b/src/Database/1.md new file mode 100644 index 0000000..dc763f9 --- /dev/null +++ b/src/Database/1.md @@ -0,0 +1,20 @@ +**所有字段的值都是不可分解的原子值**。即实体中的某个属性有多个值时,必须拆分为不同的属性。例如: + +**用户信息表** + +| 编号 | 姓名 | 年龄 | 地址 | +| ---- | ---- | ---- | ---------------------------- | +| 1 | 小王 | 23 | 浙江省杭州市拱墅区湖州街51号 | + +当实际需求对地址没有特定的要求下,这个用户信息表的每一列都是不可分割的。但是当实际需求对省份或者城市有特别要求时,这个用户信息表中的地址就是可以分割的,改为: + +**用户信息表** + +| 编号 | 姓名 | 年龄 | 省份 | 城市 | 区县 | 详细地址 | +| ---- | ---- | ---- | ------ | ------ | ------ | ---------- | +| 1 | 小王 | 23 | 浙江省 | 杭州市 | 拱墅区 | 湖州街51号 | + +**好处** + +- 表结构相对清晰 +- 易于查询 \ No newline at end of file diff --git a/src/Database/1001.md b/src/Database/1001.md new file mode 100644 index 0000000..951c6b8 --- /dev/null +++ b/src/Database/1001.md @@ -0,0 +1,15 @@ +我们在考虑MySQL数据库的高可用架构时,主要考虑如下几方面: + +- 如果数据库发生了宕机或者意外中断等故障,能尽快恢复数据库的可用性,尽可能的减少停机时间,保证业务不会因为数据库的故障而中断 +- 用作备份、只读副本等功能的非主节点的数据应该和主节点的数据实时或者最终保持一致 +- 当业务发生数据库切换时,切换前后的数据库内容应当一致,不会因为数据缺失或者数据不一致而影响业务 + +关于对高可用的分级我们暂不做详细的讨论,这里只讨论常用高可用方案的优缺点以及选型。 + + + +随着人们对数据一致性要求不断的提高,越来越多的方法被尝试用来解决分布式数据一致性的问题,如MySQL自身的优化、MySQL集群架构的优化、Paxos、Raft、2PC算法的引入等。 + +而使用分布式算法用来解决MySQL数据库数据一致性问题的方法,也越来越被人们所接受,一系列成熟的产品如PhxSQL、MariaDB Galera Cluster、Percona XtraDB Cluster等越来越多的被大规模使用。 + +随着官方MySQL Group Replication的GA,使用分布式协议来解决数据一致性问题已经成为了主流的方向。期望越来越多优秀的解决方案被提出,MySQL高可用问题也可以被更好的解决。 \ No newline at end of file diff --git a/src/Database/1002.md b/src/Database/1002.md new file mode 100644 index 0000000..13f2bc9 --- /dev/null +++ b/src/Database/1002.md @@ -0,0 +1,16 @@ +使用双节点数据库,搭建单向或者双向的半同步复制。在5.7以后的版本中,由于lossless replication、logical多线程复制等一些列新特性的引入,使得MySQL原生半同步复制更加可靠。常见架构如下: + +![主从或主主半同步复制](images/Database/主从或主主半同步复制.jpg) + +通常会和Proxy、Keepalived等第三方软件同时使用,即可以用来监控数据库的健康,又可以执行一系列管理命令。如果主库发生故障,切换到备库后仍然可以继续使用数据库。 + +**优点** + +- 架构比较简单,使用原生半同步复制作为数据同步的依据 +- 双节点,没有主机宕机后的选主问题,直接切换即可 +- 双节点,需求资源少,部署简单 + +**缺点** + +- 完全依赖于半同步复制,如果半同步复制退化为异步复制,数据一致性无法得到保证 +- 需要额外考虑HAProxy、Keepalived的高可用机制 \ No newline at end of file diff --git a/src/Database/1003.md b/src/Database/1003.md new file mode 100644 index 0000000..59d187e --- /dev/null +++ b/src/Database/1003.md @@ -0,0 +1,28 @@ +半同步复制机制是可靠的。如果半同步复制一直是生效的,那么可以认为数据是一致的。但是由于网络波动等一些客观原因,导致半同步复制发生超时而切换为异步复制,这时便不能保证数据的一致性。所以尽可能的保证半同步复制,就可以提高数据的一致性。 + +该方案同样使用双节点架构,但是在原有半同复制的基础上做了功能上的优化,使半同步复制的机制变得更加可靠。可参考的优化方案如下: + +### 双通道复制 + +![双通道复制](images/Database/双通道复制.jpg) + +半同步复制由于发生超时后,复制断开,当再次建立起复制时,同时建立两条通道,其中一条半同步复制通道从当前位置开始复制,保证从机知道当前主机执行的进度。另外一条异步复制通道开始追补从机落后的数据。当异步复制通道追赶到半同步复制的起始位置时,恢复半同步复制。 + + + +### binlog文件服务器 + +![binlog文件服务器](images/Database/binlog文件服务器.jpg) + +搭建两条半同步复制通道,其中连接文件服务器的半同步通道正常情况下不启用,当主从的半同步复制发生网络问题退化后,启动与文件服务器的半同步复制通道。当主从半同步复制恢复后,关闭与文件服务器的半同步复制通道。 + +**优点** + +- 双节点,需求资源少,部署简单 +- 架构简单,没有选主的问题,直接切换即可 +- 相比于原生复制,优化后的半同步复制更能保证数据的一致性 + +**缺点** + +- 需要修改内核源码或者使用MySQL通信协议。需要对源码有一定的了解,并能做一定程度的二次开发 +- 依旧依赖于半同步复制,没有从根本上解决数据一致性问题 \ No newline at end of file diff --git a/src/Database/1004.md b/src/Database/1004.md new file mode 100644 index 0000000..86732df --- /dev/null +++ b/src/Database/1004.md @@ -0,0 +1,43 @@ +将双节点数据库扩展到多节点数据库,或者多节点数据库集群。可以根据自己的需要选择一主两从、一主多从或者多主多从的集群。由于半同步复制,存在接收到一个从机的成功应答即认为半同步复制成功的特性,所以多从半同步复制的可靠性要优于单从半同步复制的可靠性。并且多节点同时宕机的几率也要小于单节点宕机的几率,所以多节点架构在一定程度上可以认为高可用性是好于双节点架构。 + +但由于数据库数量较多,所以需要数据库管理软件来保证数据库的可维护性。可以选择MMM、MHA或者各个版本的Proxy等等。常见方案如下: + +### MHA+多节点集群 + +![MHA+多节点集群](images/Database/MHA+多节点集群.jpg) + +MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master,整个故障转移过程对应用程序完全透明。MHA Node运行在每台MySQL服务器上,主要作用是切换时处理二进制日志,确保切换尽量少丢数据。MHA也可以扩展到如下的多节点集群: + +![MHA-Manager](images/Database/MHA-Manager.jpg) + +**优点** + +- 可以进行故障的自动检测和转移 +- 可扩展性较好,可以根据需要扩展MySQL的节点数量和结构 +- 相比于双节点的MySQL复制,三节点/多节点的MySQL发生不可用的概率更低 + +**缺点** + +- 至少需要三节点,相对于双节点需要更多的资源 +- 逻辑较为复杂,发生故障后排查问题,定位问题更加困难 +- 数据一致性仍然靠原生半同步复制保证,仍然存在数据不一致的风险 +- 可能因为网络分区发生脑裂现象。 +- 在此我向大家推荐一个架构学习交流群。交流学习群号:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 + + + +### ZooKeeper+Proxy + +ZooKeeper使用分布式算法保证集群数据的一致性,使用ZooKeeper可以有效的保证Proxy的高可用性,可以较好地避免网络分区现象的产生。 + +![ZooKeeper+Proxy](images/Database/ZooKeeper+Proxy.jpg) + +**优点** + +- 较好的保证了整个系统的高可用性,包括Proxy、MySQL +- 扩展性较好,可以扩展为大规模集群 + +**缺点** + +- 数据一致性仍然依赖于原生的mysql半同步复制 +- 引入ZK,整个系统的逻辑变得更加复杂 \ No newline at end of file diff --git a/src/Database/1005.md b/src/Database/1005.md new file mode 100644 index 0000000..54aa6c2 --- /dev/null +++ b/src/Database/1005.md @@ -0,0 +1,41 @@ +共享存储实现了数据库服务器和存储设备的解耦,不同数据库之间的数据同步不再依赖于MySQL的原生复制功能,而是通过磁盘数据同步的手段,来保证数据的一致性。 + +### SAN共享储存 + +SAN的概念是允许存储设备和处理器(服务器)之间建立直接的高速网络(与LAN相比)连接,通过这种连接实现数据的集中式存储。常用架构如下: + +![SAN共享储存](images/Database/SAN共享储存.jpg) + +使用共享存储时,MySQL服务器能够正常挂载文件系统并操作,如果主库发生宕机,备库可以挂载相同的文件系统,保证主库和备库使用相同的数据。 + +**优点** + +- 两节点即可,部署简单,切换逻辑简单 +- 很好的保证数据的强一致性 +- 不会因为MySQL的逻辑错误发生数据不一致的情况 + +**缺点** + +- 需要考虑共享存储的高可用 +- 价格昂贵 + + + +### DRBD磁盘复制 + +DRBD是一种基于软件、基于网络的块复制存储解决方案,主要用于对服务器之间的磁盘、分区、逻辑卷等进行数据镜像,当用户将数据写入本地磁盘时,还会将数据发送到网络中另一台主机的磁盘上,这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。常用架构如下: + +![DRBD磁盘复制](images/Database/DRBD磁盘复制.jpg) + +当本地主机出现问题,远程主机上还保留着一份相同的数据,可以继续使用,保证了数据的安全。DRBD是Linux内核模块实现的快级别的同步复制技术,可以与SAN达到相同的共享存储效果。 + +**优点** + +- 两节点即可,部署简单,切换逻辑简单 +- 相比于SAN储存网络,价格低廉 +- 保证数据的强一致性 + +**缺点** + +- 对IO性能影响较大 +- 从库不提供读操作 \ No newline at end of file diff --git a/src/Database/1006.md b/src/Database/1006.md new file mode 100644 index 0000000..2a71790 --- /dev/null +++ b/src/Database/1006.md @@ -0,0 +1,58 @@ +分布式协议可以很好地解决数据一致性问题。比较常见的方案如下: + +### MySQL Cluster + +MySQL Cluster是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。 + +![MySQL-Cluster](images/Database/MySQL-Cluster.jpg) + +**优点** + +- 全部使用官方组件,不依赖于第三方软件 +- 可以实现数据的强一致性 + +**缺点** + +- 国内使用的较少 +- 配置较复杂,需要使用NDB储存引擎,与MySQL常规引擎存在一定差异 +- 至少三节点 + + + +### Galera + +基于Galera的MySQL高可用集群, 是多主数据同步的MySQL集群解决方案,使用简单,没有单点故障,可用性高。常见架构如下: + +![MySQL-Galera](images/Database/MySQL-Galera.jpg) + +**优点** + +- 多主写入,无延迟复制,能保证数据强一致性 +- 有成熟的社区,有互联网公司在大规模的使用 +- 自动故障转移,自动添加、剔除节点 + +**缺点** + +- 需要为原生MySQL节点打wsrep补丁 +- 只支持innodb储存引擎 +- 至少三节点 + + + +### Paxos + +Paxos算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。这个算法被认为是同类算法中最有效的。Paxos与MySQL相结合可以实现在分布式的MySQL数据的强一致性。常见架构如下: + +![MySQL-Paxos](images/Database/MySQL-Paxos.jpg) + +**优点** + +- 多主写入,无延迟复制,能保证数据强一致性 +- 有成熟理论基础 +- 自动故障转移,自动添加、剔除节点 + +**缺点** + +- 只支持InnoDB储存引擎 +- 至少三节点 +- 在此我向大家推荐一个架构学习交流群。交流学习群号:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 \ No newline at end of file diff --git a/src/Database/1007.md b/src/Database/1007.md new file mode 100644 index 0000000..d128603 --- /dev/null +++ b/src/Database/1007.md @@ -0,0 +1,173 @@ +在实际的生产环境中,由单台MySQL作为独立的数据库是完全不能满足实际需求的,无论是在安全性,高可用性以及高并发等各个方面。因此,一般来说都是通过集群主从复制(Master-Slave)的方式来同步数据,再通过读写分离(MySQL-Proxy)来提升数据库的并发负载能力进行部署与实施。总结MySQL主从集群带来的作用是: + +- 提高数据库负载能力,主库执行读写任务(增删改),备库仅做查询 +- 提高系统读写性能、可扩展性和高可用性 +- 数据备份与容灾,备库在异地,主库不存在了,备库可以立即接管,无须恢复时间 + +![MySQL主从集群](images/Database/MySQL主从集群.jpg) + + + +### biglog + +**binlog是什么?有什么作用?** + +用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。可以简单理解为记录的就是sql语句。binlog 是 mysql 的逻辑日志,并且由 `Server`层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。在实际应用中, binlog 的主要使用场景有两个: + +- 用于主从复制,在主从结构中,binlog 作为操作记录从 master 被发送到 slave,slave服务器从 master 接收到的日志保存到 relay log 中 +- 用于数据备份,在数据库备份文件生成后,binlog保存了数据库备份后的详细信息,以便下一次备份能从备份点开始 + + + +**日志格式** + +binlog日志有三种格式,分别为STATMENT 、 ROW 和 MIXED。在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 `binlog-format` 指定。 + +- **STATMENT** :基于 SQL 语句的复制,每一条会修改数据的sql语句会记录到 binlog 中 +- **ROW** :基于行的复制 +- **MIXED** :基于 STATMENT 和 ROW 两种模式的混合复制,比如一般的数据操作使用 row 格式保存,有些表结构的变更语句,使用 statement 来记录 + + + +我们还可以通过mysql提供的查看工具mysqlbinlog查看文件中的内容,例如: + +```mysql +mysqlbinlog mysql-bin.00001 | more +``` + +binlog文件大小和个数会不断的增加,后缀名会按序号递增,例如`mysql-bin.00002`等。 + + + +### 主从复制原理 + +![主从复制原理](images/Database/主从复制原理.jpg) + +可以看到mysql主从复制需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** + +- **binlog dump线程:** 主库中有数据更新时,根据设置的binlog格式,将更新的事件类型写入到主库的binlog文件中,并创建log dump线程通知slave有数据更新。当I/O线程请求日志内容时,将此时的binlog名称和当前更新的位置同时传给slave的I/O线程 +- **I/O线程:** 该线程会连接到master,向log dump线程请求一份指定binlog文件位置的副本,并将请求回来的binlog存到本地的relay log中 +- **SQL线程:** 该线程检测到relay log有更新后,会读取并在本地做redo操作,将发生在主库的事件在本地重新执行一遍,来保证主从数据同步 + + + +**基本过程总结** + +- 主库写入数据并且生成binlog文件。该过程中MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的 +- 在事件写入二进制日志完成后,master通知存储引擎提交事务 +- 从库服务器上的IO线程连接Master服务器,请求从执行binlog日志文件中的指定位置开始读取binlog至从库 +- 主库接收到从库IO线程请求后,其上复制的IO线程会根据Slave的请求信息分批读取binlog文件然后返回给从库的IO线程 +- Slave服务器的IO线程获取到Master服务器上IO线程发送的日志内容、日志文件及位置点后,会将binlog日志内容依次写到Slave端自身的Relay Log(即中继日志)文件的最末端,并将新的binlog文件名和位置记录到`master-info`文件中,以便下一次读取master端新binlog日志时能告诉Master服务器从新binlog日志的指定文件及位置开始读取新的binlog日志内容 +- 从库服务器的SQL线程会实时监测到本地Relay Log中新增了日志内容,然后把RelayLog中的日志翻译成SQL并且按照顺序执行SQL来更新从库的数据 +- 从库在`relay-log.info`中记录当前应用中继日志的文件名和位置点以便下一次数据复制 + + + +**并行复制** + +在MySQL 5.6版本之前,Slave服务器上有两个线程I/O线程和SQL线程。I/O线程负责接收二进制日志,SQL线程进行回放二进制日志。如果在MySQL 5.6版本开启并行复制功能,那么SQL线程就变为了coordinator线程,coordinator线程主要负责以前两部分的内容: + +![并行复制](images/Database/并行复制.jpg) + +**上图的红色框框部分就是实现并行复制的关键所在**。这意味着coordinator线程并不是仅将日志发送给worker线程,自己也可以回放日志,但是所有可以并行的操作交付由worker线程完成。coordinator线程与worker是典型的生产者与消费者模型。 + +![coordinator线程](images/Database/coordinator线程.jpg) + +不过到MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就是slave服务器的回放与主机是一致的即master服务器上是怎么并行执行的slave上就怎样进行并行回放。不再有库的并行复制限制,对于二进制日志格式也无特殊的要求。为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量`slave-parallel-type`,其可以配置的值有: + +- **DATABASE**:默认值,基于库的并行复制方式 +- **LOGICAL_CLOCK**:基于组提交的并行复制方式 + + + +**按库并行** + +每个 worker 线程对应一个 hash 表,用于保存当前正在这个worker的执行队列里的事务所涉及到的库。其中hash表里的key是数据库名,用于决定分发策略。该策略的优点是构建hash值快,只需要库名,同时对于binlog的格式没有要求。但这个策略的效果,只有在主库上存在多个DB,且各个DB的压力均衡的情况下,这个策略效果好。因此,对于主库上的表都放在同一个DB或者不同DB的热点不同,则起不到多大效果: + +![按库并行](images/Database/按库并行.jpg) + + + +**组提交优化** + +该特性如下: + +- 能够同一组里提交的事务,定不会修改同一行 +- 主库上可以并行执行的事务,从库上也一定可以并行执行 + +具体是如何实现的: + +- 在同一组里面一起提交的事务,会有一个相同的`commit_id`,下一组为`commit_id+1`,该`commit_id`会直接写到binlog中 +- 在从库使用时,相同`commit_id`的事务会被分发到多个worker并行执行,直到这一组相同的`commit_id`执行结束后,coordinator再取下一批 + + + +### 主从延迟 + +根据主从复制的原理可以看出,两者之间是存在一定时间的数据不一致,也就是所谓的主从延迟。导致主从延迟的时间点: + +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为T1 +- 传给从库B,从库接受完这个binlog的时刻记为T2 +- 从库B执行完这个事务,该时刻记为T3 + +那么所谓主从延迟,就是同一个事务,从库执行完成的时间和主库执行完成的时间之间的差值,即T3-T1。我们也可以通过在从库执行`show slave status`,返回结果会显示`seconds_behind_master`,表示当前从库延迟了多少秒。 + +**seconds_behind_master如何计算的?** + +- 每一个事务的binlog都有一个时间字段,用于记录主库上写入的时间 +- 从库取出当前正在执行的事务的时间字段,跟当前系统的时间进行相减,得到的就是`seconds_behind_master`,也就是前面所描述的T3-T1 + + + +**为什么会主从延迟?** + +正常情况下,如果网络不延迟,那么日志从主库传给从库的时间是相当短,所以T2-T1可以基本忽略。最直接的影响就是从库消费中转日志(relaylog)的时间段,而造成原因一般是以下几种: + +- **从库的机器性能比主库要差** + + 比如将20台主库放在4台机器,把从库放在一台机器。这个时候进行更新操作,由于更新时会触发大量读操作,导致从库机器上的多个从库争夺资源,导致主从延迟。不过,目前大部分部署都是采取主从使用相同规格的机器部署 + +- **从库的压力大** + + 按照正常的策略,读写分离,主库提供写能力,从库提供读能力。将进行大量查询放在从库上,结果导致从库上耗费了大量的CPU资源,进而影响了同步速度,造成主从延迟。对于这种情况,可以通过一主多从,分担读压力;也可以采取binlog输出到外部系统,比如Hadoop,让外部系统提供查询能力 + +- **大事务的执行** + + 一旦执行大事务,那么主库必须要等到事务完成之后才会写入binlog。比如主库执行了一条insert … select非常大的插入操作,该操作产生了近几百G的binlog文件传输到只读节点,进而导致了只读节点出现应用binlog延迟。因此,DBA经常会提醒开发,不要一次性地试用delete语句删除大量数据,尽可能控制数量,分批进行 + +- **主库的DDL(alter、drop、create)** + + - 只读节点与主库的DDL同步是串行进行,如果DDL操作在主库执行时间很长,那么从库也会消耗同样的时间,比如在主库对一张500W的表添加一个字段耗费了10分钟,那么从节点上也会耗费10分钟 + - 从节点上有一个执行时间非常长的的查询正在执行,那么这个查询会堵塞来自主库的DDL,表被锁,直到查询结束为止,进而导致了从节点的数据延迟 + +- **锁冲突** + + 锁冲突问题也可能导致从节点的SQL线程执行慢,比如从机上有一些select .... for update的SQL,或者使用了MyISAM引擎等 + +- **从库的复制能力** + + 一般场景中,因偶然情况导致从库延迟了几分钟,都会在从库恢复之后追上主库。但若是从库执行速度低于主库,且主库持续具有压力,就会导致长时间主从延迟,很有可能就是从库复制能力的问题。 + +从库上的执行,即`sql_thread`更新逻辑,在5.6版本之前,是只支持单线程,那么在主库并发高、TPS高时,就会出现较大的主从延迟。因此,MySQL自5.7版本后就已经支持并行复制了。可以在从服务上设置 `slave_parallel_workers`为一个大于0的数,然后把`slave_parallel_type`参数设置为`LOGICAL_CLOCK`,这就可以了 + +```mysql +mysql> show variables like 'slave_parallel%'; ++------------------------+----------+ +| Variable_name | Value | ++------------------------+----------+ +| slave_parallel_type | DATABASE | +| slave_parallel_workers | 0 | ++------------------------+----------+ +``` + + + +**怎么减少主从延迟?** + +主从同步问题永远都是一致性和性能的权衡,得看实际的应用场景,若想要减少主从延迟的时间,可以采取下面的办法: + +- 降低多线程大事务并发的概率,优化业务逻辑 +- 优化SQL,避免慢SQL,减少批量操作,建议写脚本以update-sleep这样的形式完成 +- 提高从库机器的配置,减少主库写binlog和从库读binlog的效率差 +- 尽量采用短的链路,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输的网络延时 +- 实时性要求的业务读强制走主库,从库只做灾备,备份 \ No newline at end of file diff --git a/src/Database/101.md b/src/Database/101.md new file mode 100644 index 0000000..8976ee9 --- /dev/null +++ b/src/Database/101.md @@ -0,0 +1,27 @@ +INNER JOIN 一般被译作内连接。内连接查询能将左表(表 A)和右表(表 B)中能关联起来的数据连接后返回。 + +**文氏图** + +![内连接(INNER-JOIN)](images/Database/内连接(INNER-JOIN).png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +INNER JOIN Table_B B +ON A.PK = B.PK; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| 1 | 1 | both ab | both ab | ++------+------+---------+---------+ +1 row in set (0.00 sec) +``` + +**注意**:其中A为Table_A的别名,B为Table_B的别名,下同。 \ No newline at end of file diff --git a/src/Database/102.md b/src/Database/102.md new file mode 100644 index 0000000..4ba50a9 --- /dev/null +++ b/src/Database/102.md @@ -0,0 +1,26 @@ +LEFT JOIN 一般被译作左连接,也写作 LEFT OUTER JOIN。左连接查询会返回左表(表 A)中所有记录,不管右表(表 B)中有没有关联的数据。在右表中找到的关联数据列也会被一起返回。 + +**文氏图** + +![左连接(LEFT-JOIN)](images/Database/左连接(LEFT-JOIN).png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +LEFT JOIN Table_B B +ON A.PK = B.PK; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| 1 | 1 | both ab | both ba | +| 2 | NULL | only a | NULL | ++------+------+---------+---------+ +2 rows in set (0.00 sec) +``` \ No newline at end of file diff --git a/src/Database/103.md b/src/Database/103.md new file mode 100644 index 0000000..6520fbd --- /dev/null +++ b/src/Database/103.md @@ -0,0 +1,26 @@ +RIGHT JOIN 一般被译作右连接,也写作 RIGHT OUTER JOIN。右连接查询会返回右表(表 B)中所有记录,不管左表(表 A)中有没有关联的数据。在左表中找到的关联数据列也会被一起返回。 + +**文氏图** + +![右连接(RIGHT-JOIN)](images/Database/右连接(RIGHT-JOIN).png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +RIGHT JOIN Table_B B +ON A.PK = B.PK; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| 1 | 1 | both ab | both ba | +| NULL | 3 | NULL | only b | ++------+------+---------+---------+ +2 rows in set (0.00 sec) +``` \ No newline at end of file diff --git a/src/Database/104.md b/src/Database/104.md new file mode 100644 index 0000000..c5fe44a --- /dev/null +++ b/src/Database/104.md @@ -0,0 +1,48 @@ +FULL JOIN 一般被译作外连接、全连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN。外连接查询能返回左右表里的所有记录,其中左右表里能关联起来的记录被连接后返回。(**MySQL不支持FULL OUTER JOIN**) + +**文氏图** + +![全连接(FULL-OUTER-JOIN)](images/Database/全连接(FULL-OUTER-JOIN).png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +FULL OUTER JOIN Table_B B +ON A.PK = B.PK; +``` + +**查询结果** + +```mysql +ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B +ON A.PK = B.PK' at line 4 +``` + +注意:我当前示例使用的 MySQL 不支持FULL OUTER JOIN。应当返回的结果(使用UNION模拟): + +```mysql +mysql> SELECT * + -> FROM Table_A + -> LEFT JOIN Table_B + -> ON Table_A.PK = Table_B.PK + -> UNION ALL + -> SELECT * + -> FROM Table_A + -> RIGHT JOIN Table_B + -> ON Table_A.PK = Table_B.PK + -> WHERE Table_A.PK IS NULL; ++------+---------+------+---------+ +| PK | Value | PK | Value | ++------+---------+------+---------+ +| 1 | both ab | 1 | both ba | +| 2 | only a | NULL | NULL | +| NULL | NULL | 3 | only b | ++------+---------+------+---------+ +3 rows in set (0.00 sec) +``` + +**SQL常见缩影** + +![SQL常用JOIN](images/Database/SQL常用JOIN.png) \ No newline at end of file diff --git a/src/Database/105.md b/src/Database/105.md new file mode 100644 index 0000000..3e8eef3 --- /dev/null +++ b/src/Database/105.md @@ -0,0 +1,26 @@ +**左连接后排除内连接**。返回左表有但右表没有关联数据的记录集。 + +**文氏图** + +![LEFT-JOIN-EXCLUDING-INNER-JOIN](images/Database/LEFT-JOIN-EXCLUDING-INNER-JOIN.png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +LEFT JOIN Table_B B +ON A.PK = B.PK +WHERE B.PK IS NULL; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| 2 | NULL | only a | NULL | ++------+------+---------+---------+ +1 row in set (0.01 sec) +``` \ No newline at end of file diff --git a/src/Database/106.md b/src/Database/106.md new file mode 100644 index 0000000..479772b --- /dev/null +++ b/src/Database/106.md @@ -0,0 +1,26 @@ +**右连接后排除内连接**。返回右表有但左表没有关联数据的记录集。 + +**文氏图** + +![RIGHT-JOIN-EXCLUDING-INNER-JOIN](images/Database/RIGHT-JOIN-EXCLUDING-INNER-JOIN.png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +RIGHT JOIN Table_B B +ON A.PK = B.PK +WHERE A.PK IS NULL; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| NULL | 3 | NULL | only b | ++------+------+---------+---------+ +1 row in set (0.00 sec) +``` \ No newline at end of file diff --git a/src/Database/107.md b/src/Database/107.md new file mode 100644 index 0000000..10575a9 --- /dev/null +++ b/src/Database/107.md @@ -0,0 +1,52 @@ +**全连接后排除内连接**。返回左表和右表里没有相互关联的记录集。 + +**文氏图** + +![FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN](images/Database/FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN.png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +FULL OUTER JOIN Table_B B +ON A.PK = B.PK +WHERE A.PK IS NULL +OR B.PK IS NULL; +``` + +因为使用到了 FULL OUTER JOIN,MySQL 在执行该查询时再次报错。 + +```mysql +ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B +ON A.PK = B.PK +WHERE A.PK IS NULL +OR B.PK IS NULL' at line 4 +``` + +应当返回的结果(用 UNION 模拟): + +```mysql +mysql> SELECT * + -> FROM Table_A + -> LEFT JOIN Table_B + -> ON Table_A.PK = Table_B.PK + -> WHERE Table_B.PK IS NULL + -> UNION ALL + -> SELECT * + -> FROM Table_A + -> RIGHT JOIN Table_B + -> ON Table_A.PK = Table_B.PK + -> WHERE Table_A.PK IS NULL; ++------+--------+------+--------+ +| PK | Value | PK | Value | ++------+--------+------+--------+ +| 2 | only a | NULL | NULL | +| NULL | NULL | 3 | only b | ++------+--------+------+--------+ +2 rows in set (0.00 sec) +``` + +**SQL所有JOIN** + +![SQL所有JOIN](images/Database/SQL所有JOIN.png) \ No newline at end of file diff --git a/src/Database/108.md b/src/Database/108.md new file mode 100644 index 0000000..c861177 --- /dev/null +++ b/src/Database/108.md @@ -0,0 +1,29 @@ +返回左表与右表之间符合条件的记录的迪卡尔集。 + +**图示** + +![CROSS-JOIN](images/Database/CROSS-JOIN.png) + +**示例查询** + +```mysql +SELECT A.PK AS A_PK, B.PK AS B_PK,A.Value AS A_Value, B.Value AS B_Value +FROM Table_A A +CROSS JOIN Table_B B; +``` + +**查询结果** + +```mysql ++------+------+---------+---------+ +| A_PK | B_PK | A_Value | B_Value | ++------+------+---------+---------+ +| 1 | 1 | both ab | both ba | +| 2 | 1 | only a | both ba | +| 1 | 3 | both ab | only b | +| 2 | 3 | only a | only b | ++------+------+---------+---------+ +4 rows in set (0.00 sec) +``` + +上面讲过的几种 JOIN 查询的结果都可以用 CROSS JOIN 加条件模拟出来,比如 INNER JOIN 对应 CROSS JOIN ... WHERE A.PK = B.PK。 \ No newline at end of file diff --git a/src/Database/109.md b/src/Database/109.md new file mode 100644 index 0000000..eb85eaa --- /dev/null +++ b/src/Database/109.md @@ -0,0 +1,43 @@ +返回表与自己连接后符合条件的记录,一般用在表里有一个字段是用主键作为外键的情况。比如 Table_C 的结构与数据如下: + +```mysql ++--------+----------+-------------+ +| EMP_ID | EMP_NAME | EMP_SUPV_ID | ++--------+----------+-------------+ +| 1001 | Ma | NULL | +| 1002 | Zhuang | 1001 | ++--------+----------+-------------+ +2 rows in set (0.00 sec) +``` + +EMP_ID 字段表示员工 ID,EMP_NAME 字段表示员工姓名,EMP_SUPV_ID 表示主管 ID。 + + + +**示例查询** + +现在我们想查询所有有主管的员工及其对应的主管 ID 和姓名,就可以用 SELF JOIN 来实现。 + +```mysql +SELECT A.EMP_ID AS EMP_ID, A.EMP_NAME AS EMP_NAME, + B.EMP_ID AS EMP_SUPV_ID, B.EMP_NAME AS EMP_SUPV_NAME +FROM Table_C A, Table_C B +WHERE A.EMP_SUPV_ID = B.EMP_ID; +``` + +**查询结果** + +```mysql ++--------+----------+-------------+---------------+ +| EMP_ID | EMP_NAME | EMP_SUPV_ID | EMP_SUPV_NAME | ++--------+----------+-------------+---------------+ +| 1002 | Zhuang | 1001 | Ma | ++--------+----------+-------------+---------------+ +1 row in set (0.00 sec) +``` + +**补充说明** + +- 文中的图使用 Keynote 绘制 +- 个人的体会是 SQL 里的 JOIN 查询与数学里的求交集、并集等很像;SQLite 不支持 RIGHT JOIN 和 FULL OUTER JOIN,可以使用 LEFT JOIN 和 UNION 来达到相同的效果 +- MySQL 不支持 FULL OUTER JOIN,可以使用 LEFT JOIN 和 UNION 来达到相同的效果 \ No newline at end of file diff --git a/src/Database/2.md b/src/Database/2.md new file mode 100644 index 0000000..ae52cc9 --- /dev/null +++ b/src/Database/2.md @@ -0,0 +1,44 @@ +**所有非主键列必须全部依赖于全部主键**。符合与不符合的场景如下: + +![img](images/Database/2NF.png) + +案例如下: + +**学生课程表** + +| 学生编号 | 课程编号 | 学生名称 | 课程名称 | 所在班级 | 班主任 | +| -------- | -------- | -------- | ---------- | --------- | ------ | +| S1 | C1 | 小王 | 计算机导论 | 计算机3班 | 陈老师 | +| S1 | C2 | 小王 | 数据结构 | 计算机3班 | 陈老师 | +| S2 | C1 | 小马 | 计算机导论 | 软件1班 | 李老师 | + +将学生编号和课程编号作为主键,能确定唯一一条数据,但是学生名称只跟学生编号有关,跟课程编号无关,即不满足完全函数依赖,改为: + +**学生表** + +| 学生编号 | 学生名称 | 所在班级 | 班主任 | +| -------- | -------- | --------- | ------ | +| S1 | 小王 | 计算机3班 | 陈老师 | +| S2 | 小马 | 软件1班 | 李老师 | + +**课程表** + +| 课程编号 | 课程名称 | +| -------- | ---------- | +| C1 | 计算机导论 | +| C2 | 数据结构 | + +**学生课程关系表** + +| 学生编号 | 课程编号 | +| -------- | -------- | +| S1 | C1 | +| S1 | C2 | +| S2 | C1 | + +**好处** + +- 相对节约空间,当学生表和课程表属性越多,效果越明显 +- 解决插入异常,当新增一门课程时,原表因为没有学生选课,导致无法插入数据 +- 解决更新繁琐,当更改一门课程名称时,原表要更改多条数据 +- 解决删除异常,当学生学完一门课,原表若要清空学生上课信息,课程编号与课程名称的关系可能会丢失 \ No newline at end of file diff --git a/src/Database/201.md b/src/Database/201.md new file mode 100644 index 0000000..7eab61a --- /dev/null +++ b/src/Database/201.md @@ -0,0 +1,47 @@ +数据库锁可以按粒度、加锁算法、加锁策略、兼容性和其它等方面进行分类: + +- **锁粒度** + - **行锁** + - 锁的是 `某⾏数据` 或 `⾏之间的间隙`。由某些**存储引擎**实现,如InnoDB + - 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高 + - **表锁** + - 锁的是某个 `table`。由MySQL的**SQL layer**层实现的 + - 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低 + - **页锁** + - 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般 + - **全局锁** + - 锁的是整个 `database`。由MySQL的**SQL layer**层实现的 + +- **加锁算法** + - **Record Lock(记录锁)** + - **Gap Lock(间隙锁)** + - **Next-Key Lock(临键锁)** + + +- **加锁策略** + - **悲观锁** + - **乐观锁** + +- **兼容性** + - **排它锁** + - **共享锁** + - **意向锁** + +- **其它锁** + - **自增锁(AUTO-INC锁)** + + + +根据不同的存储引擎,MySQL中锁的特性可以大致归纳如下: + +| 存储引擎 | 行锁 | 表锁 | 页锁 | +| -------- | ---- | ---- | ---- | +| InnoDB | √ | √ | | +| MyISAM | | √ | | +| BDB | √ | √ | √ | + +在 InnoDB 存储引擎中: + +- `SELECT` 操作的不可重复读问题通过 `MVCC` 得到了解决 +- `UPDATE`、`DELETE` 的不可重复读问题通过 `Record Lock` (记录锁)解决 +- `INSERT` 的不可重复读问题是通过 `Next-Key Lock` (临键锁)解决 \ No newline at end of file diff --git a/src/Database/202.md b/src/Database/202.md new file mode 100644 index 0000000..92701a0 --- /dev/null +++ b/src/Database/202.md @@ -0,0 +1,64 @@ +**全局锁就是对整个数据库实例加锁**。MySQL 提供了一个加全局读锁的方法,命令是`Flush tables with read lock (FTWRL)`。 当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞: + +- **数据更新语句**(数据的增删改) +- **数据定义语句**(包括建表、修改表结构等) +- **更新类事务的提交语句** + + + +**使用场景** + +- 典型使用场景是做**全库逻辑备份(mysqldump)** + +**数据库只读状态的危险性** + +- 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就能停止 +- 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟 + + + +全局锁两种方法: + +- **FLUSH TABLES WRITE READ LOCK(`FTWRL`)** +- **set global readonly=true** + +一般建议使用 `FTWRL` 方式,因为: + +- 有些系统中 readonly 的值会被用来做其它逻辑。如判断一个库是主库或备库。因此修改 global 变量的方式影响面更大 +- 异常处理机制上有差异: + - 如果执行`FTWRL`后由于客户端发生异常断开,则MySQL会自动释放这个全局锁,整个库回到可正常更新状态 + - 将库设置为`readonly`后,若客户端发生异常,则数据库会一直保持`readonly`状态,导致整库长时间处于不可写状态 + - readonly 对super用户权限无效 + + + +### 全局读锁(FTWRL) + +**为什么需要全局读锁(FTWRL)?** + +当 `mysqldump` 使用参数 `--single-transaction` 的时候,导数据之前就会启动一个事务,来确保拿到一致性快照视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。所以,**single-transaction 方法只适用于所有的表使用事务引擎的库**。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。 + + + +要使用全局锁,则要执行这条命: + +```sql +flush tables with read lock +``` + +执行后,**整个数据库就处于只读状态了**,这时其他线程执行以下操作,都会被阻塞: + +- 对数据的增删查改操作,比如 insert、delete、update等语句; +- 对表结构的更改操作,比如 alter table、drop table 等语句。 + +如果要释放全局锁,则要执行这条命令: + +```sql +unlock tables +``` + +当然,当会话断开了,全局锁会被自动释放。 + + + +### set global readonly=true \ No newline at end of file diff --git a/src/Database/203.md b/src/Database/203.md new file mode 100644 index 0000000..3829c2e --- /dev/null +++ b/src/Database/203.md @@ -0,0 +1,91 @@ +表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁分为: + +- **表共享读锁(共享锁)** +- **表独占写锁(排他锁)** + +**特点:开销小、加锁快、不会出现死锁、发生锁冲突的概率最高、并发度也最低。** + +### 表锁 + +**表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作**。 + +如果我们想对学生表(t_student)加表锁,可以使用下面的命令: + +```sql +-- 表级别的共享锁,也就是读锁 +lock tables t_student read; +-- 表级别的独占锁,也就是写锁 +lock tables t_stuent wirte; +``` + +不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,**InnoDB 的优势在于实现了颗粒度更细的行级锁**。要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁: + +```sql +unlock tables +``` + + + +### 元数据锁(MDL) + +**MDL作用是防止DDL和DML并发的冲突,保证读写的正确性**。元数据锁是为了保证当用户对表执行 CRUD 操作时,防止其它线程对这个表结构做了变更。不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL。 + +- 对一张表进行 CRUD 操作时,加的是 **MDL 读锁** + + 当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。 + +- 对一张表做结构变更操作的时候,加的是 **MDL 写锁** + + 当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。 + + + +MDL 是在事务提交后才会释放,这意味着**事务执行期间,MDL是一直持有的**。申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。 + + + +### 意向锁(Intention Lock) + +**意向锁是表级锁,是InnoDB主动加的,不需要手动处理。其目的是为了快速判断表里是否有记录被加锁。** + +- 如果没有**意向锁**,那么加**独占表锁**时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢 +- 如果有了**意向锁**,在对记录加独占锁前,先会加上表级的意向独占锁,那么在加**独占表锁**时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录 + +对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。 + +- 共享锁:`SELECT ... LOCK IN SHARE MODE;` +- 排他锁:`SELECT ... FOR UPDATE;` + + + +#### 意向共享锁(Intention Shared Lock) + +**意向共享锁(IS锁)是指当事务准备在某行记录上加共享锁(S锁)时,需要先在表级别加一个IS锁。** + +**作用**:通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时innodb会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。 + + + +#### 意向排他锁(Intention Exclusive Lock) + +**意向排他锁(IX锁)是指当事务准备在某行记录上加排他锁(X锁)时,需要先在表级别加一个IX锁** + +**作用**:通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加排他锁,那么此时innodb会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。 + + + +### AUTO-INC锁(自增长锁) + +在为某个字段声明 `AUTO_INCREMENT` 属性时,之后可以在插入数据时,可以不指定该字段的值,数据库会自动给该字段赋值递增的值,这主要是通过 `AUTO-INC` 锁实现的。 + +`AUTO-INC` 锁是特殊的表锁机制,锁**不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放**。**在插入数据时,会加一个表级别的 `AUTO-INC` 锁**,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 `AUTO-INC` 锁释放掉。 + +`AUTO-INC` 锁在对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。 + +InnoDB 存储引擎提供了个`innodb_autoinc_lock_mode`的系统变量,是用来控制选择用 `AUTO-INC` 锁,还是**轻量级**的锁。 + +- innodb_autoinc_lock_mode=0:**采用`AUTO-INC`锁** +- innodb_autoinc_lock_mode=2:**采用轻量级锁**。性能最高,但自增长值可能不是连续的,在主从复制场景中是不安全的 +- innodb_autoinc_lock_mode=1:默认值,两种锁混着用 + - 如果能够确定插入记录的数量就采用**轻量级**锁 + - 不确定时就采用 `AUTO-INC` 锁 \ No newline at end of file diff --git a/src/Database/204.md b/src/Database/204.md new file mode 100644 index 0000000..c7ba33b --- /dev/null +++ b/src/Database/204.md @@ -0,0 +1,3 @@ +页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折中的页级锁,**一次锁定相邻的一组记录**。BDB 支持页级锁。 + +**特点:开销和加锁时间界于表锁和行锁之间、会出现死锁、锁定粒度界于表锁和行锁之间、并发度一般。** \ No newline at end of file diff --git a/src/Database/205.md b/src/Database/205.md new file mode 100644 index 0000000..0b516df --- /dev/null +++ b/src/Database/205.md @@ -0,0 +1,168 @@ +行级锁是MySQL中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为: + +- **共享锁** +- **排他锁** + +**特点:开销大、加锁慢、会出现死锁、发生锁冲突的概率最低、并发度也最高。** + + + +在 `InnoDB` 事务中,行锁通过给索引上的**索引项加锁**来实现。只有通过索引条件检索数据,才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和 `排他锁`,以及加锁前需要先获得的 `意向共享锁` 和 `意向排他锁`。行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是**两阶段锁协议**。 + +行锁实现算法(**3种锁都是排它锁(X锁)**): + +- **Record Lock(记录锁)** +- **Gap Lock(间隙锁)** +- **Next-Key Lock(临键锁)** + + + +### 属性锁 + +按照锁的共享策略来分: + +- **共享锁(S锁,Shared Locks)** +- **排它锁(X锁,Exclusive Locks)** + +| 请求锁模式是否兼容当前锁模式 | X锁 | IX锁 | S锁 | IS锁 | +| ---------------------------- | ------ | ------ | ------ | ------ | +| X锁 | `冲突` | `冲突` | `冲突` | `冲突` | +| IX锁 | `冲突` | 兼容 | `冲突` | 兼容 | +| S锁 | `冲突` | `冲突` | 兼容 | 兼容 | +| IS锁 | `冲突` | 兼容 | 兼容 | 兼容 | + +若一个事务请求锁模式与当前锁兼容,InnoDB就将请求锁授予该事务;反之,两者不兼容,该事务就要等待锁释放。 + +#### 共享锁(Shared Locks) + +**共享锁( `S锁 ` 或 `读锁`)是指针对同一份数据,多个读操作可以同时进行而不会互相影响。**若事务T对数据对象A加上**S锁**,则事务T只能读A;其它事务只能再对A加**S锁**,而不能加X锁,直到T释放A上的S锁。加锁方式: + +- `select ... lock in share mode` + +**注意**: + +- 共享锁都是行锁 + + + +#### 排它锁(Exclusive Locks) + +**排它锁( `X锁` 或 `写锁`)是指当前写操作没有完成前,它会阻断其它写锁和读锁。**若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它事务都不能再对A加任何类型的锁,直到T释放A上的锁。加锁方式: + +- `insert` +- `update` +- `delete` +- `select ... for update` + +**注意** + +- 在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁 + +- 排它锁可以是**行锁**也可以是**表锁** + + ```sql + -- 当username是主键时,锁定的是行锁;当username不是主键时,是表锁 + SELECT count(*) as total FROM test WHERE username = "zhangsan" FOR UPDATE + ``` + + + +### 锁实现方式 + +#### Record Lock(记录锁) + +**为单个行记录上的锁,总是会去锁住索引记录**。 + +```sql +-- id 列为主键列或唯一索引列 +SELECT * FROM t_user WHERE id = 1 FOR UPDATE; +``` + +id 为 1 的记录行会被锁住。需要注意: + +- `id` 列必须为`唯一索引列`或`主键列`,否则上述语句加的锁就会变成`临键锁` +- 同时查询语句必须为`精准匹配`(`=`),不能为 `>`、`<`、`like`等,否则也会退化成`临键锁` + +也可以在通过 `主键索引` 与 `唯一索引` 对数据行进行 UPDATE 操作时,也会对该行数据加`记录锁`: + +```sql +-- id 列为主键列或唯一索引列 +UPDATE t_user SET age = 50 WHERE id = 1; +``` + +**注意**:在MySQL中,**行级锁**并不是直接锁记录,而是**锁索引**。索引分为**主键索引**和**非主键索引**两种: + +- 如果一条sql语句操作了**主键索引**,MySQL就会**锁定这条主键索引** +- 如果一条语句操作了**非主键索引**,MySQL会**先锁定该非主键索引**,**再锁定相关的主键索引** + + + +#### Gap Lock(间隙锁) + +间隙锁,想一下幻读的原因,其实就是行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“间隙”。**所以加入间隙锁来解决幻读。** + +**间隙锁**基于`非唯一索引`,它`锁定一段范围内的索引记录`。**间隙锁**基于下面将会提到的`Next-Key Locking` 算法,请务必牢记:**使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据**。 + +```sql +SELECT * FROM t_user WHERE id BETWEN 1 AND 10 FOR UPDATE; +-- 或 +SELECT * FROM t_user WHERE id > 1 AND id < 10 FOR UPDATE; +``` + +即所有在`(1,10)`区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。除了手动加锁外,在执行完某些 `SQL`后,`InnoDB`也会自动加**间隙锁**。 + +**幻读原因**:因为行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“`间隙`”。所以加入间隙锁来解决幻读。 + + + +**间隙锁的目的** + +1. 防止幻读,以满足相关隔离级别的要求 +2. 满足恢复和复制的需要 + +**产生间隙锁的条件(RR事务隔离级别下)** + +1. 使用普通索引锁定 +2. 使用多列唯一索引 +3. 使用唯一索引锁定多行记录 + + + +#### Next-Key Lock(临键锁) + +**Next-Key Lock(Record Lock + Gap Lock)锁定的是一个范围,并且锁定记录本身,MySql 防止幻读就是使用此锁实现。** + +临键锁是一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决`幻读`的问题。每个数据行上的`非唯一索引列`上都会存在一把**临键锁**,当某个事务持有该数据行的**临键锁**时,会锁住一段**左开右闭区间**的数据。需要强调的一点是,`InnoDB` 中`行级锁`是基于索引实现的,**临键锁**只与`非唯一索引列`有关,在`唯一索引列`(包括`主键列`)上不存在**临键锁**。 + +比如:表信息 `t_user(id PK, age KEY, name)` + +![Next-Key-Locks](images/Database/Next-Key-Locks.jpg) + +该表中 `age` 列潜在的`临键锁`有: + +![Next-Key-Locks-临键锁](images/Database/Next-Key-Locks-临键锁.jpg) + +在`事务 A` 中执行如下命令: + +```sql +-- 根据非唯一索引列 UPDATE 某条记录 +UPDATE t_user SET name = Vladimir WHERE age = 24; +-- 或根据非唯一索引列 锁住某条记录 +SELECT * FROM t_user WHERE age = 24 FOR UPDATE; +``` + +不管执行了上述 `SQL` 中的哪一句,之后如果在`事务 B` 中执行以下命令,则该命令会被阻塞: + +```sql +INSERT INTO t_user VALUES(100, 26, 'tian'); +``` + +很明显,`事务 A` 在对 `age` 为 24 的列进行 UPDATE 操作的同时,也获取了 `(24, 32]` 这个区间内的临键锁。 + +不仅如此,在执行以下 SQL 时,也会陷入阻塞等待: + +```sql +INSERT INTO table VALUES(100, 30, 'zhang'); +``` + +那最终我们就可以得知,在根据`非唯一索引` 对记录行进行 `UPDATE`、`FOR UPDATE`、`LOCK IN SHARE MODE` 操作时,InnoDB 会获取该记录行的 `临键锁` ,并同时获取该记录行下一个区间的`间隙锁`。即`事务 A`在执行了上述的 SQL 后,最终被锁住的记录区间为 `(10, 32)`。 \ No newline at end of file diff --git a/src/Database/206.md b/src/Database/206.md new file mode 100644 index 0000000..4686615 --- /dev/null +++ b/src/Database/206.md @@ -0,0 +1,21 @@ +### 乐观锁(Optimistic Lock) + +乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。 + +乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号或时间戳。乐观锁的两种实现方式: + +- **使用数据版本(version)记录对比机制** + + 当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。 + +- **使用时间戳(timestamp)记录对比机制** + + 在更新提交时检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,一致则OK,否则为版本冲突。 + + + +### 悲观锁(Pessimistic Lock) + +悲观锁(**一锁二查三更新**)的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。**在数据库上的悲观锁需要数据库本身提供支持,即通过常用的 `select … for update` 操作来实现悲观锁。** + +MySQL还有个问题是 `select... for update` 语句执行中,如果数据表没有添加索引或主键,所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描。 \ No newline at end of file diff --git a/src/Database/207.md b/src/Database/207.md new file mode 100644 index 0000000..0178ff1 --- /dev/null +++ b/src/Database/207.md @@ -0,0 +1,93 @@ +### 死锁 + +**MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在InnoDB中,锁是逐步获得的,就造成了死锁的可能。** + +当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” `Deadlock found when trying to get lock...`”。 + +#### 避免死锁 + +三种常见的避免死锁方式: + +- 如果不同程序会并发存取多个表,尽量**约定以相同的顺序访问表**,可以大大降低死锁机会 +- 在同一个事务中,尽可能做到**一次锁定所需要的所有资源**,减少死锁产生概率 +- 对于非常容易产生死锁的业务部分,可以尝试使用**升级锁定颗粒度**,通过表级锁定来减少死锁产生的概率 + + + +#### 预防死锁 + +- `innodb_lock_wait_timeout` **等待锁超时回滚事务** + + 直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另一个事务就能继续执行。 + +- `wait-for graph`**算法来主动进行死锁检测** + + 每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法都会被触发。wait-for graph要求数据库保存以下两种信息: + + - 锁的信息链表 + - 事务等待链表 + + + +#### 解决死锁 + +- **等待事务超时,主动回滚** +- **进行死锁检查,主动回滚某条事务,让别的事务能继续走下去** + +下面提供一种方法,解决死锁的状态: + +```sql +-- 查看正在被锁的事务 +SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; +``` + +![解决死锁](images/Database/解决死锁.jpg) + +```sql +--上图trx_mysql_thread_id列的值 +kill trx_mysql_thread_id; +``` + + + +### 脏读 + +**脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据**。 + +例如:T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 + +![img](images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg) + + + +### 不可重复读 + +**不可重复读指的是同一事务内多次读取同一数据集合,读取到的数据是不一样的情况**。 + +例如:T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 + +![img](images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg) + +在 InnoDB 存储引擎中: + +- `SELECT`:操作的不可重复读问题通过 MVCC 得到了解决的 +- `UPDATE/DELETE`:操作的不可重复读问题是通过 Record Lock 解决的 +- `INSERT`:操作的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的 + + + +### 幻读 + +**幻读是指在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行**。幻影读是一种特殊的不可重复读问题。 + + + +### 丢失更新 + +**一个事务的更新操作会被另一个事务的更新操作所覆盖**。 + +例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 + +![img](images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg) + +这类型问题可以通过给 SELECT 操作加上排他锁来解决,不过这可能会引入性能问题,具体使用要视业务场景而定。 \ No newline at end of file diff --git a/src/Database/3.md b/src/Database/3.md new file mode 100644 index 0000000..85ccb00 --- /dev/null +++ b/src/Database/3.md @@ -0,0 +1,35 @@ +**在满足1NF和2NF的前提下,非主键列之间不存在间接依赖关系**(消除传递函数依赖)。 + +![img](images/Database/3NF.png) + +案例如下: + +**学生表** + +| 学生编号 | 学生名称 | 班级编号 | 班级名字 | +| -------- | -------- | -------- | --------- | +| S1 | 小王 | 001 | 计算机1班 | +| S2 | 小马 | 003 | 计算机3班 | + +学生编号作为主键满足第二范式(2NF)。通过学生编号 ⇒⇒ 班级编号 ⇒⇒ 班级名字,所以班级编号和班级名字之间存在依赖关系,改为: + +**学生表** + +| 学生编号 | 学生名称 | 班级编号 | +| -------- | -------- | -------- | +| S1 | 小王 | 001 | +| S2 | 小马 | 003 | + +**班级表** + +| 班级编号 | 班级名称 | +| -------- | --------- | +| 001 | 计算机1班 | +| 003 | 计算机3班 | + +**好处** + +- 相对节约空间 +- 解决更新繁琐 +- 解决插入异常,当班级分配了老师,还没分配学生的时候,原表将不可插入数据 +- 解决删除异常,当学生毕业后,若要清空学生信息,班级和老师的关系可能会丢失 \ No newline at end of file diff --git a/src/Database/301.md b/src/Database/301.md new file mode 100644 index 0000000..8958d7c --- /dev/null +++ b/src/Database/301.md @@ -0,0 +1,3 @@ +**什么叫事务?** + +事务是一系列对系统中数据进行访问与更新的操作组成的一个程序逻辑单元。即不可分割的许多基础数据库操作。 \ No newline at end of file diff --git a/src/Database/302.md b/src/Database/302.md new file mode 100644 index 0000000..32a9e5d --- /dev/null +++ b/src/Database/302.md @@ -0,0 +1,106 @@ +### 原子性(Atomicity) + +**原子性是指一个事务(事务是最小的执行单位)是一个不可分割的工作单元,其中的操作要么都做,要么都不做**。如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。 + + + +**实现原理** + +**① 回滚日志(undo log)** + +InnoDB实现回滚靠的是undo log。当事务对数据库进行修改时,InnoDB会生成对应的undo log。如果事务执行失败或调用了rollback,导致事务需要回滚,便可利用undo log中的信息将数据回滚到修改前。 + +- 对于每个`insert`,回滚时会执行`delete` +- 对于每个 `delete`,回滚时会执行`insert` +- 对于每个 `update`,回滚时会执行一个相反的 `update`,把数据改回去 + + + +### 一致性(Consistency) + +**一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。** + +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。 + + + +**实现原理** + +一致性是事务追求的最终目标。前面提到的**原子性、持久性和隔离性**,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措施包括: + +**① 保证原子性、持久性和隔离性**。如果这些特性无法保证,事务的一致性也无法保证 + +**② 数据库本身提供保障**。如不允许向整型列插入字符串值、字符串长度不能超过列的限制等 + +**③ 应用层面进行保障**。如转账操作只扣除转账者余额,而未增加接收者余额 + + + +### 隔离性(Isolation) + +**隔离性是指事务内部的操作与其它事务是隔离的,并发执行的各个事务之间不能互相干扰。**严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。 + + + +**实现原理** + +隔离性追求的是并发情形下事务之间互不干扰。主要分为两个方面: + +**① 加锁机制保证隔离性**:(一个事务)写操作对(另一个事务)写操作的影响 + +事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其它事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。 + +**② MVCC(多版本并发控制)保证隔离性**:(一个事务)写操作对(另一个事务)读操作的影响 + +MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。最大优点是读不加锁,因此读写不冲突,并发性能好。InnoDB的MVCC实现了多个版本的数据可共存,主要基于以下技术及数据结构: + +- **隐藏列**:在Innodb引擎中每行数据都会有两个隐藏列(实际是三个列) + - **隐藏id**(`id`,如果建表时没有显式指定,则会生成这个隐藏id作为主键,实际和mvcc没有关系) + - **创建版本号**(`data_trx_id`,事务id):用来标识最近对本行记录做修改的事务 id + - **回滚指针**(`data_roll_pointer`,指向undo log的指针) +- **基于undo log版本链**:每条undo log也会指向更早版本的undo log,从而形成一条版本链 +- **ReadView**:**通过隐藏列和版本链可以将数据恢复到指定版本,但具体要恢复到哪个版本,则需要根据ReadView来确定。**当进行查询操作时,事务会生成一个ReadView(是一个事务快照),准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见的。在每一条 SQL 开始的时候被创建,有几个重要属性: + - **trx_ids:** 当前系统活跃(未提交)事务版本号集合 + - **low_limit_id:** 创建当前 read view 时“当前系统最大**事务版本号**+1” + - **up_limit_id:** 创建当前read view 时“系统正处于**活跃事务**最小版本号” + - **creator_trx_id:** 创建当前read view的事务版本号 + + + +**MVCC查询流程** + +现在开始查询,一个 select 过来了,找到了一行数据。 + +- **data_trx_id < up_limit_id**:说明数据在当前事务之前就存在了,显示 +- **data_trx_id >= low_limit_id**:说明该数据是在当前read view 创建后才产生的,数据不显示。不显示怎么办,根据 data_roll_pointer 从 undo log 中找到历史版本,找不到就空 +- **up_limit_id < data_trx_id < low_limit_id**:就要看隔离级别了 + + + +**MVCC应用场景** + +在Mysql的InnoDB引擎中,只有**已提交读**和**可重复读**这两种隔离级别的事务采用了MVCC机制: + +- **已提交读(READ COMMITTD)**:事务中的每次读操作都会生成一个新的ReadView,也就是说如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据 +- **可重复读(REPEATABLE READ)**:事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了 + + + +### 持久性(Durability) + +**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的,即使数据库发生故障也不受影响。** + + + +**实现原理** + +**① redo log** + +当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘(刷脏页)。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。 + + + +**为什么redo log写入磁盘比直接将Buffer Pool中修改数据写入磁盘(刷脏)快?** + +- 刷脏是随机I/O,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序I/O +- 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效I/O大大减少 \ No newline at end of file diff --git a/src/Database/303.md b/src/Database/303.md new file mode 100644 index 0000000..4e0fa3e --- /dev/null +++ b/src/Database/303.md @@ -0,0 +1,90 @@ +**事务并发问题** + +在事务的并发操作中,不做隔离操作则可能会出现 **脏读、不可重复读、幻读** 问题: + +- **脏读**:**指一个事务读取到了另一个未提交事务修改过的数据** + + 事务A中读到了事务B中未提交的更新数据内容,然后B回滚操作,那么A读取到的数据是脏数据。 + +- **不可重复读**:**同一个事务内,前后多次读取,读取到的数据内容不一致** + + 事务A读到事务B已经提交后的数据,即事务A多次读取同一数据时,返回结果不一致。 + +- **幻读**:**指一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录(如insert、delete、update),再次查询出的结果则出现不一致** + + 事物A执行select后,事物B增或删了一条数据,事务A再执行同一条SQL后发现多或少了一条数据。 + +- **第一类丢失更新:** A事务撤销事务时,覆盖了B事务提交的事务(现代关系型数据库中已经不会发生) + +- **第二类丢失更新:** A事务提交事务时,覆盖了B事务提交的事务(是不可重复读的特殊情况) + +**小结**:不可重复读的和幻读很容易混淆,**不可重复读**侧重于**修改**,**幻读**侧重于**新增或删除**。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。 + + + +**InnoDB存储引擎下**的四种隔离级别发生问题的可能性如下: + +| 隔离级别 | 第一类丢失更新 | 第二类丢失更新 | 脏读 | 不可重复读 | 幻读 | +| -------------------------- | -------------- | -------------- | -------- | ---------- | -------- | +| Read Uncommitted(读未提交) | 不可能 | **可能** | **可能** | **可能** | **可能** | +| Read Committed(读已提交) | 不可能 | **可能** | 不可能 | **可能** | **可能** | +| Repeatable Read(可重复读) | 不可能 | 不可能 | 不可能 | 不可能 | **可能** | +| Serializable(串行化) | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 | + + + +### Read Uncommitted(读未提交) + +只限制了两个数据**不能同时修改**,但即使事务未提交也会**读取到其它事务未提交的内容(可能会被回滚)**。会有**脏读、重复读、幻读**的问题,读取未提交的数据,也被称之为**脏读(Dirty Read)**。 + +**特点**:最低级别,任何情况都无法保证 + +**数据库锁情况** + +- 读取数据:**未加锁,每次都读到最新数据,性能最好** +- 写入数据:**只对数据增加行级共享锁,写完释放** + + + +### Read Committed(读已提交) + +当前事务只能读取到其它事务**已提交**的数据。因同一事务的其它实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果,这就是所谓的**不可重复读(Nonrepeatable Read)**。该隔离级别**解决了脏读**问题,但还是会存在**重复读、幻读**问题。 + +**特点**:避免脏读 + +**脏读解决方案**:基于乐观锁理论的MVCC(多版本并发控)实现 + +**数据库锁情况** + +- 读取数据:**加行级共享锁(读到时才加锁),读完后立即释放** +- 写入数据:**在更新时的瞬间对其加行级排它锁,直到事务结束才释放** + + + +### Repeatable Read(可重复读) + +限制了读取数据时**不可以进行修改**,所以**解决了不能重复读**的问题。但是读取范围数据的时候,是可以插入或删除数据,所以还会存在**幻读(Phantom Read)**问题。 + +**幻读** 是户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当该用户再读取该范围的数据行时,会发现有新的“幻影” 行。 + +**特点**:避免脏读、不可重复读。MySQL默认事务隔离级别 + +**不可重复读解决方案**:基于乐观锁理论的MVCC(多版本并发控)实现 + +**数据库锁情况** + +- 读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放** +- 写入数据:**开始更新的瞬间对其增加行级排他锁,直到事务结束才释放** + + + +### Serializable(可串行化) + +所有事务都是进行**串行化顺序**执行的。可以避免**脏读**、**不可重复读**与**幻读**所有并发问题。但该事务隔离级别下,事务执行很耗性能。 + +**特点**:避免脏读、不可重复读、幻读 + +**数据库锁情况** + +- 读取数据:**先对其加表级共享锁 ,直到事务结束才释放** +- 写入数据:**先对其加表级排他锁 ,直到事务结束才释放** \ No newline at end of file diff --git a/src/Database/304.md b/src/Database/304.md new file mode 100644 index 0000000..f0983ce --- /dev/null +++ b/src/Database/304.md @@ -0,0 +1,112 @@ +### 实现方式 + +在Spring中事务有两种实现方式: + +- **编程式事务管理**: 编程式事务管理使用`TransactionTemplate`或直接使用底层的`PlatformTransactionManager` +- **声明式事务管理**: 建立在`AOP`之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码,通过`@Transactional`就可以进行事务操作,更快捷而且简单 + + + +### 提交方式 + +**默认情况下,数据库处于自动提交模式**。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。 +对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,Spring会将底层连接的自动提交特性设置为false。也就是在使用Spring进行事物管理的时候,Spring会将是否自动提交设置为false,等价于JDBC中的 `connection.setAutoCommit(false);`,在执行完之后在进行提交,`connection.commit();` 。 + + + +### 事务隔离级别 + +隔离级别是指若干个并发的事务之间的隔离程度。 + +```java +@Transactional(isolation = Isolation.READ_UNCOMMITTED) +public void addGoods(){ + ...... +} +``` + +枚举类Isolation中定义了五种隔离级别: + +- `DEFAULT`:默认值。表示使用底层数据库的默认隔离级别。对大部分数据库,通常这值就是**READ_COMMITTED** +- `READ_UNCOMMITTED`:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别 +- `READ_COMMITTED`:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 +- `REPEATABLE_READ`:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读 +- `SERIALIZABLE`:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 + + + +### 事务传播行为 + +事务的传播性一般用在事务嵌套的场景,如一个事务方法里面调用了另外一个事务方法,那两个方法是各自作为独立的方法提交还是内层事务合并到外层事务一起提交,这就需要事务传播机制配置来确定怎么样执行。 + +```java +@Transactional(propagation=Propagation.REQUIRED) +public void addGoods(){ + ...... +} +``` + +枚举类Propagation中定义了七种事务传播机制如下: + +- `REQUIRED`(required,要求,Spring默认):**当前存在事务,则加入该事务;当前没有事务,则创建一个新的事务** +- `REQUIRES_NEW`(requires_new,要求新的):**创建一个新事务,如果存在当前事务,则挂起该事务** +- `SUPPORTS`(supports,支持):**如果当前存在事务,则加入当前事务;如果当前没有事务,就以非事务方法执行** +- `NOT_SUPPORTED`(not_supported,不支持):**始终以非事务方式执行,如果当前存在事务,则挂起当前事务** +- `NEVER`(never,都不):**不使用事务,如果当前事务存在,则抛出异常** +- `MANDATORY`(mandatory,强制):**如果当前存在事务,则加入当前事务;如果当前事务不存在,则抛出异常** +- `NESTED`(nested,嵌套)**如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)** + + + +### 事务回滚规则 + +指示Spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。Spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。 +默认配置下,Spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。 +可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。 + + + +### 事务常用配置 + +- **readOnly** + + 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) + +- **rollbackFor** + + 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) + +- **rollbackForClassName** + + 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”}) + +- **noRollbackFor** + + 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) + +- **noRollbackForClassName** + + 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”}) + +- **propagation** + + 该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) + +- **isolation** + + 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 + +- **timeout** + + 该属性用于设置事务的超时秒数,默认值为-1表示永不超时 + + + +### 失效场景 + +- **@Transactional 应用在非 public 修饰的方法上** +- **数据库引擎要不支持事务** +- **由于propagation 设置错误,导致注解失效** +- **rollbackFor 设置错误,@Transactional 注解失效** +- **方法之间的互相调用也会导致@Transactional失效** +- **异常被你的 catch“吃了”导致@Transactional失效** \ No newline at end of file diff --git a/src/Database/4.md b/src/Database/4.md new file mode 100644 index 0000000..43e8dc7 --- /dev/null +++ b/src/Database/4.md @@ -0,0 +1,41 @@ +在3NF基础上,**主属性之间不存在部分或传递依赖**。例如: + +**配件表** + +| 仓库号 | 配件号 | 职工号 | 配件数量 | +| ------ | ------ | ------ | -------- | +| W1 | P1 | E1 | 10 | +| W1 | P2 | E1 | 10 | +| W2 | P1 | E2 | 20 | + +有以下约束: + +- 一个仓库有多个职工 +- 一个职工只在一个仓库 +- 一种配件可以放多个仓库 +- 一个仓库,一个职工管理多个配件,一种配件由唯一一个职工管理 + +由此,将(仓库号,配件号)作为主键,满足 3NF,但是(仓库号,配件号)⇒⇒ 职工号 ⇒⇒ 仓库号,造成传递函数依赖,改为: + +**仓库表** + +| 仓库号 | 职工号 | +| ------ | ------ | +| W1 | E1 | +| W2 | E2 | + +**工作表** + +| 职工号 | 配件号 | 配件数量 | +| ------ | ------ | -------- | +| E1 | P1 | 10 | +| E1 | P2 | 10 | +| E2 | P1 | 20 | + +**好处** + +- 解决一些冗余和一些异常情况 + +**不足** + +- 丢失一些函数依赖,如丢失(仓库号,配件号)⇒⇒ 职工号,无法通过单表来确定一个职工号 \ No newline at end of file diff --git a/src/Database/401.md b/src/Database/401.md new file mode 100644 index 0000000..f5d180f --- /dev/null +++ b/src/Database/401.md @@ -0,0 +1,49 @@ +InnoDB存储引擎是多线程的模型,所以犹太有多个不同的后台线程,负责处理不同的任务,主要有:`Master Thread`、`I/O Thread`、`Purge Thread`、`Page Cleaner Thread` 四种。 + +### Master Thread + +**核心后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(Insert Buffer)、回滚页(UNDO PAGE)的回收等。** + + + +### I/O Thread + +**在InnoDB存储引擎中大量使用AIO来处理IO请求,而I/O Thread主要负责处理这些 I/O 请求的回调(call back)处理。**这里共有四类I/O线程,分别是:`insert buffer thread`、`log thread`、`read thread`、`write thread`。 + +```sql +mysql> show variables like "%innodb%io_threads%"; ++-------------------------+-------+ +| Variable_name | Value | ++-------------------------+-------+ +| innodb_read_io_threads | 4 | +| innodb_write_io_threads | 4 | ++-------------------------+-------+ +``` + + + +### Purge Thread + +**purge thread线程用来回收事务提交后其被分配的undo页,默认是开启的,可以通过修改配置文件来配置多个Purge Thread线程。** + +```sql +mysql> show variables like "%purge_threads%"; ++----------------------+-------+ +| Variable_name | Value | ++----------------------+-------+ +| innodb_purge_threads | 4 | ++----------------------+-------+ +1 row in set (0.00 sec) +``` + +在提交了一个事务给数据库时,为了保证事务能够回滚,会在内存中缓存下该事务未执行时的状态(undo 日志),如果事务执行不成功,那么就恢复之前的状态;如果执行成功,undo日志就用不着了,要把它删除以腾出空间。Purge 线程就是做这个删除工作的。默认有4个线程。 + + + +### Page Cleaner Thread + +**用于多版本控制功能中回收delete和update操作产生的脏页,用来执行将脏页刷新到磁盘。** + +MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理性能。设置命令,比如: + +`SET GLOBAL innodb_page_cleaner = 3` \ No newline at end of file diff --git a/src/Database/402.md b/src/Database/402.md new file mode 100644 index 0000000..170c538 --- /dev/null +++ b/src/Database/402.md @@ -0,0 +1,93 @@ +数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。通常情况下,单个数据页默认的大小是`16kb`。当然也可以通过参数 `innodb_page_size` 来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下: + +![InnoDB页结构示意图](images/Database/InnoDB页结构示意图.jpg) + +![InnoDB页各组成部分简单描述](images/Database/InnoDB页各组成部分简单描述.jpg) + +### File Header(文件头) + +**用于记录页(Page)的信息**,如页类型、上一页和下一页等,占固定的 `38byte`。重要字段结构如下: + +- `FIL_PAGE_SPACE_OR_CHKSUM`:**校验和**。为了快速比较、保证数据的完整性防止遭到破坏等 + +- `FIL_PAGE_OFFSET`:**页号**。InnoDB通过页号来可以唯一定位一个页 + +- `FIL_PAGE_TYPE`:**页的类型**。InnoDB为了不同的目的而把页分为不同的类型 + +- `FIL_PAGE_PREV、FIL_PAGE_NEXT`:分别指向**上一页**和**下一页** + + + +### Page Header(页头) + +**存储页内的一些状态和汇总信息**,如本页有多少条记录等,占固定的 `56byte`。重要字段结构如下: + +- `PAGE_N_DIR_SLOTS`:**页内槽的个数**,其占用`2byte`。新建空数据页初值为2,分别指向Infimum最小记录、Supremum最大记录 + +- `PAGE_HEAP_TOP`:**第一条记录地址** + +- `PAGE_N_HEAP`:**页内记录数**,含最大最小记录及标记删除的记录 + + + +### Infimun+Supremum Records + +**为了快速找到最大或最小记录,在保存用户记录时,数据库会自动创建两条额外的记录,最大记录保存到Supremum记录中,最小记录保存在Infimum记录中**。如下图所示: + +![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg) + + + +### User Records(用户记录) + +**用来存储用户插入的数据记录**。对于新申请的数据页,用户记录是空的。当插入数据时,Innodb会将一部分`空闲空间`分配给用户记录。我们平时保存到数据库中的数据,就存储在它里面。Innodb支持的数据行格式有四种: + +- compact行格式 +- redundant行格式 +- dynamic行格式 +- compressed行格式 + + + +以compact行格式为例: + +![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg) + +一条用户记录主要包含三部分内容: + +- **记录额外信息**:它包含了变长字段、null值列表和记录头信息 + + **记录头信息**用于描述一些特殊的属性。它主要包含: + + - deleted_flag:即删除标记,用于标记该记录是否被删除了 + - min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记 + - n_owned:即拥有的记录数,记录该组索引记录的条数 + - heap_no:即堆上的位置,它表示当前记录在堆上的位置 + - record_type:即记录类型,其中0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录 + - next_record:即下一条记录的位置 + +- **隐藏列**:它包含了行id(`db_row_id`)、事务id(`db_trx_id`)和回滚点(`db_roll_ptr`) + +- **真正的数据列**:包含真正的用户数据,可以有很多列 + + + +### Free Space(空闲空间) + +**为页面的剩余空间**。User Records部分从上往下使用剩余空间,而Page Directory则从下往上使用剩余空间。 + +![FreeSpace剩余空间](images/Database/FreeSpace剩余空间.jpg) + + + +### Page Directory(页目录) + +**为了在单页中能快速查找到对应的记录(最坏情况为全页扫描),把一页用户记录分为若干组,每一组的最大记录都保存到`页目录`,每一组的最大记录叫做`槽`,然后就能通过二分查找进行快速定位记录**。所下图所示: + +![InnoDB-页目录](images/Database/InnoDB-页目录.jpg) + + + +### File Trailer(文件结尾信息) + +用于检验当前页的完整性,主要记录了页面的`校验和(checksum)`。具体地其占用 `8byte`,前`4byte`为校验和(checksum),后`4byte`为页面被最后修改时相应的日志序列位置(LSN)。 \ No newline at end of file diff --git a/src/Database/403.md b/src/Database/403.md new file mode 100644 index 0000000..a641a19 --- /dev/null +++ b/src/Database/403.md @@ -0,0 +1,7 @@ +### compact行格式 + +### redundant行格式 + +### dynamic行格式 + +### compressed行格式 \ No newline at end of file diff --git a/src/Database/404.md b/src/Database/404.md new file mode 100644 index 0000000..dd5a3f9 --- /dev/null +++ b/src/Database/404.md @@ -0,0 +1,158 @@ +### Buffer Pool + +`InnoDB `为了解决磁盘 `I/O` 频繁操作问题,`MySQL `需要申请一块内存空间,这块内存空间称为`Buffer Pool`。 + +![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png) + +#### 缓存页 + +`MySQL`数据是以页为单位,每页默认`16KB`,称为**数据页**。在`Buffer Pool`里面会划分出若干个**缓存页**与**数据页**对应。 + +![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png) + + + +#### 描述数据 + +每个**缓存页**会有对应的一份**描述数据**(一一对应),里面存储了**缓存页的元数据信息**,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等。可用于**缓存页**直接映射到对应的**数据页**。每个描述数据默认为 `800Byte`。 + +![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png) + +后续对数据的增删改查都是在`Buffer Pool`里操作 + +- 查询:从磁盘加载到缓存,后续直接查缓存 +- 插入:直接写入缓存 +- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新 + +直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。 + + + +#### 缓存页哈希表 + +查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。 + +![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png) + +所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。 + +![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png) + +**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。 + +![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png) + +后续的查询,就可以通过**缓存页哈希表**路由定位了。 + + + +#### Free链表 + +**Free链表可以帮助我们快速找到空闲的缓存页**。当执行增删改查时,需要从数据页加载数据,然后从free链表(双向链表)中找到空闲的缓存页。把数据页的表空间号和数据页号写入描述信息块,加载数据到缓存页后,会把缓存页对应的描述信息块从free链表中移除。`Free`链表设计: + +- 新增**`free`基础节点** +- **描述数据**添加**`free`节点指针** + +![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png) + + + +#### Flush链表 + +**Free链表可以帮助我们快速找到脏页的缓存页**。当空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性。但不会每次把`Buffer Pool`里所有缓存页都刷入磁盘,因为磁盘`I/O`开销太大,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可哪些缓存页是**脏页**?参照`free`链表设计`flush`链表,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。`Flush`链表设计: + +- 新增**`flush`基础节点** +- **描述数据**添加**`flush`节点指针** + +![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png) + +后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。 + + + +#### LRU链表 + +`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。为了解决该问题,需要淘汰缓存页,腾出空闲缓存页。这里借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。`LRU`链表设计: + +- 新增**`LRU`基础节点** +- **描述数据**添加**`LRU`节点指针** + +![LRU链表设计思路](images/Database/LRU链表设计思路.png) + +实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。 + + + +**LRU链表优化** + +缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰: + +**① 预读机制** + +InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。 + +预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。 + +![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png) + +**② 全表扫描** + +如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。 + +![图片](images/Database/819dbbcd31605b3a692576932f25d325.png) + +**解决方案** + +为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。**以7:3为例,young区域70%,old区域30%** + +![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png) + +如上图所示,数据页第一次加载进缓存页的时候,是先放入冷数据区域的头部,如果1秒后再次访问缓存页,则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。 + +- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头 + +- `young`区域前面`1/4`被访问不会移动到链表头部,只有后面的`3/4`被访问了才会 + +记住是按照某个比例将`LRU`链表分成两部分,不是某些节点固定是`young`区域的,某些节点固定是`old`区域的,随着程序的运行,某个节点所属的区域也可能发生变化。 + +- InnoDB在LRU列表中引入了midpoint参数。新读取的页并不会直接放在LRU列表的首部,而是放在LRU列表的midpoint位置,即 innodb_old_blocks_pct这个点的设置。默认是37%,最小是5,最大是95;如果内存比较大的话,可以将这个数值调低,通常会调成20,也就是说20%的是冷数据块。目的是为了保护热区数据不被刷出内存。 +- InnoDB还引入了innodb_old_blocks_time参数,控制成为热数据的所需时间,默认是1000ms,也就是1s,也就是数据在1s内没有被刷走,就调入热区。 + + + +### Change Buffer + +**可变缓冲区(Change Buffer)**存在于内存中,当对辅助索引(secondary index) 进行DML操作时,Buffer Pool没有其相应的Page,会将这些变更缓存到Change Buffer中。当Change Buffer里的Page被read的时候,会被合并到Buffer Pool中。**当脏页超过一定比例时,会将其flush磁盘中。** Change Buffer的内存默认占Buffer Pool的25%。 + +原本修改Buffer Pool不存在的Page需要先从磁盘读取(一次I/O操作)到内存,然后写redo log。引入Change Buffer后,会先缓存在Change Buffer,然后写redo log。**由于Change Buffer的存在,避免了从磁盘读取辅助索引到缓冲池所需的大量随机访问I/O。** + + + +**应用场景** + +**① 适合写多读少场景** + +对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 Change Buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。 + +**② 不适合写后立即查询场景** + +写入之后马上会做查询,那么即使满足了条件,将更新先记录在 Change Buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 I/O 的次数不会减少,反而增加了 Change Buffer 的维护代价。所以,对于这种业务模式来说,Change Buffer 反而起到了副作用。 + + + +### 自适应哈希索引 + +**自适应哈希索引(Adaptive Hash Index)**对InnoDB在Buffer Pool的查询有很大的优化。针对Buffer Pool中**热点页数据**,构建索引(一般使用索引键的前缀构建哈希索引)。因为Hash索引的**等值查询**效率远高于B+Tree,所以当查询命中Hash,就能很快返回结果,不用再去遍历B+Tree。 + + + +### Log Buffer + +**日志缓冲区(Log Buffer)**用来存储那些即将被**刷入到磁盘**文件中的日志(如RedoLog)。默认为16MB。参数设置如下: + +- `innodb_log_buffer_size`:设置大小 +- `innodb_flush_log_at_trx_commit`:控制如何将日志缓冲区的内容写入并刷新到磁盘 + - `0`:日志每秒刷新到磁盘。 未刷新日志的事务会在mysql崩溃中丢失 + - `1`:为默认值。每次提交事务时,写入并刷新日志到磁盘 + - `2`:每次提交事务后写入日志,并每秒刷新一次磁盘。 未刷新日志的事务可能会在mysql崩溃中丢失 +- `innodb_flush_log_at_timeout`:控制日志刷新频率。每几秒刷新日志,取值范围 [1,2700] (second) \ No newline at end of file diff --git a/src/Database/405.md b/src/Database/405.md new file mode 100644 index 0000000..495876f --- /dev/null +++ b/src/Database/405.md @@ -0,0 +1,79 @@ +### 表空间(Tablespaces) + +#### 系统表空间 + +**系统表空间(The System Tablespace)是`数据字典`、`双写缓冲区(Doublewrite Buffer)`、`Change buffer`和`Undo Logs`的储存区域**。该空间的数据文件通过参数`innodb_data_file_path`控制,默认值是`ibdata1:12M:autoextend`(文件名为ibdata1,大小略大于12MB,自动扩展)。8.0之后InnoDB将元数据存在该区域的数据字典中(data dictionary)。 + + + +#### 独占表空间 + +**独占表空间(File-Per-Table Tablespaces)**默认开启,为每个表都独立建一个.ibd文件。 通过参数`innodb_file_per_tabl` 可以设置关闭,这样的话所有表数据是都存在The System Tablespace的ibdata。 + + + +#### 通用表空间 + +**通用表空间(General Tablespaces)**是通过`CREATE TABLESPACE`创建的共享表空间。通用表空间可以创建于MySQL数据目录外的其他表空间,其可以容纳多张表,且其支持所有的行格式。 + + + +#### 撤销表空间 + +**撤销表空间(Undo Log Tablespaces)**保存的是undo log ,用于回滚事务。undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括select) 。用来保证在必要时实现回滚,如果另一个事务需要在一致性读操作中查看原始数据,则从undo日志记录中检索未修改的数据,也就是说MVCC机制也依赖于undo log来实现。在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,属于逻辑格式的日志。 + + + +#### 临时表空间 + +**临时表空间(Temporary Tablespaces)**存储临时表的数据,包括用户创建的临时表和磁盘的内部临时表。对应数据目录下的ibtmp1文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。 + + + +### 数据字典(Data Dictionary) + +**Data Dictironary(DD,数据字典)是有关数据库对象的合集**,例如表、视图、索引等,可以看做是数据库的元信息。换句话说,数据字典存储了有关表结构的信息,每个表具有的列,表的索引等 + +InnoDB数据字典由内部系统表组成,这些表包含用于**查找表、索引和表字段**等对象的元数据。元数据物理上位于InnoDB系统表空间中。由于历史原因,数据字典元数据在一定程度上与InnoDB表元数据文件(.frm文件)中存储的信息重叠。 + + + +### 双写缓冲区(Doublewrite Buffer) + +**Doublewrite Buffer机制极大的保障了Innodb引擎的数据安全性。尽管出现了宕机坏页的状况,也能够从Doublewrite Buffer读取正常页来恢复**。Doublewrite缓冲区是一个存储区域,InnoDB在将页面写入InnoDB数据文件中的适当位置以前,会在其中写入从缓冲池中刷新的页面。若是在页面写入过程当中发生操做系统,存储子系统或mysqld进程崩溃,则InnoDB能够在崩溃恢复期间从Doublewrite缓冲区中找到该页面的良好副本。 + +在MySQL 8.0.20以前,Doublewrite缓冲区存储区位于InnoDB系统表空间中。从MySQL 8.0.20开始,Doublewrite缓冲区存储区位于Doublewrite文件中。从MySQL 8.0.20开始,默认会建立2个Doublewrite Buffer文件。 + + + +**为何有了redo,还要Doublewrite Buffer机制?数据库双写的好处是什么?** +Doublewrite Buffer机制主要是更大的保障了数据页的可靠性。**主要是解决部分写失效的问题。**好比16KB的页,只写了前面4KB,以后就发生宕机了,这种状况被称为部分写失效。针对部分写失效的问题,redo重作日志也不能解决这个问题。 + + + +### 重做日志(Redo Log) + +重作日志是基于磁盘的数据结构,主要做用是在崩溃恢复期间用于纠正不完整事务写入的数据。在正常操做期间,重作日志对更改表数据的请求进行编码记录,这些请求是由SQL语句或低级API调用引发的。在初始化期间以及接受链接以前,会自动重播未完成意外关闭以前未完成更新数据文件的修改。默认状况下,redo log会自动生成2个文件:`ib_logfile0`和`ib_logfile1` 。 + + + +**WAL机制** +WAL 的全称是 Write-Ahead Logging,中文称**预写式日志**,是一种数据安全写入机制。就是**先写日志,而后在写入磁盘,这样保证数据的安全性**。Mysql中的Redo Log就是采用WAL机制。(这里的写日志因为是顺序写,因此不会成为性能瓶颈。) + +**WAL做用** +Mysql中若是为了保证数据的持久性,在每提交一个事务就将日志刷新到磁盘上,这样效率就过低了,严重影响性能,因此就有了Write-Ahead 。 +**Write-Ahead工作机制** +先在内存中提交事务,而后写日志(在InnoDB中就是Redo Log,日志是为了防止宕机致使内存数据丢失),而后再后台任务中把内存中的数据异步刷到磁盘。 + + + +### 撤销日志(Undo Logs) + +回滚日志主要是为了支持事务回滚功能。默认会生成2个回滚日志,保存在undo tablespaces,默认状况下就在数据目录下:`undo_001`和`undo_002`。 + +一个事务最多能够分配四个撤消日志,如下每种操做类型均可以分配一个: + +- 对用户自定义表执行插入操做 +- 对用户自定义表执行删除和更新操做 +- 对用户自定义的临时表执行插入操做 +- 对用户自定义的临时表执行删除和更新操做 \ No newline at end of file diff --git a/src/Database/406.md b/src/Database/406.md new file mode 100644 index 0000000..1a75129 --- /dev/null +++ b/src/Database/406.md @@ -0,0 +1,77 @@ +`InnoDB`支持`3`种常见索引: + +- `B+ `树索引 +- 全文索引 +- 哈希索引 + +### B+树索引 + +**B+树是为磁盘或其它直接存取辅助设备涉及的一种近似平衡的查找树**。在InnoDB存储引擎中,B+树索引**根据键值类型**可以分为**聚集索引**和**非聚集索引**。 + +#### 聚集索引(主键索引) + +**聚集索引(Clustered index)**的**非叶子节点**存放**索引页**,**叶子节点**存放**数据页**,数据页中的所有的行记录都是按照键值递增方式顺序的存放在同一层的叶子结点上,各叶子结点通过前驱后继两个指针串联成一个双向循环链表(数据页本身就是聚集索引的一部分)。 +![Innodb-聚集索引](images/Database/Innodb-聚集索引.png) + +InnoDB存储引擎在创建表的过程中就会根据主键自动构造一棵B+树索引,其高度一般是2~4层,故需要2~4次的查询才能找到对应的数据页。聚集索引使用时,根据主键在B+树中依次比较,直到找到底层对应的数据页(叶子节点),然后通过**二分查找**的方式在数据页中**定位到目标行**,故其对针对主键的排序查找和范围查找的速度是特别快的。 + + + +#### 非聚集索引(辅助索引) + +**非聚集索引**的**非叶子节点**包含对应的**辅助索引键值**,叶子节点不同于聚集索引,不含行数据,而是包含了一个聚集索引键(主键),通过查找非聚集索引B+树找到其叶子节点上的主键,然后再去聚集索引B+树上去查找对应的行数据。其树高一般也是2~4层,故查询次数较聚集索引多了一倍。 + +![Innodb-非聚集索引](images/Database/Innodb-非聚集索引.png) + + + +### 全文索引(FULLTEXT) + +B+树索引的特点是可以通过索引字段等值查询(where a = “xxx”)或者进行前缀匹配(where a like “xxx%”)查找,然而如果数据库中存储的数据为blog或weibo之类的长篇文本,用户一般希望以模糊匹配(where a like “%xxx%”)的方式来查询,由于条件的前缀并不是固定的值,所以没法利用B+树索引进行比较查找,故而效率会很低,为了解决这类问题,引入了全文索引。 +全文索引(Full-Text Search)是将数据库中某段文字(或词语)快速查找出来的技术。其本质是一种映射表结构,在表中存储了单词与单词所在文档的位置的映射关系(其key是对应的数据(单词),value是数据的位置),相对于B+树索引的key-val来讲,顺序是倒过来的,所以也叫倒排索引。这个索引表,也叫做辅助表。 +![Innodb-全文索引](images/Database/Innodb-全文索引.png) + +InnoDB存储引擎生成了6张辅助表以提高全文检索的并发性能,同时该表是持久化的保存在磁盘上的,在服务起来后,动态将其加载到内存里。辅助表在内存中并不是表结构(能想来表的查询效率并不高),其在内存中的格式是一棵红黑树,根据(text,documents)来进行排序,该缓存也被起个响亮的名字——全文检索索引缓存。 +参数innodb_ft_cache_size用来控制全文索引缓存的大小,默认一般是32M。缓存满时,会将其中的(text,documents)映射信息保存到磁盘中去。若缓存设置过小,可能会导致频繁换页引起索引效率下降。 + +另外值得一提的是,InnoDB存储引擎的全文索引只支持如英文这类的有单词分界符的语言,像中文这样的语言是不支持的 + + +### 自适应哈希索引 + +B+树索引树高一般是2~4层,故需要2~4次的查询才能找到对应的数据页,而哈希索引是一种非常快的查找方法,通常情况下它的时间复杂度是O(1)的。InnDB存储引擎一般会默认开启自适应哈希索引的开关,通过如下命令可以查看: + +```sql +show variables like '%adaptive_hash%'; +``` + +开启后,引擎会自动监控对表上各个索引页的查询,如果观察到建立哈希索引可以带来一定的效率提升,则会自动构建哈希索引,所以称之为自适应哈希索引(Adaptive Hash Index)。AHI是通过缓冲池中的B+树索引页构造而来,无需访问磁盘,故而创建速度是很快的。同样的,因为是自适应的,所以无法主动的去构造AHI,我们只能控制它的开启或关闭: + +```sql +set global innodb_adaptive_hash_index=off/on +``` + +该引擎根据访问的频率以及查询条件来为某些热点页建立AHI: + +- 查询条件必须是指定的值而不是范围:如where a=xxx and b=xxx +- 以该模式的查询条件连续访问100次以上(或查询次数达到了页中记录的1/16) + +![自适应哈希索引](images/Database/自适应哈希索引.png) + +InnoDB存储引擎官方提供的性能数据显示,开启AHI后,读取和写入性能可以提高2倍,辅助索引的连接操作性能可以提高5倍,显然,一般情况下开启AHI还是很有必要且值得的。另外,我们还可以通过命令来查看当前AHI的使用情况: + +```sql +mysql> show engine innodb status \G +------------------------------------- +INSERT BUFFER AND ADAPTIVE HASH INDEX +------------------------------------- +Ibuf: size 1, free list len 0, seg size 2, 0 merges +merged operations: + insert 0, delete mark 0, delete 0 +discarded operations: + insert 0, delete mark 0, delete 0 +Hash table size 553229, node heap has 17 buffer(s) +0.00 hash searches/s, 0.00 non-hash searches/s +``` + +其中包括AHI的大小、使用情况、每秒使用AHI搜索的情况。通过hash searches/non-hash searches可以大概的了解AHI的调用频率。根据频率的大小,我们可以考虑是否需要调整它的开关状态。 \ No newline at end of file diff --git a/src/Database/5.md b/src/Database/5.md new file mode 100644 index 0000000..cfd2b0f --- /dev/null +++ b/src/Database/5.md @@ -0,0 +1,25 @@ +在BCNF基础上,**需要消除多值依赖**。 + +例如: + +**客户联系方式** + +| 客户编号 | 固定电话 | 移动电话 | +| -------- | -------- | ----------- | +| 10 | 88-123 | 151xxxxxxxx | +| 10 | 88-124 | 183xxxxxxxx | + +一个用户拥有多个固定电话和移动电话,给表的维护带来很多麻烦。比如增加一个固定电话,那么移动电话这一栏就较难维护,改为: + +**客户电话表** + +| 客户编号 | 电话号码 | 电话类型 | +| -------- | ----------- | -------- | +| 10 | 88-123 | 固定电话 | +| 10 | 88-124 | 固定电话 | +| 10 | 151xxxxxxxx | 移动电话 | +| 10 | 183xxxxxxxx | 移动电话 | + +**好处** + +- 解决一些异常,使表结构更加合理 \ No newline at end of file diff --git a/src/Database/501.md b/src/Database/501.md new file mode 100644 index 0000000..0d2c588 --- /dev/null +++ b/src/Database/501.md @@ -0,0 +1 @@ +**重做日志(redo log)、二进制日志(bin log)、回滚日志(undo log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)、中继日志(relay log)。** \ No newline at end of file diff --git a/src/Database/502.md b/src/Database/502.md new file mode 100644 index 0000000..24b9828 --- /dev/null +++ b/src/Database/502.md @@ -0,0 +1,34 @@ +`binlog` 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。`binlog` 是 `mysql`的逻辑日志(即SQL语句),并且由 `Server` 层进行记录,使用任何存储引擎的 `mysql` 数据库都会记录 `binlog` 日志。`binlog` 是通过追加的方式进行写入的,可以通过`max_binlog_size` 参数设置每个 `binlog`文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。 + + + +**作用** + +- **主从复制**:在主从复制中,从库利用主库上的binlog进行重播,实现主从同步 +- **数据恢复**:通过使用mysqlbinlog`工具来实现数据库基于时间点的还原 + + + +### 刷盘时机 + +对于 `InnoDB` 存储引擎而言,只有在事务提交时才会记录`biglog` ,此时记录还在内存中,那么 `biglog`是什么时候刷到磁盘中的呢?`mysql` 通过 `sync_binlog` 参数控制 `biglog` 的刷盘时机,取值范围是 `0-N`: + +- `sync_binlog=0`:不去强制要求,由系统自行判断何时写入磁盘 +- `sync_binlog=1`:每次 `commit` 的时候都要将 `binlog` 写入磁盘 +- `sync_binlog=N(N>1)`:每N个事务,才会将 `binlog` 写入磁盘 + +从上面可以看出, `sync_binlog` 最安全的是设置是 `1` ,这也是`MySQL 5.7.7`之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。 + + + +### 日志格式 + +`binlog` 日志有三种格式,日志格式通过 `binlog-format` 指定。在 `MySQL 5.7.7` 之前,默认的格式是 `STATEMENT` , `MySQL 5.7.7` 之后,默认值是 `ROW`。 + +- `STATMENT`:基于`SQL`语句复制(`statement-based replication, SBR`),每一条会修改数据的sql语句会记录到`binlog`中 + - **优点**:不需要记录每一行的变化,减少了binlog日志量,节约了IO,从而提高了性能 + - **缺点**:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp() 等 +- `ROW`:基于行复制(`row-based replication, RBR` ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了。 + - **优点**:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 + - **缺点**:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨 +- `MIXED`:基于`STATMENT` 和 `ROW` 两种模式的混合复制(`mixed-based replication, MBR` ),一般的复制使用`STATEMENT` 模式保存 `binlog` ,对于 `STATEMENT` 模式无法复制的操作使用 `ROW` 模式保存 `binlog` \ No newline at end of file diff --git a/src/Database/503.md b/src/Database/503.md new file mode 100644 index 0000000..e8c1b20 --- /dev/null +++ b/src/Database/503.md @@ -0,0 +1,39 @@ +**重做日志是InnoDB引擎层日志,用来记录事务操作引起数据变化,记录的是数据页的物理修改。**包括两部分: + +- **内存中的日志缓冲(redo log buffer)** +- **磁盘上的日志文件(redo logfile)** + +MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file,种先写日志再写磁盘的技术就是 WAL(Write-Ahead Logging) 技术。之后MySQL会在合适的时候将操作记录 flush 到磁盘上面,flush 的条件可能是系统比较空闲,或 redo log 空间不足时。redo log 文件的大小是固定的,如可以是由4个1GB文件组成的集合。 + + + +**WAL(Write-Ahead Logging)** + +WAL(Write-Ahead Logging,预写式日志)是一种数据安全写入机制,即**先写日志,再写磁盘**,这样保证数据的安全性。先在内存中提交事务,然后写日志(在InnoDB中就是Redo Log,日志是为了防止宕机导致内存数据丢失),然后再后台任务中把内存中的数据异步刷到磁盘。 + + + +**作用** + +- **确保事务的持久性** +- **防止发生故障时,尚有脏页未写入磁盘**。在重启mysql服务时,根据redo log进行重做,从而达到事务的持久性特性 + + + +### 写入流程 + +为了控制 `redo log` 的写入策略,`innodb_flush_log_at_trx_commit` 会有下面 3 中取值: + +- `0`:**每次提交事务只写在 `redo log buffer` 中** +- `1`:**每次提交事务持久化到磁盘** +- `2`:**每次提交事务写到 文件系统的 `page cache` 中** + + + +### 刷盘场景 + +redo log 实际的触发 fsync 操作写盘包含以下几个场景: + +- **后台每隔 1 秒钟的线程轮询** +- **innodb_flush_log_at_trx_commit 设置成 1 时,事务提交时触发** +- **innodb_log_buffer_size 是设置 redo log 大小的参数**。当 redo log buffer 达到 innodb_log_buffer_size / 2 时,也会触发一次 fsync \ No newline at end of file diff --git a/src/Database/504.md b/src/Database/504.md new file mode 100644 index 0000000..4f407ca --- /dev/null +++ b/src/Database/504.md @@ -0,0 +1,8 @@ +回滚日志(undo log)主要存储的是数据的逻辑变化日志,用于回滚操作。比如我们要 insert 一条数据,那么 undo log 就会生成一条对应的 delete 日志。所以当需要回滚时,只需要利用undo log 就可以恢复都修改前的数据。 + + + +**作用** + +- **提供回滚操作**。undo log实现事务的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚 +- **提供多版本并发控制(MVCC)下的读**。也即非锁定读 \ No newline at end of file diff --git a/src/Database/6.md b/src/Database/6.md new file mode 100644 index 0000000..ed7bce0 --- /dev/null +++ b/src/Database/6.md @@ -0,0 +1,39 @@ +在4NF基础上,**消除传递依赖**。 + +例如: + +**销售表** + +| 销售人员 | 供应商 | 产品 | +| -------- | ------ | ---- | +| S1 | V1 | P1 | +| S2 | V2 | P2 | +| S1 | V1 | P1 | +| S2 | V2 | P2 | + +要想找到某一条数据,必须以(销售人员,供应商,产品)为主键,改为: + +**销售人员_供应商表** + +| 销售人员 | 供应商 | +| -------- | ------ | +| S1 | V1 | +| S2 | V2 | + +**销售人员_产品表** + +| 销售人员 | 产品 | +| -------- | ---- | +| S1 | P1 | +| S2 | P2 | + +**供应商_产品表** + +| 供应商 | 产品 | +| ------ | ---- | +| V1 | P1 | +| V2 | P2 | + +**好处** + +- 解决某些异常操作 \ No newline at end of file diff --git a/src/Database/601.md b/src/Database/601.md new file mode 100644 index 0000000..8137dbc --- /dev/null +++ b/src/Database/601.md @@ -0,0 +1,16 @@ +索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。 + +**索引意义**:索引能极大的减少存储引擎需要扫描的数据量,索引可以把随机IO变成顺序IO。索引可以帮助我们在进行分组、排序等操作时,避免使用临时表。正确的创建合适的索引是提升数据库查询性能的基础。 + + + +**优势** + +- **大大减少服务器需要扫描的数据量** +- **可以提高数据检索的效率(将随机I/O变成顺序I/O),降低数据库的I/O成本** +- **通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗** + +**劣势** + +- **索引会占据磁盘空间** +- **索引虽然会提高查询效率,但是会降低更新表的效率**。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件 \ No newline at end of file diff --git a/src/Database/602.md b/src/Database/602.md new file mode 100644 index 0000000..b2dec10 --- /dev/null +++ b/src/Database/602.md @@ -0,0 +1,124 @@ +### Hash索引 + +**原理**是先将索引通过hash算法后得到的hash值(即磁盘文件指针)存到hash表中。在进行查询时,将索引通过hash算法,得到hash值,与hash表中的hash值比对。通过磁盘文件指针,只要**一次磁盘IO**就能找到要的值。 + +![Hash索引](images/Database/Hash索引.png) + +**优点** + +- **快速查询**。参与索引的字段只要进行Hash运算之后就可以快速定位到该记录,时间复杂度约为1 + +**缺点** + +- 哈希索引只包含哈希值和行指针,所以不能用索引中的值来避免读取行 +- 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序和范围查询 +- 哈希索引也不支持部分索引列查询,因为哈希索引始终是使用索引列的全部数据进行哈希计算的 +- 哈希索引只支持等值比较查询,如=,IN(),<=>操作 +- 如果哈希冲突较多,一些索引的维护操作的代价也会更高 + + + +### B-Tree索引 + +**背景**:二叉查找树查询的时间复杂度是O(logN),查找速度最快和比较次数较少。但用于数据库索引,当数据量过大,不可能将所有索引加载进内存,使用二叉树会导致磁盘I/O过于频繁,最坏的情况下磁盘I/O的次数由树的高度来决定。 + +**B-Tree(平衡多路查找树)**对二叉树进行了横向扩展,能很好**解决红黑树中遗留的高度问题**,使树结构更加**矮胖**,使得一次I/O能加载更多关键字,对比在内存中完成,减少了磁盘I/O次数,更适用于大型数据库,但是为了保持自平衡,插入或者删除元素都会导致节点发生裂变反应,有时候会非常麻烦。 + +![索引-B树结构](images/Database/索引-B树结构.png) + +**存在问题** + +- **不支持范围查询的快速查找**。可以用B+Tree解决 +- **每行数据量很大时,会导致B-Tree深度较大,进而影响查询效率**。可以用B+Tree解决 + + + +**案例分析**:模拟下查找key为10的data的过程 + +![B-Tree案例分析](images/Database/B-Tree案例分析.png) + +- 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】 +- 磁盘块1存储15,45和三个指针数据。我们发现10<15,因此我们找到指针p1 +- 根据p1指针,我们定位并读取磁盘块2。【磁盘IO操作**2次**】 +- 磁盘块2存储7,12和三个指针数据。我们发现7<10<12,因此我们找到指针p2 +- 根据p2指针,我们定位并读取磁盘块6。【磁盘IO操作**3次**】 +- 磁盘块6中存储8,10。我们找到10,获取10所对应的数据data + + + +### B+Tree索引 + +B+树是B-树的变体,也是一种多路搜索树。其定义基本与B-树相同,与B-Tree相比B+Tree有以下不同点: + +- **非叶子结点的子树指针与关键字个数相同** +- **非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间)** +- **为所有叶子结点增加一个链指针** +- **所有关键字都在叶子结点出现** +- **内节点不存储data,只存储key** + +![索引-B+Tree结构](images/Database/索引-B+Tree结构.png) + +**优点** + +- 单次请求涉及的磁盘IO次数少(出度d大,且非叶子节点不包含表数据,树的高度小) +- 查询效率稳定(任何关键字的查询必须走从根结点到叶子结点,查询路径长度相同) +- 遍历效率高(从符合条件的某个叶子节点开始遍历即可) + +**缺点** + +B+树最大的性能问题在于会产生大量的随机IO,主要存在以下两种情况: + +- 主键不是有序递增的,导致每次插入数据产生大量的数据迁移和空间碎片 +- 即使主键是有序递增的,大量写请求的分布仍是随机的 + + + +**案例分析:等值查询** + +假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。 + +![B+树索引-等值查询](images/Database/B+树索引-等值查询.png) + +- 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2 +- 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6 +- 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。) + + + +**案例分析:范围查询** +假如我们想要查找9和26之间的数据。查找路径是磁盘块1->磁盘块2->磁盘块6->磁盘块7。 + +![案例分析-B+树-范围查询](images/Database/案例分析-B+树-范围查询.png) + +- 首先查找值等于9的数据,将值等于9的数据缓存到结果集。这一步和前面等值查询流程一样,发生了三次磁盘IO +- 查找到15之后,底层的叶子节点是一个有序列表,我们从磁盘块6,键值9开始向后遍历筛选所有符合筛选条件的数据 +- 第四次磁盘IO:根据磁盘6后继指针到磁盘中寻址定位到磁盘块7,将磁盘7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将data缓存到结果集。 +- 主键具备唯一性(后面不会有<=26的数据),不需再向后查找,查询终止。将结果集返回给用户 + + + +### B*Tree + +是B+树的变体,**在B+树的非根和非叶子结点再增加指向兄弟的指针**。 + +![索引机制-B星树](images/Database/索引机制-B星树.jpg) + +B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。 + + + +**B+树的分裂** + +当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。 + + + +**B*树的分裂** + +当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B*树分配新结点的概率比B+树要低,空间使用率更高。 + + + +### R-Tree索引 + +空间数据索引(R-Tree),MyISAM支持空间索引,可以用作地理数据存储。 \ No newline at end of file diff --git a/src/Database/603.md b/src/Database/603.md new file mode 100644 index 0000000..b7bb83c --- /dev/null +++ b/src/Database/603.md @@ -0,0 +1,104 @@ +### 普通索引 + +基本的索引类型,值可以为空,没有唯一性的限制。 + +```sql +-- 直接创建索引 +CREATE INDEX index_name ON table_name(col_name); +-- 修改表结构的方式添加索引 +ALTER TABLE table_name ADD INDEX index_name(col_name); + +-- 创建表的时候同时创建索引 +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` varchar(255) NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`), + INDEX index_name (title(255)) +) + +-- 删除索引 +DROP INDEX index_name ON table_name; +-- 或 +alter table `表名` drop index 索引名; +``` + + + +### 唯一索引 + +索引列中的值必须是唯一的,但是允许为空值(只允许存在一条空值)。 + +```mysql +-- 创建单个索引 +CREATE UNIQUE INDEX index_name ON table_name(col_name); +-- 创建多个索引 +CREATE UNIQUE INDEX index_name on table_name(col_name,...); + +-- 修改表结构 +-- 单个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name); +-- 多个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...); +``` + + + +### 主键索引 + +主键是一种唯一性索引,但它必须指定为`PRIMARY KEY`,每个表只能有一个主键。 + +```sql +-- 主键索引(创建表时添加) +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` varchar(255) NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`) +) + +-- 主键索引(创建表后添加) +CREATE TABLE `order` ( + `orderId` varchar(36) NOT NULL, + `productId` varchar(36) NOT NULL , + `time` varchar(20) NULL DEFAULT NULL +) +alter table `order` add primary key(`orderId`); +``` + + + +### 组合索引 + +**在多个字段上创建的索引**。组合索引遵守“**最左前缀**”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。 + +```sql +-- 创建一个复合索引 +create index index_name on table_name(col_name1,col_name2,...); + +-- 修改表结构的方式添加索引 +alter table table_name add index index_name(col_name,col_name2,...); +``` + + + +### 全文索引 + +全文索引的索引类型为`FULLTEXT`。全文索引可以在`varchar、char、text`类型的列上创建。可以通过`ALTER TABLE`或`CREATE INDEX`命令创建。对于大规模的数据集,通过`ALTER TABLE`(或`CREATE INDEX`)命令创建全文索引要比把记录插入带有全文索引的空表更快。`MyISAM`支持全文索引,`InnoDB`在mysql5.6之后支持了`全文索引`。 全文索引`不支持中文`需要借`sphinx(coreseek)`或`迅搜<、code>技术处理中文。` + +```mysql +-- 创建表的适合添加全文索引 +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` text NOT NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`), + FULLTEXT (content) +) + +-- 修改表结构添加全文索引 +ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name); +``` \ No newline at end of file diff --git a/src/Database/604.md b/src/Database/604.md new file mode 100644 index 0000000..c347d9a --- /dev/null +++ b/src/Database/604.md @@ -0,0 +1,154 @@ +### MyIsam索引 + +MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。 + +#### 主键索引 + +![在这里插入图片描述](images/Database/20201024114325883.png) + +表user的索引存储在索引文件`user.MYI`中,数据文件存储在数据文件 `user.MYD`中。简单分析下查询时的磁盘IO情况: + + + +**场景一:根据主键等值查询数据** +![在这里插入图片描述](images/Database/20201024114404727.png) + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于30的索引项。(1次磁盘IO) +- 从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。(1次磁盘IO) +- 将记录返给客户端。 + +**磁盘IO次数:3次索引检索+记录数据检索。** + + + +**场景二:根据主键范围查询数据** + +![在这里插入图片描述](images/Database/20201024114510253.png) + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。 + - 根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) + - 我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。 + +- 向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) +- 最后得到两条符合筛选条件,将查询结果集返给客户端。 + +**磁盘IO次数:4次索引检索+记录数据检索。** + +**备注:**以上分析仅供参考,MyISAM在查询时,会将索引节点缓存在MySQL缓存中,而数据缓存依赖于操作系统自身的缓存,所以并不是每次都是走的磁盘,这里只是为了分析索引的使用过程。 + + + +#### 辅助索引 + +在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。 + + +### InnoDB索引 + +#### 主键索引 + +每个InnoDB表都有一个主键索引(聚簇索引) ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下: + +- 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引 +- 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引 +- 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增 + +除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值都。 在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。 +主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。 + +![在这里插入图片描述](images/Database/202010241146330.png) + + + +**场景一:等值查询数据** + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO) + +**磁盘IO数量:3次。** + +![在这里插入图片描述](images/Database/20201024114716460.png) + + + +#### 辅助索引 + +除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。以表user_innodb的age列为例,age索引的索引结果如下图: + +![在这里插入图片描述](images/Database/20201024114750255.png) + +底层叶子节点的按照(age,id)的顺序排序,先按照age列从小到大排序,age列相同时按照id列从小到大排序。使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后使用主键到主索引中检索获得记录。 + + + +**场景一:等值查询的情况** + +```sql +select * from t_user_innodb where age=19; +``` + +![在这里插入图片描述](images/Database/2020102411481097.png) + +根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为**回表**查询。 + +**磁盘IO数:辅助索引3次+获取记录回表3次** + + + +#### 组合索引 + +还是以自己创建的一个表为例:表 abc_innodb,id为主键索引,创建了一个联合索引idx_abc(a,b,c)。 + +```sql +CREATE TABLE `abc_innodb` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `a` int(11) DEFAULT NULL, + `b` int(11) DEFAULT NULL, + `c` varchar(10) DEFAULT NULL, + `d` varchar(10) DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_abc` (`a`, `b`, `c`) +) ENGINE = InnoDB; +``` + +组合索引的数据结构: + +![在这里插入图片描述](images/Database/20201024114900213.png) + +**组合索引的查询过程:** + +```sql +select * from abc_innodb where a = 13 and b = 16 and c = 4; +``` + +![在这里插入图片描述](images/Database/20201024115012887.png) + +**最左匹配原则** + +最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。 + +在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排列,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内递增有序,而c列只能在a,b两列相等的情况下小范围内递增有序。 + +就像上面的查询,B+树会先比较a列来确定下一步应该搜索的方向,往左还是往右。如果a列相同再比较b列。但是如果查询条件没有a列,B+树就不知道第一步应该从哪个节点查起。 + +可以说创建的idx_abc(a,b,c)索引,相当于创建了(a)、(a,b)(a,b,c)三个索引。、 + +组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配。 + + + +#### 覆盖索引 + +覆盖索引并不是说是索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到主键值,相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况,在上面abc_innodb表中的组合索引查询时,如果我只需要abc字段的,那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了,而不需要回表。这种情况就是覆盖索引。可以看一下执行计划: + +**覆盖索引的情况:** + +![在这里插入图片描述](images/Database/20201024115203337.png)**未使用到覆盖索引:** + +![在这里插入图片描述](images/Database/20201024115218222.png) \ No newline at end of file diff --git a/src/Database/605.md b/src/Database/605.md new file mode 100644 index 0000000..a85f35c --- /dev/null +++ b/src/Database/605.md @@ -0,0 +1,99 @@ +**场景一:where语句中包含or时,可能会导致索引失效** + +使用or并不是一定会使索引失效,你需要看or左右两边的查询列是否命中相同的索引。 + +```sql +-- 假设user表中的user_id列有索引,age列没有索引 +-- 能命中索引 +select * from user where user_id = 1 or user_id = 2; +-- 无法命中索引 +select * from user where user_id = 1 or age = 20; +-- 假设age列也有索引的话,依然是无法命中索引的 +select * from user where user_id = 1 or age = 20; +``` + +可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 + + + +**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** + +负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 + + + +**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** + +其实单个索引字段,使用is null或is not null时,是可以命中索引的。 + + + +**场景四:在索引列上使用内置函数,一定会导致索引失效** + +比如下面语句中索引列login_time上使用了函数,会索引失效: + +```sql +select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; +``` + + + +**场景五:隐式类型转换导致的索引失效** + +如下面语句中索引列user_id为varchar类型,不会命中索引: + +```mysql +select * from user where user_id = 12; +``` + + + +**场景六:对索引列进行运算,一定会导致索引失效** + +运算如+,-,\*,/等,如下: + +```mysql +select * from user where age - 1 = 10; +``` + +优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: + +```sql +select * from user where age = 10 - 1; +``` + + + +**场景七:like通配符可能会导致索引失效** + +like查询以%开头时,会导致索引失效。解决办法有两种: + +- 将%移到后面,如: + +```sql +select * from user where `name` like '李%'; +``` + +- 利用覆盖索引来命中索引: + +```sql +select name from user where `name` like '%李%'; +``` + + + +**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** + +当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: + +```sql +select * from t where k2=2; +select * from t where k3=3; +select * from t where k2=2 and k3=3; +``` + +下面的语句只会命中索引(k1): + +```sql +select * from t where k1=1 and k3=3; +``` \ No newline at end of file diff --git a/src/Database/701.md b/src/Database/701.md new file mode 100644 index 0000000..87611be --- /dev/null +++ b/src/Database/701.md @@ -0,0 +1,3 @@ +水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 + +![img](images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg) \ No newline at end of file diff --git a/src/Database/702.md b/src/Database/702.md new file mode 100644 index 0000000..cc43116 --- /dev/null +++ b/src/Database/702.md @@ -0,0 +1,3 @@ +垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。 + +![img](images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg) \ No newline at end of file diff --git a/src/Database/703.md b/src/Database/703.md new file mode 100644 index 0000000..0bff480 --- /dev/null +++ b/src/Database/703.md @@ -0,0 +1,3 @@ +- 哈希取模:hash(key)%N +- 范围:可以是 ID 范围也可以是时间范围 +- 映射表:使用单独的一个数据库来存储映射关系 \ No newline at end of file diff --git a/src/Database/704.md b/src/Database/704.md new file mode 100644 index 0000000..fab534c --- /dev/null +++ b/src/Database/704.md @@ -0,0 +1,8 @@ +- **事务问题**:使用分布式事务来解决,比如 XA 接口 + +- **连接**:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。 + +- **唯一性** + - 使用全局唯一 ID (GUID) + - 为每个分片指定一个 ID 范围 + - 分布式 ID 生成器(如 Twitter 的 Snowflake 算法) \ No newline at end of file diff --git a/src/Database/801.md b/src/Database/801.md new file mode 100644 index 0000000..6a07b38 --- /dev/null +++ b/src/Database/801.md @@ -0,0 +1,50 @@ +**第1步:通过慢查日志等定位那些执行效率较低的SQL语句** + +**第2步:explain分析SQL的执行计划** + +需要重点关注`type`、`rows`、`filtered`、`extra`。 + +- `type`:由上至下,效率越来越高 + + - `ALL`:全表扫描 + - `index`:索引全扫描 + - `range`:索引范围扫描,常用语`<`、`<=`、`>=`、`between`、`in`等操作 + - `ref`:使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中 + - `eq_ref`:类似ref,区别在于使用的是唯一索引,使用主键的关联查询 + - `const/system`:单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询 + - `null`:MySQL不访问任何表或索引,直接返回结果 + + 虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引`idx1(a, b, c)`,`idx2(a, c)`,SQL为`select * from t where a = 1 and b in (1, 2) order by c;`如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2 + +- `Extra` + + - `Using filesort`:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。 + - `Using temporary`:使用了临时表保存中间结果,性能特别差,需要重点优化 + - `Using index`:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。 + - `Using index condition`:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。 + +**第3步:show profile 分析** + +了解SQL执行的线程的状态及消耗的时间。默认是关闭的,开启语句“set profiling = 1;” + +```mysql +SHOW PROFILES ; +SHOW PROFILE FOR QUERY #{id}; +``` + +**第4步:trace** + +trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。 + +```mysql +set optimizer_trace="enabled=on"; +set optimizer_trace_max_mem_size=1000000; +select * from information_schema.optimizer_trace; +``` + +**第5步:确定问题并采用相应的措施** + +- 优化索引 +- 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤 +- 改用其他实现方式:ES、数仓等 +- 数据碎片处理 \ No newline at end of file diff --git a/src/Database/802.md b/src/Database/802.md new file mode 100644 index 0000000..4ed1a1e --- /dev/null +++ b/src/Database/802.md @@ -0,0 +1,66 @@ +### 批量去重插入 + +**问题:MySQL批量插入,如何不插入重复数据?** + +**解决方案1:insert ignore into** + +当插入数据时,如出现错误时,如重复数据,将不返回错误,只以警告形式返回。所以使用ignore请确保语句本身没有问题,否则也会被忽略掉。例如: + +```mysql +INSERT IGNORE INTO user (name) VALUES ('telami') +``` + +这种方法很简便,但是有一种可能,就是插入不是因为重复数据报错,而是因为其他原因报错的,也同样被忽略了~ + + + +**解决方案2:on duplicate key update** + +当primary或者unique重复时,则执行`update`语句,如`update`后为无用语句,如`id=id`,则同1功能相同,但错误不会被忽略掉。在公众号顶级架构师后台回复“架构整洁”,获取一份惊喜礼包。例如,为了实现name重复的数据插入不报错,可使用一下语句: + +```mysql +INSERT INTO user (name) VALUES ('telami') ON duplicate KEY UPDATE id = id +``` + +这种方法有个前提条件,就是,需要插入的约束,需要是主键或者唯一约束(在你的业务中那个要作为唯一的判断就将那个字段设置为唯一约束也就是`unique key`)。 + + + +**解决方案3:insert … select … where not exist** + +根据select的条件判断是否插入,可以不光通过`primary`和`unique`来判断,也可通过其它条件。例如: + +```mysql +INSERT INTO user (name) SELECT 'telami' FROM dual WHERE NOT EXISTS (SELECT id FROM user WHERE id = 1) +``` + +这种方法其实就是使用了`MySQL`的一个临时表的方式,但是里面使用到了子查询,效率也会有一点点影响,如果能使用上面的就不使用这个。 + + + +**解决方案4:replace into** + +如果存在`primary or unique`相同的记录,则先删除掉。再插入新记录。 + +```mysql +REPLACE INTO user SELECT 1, 'telami' FROM books +``` + +这种方法就是不管原来有没有相同的记录,都会先删除掉然后再插入。选择的是第二种方式 + +```xml + + insert into user (id,username,mobile_number) + values + + ( + #{item.id}, + #{item.username}, + #{item.mobileNumber} + ) + + ON duplicate KEY UPDATE id = id + +``` + +这里用的是Mybatis,批量插入的一个操作,`mobile_number`已经加了唯一约束。这样在批量插入时,如果存在手机号相同的话,是不会再插入了的。 \ No newline at end of file diff --git a/src/Database/803.md b/src/Database/803.md new file mode 100644 index 0000000..8b054cb --- /dev/null +++ b/src/Database/803.md @@ -0,0 +1,150 @@ +### 案例1:最左匹配 + +**索引** + +```mysql +KEY `idx_shopid_orderno` (`shop_id`,`order_no`) +``` + +**SQL语句** + +```mysql +select * from _t where orderno='xxx'; +``` + +查询匹配从左往右匹配,要使用`order_no`走索引,必须查询条件携带`shop_id`或者索引(`shop_id`,`order_no`)调换前后顺序。 + + + +### 案例2:隐式转换 + +**索引** + +```mysql +KEY `idx_mobile` (`mobile`) +``` + +**SQL语句** + +```mysql +select * from _user where mobile=12345678901; +``` + +隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。 + + + +### 案例3:大分页 + +**索引** + +```mysql +KEY `idx_a_b_c` (`a`, `b`, `c`) +``` + +**SQL语句** + +```mysql +select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10; +``` + +对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式: + +- 把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行 +- 采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下 + +```mysql +SELECT t1.* FROM _t t1, (SELECT id FROM _t WHERE a=1 AND b=2 ORDER BY c DESC LIMIT 10000,10) t2 WHERE t1.id=t2.id; +``` + + + +### 案例4:in+order by + +**索引** + +```mysql +KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`) +``` + +**SQL语句** + +```mysql +SELECT * FROM _order WHERE shop_id = 1 AND order_status IN ( 1, 2, 3 ) ORDER BY created_at DESC LIMIT 10 +``` + +in查询在MySQL底层是通过`n*m`的方式去搜索,类似union,但是效率比union高。in查询在进行cost代价计算时(`代价 = 元组数 * IO平均值`),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(`eq_range_index_dive_limit`),5.6之后超过这个临界值后该列的cost就不参与计算了。 + +因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。处理方式,可以(`order_status`, `created_at`)互换前后顺序,并且调整SQL为延迟关联。 + + + +### 案例5:范围查询索引失效 + +范围查询阻断,后续字段不能走索引。 + +**索引** + +```mysql +KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`) +``` + +**SQL语句** + +```mysql +SELECT * FROM _order WHERE shop_id=1 AND created_at > '2021-01-01 00:00:00' AND order_status=10; +``` + +范围查询还有“IN、between”。 + + + +### 案例6:避免使用非快速索引 + +不等于、不包含不能用到索引的快速搜索。 + +```mysql +select * from _order where shop_id=1 and order_status not in (1,2); +select * from _order where shop_id=1 and order_status != 1; +``` + +在索引上,避免使用`NOT`、`!=`、`<>`、`!<`、`!>`、`NOT EXISTS`、`NOT IN`、`NOT LIKE`等。 + + + +### 案例7:优化器选择索引失效 + +如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。 + +```mysql +select * from _order where order_status = 1 +``` + +查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。 + + + +### 案例8:复杂查询 + +```mysql +select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01'; +select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10; +``` + +如果是统计某些数据,可能改用数仓进行解决;如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决。 + + + +### 案例9:asc和desc混用 + +```mysql +select * from _t where a=1 order by b desc, c asc +``` + +desc 和asc混用时会导致索引失效 + + + +### 案例10:大数据 + +对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存。那么需要注意,频繁的清理数据,会照成数据碎片,需要联系DBA进行数据碎片处理。 \ No newline at end of file diff --git a/src/Database/901.md b/src/Database/901.md new file mode 100644 index 0000000..45c8e7c --- /dev/null +++ b/src/Database/901.md @@ -0,0 +1,39 @@ +![MySQL架构设计](images/Database/MySQL架构设计.jpg) + +从上面的示意图可以看出,MySQL从上到下包含了:**客户端、Server层和存储引擎层**。 + +- **客户端**:可以是我们常用的MySQL命令行窗口,或者是Java的客户端程序等 +- **Server层**:连接器、查询缓存、分析器、优化器和执行器等。大部分MySQL对用户提供的功能都在这一层实现,包括了内置函数的实现,存储过程、触发器、视图等 +- **存储层**:存储引擎层负责数据的存储和提取,存储引擎的实现是插件式的。也就是说用户可以选择自己所需要的存储引擎,如InnoDB、MyISAM等 + + + +### 连接器 + +连接器是MySQL服务端对外的门户,当我们使用命令行黑窗口或者JDBC的Connection.connect(),连接到MySQL Server端时,会校验用户名和密码;然后会查询用户对应的权限列表。当连接建立后,后续的权限范围就在此时确定了,如果连接没有断开的情况下,更改了用户的权限,此时对于该连接也不生效。 + + + +### 查询缓存 + +当连接建立完成后,执行select 语句的时候,就会来到查询缓存。MySQL会将Select 语句为 KEY,将查询结果为VALUE 的形式保存在内存中。如果匹配到对应的 KEY 就会直接从内存中返回结果。 + +但是常我们不会使用MySQL自身的查询缓存,因为当有一条Update 或 Insert 的改表语句时,就会清空对该表的所有查询缓存。缓存的粒度比较大,可以考虑类似 Redis 的分布式缓存做业务数据的缓存。在MySQL 8.0 中,查询缓存直接被移除了。 + + + +### 分析器 + +如果在查询缓存中没有查到数据,就要真正的开始执行SQL语句了。分析器首先会做“词法分析”。词法分析就是识别上面字符串,id、name 是表的字段名,T 是表的名称等等。之后就是语法分析,如果SQL有语法错误,在此时就会报错。 + + + +### 优化器 + +当分析器处理过之后,MySQL就知道SQL 要干什么了,但是此时还需要优化器对待执行的SQL 进行优化。当然MySQL 提供的优化器,相比其他几款商用收费的数据库来说还是比较弱的。当然MySQL 的优化器还是可以对 join 操作,表达式计算等等进行优化,本篇不做过多的介绍。 + + + +### 执行器 + +执行阶段,首先会检查当前用户有没有权限操作该 SQL 语句。如果有,则继续执行后续的操作。 \ No newline at end of file diff --git a/src/Database/902.md b/src/Database/902.md new file mode 100644 index 0000000..452388a --- /dev/null +++ b/src/Database/902.md @@ -0,0 +1,80 @@ +InnoDB 和 MyISAM 的比较 + +- 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 +- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 +- 外键:InnoDB 支持外键。 +- 备份:InnoDB 支持在线热备份。 +- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +- 其它特性:MyISAM 支持压缩表和空间数据索引。 + + + +### InnoDB引擎 + +InnoDB 是一个事务安全的存储引擎,它具备提交、回滚以及崩溃恢复的功能以保护用户数据。InnoDB 的行级别锁定保证数据一致性提升了它的多用户并发数以及性能。InnoDB 将用户数据存储在聚集索引中以减少基于主键的普通查询所带来的 I/O 开销。为了保证数据的完整性,InnoDB 还支持外键约束。默认使用B+TREE数据结构存储索引。 + + + +**特点** + +- 支持事务,支持4个事务隔离(ACID)级别 +- 行级锁定(更新时锁定当前行) +- 读写阻塞与事务隔离级别相关 +- 既能缓存索引又能缓存数据 +- 支持外键 +- InnoDB更消耗资源,读取速度没有MyISAM快 +- 在InnoDB中存在着缓冲管理,通过缓冲池,将索引和数据全部缓存起来,加快查询的速度; +- 对于InnoDB类型的表,其数据的物理组织形式是聚簇表。所有的数据按照主键来组织。数据和索引放在一块,都位于B+数的叶子节点上 + + + +**业务场景** + +- 需要支持事务的场景(银行转账之类) +- 适合高并发,行级锁定对高并发有很好的适应能力,但需要确保查询是通过索引完成的 +- 数据修改较频繁的业务 + + + +**InnoDB引擎调优** + +- 主键尽可能小,否则会给Secondary index带来负担 +- 避免全表扫描,这会造成锁表 +- 尽可能缓存所有的索引和数据,减少IO操作 +- 避免主键更新,这会造成大量的数据移动 + + + +### MyISAM引擎 + +MyISAM既不支持事务、也不支持外键、其优势是访问速度快,但是表级别的锁定限制了它在读写负载方面的性能,因此它经常应用于只读或者以读为主的数据场景。默认使用B+TREE数据结构存储索引。 + + + +**特点** + +- 不支持事务 +- 表级锁定(更新时锁定整个表) +- 读写互相阻塞(写入时阻塞读入、读时阻塞写入;但是读不会互相阻塞) +- 只会缓存索引(通过key_buffer_size缓存索引,但是不会缓存数据) +- 不支持外键 +- 读取速度快 + + + +**业务场景** + +- 不需要支持事务的场景(像银行转账之类的不可行) +- 一般读数据的较多的业务 +- 数据修改相对较少的业务 +- 数据一致性要求不是很高的业务 + + + +**MyISAM引擎调优** + +- 设置合适索引 +- 启用延迟写入,尽量一次大批量写入,而非频繁写入 +- 尽量顺序insert数据,让数据写入到尾部,减少阻塞 +- 降低并发数,高并发使用排队机制 +- MyISAM的count只有全表扫描比较高效,带有其它条件都需要进行实际数据访问 \ No newline at end of file diff --git a/src/Database/903.md b/src/Database/903.md new file mode 100644 index 0000000..44e4cbe --- /dev/null +++ b/src/Database/903.md @@ -0,0 +1,23 @@ +### 主从复制 + +主要涉及三个线程: + +- **binlog 线程**:负责将主服务器上的数据更改写入二进制日志(Binary log)中 +- **I/O 线程**:负责从主服务器上读取- 二进制日志,并写入从服务器的中继日志(Relay log) +- **SQL 线程**:负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay) + +![img](images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg) + + + +### 读写分离 + +主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。读写分离能提高性能的原因在于: + +- 主从服务器负责各自的读和写,极大程度缓解了锁的争用 +- 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销 +- 增加冗余,提高可用性 + +读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 + +![img](images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg) \ No newline at end of file diff --git a/src/Database/904.md b/src/Database/904.md new file mode 100644 index 0000000..3f659d4 --- /dev/null +++ b/src/Database/904.md @@ -0,0 +1 @@ +![MySQL查询过程](images/Database/MySQL查询过程.png) \ No newline at end of file diff --git a/src/Database/905.md b/src/Database/905.md new file mode 100644 index 0000000..6ece6f0 --- /dev/null +++ b/src/Database/905.md @@ -0,0 +1,25 @@ +### 基于语句的复制 + +基于语句的复制模式下,主库会记录那些造成数据更改的查询,当备库读取并重放这些事件时,实际上只把主库上执行过的SQL再执行一遍。 + +- **优点** + + 最明显的好处是实现相当简单。理论上讲,简单地记录和执行这些语句,能够让备库保持同步。另外好处是binlog日志里的事件更加紧凑,所以相对而言,基于语句的模式不会使用太多带宽。一条更新好几兆数据的语句在二进制日志里可能只占用几十字节。 + +- **缺点** + + 有些数据更新语句,可能依赖其他因素。例如,同一条sql在主库和备库上执行的时间可能稍微或很不相同,因此在传输的binlog日志中,除了查询语句,还包括一些元数据信息,如当前的时间戳。即便如此,还存在着一些无法被正确复制的SQL,例如,使用CURRENT_USER()函数语句。存储过程和触发器在使用基于语句的复制模式时也可能存在问题。另外一个问题是更新必须是串行的。这需要更多的锁。并且不是所有的存储引擎都支持这种复制模式。 + + + +### 基于行的复制 + +MySQL5.1开始支持基于行的复制,这种方式会将实际数据记录在二进制日志中,跟其他数据库的实现比较相像。 + +- **优点** + + 最大的好处是可以正确的复制每一行,一些语句可以呗更加有效地复制。由于无需重放更新主库数据的查询,使用基于行的复制模式能够更高效地复制数据。重放一些查询的代价会很高 + +- **缺点** + + 全表更行,使用基于行复制开销会大很多,因为每一行数据都会呗记录到二进制日志中,这使得二进制日志时间非常庞大 \ No newline at end of file diff --git a/src/Database/README.md b/src/Database/README.md new file mode 100644 index 0000000..d4ba99f --- /dev/null +++ b/src/Database/README.md @@ -0,0 +1,5 @@ +
Database
+ +Introduction:收纳技术相关的数据库知识 `事务`、`索引`、`锁`、`SQL优化` 等总结! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/Database/_sidebar.md b/src/Database/_sidebar.md new file mode 100644 index 0000000..5f525d9 --- /dev/null +++ b/src/Database/_sidebar.md @@ -0,0 +1,67 @@ +* 🏁数据库范式 + * [✍第一范式(1NF)](src/Database/1 "第一范式(1NF)") + * [✍第二范式(2NF)](src/Database/2 "第二范式(2NF)") + * [✍第三范式(3NF)](src/Database/3 "第三范式(3NF)") + * [✍巴斯-科德范式(BCNF)](src/Database/4 "巴斯-科德范式(BCNF)") + * [✍第四范式(4NF)](src/Database/5 "第四范式(4NF)") + * [✍第五范式(5NF)](src/Database/6 "第五范式(5NF)") +* 🏁连接方式 + * [✍内连接(INNER JOIN)](src/Database/101 "内连接(INNER JOIN)") + * [✍左连接(LEFT JOIN)](src/Database/102 "左连接(LEFT JOIN)") + * [✍右连接(RIGHT JOIN)](src/Database/103 "右连接(RIGHT JOIN)") + * [✍全连接(FULL JOIN)](src/Database/104 "全连接(FULL JOIN)") + * [✍LEFT JOIN EXCLUDING INNER JOIN](src/Database/105 "LEFT JOIN EXCLUDING INNER JOIN") + * [✍RIGHT JOIN EXCLUDING INNER JOIN](src/Database/106 "RIGHT JOIN EXCLUDING INNER JOIN") + * [✍FULL JOIN EXCLUDING INNER JOIN](src/Database/107 "FULL JOIN EXCLUDING INNER JOIN") + * [✍CROSS JOIN](src/Database/108 "CROSS JOIN") + * [✍SELF JOIN](src/Database/109 "SELF JOIN") +* [🏁数据库锁](src/Database/201 "数据库锁") + * [✍全局锁(Global-Level)](src/Database/202 "全局锁(Global-Level)") + * [✍表级锁(Table-Level)](src/Database/203 "表级锁(Table-Level)") + * [✍页级锁(Page-Level)](src/Database/204 "页级锁(Page-Level)") + * [✍行级锁(Row-Level)](src/Database/205 "行级锁(Row-Level)") + * [✍加锁机制](src/Database/206 "加锁机制") + * [✍锁问题](src/Database/207 "锁问题") +* [🏁数据库事务](src/Database/301 "数据库事务") + * [✍事务特性(ACID)](src/Database/302 "事务特性(ACID)") + * [✍隔离级别](src/Database/303 "隔离级别") + * [✍Spring事务机制](src/Database/304 "Spring事务机制") +* 🏁InnoDB引擎 + * [✍线程模型](src/Database/401 "线程模型") + * [✍数据页](src/Database/402 "数据页") + * [✍行格式](src/Database/403 "行格式") + * [✍内存结构](src/Database/404 "内存结构") + * [✍磁盘结构](src/Database/405 "磁盘结构") + * [✍存储索引](src/Database/406 "存储索引") +* [🏁MySQL日志](src/Database/501 "MySQL日志") + * [✍二进制日志(bin log)](src/Database/502 "二进制日志(bin log)") + * [✍重做日志(redo log)](src/Database/503 "重做日志(redo log)") + * [✍回滚日志(undo log)](src/Database/504 "回滚日志(undo log)") +* [🏁索引机制](src/Database/601 "索引机制") + * [✍数据结构](src/Database/602 "数据结构") + * [✍索引类型](src/Database/603 "索引类型") + * [✍索引实现](src/Database/604 "索引实现") + * [✍失效场景](src/Database/605 "失效场景") +* 🏁数据切分 + * [✍水平切分](src/Database/701 "水平切分") + * [✍垂直切分](src/Database/702 "垂直切分") + * [✍Sharding策略](src/Database/703 "Sharding策略") + * [✍Sharding存在的问题](src/Database/704 "Sharding存在的问题") +* 🏁SQL优化 + * [✍优化步骤](src/Database/801 "优化步骤") + * [✍特殊需求](src/Database/802 "特殊需求") + * [✍场景分析](src/Database/803 "场景分析") +* 🏁MySQL原理 + * [✍架构设计](src/Database/901 "架构设计") + * [✍存储引擎](src/Database/902 "存储引擎") + * [✍复制](src/Database/903 "复制") + * [✍查询过程](src/Database/904 "查询过程") + * [✍mysql复制原理](src/Database/905 "mysql复制原理") +* [🏁高可用方案](src/Database/1001 "高可用方案") + * [✍主从或主主半同步复制](src/Database/1002 "主从或主主半同步复制") + * [✍半同步复制优化](src/Database/1003 "半同步复制优化") + * [✍高可用架构优化](src/Database/1004 "高可用架构优化") + * [✍共享存储](src/Database/1005 "共享存储") + * [✍分布式协议](src/Database/1006 "分布式协议") + * [✍主从延迟](src/Database/1007 "主从延迟") +* 🏁ClickHouse \ No newline at end of file diff --git a/src/Database/images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg b/src/Database/images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg new file mode 100644 index 0000000..30793e1 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfxu6baej30j30kijsr.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg b/src/Database/images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg new file mode 100644 index 0000000..1955687 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfxx1pw3j30i90j0myc.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg b/src/Database/images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg new file mode 100644 index 0000000..0bb3688 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfxzqa84j30h30eowfd.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg b/src/Database/images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg new file mode 100644 index 0000000..85e6440 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg b/src/Database/images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg new file mode 100644 index 0000000..0530285 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg b/src/Database/images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg new file mode 100644 index 0000000..8d6cec1 Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfy97e83j30jk09ltav.jpg differ diff --git a/src/Database/images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg b/src/Database/images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg new file mode 100644 index 0000000..80ce94d Binary files /dev/null and b/src/Database/images/Database/007S8ZIlly1gjjfycefayj313k0s20wl.jpg differ diff --git a/src/Database/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png b/src/Database/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png new file mode 100644 index 0000000..15ac0ec Binary files /dev/null and b/src/Database/images/Database/0f2c2610773fb9fe304c374fc37af4ac.png differ diff --git a/src/Database/images/Database/20201024114325883.png b/src/Database/images/Database/20201024114325883.png new file mode 100644 index 0000000..5c24346 Binary files /dev/null and b/src/Database/images/Database/20201024114325883.png differ diff --git a/src/Database/images/Database/20201024114404727.png b/src/Database/images/Database/20201024114404727.png new file mode 100644 index 0000000..634bf54 Binary files /dev/null and b/src/Database/images/Database/20201024114404727.png differ diff --git a/src/Database/images/Database/20201024114510253.png b/src/Database/images/Database/20201024114510253.png new file mode 100644 index 0000000..1a7a9a8 Binary files /dev/null and b/src/Database/images/Database/20201024114510253.png differ diff --git a/src/Database/images/Database/202010241146330.png b/src/Database/images/Database/202010241146330.png new file mode 100644 index 0000000..e0c84fc Binary files /dev/null and b/src/Database/images/Database/202010241146330.png differ diff --git a/src/Database/images/Database/20201024114716460.png b/src/Database/images/Database/20201024114716460.png new file mode 100644 index 0000000..523301e Binary files /dev/null and b/src/Database/images/Database/20201024114716460.png differ diff --git a/src/Database/images/Database/20201024114750255.png b/src/Database/images/Database/20201024114750255.png new file mode 100644 index 0000000..e5f5b35 Binary files /dev/null and b/src/Database/images/Database/20201024114750255.png differ diff --git a/src/Database/images/Database/2020102411481097.png b/src/Database/images/Database/2020102411481097.png new file mode 100644 index 0000000..f58246f Binary files /dev/null and b/src/Database/images/Database/2020102411481097.png differ diff --git a/src/Database/images/Database/20201024114900213.png b/src/Database/images/Database/20201024114900213.png new file mode 100644 index 0000000..b7d30e3 Binary files /dev/null and b/src/Database/images/Database/20201024114900213.png differ diff --git a/src/Database/images/Database/20201024115012887.png b/src/Database/images/Database/20201024115012887.png new file mode 100644 index 0000000..b25427d Binary files /dev/null and b/src/Database/images/Database/20201024115012887.png differ diff --git a/src/Database/images/Database/20201024115203337.png b/src/Database/images/Database/20201024115203337.png new file mode 100644 index 0000000..29f2576 Binary files /dev/null and b/src/Database/images/Database/20201024115203337.png differ diff --git a/src/Database/images/Database/20201024115218222.png b/src/Database/images/Database/20201024115218222.png new file mode 100644 index 0000000..540d8b8 Binary files /dev/null and b/src/Database/images/Database/20201024115218222.png differ diff --git a/src/Database/images/Database/2NF.png b/src/Database/images/Database/2NF.png new file mode 100644 index 0000000..08196e4 Binary files /dev/null and b/src/Database/images/Database/2NF.png differ diff --git a/src/Database/images/Database/3NF.png b/src/Database/images/Database/3NF.png new file mode 100644 index 0000000..fa28184 Binary files /dev/null and b/src/Database/images/Database/3NF.png differ diff --git a/src/Database/images/Database/819dbbcd31605b3a692576932f25d325.png b/src/Database/images/Database/819dbbcd31605b3a692576932f25d325.png new file mode 100644 index 0000000..7b6ea69 Binary files /dev/null and b/src/Database/images/Database/819dbbcd31605b3a692576932f25d325.png differ diff --git a/src/Database/images/Database/B+树索引-等值查询.png b/src/Database/images/Database/B+树索引-等值查询.png new file mode 100644 index 0000000..ef1dd82 Binary files /dev/null and b/src/Database/images/Database/B+树索引-等值查询.png differ diff --git a/src/Database/images/Database/B-Tree案例分析.png b/src/Database/images/Database/B-Tree案例分析.png new file mode 100644 index 0000000..02ab48d Binary files /dev/null and b/src/Database/images/Database/B-Tree案例分析.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Flush链表-缓存页.png b/src/Database/images/Database/Buffer-Pool-Flush链表-缓存页.png new file mode 100644 index 0000000..720d2dd Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Flush链表-缓存页.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Flush链表.png b/src/Database/images/Database/Buffer-Pool-Flush链表.png new file mode 100644 index 0000000..b521ff9 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Flush链表.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Free链表-增删改查.png b/src/Database/images/Database/Buffer-Pool-Free链表-增删改查.png new file mode 100644 index 0000000..924e711 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Free链表-增删改查.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Free链表-获取描述数据.png b/src/Database/images/Database/Buffer-Pool-Free链表-获取描述数据.png new file mode 100644 index 0000000..0f2481a Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Free链表-获取描述数据.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Free链表.png b/src/Database/images/Database/Buffer-Pool-Free链表.png new file mode 100644 index 0000000..07a3298 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Free链表.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Free链表组成.png b/src/Database/images/Database/Buffer-Pool-Free链表组成.png new file mode 100644 index 0000000..05cf1fb Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Free链表组成.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Free链表设计.png b/src/Database/images/Database/Buffer-Pool-Free链表设计.png new file mode 100644 index 0000000..75a0b37 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Free链表设计.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表-无法加载.png b/src/Database/images/Database/Buffer-Pool-LRU链表-无法加载.png new file mode 100644 index 0000000..8590610 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表-无法加载.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表-结构.png b/src/Database/images/Database/Buffer-Pool-LRU链表-结构.png new file mode 100644 index 0000000..09fc1ae Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表-结构.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表-节点.png b/src/Database/images/Database/Buffer-Pool-LRU链表-节点.png new file mode 100644 index 0000000..4922e2a Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表-节点.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表.png b/src/Database/images/Database/Buffer-Pool-LRU链表.png new file mode 100644 index 0000000..aa966a7 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png b/src/Database/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png new file mode 100644 index 0000000..1ee72cb Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表优化-全表扫描.png differ diff --git a/src/Database/images/Database/Buffer-Pool-LRU链表优化.png b/src/Database/images/Database/Buffer-Pool-LRU链表优化.png new file mode 100644 index 0000000..098008b Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-LRU链表优化.png differ diff --git a/src/Database/images/Database/Buffer-Pool-Write-Ahead-Logging.png b/src/Database/images/Database/Buffer-Pool-Write-Ahead-Logging.png new file mode 100644 index 0000000..b10ac22 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-Write-Ahead-Logging.png differ diff --git a/src/Database/images/Database/Buffer-Pool-描述数据.png b/src/Database/images/Database/Buffer-Pool-描述数据.png new file mode 100644 index 0000000..0601f9b Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-描述数据.png differ diff --git a/src/Database/images/Database/Buffer-Pool-缓存页.png b/src/Database/images/Database/Buffer-Pool-缓存页.png new file mode 100644 index 0000000..0e212d3 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-缓存页.png differ diff --git a/src/Database/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png b/src/Database/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png new file mode 100644 index 0000000..25faac3 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-缓存页哈希表-复杂度.png differ diff --git a/src/Database/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png b/src/Database/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png new file mode 100644 index 0000000..3952ec0 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-缓存页哈希表-映射关系.png differ diff --git a/src/Database/images/Database/Buffer-Pool-缓存页哈希表.png b/src/Database/images/Database/Buffer-Pool-缓存页哈希表.png new file mode 100644 index 0000000..cfe5bba Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool-缓存页哈希表.png differ diff --git a/src/Database/images/Database/Buffer-Pool.png b/src/Database/images/Database/Buffer-Pool.png new file mode 100644 index 0000000..cfca559 Binary files /dev/null and b/src/Database/images/Database/Buffer-Pool.png differ diff --git a/src/Database/images/Database/CROSS-JOIN.png b/src/Database/images/Database/CROSS-JOIN.png new file mode 100644 index 0000000..be31bcf Binary files /dev/null and b/src/Database/images/Database/CROSS-JOIN.png differ diff --git a/src/Database/images/Database/DRBD磁盘复制.jpg b/src/Database/images/Database/DRBD磁盘复制.jpg new file mode 100644 index 0000000..c27cc0d Binary files /dev/null and b/src/Database/images/Database/DRBD磁盘复制.jpg differ diff --git a/src/Database/images/Database/FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN.png b/src/Database/images/Database/FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN.png new file mode 100644 index 0000000..2f21d38 Binary files /dev/null and b/src/Database/images/Database/FULL-OUTER-JOIN-EXCLUDING-INNER-JOIN.png differ diff --git a/src/Database/images/Database/FreeSpace剩余空间.jpg b/src/Database/images/Database/FreeSpace剩余空间.jpg new file mode 100644 index 0000000..eaf7939 Binary files /dev/null and b/src/Database/images/Database/FreeSpace剩余空间.jpg differ diff --git a/src/Database/images/Database/Hash索引.png b/src/Database/images/Database/Hash索引.png new file mode 100644 index 0000000..d71dbf1 Binary files /dev/null and b/src/Database/images/Database/Hash索引.png differ diff --git a/src/Database/images/Database/InnoDB-compact行格式.jpg b/src/Database/images/Database/InnoDB-compact行格式.jpg new file mode 100644 index 0000000..324e1e0 Binary files /dev/null and b/src/Database/images/Database/InnoDB-compact行格式.jpg differ diff --git a/src/Database/images/Database/InnoDB-单个数据页内容.jpg b/src/Database/images/Database/InnoDB-单个数据页内容.jpg new file mode 100644 index 0000000..5b9966e Binary files /dev/null and b/src/Database/images/Database/InnoDB-单个数据页内容.jpg differ diff --git a/src/Database/images/Database/InnoDB-数据页-写操作.jpg b/src/Database/images/Database/InnoDB-数据页-写操作.jpg new file mode 100644 index 0000000..d252b27 Binary files /dev/null and b/src/Database/images/Database/InnoDB-数据页-写操作.jpg differ diff --git a/src/Database/images/Database/InnoDB-数据页-读操作.jpg b/src/Database/images/Database/InnoDB-数据页-读操作.jpg new file mode 100644 index 0000000..f6c5b42 Binary files /dev/null and b/src/Database/images/Database/InnoDB-数据页-读操作.jpg differ diff --git a/src/Database/images/Database/InnoDB-数据页.jpg b/src/Database/images/Database/InnoDB-数据页.jpg new file mode 100644 index 0000000..47d862c Binary files /dev/null and b/src/Database/images/Database/InnoDB-数据页.jpg differ diff --git a/src/Database/images/Database/InnoDB-文件头部.jpg b/src/Database/images/Database/InnoDB-文件头部.jpg new file mode 100644 index 0000000..08bae08 Binary files /dev/null and b/src/Database/images/Database/InnoDB-文件头部.jpg differ diff --git a/src/Database/images/Database/InnoDB-最大和最小记录.jpg b/src/Database/images/Database/InnoDB-最大和最小记录.jpg new file mode 100644 index 0000000..f361273 Binary files /dev/null and b/src/Database/images/Database/InnoDB-最大和最小记录.jpg differ diff --git a/src/Database/images/Database/InnoDB-隐藏列.jpg b/src/Database/images/Database/InnoDB-隐藏列.jpg new file mode 100644 index 0000000..f64940a Binary files /dev/null and b/src/Database/images/Database/InnoDB-隐藏列.jpg differ diff --git a/src/Database/images/Database/InnoDB-页目录.jpg b/src/Database/images/Database/InnoDB-页目录.jpg new file mode 100644 index 0000000..c31a1c9 Binary files /dev/null and b/src/Database/images/Database/InnoDB-页目录.jpg differ diff --git a/src/Database/images/Database/InnoDB存储引擎SQL隔离级别锁比较.png b/src/Database/images/Database/InnoDB存储引擎SQL隔离级别锁比较.png new file mode 100644 index 0000000..0ed4118 Binary files /dev/null and b/src/Database/images/Database/InnoDB存储引擎SQL隔离级别锁比较.png differ diff --git a/src/Database/images/Database/InnoDB的锁机制兼容情况.jpg b/src/Database/images/Database/InnoDB的锁机制兼容情况.jpg new file mode 100644 index 0000000..a844301 Binary files /dev/null and b/src/Database/images/Database/InnoDB的锁机制兼容情况.jpg differ diff --git a/src/Database/images/Database/InnoDB页各组成部分简单描述.jpg b/src/Database/images/Database/InnoDB页各组成部分简单描述.jpg new file mode 100644 index 0000000..1c1e82e Binary files /dev/null and b/src/Database/images/Database/InnoDB页各组成部分简单描述.jpg differ diff --git a/src/Database/images/Database/InnoDB页结构示意图.jpg b/src/Database/images/Database/InnoDB页结构示意图.jpg new file mode 100644 index 0000000..141e313 Binary files /dev/null and b/src/Database/images/Database/InnoDB页结构示意图.jpg differ diff --git a/src/Database/images/Database/Innodb-全文索引.png b/src/Database/images/Database/Innodb-全文索引.png new file mode 100644 index 0000000..dff4caf Binary files /dev/null and b/src/Database/images/Database/Innodb-全文索引.png differ diff --git a/src/Database/images/Database/Innodb-聚集索引.png b/src/Database/images/Database/Innodb-聚集索引.png new file mode 100644 index 0000000..4c3f21f Binary files /dev/null and b/src/Database/images/Database/Innodb-聚集索引.png differ diff --git a/src/Database/images/Database/Innodb-非聚集索引.png b/src/Database/images/Database/Innodb-非聚集索引.png new file mode 100644 index 0000000..261bfc1 Binary files /dev/null and b/src/Database/images/Database/Innodb-非聚集索引.png differ diff --git a/src/Database/images/Database/LEFT-JOIN-EXCLUDING-INNER-JOIN.png b/src/Database/images/Database/LEFT-JOIN-EXCLUDING-INNER-JOIN.png new file mode 100644 index 0000000..c1560cb Binary files /dev/null and b/src/Database/images/Database/LEFT-JOIN-EXCLUDING-INNER-JOIN.png differ diff --git a/src/Database/images/Database/LRU链表设计思路.png b/src/Database/images/Database/LRU链表设计思路.png new file mode 100644 index 0000000..4922e2a Binary files /dev/null and b/src/Database/images/Database/LRU链表设计思路.png differ diff --git a/src/Database/images/Database/MHA+多节点集群.jpg b/src/Database/images/Database/MHA+多节点集群.jpg new file mode 100644 index 0000000..e4417e5 Binary files /dev/null and b/src/Database/images/Database/MHA+多节点集群.jpg differ diff --git a/src/Database/images/Database/MHA-Manager.jpg b/src/Database/images/Database/MHA-Manager.jpg new file mode 100644 index 0000000..b0f9238 Binary files /dev/null and b/src/Database/images/Database/MHA-Manager.jpg differ diff --git a/src/Database/images/Database/MySQL-Cluster.jpg b/src/Database/images/Database/MySQL-Cluster.jpg new file mode 100644 index 0000000..994f7cb Binary files /dev/null and b/src/Database/images/Database/MySQL-Cluster.jpg differ diff --git a/src/Database/images/Database/MySQL-Galera.jpg b/src/Database/images/Database/MySQL-Galera.jpg new file mode 100644 index 0000000..8d816fa Binary files /dev/null and b/src/Database/images/Database/MySQL-Galera.jpg differ diff --git a/src/Database/images/Database/MySQL-Paxos.jpg b/src/Database/images/Database/MySQL-Paxos.jpg new file mode 100644 index 0000000..09530fc Binary files /dev/null and b/src/Database/images/Database/MySQL-Paxos.jpg differ diff --git a/src/Database/images/Database/MySQL主从集群.jpg b/src/Database/images/Database/MySQL主从集群.jpg new file mode 100644 index 0000000..e059d12 Binary files /dev/null and b/src/Database/images/Database/MySQL主从集群.jpg differ diff --git a/src/Database/images/Database/MySQL架构设计.jpg b/src/Database/images/Database/MySQL架构设计.jpg new file mode 100644 index 0000000..01a70fe Binary files /dev/null and b/src/Database/images/Database/MySQL架构设计.jpg differ diff --git a/src/Database/images/Database/MySQL查询过程.png b/src/Database/images/Database/MySQL查询过程.png new file mode 100644 index 0000000..ff21c49 Binary files /dev/null and b/src/Database/images/Database/MySQL查询过程.png differ diff --git a/src/Database/images/Database/MySQL索引-explain.jpg b/src/Database/images/Database/MySQL索引-explain.jpg new file mode 100644 index 0000000..49eb86c Binary files /dev/null and b/src/Database/images/Database/MySQL索引-explain.jpg differ diff --git a/src/Database/images/Database/MySQL索引-like使索引失效.png b/src/Database/images/Database/MySQL索引-like使索引失效.png new file mode 100644 index 0000000..ac1d978 Binary files /dev/null and b/src/Database/images/Database/MySQL索引-like使索引失效.png differ diff --git a/src/Database/images/Database/MySQL索引-or使索引失效.jpg b/src/Database/images/Database/MySQL索引-or使索引失效.jpg new file mode 100644 index 0000000..24a3b30 Binary files /dev/null and b/src/Database/images/Database/MySQL索引-or使索引失效.jpg differ diff --git a/src/Database/images/Database/MySQL索引-数据类型不匹配使索引失效.png b/src/Database/images/Database/MySQL索引-数据类型不匹配使索引失效.png new file mode 100644 index 0000000..689cbe9 Binary files /dev/null and b/src/Database/images/Database/MySQL索引-数据类型不匹配使索引失效.png differ diff --git a/src/Database/images/Database/Next-Key-Locks-临键锁.jpg b/src/Database/images/Database/Next-Key-Locks-临键锁.jpg new file mode 100644 index 0000000..37ec331 Binary files /dev/null and b/src/Database/images/Database/Next-Key-Locks-临键锁.jpg differ diff --git a/src/Database/images/Database/Next-Key-Locks.jpg b/src/Database/images/Database/Next-Key-Locks.jpg new file mode 100644 index 0000000..5029adc Binary files /dev/null and b/src/Database/images/Database/Next-Key-Locks.jpg differ diff --git a/src/Database/images/Database/RIGHT-JOIN-EXCLUDING-INNER-JOIN.png b/src/Database/images/Database/RIGHT-JOIN-EXCLUDING-INNER-JOIN.png new file mode 100644 index 0000000..2ff8bfa Binary files /dev/null and b/src/Database/images/Database/RIGHT-JOIN-EXCLUDING-INNER-JOIN.png differ diff --git a/src/Database/images/Database/SAN共享储存.jpg b/src/Database/images/Database/SAN共享储存.jpg new file mode 100644 index 0000000..5c06a45 Binary files /dev/null and b/src/Database/images/Database/SAN共享储存.jpg differ diff --git a/src/Database/images/Database/SQL常用JOIN.png b/src/Database/images/Database/SQL常用JOIN.png new file mode 100644 index 0000000..a1ac639 Binary files /dev/null and b/src/Database/images/Database/SQL常用JOIN.png differ diff --git a/src/Database/images/Database/SQL所有JOIN.png b/src/Database/images/Database/SQL所有JOIN.png new file mode 100644 index 0000000..b4b5a0e Binary files /dev/null and b/src/Database/images/Database/SQL所有JOIN.png differ diff --git a/src/Database/images/Database/SQL查询流程图.png b/src/Database/images/Database/SQL查询流程图.png new file mode 100644 index 0000000..5a157da Binary files /dev/null and b/src/Database/images/Database/SQL查询流程图.png differ diff --git a/src/Database/images/Database/ZooKeeper+Proxy.jpg b/src/Database/images/Database/ZooKeeper+Proxy.jpg new file mode 100644 index 0000000..2289204 Binary files /dev/null and b/src/Database/images/Database/ZooKeeper+Proxy.jpg differ diff --git a/src/Database/images/Database/binlog写入流程.jpg b/src/Database/images/Database/binlog写入流程.jpg new file mode 100644 index 0000000..0acffb0 Binary files /dev/null and b/src/Database/images/Database/binlog写入流程.jpg differ diff --git a/src/Database/images/Database/binlog和redolog区别.jpg b/src/Database/images/Database/binlog和redolog区别.jpg new file mode 100644 index 0000000..775bccf Binary files /dev/null and b/src/Database/images/Database/binlog和redolog区别.jpg differ diff --git a/src/Database/images/Database/binlog文件服务器.jpg b/src/Database/images/Database/binlog文件服务器.jpg new file mode 100644 index 0000000..984b87f Binary files /dev/null and b/src/Database/images/Database/binlog文件服务器.jpg differ diff --git a/src/Database/images/Database/coordinator线程.jpg b/src/Database/images/Database/coordinator线程.jpg new file mode 100644 index 0000000..dd927be Binary files /dev/null and b/src/Database/images/Database/coordinator线程.jpg differ diff --git a/src/Database/images/Database/modb_20210816_ed8bed5e-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ed8bed5e-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..cfca559 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ed8bed5e-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_edb843d6-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_edb843d6-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..0e212d3 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_edb843d6-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_ee04e31c-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ee04e31c-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..0601f9b Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ee04e31c-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_ee4159be-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ee4159be-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..b10ac22 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ee4159be-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_ee96f5fe-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ee96f5fe-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..07a3298 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ee96f5fe-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_eee505d2-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_eee505d2-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..924e711 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_eee505d2-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_ef31ab6c-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ef31ab6c-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..75a0b37 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ef31ab6c-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_ef7cf4dc-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_ef7cf4dc-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..05cf1fb Binary files /dev/null and b/src/Database/images/Database/modb_20210816_ef7cf4dc-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_efb93140-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_efb93140-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..0f2481a Binary files /dev/null and b/src/Database/images/Database/modb_20210816_efb93140-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_f02dad7c-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_f02dad7c-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..cfe5bba Binary files /dev/null and b/src/Database/images/Database/modb_20210816_f02dad7c-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_f065700e-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_f065700e-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..25faac3 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_f065700e-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/modb_20210816_f099ac7a-fe80-11eb-8072-00163e068ecd.png b/src/Database/images/Database/modb_20210816_f099ac7a-fe80-11eb-8072-00163e068ecd.png new file mode 100644 index 0000000..3952ec0 Binary files /dev/null and b/src/Database/images/Database/modb_20210816_f099ac7a-fe80-11eb-8072-00163e068ecd.png differ diff --git a/src/Database/images/Database/redolog位置指针.jpg b/src/Database/images/Database/redolog位置指针.jpg new file mode 100644 index 0000000..3ecc755 Binary files /dev/null and b/src/Database/images/Database/redolog位置指针.jpg differ diff --git a/src/Database/images/Database/redolog写入流程.jpg b/src/Database/images/Database/redolog写入流程.jpg new file mode 100644 index 0000000..3501c15 Binary files /dev/null and b/src/Database/images/Database/redolog写入流程.jpg differ diff --git a/src/Database/images/Database/update的binlog执行流程.jpg b/src/Database/images/Database/update的binlog执行流程.jpg new file mode 100644 index 0000000..5de7eb6 Binary files /dev/null and b/src/Database/images/Database/update的binlog执行流程.jpg differ diff --git a/src/Database/images/Database/主从复制原理.jpg b/src/Database/images/Database/主从复制原理.jpg new file mode 100644 index 0000000..d1a3770 Binary files /dev/null and b/src/Database/images/Database/主从复制原理.jpg differ diff --git a/src/Database/images/Database/主从或主主半同步复制.jpg b/src/Database/images/Database/主从或主主半同步复制.jpg new file mode 100644 index 0000000..ee71630 Binary files /dev/null and b/src/Database/images/Database/主从或主主半同步复制.jpg differ diff --git a/src/Database/images/Database/全连接(FULL-OUTER-JOIN).png b/src/Database/images/Database/全连接(FULL-OUTER-JOIN).png new file mode 100644 index 0000000..ceeed90 Binary files /dev/null and b/src/Database/images/Database/全连接(FULL-OUTER-JOIN).png differ diff --git a/src/Database/images/Database/内连接(INNER-JOIN).png b/src/Database/images/Database/内连接(INNER-JOIN).png new file mode 100644 index 0000000..47d4423 Binary files /dev/null and b/src/Database/images/Database/内连接(INNER-JOIN).png differ diff --git a/src/Database/images/Database/双通道复制.jpg b/src/Database/images/Database/双通道复制.jpg new file mode 100644 index 0000000..16562a1 Binary files /dev/null and b/src/Database/images/Database/双通道复制.jpg differ diff --git a/src/Database/images/Database/右连接(RIGHT-JOIN).png b/src/Database/images/Database/右连接(RIGHT-JOIN).png new file mode 100644 index 0000000..d11170d Binary files /dev/null and b/src/Database/images/Database/右连接(RIGHT-JOIN).png differ diff --git a/src/Database/images/Database/左连接(LEFT-JOIN).png b/src/Database/images/Database/左连接(LEFT-JOIN).png new file mode 100644 index 0000000..7b14683 Binary files /dev/null and b/src/Database/images/Database/左连接(LEFT-JOIN).png differ diff --git a/src/Database/images/Database/并行复制.jpg b/src/Database/images/Database/并行复制.jpg new file mode 100644 index 0000000..c6394e6 Binary files /dev/null and b/src/Database/images/Database/并行复制.jpg differ diff --git a/src/Database/images/Database/按库并行.jpg b/src/Database/images/Database/按库并行.jpg new file mode 100644 index 0000000..0dc5dcd Binary files /dev/null and b/src/Database/images/Database/按库并行.jpg differ diff --git a/src/Database/images/Database/案例分析-B+树-范围查询.png b/src/Database/images/Database/案例分析-B+树-范围查询.png new file mode 100644 index 0000000..40623dd Binary files /dev/null and b/src/Database/images/Database/案例分析-B+树-范围查询.png differ diff --git a/src/Database/images/Database/索引-B+Tree结构.png b/src/Database/images/Database/索引-B+Tree结构.png new file mode 100644 index 0000000..b129159 Binary files /dev/null and b/src/Database/images/Database/索引-B+Tree结构.png differ diff --git a/src/Database/images/Database/索引-B树结构.png b/src/Database/images/Database/索引-B树结构.png new file mode 100644 index 0000000..ba377ac Binary files /dev/null and b/src/Database/images/Database/索引-B树结构.png differ diff --git a/src/Database/images/Database/索引机制-B+树.jpg b/src/Database/images/Database/索引机制-B+树.jpg new file mode 100644 index 0000000..8882e00 Binary files /dev/null and b/src/Database/images/Database/索引机制-B+树.jpg differ diff --git a/src/Database/images/Database/索引机制-B-树.jpg b/src/Database/images/Database/索引机制-B-树.jpg new file mode 100644 index 0000000..afa1222 Binary files /dev/null and b/src/Database/images/Database/索引机制-B-树.jpg differ diff --git a/src/Database/images/Database/索引机制-B星树.jpg b/src/Database/images/Database/索引机制-B星树.jpg new file mode 100644 index 0000000..0aeed93 Binary files /dev/null and b/src/Database/images/Database/索引机制-B星树.jpg differ diff --git a/src/Database/images/Database/索引结构-B+Tree.png b/src/Database/images/Database/索引结构-B+Tree.png new file mode 100644 index 0000000..b6a5d98 Binary files /dev/null and b/src/Database/images/Database/索引结构-B+Tree.png differ diff --git a/src/Database/images/Database/索引结构-B+Tree指针.png b/src/Database/images/Database/索引结构-B+Tree指针.png new file mode 100644 index 0000000..c6d59cf Binary files /dev/null and b/src/Database/images/Database/索引结构-B+Tree指针.png differ diff --git a/src/Database/images/Database/索引结构-B+Tree案例.png b/src/Database/images/Database/索引结构-B+Tree案例.png new file mode 100644 index 0000000..dae81a7 Binary files /dev/null and b/src/Database/images/Database/索引结构-B+Tree案例.png differ diff --git a/src/Database/images/Database/索引结构-B-Tree.png b/src/Database/images/Database/索引结构-B-Tree.png new file mode 100644 index 0000000..1bc2581 Binary files /dev/null and b/src/Database/images/Database/索引结构-B-Tree.png differ diff --git a/src/Database/images/Database/索引结构-B-Tree指针.png b/src/Database/images/Database/索引结构-B-Tree指针.png new file mode 100644 index 0000000..025dbed Binary files /dev/null and b/src/Database/images/Database/索引结构-B-Tree指针.png differ diff --git a/src/Database/images/Database/索引结构-B-Tree问题.png b/src/Database/images/Database/索引结构-B-Tree问题.png new file mode 100644 index 0000000..e7e8adf Binary files /dev/null and b/src/Database/images/Database/索引结构-B-Tree问题.png differ diff --git a/src/Database/images/Database/索引结构-二叉树.png b/src/Database/images/Database/索引结构-二叉树.png new file mode 100644 index 0000000..1ea66e7 Binary files /dev/null and b/src/Database/images/Database/索引结构-二叉树.png differ diff --git a/src/Database/images/Database/索引结构-红黑树.png b/src/Database/images/Database/索引结构-红黑树.png new file mode 100644 index 0000000..e4135ac Binary files /dev/null and b/src/Database/images/Database/索引结构-红黑树.png differ diff --git a/src/Database/images/Database/自适应哈希索引.png b/src/Database/images/Database/自适应哈希索引.png new file mode 100644 index 0000000..ae79318 Binary files /dev/null and b/src/Database/images/Database/自适应哈希索引.png differ diff --git a/src/Database/images/Database/解决死锁.jpg b/src/Database/images/Database/解决死锁.jpg new file mode 100644 index 0000000..ef8b36d Binary files /dev/null and b/src/Database/images/Database/解决死锁.jpg differ diff --git a/src/JAVA/1.md b/src/JAVA/1.md new file mode 100644 index 0000000..b1863f6 --- /dev/null +++ b/src/JAVA/1.md @@ -0,0 +1,26 @@ +JAVA里面进行多线程通信的主要方式就是 `共享内存` 的方式,共享内存主要的关注点有两个:`可见性` 和 `有序性`。加上复合操作的 `原子性`,可以认为JAVA的线程安全性问题主要关注点有3个(JAVA内存模型JMM解决了可见性和有序性的问题,而锁解决了原子性的问题):`可见性`、`有序性`、`原子性` + +- **原子性(Atomicity)**:在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败 +- **有序性(Ordering)**:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序) +- **可见性(Visibility)**:指在多线程环境下,当一个线程修改了某一个共享变量的值,其它线程能够立刻知道这个修改 + + + +**① 重排序** + +指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。从JAVA源码到最终实际执行的指令序列,会经历下面3种重排序(主要流程): + +![源码指令](images/JAVA/源码指令.png) + +**指令重排序分类** + +- **编译器优化的重排序**:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; +- **指令级并行的重排序**:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序; +- **内存系统的重排序**:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。 + +**② 顺序一致性** + +顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。顺序一致性特征如下: + +- 一个线程中的所有操作必须按照程序的顺序来执行 +- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性的内存模型中,每个操作必须原子执行并且立刻对所有线程可见 \ No newline at end of file diff --git a/src/JAVA/10.md b/src/JAVA/10.md new file mode 100644 index 0000000..4d7328f --- /dev/null +++ b/src/JAVA/10.md @@ -0,0 +1,9 @@ +LongAdder是JDK1.8开始出现的,所提供的API基本上可替换掉原先AtomicLong。LongAdder所使用思想就是**热点分离**,这点可以类比一下ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。如下图所示: + +![LongAdder原理](images/JAVA/LongAdder原理.png) + +LonAdder和AtomicLong性能测试对比: + +![LongAdder和AtomicLong性能对比](images/JAVA/LongAdder和AtomicLong性能对比.png) + +LongAdder就是基于Striped64实现,用于并发计数时,若不存在竞争或竞争比较低时,LongAdder具有和AtomicLong差不多的效率。但是,高并发环境下,竞争比较严重时,LongAdder的cells表发挥作用,将并发更新分散到不同Cell进行,有效降低了CAS更新的竞争,从而极大提高了LongAdder的并发计数能力。因此,高并发场景下,要实现吞吐量更高的计数器,推荐使用LongAdder。 \ No newline at end of file diff --git a/src/JAVA/101.md b/src/JAVA/101.md new file mode 100644 index 0000000..d74464c --- /dev/null +++ b/src/JAVA/101.md @@ -0,0 +1,69 @@ +### ArrayList(数组) + +ArrayList 是一个**数组队列**,相当于 **动态数组**。与Java中的数组相比,它的容量能动态增长。 + +它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。 + + + +### LinkedList(链表) + +LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。 + +- LinkedList是有序的双向链表 +- LinkedList在内存中开辟的内存不连续【ArrayList开辟的内存是连续的】 +- LinkedList插入和删除元素很快,但是查询很慢【相对于ArrayList】 + + + +### Vector(数组实现、线程同步) + +Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。 + + + +### CopyOnWriteArrayList + +**Copy-On-Write是什么?** + +顾名思义,在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行`写`操作,而是将内存拷贝一份,在新的内存中进行`写`操作,`写`完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉。 + + + +CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口,支持高并发。和ArrayList一样,它是个可变数组,但是和ArrayList不同的时,它具有以下特性: + +- 最适合于具有以下特征的应用:List大小通常保持很小,只读操作远多于可变操作,需在遍历期间防止线程间的冲突 +- 它是线程安全的 +- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大 +- 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作 +- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照 + + + +**CopyOnWriteArrayList数据结构** + +![CopyOnWriteArrayList数据结构](images/JAVA/CopyOnWriteArrayList数据结构.jpg) + +- CopyOnWriteArrayList实现了List接口,因此它是一个队列 +- CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问 +- CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的 + + + +**CopyOnWriteArrayList原理** + +下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。 + +- **CopyOnWriteArrayList的“动态数组”机制** + + 它内部有个“volatile数组”(array)来保持数据。 + + 在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因。CopyOnWriteArrayList就是通过这种方式实现的动态数组,不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很 + 低。但是单单只是进行遍历查找的话,效率比较高 + +- **CopyOnWriteArrayList的“线程安全”机制** + + 是通过volatile和互斥锁来实现的。 + + - **CopyOnWriteArrayList是通过“volatile数组”来保存数据的**。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入。就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证 + - **CopyOnWriteArrayList通过互斥锁来保护数据**。在“添加、修改、删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的 \ No newline at end of file diff --git a/src/JAVA/102.md b/src/JAVA/102.md new file mode 100644 index 0000000..58ca445 --- /dev/null +++ b/src/JAVA/102.md @@ -0,0 +1,71 @@ +### HashSet(Hash表) + +**HashSet 基于 HashMap,底层是通过 HashMap 的API来实现的。**哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法,如果 equals 结果为 true ,HashSet 就视为同一个元素;如果 equals 为 false 就不是同一个元素。hashcode相同,equals不相等,则使用链表存储。 + + + +### TreeSet(二叉树) + +底层通过TreeMap来实现,非线程安全,具有排序功能(自然排序(默认)和自定义排序)。它继承AbstractSet,实现NavigableSet(搜索功能), Cloneable(克隆), Serializable(序列化,可用hessian协议来传输)接口。 + +- TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置 +- Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用 +- 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序 +- 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数 + + + +### LinkedHashSet + +对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。 + + + +### ConcurrentSkipListSet + +ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。 + + + +### CopyOnWriteArraySet + +CopyOnWriteArraySet是线程安全的无序的集合,可以将它理解成线程安全的HashSet。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。 + +- **通过“动态数组(CopyOnWriteArrayList)”实现(HashSet是通过“散列表(HashMap)”实现的)** +- **线程安全是通过volatile和互斥锁来实现** +- **无序的不能重复集合** + + + +**CopyOnWriteArraySet特性** + +- 它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突 +- 它是线程安全的 +- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大 +- 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作 +- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照 + + + +**数据结构** + +![CopyOnWriteArraySet数据结构](images/JAVA/CopyOnWriteArraySet数据结构.jpg) + +- CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合 +- CopyOnWriteArraySet包含CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”! CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作 + + + +### ConcurrentSkipListSet + +ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。 + + + +**数据结构** + + ![ConcurrentSkipListSet数据结构](images/JAVA/ConcurrentSkipListSet数据结构.jpg) + +- ConcurrentSkipListSet继承于AbstractSet。因此,它本质上是一个集合 +- ConcurrentSkipListSet实现了NavigableSet接口。因此,ConcurrentSkipListSet是一个有序的集合 +- ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的。它包含一个ConcurrentNavigableMap对象m,而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap的实例。ConcurrentSkipListMap中的元素是key-value键值对;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key \ No newline at end of file diff --git a/src/JAVA/103.md b/src/JAVA/103.md new file mode 100644 index 0000000..39b8a28 --- /dev/null +++ b/src/JAVA/103.md @@ -0,0 +1,418 @@ +### HashMap(数组+链表+红黑树) + +**工作原理** + +**HashMap(数组+链表+红黑树)**。HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍HashMap 的结构。 + + + +hashCode 是定位的,**存储位置**;equals是定性的,**比较两者是否相等**。 + +**put()** + +- 第一步:调用 hash(K) 方法**计算 K 的 hash 值**,然后结合数组长度,计算得数组下标 +- 第二步:**调整数组大小**(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n) +- 第三步: + - 如果 **K 的 hash 值**在 HashMap 中**不存在**,则执行**插入**,若存在,则发生**碰撞** + - 如果 K 的 hash 值在 HashMap 中**存在**,且它们两者 **equals 返回 true**,则**更新键值对** + - 如果 K 的 hash 值在 HashMap 中**存在**,且它们两者 **equals 返回 false**,则**插入链表的尾部(尾插法)或者红黑树中(树的添加方式)** + +**get()** + +- 第一步:调用 hash(K) 方法(**计算 K 的 hash 值**)从而**获取该键值所在链表的数组下标** +- 第二步:顺序遍历链表,equals()方法查找**相同 Node 链表中 K 值**对应的 V 值 + + + +![Java7HashMap结构](images/JAVA/Java7HashMap结构.png) + +大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。 + +- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍 +- loadFactor:负载因子,默认为 0.75 +- threshold:扩容的阈值,等于 capacity * loadFactor + + + +![Java8HashMap结构](images/JAVA/Java8HashMap结构.png) + +Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。 + + + +HashMap具有如下特性: + +- HashMap 的存取是没有顺序的 +- KV 均允许为 NULL +- 多线程情况下该类不安全,可以考虑用 HashTable +- JDk8底层是数组 + 链表 + 红黑树,JDK7底层是数组 + 链表 +- 初始容量和装载因子是决定整个类性能的关键点,轻易不要动 +- HashMap是**懒汉式**创建的,只有在你put数据时候才会 build +- 单向链表转换为红黑树的时候会先变化为**双向链表**最终转换为**红黑树**,切记双向链表跟红黑树是`共存`的 +- 对于传入的两个`key`,会强制性的判别出个高低,目的是为了决定向左还是向右放置数据 +- 链表转红黑树后会努力将红黑树的`root`节点和链表的头节点 跟`table[i]`节点融合成一个 +- 在删除时候是先判断删除节点红黑树个数是否需要转链表,不转链表就跟`RBT`类似,找个合适的节点来填充已删除的节点 +- 红黑树的`root`节点`不一定`跟`table[i]`也就是链表的头节点是同一个,三者同步是靠`MoveRootToFront`实现的。而`HashIterator.remove()`会在调用`removeNode`的时候`movable=false` + + + +### TreeMap(可排序) + +TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException 类型的异常。 + + + +### HashTable(线程安全) + +Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。 + + + +### LinkedHashMap(记录插入顺序) + +LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。 + + + +**如何实现LRUCache?** + +LRU(Least Recently Used)**最近最少使用算法**。LruCache就是利用 LinkedHashMap 的一个特性( `accessOrder=true`基于访问顺序 )再加上对 LinkedHashMap 的`数据操作上锁`实现的缓存策略。 + +- `LruCache`是通过`LinkedHashMap`构造方法的第三个参数的`accessOrder=true`实现了`LinkedHashMap`的数据排序基于访问顺序 (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除,从而实现了LRU数据缓存机制 +- 然后在每次 `LruCache.get(K key)` 方法里都会调用`LinkedHashMap.get(Object key)` +- 如上述设置了 `accessOrder=true` 后,每次 `LinkedHashMap.get(Object key)`都会进行 `LinkedHashMap.makeTail(LinkedEntry e)` +- LinkedHashMap 是`双向循环链表`,然后每次 `LruCache.get -> LinkedHashMap.get` 的数据就被放到最末尾了 +- 在 put 和 `trimToSize` 的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部) + +- LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了) +- LruCache 自身并没有释放内存,将LinkedHashMap的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存 +- 覆写entryRemoved方法能知道 LruCache 数据移除时是否发生了冲突,也可以去手动释放资源 +- maxSize 和 sizeOf(K key, V value) 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 ) + + + + +#### LruCache的唯一构造方法 + +```java +/** + * LruCache的构造方法:需要传入最大缓存个数 + */ +public LruCache(int maxSize) { + + ... + + this.maxSize = maxSize; + /* + * 初始化LinkedHashMap + * 第一个参数:initialCapacity,初始大小 + * 第二个参数:loadFactor,负载因子=0.75f,即到75%容量的时候就会扩容 + * 第三个参数:①accessOrder=true基于访问顺序排序②accessOrder=false基于插入顺序排序 + */ + this.map = new LinkedHashMap(0, 0.75f, true); +} +``` + + + +#### LruCache.get(K key) + +下述的 get 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 mapValue = map.get(key);里,调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true。 + +```java +/** + * 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。 + * 如果值返回了,那么它将被移动到双向循环链表的的尾部。 + * 如果如果没有缓存的值,则返回 null。 + */ +public final V get(K key) { + + ... + + V mapValue; + synchronized (this) { + // 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序 + mapValue = map.get(key); + // 计算 命中次数 + if (mapValue != null) { + hitCount++; + return mapValue; + } + // 计算 丢失次数 + missCount++; + } + + /* + * 官方解释: + * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时 + * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。 + */ + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + /*************************** + * 不覆写create方法走不到下面 * + ***************************/ + + /* + * 正常情况走不到这里 + * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑 + * 因为默认的 create(K key) 逻辑为null + */ + synchronized (this) { + // 记录 create 的次数 + createCount++; + // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值 + mapValue = map.put(key, createdValue); + + // 如果之前存在相同key的value,即有冲突。 + if (mapValue != null) { + /* + * 有冲突 + * 所以 撤销 刚才的 操作 + * 将 之前相同key 的值 重新放回去 + */ + map.put(key, mapValue); + } else { + // 拿到键值对,计算出在容量中的相对长度,然后加上 + size += safeSizeOf(key, createdValue); + } + } + + // 如果上面 判断出了 将要放入的值发生冲突 + if (mapValue != null) { + /* + * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了 + * 告诉 自定义 的 entryRemoved 方法 + */ + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + // 上面 进行了 size += 操作 所以这里要重整长度 + trimToSize(maxSize); + return createdValue; + } +} +``` + + + +#### LinkedHashMap.get(Object key) + +主要看`if (accessOrder)`逻辑即可,如果`accessOrder=true`那么每次get都会执行N次 `makeTail(LinkedEntry e)` 。多了一步mainTail动作,把获取的数据,移到双向链表的尾部tail。 + +```java +/** + * Returns the value of the mapping with the specified key. + * + * @param key the key. + * @return the value of the mapping with the specified key, or {@code null} if no mapping for the specified key is found. + */ +@Override +public V get(Object key) { + /* + * This method is overridden to eliminate the need for a polymorphic + * invocation in superclass at the expense of code duplication. + */ + if (key == null) { + HashMapEntry e = entryForNullKey; + if (e == null) + return null; + if (accessOrder) + makeTail((LinkedEntry) e); //把访问的节点迁移到链表的尾部 + return e.value; + } + + int hash = Collections.secondaryHash(key); + HashMapEntry[] tab = table; + for (HashMapEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { //从数组中获取 + K eKey = e.key; + if (eKey == key || (e.hash == hash && key.equals(eKey))) { + if (accessOrder) + makeTail((LinkedEntry) e); //把访问的节点迁移到链表尾部。 + return e.value; + } + } + return null; +} + +/** + * Relinks the given entry to the tail of the list. Under access ordering, + * this method is invoked whenever the value of a pre-existing entry is + * read by Map.get or modified by Map.put. + */ +private void makeTail(LinkedEntry e) { + // Unlink e 在链表中删除该节点e + e.prv.nxt = e.nxt; + e.nxt.prv = e.prv; + + // Relink e as tail 在尾部添加 + LinkedEntry header = this.header; + LinkedEntry oldTail = header.prv; + e.nxt = header; + e.prv = oldTail; + oldTail.nxt = header.prv = e; + modCount++; +} +``` + + + +#### LruCache.put(K key, V value) + +```java +public final V put(K key, V value) { + ... + synchronized (this) { + ... + // 拿到键值对,计算出在容量中的相对长度,然后加上 + size += safeSizeOf(key, value); + ... + } + ... + trimToSize(maxSize); + return previous; +} +``` + +注意: + +- `put` 开始的时候确实是把值放入 `LinkedHashMap` 了,不管超不超过你设定的缓存容量 +- 然后根据 `safeSizeOf` 方法计算 此次添加数据的容量是多少,并且加到 `size` 里 +- 说到 `safeSizeOf` 就要讲到 `sizeOf(K key, V value)` 会计算出此次添加数据的大小 +- 直到 `put` 要结束时,进行了 `trimToSize` 才判断 `size` 是否 大于 `maxSize` 然后进行最近很少访问数据的移除 + + + +#### LruCache.trimToSize(int maxSize) + +会判断之前 `size` 是否大于 `maxSize` 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 `makeTail` 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。 + +```java +public void trimToSize(int maxSize) { + /* + * 这是一个死循环, + * 1.只有 扩容 的情况下能立即跳出 + * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出 + */ + while (true) { + K key; + V value; + synchronized (this) { + // 在重新调整容量大小前,本身容量就为空的话,会出异常的。 + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); + } + // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况 + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + // 拿到键值对,计算出在容量中的相对长度,然后减去。 + size -= safeSizeOf(key, value); + // 添加一次收回次数 + evictionCount++; + } + /* + * 将最后一次删除的最少访问数据回调出去 + */ + entryRemoved(true, key, value, null); + } +} +``` + + + +### ConcurrentHashMap + +ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。 + + + +**Segment** **段** + +ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个segment。 + + + +**线程安全(Segment继承ReentrantLock加锁)** + +简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。 + +![Java7ConcurrentHashMap结构](images/JAVA/Java7ConcurrentHashMap结构.png) + + + +**并行度(默认16)** + +concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。 + + + +**Java8** **实现 (引入了红黑树)** + +Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。 + +![Java8ConcurrentHashMap结构](images/JAVA/Java8ConcurrentHashMap结构.png) + + + +**ConcurrentHashMap并发** + +- **减小锁粒度** +- **ConcurrentHashMap 分段锁(Segment)** + + + +**ConcurrentHashMap在1.7和1.8的区别** + +- **整体结构** + - 1.7:`Segment + HashEntry + Unsafe` + - 1.8:移除Segment,使锁的粒度更小,`Synchronized + CAS + Node + Unsafe` + +- **put()** + + - 1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起 + 1. 定位Segment:先通过key的 `rehash值的高位` 和 `segments数组大小-1` 相与得到在 segments中的位置 + 2. 定位桶:然后在通过 `key的rehash值` 和 `table数组大小-1` 相与得到在table中的位置 + + - 1.8:由于移除了Segment,可根据 `rehash值` 直接定位到桶,拿到table[i]的 `首节点first`后进行判断: + 1. 如果为 `null` ,通过 `CAS` 的方式把 value put进去 + 2. 如果 `非null` ,并且 `first.hash == -1` ,说明其他线程在扩容,参与一起扩容 + 3. 如果 `非null` ,并且 `first.hash != -1` ,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。 + +- **get()** + - 1.7和1.8基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁 + +- **resize()** + - 1.7:与HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容(1.`new个2倍数组` 2.`遍历old数组节点搬去新数组`),避免了HashMap在1.7中扩容时死循环的问题,保证线程安全 + - 1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了 + +- **size()** + - 1.7:很经典的思路 + 1. 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回 + 2. 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的 `Count` 的和 + - 1.8:由于没有segment的概念,所以只需要用一个 `baseCount` 变量来记录ConcurrentHashMap 当前 `节点的个数`。 + 1. 先尝试通过CAS 修改 `baseCount` + 2. 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将 `CELLSBUSY` 置1,成功则可以把 `baseCount变化的次数` 暂存到一个数组 `counterCells` 里,后续数组 `counterCells` 的值会加到 `baseCount` 中 + 3. 如果 `CELLSBUSY` 置1失败又会反复进行CAS`baseCount` 和 CAS`counterCells`数组 + + + +### ConcurrentSkipListMap + +ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap)。它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。 + + + +**数据结构** + +![ConcurrentSkipListMap数据结构](images/JAVA/ConcurrentSkipListMap数据结构.jpg) + +ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。关于跳表(Skip List),它是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。 \ No newline at end of file diff --git a/src/JAVA/11.md b/src/JAVA/11.md new file mode 100644 index 0000000..5b63418 --- /dev/null +++ b/src/JAVA/11.md @@ -0,0 +1,10 @@ +Semaphore是一个计数信号量,它的本质是一个"共享锁"。信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。 + + + +**数据结构** + +![Semaphore数据结构](images/JAVA/Semaphore数据结构.jpg) + +- 和"ReentrantLock"一样,Semaphore也包含sync对象,sync是Sync类型;而且Sync是一个继承于AQS的抽象类 +- Sync包括两个子类:"公平信号量"FairSync 和 "非公平信号量"NonfairSync。sync是"FairSync的实例",或者"NonfairSync的实例";默认情况下,sync是NonfairSync(即,默认是非公平信号量) \ No newline at end of file diff --git a/src/JAVA/12.md b/src/JAVA/12.md new file mode 100644 index 0000000..721cb61 --- /dev/null +++ b/src/JAVA/12.md @@ -0,0 +1,16 @@ +CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。 + + + +**比较CountDownLatch和CyclicBarrier** + +- CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待 +- CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier + + + +**数据结构** + +![CyclicBarrier数据结构](images/JAVA/CyclicBarrier数据结构.jpg) + +CyclicBarrier是包含了"ReentrantLock对象lock"和"Condition对象trip",它是通过独占锁实现的。下面通过源码去分析到底是如何实现的。 \ No newline at end of file diff --git a/src/JAVA/13.md b/src/JAVA/13.md new file mode 100644 index 0000000..cce59e8 --- /dev/null +++ b/src/JAVA/13.md @@ -0,0 +1,16 @@ +CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 + + + +**CountDownLatch和CyclicBarrier的区别** + +- CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待 +- CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier + + + +**数据结构** + +![CountDownLatch数据结构](images/JAVA/CountDownLatch数据结构.jpg) + +CountDownLatch的数据结构很简单,它是通过"共享锁"实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。 \ No newline at end of file diff --git a/src/JAVA/14.md b/src/JAVA/14.md new file mode 100644 index 0000000..47c1438 --- /dev/null +++ b/src/JAVA/14.md @@ -0,0 +1,329 @@ +CompletableFuture是Java 8新增的一个类,用于异步编程,继承了Future和CompletionStage。Future主要具备对请求结果独立处理的功能,CompletionStage用于实现流式处理,实现异步请求的各个阶段组合或链式处理,因此CompletableFuture能实现整个异步调用接口的扁平化和流式处理,解决原有Future处理一系列链式异步请求时的复杂编码: + +![img](images/JAVA/CompletableFuture.jpg) + +**Future的局限性** + +- **Future 的结果在非阻塞的情况下,不能执行更进一步的操作** + + 我们知道,使用Future时只能通过isDone()方法判断任务是否完成,或者通过get()方法阻塞线程等待结果返回,它不能非阻塞的情况下,执行更进一步的操作 + +- **不能组合多个Future的结果** + + 假设你有多个Future异步任务,你希望最快的任务执行完时,或者所有任务都执行完后,进行一些其他操作 + +- **多个Future不能组成链式调用** + + 当异步任务之间有依赖关系时,Future不能将一个任务的结果传给另一个异步任务,多个Future无法创建链式的工作流 + +- **没有异常处理** + + + +**注意事项** + +- **CompletableFuture默认线程池是否满足使用** + + 前面提到创建CompletableFuture异步任务的静态方法runAsync和supplyAsync等,可以指定使用的线程池,不指定则用CompletableFuture的默认线程池: + + ```java + private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor(); + ``` + + 可以看到,CompletableFuture默认线程池是调用ForkJoinPool的commonPool()方法创建,这个默认线程池的核心线程数量根据CPU核数而定,公式为`Runtime.getRuntime().availableProcessors() - 1`,以4核双槽CPU为例,核心线程数量就是`4*2-1=7`个。这样的设置满足CPU密集型的应用,但对于业务都是IO密集型的应用来说,是有风险的,当qps较高时,线程数量可能就设的太少了,会导致线上故障。所以可以根据业务情况自定义线程池使用。 + +- **get设置超时时间不能串行get,不然会导致接口延时`线程数量\*超时时间`** + + + +**① 创建异步任务** + +通常可以使用下面几个CompletableFuture的静态方法创建一个异步任务 + +```java +// 创建无返回值的异步任务 +public static CompletableFuture runAsync(Runnable runnable); +// 无返回值,可指定线程池(默认使用ForkJoinPool.commonPool) +public static CompletableFuture runAsync(Runnable runnable, Executor executor); +// 创建有返回值的异步任务 +public static CompletableFuture supplyAsync(Supplier supplier); +// 有返回值,可指定线程池 +public static CompletableFuture supplyAsync(Supplier supplier, Executor executor); +``` + +使用示例: + +```java +Executor executor = Executors.newFixedThreadPool(10); +CompletableFuture future = CompletableFuture.runAsync(() -> { + //do something +}, executor); + +int poiId = 111; +CompletableFuture future = CompletableFuture.supplyAsync(() -> { + PoiDTO poi = poiService.loadById(poiId); + return poi.getName(); +}); +// Block and get the result of the Future +String poiName = future.get(); +``` + + + +**② 使用回调方法** + +通过`future.get()`方法获取异步任务的结果,还是会阻塞的等待任务完成 + +CompletableFuture提供了几个回调方法,可以不阻塞主线程,在异步任务完成后自动执行回调方法中的代码 + +```java +// 无参数、无返回值 +public CompletableFuture thenRun(Runnable runnable); +// 接受参数,无返回值 +public CompletableFuture thenAccept(Consumer action); +// 接受参数T,有返回值U +public CompletableFuture thenApply(Function fn); +``` + +使用示例: + +```java +CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello") + .thenRun(() -> System.out.println("do other things. 比如异步打印日志或发送消息")); +// 如果只想在一个CompletableFuture任务执行完后,进行一些后续的处理,不需要返回值,那么可以用thenRun回调方法来完成。 +// 如果主线程不依赖thenRun中的代码执行完成,也不需要使用get()方法阻塞主线程。 + +CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello") + .thenAccept((s) -> System.out.println(s + " world")); +// 输出:Hello world +// 回调方法希望使用异步任务的结果,并不需要返回值,那么可以使用thenAccept方法 + +CompletableFuture future = CompletableFuture.supplyAsync(() -> { + PoiDTO poi = poiService.loadById(poiId); + return poi.getMainCategory(); +}).thenApply((s) -> isMainPoi(s)); // boolean isMainPoi(int poiId); +future.get(); +// 希望将异步任务的结果做进一步处理,并需要返回值,则使用thenApply方法。 +// 如果主线程要获取回调方法的返回,还是要用get()方法阻塞得到 +``` + + + +**③ 组合两个异步任务** + +```java +// thenCompose方法中的异步任务依赖调用该方法的异步任务 +public CompletableFuture thenCompose(Function> fn); +// 用于两个独立的异步任务都完成的时候 +public CompletableFuture thenCombine(CompletionStage other, BiFunction fn); +``` + +使用示例: + +```java +CompletableFuture> poiFuture = CompletableFuture.supplyAsync( + () -> poiService.queryPoiIds(cityId, poiId) +); +// 第二个任务是返回CompletableFuture的异步方法 +CompletableFuture> getDeal(List poiIds){ + return CompletableFuture.supplyAsync(() -> poiService.queryPoiIds(poiIds)); +} +// thenCompose +CompletableFuture> resultFuture = poiFuture.thenCompose(poiIds -> getDeal(poiIds)); +resultFuture.get(); +``` + +thenCompose和thenApply的功能类似,两者区别在于thenCompose接受一个返回`CompletableFuture`的Function,当想从回调方法返回的`CompletableFuture`中直接获取结果U时,就用thenCompose。如果使用thenApply,返回结果resultFuture的类型是`CompletableFuture>>`,而不是`CompletableFuture>` + +```java +CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello") + .thenCombine(CompletableFuture.supplyAsync(() -> "world"), (s1, s2) -> s1 + s2); +// future.get() +``` + + + +**④ 组合多个CompletableFuture** + +当需要多个异步任务都完成时,再进行后续处理,可以使用**allOf**方法: + +```java +CompletableFuture poiIDTOFuture = CompletableFuture + .supplyAsync(() -> poiService.loadPoi(poiId)) + .thenAccept(poi -> { + model.setModelTitle(poi.getShopName()); + // do more thing + }); + +CompletableFuture productFuture = CompletableFuture + .supplyAsync(() -> productService.findAllByPoiIdOrderByUpdateTimeDesc(poiId)) + .thenAccept(list -> { + model.setDefaultCount(list.size()); + model.setMoreDesc("more"); + }); +// future3等更多异步任务,这里就不一一写出来了 + +// allOf组合所有异步任务,并使用join获取结果 +CompletableFuture.allOf(poiIDTOFuture, productFuture, future3, ...).join(); +``` + +该方法挺适合C端的业务,通过poiId异步的从多个服务拿门店信息,然后组装成自己需要的模型,最后所有门店信息都填充完后返回。这里使用了join方法获取结果,它和get方法一样阻塞的等待任务完成。多个异步任务有任意一个完成时就返回结果,可以使用**anyOf**方法: + +```java +CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + return "Result of Future 1"; +}); + +CompletableFuture future2 = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + return "Result of Future 2"; +}); + +CompletableFuture future3 = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + return "Result of Future 3"; +}); + +CompletableFuture anyOfFuture = CompletableFuture.anyOf(future1, future2, future3); + +System.out.println(anyOfFuture.get()); // Result of Future 2 +``` + + + +**⑤ 异常处理** + +```java +Integer age = -1; + +CompletableFuture maturityFuture = CompletableFuture.supplyAsync(() -> { + if(age < 0) { + throw new IllegalArgumentException("Age can not be negative"); + } + if(age > 18) { + return "Adult"; + } else { + return "Child"; + } +}).exceptionally(ex -> { + System.out.println("Oops! We have an exception - " + ex.getMessage()); + return "Unknown!"; +}).thenAccept(s -> System.out.print(s)); +// Unkown! +``` + +exceptionally方法可以处理异步任务的异常,在出现异常时,给异步任务链一个从错误中恢复的机会,可以在这里记录异常或返回一个默认值。使用handler方法也可以处理异常,并且无论是否发生异常它都会被调用: + +```java +Integer age = -1; + +CompletableFuture maturityFuture = CompletableFuture.supplyAsync(() -> { + if(age < 0) { + throw new IllegalArgumentException("Age can not be negative"); + } + if(age > 18) { + return "Adult"; + } else { + return "Child"; + } +}).handle((res, ex) -> { + if(ex != null) { + System.out.println("Oops! We have an exception - " + ex.getMessage()); + return "Unknown!"; + } + return res; +}); +``` + + + +**⑥ 分片处理** + +分片和并行处理:分片借助stream实现,然后通过CompletableFuture实现并行执行,最后做数据聚合(其实也是stream的方法)。CompletableFuture并不提供单独的分片api,但可以借助stream的分片聚合功能实现。举个例子: + +```java +// 请求商品数量过多时,做分批异步处理 +List> skuBaseIdsList = ListUtils.partition(skuIdList, 10);//分片 +// 并行 +List>> futureList = Lists.newArrayList(); +for (List skuId : skuBaseIdsList) { + CompletableFuture> tmpFuture = getSkuSales(skuId); + futureList.add(tmpFuture); +} +// 聚合 +futureList.stream().map(CompletalbleFuture::join).collent(Collectors.toList()); +``` + + + +**⑦ 应用案例** + +首先还是需要先完成分工方案,在下面的程序中,我们分了3个任务: + +- 任务1负责洗水壶、烧开水 +- 任务2负责洗茶壶、洗茶杯和拿茶叶 +- 任务3负责泡茶。其中任务3要等待任务1和任务2都完成后才能开始 + +```java +// 任务1:洗水壶->烧开水 +CompletableFuture f1 = + CompletableFuture.runAsync(()->{ + System.out.println("T1:洗水壶..."); + sleep(1, TimeUnit.SECONDS); + + System.out.println("T1:烧开水..."); + sleep(15, TimeUnit.SECONDS); +}); + +// 任务2:洗茶壶->洗茶杯->拿茶叶 +CompletableFuture f2 = + CompletableFuture.supplyAsync(()->{ + System.out.println("T2:洗茶壶..."); + sleep(1, TimeUnit.SECONDS); + + System.out.println("T2:洗茶杯..."); + sleep(2, TimeUnit.SECONDS); + + System.out.println("T2:拿茶叶..."); + sleep(1, TimeUnit.SECONDS); + return "龙井"; +}); + +// 任务3:任务1和任务2完成后执行:泡茶 +CompletableFuture f3 = + f1.thenCombine(f2, (__, tf)->{ + System.out.println("T3:拿到茶叶:" + tf); + System.out.println("T3:泡茶..."); + return "上茶:" + tf; + }); + +// 等待任务3执行结果 +System.out.println(f3.join()); +void sleep(int t, TimeUnit u) { + try { + u.sleep(t); + }catch(InterruptedException e){} +} + +// 一次执行结果: +T1:洗水壶... +T2:洗茶壶... +T1:烧开水... +T2:洗茶杯... +T2:拿茶叶... +T3:拿到茶叶:龙井 +T3:泡茶... +上茶:龙井 +``` \ No newline at end of file diff --git a/src/JAVA/2.md b/src/JAVA/2.md new file mode 100644 index 0000000..2b29ef9 --- /dev/null +++ b/src/JAVA/2.md @@ -0,0 +1,18 @@ +Java不能直接访问操作系统底层,而是通过本地方法来访问。**Unsafe类提供了硬件级别的原子操作**,主要提供以下功能: + +- **通过Unsafe类可以分配内存,可以释放内存** + + 类中提供的3个本地方法allocateMemory(申请)、reallocateMemory(扩展)、freeMemory(销毁)分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。 + +- **可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的** + + - 字段的定位 + - 数组元素定位 + +- **挂起与恢复** + + 将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。 + +- **CAS操作** + + 是通过compareAndSwapXXX方法实现的 \ No newline at end of file diff --git a/src/JAVA/201.md b/src/JAVA/201.md new file mode 100644 index 0000000..402a0f2 --- /dev/null +++ b/src/JAVA/201.md @@ -0,0 +1,46 @@ +### Queue + +Queue:队列的上层接口,提供了插入、删除、获取元素这3种类型的方法,而且对每一种类型都提供了两种方式。 + +**插入方法** + +- **add(E e)**:插入元素到队尾,插入成功返回true,没有可用空间抛出异常 IllegalStateException +- **offer(E e)**: 插入元素到队尾,插入成功返回true,否则返回false + +add和offer作为插入方法的唯一不同就在于队列满了之后的处理方式。add抛出异常,而offer返回false。 + + + +**删除和获取元素方法(和插入方法类似)** + +- **remove()**:获取并移除队首的元素,该方法和poll方法的不同之处在于,如果队列为空该方法会抛出异常,而poll不会 +- **poll()**:获取并移除队首的元素,如果队列为空,返回null +- **element()**:获取队列首的元素,该方法和peek方法的不同之处在于,如果队列为空该方法会抛出异常,而peek不会 +- **peek()**:获取队列首的元素,如果队列为空,返回null + +如果队列是空,remove和element方法会抛出异常,而poll和peek返回null。当然,Queue只是单向队列,为了提供更强大的功能,JDK在1.6的时候新增了一个双向队列Deque,用来实现更灵活的队列操作。 + + + +### Deque + +Deque在Queue的基础上,增加了以下几个方法: + +- **addFirst(E e)**:在前端插入元素,异常处理和add一样 +- **addLast(E e)**:在后端插入元素,和add一样的效果 +- **offerFirst(E e)**:在前端插入元素,异常处理和offer一样 +- **offerLast(E e)**:在后端插入元素,和offer一样的效果 +- **removeFirst()**:移除前端的一个元素,异常处理和remove一样 +- **removeLast()**:移除后端的一个元素,和remove一样的效果 +- **pollFirst()**:移除前端的一个元素,和poll一样的效果 +- **pollLast()**:移除后端的一个元素,异常处理和poll一样 +- **getFirst()**:获取前端的一个元素,和element一样的效果 +- **getLast()**:获取后端的一个元素,异常处理和element一样 +- **peekFirst()**:获取前端的一个元素,和peek一样的效果 +- **peekLast()**:获取后端的一个元素,异常处理和peek一样 +- **removeFirstOccurrence(Object o)**:从前端开始移除第一个是o的元素 +- **removeLastOccurrence(Object o)**:从后端开始移除第一个是o的元素 +- **push(E e)**:和addFirst一样的效果 +- **pop()**:和removeFirst一样的效果 + +其实很多方法的效果都是一样的,只不过名字不同。比如Deque为了实现Stack的语义,定义了push和pop两个方法。 \ No newline at end of file diff --git a/src/JAVA/202.md b/src/JAVA/202.md new file mode 100644 index 0000000..39c710f --- /dev/null +++ b/src/JAVA/202.md @@ -0,0 +1,275 @@ +### BlockingQueue + +**BlockingQueue(阻塞队列)**,在Queue的基础上实现了阻塞等待的功能。它是JDK 1.5中加入的接口,它是指这样的一个队列:当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。 + +先给出BlockingQueue新增的方法: + +- put(E e):向队尾插入元素。如果队列满了,阻塞等待,直到被中断为止。 +- boolean offer(E e, long timeout, TimeUnit unit):向队尾插入元素。如果队列满了,阻塞等待timeout个时长,如果到了超时时间还没有空间,抛弃该元素。 +- take():获取并移除队首的元素。如果队列为空,阻塞等待,直到被中断为止。 +- poll(long timeout, TimeUnit unit):获取并移除队首的元素。如果队列为空,阻塞等待timeout个时长,如果到了超时时间还没有元素,则返回null。 +- remainingCapacity():返回在无阻塞的理想情况下(不存在内存或资源约束)此队列能接受的元素数量,如果该队列是无界队列,返回Integer.MAX_VALUE。 +- drainTo(Collection c):移除此队列中所有可用的元素,并将它们添加到给定 collection 中。 +- drainTo(Collection c, int maxElements):最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。 + +**BlockingQueue**最重要的也就是关于阻塞等待的几个方法,而这几个方法正好可以用来实现**生产-消费的模型**。 + +从图中我们可以知道实现了BlockingQueue的类有以下几个: + +- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。 +- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。 +- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。 +- SynchronousQueue:一个不存储元素的阻塞队列。 +- DelayQueue:一个使用优先级队列实现的无界阻塞队列。 + + + +#### ArrayBlockingQueue + +**ArrayBlockingQueue是一个用数组实现的有界阻塞队列**。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。 + +**特性** + +- **内部使用循环数组进行存储** +- **内部使用ReentrantLock来保证线程安全** +- **由Condition的await和signal来实现等待唤醒功能** +- **支持对生产者线程和消费者线程进行公平的调度**。默认情况下是不保证公平性的。公平性通常会降低吞吐量,但是减少了可变性和避免了线程饥饿问题 + + + +**数据结构 —— 数组** + +通常,队列的实现方式有数组和链表两种方式。对于数组这种实现方式来说,我们可以通过维护一个队尾指针,使得在入队的时候可以在O(1)的时间内完成。但是对于出队操作,在删除队头元素之后,必须将数组中的所有元素都往前移动一个位置,这个操作的复杂度达到了O(n),效果并不是很好。如下图所示: + +![数据结构—数组](images/JAVA/数据结构—数组.png) + + + +**数据结构 —— 环型结构** + +为了解决这个问题,我们可以使用另外一种逻辑结构来处理数组中各个位置之间的关系。假设现在我们有一个数组A[1…n],我们可以把它想象成一个环型结构,即A[n]之后是A[1],相信了解过一致性Hash算法的应该很容易能够理解。如下图所示:![数据结构—环型结构](images/JAVA/数据结构—环型结构.png) + +我们可以使用两个指针,分别维护队头和队尾两个位置,使入队和出队操作都可以在O(1)的时间内完成。当然,这个环形结构只是逻辑上的结构,实际的物理结构还是一个普通的数据。 + + + +**入队方法** + +ArrayBlockingQueue 提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种: + +- **boolean add(E e)**:其调用的是父类,即AbstractQueue的add方法,实际上调用的就是offer方法 + +- **void put(E e)**:在count等于items长度时,一直等待,直到被其他线程唤醒。唤醒后调用enqueue方法放入队列 + +- **boolean offer(E e)**:offer方法在队列满了的时候返回false,否则调用enqueue方法插入元素,并返回true。 + + **enqueue**:方法首先把元素放在items的putIndex位置,接着判断在putIndex+1等于队列的长度时把putIndex设置为0,也就是上面提到的圆环的index操作。最后唤醒等待获取元素的线程。 + +- **boolean offer(E e, long timeout, TimeUnit unit)**:只是在offer(E e)的基础上增加了超时时间的概念 + + + +**出队方法** + +ArrayBlockingQueue提供了多种出队操作的实现来满足不同情况下的需求,如下: + +- **E poll()**:poll方法是非阻塞方法,如果队列没有元素返回null,否则调用dequeue把队首的元素出队列。 + + **dequeue**:会根据takeIndex获取到该位置的元素,并把该位置置为null,接着利用圆环原理,在takeIndex到达列表长度时设置为0,最后唤醒等待元素放入队列的线程。 + +- **E poll(long timeout, TimeUnit unit)**:该方法是poll()的可配置超时等待方法,和上面的offer一样,使用while循环+Condition的awaitNanos来进行等待,等待时间到后执行dequeue获取元素 + +- **E take()**: + + + +**获取元素方法** + +- **peek()**:这里获取元素时上锁是为了避免脏数据的产生 + + + +**删除元素方法** + +- **remove(Object o)**:从takeIndex一直遍历到putIndex,直到找到和元素o相同的元素,调用removeAt进行删除。removeAt(): + - 当removeIndex == takeIndex时就不需要后面的元素整体往前移了,而只需要把takeIndex的指向下一个元素即可(还记得前面说的ArrayBlockingQueue可以类比为圆环吗) + - 当removeIndex != takeIndex时,通过putIndex将removeIndex后的元素往前移一位 + + + +#### LinkedBlockingQueue + +**LinkedBlockingQueue是一个用链表实现的有界阻塞队列**。此队列的默认和最大长度为`Integer.MAX_VALUE`,也就是无界队列,所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。此队列按照先进先出的原则对元素进行排序。 + +LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。 + + + +**LinkedBlockingQueue和ArrayBlockingQueue的不同点** + +- 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题 +- 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表 +- 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响 +- 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能 + + + +**入队方法** + +LinkedBlockingQueue提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种: + +- **void put(E e)**: + - 队列已满,阻塞等待 + - 队列未满,创建一个node节点放入队列中,如果放完以后队列还有剩余空间,继续唤醒下一个添加线程进行添加。如果放之前队列中没有元素,放完以后要唤醒消费线程进行消费 +- **boolean offer(E e)**:offer仅仅对put方法改动了一点点,当队列没有可用元素的时候,不同于put方法的阻塞等待,offer方法直接方法false +- **boolean offer(E e, long timeout, TimeUnit unit)**:该方法只是对offer方法进行了阻塞超时处理,使用了Condition的awaitNanos来进行超时等待。为什么要用while循环?因为awaitNanos方法是可中断的,为了防止在等待过程中线程被中断,这里使用while循环进行等待过程中中断的处理,继续等待剩下需等待的时间 + + + +**出队方法** + +入队列的方法说完后,我们来说说出队列的方法。LinkedBlockingQueue提供了多种出队操作的实现来满足不同情况下的需求,如下: + +- **E take()**: + - 队列为空,阻塞等待 + - 队列不为空,从队首获取并移除一个元素,如果消费后还有元素在队列中,继续唤醒下一个消费线程进行元素移除。如果放之前队列是满元素的情况,移除完后要唤醒生产线程进行添加元素 +- **E poll()**:poll方法去除了take方法中元素为空后阻塞等待 +- **E poll(long timeout, TimeUnit unit)**:利用了Condition的awaitNanos方法来进行阻塞等待直至超时 + + + +**获取元素方法** + +- **peek()**:加锁获取。枷锁后获取到head节点的next节点,如果为空返回null,如果不为空,返回next节点的item值 + + + +**删除元素方法** + +- **remove(Object o)**:因为remove方法使用两个锁(put锁和take锁)全部上锁,所以其它操作都需要等待它完成,而该方法需要从head节点遍历到尾节点,所以时间复杂度为O(n) + + + +#### PriorityBlockingQueue + +**PriorityBlockingQueue是一个支持优先级的无界队列**。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。 + + + +#### SynchronousQueue + +**SynchronousQueue是一个不存储元素的阻塞队列**。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。 + + + +#### DelayQueue + +**DelayQueue是一个支持延时获取元素的无界阻塞队列**。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景: + +- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。 +- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。 + + + +### BlockingDeque + +**BlockingDeque(阻塞双端队列)**在Deque的基础上实现了双端阻塞等待的功能。和第2节说的类似,BlockingDeque也提供了双端队列该有的阻塞等待方法: + +- putFirst(E e):在队首插入元素,如果队列满了,阻塞等待,直到被中断为止。 +- putLast(E e):在队尾插入元素,如果队列满了,阻塞等待,直到被中断为止。 +- offerFirst(E e, long timeout, TimeUnit unit):向队首插入元素。如果队列满了,阻塞等待timeout个时长,如果到了超时时间还没有空间,抛弃该元素。 +- offerLast(E e, long timeout, TimeUnit unit):向队尾插入元素。如果队列满了,阻塞等待timeout个时长,如果到了超时时间还没有空间,抛弃该元素。 +- takeFirst():获取并移除队首的元素。如果队列为空,阻塞等待,直到被中断为止。 +- takeLast():获取并移除队尾的元素。如果队列为空,阻塞等待,直到被中断为止。 +- pollFirst(long timeout, TimeUnit unit):获取并移除队首的元素。如果队列为空,阻塞等待timeout个时长,如果到了超时时间还没有元素,则返回null。 +- pollLast(long timeout, TimeUnit unit):获取并移除队尾的元素。如果队列为空,阻塞等待timeout个时长,如果到了超时时间还没有元素,则返回null。 +- removeFirstOccurrence(Object o):从队首开始移除第一个和o相等的元素。 +- removeLastOccurrence(Object o):从队尾开始移除第一个和o相等的元素。 + + + +#### LinkedBlockingDeque + +**LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列**,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。`LinkedBlockingDeque`是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为`Integer.MAX_VALUE`。 + +相比于其它阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 + + + +**LinkedBlockingDeque和LinkedBlockingQueue的相同点** + +- 基于链表 +- 容量可选,不设置的话,就是Int的最大值 + + + +**LinkedBlockingDeque和LinkedBlockingQueue的不同点** + +- 双端链表和单链表 +- 不存在头节点 +- 一把锁+两个条件 + + + +**入队方法** + +LinkedBlockingDeque提供了多种入队操作的实现来满足不同情况下的需求,入队操作有如下几种: + +- add(E e)、addFirst(E e)、addLast(E e) +- offer(E e)、offerFirst(E e)、offerLast(E e) +- offer(E e, long timeout, TimeUnit unit)、offerFirst(E e, long timeout, TimeUnit unit)、offerLast(E e, long timeout, TimeUnit unit) +- put(E e)、putFirst(E e)、putLast(E e) + + + +**出队方法** + +入队列的方法说完后,我们来说说出队列的方法。LinkedBlockingDeque提供了多种出队操作的实现来满足不同情况下的需求,如下: + +- **remove()、removeFirst()、removeLast()** +- **poll()、pollFirst()、pollLast()** +- **take()、takeFirst()、takeLast()** +- **poll(long timeout, TimeUnit unit)、pollFirst(long timeout, TimeUnit unit)、pollLast(long timeout, TimeUnit unit)** + + + +**获取元素方法** + +获取元素前加锁,防止并发问题导致数据不一致。利用first和last节点直接可以获得元素。 + +- **element()** +- **peek()** + + + +**删除元素方法** + +删除元素是从头/尾向两边进行遍历比较,故时间复杂度为O(n),最后调用unlink把要移除元素的prev和next进行关联,把要移除的元素从链中脱离,等待下次GC回收。 + +- **remove(Object o)**: + + + +### TransferQueue + +TransferQueue是JDK 1.7对于并发类库新增加的一个接口,它扩展自BlockingQueue,所以保持着阻塞队列的所有特性。 + +TransferQueue对比与BlockingQueue更强大的一点是,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立Java内存模型中的happens-before关系的方式)。 + + + +该接口提供的标准方法: + +- tryTransfer(E e):若当前存在一个正在等待获取的消费者线程(使用take()或者poll()函数),使用该方法会即刻转移/传输对象元素e并立即返回true;**若不存在,则返回false,并且不进入队列。这是一个不阻塞的操作** +- transfer(E e):若当前存在一个正在等待获取的消费者线程,即立刻移交之;**否则,会插入当前元素e到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素** +- tryTransfer(E e, long timeout, TimeUnit unit):若当前存在一个正在等待获取的消费者线程,会立即传输给它;**否则将插入元素e到队列尾部,并且等待被消费者线程获取消费掉;若在指定的时间内元素e无法被消费者线程获取,则返回false,同时该元素被移除** +- hasWaitingConsumer():判断是否存在消费者线程 +- getWaitingConsumerCount():获取所有等待获取元素的消费线程数量 + + + +#### LinkedTransferQueue + +LinkedTransferQueue 是**单向链表结构的无界阻塞队列**。 + +LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,**生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)**。新添加的 transfer 方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程 transfer 到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立 Java 内存模型中的 happens-before 关系的方式)。**Doug Lea 说从功能角度来讲,LinkedTransferQueue 实际上是 ConcurrentLinkedQueue、SynchronousQueue(公平模式)和 LinkedBlockingQueue 的超集。**而且 LinkedTransferQueue 更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。 \ No newline at end of file diff --git a/src/JAVA/3.md b/src/JAVA/3.md new file mode 100644 index 0000000..35d2b32 --- /dev/null +++ b/src/JAVA/3.md @@ -0,0 +1,5 @@ +LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞。 + +LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。 + +park()和unpark()不会有 `Thread.suspend` 和 `Thread.resume` 所可能引发的死锁问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。 \ No newline at end of file diff --git a/src/JAVA/301.md b/src/JAVA/301.md new file mode 100644 index 0000000..bc06bdc --- /dev/null +++ b/src/JAVA/301.md @@ -0,0 +1,31 @@ +**什么是进程 ?** + +进程是**资源分配的最小单位**。(资源包括各种表格、内存空间、磁盘空间) 同一进程中的多条线程将共享该进程中的全部系统资源。 + +**什么是线程 ?** + +线程是**CPU调度的最小单位**。线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表组成。 而寄存器可被用来存储线程内的局部变量。 + +**什么是并行和并发 ?** + +- **并行运行**:总线程数≤CPU数量×核心数 +- **并发运行**:总线程数>CPU数量×核心数(如:有的操作系统CPU线程切换之间用的时间片轮转进程调度算法) + + + +**线程优缺点** + +- **优点** + - 创建一个新线程的代价要比创建一个新进程小的多 + - 线程之间的切换相较于进程之间的切换需要操作系统做的工作很少 + - 线程占用的资源要比进程少很多 + - 能充分利用多处理器的可并行数量 + - 等待慢速IO操作结束以后,程序可以继续执行其他的计算任务 + - 计算(CPU)密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 + - IO密集型应用,为了提高性能,将IO操作重叠,线程可以等待不同的IO操作 + +- **缺点** + - 性能损失 + - 健壮性降低 + - 缺乏访问控制 + - 编程难度提高 \ No newline at end of file diff --git a/src/JAVA/302.md b/src/JAVA/302.md new file mode 100644 index 0000000..cab4374 --- /dev/null +++ b/src/JAVA/302.md @@ -0,0 +1,43 @@ +**实现线程**只有一种方式: + +- **new Thread()** + + + +**实现线程执行内容**有两种方式: + +- **继续Thread类**:Thread实现了Runable接口 +- **实现Runable接口**:new Thread(new Runable(){……}),本质是通过Thread的run()进行调用触发 + + + +更多实现线程执行内容的方式,只需在此基础上进行封装: + +- **线程池创建线程**:本质是通过 new Thread() 的方式实现 +- **有返回值的Callable创建线程**:需要提交到线程池中执行。本质是通过实例化Thread的方式实现 +- **定时器Timer**:本质是继承自 Thread 类实现 + + + +**Thread、Runnable和Callable的区别** + +- Runnable相对于Thread的优势是:**避免单继承**的局限,适合于**资源共享**场景 +- Thread使用JNI调用(native修饰的start0方法)系统函数来完成start,Runnable则由JVM来实现start,Callable也需要调用Thread.start()启动线程 +- 一般情况,多线程中优先选择实现Runnable接口 +- Callable能返回任务线程执行结果,而Runable不能返回 +- Callable的call方法允许抛出异常,而Runable异常只能在run方法内部消化 + + + +**Thread.sleep(0)的作用是什么?** + +由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。 + + + +**wait()和sleep()的区别?** + +- wait()来自Object ,sleep()来自Thread +- 调用wait()时线程会释放锁,调用sleep()时线程不会释放对象锁(只是暂时让出CPU的执行权) +- wait()只能在同步控制方法或者同步控制块中使用,sleep()可以在任何地方使用 +- wait()可以通过notify()或notifyAll()被结束 ,sleep()只能等待休眠时间到期后才结束 \ No newline at end of file diff --git a/src/JAVA/303.md b/src/JAVA/303.md new file mode 100644 index 0000000..417ad88 --- /dev/null +++ b/src/JAVA/303.md @@ -0,0 +1,4 @@ +- 继承Thread类 +- 实现Runnable接口 +- ExecutorService、Callable、Future有返回值线程 +- 基于线程池的方式 \ No newline at end of file diff --git a/src/JAVA/304.md b/src/JAVA/304.md new file mode 100644 index 0000000..3338b51 --- /dev/null +++ b/src/JAVA/304.md @@ -0,0 +1,93 @@ +Linux中线程状态一共有5种: + +- **初始状态(New)**:对应 Java中的 **NEW** 状态 +- **可运行状态(Ready)**:对应 Java中的 **RUNNBALE** 状态 +- **运行状态(Running)**:对应 Java中的 **RUNNBALE** 状态 +- **等待状态(Waiting)**:该状态在 Java中被划分为了 **BLOCKED**、**WAITING**、**TIMED_WAITING** 三种状态 +- **终止状态 (Terminated)**:对应 Java中的 **TERMINATED** 状态 + + + +Java中线程状态一共有6种(生命周期): + +- **New(新创建)**:新创建了一个线程对象,但还没有调用start()方法 +- **Runnable(可运行)**:Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其它线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running) +- **Blocked(被阻塞)**:表示线程阻塞于锁 +- **Waiting(等待)**:进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断) +- **Timed Waiting(计时等待)**:该状态不同于WAITING,它可以在指定的时间后自行返回 +- **Terminated(被终止)**:表示该线程已经执行完毕。线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常 + + + +如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。线程状态切换: + +![线程状态切换](images/JAVA/线程状态切换.jpg) + + + +### 新建状态(NEW) + +![Thread-NEW](images/JAVA/Thread-NEW.png) + +New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable,也就是状态转换图中中间的这个大方框里的内容。 + + + +### 可运行状态(RUNNABLE) + +![Thread-RUNNABLE](images/JAVA/Thread-RUNNABLE.png) + +Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。 + +所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。 + + + +### 被阻塞状态(BLOCKED) + +![Thread-BLOCKED](images/JAVA/Thread-BLOCKED.png) + +首先来看最简单的 Blocked,从箭头的流转方向可以看出,从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁,无论是进入 synchronized 代码块,还是 synchronized 方法,都是一样。当处于 Blocked 的线程抢到 monitor 锁,就会从 Blocked 状态回到Runnable 状态。 + + + +### 等待状态(WAITING) + +![Thread-WAITING](images/JAVA/Thread-WAITING.png) + +线程进入 Waiting 状态有三种可能性: + +- 没有设置 Timeout 参数的 Object.wait() 方法 +- 没有设置 Timeout 参数的 Thread.join() 方法 +- LockSupport.park() 方法 + +Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态。 + +Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。 + + + +### 计时等待状态(TIMED_WAITING) + +![Thread-TIMED_WAITING](images/JAVA/Thread-TIMED_WAITING.png) + +在 Waiting 上面是 Timed Waiting 状态,这两个状态是非常相似的,区别仅在于有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。以下情况会让线程进入 Timed Waiting 状态。 + +- 设置了时间参数的 Thread.sleep(long millis) 方法 +- 设置了时间参数的 Object.wait(long timeout) 方法 +- 设置了时间参数的 Thread.join(long millis) 方法 +- 设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法 + + + +### 已终止状态(TERMINATED) + +![Thread-TERMINATED](images/JAVA/Thread-TERMINATED.png) + +线程会以下面三种方式结束,结束后就是终止状态: + +- **正常结束**:run()或 call()方法执行完成,线程正常结束 + +- **异常结束**:线程抛出一个未捕获的 Exception 或 Error + +- **调用** **stop**:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用 \ No newline at end of file diff --git a/src/JAVA/305.md b/src/JAVA/305.md new file mode 100644 index 0000000..231a10e --- /dev/null +++ b/src/JAVA/305.md @@ -0,0 +1,21 @@ +### newCachedThreadPool + +创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。 + + + +### newFixedThreadPool + +创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。 + + + +### newScheduledThreadPool + +创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 + + + +### newSingleThreadExecutor + +Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。 \ No newline at end of file diff --git a/src/JAVA/306.md b/src/JAVA/306.md new file mode 100644 index 0000000..cd9a571 --- /dev/null +++ b/src/JAVA/306.md @@ -0,0 +1,33 @@ +### 线程等待(wait) + +调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。 + + + +### 线程睡眠(sleep) + +sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。 + + + +### 线程让步(yield) + +yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。 + + + +### 线程中(interrupt) + +中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。 + + + +### 等待其他线程终止(join) + +join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。 + + + +### 线程唤醒(notify) + +Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。 \ No newline at end of file diff --git a/src/JAVA/307.md b/src/JAVA/307.md new file mode 100644 index 0000000..e1ab49f --- /dev/null +++ b/src/JAVA/307.md @@ -0,0 +1,14 @@ +线程安全是指当多个线程访问某个类时,该类始终都表现正确行为,则称该类是线程安全的。 + + + +**出现线程安全问题的原因?** + +在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。 + + + +**如何避免线程安全问题?** + +- 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性) +- 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性) \ No newline at end of file diff --git a/src/JAVA/308.md b/src/JAVA/308.md new file mode 100644 index 0000000..e1addfb --- /dev/null +++ b/src/JAVA/308.md @@ -0,0 +1,9 @@ +多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。线程同步的方式: + +- **同步方法(synchronized )** +- **同步代码块(synchronized )** +- **使用特殊域变量(volatile)实现线程同步** +- **使用重入锁(ReentrantLock)实现线程同步** +- **使用局部变量(ThreadLocal)实现线程同步** +- **使用阻塞队列(LinkedBlockingQueue)实现线程同步** +- **使用原子变量(AtomicXxx)实现线程同步** \ No newline at end of file diff --git a/src/JAVA/309.md b/src/JAVA/309.md new file mode 100644 index 0000000..435cea2 --- /dev/null +++ b/src/JAVA/309.md @@ -0,0 +1,7 @@ +多线程通讯的方式主要包括以下几种: + +- **使用volatile关键词:基于共享内存的思想** +- **使用Synchronized+Object类的wait()/notify()/notifyAll()方法** +- **使用JUC工具类CountDownLatch:基于共享变量state实现** +- **使用Lock(ReentrantLock)结合Condition** +- **基于LockSupport实现线程间的阻塞和唤醒** \ No newline at end of file diff --git a/src/JAVA/310.md b/src/JAVA/310.md new file mode 100644 index 0000000..570068b --- /dev/null +++ b/src/JAVA/310.md @@ -0,0 +1,41 @@ +### sleep/yield/join + +**sleep()** + + - 让当前线程暂停指定的时间(毫秒) + - wait方法依赖于同步,而sleep方法可以直接调用 + - sleep方法只是暂时让出CPU的执行权,并不释放锁,而wait方法则需要释放锁 + +**yield()** + + - 暂停当前线程,让出当前CPU的使用权,以便其它线程有机会执行 + - 不能指定暂停的时间,并且也不能保证当前线程马上停止 + - 会让当前线程从运行状态转变为就绪状态 + - yield只能使同优先级或更高优先级的线程有执行的机会 + +**join()** + + - 等待调用 join 方法的线程执行结束,才执行后面的代码 + - 其调用一定要在 start 方法之后(看源码可知) + - 作用是父线程等待子线程执行完成后再执行(即将异步执行的线程合并为同步的线程) + + + +### wait/notify/notifyAll + +一般需要配合**synchronized**一起使用。**Object**的主要方法如下: + +- **wait()**:阻塞当前线程,直到 notify 或者 notifyAll 来唤醒 +- **notify()**:只能唤醒一个处于 wait 的线程 +- **notifyAll()**:唤醒全部处于 wait 的线程 + + + +### await/signal/signalAll + +使用显式的 **Lock** 和 **Condition** 对象: + +- **await()**:当前线程进入等待状态,直到被通知(signal/signalAll)、中断或超时 +- **signal()**:唤醒一个等待在Condition上的线程,将该线程从**等待队列**中转移到**同步队列**中 + +- **signalAll()**:能够唤醒所有等待在Condition上的线程 \ No newline at end of file diff --git a/src/JAVA/311.md b/src/JAVA/311.md new file mode 100644 index 0000000..1da086d --- /dev/null +++ b/src/JAVA/311.md @@ -0,0 +1,15 @@ +**造成死锁的原因?** + +多个线程竞争共享资源,由于资源被占用、资源不足或进程推进顺序不当等原因造成线程处于永久阻塞状态,从而引发死锁。死锁形成的原因: + +- 系统资源不足 +- 进程(线程)推进的顺序不恰当 +- 资源分配不当 + + + +**死锁的解决办法?** + +- 让程序每次至多只能获得一个锁(多线程环境中并不现实) +- 设计时考虑清楚锁的顺序,尽量减少嵌套加锁和交互数量 +- 设置锁等待时间上限 \ No newline at end of file diff --git a/src/JAVA/312.md b/src/JAVA/312.md new file mode 100644 index 0000000..80c4e07 --- /dev/null +++ b/src/JAVA/312.md @@ -0,0 +1,5 @@ +守护线程(daemon=true):当线程只剩下守护线程的时候,JVM就会退出;但是如果还有其它的任意一个用户线程还在,JVM就不会退出。在Java中有两类线程:**User Thread(用户线程)、Daemon Thread(守护线程)** + +- thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常 +- 在Daemon线程中产生的新线程也是Daemon的 +- 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑 \ No newline at end of file diff --git a/src/JAVA/313.md b/src/JAVA/313.md new file mode 100644 index 0000000..a812dbe --- /dev/null +++ b/src/JAVA/313.md @@ -0,0 +1,25 @@ +**sleep与wait区别** + +- 来源不同:**wait** 来自**Object**,**sleep** 来自 **Thread** +- 是否释放锁:**wait** 释放锁,**sleep** 不释放 +- 使用范围:**wait** 必须在同步代码块中,**sleep** 可以任意使用 +- 捕捉异常:**wait** 不需要捕获异常,**sleep** 需捕获异常 + + + +**start与run区别** + +- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码 +- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行 +- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程 + + + +**yield跟sleep区别** + +- **yield** 跟 **sleep** 都能暂停当前线程,都**不会释放锁资源**,**sleep** 可以指定具体休眠的时间,而 **yield** 则依赖 **CPU** 的时间片划分 +- **sleep**方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。**yield**方法只会给相同优先级或更高优先级的线程以运行的机会 +- 调用 **sleep** 方法使线程进入**等待状态**,等待休眠时间达到,而调用我们的 **yield**方法,线程会进入**就绪状态**,也就是**sleep**需要等待设置的时间后才会进行**就绪状态**,而**yield**会立即进入**就绪状态** +- **sleep**方法声明会抛出 **InterruptedException**,而 **yield** 方法没有声明任何异常 +- **yield** 不能被中断,而 **sleep** 则可以接受中断 +- **sleep**方法比**yield**方法具有更好的移植性(跟操作系统CPU调度相关) \ No newline at end of file diff --git a/src/JAVA/314.md b/src/JAVA/314.md new file mode 100644 index 0000000..37c7eb8 --- /dev/null +++ b/src/JAVA/314.md @@ -0,0 +1,133 @@ +把ThreadLocal看成一个全局Map,每个线程获取ThreadLocal变量时,总是**使用Thread自身作为key**。相当于给每个线程都开辟了一个独立的存储空间,**各个线程的ThreadLocal关联的实例互不干扰**。 + +- ThreadLocal表示线程的"局部变量",它确保每个线程的ThreadLocal变量都是各自独立的 +- ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递) +- 使用ThreadLocal要用try ... finally结构,并在finally中清除 + + + +ThreadLocal常用的方法 + +- set:为当前线程设置变量,当前ThreadLocal作为索引 +- get:获取当前线程变量,当前ThreadLocal作为索引 +- initialValue(钩子方法需要子类实现):赖加载形式初始化线程本地变量,执行get时,发现线程本地变量为null,就会执行initialValue的内容 +- remove:清空当前线程的ThreadLocal索引与映射的元素 + + + +### 底层结构 + +![img](images/JAVA/007S8ZIlly1gh4fy6gvw0j30w0093jsu.jpg) + + + +### set流程 + +![img](images/JAVA/007S8ZIlly1gh4ipc80hfj30w10hugo5.jpg) + +然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上; + +```java +private void set(ThreadLocal key, Object value) { + Entry[] tab = table; + int len = tab.length; + // 根据ThreadLocal对象的hash值,定位到table中的位置i + int i = key.threadLocalHashCode & (len - 1); + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + + // 如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value + if (k == key) { + e.value = value; + return; + } + + // 如果当前位置是空的,就初始化一个Entry对象放在位置i上 + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + + // 如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止 + } + + tab[i] = new Entry(key, value); + int sz = ++size; + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +} +``` + + + +### get流程 + +在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。 + +```java +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + // get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。 + while (e != null) { + ThreadLocal k = e.get(); + // 相等就直接返回,不相等就继续查找,找到相等位置。 + if (k == key) + return e; + if (k == null) + expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; +} +``` + + + +**如果想共享线程的ThreadLocal数据怎么办?** + +使用`InheritableThreadLocal`可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个`InheritableThreadLocal`的实例,然后在子线程中得到这个`InheritableThreadLocal`实例设置的值。 + + + +### 内存泄露 + +![img](images/JAVA/007S8ZIlly1gh4mkx8upjj30jz06m74u.jpg) + +ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。 + +![img](images/JAVA/007S8ZIlly1gh4nh8v3haj30w10bbabr.jpg) + +**弱引用**:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。 + +**解决**:在finally中remove即可。 + +**那为什么ThreadLocalMap的key要设计成弱引用?**key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。 + + + +### InheritableThreadLocal + +`InheritableThreadLocal` 是 JDK 本身自带的一种线程传递解决方案。顾名思义,由当前线程创建的线程,将会继承当前线程里 ThreadLocal 保存的值。 + +`JDK`的`InheritableThreadLocal`类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的`ThreadLocal`值传递已经没有意义,应用需要的实际上是把 **任务提交给线程池时**的`ThreadLocal`值传递到 **任务执行时**。 + + + +### TransmittableThreadLocal + +**TransmittableThreadLocal** 是Alibaba开源的、用于解决 **“在使用线程池等会缓存线程的组件情况下传递ThreadLocal”** 问题的 InheritableThreadLocal 扩展。若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnable 和 TtlCallable使用。 + + + +**使用场景** + +- 分布式跟踪系统 +- 应用容器或上层框架跨应用代码给下层SDK传递信息 +- 日志收集记录系统上下文 \ No newline at end of file diff --git a/src/JAVA/315.md b/src/JAVA/315.md new file mode 100644 index 0000000..af0f91b --- /dev/null +++ b/src/JAVA/315.md @@ -0,0 +1,249 @@ +ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示: + +![ThreadPoolExecutor运行流程](images/JAVA/ThreadPoolExecutor运行流程.png) + +线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。 + + + +**线程池(Thread Pool)** + +线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中。 + +线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。当然,使用线程池可以带来一系列好处: + +- **降低资源消耗**:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗 +- **提高响应速度**:任务到达时,无需等待线程创建即可立即执行 +- **提高线程的可管理性**:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控 +- **提供更多更强大的功能**:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行 + + + +**线程池要解决的两个问题** + +- **解决频繁创建和销毁线程所产生的开销**。减少在创建和销毁线程上所花的时间以及系统资源的开销 +- **解决无限制创建线程引起系统崩溃**。不使用线程池,可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换” + + + +**多线程优缺点** + +- **多线程的优点** + - 资源利用率更好 + - 程序设计在某些情况下更简单 + - 程序响应更快 +- **多线程的缺点** + - 设计更复杂 + - 上下文切换的开销 + - 增加资源消耗 + + + +**线程池类型** + +JDK默认提供四种线程池: + +- **newCachedThreadPool**:可变尺寸的线程池 +- **newFixedThreadPool**:可重用的固定线程数的线程池 +- **newScheduledThreadPool**:定时以及周期性执行任务的线程池 +- **newSingleThreadExecutor**:单任务的线程池 + + + +### 重要参数 + +- **corePoolSize**(核心线程数,默认值为1) + + - 核心线程会一直存活,即使没有任务需要执行 + - 当线程数<核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理 + - 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭 + +- **maximumPoolSize**(最大线程数,默认值为Integer.MAX_VALUE) + + - 当线程数≥corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务 + - 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常 + +- **keepAliveTime**(线程空闲时间,Single和Fixed默认值为0ms,Cached默认值为60s) + + - 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize + - 如果allowCoreThreadTimeout=true,则会直到线程数量=0 + +- **unit**(时间单位):用于设置keepAliveTime的时间单位 + +- **workQueue**(任务队列容量,阻塞队列,默认值为Integer.MAX_VALUE) + + - 当核心线程数达到最大时,新任务会放在队列中排队等待执行 + +- **threadFactory**( 新建线程工厂) + + - 通常用于线程命名 + - 设置线程守护(daemon) + +- **allowCoreThreadTimeout**:允许核心线程超时,默认值为false + +- **handler**(任务拒绝处理器,默认值为策略为AbortPolicy) + + 以下两种情况会拒绝处理任务: + + - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务 + - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务 + + + +### 线程池状态转换 + +下图为线程池的状态转换过程: +![ThreadPoolExecutor状态转换](images/JAVA/ThreadPoolExecutor状态转换.png) + +线程池最重要的5种状态: + +- **RUNNING**:能接受新提交的任务,并且也能处理阻塞队列中的任务 +- **SHUTDOWN**:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态) +- **STOP**:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态 +- **TIDYING**:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态 +- **TERMINATED**:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做 + + + +### execute销毁流程 + +**execute到线程销毁的整个流程图** +![ThreadPoolExecutor线程销毁流程](images/JAVA/ThreadPoolExecutor线程销毁流程.png) + + + +### 线程池的监控 + +通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用 + +- **getTaskCount**:线程池已经执行的和未执行的任务总数 +- **getCompletedTaskCount**:线程池已完成的任务数量,该值小于等于taskCount +- **getLargestPoolSize**:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize +- **getPoolSize**:线程池当前的线程数量 +- **getActiveCount**:当前线程池中正在执行任务的线程数量 + + + +### 生命周期 + +hreadPoolExecutor的运行状态有5种,分别为: + +![生命周期状态](images/JAVA/生命周期状态.png) + +其生命周期转换如下入所示: + +![线程池生命周期](images/JAVA/线程池生命周期.png) + + + +### 任务调度 + +![ThreadPoolExecutor任务调度流程](images/JAVA/ThreadPoolExecutor任务调度流程.png) + +线程池分配线程时,其执行过程如下: + +- 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务 +- **当线程数<核心线程数(corePoolSize)**时,每次都创建新线程 +- **当线程数 ≥ 核心线程数(corePoolSize)**时 + - **任务队列(queueCapacity)未满** + - 将任务放入任务队列 + - **任务队列(queueCapacity)已满** + - **若线程数<最大线程数(maxPoolSize)**,则创建线程 + - **若线程数 = 最大线程数(maxPoolSize)**,则抛出异常而拒绝任务 + + + +### 任务缓冲 + +任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。使用不同的队列可以实现不一样的任务存取策略。以下是阻塞队列的成员: + +![任务缓冲策略](images/JAVA/任务缓冲策略.png) + + + +### 任务申请 + +由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。 + +线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示: + +![获取任务流程图](images/JAVA/获取任务流程图.png) + +getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。 + + + +### 任务拒绝 + +任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。拒绝策略是一个接口,其设计如下: + +![任务拒绝](images/JAVA/任务拒绝.png) + + + +### 并发类型 + +**CPU密集型(CPU-bound)** + +CPU密集型(又称计算密集型),是指任务需要进行大量复杂的运算,几乎没有阻塞,需要CPU长时间高速运行。在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,**避免过多的线程上下文切换**,比较理想方案是: + +`线程数 = CPU核数 + 1` + +JVM可运行的CPU核数可以通过`Runtime.getRuntime().availableProcessors()`查看。也可设置成CPU核数×2 ,这还是要看JDK的使用版本以及CPU配置(服务器的CPU有超线程)。对于JDK1.8来说,里面增加了一个并行计算,因此计算密集型的较理想: + +`线程数 = CPU内核线程数×2` + + + +**IO密集型(IO-bound)** + +对于IO密集型的应用,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输或磁盘读写,线程花费更多的时间在IO阻塞上,而不是CPU运算。如与数据库和缓存之间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束且数据准备好后,线程才会继续执行。因此对于IO密集型的应用,我们可以多设置一些线程池中线程的数量(但不宜过多,因为线程上下文切换是有代价的),这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。对于IO密集型应用: + +`线程数=CPU数/(1-阻塞系数)` + + `阻塞系数=线程等待时间/(线程等待时间+CPU处理时间) ` + +或 `线程数=CPU核数×2 + 1` + +这个阻塞系数一般为 **0.8~0.9** 之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是:2÷(1-0.9)=20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整。 + + + +### 参数计算 + +**目标假设** + +- **tasks** :每秒的任务数假设为**500~1000** +- **taskCost**:每个任务花费时间假设为**0.1s**,则 + - **corePoolSize**=TPS÷平均耗时 + - **1÷taskCost**:表示单个线程每秒的处理能力(处理数量) +- **responseTime**:系统允许容忍的最大响应时间,假设为**1s** + - **queueCapacity**=corePoolSize÷平均耗时×最大容忍耗时 + + + +**参数计算** + +- **每秒需要多少个线程处理(corePoolSize) = tasks ÷ (1 ÷ taskCost)** + + ① corePoolSize = tasks ÷ (1 ÷ taskCost) = (500~1000) ÷ (1 ÷ 0.1) = 50~100个线程 + + ② 根据2/8原则,如果每秒任务数80%都小于800,那么corePoolSize设置为80即可 + +- **线程池缓冲队列大小(queueCapacity) = (coreSizePool ÷ taskCost) × responseTime** + + ① queueCapacity = 80÷0.1×1 = 80,即队列里的线程可以等待1s,超过了的需要新开线程来执行 + + ② 禁止设置为Integer.MAX_VALUE,否则CPU Load会飙满,耗时会变长,内存也会OOM + +- **最大线程数(maximumPoolSize )= (Max(tasks) - queueCapacity) ÷ (1÷taskCost)** + + ① 计算可得 maximumPoolSize = (1000 - 80) ÷ 10 = 92 + +- rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则利用一些缓冲机制来处理 + +- keepAliveTime和allowCoreThreadTimeout采用默认通常能满足 + +- prestartAllCoreThreads:调用线程池的prestartAllCoreThreads方法,可以实现提前创建并启动好所有基本线程 + +**注意:** 以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器CPU Load已经满了,则需要通过升级硬件和优化代码,降低taskCost来处理。 \ No newline at end of file diff --git a/src/JAVA/4.md b/src/JAVA/4.md new file mode 100644 index 0000000..faf3580 --- /dev/null +++ b/src/JAVA/4.md @@ -0,0 +1,29 @@ +CAS(`Compare And Swap`,即比较并交换),是解决多线程并行情况下使用锁造成性能损耗的一种机制。其原理是利用`sun.misc.Unsafe.java` 类通过JNI来调用硬件级别的原子操作来实现CAS(即CAS是借助C来调用CPU底层指令实现的)。 + + + +**CAS机制=比较并交换+乐观锁机制+锁自旋** + +**设计思想**:如果`内存位置` 的值与 `预期原值` 相匹配,那么处理器会自动将该位置值更新为新值,否则处理器不做任何操作。 + +ReentrantLock、ReentrantReadWriteLock 都是基于 AbstractQueuedSynchronizer (AQS),而 AQS 又是基于 CAS。CAS 的全称是 Compare And Swap(比较与交换),它是一种无锁算法。synchronized和Lock都采用了悲观锁的机制,而CAS是一种乐观锁的实现。乐观锁的原理就是每次不加锁去执行某项操作,如果发生冲突则失败并重试,直到成功为止,其实本质上不算锁,所以很多地方也称之为**自旋**。乐观锁用到的主要机制就是**CAS**(Compare And Swap)。 + + + +**CAS特性** + +- 通过JNI借助C来调用CPU底层指令实现 +- 非阻塞算法 +- 非独占锁 + + + +**CAS缺陷** + +- **ABA问题**:X线程读到为A;Y线程立刻改为B,又改为A;X线程发现值还是A,此时CAS比较值相等,自旋成功 + - 使用数据乐观锁的方式给它加一个版本号或者时间戳,如使用 `AtomicStampedReference` 解决 +- **自旋消耗资源**:多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU + - 破坏掉死循环,当超过一定时间或者一定次数时,return退出 +- **只能保证一个共享变量的原子操作** + - 可以加锁来解决 + - 封装成对象类解决,如使用 `AtomicReference` 解决 \ No newline at end of file diff --git a/src/JAVA/401.md b/src/JAVA/401.md new file mode 100644 index 0000000..a650db4 --- /dev/null +++ b/src/JAVA/401.md @@ -0,0 +1,166 @@ +`synchronized` 是依赖监视器 `Monitor` 实现方法同步或代码块同步的,代码块同步使用的是 `monitorenter` 和 `monitorexit` 指令来实现的: + +- monitorenter指令:是在编译后插入到同步代码块的开始位置 +- monitorexit指令:是插入到方法结束处和异常处的 + +任何对象都有一个 Monitor 与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。synchronized是一种 **互斥锁**,它通过字节码指令monitorenter和monitorexist隐式的来使用lock和unlock操作,synchronized 具有 **原子性** 和 **可见性** 。 + +共享资源代码段又称为**临界区**(`critical section`),保证**临界区互斥**,是指执行**临界区**(`critical section`)的只能有一个线程执行,其他线程阻塞等待,达到排队效果。 + +![synchronized](images/JAVA/synchronized.png) + + + +**synchronize缺点** + +- 性能较低 +- 无法中断一个正在等候获得锁的线程 +- 无法通过投票得到锁,如果不想等下去,也就没办法得到锁 + + + +**synchronized和lock的区别** + +| compare | synchronized | lock | +| :------ | :----------------------- | :---------------------------------------- | +| 哪层面 | 虚拟机层面 | 代码层面 | +| 锁类型 | 可重入、不可中断、非公平 | 可重入、可中断、可公平 | +| 锁获取 | A线程获取锁,B线程等待 | 可以尝试获取锁,不需要一直等待 | +| 锁释放 | 由JVM 释放锁 | 在finally中手动释放。如不释放,会造成死锁 | +| 锁状态 | 无法判断 | 可以判断 | + + + +### Monitor + +在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图: +![JAVA_Monitor](images/JAVA/JAVA_Monitor.jpg) + +- **进入区(Entrt Set)**:表示线程通过synchronized要求获取对象的锁。如果对象未被锁住,则进入拥有者;否则在进入等待区。一旦对象锁被其他线程释放,立即参与竞争 +- **拥有者(The Owner)**:表示某一线程成功竞争到对象锁 +- **等待区(Wait Set)** :表示线程通过对象的wait方法,释放对象的锁,并在等待区等待被唤醒 + +从图中可以看出,一个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。 先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。 + + + +### 使用方式 + +`Synchronized` 的使用方式有三种: + +- **修饰普通函数**。监视器锁(`monitor`)便是对象实例(`this`) +- **修饰静态函数**。视器锁(`monitor`)便是对象的`Class`实例(每个对象只有一个`Class`实例) +- **修饰代码块**。监视器锁(`monitor`)是指定对象实例 + + + +**修饰普通函数** + +![synchronized-修饰普通函数](images/JAVA/synchronized-修饰普通函数.png) + +**修饰静态函数** + +![synchronized-修饰静态函数](images/JAVA/synchronized-修饰静态函数.png) + +**修饰代码块** + +![synchronized-修饰代码块](images/JAVA/synchronized-修饰代码块.png) + + + +### synchronized原理 + +![synchronized原理](images/JAVA/synchronized原理.png) + +![synchronized](images/JAVA/synchronized.jpg) + + + +### synchronized优化 + +`Jdk 1.5`以后对`Synchronized`关键字做了各种的优化,经过优化后`Synchronized`已经变得原来越快了,这也是为什么官方建议使用`Synchronized`的原因,具体的优化点如下: + +- **轻量级锁和偏向锁**:引入轻量级锁和偏向锁来减少重量级锁的使用 +- **适应性自旋(Adaptive Spinning)**:自旋成功,则下次自旋次数增加;自旋失败,则下次自旋次数减少 +- **锁粗化(Lock Coarsening)**:将多次连接在一起的加锁、解锁操作合并为一次,将多个连续锁扩展成一个范围更大的锁 +- **锁消除(Lock Elimination)**:锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁 + + + +### 锁膨胀机制 + +`Java`中每个对象都拥有对象头,对象头由`Mark World` 、指向类的指针、以及数组长度三部分组成,本文,我们只需要关心`Mark World` 即可,`Mark World` 记录了对象的`HashCode`、分代年龄和锁标志位信息。**Mark World简化结构:** + +| 锁状态 | 存储内容 | 锁标记 | +| :------- | :------------------------------------------------------ | :----- | +| 无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 | +| 偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 | +| 轻量级锁 | 指向栈中锁记录的指针 | 00 | +| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | + +锁的升级变化,体现在锁对象的对象头`Mark World`部分,也就是说`Mark World`的内容会随着锁升级而改变。`Java1.5`以后为了减少获取锁和释放锁带来的性能消耗,引入了**偏向锁**和**轻量级锁**,`Synchronized`的升级顺序是 「**无锁-->偏向锁-->轻量级锁-->重量级锁,只会升级不会降级**」 + + + +#### 偏向锁 + +HotSpot 作者经过研究实践发现,在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得的,为了让线程获得锁的代价更低,于是就引进了偏向锁。 + +偏向锁(Biased Locking)指的是,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下会给线程加一个偏向锁。 + + + +**偏向锁执行流程** + +当一个线程访问同步代码块并获取锁时,会在对象头的 Mark Word 里存储锁偏向的线程 ID,在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁,如果 Mark Word 中的线程 ID 和访问的线程 ID 一致,则可以直接进入同步块进行代码执行,如果线程 ID 不同,则使用 CAS 尝试获取锁,如果获取成功则进入同步块执行代码,否则会将锁的状态升级为轻量级锁。具体流程如下: + +![synchronized-偏向锁](images/JAVA/synchronized-偏向锁.png) + + + +**偏向锁的优点** + +偏向锁是为了在无多线程竞争的情况下,尽量减少不必要的锁切换而设计的,因为锁的获取及释放要依赖多次 CAS 原子指令,而偏向锁只需要在置换线程 ID 的时候执行一次 CAS 原子指令即可。 + + + +**Mark Word** + +在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为以下 3 个区域: + +- **对象头(Header)** +- **实例数据(Instance Data)** +- **对齐填充(Padding)** + +对象头中又包含了: + +- **Mark Word(标记字段):我们的偏向锁信息就是存储在此区域的** +- **Klass Pointer(Class 对象指针)** + +对象在内存中的布局如下: + +![MarkWord-对象内存布局](images/JAVA/MarkWord-对象内存布局.jpg) + +在 JDK 1.6 中默认是开启偏向锁的,可以通过“-XX:-UseBiasedLocking=false”命令来禁用偏向锁。 + + + +#### 轻量级锁 + +**轻量级锁考虑的是竞争锁对象的线程不多,持有锁时间也不长的场景**。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失,所以干脆不阻塞这个线程,让它自旋一段时间等待锁释放。当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。 + +![synchronized-轻量级锁](images/JAVA/synchronized-轻量级锁.png) + +**注意事项** + +需要强调一点:**轻量级锁并不是用来代替重量级锁的**,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果同一时间多个线程同时访问时,就会导致轻量级锁膨胀为重量级锁。 + + + +#### 重量级锁 + +轻量级锁膨胀之后,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,这个成本非常高,这就是为什么Java1.6之前Synchronized效率低的原因。 + +升级为重量级锁时,锁标志位的状态值变为10,此时Mark Word中存储内容的是重量级锁的指针,等待锁的线程都会进入阻塞状态,下面是简化版的锁升级过程。 + +![synchronized-重量级锁](images/JAVA/synchronized-重量级锁.png) \ No newline at end of file diff --git a/src/JAVA/402.md b/src/JAVA/402.md new file mode 100644 index 0000000..870c9ad --- /dev/null +++ b/src/JAVA/402.md @@ -0,0 +1,13 @@ +ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchronizer的数据结构,关于AQS的数据结构,在前一篇已经介绍过,不再累赘。 + +- ReentrantLock是一个可重入的互斥锁,又被称为“独占锁” +- ReentrantLock锁在同一个时间点只能被一个线程锁持有;可重入表示,ReentrantLock锁可以被同一个线程多次获取 +- ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁 + + + +**ReentrantLock和synchronized比较** + +- synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活 +- synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁 +- synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断 \ No newline at end of file diff --git a/src/JAVA/403.md b/src/JAVA/403.md new file mode 100644 index 0000000..992dc02 --- /dev/null +++ b/src/JAVA/403.md @@ -0,0 +1,7 @@ +**数据结构** + +![ReentrantReadWriteLock数据结构](images/JAVA/ReentrantReadWriteLock数据结构.jpg) + +- ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 "获取写锁的writeLock()函数" +- ReentrantReadWriteLock中包含:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问 +- 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。sync对象是"FairSync"和"NonfairSync"中的一个,默认是"NonfairSync" \ No newline at end of file diff --git a/src/JAVA/404.md b/src/JAVA/404.md new file mode 100644 index 0000000..174ebf6 --- /dev/null +++ b/src/JAVA/404.md @@ -0,0 +1,86 @@ +- 锁的4种状态:无锁、偏向锁、轻量级锁和重量级锁 +- 锁状态只能升级不能降级 +- 锁的状态是通过对象监视器在对象头中的字段来表明的 + + + +**锁的升级流程** + +- **偏向锁:** 指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价 +- **轻量级锁:** 指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能 +- **重量级锁:** 指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它申请的线程进入阻塞,性能降低 + + + +**Java对象头** + +Hotspot虚拟机的对象头主要包括两部分数据(synchronized的锁就是存在Java对象头中): + +- **Mark Word(标记字段)**:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化 +- **Klass Point(类型指针)**:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 + + + +**Monitor** + +- Monitor可以理解为一个同步工具或一种同步机制 +- synchronized通过Monitor来实现线程同步 +- Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步 +- 每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁 +- Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表 + + + +**锁状态升级流程** + +![锁状态升级流程](images/JAVA/锁状态升级流程.png) + +- **偏向锁**:通过对比Mark Word解决加锁问题,避免执行CAS操作 +- **轻量级锁**:通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能 +- **重量级锁**:将除了拥有锁的线程以外的线程都阻塞 + + + +**优缺点对比** + +| 锁 | 优点 | 缺点 | 适用场景 | +| :------- | :----------------------------------------------------------- | :--------------------------------------------- | :--------------------------------- | +| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 | +| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间同步块执行速度非常快 | +| 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 | + + + +### 无锁 + +- 无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功 +- 修改操作在循环内进行,线程会不断的尝试修改共享资源 +- CAS即是无锁的实现 +- 无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的 + + + +### 偏向锁 + +指同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。适用于只有一个线程访问同步块场景。 + +- **优点**:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 +- **缺点**:如果线程间存在锁竞争,会带来额外的锁撤销的消耗 + + + +### 轻量级锁 + +指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。适用于追求响应时间,同步块执行速度非常快。 + +- **优点**:竞争的线程不会阻塞,提高了程序的响应速度 +- **缺点**:如果始终得不到锁竞争的线程使用自旋会消耗CPU + + + +### 重量级锁 + +指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它申请的线程进入阻塞,性能降低。适用于追求吞吐量,同步块执行速度较长。 + +- **优点**:线程竞争不使用自旋,不会消耗CPU +- **缺点**:线程阻塞,响应时间缓慢 \ No newline at end of file diff --git a/src/JAVA/405.md b/src/JAVA/405.md new file mode 100644 index 0000000..2e69eb0 --- /dev/null +++ b/src/JAVA/405.md @@ -0,0 +1,161 @@ +**指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环**。普通自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。其特点如下: + +- 自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短 +- 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间 +- 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长 + +![自旋锁(SpinLock)](images/JAVA/自旋锁(SpinLock).png) + +简单代码实现: + +```java +public class SpinLock { + private AtomicReference owner = new AtomicReference(); + public void lock() { + Thread currentThread = Thread.currentThread(); + // 如果锁未被占用,则设置当前线程为锁的拥有者 + while (owner.compareAndSet(null, currentThread)) { + } + } + + public void unlock() { + Thread currentThread = Thread.currentThread(); + // 只有锁的拥有者才能释放锁 + owner.compareAndSet(currentThread, null); + } +} +``` + +**缺点** + +- CAS操作需要硬件的配合 +- 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重 +- 没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁 + + + +### Ticket Lock + +TicketLock即为排队自旋锁。思路:类似银行办业务,先取一个号,然后等待叫号叫到自己。好处:保证FIFO,先取号的肯定先进入。而普通的SpinLock,大家都在转圈,锁释放后谁刚好转到这谁进入。简单的实现: + +```java +public class TicketLock { + private AtomicInteger serviceNum = new AtomicInteger(); // 服务号 + private AtomicInteger ticketNum = new AtomicInteger(); // 排队号 + + public int lock() { + // 首先原子性地获得一个排队号 + int myTicketNum = ticketNum.getAndIncrement(); + + // 只要当前服务号不是自己的就不断轮询 + while (serviceNum.get() != myTicketNum) { + } + + return myTicketNum; + } + + public void unlock(int myTicket) { + // 只有当前线程拥有者才能释放锁 + int next = myTicket + 1; + serviceNum.compareAndSet(myTicket, next); + } +} +``` + +**缺点** + +Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。 + + + +### MCS Lock + +MCS SpinLock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。 + +```java +public class MCSLock { + public static class MCSNode { + MCSNode next; + boolean isLocked = true; // 默认是在等待锁 + } + + volatile MCSNode queue ;// 指向最后一个申请锁的MCSNode + private static final AtomicReferenceFieldUpdater UPDATER = + AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode. class, "queue" ); + + public void lock(MCSNode currentThread) { + MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1 + if (predecessor != null) { + predecessor.next = currentThread;// step 2 + while (currentThread.isLocked ) {// step 3 + } + } + } + + public void unlock(MCSNode currentThread) { + if ( UPDATER.get( this ) == currentThread) {// 锁拥有者进行释放锁才有意义 + if (currentThread.next == null) {// 检查是否有人排在自己后面 + if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4 + // compareAndSet返回true表示确实没有人排在自己后面 + return; + } else { + // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者 + // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完 + while (currentThread.next == null) { // step 5 + } + } + } + + currentThread.next.isLocked = false; + currentThread.next = null;// for GC + } + } +} +``` + + + +### CLH Lock + +CLH(Craig, Landin, and Hagersten Locks)锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。 + +```java +public class CLHLock { + public static class CLHNode { + private boolean isLocked = true; // 默认是在等待锁 + } + + @SuppressWarnings("unused" ) + private volatile CLHNode tail ; + private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater + . newUpdater(CLHLock.class, CLHNode .class , "tail" ); + + public void lock(CLHNode currentThread) { + CLHNode preNode = UPDATER.getAndSet( this, currentThread); + if(preNode != null) {//已有线程占用了锁,进入自旋 + while(preNode.isLocked ) { + } + } + } + + public void unlock(CLHNode currentThread) { + // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。 + if (!UPDATER .compareAndSet(this, currentThread, null)) { + // 还有后续线程 + currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋 + } + } +} +``` + +**CLH优势** + +- 公平、FIFO、先来后到的顺序进入锁 +- 且没有竞争同一个变量,因为每个线程只要等待自己的前继释放即可 + +**CLH锁与MCS锁的比较** + +- 从代码实现来看,CLH比MCS要简单得多 +- 从自旋的条件来看,CLH是在本地变量上自旋,MCS是自旋在其他对象的属性 +- 从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的 +- CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性 \ No newline at end of file diff --git a/src/JAVA/406.md b/src/JAVA/406.md new file mode 100644 index 0000000..26cf192 --- /dev/null +++ b/src/JAVA/406.md @@ -0,0 +1,84 @@ +### 乐观锁/悲观锁 + +**悲观锁** +当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候总会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized。 + + + +**乐观锁** +每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响 + + + +**小结**:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁大。 + + + +### 公平锁/非公平锁 + +**公平锁** +指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO: First Input First Output来实现) + + + +**非公平锁** +获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock。 + + + +**小结**:非公平锁性能高于公平锁,更能重复利用CPU的时间。ReentrantLock中可以通过构造方法指定是否为公平锁,默认为非公平锁!synchronized无法指定为公平锁,一直都是非公平锁。 + + + +### 可重入锁/不可重入锁 + +**可重入锁** +也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。一个线程获取锁之后再尝试获取锁时会自动获取锁,可重入锁的优点是避免死锁。 + + + +**不可重入锁** +若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。 + + + +**小结**:可重入锁能一定程度的避免死锁 synchronized、ReentrantLock都是可重入锁。 + + + +### 独占锁/共享锁 + +**独享锁** + +指锁一次只能被一个线程持有。也叫X锁/排它锁/写锁/独享锁:该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得独享锁的线程即能读数据又能修改数据! + + + +**共享锁** + +指锁一次可以被多个线程持有。也叫S锁/读锁,能查看数据,但无法修改和删除数据的一种锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享! + + + +**小结**:ReentrantLock和synchronized都是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁。 + + + +### 互斥锁/读写锁 + +与独享锁/共享锁的概念差不多,是独享锁/共享锁的具体实现。 + +ReentrantLock和synchronized都是互斥锁,ReadWriteLock是读写锁 + + + +### 自旋锁 + +**自旋锁** + +- 一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁。 +- 不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU。 + + + +**常见的自旋锁**:TicketLock,CLHLock,MSCLock \ No newline at end of file diff --git a/src/JAVA/407.md b/src/JAVA/407.md new file mode 100644 index 0000000..4284b1b --- /dev/null +++ b/src/JAVA/407.md @@ -0,0 +1,75 @@ +### 偏向锁/轻量级锁 + +**偏向锁** + +大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了 `让线程获得锁的代价更低` 而引入了偏向锁,让该线程会自动获取锁,减少不必要的CAS操作。 + +**轻量级锁** + +对于轻量级锁,其性能提升依据:“`对于绝大部分锁,在整个生命周期内都是不会存在竞争的`”。轻量级锁的目标:`减少无实际竞争情况下,使用重量级锁产生的性能消耗`,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。 + + + +### 自旋锁 + +**背景** + +线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 + +**定义** +自旋锁就是 `让该线程等待(即执行一段无意义的循环为自旋)固定的一段时间`,不会被立即挂起,看持有锁的线程是否会很快释放锁。 + +**弊端** + +自旋可以避免线程切换带来的开销,但它占用了处理器(CPU)的时间。长时间的自旋而不处理任何事,就会浪费资源,所以需要设置自旋等待时间(即自旋次数)。自旋的次数虽然可以通过参数-XX:PreBlockSpin来调整(默认为10次),但固定的自旋次数,`会对部分场景(如只需要自旋一两次就可获得锁)造成浪费`,因此JDK1.6引入了自适应自旋锁。 + + + +### 适应性自旋锁 + +所谓自适应就意味着 `自旋的次数不再是固定的`,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 + +- **自旋成功,次数增加**:因为虚拟机认为既然上次成功,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多 +- **自旋失败,次数减少**:如果对于某个锁,很少有自旋能够成功的,那么在以后要获得这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源 + + + +### 锁消除 + +为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除,锁消除的依据是 `逃逸分析` 的数据支持。 + +锁消除主要是解决我们使用JDK内置API时存在的 `隐形加锁操作`。如StringBuffer、Vector、HashTable等,StringBuffer的append()方法,Vector的add()方法: + +```java +public void vectorTest(){ + Vector vector = new Vector(); + for(int i = 0 ; i < 10 ; i++){ + vector.add(i + ""); + } + System.out.println(vector); +} +``` + +在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。 + + + +### 锁粗化 + +在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 +在大多数情况下,上述观点是正确的。但如果 `一系列的连续加锁解锁操作,可能会导致不必要的性能损耗`,所以引入锁粗化的概念。锁粗化概念比较好理解,就是 `将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁`。 + +如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。 + + + +### 分段锁 + +分段锁其实是一种锁的设计,并不是具体的一种锁,对于 `ConcurrentHashMap` 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 + + + +### 锁细化 + +- **减少锁的时间**:不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放 +- **减少锁的粒度**:将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。其思想是用空间来换时间 \ No newline at end of file diff --git a/src/JAVA/5.md b/src/JAVA/5.md new file mode 100644 index 0000000..bc504b7 --- /dev/null +++ b/src/JAVA/5.md @@ -0,0 +1,576 @@ +![AQS-简化流程图](images/JAVA/AQS-简化流程图.png) + +### 基础 + +AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,如常用的ReentrantLock、Semaphore、CountDownLatch等。 + +AQS定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,虽然大多数开发者可能永远不会使用AQS实现自己的同步器(JUC包下提供的同步器基本足够应对日常开发),但是知道AQS的原理对于架构设计还是很有帮助的,面试还可以吹吹牛,下面是AQS的组成结构。 + +![AQS的组成结构](images/JAVA/AQS的组成结构.png) + +三部分组成:`volatile int state同步状态`、`Node组成的CLH队列`、`ConditionObject条件变量`(包含Node组成的条件单向队列)。 + + + +**状态** + +- `getState()`:返回同步状态 +- `setState(int newState)`:设置同步状态 +- `compareAndSetState(int expect, int update)`:使用CAS设置同步状态 +- `isHeldExclusively()`:当前线程是否持有资源 + + + +**独占资源(不响应线程中断)** + +- `tryAcquire(int arg)`:独占式获取资源,子类实现 +- `acquire(int arg)`:独占式获取资源模板 +- `tryRelease(int arg)`:独占式释放资源,子类实现 +- `release(int arg)`:独占式释放资源模板 + + + +**共享资源(不响应线程中断)** + +- `tryAcquireShared(int arg)`:共享式获取资源,返回值大于等于0则表示获取成功,否则获取失败,子类实现 +- `acquireShared(int arg)`:共享形获取资源模板 +- `tryReleaseShared(int arg)`:共享式释放资源,子类实现 +- `releaseShared(int arg)`:共享式释放资源模板 + + +### 同步状态 + +在AQS中维护了一个同步状态变量state,getState函数获取同步状态,setState、compareAndSetState函数修改同步状态,对于AQS来说,线程同步的关键是对state的操作,可以说获取、释放资源是否成功都是由state决定的,比如state>0代表可获取资源,否则无法获取,所以state的具体语义由实现者去定义,现有的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch定义的state语义都不一样。 + +- ReentrantLock的state用来表示是否有锁资源 +- ReentrantReadWriteLock的state高16位代表读锁状态,低16位代表写锁状态 +- Semaphore的state用来表示可用信号的个数 +- CountDownLatch的state用来表示计数器的值 + + + +### CLH队列 + +CLH是AQS内部维护的FIFO(先进先出)双端双向队列(方便尾部节点插入),基于链表数据结构,当一个线程竞争资源失败,就会将等待资源的线程封装成一个Node节点,通过CAS原子操作插入队列尾部,最终不同的Node节点连接组成了一个CLH队列,所以说AQS通过CLH队列管理竞争资源的线程,个人总结CLH队列具有如下几个优点: + +- 先进先出保证了公平性 +- 非阻塞的队列,通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入 +- 采用了自旋锁思想,所以CLH也是一种基于链表的可扩展、高性能、公平的自旋锁 + + + +### Node内部类 + +`Node`是`AQS`的内部类,每个等待资源的线程都会封装成`Node`节点组成`CLH`队列、等待队列,所以说`Node`是非常重要的部分,理解它是理解`AQS`的第一步。 + +![AQS-Node](images/JAVA/AQS-Node.png)**waitStatus等待状态如下** + +![AQS-waitStatus等待状态](images/JAVA/AQS-waitStatus等待状态.png) + +**nextWaiter特殊标记** + +- **`Node`在`CLH`队列时,`nextWaiter`表示共享式或独占式标记** +- **`Node`在条件队列时,`nextWaiter`表示下个`Node`节点指针** + + + +### 流程概述 + +线程获取资源失败,封装成`Node`节点从`CLH`队列尾部入队并阻塞线程,某线程释放资源时会把`CLH`队列首部`Node`节点关联的线程唤醒(**此处的首部是指第二个节点,后面会细说**),再次获取资源。 + +![AQS-流程](images/JAVA/AQS-流程.png) + + + +### 入队 + +获取资源失败的线程需要封装成`Node`节点,接着尾部入队,在`AQS`中提供`addWaiter`函数完成`Node`节点的创建与入队。 + +```java +/** + * @description: Node节点入队-CLH队列 + * @param mode 标记Node.EXCLUSIVE独占式 or Node.SHARED共享式 + */ +private Node addWaiter(Node mode) { + // 根据当前线程创建节点,等待状态为0 + Node node = new Node(Thread.currentThread(), mode); + // 获取尾节点 + Node pred = tail; + if (pred != null) { + // 如果尾节点不等于null,把当前节点的前驱节点指向尾节点 + node.prev = pred; + // 通过CAS把尾节点指向当前节点 + if (compareAndSetTail(pred, node)) { + // 之前尾节点的下个节点指向当前节点 + pred.next = node; + return node; + } + } + + // 如果添加失败或队列不存在,执行end函数 + enq(node); + return node; +} +``` + +添加节点的时候,如果从`CLH`队列已经存在,通过`CAS`快速将当前节点添加到队列尾部,如果添加失败或队列不存在,则指向`enq`函数自旋入队。 + +```java +/** + * @description: 自旋cas入队 + * @param node 节点 + */ +private Node enq(final Node node) { + for (;;) { //循环 + //获取尾节点 + Node t = tail; + if (t == null) { + //如果尾节点为空,创建哨兵节点,通过cas把头节点指向哨兵节点 + if (compareAndSetHead(new Node())) + //cas成功,尾节点指向哨兵节点 + tail = head; + } else { + //当前节点的前驱节点设指向之前尾节点 + node.prev = t; + //cas设置把尾节点指向当前节点 + if (compareAndSetTail(t, node)) { + //cas成功,之前尾节点的下个节点指向当前节点 + t.next = node; + return t; + } + } + } +} +``` + +通过自旋`CAS`尝试往队列尾部插入节点,直到成功,自旋过程如果发现`CLH`队列不存在时会初始化`CLH`队列,入队过程流程如下图: + +![AQS-入队过程流程](images/JAVA/AQS-入队过程流程.png) + +第一次循环 + +- 刚开始C L H队列不存在,head与tail都指向null +- 要初始化C L H队列,会创建一个哨兵节点,head与tail都指向哨兵节点 + +第二次循环 + +- 当前线程节点的前驱节点指向尾部节点(哨兵节点) +- 设置当前线程节点为尾部,tail指向当前线程节点 +- 前尾部节点的后驱节点指向当前线程节点(当前尾部节点) + + + +最后结合addWaiter与enq函数的入队流程图如下 +![AQS-入队流程图](images/JAVA/AQS-入队流程图.png) + +### 出队 + +`CLH`队列中的节点都是获取资源失败的线程节点,当持有资源的线程释放资源时,会将`head.next`指向的线程节点唤醒(**`CLH`队列的第二个节点**),如果唤醒的线程节点获取资源成功,线程节点清空信息设置为头部节点(**新哨兵节点**),原头部节点出队(**原哨兵节点**)**acquireQueued函数中的部分代码** + +```java +//1.获取前驱节点 +final Node p = node.predecessor(); +//如果前驱节点是首节点,获取资源(子类实现) +if (p == head && tryAcquire(arg)) { + //2.获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点 + setHead(node); + //3.原来首节点下个节点指向为null + p.next = null; // help GC + //4.非异常状态,防止指向finally逻辑 + failed = false; + //5.返回线程中断状态 + return interrupted; +} + +private void setHead(Node node) { + //节点设置为头部 + head = node; + //清空线程 + node.thread = null; + //清空前驱节点 + node.prev = null; +} +``` + +只需要关注`1~3`步骤即可,过程非常简单,假设获取资源成功,更换头部节点,并把头部节点的信息清除变成哨兵节点,注意这个过程是不需要使用`CAS`来保证,因为只有一个线程能够成功获取到资源。 + +![AQS-出队流程](images/JAVA/AQS-出队流程.png) + + + +### 条件变量 + +Object的wait、notify函数是配合Synchronized锁实现线程间同步协作的功能,A Q S的ConditionObject条件变量也提供这样的功能,通过ConditionObject的await和signal两类函数完成。不同于Synchronized锁,一个A Q S可以对应多个条件变量,而Synchronized只有一个。 +![AQS-条件变量](images/JAVA/AQS-条件变量.png) + +如上图所示,ConditionObject内部维护着一个单向条件队列,不同于C H L队列,条件队列只入队执行await的线程节点,并且加入条件队列的节点,不能在C H L队列, 条件队列出队的节点,会入队到C H L队列。 + +当某个线程执行了ConditionObject的await函数,阻塞当前线程,线程会被封装成Node节点添加到条件队列的末端,其他线程执行ConditionObject的signal函数,会将条件队列头部线程节点转移到C H L队列参与竞争资源,具体流程如下图 +![AQS-CHL队列参与流程](images/JAVA/AQS-CHL队列参与流程.png) + +### 模板方法 + +`AQS`采用了模板方法设计模式,提供了两类模板,一类是独占式模板,另一类是共享形模式,对应的模板函数如下 + +- 独占式 + - **`acquire`获取资源** + - **`release`释放资源** +- 共享式 + - **`acquireShared`获取资源** + - **`releaseShared`释放资源** + +#### 独占式获取资源 + +`acquire`是个模板函数,模板流程就是线程获取共享资源,如果获取资源成功,线程直接返回,否则进入`CLH`队列,直到获取资源成功为止,且整个过程忽略中断的影响,`acquire`函数代码如下 + +![AQS-acquire函数代码](images/JAVA/AQS-acquire函数代码.png) + +- 执行tryAcquire函数,tryAcquire是由子类实现,代表获取资源是否成功,如果资源获取失败,执行下面的逻辑 +- 执行addWaiter函数(前面已经介绍过),根据当前线程创建出独占式节点,并入队CLH队列 +- 执行acquireQueued函数,自旋阻塞等待获取资源 +- 如果acquireQueued函数中获取资源成功,根据线程是否被中断状态,来决定执行线程中断逻辑 + +![AQS-acquireQueued流程](images/JAVA/AQS-acquireQueued流程.png) + +`acquire`函数的大致流程都清楚了,下面来分析下`acquireQueued`函数,线程封装成节点后,是如何自旋阻塞等待获取资源的,代码如下: + +```java +/** + * @description: 自旋机制等待获取资源 + * @param node + * @param arg + * @return: boolean + */ + final boolean acquireQueued(final Node node, int arg) { + //异常状态,默认是 + boolean failed = true; + try { + //该线程是否中断过,默认否 + boolean interrupted = false; + for (;;) {//自旋 + //获取前驱节点 + final Node p = node.predecessor(); + //如果前驱节点是首节点,获取资源(子类实现) + if (p == head && tryAcquire(arg)) { + //获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点 + setHead(node); + //原来首节点下个节点指向为null + p.next = null; // help GC + //非异常状态,防止指向finally逻辑 + failed = false; + //返回线程中断状态 + return interrupted; + } + /** + * 如果前驱节点不是首节点,先执行shouldParkAfterFailedAcquire函数,shouldParkAfterFailedAcquire做了三件事 + * 1.如果前驱节点的等待状态是SIGNAL,返回true,执行parkAndCheckInterrupt函数,返回false + * 2.如果前驱节点的等大状态是CANCELLED,把CANCELLED节点全部移出队列(条件节点) + * 3.以上两者都不符合,更新前驱节点的等待状态为SIGNAL,返回false + */ + if (shouldParkAfterFailedAcquire(p, node) && + //使用LockSupport类的静态方法park挂起当前线程,直到被唤醒,唤醒后检查当前线程是否被中断,返回该线程中断状态并重置中断状态 + parkAndCheckInterrupt()) + //该线程被中断过 + interrupted = true; + } + } finally { + // 尝试获取资源失败并执行异常,取消请求,将当前节点从队列中移除 + if (failed) + cancelAcquire(node); + } + } +``` + +一图胜千言,核心流程图如下: + +![AQS-独占式获取资源流程](images/JAVA/AQS-独占式获取资源流程.png) + + + +#### 独占式释放资源 + +有获取资源,自然就少不了释放资源,`A Q S`中提供了`release`模板函数来释放资源,模板流程就是线程释放资源成功,唤醒`CLH`队列的第二个线程节点(**首节点的下个节点**),代码如下 + +```java + /** + * @description: 独占式-释放资源模板函数 + * @param arg + * @return: boolean + */ + public final boolean release(int arg) { + + if (tryRelease(arg)) {//释放资源成功,tryRelease子类实现 + //获取头部线程节点 + Node h = head; + if (h != null && h.waitStatus != 0) //头部线程节点不为null,并且等待状态不为0 + //唤醒CHL队列第二个线程节点 + unparkSuccessor(h); + return true; + } + return false; + } + + + private void unparkSuccessor(Node node) { + //获取节点等待状态 + int ws = node.waitStatus; + if (ws < 0) + //cas更新节点状态为0 + compareAndSetWaitStatus(node, ws, 0); + + //获取下个线程节点 + Node s = node.next; + if (s == null || s.waitStatus > 0) { //如果下个节点信息异常,从尾节点循环向前获取到正常的节点为止,正常情况不会执行 + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + //唤醒线程节点 + LockSupport.unpark(s.thread); + } + } +``` + +`release`逻辑非常简单,流程图如下: + +![AQS-release流程](images/JAVA/AQS-release流程.png) + + + +#### 共享式获取资源 + +`acquireShared`是个模板函数,模板流程就是线程获取共享资源,如果获取到资源,线程直接返回,否则进入`CLH`队列,直到获取到资源为止,且整个过程忽略中断的影响,`acquireShared`函数代码如下 + +```java + /** + * @description: 共享式-获取资源模板函数 + * @param arg + * @return: void + */ + public final void acquireShared(int arg) { + /** + * 1.负数表示失败 + * 2.0表示成功,但没有剩余可用资源 + * 3.正数表示成功且有剩余资源 + */ + if (tryAcquireShared(arg) < 0) //获取资源失败,tryAcquireShared子类实现 + //自旋阻塞等待获取资源 + doAcquireShared(arg); + } +``` + +`doAcquireShared`函数与独占式的`acquireQueued`函数逻辑基本一致,唯一的区别就是下图红框部分 + +![AQS-共享式获取资源](images/JAVA/AQS-共享式获取资源.png) + +- **节点的标记是共享式** +- **获取资源成功,还会唤醒后续资源,因为资源数可能`>0`,代表还有资源可获取,所以需要做后续线程节点的唤醒** + + + +#### 共享式释放资源 + +`AQS`中提供了`releaseShared`模板函数来释放资源,模板流程就是线程释放资源成功,唤醒CHL队列的第二个线程节点(**首节点的下个节点**),代码如下 + +```java + /** + * @description: 共享式-释放资源模板函数 + * @param arg + * @return: boolean + */ + public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) {//释放资源成功,tryReleaseShared子类实现 + //唤醒后继节点 + doReleaseShared(); + return true; + } + return false; + } + + private void doReleaseShared() { + for (;;) { + //获取头节点 + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; + + if (ws == Node.SIGNAL) {//如果头节点等待状态为SIGNAL + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//更新头节点等待状态为0 + continue; // loop to recheck cases + //唤醒头节点下个线程节点 + unparkSuccessor(h); + } + //如果后继节点暂时不需要被唤醒,更新头节点等待状态为PROPAGATE + else if (ws == 0 && + !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + if (h == head) + break; + } + } +``` + +与独占式释放资源区别不大,都是唤醒头节点的下个节点。 + + + + + + + + + +**什么是AQS?** + +`AQS` 的全称是 `AbstractQueuedSynchronizer`,即`抽象队列同步器`。是Java并发工具的基础,采用乐观锁,通过CAS与自旋轻量级的获取锁。维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。很多JUC包,比如ReentrantLock、Semaphore、CountDownLatch等并发类均是继承AQS,通过AQS的模板方法,来实现的。 + +![AQS](images/JAVA/AQS.png) + +**核心思想** + +- 若请求的共享资源空闲,则将当前请求的线程设置为有效的工作线程,并将共享资源设置为锁定状态 +- 若共享资源被占用,则需要阻塞等待唤醒机制保证锁的分配 + + + +**工作原理** + +**AQS = `同步状态(volatile int state)` + `同步队列(即等待队列,FIFO的CLH队列)` + `条件队列(ConditionObject)`** + +- **state**:代表共享资源。`volatile` 保证并发读,`CAS` 保证并发写 +- **同步队列(即等待队列,CLH队列)**:是CLH变体的虚拟双向队列(先进先出FIFO)来等待获取共享资源。当前线程可以通过signal和signalAll将条件队列中的节点转移到同步队列中 +- **条件队列(ConditionObject)**:当前线程存在于同步队列的头节点,可以通过await从同步队列转移到条件队列中 + + + +**实现原理** + +- 通过CLH队列的变体:FIFO双向队列实现的 +- 每个请求资源的线程被包装成一个节点来实现锁的分配 +- 通过`volatile`的`int`类型的成员变量`state`表示同步状态 +- 通过FIFO队列完成资源获取的排队工作 +- 通过CAS完成对`state`的修改 + + + +### 共享方式 + +AQS定义两种资源共享方式。无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量`state`的获取。`state`是一个原子的int变量,用来表示锁状态、资源数等。 + +**① 独占锁(`Exclusive`)模式**:只能被一个线程获取到(`Reentrantlock`)。 + +![独占锁(Exclusive)模式](images/JAVA/独占锁(Exclusive)模式.png) + + + +**② 共享锁(`Share`)模式**:可以被多个线程同时获取(`Semaphore/CountDownLatch/ReadWriteLock`)。 + +![共享锁(Share)模式](images/JAVA/共享锁(Share)模式.png) + + + +### state机制 + +提供`volatile`变量`state`,用于同步线程之间的共享状态。通过 `CAS` 和 `volatile` 保证其原子性和可见性。核心要点: + +- state 用 `volatile` 修饰,保证多线程中的可见性 +- `getState()` 和 `setState()` 方法**采用final修饰**,限制AQS的子类重写它们两 +- `compareAndSetState()` 方法采用乐观锁思想的CAS算法,也是采用final修饰的,不允许子类重写 + + + +**state应用案例**: + +| 案例 | 描述 | +| ------------------------ | ------------------------------------------------------------ | +| `Semaphore` | 使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数 | +| `CountDownLatch` | 使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过 | +| `ReentrantReadWriteLock` | 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数 | +| `ThreadPoolExecutor` | Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease) | +| `ReentrantLock` | 使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理 | + + + +### 双队列 + +![AQS同步队列与条件队列](images/JAVA/AQS同步队列与条件队列.png) + +![AQS-Lock-Condition](images/JAVA/AQS-Lock-Condition.png) + +- **同步队列(syncQueue):管理多个线程的休眠与唤醒** +- **条件队列(waitQueue):类似wait与signal作用,实现在使用锁时对线程管理** + + + +**注意** + +- 同步队列与条件队列节点可相互转化 +- 一个线程只能存在于两个队列中的一个 + + + +#### 同步队列 + +**同步队列是用来管理多个线程的休眠与唤醒**。 + +同步队列依赖一个双向链表(CHL)来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。通过`signal`或`signalAll`将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列) + +![同步队列(syncQueue)结构](images/JAVA/同步队列(syncQueue)结构.png) + +如果没有锁竞争,线程可以直接获取到锁,就不会进入同步队列。即没有锁竞争时,同步队列(syncQueue)是空的,当存在锁竞争时,线程会进入到同步队列中。一旦进入到同步队列中,就会有线程切换。标准的 CHL 无锁队列是单向链表,同步队列(syncQueue) 在 CHL 基础上做了改进: + +- **同步队列是双向链表**。和二叉树一样,双向链表目前也没有无锁算法的实现。双向链表需要同时设置前驱和后继结点,这两次操作只能保证一个是原子性的 + +- **node.pre一定可以遍历所有结点,是线程安全的**。而后继结点 node.next 则是线程不安全的。也就是说,node.pre 一定可以遍历整个链表,而 node.next 则不一定 + + + +#### 条件队列 + +**条件队列是类似wait与signal作用,实现在使用锁时对线程管理。且由于实现了Condition,对线程的管理可更加细化**。 + +当线程存在于同步队列的头结点时,调用 `await` 方法进行阻塞(从同步队列转化到条件队列)。Condition条件队列(`waitQueue`)要比Lock同步队列(`syncQueue`)简单很多,最重要的原因是 `waitQueue` 的操作都是在获取锁的线程中执行,不存在数据竞争的问题。 + +![Condition等待队列结构](images/JAVA/Condition等待队列结构.png) + +ConditionObject重要的方法说明: + +- **await**:阻塞线程并放弃锁,加入到等待队列中 +- **signal**:唤醒等待线程,没有特殊的要求,尽量使用 signalAll +- **addConditionWaiter**:将结点(状态为 CONDITION)添加到等待队列 waitQueue 中,不存在锁竞争 +- **fullyRelease**:释放锁,并唤醒后继等待线程 +- **isOnSyncQueue**:根据结点是否在同步队列上,判断等待线程是否已经被唤醒 +- **acquireQueued**:Lock 接口中的方法,通过同步队列方法竞争锁 +- **unlinkCancelledWaiters**:清理取消等待的线程 + + + +### 框架架构图 + +![AQS框架架构图](images/JAVA/AQS框架架构图.png) + + + +**常见问题** + +**问题1:state为什么要提供setState和compareAndSetState两种修改状态的方法?** + +这个问题,关键是修改状态时是否存在数据竞争,如果有则必须使用 compareAndSetState。 + +- lock.lock() 获取锁时会发生数据竞争,必须使用 CAS 来保障线程安全,也就是 compareAndSetState 方法 +- lock.unlock() 释放锁时,线程已经获取到锁,没有数据竞争,也就可以直接使用 setState 修改锁的状态 + +**问题2:AQS为什么选择node.prev前驱结点的原子性,而node.next后继结点则是辅助结点?** + +- next 域:需要修改二处来保证原子性,一是 tail.next;二是 tail 指针 +- prev 域:只需要修改一处来保证原子性,就是 tail 指针。你可能会说不需要修改 node.prev 吗?当然需要,但 node 还没添加到链表中,其 node.prev 修改并没有锁竞争的问题,将 tail 指针指向 node 时,如果失败会通过自旋不断尝试 + +**问题3:AQS明知道node.next有可见性问题,为什么还要设计成双向链表?** + +唤醒同步线程时,如果有后继结点,那么时间复杂为 O(1)。否则只能只反向遍历,时间复杂度为 O(n)。以下两种情况,则认为 node.next 不可靠,需要从 tail 反向遍历。 + +- node.next=null:可能结点刚刚插入链表中,node.next 仍为空。此时有其它线程通过 unparkSuccessor 来唤醒该线程 +- node.next.waitStatus>0:结点已经取消,next 值可能已经改变 \ No newline at end of file diff --git a/src/JAVA/501.md b/src/JAVA/501.md new file mode 100644 index 0000000..049247a --- /dev/null +++ b/src/JAVA/501.md @@ -0,0 +1 @@ +![Throwable](images/JAVA/Throwable.png) \ No newline at end of file diff --git a/src/JAVA/502.md b/src/JAVA/502.md new file mode 100644 index 0000000..f4d735c --- /dev/null +++ b/src/JAVA/502.md @@ -0,0 +1 @@ +Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。 \ No newline at end of file diff --git a/src/JAVA/503.md b/src/JAVA/503.md new file mode 100644 index 0000000..e156734 --- /dev/null +++ b/src/JAVA/503.md @@ -0,0 +1,13 @@ +### CheckedException + +检查异常(CheckedException)。一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面: + +- 试图在文件尾部读取数据 +- 试图打开一个错误格式的 URL +- 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在 + + + +### RuntimeException + +运行时异常(RuntimeException)。如 :NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如I/O错误导致的IOException、SQLException。 RuntimeException 是那些可能在Java虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一定是程序员的错。 \ No newline at end of file diff --git a/src/JAVA/504.md b/src/JAVA/504.md new file mode 100644 index 0000000..e691e6c --- /dev/null +++ b/src/JAVA/504.md @@ -0,0 +1,14 @@ +抛出异常有三种形式: + +- throw +- throws +- 系统自动抛异常 + + + +**throw 和 throws 的区别** + +- throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象 +- throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到 +- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象 +- 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理 \ No newline at end of file diff --git a/src/JAVA/6.md b/src/JAVA/6.md new file mode 100644 index 0000000..ab4123b --- /dev/null +++ b/src/JAVA/6.md @@ -0,0 +1 @@ +Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。 \ No newline at end of file diff --git a/src/JAVA/601.md b/src/JAVA/601.md new file mode 100644 index 0000000..673f81a --- /dev/null +++ b/src/JAVA/601.md @@ -0,0 +1 @@ +![Annotation](images/JAVA/Annotation.png) \ No newline at end of file diff --git a/src/JAVA/602.md b/src/JAVA/602.md new file mode 100644 index 0000000..0fd1b2e --- /dev/null +++ b/src/JAVA/602.md @@ -0,0 +1,183 @@ +Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类、成员内部类、局部内部类和匿名内部类四种。 + +### 静态内部类 + +使用static修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。没有这个引用就意味着: + +- 它的创建是不需要依赖于外围类的 +- 它不能使用任何外围类的非static成员变量和方法 + +```java +public class OuterClass { + private String sex; + public static String name = "chenssy"; + + /** + *静态内部类 + */ + static class InnerClass1{ + /* 在静态内部类中可以存在静态成员 */ + public static String _name1 = "chenssy_static"; + + public void display(){ + /* + * 静态内部类只能访问外围类的静态成员变量和方法 + * 不能访问外围类的非静态成员变量和方法 + */ + System.out.println("OutClass name :" + name); + } + } + + /** + * 非静态内部类 + */ + class InnerClass2{ + /* 非静态内部类中不能存在静态成员 */ + public String _name2 = "chenssy_inner"; + /* 非静态内部类中可以调用外围类的任何成员,不管是静态的还是非静态的 */ + public void display(){ + System.out.println("OuterClass name:" + name); + } + } + + /** + * @desc 外围类方法 + * @author chenssy + * @data 2013-10-25 + * @return void + */ + public void display(){ + /* 外围类访问静态内部类:内部类. */ + System.out.println(InnerClass1._name1); + /* 静态内部类 可以直接创建实例不需要依赖于外围类 */ + new InnerClass1().display(); + + /* 非静态内部的创建需要依赖于外围类 */ + OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2(); + /* 方位非静态内部类的成员需要使用非静态内部类的实例 */ + System.out.println(inner2._name2); + inner2.display(); + } + + public static void main(String[] args) { + OuterClass outer = new OuterClass(); + outer.display(); + } +} +``` + + + +### 成员内部类 + +成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所有 成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。在成员内部类中要注意两点: + +- **成员内部类中不能存在任何static的变量和方法** +- **成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类** +- **推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时** + +```java +public class OuterClass { + private String str; + + public class InnerClass{ + public void innerDisplay(){ + //使用外围内的属性 + str = "chenssy..."; + System.out.println(str); + //使用外围内的方法 + outerDisplay(); + } + } + + /* 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时 */ + public InnerClass getInnerClass(){ + return new InnerClass(); + } + + public static void main(String[] args) { + OuterClass outer = new OuterClass(); + OuterClass.InnerClass inner = outer.getInnerClass(); + inner.innerDisplay(); + } +} +``` + + + +### 局部内部类 + +有这样一种内部类,它是嵌套在方法和作用于内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。 + +- 定义在方法里: + + ```java + public class Parcel5 { + public Destionation destionation(String str){ + class PDestionation implements Destionation{ + private String label; + private PDestionation(String whereTo){ + label = whereTo; + } + public String readLabel(){ + return label; + } + } + return new PDestionation(str); + } + + public static void main(String[] args) { + Parcel5 parcel5 = new Parcel5(); + Destionation d = parcel5.destionation("chenssy"); + } + } + ``` + +- 定义在作用域内: + + ```java + public class Parcel6 { + private void internalTracking(boolean b){ + if(b){ + class TrackingSlip{ + private String id; + TrackingSlip(String s) { + id = s; + } + String getSlip(){ + return id; + } + } + TrackingSlip ts = new TrackingSlip("chenssy"); + String string = ts.getSlip(); + } + } + + public void track(){ + internalTracking(true); + } + + public static void main(String[] args) { + Parcel6 parcel6 = new Parcel6(); + parcel6.track(); + } + } + ``` + + + +### 匿名内部类 + +- 匿名内部类是没有访问修饰符的 +- new 匿名内部类,这个类首先是要存在的。如果我们将那个InnerClass接口注释掉,就会出现编译出错 +- 注意getInnerClass()方法的形参,第一个形参是用final修饰的,而第二个却没有。同时我们也发现第二个形参在匿名内部类中没有使用过,所以当所在方法的形参需要被匿名内部类使用,那么这个形参就必须为final +- 匿名内部类是没有构造方法的。因为它连名字都没有何来构造方法 + +```java +button2.addActionListener( + new ActionListener(){ + public void actionPerformed(ActionEvent e) { + System.out.println("你按了按钮二"); + } + }); +``` \ No newline at end of file diff --git a/src/JAVA/603.md b/src/JAVA/603.md new file mode 100644 index 0000000..ea6f781 --- /dev/null +++ b/src/JAVA/603.md @@ -0,0 +1,61 @@ +### 获取类泛型类型 + +获取当前类上的泛型类型方式如下: + +```java +public class DefaultTargetType { + + private Type type; + private Class classType; + + @SuppressWarnings("unchecked") + public DefaultTargetType() { + Type superClass = getClass().getGenericSuperclass(); + this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + if (this.type instanceof ParameterizedType) { + this.classType = (Class) ((ParameterizedType) this.type).getRawType(); + } else { + this.classType = (Class) this.type; + } + } + +} +``` + +获取到泛型中的类型方式: + +```java +Class> classType = new DefaultTargetType>() {}.getClassType(); +``` + + + +### 获取接口泛型类型 + +获取当前类的父类接口上的泛型类型方式如下: + +```java +public class DefaultTargetType implements TargetType { + + private Type type; + private Class classType; + + @SuppressWarnings("unchecked") + public DefaultTargetType() { + Type superClass = getClass().getGenericInterfaces()[0]; + this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + if (this.type instanceof ParameterizedType) { + this.classType = (Class) ((ParameterizedType) this.type).getRawType(); + } else { + this.classType = (Class) this.type; + } + } + +} +``` + +获取到泛型中的类型方式: + +```java +Class> classType = new DefaultTargetType>() {}.getClassType(); +``` \ No newline at end of file diff --git a/src/JAVA/604.md b/src/JAVA/604.md new file mode 100644 index 0000000..f561e08 --- /dev/null +++ b/src/JAVA/604.md @@ -0,0 +1,9 @@ +### 浅复制 + +被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。 + + + +### 深复制 + +被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。 \ No newline at end of file diff --git a/src/JAVA/7.md b/src/JAVA/7.md new file mode 100644 index 0000000..958f406 --- /dev/null +++ b/src/JAVA/7.md @@ -0,0 +1,40 @@ +Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。 + + + +**volatile特性与原理** + +- **可见性**:volatile修饰的变量,JVM保证了每次都跳过工作内存和缓存行(CPU Cache)来读取主内存中的最新值 +- **有序性**:即JVM使用内存屏障来禁止了该变量操作的指令重排序优化 +- **volatile性能**:volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行 +- **内存屏障**:加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(也称内存栅栏) +- **轻量级同步机制**:Java提供了一种稍弱同步机制,即volatile变量,是一种比sychronized关键字更轻量级的同步机制 +- **禁止重排序**:volatile 禁止了指令重排 + + + +**使用场景** + +- 状态标记量 + +- DCL(Double Check Lock) + + + +**CPU缓存** + +CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多。按照读取顺序与CPU结合的紧密程度,CPU缓存可分为: + +- **一级缓存**:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存 +- **二级缓存**:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半 +- **三级缓存**:简称L3 Cache,部分高端CPU才有 + + + +**volatile的特性** + +- 保证了线程可见性,不保证原子性,保证一定的有序性(禁止指令重排序) +- 在JVM底层volatile是采用“内存屏障”来实现的 +- volatile常用场景:状态标记、DCL(双重检查锁,Double Check) +- volatile不会引起线程上下文的切换和调度 +- 基于lock前缀指令,相当于内存屏障(内存栅栏) \ No newline at end of file diff --git a/src/JAVA/8.md b/src/JAVA/8.md new file mode 100644 index 0000000..3121d93 --- /dev/null +++ b/src/JAVA/8.md @@ -0,0 +1,325 @@ +### 函数式接口 + +函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。使用@FunctionalInterface注解修饰的类,编译器会检测该类是否只有一个抽象方法或接口,否则,会报错。可以有多个默认方法,静态方法。JAVA8自带的常用函数式接口: + +| 函数接口 | 抽象方法 | 功能 | 参数 | 返回类型 | 示例 | +| -------------- | --------------- | ---------------------- | ----- | -------- | ------------------- | +| Predicate | test(T t) | 判断真假 | T | boolean | 身高大于185cm吗? | +| Consumer | accept(T t) | 消费消息 | T | void | 输出一个值 | +| Function | R apply(T t) | 将T映射为R(转换功能) | T | R | 取student对象的名字 | +| Supplier | T get() | 生产消息 | None | T | 工厂方法 | +| UnaryOperator | T apply(T t) | 一元操作 | T | T | 逻辑非(!) | +| BinaryOperator | apply(T t, U u) | 二元操作 | (T,T) | (T) | 求两个数的乘积(*) | + + + +### 常用的流 + +#### collect + +**将流转换为集合。有toList()、toSet()、toMap()等,及早求值**。 + +```java +public class TestCase { + public static void main(String[] args) { + List studentList = Stream.of(new Student("路飞", 22, 175), + new Student("红发", 40, 180), new Student("白胡子", 50, 185)).collect(Collectors.toList()); + System.out.println(studentList); + } +} + +// 输出结果 +// [Student{name='路飞', age=22, stature=175, specialities=null},  +// Student{name='红发', age=40, stature=180, specialities=null},  +// Student{name='白胡子', age=50, stature=185, specialities=null}] +``` + + + +#### filter + +顾名思义,起**过滤筛选**的作用。**内部就是Predicate接口。惰性求值。** + +![lambda-filter](images/JAVA/lambda-filter.jpg) + +```java +public class TestCase { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + List list = students.stream() + .filter(stu -> stu.getStature() < 180) + .collect(Collectors.toList()); + System.out.println(list); + } +} + +// 输出结果 +// [Student{name='路飞', age=22, stature=175, specialities=null}] +``` + + + +#### map + +**转换功能,内部就是Function接口。惰性求值。** + +![lambda-map](images/JAVA/lambda-map.jpg) + +```java +public class TestCase { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + List names = students.stream().map(student -> student.getName()) + .collect(Collectors.toList()); + System.out.println(names); + } +} + +// 输出结果 +// [路飞, 红发, 白胡子] +``` + +例子中将student对象转换为String对象,获取student的名字。 + + + +#### flatMap + +**将多个Stream合并为一个Stream。惰性求值。** + +![lambda-flatMap](images/JAVA/lambda-flatMap.jpg) + +```java +public class TestCase { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + List studentList = Stream.of(students, + asList(new Student("艾斯", 25, 183), + new Student("雷利", 48, 176))) + .flatMap(students1 -> students1.stream()).collect(Collectors.toList()); + System.out.println(studentList); + } +} + +// 输出结果 +// [Student{name='路飞', age=22, stature=175, specialities=null}, +// Student{name='红发', age=40, stature=180, specialities=null}, +// Student{name='白胡子', age=50, stature=185, specialities=null}, +// Student{name='艾斯', age=25, stature=183, specialities=null}, +// Student{name='雷利', age=48, stature=176, specialities=null}] +``` + +调用Stream.of的静态方法将两个list转换为Stream,再通过flatMap将两个流合并为一个。 + + + +#### max和min + +我们经常会在集合中**求最大或最小值**,使用流就很方便。**及早求值。** + +```java +public class TestCase { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + Optional max = students.stream() + .max(Comparator.comparing(stu -> stu.getAge())); + Optional min = students.stream() + .min(Comparator.comparing(stu -> stu.getAge())); + //判断是否有值 + if (max.isPresent()) { + System.out.println(max.get()); + } + if (min.isPresent()) { + System.out.println(min.get()); + } + } +} + +// 输出结果 +// Student{name='白胡子', age=50, stature=185, specialities=null} +// Student{name='路飞', age=22, stature=175, specialities=null} +``` + +**max、min接收一个Comparator**(例子中使用java8自带的静态函数,只需要传进需要比较值即可),并且返回一个Optional对象,该对象是java8新增的类,专门为了防止null引发的空指针异常。可以使用max.isPresent()判断是否有值;可以使用max.orElse(new Student()),当值为null时就使用给定值;也可以使用max.orElseGet(() -> new Student());这需要传入一个Supplier的lambda表达式。 + + + +#### count + +**统计功能,一般都是结合filter使用,因为先筛选出我们需要的再统计即可。及早求值。** + +```java +public class TestCase { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + long count = students.stream().filter(s1 -> s1.getAge() < 45).count(); + System.out.println("年龄小于45岁的人数是:" + count); + } +} + +// 输出结果 +// 年龄小于45岁的人数是:2 +``` + + + +#### reduce + +**reduce 操作可以实现从一组值中生成一个值**。在上述例子中用到的 count 、 min 和 max 方法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作。**及早求值。** + +![lambda-reduce](images/JAVA/lambda-reduce.jpg) + +```java +public class TestCase { + public static void main(String[] args) { + Integer reduce = Stream.of(1, 2, 3, 4).reduce(0, (acc, x) -> acc+ x); + System.out.println(reduce); + } +} + +// 输出结果:10 +``` + +我们看得reduce接收了一个初始值为0的累加器,依次取出值与累加器相加,最后累加器的值就是最终的结果。 + + + +### 高级集合类及收集器 + +#### 转换成值 + +**收集器,一种通用的、从流生成复杂值的结构。**只要将它传给 collect 方法,所有的流就都可以使用它了。标准类库已经提供了一些有用的收集器,**以下示例代码中的收集器都是从 java.util.stream.Collectors 类中静态导入的。** + +```java +public class CollectorsTest { + public static void main(String[] args) { + List students1 = new ArrayList<>(3); + students1.add(new Student("路飞", 23, 175)); + students1.add(new Student("红发", 40, 180)); + students1.add(new Student("白胡子", 50, 185)); + + OutstandingClass ostClass1 = new OutstandingClass("一班", students1); + //复制students1,并移除一个学生 + List students2 = new ArrayList<>(students1); + students2.remove(1); + OutstandingClass ostClass2 = new OutstandingClass("二班", students2); + //将ostClass1、ostClass2转换为Stream + Stream classStream = Stream.of(ostClass1, ostClass2); + OutstandingClass outstandingClass = biggestGroup(classStream); + System.out.println("人数最多的班级是:" + outstandingClass.getName()); + + System.out.println("一班平均年龄是:" + averageNumberOfStudent(students1)); + } + + /** + * 获取人数最多的班级 + */ + private static OutstandingClass biggestGroup(Stream outstandingClasses) { + return outstandingClasses.collect( + maxBy(comparing(ostClass -> ostClass.getStudents().size()))) + .orElseGet(OutstandingClass::new); + } + + /** + * 计算平均年龄 + */ + private static double averageNumberOfStudent(List students) { + return students.stream().collect(averagingInt(Student::getAge)); + } +} + +// 输出结果 +// 人数最多的班级是:一班 +// 一班平均年龄是:37.666666666666664 +``` + +maxBy或者minBy就是求最大值与最小值。 + + + +#### 转换成块 + +**常用的流操作是将其分解成两个集合,Collectors.partitioningBy帮我们实现了,接收一个Predicate函数式接口。** + +![lambda-partitioningBy](images/JAVA/lambda-partitioningBy.jpg) + +将示例学生分为会唱歌与不会唱歌的两个集合。 + +```java +public class PartitioningByTest { + public static void main(String[] args) { + // 省略List students的初始化 + Map> listMap = students.stream().collect( + Collectors.partitioningBy(student -> student.getSpecialities(). + contains(SpecialityEnum.SING))); + } +} +``` + + + +#### 数据分组 + +数据分组是一种更自然的分割数据操作,与将数据分成 ture 和 false 两部分不同,**可以使用任意值对数据分组。Collectors.groupingBy接收一个Function做转换。** + +![lambda-groupingBy](images/JAVA/lambda-groupingBy.jpg) + +**如图,使用groupingBy将根据进行分组为圆形一组,三角形一组,正方形一组。**例子:根据学生第一个特长进行分组 + +```java +public class GroupingByTest { + public static void main(String[] args) { + //省略List students的初始化 + Map> listMap = + students.stream().collect( + Collectors.groupingBy(student -> student.getSpecialities().get(0))); + } +} +``` + +Collectors.groupingBy与SQL 中的 group by 操作是一样的。 + + + +#### 字符串拼接 + +如果将所有学生的名字拼接起来,怎么做呢?通常只能创建一个StringBuilder,循环拼接。使用Stream,使用Collectors.joining()简单容易。 + +```java +public class JoiningTest { + public static void main(String[] args) { + List students = new ArrayList<>(3); + students.add(new Student("路飞", 22, 175)); + students.add(new Student("红发", 40, 180)); + students.add(new Student("白胡子", 50, 185)); + + String names = students.stream() + .map(Student::getName).collect(Collectors.joining(",","[","]")); + System.out.println(names); + } +} +//输出结果 +//[路飞,红发,白胡子] +``` + +joining接收三个参数,第一个是分界符,第二个是前缀符,第三个是结束符。也可以不传入参数Collectors.joining(),这样就是直接拼接。 \ No newline at end of file diff --git a/src/JAVA/9.md b/src/JAVA/9.md new file mode 100644 index 0000000..e94c4cb --- /dev/null +++ b/src/JAVA/9.md @@ -0,0 +1,15 @@ +Striped64的设计思路是在竞争激烈的时候尽量分散竞争。 + + + +**Striping(条带化)** + +大多数磁盘系统都对访问次数(每秒的 I/O 操作,IOPS)和数据传输率(每秒传输的数据量,TPS)有限制。当达到这些限制时,后续需要访问磁盘的进程就需要等待,这就是所谓的磁盘冲突。当多个进程同时访问一个磁盘时,可能会出现磁盘冲突。因此,避免磁盘冲突是优化 I/O 性能的一个重要目标。 + +条带(strip)是把连续的数据分割成相同大小的数据块,把每段数据分别写入到阵列中的不同磁盘上的方法。使用条带化技术使得多个进程同时访问数据的多个不同部分而不会造成磁盘冲突,而且在需要对这种数据进行顺序访问的时候可以获得最大程度上的 I/O 并行能力,从而获得非常好的性能。 + + + +**Striped64设计** + +Striped64通过维护一个原子更新Cell表和一个base字段,并使用每个线程的探针字段作为哈希码映射到表的指定Cell。当竞争激烈时,将多线程的更新分散到不同Cell进行,有效降低了高并发下CAS更新的竞争,从而最大限度地提高了Striped64的吞吐量。Striped64为实现高吞吐量的并发计数组件奠定了基础,其中LongAdder就是基于Striped64实现,此外Java8中ConcurrentHashMap实现的并发计数功能也是基于Striped64的设计理念,还有hystrix、guava等实现的并发计数组件也离不开Striped64。 \ No newline at end of file diff --git a/src/JAVA/README.md b/src/JAVA/README.md new file mode 100644 index 0000000..6f47696 --- /dev/null +++ b/src/JAVA/README.md @@ -0,0 +1,5 @@ +
JAVA
+ +Introduction:收纳技术相关的JAVA知识 `JUC`、`Thread`、`Lock`、`I/O` 等总结! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/JAVA/_sidebar.md b/src/JAVA/_sidebar.md new file mode 100644 index 0000000..c689a9b --- /dev/null +++ b/src/JAVA/_sidebar.md @@ -0,0 +1,54 @@ +* 🏁J.U.C + * [✍并发特性](src/JAVA/1 "并发特性") + * [✍Unsafe](src/JAVA/2 "Unsafe") + * [✍LockSupport](src/JAVA/3 "LockSupport") + * [✍CAS机制](src/JAVA/4 "CAS机制") + * [✍AQS框架](src/JAVA/5 "AQS框架") + * [✍Condition](src/JAVA/6 "Condition") + * [✍volatile](src/JAVA/7 "volatile") + * [✍lambda](src/JAVA/8 "lambda") + * [✍Striped64](src/JAVA/9 "Striped64") + * [✍LongAdder](src/JAVA/10 "LongAdder") + * [✍Semaphore](src/JAVA/11 "Semaphore") + * [✍CyclicBarrier](src/JAVA/12 "CyclicBarrier") + * [✍CountDownLatch](src/JAVA/13 "CountDownLatch") + * [✍CompletableFuture](src/JAVA/14 "CompletableFuture") +* 🏁集合 + * [✍List](src/JAVA/101 "List") + * [✍Set](src/JAVA/102 "Set") + * [✍Map](src/JAVA/103 "Map") +* 🏁Queue + * [✍队列](src/JAVA/201 "队列") + * [✍阻塞队列](src/JAVA/202 "阻塞队列") +* [🏁Thread](src/JAVA/301 "Thread") + * [✍线程实现方式](src/JAVA/302 "线程实现方式") + * [✍线程创建方式](src/JAVA/303 "线程创建方式") + * [✍线程生命周期](src/JAVA/304 "线程生命周期") + * [✍JDK线程池](src/JAVA/305 "JDK线程池") + * [✍线程方法](src/JAVA/306 "线程方法") + * [✍线程安全](src/JAVA/307 "线程安全") + * [✍线程同步](src/JAVA/308 "线程同步") + * [✍多线程通信](src/JAVA/309 "多线程通信") + * [✍线程协作](src/JAVA/310 "线程协作") + * [✍线程死锁](src/JAVA/311 "线程死锁") + * [✍守护线程](src/JAVA/312 "守护线程") + * [✍常见问题](src/JAVA/313 "常见问题") + * [✍ThreadLocal](src/JAVA/314 "ThreadLocal") + * [✍ThreadPoolExecutor](src/JAVA/315 "ThreadPoolExecutor") +* 🏁Lock + * [✍synchronized](src/JAVA/401 "synchronized") + * [✍ReentrantLock](src/JAVA/402 "ReentrantLock") + * [✍ReentrantReadWriteLock](src/JAVA/403 "ReentrantReadWriteLock") + * [✍锁的状态](src/JAVA/404 "锁的状态") + * [✍自旋锁(SpinLock)](src/JAVA/405 "自旋锁(SpinLock)") + * [✍常见锁](src/JAVA/406 "常见锁") + * [✍锁优化](src/JAVA/407 "锁优化") +* [🏁Throwable](src/JAVA/501 "Throwable") + * [✍Error](src/JAVA/502 "Error") + * [✍Exception](src/JAVA/503 "Exception") + * [✍异常处理方式](src/JAVA/504 "异常处理方式") +* 🏁Others + * [✍Annotation](src/JAVA/601 "Annotation") + * [✍JAVA内部类](src/JAVA/602 "JAVA内部类") + * [✍泛型](src/JAVA/603 "泛型") + * [✍JAVA复制](src/JAVA/604 "JAVA复制") \ No newline at end of file diff --git a/src/JAVA/images/JAVA/007S8ZIlly1gh4fy6gvw0j30w0093jsu.jpg b/src/JAVA/images/JAVA/007S8ZIlly1gh4fy6gvw0j30w0093jsu.jpg new file mode 100644 index 0000000..ad4acc0 Binary files /dev/null and b/src/JAVA/images/JAVA/007S8ZIlly1gh4fy6gvw0j30w0093jsu.jpg differ diff --git a/src/JAVA/images/JAVA/007S8ZIlly1gh4ipc80hfj30w10hugo5.jpg b/src/JAVA/images/JAVA/007S8ZIlly1gh4ipc80hfj30w10hugo5.jpg new file mode 100644 index 0000000..2c3eff2 Binary files /dev/null and b/src/JAVA/images/JAVA/007S8ZIlly1gh4ipc80hfj30w10hugo5.jpg differ diff --git a/src/JAVA/images/JAVA/007S8ZIlly1gh4mkx8upjj30jz06m74u.jpg b/src/JAVA/images/JAVA/007S8ZIlly1gh4mkx8upjj30jz06m74u.jpg new file mode 100644 index 0000000..0f6e2f3 Binary files /dev/null and b/src/JAVA/images/JAVA/007S8ZIlly1gh4mkx8upjj30jz06m74u.jpg differ diff --git a/src/JAVA/images/JAVA/007S8ZIlly1gh4nh8v3haj30w10bbabr.jpg b/src/JAVA/images/JAVA/007S8ZIlly1gh4nh8v3haj30w10bbabr.jpg new file mode 100644 index 0000000..63f411e Binary files /dev/null and b/src/JAVA/images/JAVA/007S8ZIlly1gh4nh8v3haj30w10bbabr.jpg differ diff --git a/src/JAVA/images/JAVA/AQS-CHL队列参与流程.png b/src/JAVA/images/JAVA/AQS-CHL队列参与流程.png new file mode 100644 index 0000000..a0a2918 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-CHL队列参与流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-Lock-Condition.png b/src/JAVA/images/JAVA/AQS-Lock-Condition.png new file mode 100644 index 0000000..7f82150 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-Lock-Condition.png differ diff --git a/src/JAVA/images/JAVA/AQS-Node.png b/src/JAVA/images/JAVA/AQS-Node.png new file mode 100644 index 0000000..7de4e61 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-Node.png differ diff --git a/src/JAVA/images/JAVA/AQS-acquireQueued流程.png b/src/JAVA/images/JAVA/AQS-acquireQueued流程.png new file mode 100644 index 0000000..a0597eb Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-acquireQueued流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-acquire函数代码.png b/src/JAVA/images/JAVA/AQS-acquire函数代码.png new file mode 100644 index 0000000..bea1d2d Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-acquire函数代码.png differ diff --git a/src/JAVA/images/JAVA/AQS-release流程.png b/src/JAVA/images/JAVA/AQS-release流程.png new file mode 100644 index 0000000..69f5afb Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-release流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-waitStatus等待状态.png b/src/JAVA/images/JAVA/AQS-waitStatus等待状态.png new file mode 100644 index 0000000..3a68eac Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-waitStatus等待状态.png differ diff --git a/src/JAVA/images/JAVA/AQS-入队流程图.png b/src/JAVA/images/JAVA/AQS-入队流程图.png new file mode 100644 index 0000000..fe04e6c Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-入队流程图.png differ diff --git a/src/JAVA/images/JAVA/AQS-入队过程流程.png b/src/JAVA/images/JAVA/AQS-入队过程流程.png new file mode 100644 index 0000000..26a3c47 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-入队过程流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-共享式获取资源.png b/src/JAVA/images/JAVA/AQS-共享式获取资源.png new file mode 100644 index 0000000..475b968 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-共享式获取资源.png differ diff --git a/src/JAVA/images/JAVA/AQS-出队流程.png b/src/JAVA/images/JAVA/AQS-出队流程.png new file mode 100644 index 0000000..80e47e7 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-出队流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-条件变量.png b/src/JAVA/images/JAVA/AQS-条件变量.png new file mode 100644 index 0000000..302fb04 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-条件变量.png differ diff --git a/src/JAVA/images/JAVA/AQS-流程.png b/src/JAVA/images/JAVA/AQS-流程.png new file mode 100644 index 0000000..47a7e82 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-独占式获取资源流程.png b/src/JAVA/images/JAVA/AQS-独占式获取资源流程.png new file mode 100644 index 0000000..0a74b76 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-独占式获取资源流程.png differ diff --git a/src/JAVA/images/JAVA/AQS-简化流程图.png b/src/JAVA/images/JAVA/AQS-简化流程图.png new file mode 100644 index 0000000..a7b2eed Binary files /dev/null and b/src/JAVA/images/JAVA/AQS-简化流程图.png differ diff --git a/src/JAVA/images/JAVA/AQS.png b/src/JAVA/images/JAVA/AQS.png new file mode 100644 index 0000000..08ceae5 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS.png differ diff --git a/src/JAVA/images/JAVA/AQS同步队列与条件队列.png b/src/JAVA/images/JAVA/AQS同步队列与条件队列.png new file mode 100644 index 0000000..6788eef Binary files /dev/null and b/src/JAVA/images/JAVA/AQS同步队列与条件队列.png differ diff --git a/src/JAVA/images/JAVA/AQS框架架构图.png b/src/JAVA/images/JAVA/AQS框架架构图.png new file mode 100644 index 0000000..611b188 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS框架架构图.png differ diff --git a/src/JAVA/images/JAVA/AQS的组成结构.png b/src/JAVA/images/JAVA/AQS的组成结构.png new file mode 100644 index 0000000..932d124 Binary files /dev/null and b/src/JAVA/images/JAVA/AQS的组成结构.png differ diff --git a/src/JAVA/images/JAVA/Annotation.png b/src/JAVA/images/JAVA/Annotation.png new file mode 100644 index 0000000..70ba9a5 Binary files /dev/null and b/src/JAVA/images/JAVA/Annotation.png differ diff --git a/src/JAVA/images/JAVA/CompletableFuture.jpg b/src/JAVA/images/JAVA/CompletableFuture.jpg new file mode 100644 index 0000000..29c34c2 Binary files /dev/null and b/src/JAVA/images/JAVA/CompletableFuture.jpg differ diff --git a/src/JAVA/images/JAVA/ConcurrentSkipListMap数据结构.jpg b/src/JAVA/images/JAVA/ConcurrentSkipListMap数据结构.jpg new file mode 100644 index 0000000..d024b0b Binary files /dev/null and b/src/JAVA/images/JAVA/ConcurrentSkipListMap数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/ConcurrentSkipListSet数据结构.jpg b/src/JAVA/images/JAVA/ConcurrentSkipListSet数据结构.jpg new file mode 100644 index 0000000..014c7bf Binary files /dev/null and b/src/JAVA/images/JAVA/ConcurrentSkipListSet数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/Condition等待队列结构.png b/src/JAVA/images/JAVA/Condition等待队列结构.png new file mode 100644 index 0000000..3a216de Binary files /dev/null and b/src/JAVA/images/JAVA/Condition等待队列结构.png differ diff --git a/src/JAVA/images/JAVA/CopyOnWriteArrayList数据结构.jpg b/src/JAVA/images/JAVA/CopyOnWriteArrayList数据结构.jpg new file mode 100644 index 0000000..978585b Binary files /dev/null and b/src/JAVA/images/JAVA/CopyOnWriteArrayList数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/CopyOnWriteArraySet数据结构.jpg b/src/JAVA/images/JAVA/CopyOnWriteArraySet数据结构.jpg new file mode 100644 index 0000000..cdb71d4 Binary files /dev/null and b/src/JAVA/images/JAVA/CopyOnWriteArraySet数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/CountDownLatch数据结构.jpg b/src/JAVA/images/JAVA/CountDownLatch数据结构.jpg new file mode 100644 index 0000000..7e287cf Binary files /dev/null and b/src/JAVA/images/JAVA/CountDownLatch数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/CyclicBarrier数据结构.jpg b/src/JAVA/images/JAVA/CyclicBarrier数据结构.jpg new file mode 100644 index 0000000..8109949 Binary files /dev/null and b/src/JAVA/images/JAVA/CyclicBarrier数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/JAVA_Monitor.jpg b/src/JAVA/images/JAVA/JAVA_Monitor.jpg new file mode 100644 index 0000000..a6ef2b3 Binary files /dev/null and b/src/JAVA/images/JAVA/JAVA_Monitor.jpg differ diff --git a/src/JAVA/images/JAVA/Java7ConcurrentHashMap结构.png b/src/JAVA/images/JAVA/Java7ConcurrentHashMap结构.png new file mode 100644 index 0000000..74dd923 Binary files /dev/null and b/src/JAVA/images/JAVA/Java7ConcurrentHashMap结构.png differ diff --git a/src/JAVA/images/JAVA/Java7HashMap结构.png b/src/JAVA/images/JAVA/Java7HashMap结构.png new file mode 100644 index 0000000..20c3064 Binary files /dev/null and b/src/JAVA/images/JAVA/Java7HashMap结构.png differ diff --git a/src/JAVA/images/JAVA/Java8ConcurrentHashMap结构.png b/src/JAVA/images/JAVA/Java8ConcurrentHashMap结构.png new file mode 100644 index 0000000..8648e60 Binary files /dev/null and b/src/JAVA/images/JAVA/Java8ConcurrentHashMap结构.png differ diff --git a/src/JAVA/images/JAVA/Java8HashMap结构.png b/src/JAVA/images/JAVA/Java8HashMap结构.png new file mode 100644 index 0000000..2a977e2 Binary files /dev/null and b/src/JAVA/images/JAVA/Java8HashMap结构.png differ diff --git a/src/JAVA/images/JAVA/LongAdder原理.png b/src/JAVA/images/JAVA/LongAdder原理.png new file mode 100644 index 0000000..374361e Binary files /dev/null and b/src/JAVA/images/JAVA/LongAdder原理.png differ diff --git a/src/JAVA/images/JAVA/LongAdder和AtomicLong性能对比.png b/src/JAVA/images/JAVA/LongAdder和AtomicLong性能对比.png new file mode 100644 index 0000000..948252d Binary files /dev/null and b/src/JAVA/images/JAVA/LongAdder和AtomicLong性能对比.png differ diff --git a/src/JAVA/images/JAVA/MarkWord-对象内存布局.jpg b/src/JAVA/images/JAVA/MarkWord-对象内存布局.jpg new file mode 100644 index 0000000..729e7f1 Binary files /dev/null and b/src/JAVA/images/JAVA/MarkWord-对象内存布局.jpg differ diff --git a/src/JAVA/images/JAVA/ReentrantReadWriteLock数据结构.jpg b/src/JAVA/images/JAVA/ReentrantReadWriteLock数据结构.jpg new file mode 100644 index 0000000..44c8787 Binary files /dev/null and b/src/JAVA/images/JAVA/ReentrantReadWriteLock数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/Semaphore数据结构.jpg b/src/JAVA/images/JAVA/Semaphore数据结构.jpg new file mode 100644 index 0000000..4e4b9ab Binary files /dev/null and b/src/JAVA/images/JAVA/Semaphore数据结构.jpg differ diff --git a/src/JAVA/images/JAVA/Thread-BLOCKED.png b/src/JAVA/images/JAVA/Thread-BLOCKED.png new file mode 100644 index 0000000..8437d10 Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-BLOCKED.png differ diff --git a/src/JAVA/images/JAVA/Thread-NEW.png b/src/JAVA/images/JAVA/Thread-NEW.png new file mode 100644 index 0000000..9acf6c4 Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-NEW.png differ diff --git a/src/JAVA/images/JAVA/Thread-RUNNABLE.png b/src/JAVA/images/JAVA/Thread-RUNNABLE.png new file mode 100644 index 0000000..167816a Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-RUNNABLE.png differ diff --git a/src/JAVA/images/JAVA/Thread-TERMINATED.png b/src/JAVA/images/JAVA/Thread-TERMINATED.png new file mode 100644 index 0000000..3da4442 Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-TERMINATED.png differ diff --git a/src/JAVA/images/JAVA/Thread-TIMED_WAITING.png b/src/JAVA/images/JAVA/Thread-TIMED_WAITING.png new file mode 100644 index 0000000..aae3671 Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-TIMED_WAITING.png differ diff --git a/src/JAVA/images/JAVA/Thread-WAITING.png b/src/JAVA/images/JAVA/Thread-WAITING.png new file mode 100644 index 0000000..5e6a614 Binary files /dev/null and b/src/JAVA/images/JAVA/Thread-WAITING.png differ diff --git a/src/JAVA/images/JAVA/ThreadPoolExecutor任务调度流程.png b/src/JAVA/images/JAVA/ThreadPoolExecutor任务调度流程.png new file mode 100644 index 0000000..fbe43d0 Binary files /dev/null and b/src/JAVA/images/JAVA/ThreadPoolExecutor任务调度流程.png differ diff --git a/src/JAVA/images/JAVA/ThreadPoolExecutor状态转换.png b/src/JAVA/images/JAVA/ThreadPoolExecutor状态转换.png new file mode 100644 index 0000000..9496379 Binary files /dev/null and b/src/JAVA/images/JAVA/ThreadPoolExecutor状态转换.png differ diff --git a/src/JAVA/images/JAVA/ThreadPoolExecutor线程销毁流程.png b/src/JAVA/images/JAVA/ThreadPoolExecutor线程销毁流程.png new file mode 100644 index 0000000..32b1c8e Binary files /dev/null and b/src/JAVA/images/JAVA/ThreadPoolExecutor线程销毁流程.png differ diff --git a/src/JAVA/images/JAVA/ThreadPoolExecutor运行流程.png b/src/JAVA/images/JAVA/ThreadPoolExecutor运行流程.png new file mode 100644 index 0000000..8486a20 Binary files /dev/null and b/src/JAVA/images/JAVA/ThreadPoolExecutor运行流程.png differ diff --git a/src/JAVA/images/JAVA/Throwable.png b/src/JAVA/images/JAVA/Throwable.png new file mode 100644 index 0000000..17569b8 Binary files /dev/null and b/src/JAVA/images/JAVA/Throwable.png differ diff --git a/src/JAVA/images/JAVA/lambda-filter.jpg b/src/JAVA/images/JAVA/lambda-filter.jpg new file mode 100644 index 0000000..c44c90c Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-filter.jpg differ diff --git a/src/JAVA/images/JAVA/lambda-flatMap.jpg b/src/JAVA/images/JAVA/lambda-flatMap.jpg new file mode 100644 index 0000000..6efc0ce Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-flatMap.jpg differ diff --git a/src/JAVA/images/JAVA/lambda-groupingBy.jpg b/src/JAVA/images/JAVA/lambda-groupingBy.jpg new file mode 100644 index 0000000..0d00ef4 Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-groupingBy.jpg differ diff --git a/src/JAVA/images/JAVA/lambda-map.jpg b/src/JAVA/images/JAVA/lambda-map.jpg new file mode 100644 index 0000000..b63833a Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-map.jpg differ diff --git a/src/JAVA/images/JAVA/lambda-partitioningBy.jpg b/src/JAVA/images/JAVA/lambda-partitioningBy.jpg new file mode 100644 index 0000000..010e7d4 Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-partitioningBy.jpg differ diff --git a/src/JAVA/images/JAVA/lambda-reduce.jpg b/src/JAVA/images/JAVA/lambda-reduce.jpg new file mode 100644 index 0000000..98e69b8 Binary files /dev/null and b/src/JAVA/images/JAVA/lambda-reduce.jpg differ diff --git a/src/JAVA/images/JAVA/synchronized-修饰代码块.png b/src/JAVA/images/JAVA/synchronized-修饰代码块.png new file mode 100644 index 0000000..2cdb3bd Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-修饰代码块.png differ diff --git a/src/JAVA/images/JAVA/synchronized-修饰普通函数.png b/src/JAVA/images/JAVA/synchronized-修饰普通函数.png new file mode 100644 index 0000000..3f897a8 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-修饰普通函数.png differ diff --git a/src/JAVA/images/JAVA/synchronized-修饰静态函数.png b/src/JAVA/images/JAVA/synchronized-修饰静态函数.png new file mode 100644 index 0000000..37fe026 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-修饰静态函数.png differ diff --git a/src/JAVA/images/JAVA/synchronized-偏向锁.png b/src/JAVA/images/JAVA/synchronized-偏向锁.png new file mode 100644 index 0000000..b8994f0 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-偏向锁.png differ diff --git a/src/JAVA/images/JAVA/synchronized-轻量级锁.png b/src/JAVA/images/JAVA/synchronized-轻量级锁.png new file mode 100644 index 0000000..8ebce65 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-轻量级锁.png differ diff --git a/src/JAVA/images/JAVA/synchronized-重量级锁.png b/src/JAVA/images/JAVA/synchronized-重量级锁.png new file mode 100644 index 0000000..e6fd0b7 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized-重量级锁.png differ diff --git a/src/JAVA/images/JAVA/synchronized.jpg b/src/JAVA/images/JAVA/synchronized.jpg new file mode 100644 index 0000000..7ee938d Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized.jpg differ diff --git a/src/JAVA/images/JAVA/synchronized.png b/src/JAVA/images/JAVA/synchronized.png new file mode 100644 index 0000000..04bf1c4 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized.png differ diff --git a/src/JAVA/images/JAVA/synchronized原理.png b/src/JAVA/images/JAVA/synchronized原理.png new file mode 100644 index 0000000..deec6b7 Binary files /dev/null and b/src/JAVA/images/JAVA/synchronized原理.png differ diff --git a/src/JAVA/images/JAVA/任务拒绝.png b/src/JAVA/images/JAVA/任务拒绝.png new file mode 100644 index 0000000..5e8629a Binary files /dev/null and b/src/JAVA/images/JAVA/任务拒绝.png differ diff --git a/src/JAVA/images/JAVA/任务缓冲策略.png b/src/JAVA/images/JAVA/任务缓冲策略.png new file mode 100644 index 0000000..adf9b23 Binary files /dev/null and b/src/JAVA/images/JAVA/任务缓冲策略.png differ diff --git a/src/JAVA/images/JAVA/共享锁(Share)模式.png b/src/JAVA/images/JAVA/共享锁(Share)模式.png new file mode 100644 index 0000000..0e91d09 Binary files /dev/null and b/src/JAVA/images/JAVA/共享锁(Share)模式.png differ diff --git a/src/JAVA/images/JAVA/同步队列(syncQueue)结构.png b/src/JAVA/images/JAVA/同步队列(syncQueue)结构.png new file mode 100644 index 0000000..ef3b86c Binary files /dev/null and b/src/JAVA/images/JAVA/同步队列(syncQueue)结构.png differ diff --git a/src/JAVA/images/JAVA/数据结构—数组.png b/src/JAVA/images/JAVA/数据结构—数组.png new file mode 100644 index 0000000..be1f546 Binary files /dev/null and b/src/JAVA/images/JAVA/数据结构—数组.png differ diff --git a/src/JAVA/images/JAVA/数据结构—环型结构.png b/src/JAVA/images/JAVA/数据结构—环型结构.png new file mode 100644 index 0000000..3aaaeda Binary files /dev/null and b/src/JAVA/images/JAVA/数据结构—环型结构.png differ diff --git a/src/JAVA/images/JAVA/源码指令.png b/src/JAVA/images/JAVA/源码指令.png new file mode 100644 index 0000000..62e90c0 Binary files /dev/null and b/src/JAVA/images/JAVA/源码指令.png differ diff --git a/src/JAVA/images/JAVA/独占锁(Exclusive)模式.png b/src/JAVA/images/JAVA/独占锁(Exclusive)模式.png new file mode 100644 index 0000000..65bb447 Binary files /dev/null and b/src/JAVA/images/JAVA/独占锁(Exclusive)模式.png differ diff --git a/src/JAVA/images/JAVA/生命周期状态.png b/src/JAVA/images/JAVA/生命周期状态.png new file mode 100644 index 0000000..f73a519 Binary files /dev/null and b/src/JAVA/images/JAVA/生命周期状态.png differ diff --git a/src/JAVA/images/JAVA/线程池生命周期.png b/src/JAVA/images/JAVA/线程池生命周期.png new file mode 100644 index 0000000..b4f5f6e Binary files /dev/null and b/src/JAVA/images/JAVA/线程池生命周期.png differ diff --git a/src/JAVA/images/JAVA/线程状态切换.jpg b/src/JAVA/images/JAVA/线程状态切换.jpg new file mode 100644 index 0000000..06f1372 Binary files /dev/null and b/src/JAVA/images/JAVA/线程状态切换.jpg differ diff --git a/src/JAVA/images/JAVA/自旋锁(SpinLock).png b/src/JAVA/images/JAVA/自旋锁(SpinLock).png new file mode 100644 index 0000000..f7077eb Binary files /dev/null and b/src/JAVA/images/JAVA/自旋锁(SpinLock).png differ diff --git a/src/JAVA/images/JAVA/获取任务流程图.png b/src/JAVA/images/JAVA/获取任务流程图.png new file mode 100644 index 0000000..012f981 Binary files /dev/null and b/src/JAVA/images/JAVA/获取任务流程图.png differ diff --git a/src/JAVA/images/JAVA/锁状态升级流程.png b/src/JAVA/images/JAVA/锁状态升级流程.png new file mode 100644 index 0000000..773ad58 Binary files /dev/null and b/src/JAVA/images/JAVA/锁状态升级流程.png differ diff --git a/src/JAVA/images/JAVA/队列类图.png b/src/JAVA/images/JAVA/队列类图.png new file mode 100644 index 0000000..27f0062 Binary files /dev/null and b/src/JAVA/images/JAVA/队列类图.png differ diff --git a/src/Middleware/1.md b/src/Middleware/1.md new file mode 100644 index 0000000..8f40599 --- /dev/null +++ b/src/Middleware/1.md @@ -0,0 +1 @@ +SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名,配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类,正因为该特性,我们可以很容易的通过 SPI 机制为程序提供拓展功能。 \ No newline at end of file diff --git a/src/Middleware/1001.md b/src/Middleware/1001.md new file mode 100644 index 0000000..a8daed7 --- /dev/null +++ b/src/Middleware/1001.md @@ -0,0 +1 @@ +InfluxDB 是用Go语言编写的一个开源分布式时序、事件和指标数据库,无需外部依赖。InfluxDB在DB-Engines的时序数据库类别里排名第一。 \ No newline at end of file diff --git a/src/Middleware/1002.md b/src/Middleware/1002.md new file mode 100644 index 0000000..85786ea --- /dev/null +++ b/src/Middleware/1002.md @@ -0,0 +1,5 @@ +- **极简架构:**单机版的InfluxDB只需要安装一个binary,即可运行使用,完全没有任何的外部依赖 +- **极强的写入能力:** 底层采用自研的TSM存储引擎,TSM也是基于LSM的思想,提供极强的写能力以及高压缩率 +- **高效查询:**对Tags会进行索引,提供高效的检索 +- **InfluxQL**:提供QL-Like的查询语言,极大的方便了使用,数据库在易用性上演进的终极目标都是提供Query Language +- **Continuous Queries**: 通过CQ能够支持auto-rollup和pre-aggregation,对常见的查询操作可以通过CQ来预计算加速查询 \ No newline at end of file diff --git a/src/Middleware/1003.md b/src/Middleware/1003.md new file mode 100644 index 0000000..09cec27 --- /dev/null +++ b/src/Middleware/1003.md @@ -0,0 +1,34 @@ +InfluxDB 采用自研的TSM (Time-Structured Merge Tree) 作为存储引擎, 其核心思想是通过牺牲掉一些功能来对性能达到极致优化,其官方文档上有项目存储引擎经历了从LevelDB到BlotDB,再到选择自研TSM的过程,整个选择转变的思考。 + +**时序数据库的需求:** + +- 数十亿个单独的数据点 +- 高写入吞吐量 +- 高读取吞吐量 +- 大型删除(数据过期) +- 主要是插入/追加工作负载,很少更新 + + + +### LSM + +**LSM 的局限性** + +在官方文档上有写, 为了解决高写入吞吐量的问题, Influxdb 一开始选择了LevelDB 作为其存储引擎。 然而,随着更多地了解人们对时间序列数据的需求,influxdb遇到了一些无法克服的挑战。 + + + +**LSM (日志结构合并树)为 LevelDB的引擎原理** + +- levelDB 不支持热备份。 对数据库进行安全备份必须关闭后才能复制。LevelDB的RocksDB和HyperLevelDB变体可以解决此问题 +- 时序数据库需要提供一种自动管理数据保存的方式。 即删除过期数据, 而在levelDB 中,删除的代价过高。(通过添加墓碑的方式, 段结构合并的时候才会真正物理性的删除) + + + +### TSM + +按不同的时间范围划分为不同的分区(Shard),因为时序数据写入都是按时间线性产生的,所以分区的产生也是按时间线性增长的,写入通常是在最新的分区,而不会散列到多个分区。分区的优点是数据回收的物理删除非常简单,直接把整个分区删除即可。 + +- 在最开始的时候, influxdb 采用的方案每个shard都是一个独立的数据库实例,底层都是一套独立的LevelDB存储引擎。 这时带来的问题是,LevelDB底层采用level compaction策略,每个存储引擎都会打开比较多的文件,随着shard的增多,最终进程打开的文件句柄会很快触及到上限 +- 由于遇到大量的客户反馈文件句柄过多的问题,InfluxDB在新版本的存储引擎选型中选择了BoltDB替换LevelDB。BoltDB底层数据结构是mmap B+树。 但由于B+ 树会产生大量的随机写。 所以写入性能较差 +- 之后Influxdb 最终决定仿照LSM 的思想自研TSM ,主要改进点是基于时序数据库的特性作出一些优化,包含Cache、WAL以及Data File等各个组件,也会有flush、compaction等这类数据操作 \ No newline at end of file diff --git a/src/Middleware/1004.md b/src/Middleware/1004.md new file mode 100644 index 0000000..44ffa26 --- /dev/null +++ b/src/Middleware/1004.md @@ -0,0 +1,5 @@ +![InfluxDB系统架构](images/Middleware/InfluxDB系统架构.jpg) + +- **DataBase:**用户可以通过 create database xxx 来创建一个database +- **Retention Policy(RP):** 数据保留策略, 可用来规定数据的的过期时间 +- **Shard Group:** 实现了数据分区,但是Shard Group只是一个逻辑概念,在它里面包含了大量Shard,Shard才是InfluxDB中真正存储数据以及提供读写服务的概念 \ No newline at end of file diff --git a/src/Middleware/101.md b/src/Middleware/101.md new file mode 100644 index 0000000..c24e381 --- /dev/null +++ b/src/Middleware/101.md @@ -0,0 +1,19 @@ +**Java类型所占字节数(或bit数)** + +| 类型 | 存储(byte) | bit数(bit) | 取值范围 | +| ------- | ---------- | ---------- | ------------------------------------------------------------ | +| int | 4字节 | 4×8位 | 即 (-2)的31次方 ~ (2的31次方) - 1 | +| short | 2字节 | 2×8位 | 即 (-2)的15次方 ~ (2的15次方) - 1 | +| long | 8字节 | 8×8位 | 即 (-2)的63次方 ~ (2的63次方) - 1 | +| byte | 1字节 | 1×8位 | 即 (-2)的7次方 ~ (2的7次方) - 1,-128~127 | +| float | 4字节 | 4×8位 | float 类型的数值有一个后缀 F(例如:3.14F) | +| double | 8字节 | 8×8位 | 没有后缀 F 的浮点数值(例如:3.14)默认为 double | +| boolean | 1字节 | 1×8位 | true、false | +| char | 2字节 | 2×8位 | Java中,只要是字符,不管是数字还是英文还是汉字,都占两个字节 | + +**注意**: + +- 英文的数字、字母或符号:1个字符 = 1个字节数 +- 中文的数字、字母或符号:1个字符 = 2个字节数 +- 计算机的基本单位:bit 。一个bit代表一个0或1,1个字节是8个bit +- 1TB=1024GB,1GB=1024MB,1MB=1024KB,1KB=1024B(字节,byte),1B=8b(bit,位) \ No newline at end of file diff --git a/src/Middleware/102.md b/src/Middleware/102.md new file mode 100644 index 0000000..16cde59 --- /dev/null +++ b/src/Middleware/102.md @@ -0,0 +1,106 @@ +![Redis线程模型](images/Middleware/Redis线程模型.png) + +Redis内部使用文件事件处理器`File Event Handler`,这个文件事件处理器是单线程的所以Redis才叫做单线程的模型。它采用`I/O`多路复用机制同时监听多个`Socket`,将产生事件的`Socket`压入到内存队列中,事件分派器根据`Socket`上的事件类型来选择对应的事件处理器来进行处理。文件事件处理器包含5个部分: + +- **多个Socket** +- **I/O多路复用程序** +- **Scocket队列** +- **文件事件分派器** +- **事件处理器**(连接应答处理器、命令请求处理器、命令回复处理器) + +![Redis文件事件处理器](images/Middleware/Redis文件事件处理器.png) + + + +### 通信流程 + +客户端与redis的一次通信过程: + +![Redis请求过程](images/Middleware/Redis请求过程.png) + +- **请求类型1**:`客户端发起建立连接的请求` + - 服务端会产生一个`AE_READABLE`事件,`I/O`多路复用程序接收到`server socket`事件后,将该`socket`压入队列中 + - 文件事件分派器从队列中获取`socket`,交给连接应答处理器,创建一个可以和客户端交流的`socket01` + - 将`socket01`的`AE_READABLE`事件与命令请求处理器关联 + +- **请求类型2**:`客户端发起set key value请求` + - `socket01`产生`AE_READABLE`事件,`socket01`压入队列 + - 将获取到的`socket01`与命令请求处理器关联 + - 命令请求处理器读取`socket01`中的`key value`,并在内存中完成对应的设置 + - 将`socket01`的`AE_WRITABLE`事件与命令回复处理器关联 + +- **请求类型3**:`服务端返回结果` + - `Redis`中的`socket01`会产生一个`AE_WRITABLE`事件,压入到队列中 + - 将获取到的`socket01`与命令回复处理器关联 + - 回复处理器对`socket01`输入操作结果,如`ok`。之后解除`socket01`的`AE_WRITABLE`事件与命令回复处理器的关联 + + + +### 文件事件处理器 + +- **基于 Reactor 模式开发了自己的网络事件处理器(文件事件处理器,file event handler)** +- 文件事件处理器 **使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字**,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 +- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件 +- 文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性 + + + +### I/O多路复用 + +I/O多路复用的I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。 +I/O多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking I/O只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞I/O好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。 + + + +**select机制** +**基本原理** +客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的I/O操作。 +**优点** + +- 几乎在所有的平台上支持,跨平台支持性好 + +**缺点** + +- 由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降 +- 每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间) +- 默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。 + + + +**poll机制** +基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd)。 + + + +**epoll机制** +**基本原理** +没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。epoll之所以高性能是得益于它的三个函数: + +- `epoll_create()`:系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd +- `epoll_ctl()`:每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数 +- `epoll_wait()`:轮训所有的callback集合,并完成对应的IO操作 + +**优点** + +- 没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄 +- 效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降 +- 内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件方法,即将一个文件或其它对象映射到进程的地址空间) + + + +例子:100万个连接,里面有1万个连接是活跃,我们可以对比 select、poll、epoll 的性能表现: + +- `select`:不修改宏定义默认是1024,则需要100w/1024=977个进程才可以支持 100万连接,会使得CPU性能特别的差 +- `poll`: 没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量资源 +- `epoll`: 请求进来时就创建fd并绑定一个callback,主需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝 + + + +### 执行效率高 + +**Redis是单线程模型为什么效率还这么高?** + +- `纯内存操作`:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础 +- `非阻塞的I/O多路复用机制`:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了时间,不在I/O上浪费过多的时间 +- `C语言实现`:距离操作系统更近,执行速度会更快 +- `单线程避免切换开销`:单线程避免了多线程上下文切换的时间开销,预防了多线程可能产生的竞争问题 \ No newline at end of file diff --git a/src/Middleware/103.md b/src/Middleware/103.md new file mode 100644 index 0000000..b7160bd --- /dev/null +++ b/src/Middleware/103.md @@ -0,0 +1,83 @@ +在 Redis 中,常用的 5 种数据类型和应用场景如下: + +- **String:** 缓存、计数器、分布式锁等 +- **List:** 链表、队列、微博关注人时间轴列表等 +- **Hash:** 用户信息、Hash 表等 +- **Set:** 去重、赞、踩、共同好友等 +- **Zset:** 访问量排行榜、点击量排行榜等 + +![Redis-基本数据类型](images/Middleware/Redis-基本数据类型.png) + +### String(字符串) + +String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字(当数字类型用 Long 可以表示的时候encoding 就是整型,其他都存储在 sdshdr 当做字符串)。 + + + +### Hash(字典) + +![Redis-Hash](images/Middleware/Redis-Hash.png) + +在 Memcached 中,我们经常将一些结构化的信息打包成 hashmap,在客户端序列化后存储为一个字符串的值(一般是 JSON 格式),比如用户的昵称、年龄、性别、积分等。这时候在需要修改其中某一项时,通常需要将字符串(JSON)取出来,然后进行反序列化,修改某一项的值,再序列化成字符串(JSON)存储回去。简单修改一个属性就干这么多事情,消耗必定是很大的,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而 Redis 的 Hash 结构可以使你像在数据库中 Update 一个属性一样只修改某一项属性值。 + +- 存储、读取、修改用户属性 + +**实战场景** + +- 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力 +- 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源 +- session:常见方案spring session + redis实现session共享 + + + +### List(列表) + +List 说白了就是链表(redis 使用双端链表实现的 List),相信学过数据结构知识的人都应该能理解其结构。使用 List 结构,我们可以轻松地实现最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一个应用就是消息队列,可以利用 List 的 *PUSH 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。Redis 还提供了操作 List 中某一段元素的 API,你可以直接查询,删除 List 中某一段的元素。 + +- 微博 TimeLine +- 消息队列 + +![Redis-List](images/Middleware/Redis-List.png) + +使用列表的技巧 + +- lpush+lpop=Stack(栈) +- lpush+rpop=Queue(队列) +- lpush+ltrim=Capped Collection(有限集合) +- lpush+brpop=Message Queue(消息队列) + +实战场景: + +- timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。 + + + +### Set(集合) + +![Redis-Set](images/Middleware/Redis-Set.png) + +Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。因为 Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。 + +- 共同好友、二度好友 +- 利用唯一性,可以统计访问网站的所有独立 IP +- 好友推荐的时候,根据 tag 求交集,大于某个 threshold 就可以推荐 + +实战场景; + +- 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人 +- 点赞,或点踩,收藏等,可以放到set中实现 + + + +### Sorted Set(有序集合) + +![Redis-SortedSet](images/Middleware/Redis-SortedSet.png) + +和Sets相比,Sorted Sets是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,比如一个存储全班同学成绩的 Sorted Sets,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务。让重要的任务优先执行。 + +- 带有权重的元素,比如一个游戏的用户得分排行榜 +- 比较复杂的数据结构,一般用到的场景不算太多 + +实战场景: + +- 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。 \ No newline at end of file diff --git a/src/Middleware/104.md b/src/Middleware/104.md new file mode 100644 index 0000000..e7be52a --- /dev/null +++ b/src/Middleware/104.md @@ -0,0 +1,223 @@ +### HyperLogLog(基数统计) + +HyperLogLog 主要的应用场景就是进行基数统计。实际上不会存储每个元素的值,它使用的是概率算法,通过存储元素的hash值的第一个1的位置,来计算元素数量。HyperLogLog 可用极小空间完成独立数统计。命令如下: + +| 命令 | 作用 | +| ----------------------------- | ----------------------- | +| pfadd key element ... | 将所有元素添加到key中 | +| pfcount key | 统计key的估算值(不精确) | +| pgmerge new_key key1 key2 ... | 合并key至新key | + + + +**应用案例** + +如何统计 Google 主页面每天被多少个不同的账户访问过? + +对于 Google 这种访问量巨大的网页而言,其实统计出有十亿的访问量或十亿零十万的访问量其实是没有太多的区别的,因此,在这种业务场景下,为了节省成本,其实可以只计算出一个大概的值,而没有必要计算出精准的值。 + +对于上面的场景,可以使用`HashMap`、`BitMap`和`HyperLogLog`来解决。对于这三种解决方案,这边做下对比: + +- `HashMap`:算法简单,统计精度高,对于少量数据建议使用,但是对于大量的数据会占用很大内存空间 +- `BitMap`:位图算法,具体内容可以参考我的这篇,统计精度高,虽然内存占用要比`HashMap`少,但是对于大量数据还是会占用较大内存 +- `HyperLogLog`:存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素,对于上面举例的应用场景,建议使用 + + + +### Geo(地理空间信息) + +Geo主要用于存储地理位置信息,并对存储的信息进行操作(添加、获取、计算两位置之间距离、获取指定范围内位置集合、获取某地点指定范围内集合)。Redis支持将Geo信息存储到有序集合(zset)中,再通过Geohash算法进行填充。命令如下: + +| 命令 | 作用 | +| ------------------------------------ | ------------------------------------------------------------ | +| geoadd key latitude longitude member | 添加成员位置(纬度、经度、名称)到key中 | +| geopos key member ... | 获取成员geo坐标 | +| geodist key member1 member2 [unit] | 计算成员位置间距离。若两个位置之间的其中一个不存在, 那返回空值 | +| georadius | 基于经纬度坐标范围查询 | +| georadiusbymember | 基于成员位置范围查询 | +| geohash | 计算经纬度hash | + + + +**GEORADIUS** + +```properties +GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] +``` + +以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。范围可以使用以下其中一个单位: + +- **m** 表示单位为米 +- **km** 表示单位为千米 +- **mi** 表示单位为英里 +- **ft** 表示单位为英尺 + +在给定以下可选项时, 命令会返回额外的信息: + +- `WITHDIST`: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离单位和范围单位保持一致 +- `WITHCOORD`: 将位置元素的经度和维度也一并返回 +- `WITHHASH`: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大 + +命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式: + +- `ASC`: 根据中心的位置, 按照从近到远的方式返回位置元素 +- `DESC`: 根据中心的位置, 按照从远到近的方式返回位置元素 + +在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 **COUNT ``** 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 `COUNT` 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 `COUNT` 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。 + + + +### Pub/Sub(发布订阅) + +发布订阅类似于广播功能。redis发布订阅包括 发布者、订阅者、Channel。常用命令如下: + +| 命令 | 作用 | 时间复杂度 | +| ----------------------- | ---------------------------------- | ------------------------------------------------------------ | +| subscribe channel | 订阅一个频道 | O(n) | +| unsubscribe channel ... | 退订一个/多个频道 | O(n) | +| publish channel msg | 将信息发送到指定的频道 | O(n+m),n 是频道 channel 的订阅者数量, M 是使用模式订阅(subscribed patterns)的客户端的数量 | +| pubsub CHANNELS | 查看订阅与发布系统状态(多种子模式) | O(n) | +| psubscribe | 订阅多个频道 | O(n) | +| unsubscribe | 退订多个频道 | O(n) | + + + +### Bitmap(位图) + +Bitmap就是位图,其实也就是字节数组(byte array),用一串连续的2进制数字(0或1)表示,每一位所在的位置为偏移(offset),位图就是用每一个二进制位来存放或者标记某个元素对应的值。通常是用来判断某个数据存不存在的,因为是用bit为单位来存储所以Bitmap本身会极大的节省储存空间。常用命令如下: + +| 命令 | 作用 | 时间复杂度 | +| --------------------------- | --------------------------------------------------- | ---------- | +| setbit key offset val | 给指定key的值的第offset赋值val | O(1) | +| getbit key offset | 获取指定key的第offset位 | O(1) | +| bitcount key start end | 返回指定key中[start,end]中为1的数量 | O(n) | +| bitop operation destkey key | 对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR) | O(n) | + + + +**应用案例** + +有1亿用户,5千万登陆用户,那么统计每日用户的登录数。每一位标识一个用户ID,当某个用户访问我们的网站就在Bitmap中把标识此用户的位设置为1。使用set集合和Bitmap存储的对比: + +| 数据类型 | 每个 userid 占用空间 | 需要存储的用户量 | 全部占用内存量 | +| -------- | ------------------------------------------------------------ | ---------------- | ---------------------------- | +| set | 32位也就是4个字节(假设userid用的是整型,实际很多网站用的是长整型) | 50,000,000 | 32位 * 50,000,000 = 200 MB | +| Bitmap | 1 位(bit) | 100,000,000 | 1 位 * 100,000,000 = 12.5 MB | + + + +**应用场景** + +- 用户在线状态 +- 用户签到状态 +- 统计独立用户 + + + +### BloomFilter(布隆过滤) + +![Redis-BloomFilter](images/Middleware/Redis-BloomFilter.jpg) + +当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(使用多个哈希函数对**元素key (bloom中不存value)** 进行哈希,算出一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个无偏哈希函数都会得到一个不同的位置),把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了: + +- 如果这些点有任何一个为0,则被检元素一定不在 +- 如果都是1,并不能完全说明这个元素就一定存在其中,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会出现误判的原因 + + + +**应用场景** + +- **解决缓存穿透**:事先把存在的key都放到redis的**Bloom Filter** 中,他的用途就是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存 +- **黑名单校验**:假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,再收到邮件时,判断邮件地址是否在布隆过滤器中即可 +- **Web拦截器**:用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中,从而提高缓存命中率 + + + +#### 基于Bitmap数据结构 + +```java +import com.google.common.base.Preconditions; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Service +public class RedisService { + + @Resource + private RedisTemplate redisTemplate; + + /** + * 根据给定的布隆过滤器添加值 + */ + public void addByBloomFilter(BloomFilterHelper bloomFilterHelper, String key, T value) { + Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空"); + int[] offset = bloomFilterHelper.murmurHashOffset(value); + for (int i : offset) { + redisTemplate.opsForValue().setBit(key, i, true); + } + } + + /** + * 根据给定的布隆过滤器判断值是否存在 + */ + public boolean includeByBloomFilter(BloomFilterHelper bloomFilterHelper, String key, T value) { + Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空"); + int[] offset = bloomFilterHelper.murmurHashOffset(value); + for (int i : offset) { + if (!redisTemplate.opsForValue().getBit(key, i)) { + return false; + } + } + + return true; + } +} +``` + + + +#### 基于RedisBloom模块 + +RedisBloom模块提供了四种数据类型: + +- **Bloom Filter (布隆过滤器)** +- **Cuckoo Filter(布谷鸟过滤器)** +- **Count-Mins-Sketch** +- **Top-K** + +`Bloom Filter` 和 `Cuckoo` 用于确定(以给定的确定性)集合中是否存在某项。使用 `Count-Min Sketch` 来估算子线性空间中的项目数,使用 `Top-K` 维护K个最频繁项目的列表。 + +```shell +# 1.git 下载 +[root@test ~]# git clone https://github.com/RedisBloom/RedisBloom.git +[root@test ~]# cd redisbloom +[root@test ~]# make + +# 2.wget 下载 +[root@test ~]# wget https://github.com/RedisBloom/RedisBloom/archive/v2.0.3.tar.gz +[root@test ~]# tar -zxvf RedisBloom-2.0.3.tar.gz +[root@test ~]# cd RedisBloom-2.0.3/ +[root@test ~]# make + +# 3.修改Redis Conf +[root@test ~]#vim /etc/redis.conf +# 在文件中添加下行 +loadmodule /root/RedisBloom-2.0.3/redisbloom.so + +# 4.启动Redis server +[root@test ~]# /redis-server /etc/redis.conf +# 或者启动服务时加载os文件 +[root@test ~]# /redis-server /etc/redis.conf --loadmodule /root/RedisBloom/redisbloom.so + +# 5.测试RedisBloom +[root@test ~]# redis-cli +127.0.0.1:6379> bf.add bloomFilter foo +127.0.0.1:6379> bf.exists bloomFilter foo +127.0.0.1:6379> cf.add cuckooFilter foo +127.0.0.1:6379> cf.exists cuckooFilter foo +``` \ No newline at end of file diff --git a/src/Middleware/105.md b/src/Middleware/105.md new file mode 100644 index 0000000..a0d26f3 --- /dev/null +++ b/src/Middleware/105.md @@ -0,0 +1,90 @@ +当然是为了追求速度,不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑,底层数据结构有 6 种。 + +![Redis数据类型与底层数据结构关系](images/Middleware/Redis数据类型与底层数据结构关系.png) + + + +### SDS(简单动态字符) + +![img](images/Middleware/SDS简单动态字符.png) + +C 语言字符串结构与 SDS 字符串结构对比图如上所示: + +- SDS 中 len 保存这字符串的长度,O(1) 时间复杂度查询字符串长度信息 +- 空间预分配:SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间 +- 惰性空间释放:当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配 + + + +### hash表(字典) + +![img](images/Middleware/Redis全局hash字典.png) + +Redis 整体就是一个 哈希表来保存所有的键值对,无论数据类型是 5 种的任意一种。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。 + +整个数据库就是一个全局哈希表,而哈希表的时间复杂度是 O(1),只需要计算每个键的哈希值,便知道对应的哈希桶位置,定位桶里面的 entry 找到对应数据,这个也是 Redis 快的原因之一。 + + + +**那 Hash 冲突怎么办?** + +当写入 Redis 的数据越来越多的时候,哈希冲突不可避免,会出现不同的 key 计算出一样的哈希值。Redis 通过链式哈希解决冲突:也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能,所以 Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。开始默认使用 hash 表 1 保存键值对数据,哈希表 2 此刻没有分配空间。 + + + +### linkedList(双端列表) + +![quicklist](images/Middleware/quicklist.png) + +后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。这也是为何 Redis 快的原因,不放过任何一个可以提升性能的细节。 + + + +### zipList(压缩列表) + +![ZipList压缩列表](images/Middleware/ZipList压缩列表.png) + +压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一。当一个列表只有少量数据的时候,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现。 + +ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构,ziplist 中可以包含多个 entry 节点,每个节点可以存放整数或者字符串。ziplist 在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。 + +```go +struct ziplist { + int32 zlbytes; // 整个压缩列表占用字节数 + int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 + int16 zllength; // 元素个数 + T[] entries; // 元素内容列表,挨个挨个紧凑存储 + int8 zlend; // 标志压缩列表的结束,值恒为 0xFF +} +``` + +如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N)。 + + + +### skipList(跳跃表) + +![skipList跳跃表](images/Middleware/skipList跳跃表.png) + +sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 + +跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位。 + + + +### intset(整数数组) + +当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现,节省内存。结构如下: + +```go +typedef struct intset{ + //编码方式 + uint32_t encoding; + //集合包含的元素数量 + uint32_t length; + //保存元素的数组 + int8_t contents[]; +}intset; +``` + +contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度。 \ No newline at end of file diff --git a/src/Middleware/106.md b/src/Middleware/106.md new file mode 100644 index 0000000..7dfac75 --- /dev/null +++ b/src/Middleware/106.md @@ -0,0 +1,240 @@ +通常来说,应该同时使用两种持久化方案,以保证数据安全: + +- 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化 +- 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可 +- 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启 +- 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡 + +当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整 + + + +### RDB模式(内存快照) + +`RDB`(Redis Database Backup File,**Redis数据备份文件**)持久化方式:是指用数据集快照的方式半持久化模式记录 Redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。 + +**优点** + +- RDB快照是一个压缩过的非常紧凑的文件。保存着某个时间点的数据集,适合做数据的备份,灾难恢复 +- 可最大化Redis的的性能。在保存RDB文件,服务器进程只需要fork一个子进程来完成RDB文件创建,父进程不需要做IO操作 +- 与AOF相比,恢复大数据集的时候会更快 + +**缺点** + +- RDB的数据安全性是不如AOF的,保存整个数据集的过程是比繁重的,根据配置可能要几分钟才快照一次,如果服务器宕机,那么就可能丢失几分钟的数据 +- Redis数据集较大时,fork的子进程要完成快照会比较耗CPU、耗时 + + + +**① 创建** + +当 Redis 持久化时,程序会将当前内存中的**数据库状态**保存到磁盘中。创建 RDB 文件主要有两个 Redis 命令:`SAVE` 和 `BGSAVE`。 + +![Redis-RDB-创建](images/Middleware/Redis-RDB-创建.png) + + + +**② 载入** + +服务器在载入 RDB 文件期间,会一直处于**阻塞**状态,直到载入工作完成为止。 + +![Redis-RDB-载入](images/Middleware/Redis-RDB-载入.png) + + + +#### save同步保存 + +`save` 命令是同步操作,执行命令时,会 **阻塞** Redis 服务器进程,拒绝客户端发送的命令请求。 + +具体流程如下: + +![Redis-RDB-Save命令](images/Middleware/Redis-RDB-Save命令.png) + +由于 `save` 命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,`save`命令执行速度会非常慢,阻塞所有客户端的请求。因此很少在生产环境直接使用SAVE 命令,可以使用BGSAVE 命令代替。如果在BGSAVE命令的保存数据的子进程发生错误的时,用 SAVE命令保存最新的数据是最后的手段。 + + + +#### bgsave异步保存 + +`bgsave` 命令是异步操作,执行命令时,子进程执行保存工作,服务器还可以继续让主线程**处理**客户端发送的命令请求。 + +具体流程如下: + +![Redis-RDB-BgSave命令](images/Middleware/Redis-RDB-BgSave命令.png) + +Redis使用Linux系统的`fock()`生成一个子进程来将DB数据保存到磁盘,主进程继续提供服务以供客户端调用。如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。 + + + +#### 自动保存 + +可通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作: + +```shell +# RDB自动持久化规则 +# 当 900 秒内有至少有 1 个键被改动时,自动进行数据集保存操作 +save 900 1 +# 当 300 秒内有至少有 10 个键被改动时,自动进行数据集保存操作 +save 300 10 +# 当 60 秒内有至少有 10000 个键被改动时,自动进行数据集保存操作 +save 60 10000 + +# RDB持久化文件名 +dbfilename dump-.rdb + +# 数据持久化文件存储目录 +dir /var/lib/redis + +# bgsave发生错误时是否停止写入,通常为yes +stop-writes-on-bgsave-error yes + +# rdb文件是否使用压缩格式 +rdbcompression yes + +# 是否对rdb文件进行校验和检验,通常为yes +rdbchecksum yes +``` + + + +#### 默认配置 + +RDB 文件默认的配置如下: + +```properties +################################ SNAPSHOTTING ################################ +# +# Save the DB on disk: +# 在给定的秒数和给定的对数据库的写操作数下,自动持久化操作。 +# save +# +save 900 1 +save 300 10 +save 60 10000 +#bgsave发生错误时是否停止写入,一般为yes +stop-writes-on-bgsave-error yes +#持久化时是否使用LZF压缩字符串对象? +rdbcompression yes +#是否对rdb文件进行校验和检验,通常为yes +rdbchecksum yes +# RDB持久化文件名 +dbfilename dump.rdb +#持久化文件存储目录 +dir ./ +``` + + + +### AOF模式(日志追加) + +AOF(Append Only File,**追加日志文件**)持久化方式:是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储保存为 aof 文件。Redis 是先执行命令,把数据写入内存,然后才记录日志。因为该模式是**只追加**的方式,所以没有任何磁盘寻址的开销,所以很快,有点像 Mysql 中的binlog,AOF更适合做热备。 + +![Redis-AOF](images/Middleware/Redis-AOF.png) + +**优点** + +- 数据更完整,安全性更高,秒级数据丢失(取决fsync策略,如果是everysec,最多丢失1秒的数据) +- AOF文件是一个只进行追加的日志文件,且写入操作是以Redis协议的格式保存的,内容是可读的,适合误删紧急恢复 + +**缺点** + +- 对于相同的数据集,AOF文件的体积要大于RDB文件,数据恢复也会比较慢 +- 根据所使用的fsync策略,AOF的速度可能会慢于RDB。 不过在一般情况下,每秒fsync的性能依然非常高 + + + +#### 持久化流程 + +![Redis-AOF持久化流程](images/Middleware/Redis-AOF持久化流程.png) + +**① 命令追加** + +若 AOF 持久化功能处于打开状态,服务器在执行完一个命令后,会以协议格式将被执行的写命令**追加**到服务器状态的 `aof_buf` 缓冲区的末尾。 + + + +**② 文件同步** + +服务器每次结束一个事件循环之前,都会调用 `flushAppendOnlyFile` 函数,这个函数会考虑是否需要将 `aof_buf` 缓冲区中的内容**写入和保存**到 AOF 文件里。`flushAppendOnlyFile` 函数执行以下流程: + +- WRITE:根据条件,将 aof_buf 中的**缓存**写入到 AOF 文件; +- SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到**磁盘**中。 + +这个函数是由服务器配置的 `appendfsync` 的三个值:`always、everysec、no` 来影响的,也被称为三种策略: + +- **always**:每条**命令**都会 fsync 到硬盘中,这样 redis 的写入数据就不会丢失。 + + ![Redis-AOF-Always](images/Middleware/Redis-AOF-Always.png) + +- **everysec**:**每秒**都会刷新缓冲区到硬盘中(默认值)。 + + ![Redis-AOF-Everysec](images/Middleware/Redis-AOF-Everysec.png) + +- **no**:根据当前**操作系统**的规则决定什么时候刷新到硬盘中,不需要我们来考虑。 + + ![Redis-AOF-No](images/Middleware/Redis-AOF-No.png) + + + +**数据加载** + +- 创建一个不带网络连接的伪客户端 +- 从 AOF 文件中分析并读取出一条写命令 +- 使用伪客户端执行被读出的写命令 +- 一直执行步骤 2 和 3,直到 AOF 文件中的所有写命令都被**处理完毕**为止 + + + +#### 文件重写 + +**为何需要文件重写** + +- 为了解决 AOF 文件**体积膨胀**的问题 +- 通过重写创建一个新的 AOF 文件来替代现有的 AOF 文件,新的 AOF 文件不会包含任何浪费空间的冗余命令 + + + +**文件重写的实现原理** + +- 不需要对现有的 AOF 文件进行任何操作 +- 从数据库中直接读取键现在的值 +- 用一条命令记录键值对,从而**代替**之前记录这个键值对的多条命令 + + + +**后台重写** + +为不阻塞父进程,Redis将AOF重写程序放到**子进程**里执行。在子进程执行AOF重写期间,服务器进程需要执行三个流程: + +- 执行客户端发来的命令 +- 将执行后的写命令追加到 AOF 缓冲区 +- 将执行后的写命令追加到 AOF 重写缓冲区 + +![Redis-AOF-后台重写](images/Middleware/Redis-AOF-后台重写.png) + + + +#### 默认配置 + +AOF 文件默认的配置如下: + +```properties +############################## APPEND ONLY MODE ############################### +#开启AOF持久化方式 +appendonly no +#AOF持久化文件名 +appendfilename "appendonly.aof" +#每秒把缓冲区的数据fsync到磁盘 +appendfsync everysec +# appendfsync no +#是否在执行重写时不同步数据到AOF文件 +no-appendfsync-on-rewrite no +# 触发AOF文件执行重写的增长率 +auto-aof-rewrite-percentage 100 +#触发AOF文件执行重写的最小size +auto-aof-rewrite-min-size 64mb +#redis在恢复时,会忽略最后一条可能存在问题的指令 +aof-load-truncated yes +#是否打开混合开关 +aof-use-rdb-preamble yes +``` \ No newline at end of file diff --git a/src/Middleware/107.md b/src/Middleware/107.md new file mode 100644 index 0000000..ac16cf7 --- /dev/null +++ b/src/Middleware/107.md @@ -0,0 +1,17 @@ +**过期策略用于处理过期缓存数据**。Redis采用过期策略:`惰性删除` + `定期删除`。memcached采用过期策略:`惰性删除`。 + +### 定时过期 + +每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是**会占用大量的CPU资源去处理过期的数据**,从而影响缓存的响应时间和吞吐量。 + + + +### 惰性过期 + +只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却**对内存非常不友好**。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。 + + + +### 定期过期 + +每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源**达到最优**的平衡效果。expires字典会保存所有设置了过期时间的key的过期时间数据,其中 key 是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。 \ No newline at end of file diff --git a/src/Middleware/108.md b/src/Middleware/108.md new file mode 100644 index 0000000..6a21b1c --- /dev/null +++ b/src/Middleware/108.md @@ -0,0 +1,40 @@ +Redis淘汰机制的存在是为了更好的使用内存,用一定的缓存丢失来换取内存的使用效率。当Redis内存快耗尽时,Redis会启动内存淘汰机制,将部分key清掉以腾出内存。当达到内存使用上限超过`maxmemory`时,可在配置文件`redis.conf`中指定 `maxmemory-policy` 的清理缓存方式。 + +```properties +# 配置最大内存限制 +maxmemory 1000mb +# 配置淘汰策略 +maxmemory-policy volatile-lru +``` + +### LRU(最近最少使用) + +- `volatile-lru`:从已设置过期时间的key中,挑选**最近最少使用(最长时间没有使用)**的key进行淘汰 +- `allkeys-lru`:从所有key中,挑选**最近最少使用**的数据淘汰 + + + +### LFU(最近最不经常使用) + +- `volatile-lfu`:从已设置过期时间的key中,挑选**最近最不经常使用(使用次数最少)**的key进行淘汰 +- `allkeys-lfu`:从所有key中,选择某段时间内内**最近最不经常使用**的数据淘汰 + + + +### Random(随机淘汰) + +- `volatile-random`:从已设置过期时间的key中,**任意选择**数据淘汰 +- `allkeys-random`:从所有key中,**任意选择数**据淘汰 + + + +### TTL(过期时间) + +- `volatile-ttl`:从已设置过期时间的key中,挑选**将要过期**的数据淘汰 +- `allkeys-random`:从所有key中,**任意选择数**据淘汰 + + + +### No-Enviction(驱逐) + +- `noenviction(驱逐)`:当达到最大内存时直接返回错误,不覆盖或逐出任何数据 \ No newline at end of file diff --git a/src/Middleware/109.md b/src/Middleware/109.md new file mode 100644 index 0000000..7040387 --- /dev/null +++ b/src/Middleware/109.md @@ -0,0 +1,47 @@ +### 单节点(Single) + +**优点** + +- 架构简单,部署方便 +- 高性价比:缓存使用时无需备用节点(单实例可用性可以用 supervisor 或 crontab 保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务 +- 高性能 + +**缺点** + +- 不保证数据的可靠性 +- 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务 +- 高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序/计算较少场景 + + + +### 主从复制(Replication) + +**基本原理** + +主从复制模式中包含一个主数据库实例(Master)与一个或多个从数据库实例(Slave),如下图: + +![Redis主从复制模式(Replication)](images/Middleware/Redis主从复制模式(Replication).png) + +![Redis主从复制模式(Replication)优缺点](images/Middleware/Redis主从复制模式(Replication)优缺点.png) + + + +### 哨兵(Sentinel) + +Sentinel主要作用如下: + +- **监控**:Sentinel 会不断的检查主服务器和从服务器是否正常运行 +- **通知**:当被监控的某个Redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知 +- **自动故障转移**:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点 + +![Redis哨兵模式(Sentinel)](images/Middleware/Redis哨兵模式(Sentinel).png) + +![Redis哨兵模式(Sentinel)优缺点](images/Middleware/Redis哨兵模式(Sentinel)优缺点.png) + + + +### 集群(Cluster) + +![Redis集群模式(Cluster)](images/Middleware/Redis集群模式(Cluster).png) + +![Redis集群模式(Cluster)优缺点](images/Middleware/Redis集群模式(Cluster)优缺点.png) \ No newline at end of file diff --git a/src/Middleware/110.md b/src/Middleware/110.md new file mode 100644 index 0000000..1061ad8 --- /dev/null +++ b/src/Middleware/110.md @@ -0,0 +1,253 @@ +**Redis安装及配置** + +Redis的安装十分简单,打开redis的官网 [http://redis.io](http://redis.io/) 。 + +- 下载一个最新版本的安装包,如 redis-version.tar.gz +- 解压 `tar zxvf redis-version.tar.gz` +- 执行 make (执行此命令可能会报错,例如确实gcc,一个个解决即可) + +如果是 mac 电脑,安装redis将十分简单执行`brew install redis`即可。安装好redis之后,我们先不慌使用,先进行一些配置。打开`redis.conf`文件,我们主要关注以下配置: + +```shell +port 6379 # 指定端口为 6379,也可自行修改 +daemonize yes # 指定后台运行 +``` + + + +### 单节点(Single) + +安装好redis之后,我们来运行一下。启动redis的命令为 : + +```shell +$ /bin/redis-server path/to/redis.config +``` + +假设我们没有配置后台运行(即:daemonize no),那么我们会看到如下启动日志: + +```shell +93825:C 20 Jan 2019 11:43:22.640 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo +93825:C 20 Jan 2019 11:43:22.640 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=93825, just started +93825:C 20 Jan 2019 11:43:22.640 # Configuration loaded +93825:S 20 Jan 2019 11:43:22.641 * Increased maximum number of open files to 10032 (it was originally set to 256). + _._ + _.-``__ ''-._ + _.-`` `. `_. ''-._ Redis 5.0.3 (00000000/0) 64 bit + .-`` .-```. ```\/ _.,_ ''-._ + ( ' , .-` | `, ) Running in standalone mode + |`-._`-...-` __...-.``-._|'` _.-'| Port: 6380 + | `-._ `._ / _.-' | PID: 93825 + `-._ `-._ `-./ _.-' _.-' + |`-._`-._ `-.__.-' _.-'_.-'| + | `-._`-._ _.-'_.-' | http://redis.io + `-._ `-._`-.__.-'_.-' _.-' + |`-._`-._ `-.__.-' _.-'_.-'| + | `-._`-._ _.-'_.-' | + `-._ `-._`-.__.-'_.-' _.-' + `-._ `-.__.-' _.-' + `-._ _.-' + `-.__.-' +``` + + + +### 主从复制(Replication) + +Redis主从配置非常简单,过程如下(演示情况下主从配置在一台电脑上): + +**第一步**:复制两个redis配置文件(启动两个redis,只需要一份redis程序,两个不同的redis配置文件即可) + +```shell +mkdir redis-master-slave +cp path/to/redis/conf/redis.conf path/to/redis-master-slave master.conf +cp path/to/redis/conf/redis.conf path/to/redis-master-slave slave.conf +``` + +**第二步**:修改配置 + +```shell +## master.conf +port 6379 + +## master.conf +port 6380 +slaveof 127.0.0.1 6379 +``` + +**第三步**:分别启动两个redis + +```shell +redis-server path/to/redis-master-slave/master.conf +redis-server path/to/redis-master-slave/slave.conf +``` + +启动之后,打开两个命令行窗口,分别执行 `telnet localhost 6379` 和 `telnet localhost 6380`,然后分别在两个窗口中执行 `info` 命令,可以看到: + +```shell +# Replication +role:master + +# Replication +role:slave +master_host:127.0.0.1 +master_port:6379 +``` + +主从配置没问题。然后在master 窗口执行 set 之后,到slave窗口执行get,可以get到,说明主从同步成功。这时,我们如果在slave窗口执行 set ,会报错: + +```shell +-READONLY You can't write against a read only replica. +``` + +因为从节点是只读的。 + + + +### 哨兵(Sentinel) + +Sentinel是用来监控主从节点的健康情况。客户端连接Redis主从的时候,先连接Sentinel,Sentinel会告诉客户端主Redis的地址是多少,然后客户端连接上Redis并进行后续的操作。当主节点挂掉的时候,客户端就得不到连接了因而报错了,客户端重新向Sentinel询问主master的地址,然后客户端得到了[新选举出来的主Redis],然后又可以愉快的操作了。 + + + +**哨兵sentinel配置** + +为了说明sentinel的用处,我们做个试验。配置3个redis(1主2从),1个哨兵。步骤如下: + +```shell +mkdir redis-sentinel +cd redis-sentinel +cp redis/path/conf/redis.conf path/to/redis-sentinel/redis01.conf +cp redis/path/conf/redis.conf path/to/redis-sentinel/redis02.conf +cp redis/path/conf/redis.conf path/to/redis-sentinel/redis03.conf +touch sentinel.conf +``` + +上我们创建了 3个redis配置文件,1个哨兵配置文件。我们将 redis01设置为master,将redis02,redis03设置为slave。 + +```shell +vim redis01.conf +port 63791 + +vim redis02.conf +port 63792 +slaveof 127.0.0.1 63791 + +vim redis03.conf +port 63793 +slaveof 127.0.0.1 63791 + +vim sentinel.conf +daemonize yes +port 26379 +sentinel monitor mymaster 127.0.0.1 63793 1 # 下面解释含义 +``` + +上面的主从配置都熟悉,只有哨兵配置 sentinel.conf,需要解释一下: + +```shell +mymaster # 为主节点名字,可以随便取,后面程序里边连接的时候要用到 +127.0.0.1 63793 # 为主节点的 ip,port +1 # 后面的数字 1 表示选举主节点的时候,投票数。1表示有一个sentinel同意即可升级为master +``` + + + +**启动哨兵** + +上面我们配置好了redis主从,1主2从,以及1个哨兵。下面我们分别启动redis,并启动哨兵: + +```shell +redis-server path/to/redis-sentinel/redis01.conf +redis-server path/to/redis-sentinel/redis02.conf +redis-server path/to/redis-sentinel/redis03.conf + +redis-server path/to/redis-sentinel/sentinel.conf --sentinel +``` + +启动之后,可以分别连接到 3个redis上,执行info查看主从信息。 + + + +**模拟主节点宕机情况** + +运行上面的程序(**注意,在实验这个效果的时候,可以将sleep时间加长或者for循环增多,以防程序提前停止,不便看整体效果**),然后将主redis关掉,模拟redis挂掉的情况。现在主redis为redis01,端口为63791 + +```shell +redis-cli -p 63791 shutdown +``` + + + +### 集群(Cluster) + +上述所做的这些工作只是保证了数据备份以及高可用,目前为止我们的程序一直都是向1台redis写数据,其他的redis只是备份而已。实际场景中,单个redis节点可能不满足要求,因为: + +- 单个redis并发有限 +- 单个redis接收所有数据,最终回导致内存太大,内存太大回导致rdb文件过大,从很大的rdb文件中同步恢复数据会很慢 + +所以需要redis cluster 即redis集群。Redis 集群是一个提供在**多个Redis间节点间共享数据**的程序集。Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。Redis 集群通过分区来提供**一定程度的可用性**,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令.。Redis 集群的优势: + +- 自动分割数据到不同的节点上 +- 整个集群的部分节点失败或者不可达的情况下能够继续处理命令 + +为了配置一个redis cluster,我们需要准备至少6台redis,为啥至少6台呢?我们可以在redis的官方文档中找到如下一句话: + +> Note that the minimal cluster that works as expected requires to contain at least three master nodes. + +因为最小的redis集群,需要至少3个主节点,既然有3个主节点,而一个主节点搭配至少一个从节点,因此至少得6台redis。然而对我来说,就是复制6个redis配置文件。本实验的redis集群搭建依然在一台电脑上模拟。 + + + +**配置 redis cluster 集群** + +上面提到,配置redis集群需要至少6个redis节点。因此我们需要准备及配置的节点如下: + +```shell +# 主:redis01 从 redis02 slaveof redis01 +# 主:redis03 从 redis04 slaveof redis03 +# 主:redis05 从 redis06 slaveof redis05 +mkdir redis-cluster +cd redis-cluster +mkdir redis01 到 redis06 6个文件夹 +cp redis.conf 到 redis01 ... redis06 +# 修改端口, 分别配置3组主从关系 +``` + + + +**启动redis集群** + +上面的配置完成之后,分别启动6个redis实例。配置正确的情况下,都可以启动成功。然后运行如下命令创建集群: + +```shell +redis-5.0.3/src/redis-cli --cluster create 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 --cluster-replicas 1 +``` + +**注意**,这里使用的是ip:port,而不是 domain:port ,因为我在使用 localhost:6371 之类的写法执行的时候碰到错误: + +```shell +ERR Invalid node address specified: localhost:6371 +``` + +执行成功之后,连接一台redis,执行 cluster info 会看到类似如下信息: + +```shell +cluster_state:ok +cluster_slots_assigned:16384 +cluster_slots_ok:16384 +cluster_slots_pfail:0 +cluster_slots_fail:0 +cluster_known_nodes:6 +cluster_size:3 +cluster_current_epoch:6 +cluster_my_epoch:1 +cluster_stats_messages_ping_sent:1515 +cluster_stats_messages_pong_sent:1506 +cluster_stats_messages_sent:3021 +cluster_stats_messages_ping_received:1501 +cluster_stats_messages_pong_received:1515 +cluster_stats_messages_meet_received:5 +cluster_stats_messages_received:3021 +``` + +我们可以看到`cluster_state:ok`,`cluster_slots_ok:16384`,`cluster_size:3`。 \ No newline at end of file diff --git a/src/Middleware/1101.md b/src/Middleware/1101.md new file mode 100644 index 0000000..d238430 --- /dev/null +++ b/src/Middleware/1101.md @@ -0,0 +1,20 @@ +Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。 + + + +**Spring的优点?** + +- spring属于低侵入式设计,代码的污染极低 +- spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性 +- Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用 +- spring对于主流的应用框架提供了集成支持 + + **使用Spring框架的好处是什么?** + +- **轻量:**Spring 是轻量的,基本的版本大约2MB +- **控制反转:**Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。 +- **面向切面的编程(AOP):**Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开 +- **容器:**Spring 包含并管理应用中对象的生命周期和配置 +- **MVC框架**:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品 +- **事务管理:**Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA) +- **异常处理:**Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常 \ No newline at end of file diff --git a/src/Middleware/1102.md b/src/Middleware/1102.md new file mode 100644 index 0000000..af642c2 --- /dev/null +++ b/src/Middleware/1102.md @@ -0,0 +1,30 @@ +![Spring关系](images/Middleware/Spring关系.jpg) + +### Spring + +Spring是一个开源容器框架,可以接管web层,业务层,dao层,持久层的组件,并且可以配置各种bean,和维护bean与bean之间的关系。其核心就是控制反转(IOC),和面向切面(AOP),简单的说就是一个分层的轻量级开源框架。 + + + +### SpringMVC + +Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。SpringMVC是一种web层mvc框架,用于替代servlet(处理|响应请求,获取表单参数,表单校验等。SpringMVC是一个MVC的开源框架,SpringMVC = struts2 + spring,springMVC就相当于是Struts2加上Spring的整合。 + +![SpringMVC工作原理](images/Middleware/SpringMVC工作原理.png) + +SpringMVC是属于SpringWeb里面的一个功能模块(SpringWebMVC)。专门用来开发SpringWeb项目的一种MVC模式的技术框架实现。 + + + +### SpringBoot + +Springboot是一个微服务框架,延续了spring框架的核心思想IOC和AOP,简化了应用的开发和部署。Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。提供了一堆依赖打包,并已经按照使用习惯解决了依赖问题—>习惯大于约定。 + +Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。Spring Boot中的一些特点: + +- 创建独立的spring应用 +- 嵌入Tomcat, JettyUndertow 而且不需要部署他们 +- 提供的“starters” poms来简化Maven配置 +- 尽可能自动配置spring应用 +- 提供生产指标,健壮检查和外部化配置 +- 绝对没有代码生成和XML配置要求 \ No newline at end of file diff --git a/src/Middleware/1103.md b/src/Middleware/1103.md new file mode 100644 index 0000000..0d8aaad --- /dev/null +++ b/src/Middleware/1103.md @@ -0,0 +1,31 @@ +### 核心组件 + +![Spring核心组件](images/Middleware/Spring核心组件.png) + + + +### Spring常用模块 + +![Spring常用模块](images/Middleware/Spring常用模块.png) + +主要包括以下七个模块: + +- **Spring Context**:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等) +- **Spring Core**:核心类库,所有功能都依赖于该类库,提供IOC和DI服务 +- **Spring AOP**:AOP服务 +- **Spring Web**:提供了基本的面向Web的综合特性,提供对常见框架如Struts2的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器 +- **Spring MVC**:提供面向Web应用的Model-View-Controller,即MVC实现 +- **Spring DAO**:对JDBC的抽象封装,简化了数据访问异常的处理,并能统一管理JDBC事务 +- **Spring ORM**:对现有的ORM框架的支持 + + + +### Spring主要包 + +![Spring主要包](images/Middleware/Spring主要包.png) + + + +### Spring常用注解 + +![Spring常用注解](images/Middleware/Spring常用注解.png) \ No newline at end of file diff --git a/src/Middleware/1104.md b/src/Middleware/1104.md new file mode 100644 index 0000000..769a7f3 --- /dev/null +++ b/src/Middleware/1104.md @@ -0,0 +1,5 @@ +IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。 + +最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。 + +Spring的IOC有三种注入方式 :**构造器注入、setter方法注入、根据注解注入**。 \ No newline at end of file diff --git a/src/Middleware/1105.md b/src/Middleware/1105.md new file mode 100644 index 0000000..8e42ed8 --- /dev/null +++ b/src/Middleware/1105.md @@ -0,0 +1,48 @@ +AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。 + + + +### 静态代理 + +AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。 + +### 动态代理 + +Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: + +- **JDK动态代理**:只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。 + + **InvocationHandler的invoke(Object proxy,Method method,Object[] args)**: + + - **proxy**:是最终生成的代理对象 + - **method**:是被代理目标实例的某个具体方法; + - **args**:是被代理目标实例某个方法的具体入参, 在方法反射调用时使用 + +- **CGLIB动态代理**:如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 + + + +**静态代理与动态代理区别?** + +生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。 + +IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。 + + + +### 应用场景 + +AOP 主要应用场景有: + +- Authentication 权限 +- Caching 缓存 +- Context passing 内容传递 +- Error handling 错误处理 +- Lazy loading 懒加载 +- Debugging 调试 +- logging, tracing, profiling and monitoring 记录跟踪 优化 校准 +- Performance optimization 性能优化 +- Persistence 持久化 +- Resource pooling 资源池 +- Synchronization 同步 +- Transactions 事务 \ No newline at end of file diff --git a/src/Middleware/1106.md b/src/Middleware/1106.md new file mode 100644 index 0000000..346b7f5 --- /dev/null +++ b/src/Middleware/1106.md @@ -0,0 +1,5 @@ +主要作用是过滤字符编码、做一些业务逻辑判断,主要用于对用户请求进行预处理,同时也可进行逻辑判断。Filter在请求进入servlet容器执行service()方法之前就会经过filter过滤,不像Intreceptor一样依赖于springmvc框架,只需要依赖于servlet。Filter启动是随WEB应用的启动而启动,只需要初始化一次,以后都可以进行拦截。Filter有如下几个种类: + +- 用户授权Filter:检查用户请求,根据请求过滤用户非法请求 +- 日志Filter:记录某些特殊的用户请求 +- 解码Filter:对非标准编码的请求解码 \ No newline at end of file diff --git a/src/Middleware/1107.md b/src/Middleware/1107.md new file mode 100644 index 0000000..f8978c8 --- /dev/null +++ b/src/Middleware/1107.md @@ -0,0 +1,30 @@ +主要作用是拦截用户请求,进行处理。比如判断用户登录情况、权限验证,只要针对Controller请求进行处理,是通过**HandlerInterceptor**。 + +Interceptor分两种情况,一种是对会话的拦截,实现spring的HandlerInterceptor接口并注册到mvc的拦截队列中,其中**preHandle()**方法在调用Handler之前进行拦截,**postHandle()**方法在视图渲染之前调用,**afterCompletion()**方法在返回相应之前执行;另一种是对方法的拦截,需要使用@Aspect注解,在每次调用指定方法的前、后进行拦截。 + + + +**Filter和Interceptor的区别** + +- Filter是基于函数回调(doFilter()方法)的,而Interceptor则是基于Java反射的(AOP思想) +- Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器 +- Filter对几乎所有的请求起作用,而Interceptor只能对action请求起作用 +- Interceptor可以访问Action的上下文,值栈里的对象,而Filter不能 +- 在action的生命周期里,Interceptor可以被多次调用,而Filter只能在容器初始化时调用一次 +- Filter在过滤是只能对request和response进行操作,而interceptor可以对request、response、handler、modelAndView、exception进行操作 + + + +### HandlerInterceptor + +HandlerInterceptor是springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor先执行。 + +实现一个HandlerInterceptor拦截器可以直接实现HandlerInterceptor接口,也可以继承HandlerInterceptorAdapter类。 + +这两种方法殊途同归,其实HandlerInterceptorAdapter也就是声明了HandlerInterceptor接口中所有方法的默认实现,而我们在继承他之后只需要重写必要的方法。 + + + +### MethodInterceptor + +MethodInterceptor是AOP项目中的拦截器,它拦截的目标是方法,即使不是controller中的方法。实现MethodInterceptor 拦截器大致也分为两种,一种是实现MethodInterceptor接口,另一种利用AspectJ的注解或配置。 \ No newline at end of file diff --git a/src/Middleware/1108.md b/src/Middleware/1108.md new file mode 100644 index 0000000..7df6744 --- /dev/null +++ b/src/Middleware/1108.md @@ -0,0 +1,65 @@ +### Spring容器启动流程 + +- 初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中 + + ① 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象 + ② 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成 BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等) + ③ 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象 + +- 将配置类的BeanDefinition注册到容器中 + +- 调用refresh()方法刷新容器 + + ① prepareRefresh()刷新前的预处理 + ② obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory + ③ prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件 + ④ postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置 + ⑤ invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器 + ⑥ registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能 + ⑦ initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析 + ⑧ initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到 + ⑨ onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑 + ⑩ registerListeners():注册监听器。将容器中所有ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件 + ⑪ finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象 + ⑫ finishRefresh():发布BeanFactory容器刷新完成事件 + + + +### SpringMVC流程 + +- 用户发送请求至前端控制器DispatcherServlet +- DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler +- 处理器映射器根据请求url找到具体的处理器Handler,生成处理器对象及处理器拦截器(如果有则生成),一并返回给DispatcherServlet +- DispatcherServlet 调用 HandlerAdapter处理器适配器,请求执行Handler +- HandlerAdapter 经过适配调用 具体处理器进行处理业务逻辑 +- Handler执行完成返回ModelAndView +- HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet +- DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析 +- ViewResolver解析后返回具体View +- DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中) +- DispatcherServlet响应用户 + +![SpringMVC流程](images/Middleware/SpringMVC流程.jpg) + +- 前端控制器 DispatcherServlet:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度 +- 处理器映射器 HandlerMapping:根据请求的URL来查找Handler +- 处理器适配器 HandlerAdapter:负责执行Handler +- 处理器 Handler:处理器,需要程序员开发 +- 视图解析器 ViewResolver:进行视图的解析,根据视图逻辑名将ModelAndView解析成真正的视图(view) +- 视图View:View是一个接口, 它的实现类支持不同的视图类型,如jsp,freemarker,pdf等等 + + + +### 非拦截器Http请求流程 + +用户的普通Http请求执行顺序: + +![用户的普通Http请求执行顺序](images/Middleware/用户的普通Http请求执行顺序.jpg) + + + +### 拦截器Http请求流程 + +过滤器、拦截器添加后的执行顺序: + +![过滤器、拦截器添加后的执行顺序](images/Middleware/过滤器、拦截器添加后的执行顺序.jpg) \ No newline at end of file diff --git a/src/Middleware/111.md b/src/Middleware/111.md new file mode 100644 index 0000000..1ec25f9 --- /dev/null +++ b/src/Middleware/111.md @@ -0,0 +1,101 @@ +### 分区(Partitioning) + +指在面临**单机**的**存储空间**瓶颈时,即将全部数据分散在多个Redis实例中,每个实例不需要关联,可以是完全独立的。 + + + +**使用方式** + +- 客户端处理 + 和传统的数据库分库分表一样,可以从**key**入手,先进行计算,找到对应数据存储的实例在进行操作。 + - **范围角度**,比如orderId:1~orderId:1000放入实例1,orderId:1001~orderId:2000放入实例2 + - **哈希计算**,就像我们的**hashmap**一样,用hash函数加上位运算或者取模,高级玩法还有一致性Hash等操作,找到对应的实例进行操作 +- 使用代理中间件 + 我们可以开发独立的代理中间件,屏蔽掉处理数据分片的逻辑,独立运行。当然Redis也有优秀的代理中间件,譬如Twemproxy,或者codis,可以结合场景选择是否使用 + + + +**缺点** + +- **无缘多key操作**,key都不一定在一个实例上,那么多key操作或者多key事务自然是不支持 +- **维护成本**,由于每个实例在物理和逻辑上,都属于单独的一个节点,缺乏统一管理 +- **灵活性有限**,范围分片还好,比如hash+MOD这种方式,如果想**动态**调整Redis实例的数量,就要考虑大量数据迁移 + + + +### 主从(Master-Slave) + +分区暂时能解决**单点**无法容纳的**数据量问题**,但是一个Key还是只在一个实例上。主从则将数据从**主节点**同步到**从节点**,然后可做**读写分离**,将读流量均摊在各个从节点,可靠性也能提高。**主从**(Master-Slave)也就是复制(Replication)方式。 + + + +**使用方式** + +- 作为主节点的Redis实例,并不要求配置任何参数,只需要正常启动 +- 作为从节点的实例,使用配置文件或命令方式`REPLICAOF 主节点Host 主节点port`即可完成主从配置 + + + +**缺点** + +- slave节点都是**只读**的,如果**写流量**大的场景,就有些力不从心 +- **故障转移**不友好,主节点挂掉后,写处理就无处安放,需要**手工**的设定新的主节点,如使用`REPLICAOF no one` 晋升为主节点,再梳理其他slave节点的新主配置,相对来说比较麻烦 + + + +### 哨兵(Sentinel) + +**主从**的手工故障转移,肯定让人很难接受,自然就出现了高可用方案-**哨兵**(Sentinel)。我们可以在主从架构不变的场景,直接加入**Redis Sentinel**,对节点进行**监控**,来完成自动的**故障发现**与**转移**。并且还能够充当**配置提供者**,提供主节点的信息,就算发生了故障转移,也能提供正确的地址。 + + + +**使用方式** + +**Sentinel**的最小配置,一行即可: + +```properties +sentinel monitor <主节点别名> <主节点host> <主节点端口> <票数> +``` + +只需要配置master即可,然后用``redis-sentinel <配置文件>`` 命令即可启用。哨兵数量建议在三个以上且为奇数。 + + + +**使用场景问题** + +- 故障转移期间短暂的不可用,但其实官网的例子也给出了`parallel-syncs`参数来指定并行的同步实例数量,以免全部实例都在同步出现整体不可用的情况,相对来说要比手工的故障转移更加方便 +- 分区逻辑需要自定义处理,虽然解决了主从下的高可用问题,但是Sentinel并没有提供分区解决方案,还需开发者考虑如何建设 +- 既然是还是主从,如果异常的写流量搞垮了主节点,那么自动的“故障转移”会不会变成自动“灾难传递”,即slave提升为Master之后挂掉,又进行提升又被挂掉 + + + +### 集群(Cluster) + +**Cluster**在分区管理上,使用了“**哈希槽**”(hash slot)这么一个概念,一共有**16384**个槽位,每个实例负责一部分**槽**,通过`CRC16(key)&16383`这样的公式,计算出来key所对应的槽位。 + + + +**使用方式** + +配置文件 + +```properties +cluster-enabled yes +cluster-config-file "redis-node.conf" +``` + +启动命令 + +```bash +redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \ +127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ +--cluster-replicas 1 +``` + + + +**存在问题** + +- 虽然是对分区良好支持,但也有一些分区的老问题。如如果不在同一个“槽”的数据,是没法使用类似mset的**多键操作** +- 在select命令页有提到, 集群模式下只能使用一个库,虽然平时一般也是这么用的,但是要了解一下 +- 运维上也要谨慎,俗话说得好,“**使用越简单底层越复杂**”,启动搭建是很方便,使用时面对带宽消耗,数据倾斜等等具体问题时,还需人工介入,或者研究合适的配置参数 \ No newline at end of file diff --git a/src/Middleware/112.md b/src/Middleware/112.md new file mode 100644 index 0000000..8009929 --- /dev/null +++ b/src/Middleware/112.md @@ -0,0 +1,17 @@ +**题目**:保证Redis 中的 20w 数据都是热点数据 说明是 被频繁访问的数据,并且要保证Redis的内存能够存放20w数据,要计算出Redis内存的大小。 + +- **保留热点数据:**对于保留 Redis 热点数据来说,我们可以使用 Redis 的内存淘汰策略来实现,可以使用**allkeys-lru淘汰策略,**该淘汰策略是从 Redis 的数据中挑选最近最少使用的数据删除,这样频繁被访问的数据就可以保留下来了 + +- **保证 Redis 只存20w的数据:**1个中文占2个字节,假如1条数据有100个中文,则1条数据占200字节,20w数据 乘以 200字节 等于 4000 字节(大概等于38M);所以要保证能存20w数据,Redis 需要38M的内存 + + + +**题目:MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?** + +限定 Redis 占用的内存,Redis 会根据自身数据淘汰策略,加载热数据到内存。所以,计算一下 20W 数据大约占用的内存,然后设置一下 Redis 内存限制即可。 + + + +**题目:假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?** + +使用 keys 指令可以扫出指定模式的 key 列表。对方接着追问:如果这个 Redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?这个时候你要回答 Redis 关键的一个特性:Redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞地提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。 \ No newline at end of file diff --git a/src/Middleware/1201.md b/src/Middleware/1201.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Middleware/1202.md b/src/Middleware/1202.md new file mode 100644 index 0000000..ee2f639 --- /dev/null +++ b/src/Middleware/1202.md @@ -0,0 +1,5 @@ +### HTTP请求处理流程 + +### 参数绑定 + +### 过滤器、拦截器、AOP执行顺序 \ No newline at end of file diff --git a/src/Middleware/1203.md b/src/Middleware/1203.md new file mode 100644 index 0000000..e3abcc1 --- /dev/null +++ b/src/Middleware/1203.md @@ -0,0 +1,3 @@ +### Bean的加载过程 + +### Bean实例化的过程 \ No newline at end of file diff --git a/src/Middleware/1204.md b/src/Middleware/1204.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Middleware/1205.md b/src/Middleware/1205.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Middleware/1301.md b/src/Middleware/1301.md new file mode 100644 index 0000000..fc2eaf1 --- /dev/null +++ b/src/Middleware/1301.md @@ -0,0 +1 @@ +![SpringCloud](images/Middleware/SpringCloud.png) \ No newline at end of file diff --git a/src/Middleware/1302.md b/src/Middleware/1302.md new file mode 100644 index 0000000..8a08b90 --- /dev/null +++ b/src/Middleware/1302.md @@ -0,0 +1,431 @@ +Netflix Eureka 是由 Netflix 开源的一款基于 REST 的服务发现组件,包括 Eureka Server 及 Eureka Client。 + +![Eurake介绍](images/Middleware/Eurake介绍.png) + +![Eurake](images/Middleware/Eurake.png) + +- **Eurake客户端**:负责将这个服务的信息注册到Eureka服务端中 +- **Eureka服务端**:相当于一个注册中心,里面有注册表,注册表中保存了各个服务所在的机器和端口号,可以通过Eureka服务端找到各个服务 + + + +### Eurka工作流程 + +Eureka的工作流程如下: + +1. **Eureka Server 启动成功,等待服务端注册**。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息 +2. Eureka Client 启动时根据配置的 Eureka Server 地址**去注册中心注册服务** +3. Eureka Client 会**每30s向 Eureka Server 发送一次心跳请求**,证明客户端服务正常 +4. 当Eureka Server **90s内没有收到Eureka Client的心跳,注册中心则认为该节点失效**,会注销该实例 +5. **单位时间内Eureka Server统计到有大量的Eureka Client没有上送心跳,则认为可能为网络异常**,进入自我保护机制,不再剔除没有上送心跳的客户端 +6. **当Eureka Client心跳请求恢复正常之后,Eureka Server自动退出自我保护模式** +7. **Eureka Client定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地** +8. 服务调用时,Eureka Client会先从本地缓存找调取的服务。若获取不到,先从注册中心刷新注册表,再同步到本地缓存 +9. Eureka Client获取到目标服务器信息,发起服务调用 +10. Eureka Client程序关闭时向Eureka Server发送取消请求,Eureka Server将实例从注册表中删除 + + + +### Eureka Server + +Eureka Server注册中心服务端主要对外提供了三个功能: + +- **服务注册** + 服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表。 + +- **提供注册表** + 服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表。 + +- **同步状态** + Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。 + + + +**自我保护机制** +默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 即会进入自我保护机制。Eureka Server 触发自我保护机制后,页面会出现提示: +![EurekaServer触发自我保护机制](images/Middleware/EurekaServer触发自我保护机制.png) + +Eureka Server进入自我保护机制,会出现以下几种情况: + +- **Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务** +- **Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)** +- **当网络稳定时,当前实例新的注册信息会被同步到其它节点中** + +Eureka自我保护机制是为了防止误杀服务而提供的一个机制: + +- **当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端** +- **当Eureka捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制** +- **当客户端心跳恢复时,Eureka会自动退出自我保护机制** + +如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开: + +```properties +eureka.server.enable-self-preservation=true +``` + + + +### Eureka Client + +Eureka Client注册中心客户端是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。 + +- **Register —— 服务注册** + 服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。 + +- **Renew —— 服务续约** + Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。服务续约的两个重要属性: + + ```properties + # 服务续约任务的调用间隔时间,默认为30秒 + eureka.instance.lease-renewal-interval-in-seconds=30 + # 服务失效的时间,默认为90秒。 + eureka.instance.lease-expiration-duration-in-seconds=90 + ``` + +- **Eviction —— 服务剔除** + 当Eureka Client和Eureka Server不再有心跳时,Eureka Server会将该服务实例从服务注册列表中删除,即服务剔除。 + +- **Cancel —— 服务下线** + Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容: + + ```java + DiscoveryManager.getInstance().shutdownComponent(); + ``` + +- **GetRegisty —— 获取注册列表信息** + Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。 + + 如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。获取服务是服务消费者的基础,所以必有两个重要参数需要注意: + + ```properties + # 启用服务消费者从注册中心拉取服务列表的功能 + eureka.client.fetch-registry=true + # 设置服务消费者从注册中心拉取服务列表的间隔 + eureka.client.registry-fetch-interval-seconds=30 + ``` + +- **Remote Call —— 远程调用** + 当Eureka Client从注册中心获取到服务提供者信息后,就可以通过Http请求调用对应的服务;服务提供者有多个时,Eureka Client客户端会通过Ribbon自动进行负载均衡。 + + + +### Eureka缓存机制 + +**Eureka Server数据存储** + +Eureka Server的数据存储层是双层的 ConcurrentHashMap: + +```java +private final ConcurrentHashMap>> registry= new ConcurrentHashMap>>(); +``` + +- **key**:为服务名,即 `spring.application.name`,也就是客户端实例注册的应用名 +- **subKey**:为 `instanceId`,也就是服务的唯一实例ID +- **Lease< InstanceInfo >**:Lease对象存储着这个实例的所有注册信息,包括 ip 、端口、属性等 + + + +**Eureka Server缓存机制** + +Eureka Server为了提供响应效率,提供了两层的缓存结构,将Eureka Client所需要的注册信息,直接存储在缓存结构中。 + +- **第一层缓存:readOnlyCacheMap,本质上是 ConcurrentHashMap** + + 依赖定时从 readWriteCacheMap 同步数据,默认时间为 30 秒。 + + readOnlyCacheMap: 是一个 CurrentHashMap 只读缓存,这个主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和 readWriteCacheMap 的值做对比,如果数据不一致,则以 readWriteCacheMap 的数据为准。 + +- **第二层缓存:readWriteCacheMap,本质上是 Guava 缓存** + + readWriteCacheMap:readWriteCacheMap 的数据主要同步于存储层。当获取缓存时判断缓存中是否没有数据,如果不存在此数据,则通过 CacheLoader 的 load 方法去加载,加载成功之后将数据放入缓存,同时返回数据。readWriteCacheMap缓存过期时间,默认为180秒,当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据。 + +Eureka Client 获取全量或者增量的数据时,会先从一级缓存中获取;如果一级缓存中不存在,再从二级缓存中获取;如果二级缓存也不存在,这时候先将存储层的数据同步到缓存中,再从缓存中获取。通过 Eureka Server 的二层缓存机制,可以非常有效地提升 Eureka Server 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。 + + + +**其它缓存设计** + +除过 Eureka Server 端存在缓存外,Eureka Client 也同样存在着缓存机制,Eureka Client 启动时会全量拉取服务列表,启动后每隔 30 秒从 Eureka Server 量获取服务列表信息,并保持在本地缓存中。Eureka Client 增量拉取失败,或者增量拉取之后对比 hashcode 发现不一致,就会执行全量拉取,这样避免了网络某时段分片带来的问题,同样会更新到本地缓存。同时对于服务调用,如果涉及到 ribbon 负载均衡,那么 ribbon 对于这个实例列表也有自己的缓存,这个缓存定时(默认30秒)从 Eureka Client 的缓存更新。这么多的缓存机制可能就会造成一些问题,一个服务启动后可能最长需要 90s 才能被其它服务感知到: + +1. 首先,Eureka Server 维护每 30s 更新的响应缓存 +2. Eureka Client 对已经获取到的注册信息也做了 30s 缓存 +3. 负载均衡组件 Ribbon 也有 30s 缓存 + +这三个缓存加起来,就有可能导致服务注册最长延迟 90s ,这个需要我们在特殊业务场景中注意其产生的影响。 + + + +### 常用配置 + +Eureka Server常用配置: + +```properties +#服务端开启自我保护模式,前面章节有介绍 +eureka.server.enable-self-preservation=true +#扫描失效服务的间隔时间(单位毫秒,默认是60*1000)即60秒 +eureka.server.eviction-interval-timer-in-ms= 60000 +#间隔多长时间,清除过期的 delta 数据 +eureka.server.delta-retention-timer-interval-in-ms=0 +#请求频率限制器 +eureka.server.rate-limiter-burst-size=10 +#是否开启请求频率限制器 +eureka.server.rate-limiter-enabled=false +#请求频率的平均值 +eureka.server.rate-limiter-full-fetch-average-rate=100 +#是否对标准的client进行频率请求限制。如果是false,则只对非标准client进行限制 +eureka.server.rate-limiter-throttle-standard-clients=false +#注册服务、拉去服务列表数据的请求频率的平均值 +eureka.server.rate-limiter-registry-fetch-average-rate=500 +#设置信任的client list +eureka.server.rate-limiter-privileged-clients= +#在设置的时间范围类,期望与client续约的百分比。 +eureka.server.renewal-percent-threshold=0.85 +#多长时间更新续约的阈值 +eureka.server.renewal-threshold-update-interval-ms=0 +#对于缓存的注册数据,多长时间过期 +eureka.server.response-cache-auto-expiration-in-seconds=180 +#多长时间更新一次缓存中的服务注册数据 +eureka.server.response-cache-update-interval-ms=0 +#缓存增量数据的时间,以便在检索的时候不丢失信息 +eureka.server.retention-time-in-m-s-in-delta-queue=0 +#当时间戳不一致的时候,是否进行同步 +eureka.server.sync-when-timestamp-differs=true +#是否采用只读缓存策略,只读策略对于缓存的数据不会过期。 +eureka.server.use-read-only-response-cache=true + + +################server node 与 node 之间关联的配置#####################33 +#发送复制数据是否在request中,总是压缩 +eureka.server.enable-replicated-request-compression=false +#指示群集节点之间的复制是否应批处理以提高网络效率。 +eureka.server.batch-replication=false +#允许备份到备份池的最大复制事件数量。而这个备份池负责除状态更新的其他事件。可以根据内存大小,超时和复制流量,来设置此值得大小 +eureka.server.max-elements-in-peer-replication-pool=10000 +#允许备份到状态备份池的最大复制事件数量 +eureka.server.max-elements-in-status-replication-pool=10000 +#多个服务中心相互同步信息线程的最大空闲时间 +eureka.server.max-idle-thread-age-in-minutes-for-peer-replication=15 +#状态同步线程的最大空闲时间 +eureka.server.max-idle-thread-in-minutes-age-for-status-replication=15 +#服务注册中心各个instance相互复制数据的最大线程数量 +eureka.server.max-threads-for-peer-replication=20 +#服务注册中心各个instance相互复制状态数据的最大线程数量 +eureka.server.max-threads-for-status-replication=1 +#instance之间复制数据的通信时长 +eureka.server.max-time-for-replication=30000 +#正常的对等服务instance最小数量。-1表示服务中心为单节点。 +eureka.server.min-available-instances-for-peer-replication=-1 +#instance之间相互复制开启的最小线程数量 +eureka.server.min-threads-for-peer-replication=5 +#instance之间用于状态复制,开启的最小线程数量 +eureka.server.min-threads-for-status-replication=1 +#instance之间复制数据时可以重试的次数 +eureka.server.number-of-replication-retries=5 +#eureka节点间间隔多长时间更新一次数据。默认10分钟。 +eureka.server.peer-eureka-nodes-update-interval-ms=600000 +#eureka服务状态的相互更新的时间间隔。 +eureka.server.peer-eureka-status-refresh-time-interval-ms=0 +#eureka对等节点间连接超时时间 +eureka.server.peer-node-connect-timeout-ms=200 +#eureka对等节点连接后的空闲时间 +eureka.server.peer-node-connection-idle-timeout-seconds=30 +#节点间的读数据连接超时时间 +eureka.server.peer-node-read-timeout-ms=200 +#eureka server 节点间连接的总共最大数量 +eureka.server.peer-node-total-connections=1000 +#eureka server 节点间连接的单机最大数量 +eureka.server.peer-node-total-connections-per-host=10 +#在服务节点启动时,eureka尝试获取注册信息的次数 +eureka.server.registry-sync-retries= +#在服务节点启动时,eureka多次尝试获取注册信息的间隔时间 +eureka.server.registry-sync-retry-wait-ms= +#当eureka server启动的时候,不能从对等节点获取instance注册信息的情况,应等待多长时间。 +eureka.server.wait-time-in-ms-when-sync-empty=0 +``` + +Eureka Client 常用配置: + +```properties +#该客户端是否可用 +eureka.client.enabled=true +#实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true +eureka.client.register-with-eureka=false +#此客户端是否获取eureka服务器注册表上的注册信息,默认为true +eureka.client.fetch-registry=false +#是否过滤掉,非UP的实例。默认为true +eureka.client.filter-only-up-instances=true +#与Eureka注册服务中心的通信zone和url地址 +eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/ + +#client连接Eureka服务端后的空闲等待时间,默认为30 秒 +eureka.client.eureka-connection-idle-timeout-seconds=30 +#client连接eureka服务端的连接超时时间,默认为5秒 +eureka.client.eureka-server-connect-timeout-seconds=5 +#client对服务端的读超时时长 +eureka.client.eureka-server-read-timeout-seconds=8 +#client连接all eureka服务端的总连接数,默认200 +eureka.client.eureka-server-total-connections=200 +#client连接eureka服务端的单机连接数量,默认50 +eureka.client.eureka-server-total-connections-per-host=50 +#执行程序指数回退刷新的相关属性,是重试延迟的最大倍数值,默认为10 +eureka.client.cache-refresh-executor-exponential-back-off-bound=10 +#执行程序缓存刷新线程池的大小,默认为5 +eureka.client.cache-refresh-executor-thread-pool-size=2 +#心跳执行程序回退相关的属性,是重试延迟的最大倍数值,默认为10 +eureka.client.heartbeat-executor-exponential-back-off-bound=10 +#心跳执行程序线程池的大小,默认为5 +eureka.client.heartbeat-executor-thread-pool-size=5 +# 询问Eureka服务url信息变化的频率(s),默认为300秒 +eureka.client.eureka-service-url-poll-interval-seconds=300 +#最初复制实例信息到eureka服务器所需的时间(s),默认为40秒 +eureka.client.initial-instance-info-replication-interval-seconds=40 +#间隔多长时间再次复制实例信息到eureka服务器,默认为30秒 +eureka.client.instance-info-replication-interval-seconds=30 +#从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒 +eureka.client.registry-fetch-interval-seconds=30 + +# 获取实例所在的地区。默认为us-east-1 +eureka.client.region=us-east-1 +#实例是否使用同一zone里的eureka服务器,默认为true,理想状态下,eureka客户端与服务端是在同一zone下 +eureka.client.prefer-same-zone-eureka=true +# 获取实例所在的地区下可用性的区域列表,用逗号隔开。(AWS) +eureka.client.availability-zones.china=defaultZone,defaultZone1,defaultZone2 +#eureka服务注册表信息里的以逗号隔开的地区名单,如果不这样返回这些地区名单,则客户端启动将会出错。默认为null +eureka.client.fetch-remote-regions-registry= +#服务器是否能够重定向客户端请求到备份服务器。 如果设置为false,服务器将直接处理请求,如果设置为true,它可能发送HTTP重定向到客户端。默认为false +eureka.client.allow-redirects=false +#客户端数据接收 +eureka.client.client-data-accept= +#增量信息是否可以提供给客户端看,默认为false +eureka.client.disable-delta=false +#eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“__“ +eureka.client.escape-char-replacement=__ +#eureka服务器序列化/反序列化的信息中获取“$”符号的替换字符串。默认为“_-” +eureka.client.dollar-replacement="_-" +#当服务端支持压缩的情况下,是否支持从服务端获取的信息进行压缩。默认为true +eureka.client.g-zip-content=true +#是否记录eureka服务器和客户端之间在注册表的信息方面的差异,默认为false +eureka.client.log-delta-diff=false +# 如果设置为true,客户端的状态更新将会点播更新到远程服务器上,默认为true +eureka.client.on-demand-update-status-change=true +#此客户端只对一个单一的VIP注册表的信息感兴趣。默认为null +eureka.client.registry-refresh-single-vip-address= +#client是否在初始化阶段强行注册到服务中心,默认为false +eureka.client.should-enforce-registration-at-init=false +#client在shutdown的时候是否显示的注销服务从服务中心,默认为true +eureka.client.should-unregister-on-shutdown=true +``` + +Eureka Instance 常用配置: + +```properties +#服务注册中心实例的主机名 +eureka.instance.hostname=localhost +#注册在Eureka服务中的应用组名 +eureka.instance.app-group-name= +#注册在的Eureka服务中的应用名称 +eureka.instance.appname= +#该实例注册到服务中心的唯一ID +eureka.instance.instance-id= +#该实例的IP地址 +eureka.instance.ip-address= +#该实例,相较于hostname是否优先使用IP +eureka.instance.prefer-ip-address=false +``` + + + +### Eureka集群原理 + +![Eureka集群原理](images/Middleware/Eureka集群原理.png) + +- **Eureka Server集群相互之间通过 Replicate(复制) 来同步数据** + + 相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其它节点。 + +- **如果某台Eureka Server宕机,Eureka Client的请求会自动切换到新的Eureka Server节点** + + 当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 所知的所有节点中。 + +- **Eureka Server的同步遵循一个原则:只要有一条边将节点连接,就可以进行信息传播与同步** + + 如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。 + +- **Eureka Server集群之间的状态是采用异步方式同步的** + + 所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。 + + + +**Eureka 分区** +Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS: + +- **region**:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region +- **zone**:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone + +上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。 + + + +**Eurka 保证 AP** + +Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。 + + + +### Eureka一致性协议 + +Eureka 和 Zookeeper 的最大区别: Eureka 是 AP 模型,Zookeeper 是 CP 模型。在出现脑裂等场景时,Eureka 可用性是每一位,也就是说出现脑裂时,每个分区仍可以独立提供服务,是去中心化的。那Eureka是如何实现最终一致性的呢? + + + +#### 消息广播 + +1. Eureka Server 管理了全部的服务器列表(PeerEurekaNodes) + +2. 当 Eureka Server 收到客户端的注册、下线、心跳请求时,通过 PeerEurekaNode 向其余的服务器进行消息广播,如果广播失败则重试,直到任务过期后取消任务,此时这两台服务器之间数据会出现短暂的不一致。 + + **注意:** 虽然消息广播失败,但只要收到客户端的心跳,仍会向所有的服务器(包括失联的服务器)广播心跳任务。 + +3. 如果网络恢复正常,收到了其它服务器广播的心跳任务,此时可能有三种情况: + + 1. 一是脑裂很快恢复,一切正常; + 2. 二是该实例已经自动过期,则重新进行注册; + 3. 三是数据冲突,出现不一致的情况,则需要发起同步请求,其实也就是重新注册一次,同时踢除老的实例。 + + 总之,通过集群之间的消息广播可以实现数据的最终一致性。 + + + +**服务注册** + +1. Spring Cloud Eureka 在应用启动时,会在 EurekaAutoServiceRegistration 这个类初始化的时候,主动去 Eureka Server 端注册。 +2. Eureka 在启动完成之后会启动一个 40 秒执行一次的定时任务,该任务会去监测自身的 IP 信息以及自身的配置信息是否发生改变,如果发生改变,则会重新发起注册。 +3. 续约返回 404 状态码时,会去重新注册 + + + +**主动下线** + +Eureka 会在 spring 容器销毁的时候执行 shutDown 方法 ,该方法首先会将自身的状态改为 DOWN,接着发送cancle 命令至 Eureka Server 请求下掉自己的服务。 + + + +**心跳续约与自动下线** + +健康检测,一般都是 TTL(Time To Live) 机制。eg: 客户端每 5s 发送心跳,服务端 15s 没收到心跳包,更新实例状态为不健康, 30s 未收到心跳包,从服务列表中删除。**Eureka Server 默认每 30s 发送心跳包,90s 未收心跳则删除。这个清理过期实例的线程,每 60s 执行一次。** + + + +#### 崩溃恢复 + +**重启** + +Spring Cloud Eureka 启动时,在初始化 EurekaServerBootstrap#initEurekaServerContext 时会调用 PeerAwareInstanceRegistryImpl#syncUp 从其它 Eureka 中同步数据。 + + + +**脑裂** + +- 发生脑裂后:和 Eureka Server 同区的服务可以正常访问,而不同区的服务则自动过期。 +- 脑裂恢复后:接收其它 Eureka Sever 发送过来的心跳请求,此时有三种情况:一是脑裂很快恢复,一切正常;二是该实例已经自动过期,则重新进行注册;三是数据冲突,出现不一致的情况,则需要发起同步请求。 \ No newline at end of file diff --git a/src/Middleware/1303.md b/src/Middleware/1303.md new file mode 100644 index 0000000..ac55a06 --- /dev/null +++ b/src/Middleware/1303.md @@ -0,0 +1,16 @@ +Zuul 是由 Netflix 孵化的一个致力于“网关 “解决方案的开源组件。 + +![Zuul介绍](images/Middleware/Zuul介绍.png) + +**网关的作用** + +- 统一入口:未全部为服务提供一个唯一的入口,网关起到外部和内部隔离的作用,保障了后台服务的安全性 +- 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求 +- 动态路由:动态的将请求路由到不同的后端集群中 +- 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射 + +![zuul过滤器的生命周期](images/Middleware/zuul过滤器的生命周期.png) + +![zuul限流参数](images/Middleware/zuul限流参数.png) + +![Zuul](images/Middleware/Zuul.png) \ No newline at end of file diff --git a/src/Middleware/1304.md b/src/Middleware/1304.md new file mode 100644 index 0000000..50d3ec6 --- /dev/null +++ b/src/Middleware/1304.md @@ -0,0 +1,165 @@ +`Feign` 使用时分成两步:一是生成 Feign 的动态代理;二是 Feign 执行。 + +![Feign整体设计](images/Middleware/Feign整体设计.png) + +**总结:** + +- 前两步是生成动态对象:将 Method 方法的注解解析成 MethodMetadata,并最终生成 Feign 动态代理对象 +- 后几步是调用过程:根据解析的MethodMetadata对象,将Method方法的参数转换成Request,最后调用Client发送请求 + + + +### Feign动态代理 + +![Feign动态代理](images/Middleware/Feign动态代理.png) + +`Feign` 的默认实现是 ReflectiveFeign,通过 Feign.Builder 构建。**总结** + +1. Contract 统一将方法解析 MethodMetadata(*),这样就可以通过实现不同的 Contract 适配各种 REST 声明式规范。 +2. buildTemplate 实际上将 Method 方法的参数转换成 Request。 +3. 将 metadata 和 buildTemplate 封装成 MethodHandler。 + + + +### 基于Feign实现负载均衡 + +基于 Feign 的负载均衡(整合 Ribbon)。想要进行负载均衡,那就要对 Client 进行包装,实现负载均衡。 相关代码见`RibbonClient`和`LBClient`。 + +```java +// RibbonClient 主要逻辑 +private final Client delegate; +private final LBClientFactory lbClientFactory; +public Response execute(Request request, Request.Options options) throws IOException { + try { + URI asUri = URI.create(request.url()); + String clientName = asUri.getHost(); + URI uriWithoutHost = cleanUrl(request.url(), clientName); + // 封装 RibbonRequest,包含 Client、Request、uri + LBClient.RibbonRequest ribbonRequest = + new LBClient.RibbonRequest(delegate, request, uriWithoutHost); + // executeWithLoadBalancer 实现负载均衡 + return lbClient(clientName).executeWithLoadBalancer( + ribbonRequest, + new FeignOptionsClientConfig(options)).toResponse(); + } catch (ClientException e) { + propagateFirstIOException(e); + throw new RuntimeException(e); + } +} +``` + +**总结:** 实际上是把 Client 对象包装了一下,通过 executeWithLoadBalancer 进行负载均衡,这是 Ribbon 提供了 API。更多关于 Ribbon 的负载均衡就不在这进一步说明了。 + +```java +public final class LBClient extends AbstractLoadBalancerAwareClient + { + + // executeWithLoadBalancer 方法通过负载均衡算法后,最终调用 execute + @Override + public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) + throws IOException, ClientException { + Request.Options options; + if (configOverride != null) { + options = new Request.Options( + configOverride.get(CommonClientConfigKey.ConnectTimeout, connectTimeout), + configOverride.get(CommonClientConfigKey.ReadTimeout, readTimeout), + configOverride.get(CommonClientConfigKey.FollowRedirects, followRedirects)); + } else { + options = new Request.Options(connectTimeout, readTimeout); + } + // http 请求 + Response response = request.client().execute(request.toRequest(), options); + if (retryableStatusCodes.contains(response.status())) { + response.close(); + throw new ClientException(ClientException.ErrorType.SERVER_THROTTLED); + } + return new RibbonResponse(request.getUri(), response); + } +} +``` + + + +### 基于Feign实现熔断 + +基于 Feign 的熔断与限流(整合 Hystrix)。想要进行限流,那就要在方法执行前进行拦截,也就是重写InvocationHandlerFactory,在方法执行前进行熔断与限流。相关代码见 `HystrixFeign`,其实也就是实现了 HystrixInvocationHandler。 + +```java +// HystrixInvocationHandler 主要逻辑 +public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + HystrixCommand hystrixCommand = + new HystrixCommand(setterMethodMap.get(method)) { + @Override + protected Object run() throws Exception { + return HystrixInvocationHandler.this.dispatch.get(method).invoke(args); + } + @Override + protected Object getFallback() { + }; + } + ... + return hystrixCommand.execute(); +} +``` + + + +### Feign参数编码整体流程 + +![Feign参数编码整体流程](images/Middleware/Feign参数编码整体流程.png)**总结:** + +- 前两步是`Feign`代理生成阶段,解析方法参数及注解元信息。后三步是调用阶段,将方法参数编码成Http请求的数据格式 +- Contract 接口将 UserService 中每个接口中的方法及其注解解析为 MethodMetadata,然后使用 RequestTemplate# request 编码为一个 Request +- RequestTemplate#request 编码为一个 Request 后就可以调用 Client#execute 发送 Http 请求 +- Client 的具体实现有 HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty 等。本文关注前三步:即 Feign 方法元信息解析及参数编码过程。 + + + +### Feign整体装配流程 + +![Feign整体装配流程](images/Middleware/Feign整体装配流程.png) + +**总结:** OpenFeign 装配有两个入口: + +1. @EnableAutoConfiguration 自动装配(spring.factories) + + ```properties + org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\ + org.springframework.cloud.openfeign.FeignAutoConfiguration + ``` + + - FeignAutoConfiguration自动装配 FeignContext 和 Targeter,以及 Client 配置。 + - `FeignContext` 是每个 FeignClient 的装配上下文,默认的配置是 FeignClientsConfiguration + - `Targeter` 有两种实现:一是 DefaultTargeter,直接调用 Feign.Builder; **二是 HystrixTargeter,调用 HystrixFeign.Builder,开启熔断。** + - `Client` :自动装配 ApacheHttpClient,OkHttpClient,装配条件不满足,默认是 Client.Default。但这些 Client 都没有实现负载均衡。 + - FeignRibbonClientAutoConfiguration实现负载均衡,负载均衡是在 Client 这一层实现的。 + - `HttpClientFeignLoadBalancedConfiguration` ApacheHttpClient 实现负载均衡 + - `OkHttpFeignLoadBalancedConfiguration` OkHttpClient实现负载均衡 + - `DefaultFeignLoadBalancedConfiguration` Client.Default实现负载均衡 + +2. @EnableFeignClients 自动扫描 + + @EnableFeignClients 注入 FeignClientsRegistrar,FeignClientsRegistrar 开启自动扫描,将包下 @FeignClient 标注的接口包装成 FeignClientFactoryBean 对象,最终通过 Feign.Builder 生成该接口的代理对象。而 Feign.Builder 的默认配置是 FeignClientsConfiguration,是在 FeignAutoConfiguration 自动注入的。 + +**注意:** 熔断与限流是 FeignAutoConfiguration 通过注入 HystrixTargeter 完成的,而负载均衡是FeignRibbonClientAutoConfiguration 注入的。 + + + +### 其它 + +Feign是是一个声明式的Web Service客户端。 + +![Feign介绍](images/Middleware/Feign介绍.png) + +Feign就是使用了动态代理: + +- 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理 +- 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心 +- Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址 +- 最后针对这个地址,发起请求、解析响应 + +![Feign](images/Middleware/Feign.png) + +![Feign远程调用流程](images/Middleware/Feign远程调用流程.png) \ No newline at end of file diff --git a/src/Middleware/1305.md b/src/Middleware/1305.md new file mode 100644 index 0000000..9fa032e --- /dev/null +++ b/src/Middleware/1305.md @@ -0,0 +1,9 @@ +Ribbon Netflix 公司开源的一个负载均衡的组件。 + +![Ribbon介绍](images/Middleware/Ribbon介绍.png) + +Ribbon在服务消费者端配置和使用,它的作用就是负载均衡,然后默认使用的负载均衡算法是轮询算法,Ribbon会从Eureka服务端中获取到对应的服务注册表,然后就知道相应服务的位置,然后Ribbon根据设计的负载均衡算法去选择一台机器,Feigin就会针对这些机器构造并发送请求,如下图所示: + +![Ribbon](images/Middleware/Ribbon.png) + +![Ribbon规则](images/Middleware/Ribbon规则.png) \ No newline at end of file diff --git a/src/Middleware/1306.md b/src/Middleware/1306.md new file mode 100644 index 0000000..18630db --- /dev/null +++ b/src/Middleware/1306.md @@ -0,0 +1,9 @@ +Hystrix是Netstflix 公司开源的一个项目,它提供了熔断器功能,能够阻止分布式系统中出现联动故障。 + +![Hystrix介绍](images/Middleware/Hystrix介绍.png) + +Hystrix是隔离、熔断以及降级的一个框架,说白了就是Hystrix会搞很多小线程池然后让这些小线程池去请求服务,返回结果,Hystrix相当于是个中间过滤区,如果我们的积分服务挂了,那我们请求积分服务直接就返回了,不需要等待超时时间结束抛出异常,这就是所谓的熔断,但是也不能啥都不干就返回啊,不然我们之后手动加积分咋整啊,那我们每次调用积分服务就在数据库里记录一条消息,这就是所谓的降级,Hystrix隔离、熔断和降级的全流程如下: + +![Hystrix](images/Middleware/Hystrix.png) + +![Hystrix熔断](images/Middleware/Hystrix熔断.png) \ No newline at end of file diff --git a/src/Middleware/1307.md b/src/Middleware/1307.md new file mode 100644 index 0000000..4d73cf4 --- /dev/null +++ b/src/Middleware/1307.md @@ -0,0 +1,3 @@ +Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0、 Spring Boot 2.0 和 Project Reactor 等技术开发的网关, Spring Cloud Gateway 旨在为微服务架构提供简单、 有效且统一的 API 路由管理方式。 + +![Gateway介绍](images/Middleware/Gateway介绍.png) \ No newline at end of file diff --git a/src/Middleware/1308.md b/src/Middleware/1308.md new file mode 100644 index 0000000..413e0f2 --- /dev/null +++ b/src/Middleware/1308.md @@ -0,0 +1,3 @@ +Spring Cloud 中提供了分布式配置中 Spring Cloud Config ,为外部配置提供了客户端和服务器端的支持。 + +![Config介绍](images/Middleware/Config介绍.png) \ No newline at end of file diff --git a/src/Middleware/1309.md b/src/Middleware/1309.md new file mode 100644 index 0000000..f6fb153 --- /dev/null +++ b/src/Middleware/1309.md @@ -0,0 +1,3 @@ +使用 Spring Cloud Bus, 可以非常容易地搭建起消息总线。 + +![Bus介绍](images/Middleware/Bus介绍.png) \ No newline at end of file diff --git a/src/Middleware/1310.md b/src/Middleware/1310.md new file mode 100644 index 0000000..521f7f4 --- /dev/null +++ b/src/Middleware/1310.md @@ -0,0 +1,3 @@ +Sprin Cloud 构建的微服务系统中可以使用 Spring Cloud OAuth2 来保护微服务系统。 + +![OAuth2介绍](images/Middleware/OAuth2介绍.png) \ No newline at end of file diff --git a/src/Middleware/1311.md b/src/Middleware/1311.md new file mode 100644 index 0000000..07fe062 --- /dev/null +++ b/src/Middleware/1311.md @@ -0,0 +1,3 @@ +Spring Cloud Sleuth是Spring Cloud 个组件,它的主要功能是在分布式系统中提供服务链路追踪的解决方案。 + +![Sleuth介绍](images/Middleware/Sleuth介绍.png) \ No newline at end of file diff --git a/src/Middleware/1401.md b/src/Middleware/1401.md new file mode 100644 index 0000000..254bfec --- /dev/null +++ b/src/Middleware/1401.md @@ -0,0 +1,32 @@ +Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。 + +作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 + +由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。 + + + +**Mybaits优缺点** + +- 优点 + - 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用 + - 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接 + - 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持) + - 能够与Spring很好的集成 + - 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护 + +- 缺点 + +- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求 +- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库 + + + +**#{}和${}的区别是什么?** + +- `${}` 是字符串替换 +- `#{}` 是预处理 + +使用#{}可以有效的防止SQL注入,提高系统安全性。 \ No newline at end of file diff --git a/src/Middleware/1402.md b/src/Middleware/1402.md new file mode 100644 index 0000000..b19941c --- /dev/null +++ b/src/Middleware/1402.md @@ -0,0 +1,16 @@ +![Mybatis核心成员数据流](images/Middleware/Mybatis核心成员数据流.png) + +核心成员说明 + +| 核心成员 | 功能说明 | +| ---------------- | ------------------------------------------------------------ | +| Configuration | 保存MyBatis大部分配置信息 | +| SqlSession | MyBatis主要的顶层API,与数据库交互,实现数据库增删改查功能 | +| Executor | MyBatis 调度器,负责SQL语句的生成和查询缓存的维护 | +| StatementHandler | 封装JDBC,负责对JDBC statement 的操作,如设置参数等 | +| ParameterHandler | 用户传递的参数转换成JDBC Statement 所对应的数据类型 | +| ResultSetHandler | 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合 | +| TypeHandler | 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换 | +| MappedStatement | MappedStatement维护一条节点的封装 | +| SqlSource | 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回 | +| BoundSql | 表示动态生成的SQL语句以及相应的参数信息 | \ No newline at end of file diff --git a/src/Middleware/1403.md b/src/Middleware/1403.md new file mode 100644 index 0000000..117a157 --- /dev/null +++ b/src/Middleware/1403.md @@ -0,0 +1 @@ +![Mybatis流程](images/Middleware/Mybatis流程.png) \ No newline at end of file diff --git a/src/Middleware/1404.md b/src/Middleware/1404.md new file mode 100644 index 0000000..3cc3e32 --- /dev/null +++ b/src/Middleware/1404.md @@ -0,0 +1,3 @@ +Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,根据类的全限定名+方法名,唯一定位到一个MapperStatement并调用执行器执行所代表的sql,然后将sql执行结果返回。 + +Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。 \ No newline at end of file diff --git a/src/Middleware/1405.md b/src/Middleware/1405.md new file mode 100644 index 0000000..a069a46 --- /dev/null +++ b/src/Middleware/1405.md @@ -0,0 +1,26 @@ +MyBatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指 SqlSession 级别的缓存,当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条 SQL。二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存,对于mapper级别的缓存不同的sqlsession 是可以共享的。一级缓存核心类是PerpetualCache,本质是一个hashMap。二级缓存默认不开启。 + +![MyBatis缓存机制](images/Middleware/MyBatis缓存机制.png) + +### 一级缓存 + +Mybatis的一级缓存原理(sqlsession级别)。第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个 map。 + +- key:MapperID+offset+limit+Sql+所有的入参 +- value:用户信息 + +同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。 + + + +### 二级缓存 + +二级缓存的范围是 mapper 级别(mapper 同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是 map。mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。 + +- key:MapperID+offset+limit+Sql+所有的入参 + +**具体使用需要配置:** + +- Mybatis 全局配置中启用二级缓存配置 +- 在对应的 Mapper.xml 中配置 cache 节点 +- 在对应的 select 查询节点中添加 useCache=true \ No newline at end of file diff --git a/src/Middleware/1406.md b/src/Middleware/1406.md new file mode 100644 index 0000000..80e0e02 --- /dev/null +++ b/src/Middleware/1406.md @@ -0,0 +1,69 @@ +![MyBatis执行sql流程](images/Middleware/MyBatis执行sql流程.png) + +### SqlSessionFactoryBuilder + +从名称长可以看出来使用的建造者设计模式(Builder),用于构建SqlSessionFactory对象: + +- 解析mybatis的xml配置文件,然后创建Configuration对象(对应标签) +- 根据创建的Configuration对象,创建SqlSessionFactory(默认使用DefaultSqlSessionFactory) + + + +### SqlSessionFactory + +从名称上可以看出使用的工厂模式(Factory),用于创建并初始化SqlSession对象(数据库会话) + +- 调用openSession方法,创建SqlSession对象,可以将SqlSession理解为数据库连接(会话) +- openSession方法有多个重载,可以创建SqlSession关闭自动提交、指定ExecutorType、指定数据库事务隔离级别 + + + +### SqlSession + +如果了解web开发,就应该知道cookie和session吧,SqlSession的session和web开发中的session概念类似。session,译为“会话、会议”,数据的有效时间范围是在会话期间(会议期间),会话(会议)结束后,数据就清除了。也可以将SqlSession理解为一个数据库连接(但也不完全正确,因为创建SqlSession之后,如果不执行sql,那么这个连接是无意义的,所以数据库连接在执行sql的时候才建立的)。 + +SqlSession只是定义了执行sql的一些方法,而具体的实现由子类来完成,比如SqlSession有一个接口实现类DefaultSqlSession。MyBatis中通过Executor来执行sql的,在创建SqlSession的时候(openSession),同时会创建一个Executor对象。 + + + +### Executor + +Executor(人称“执行器”)是一个接口,定义了对JDBC的封装。MyBatis中提供了多种执行器,如下: + +![MyBatis-Executor](images/Middleware/MyBatis-Executor.png)CacheExecutor其实是一个Executor代理类,包含一个delegate,需要创建时手动传入(要入simple、reuse、batch三者之一)。ClosedExecutor,所有接口都会抛出异常,表示一个已经关闭的Executor。创建SqlSession时,默认使用的是SimpleExecutor。 + +Executor是对JDBC的封装。当我们使用JDBC来执行sql时,一般会先预处理sql,也就是conn.prepareStatement(sql),获取返回的PreparedStatement对象(实现了Statement接口),再调用statement的executeXxx()来执行sql。也就是说,Executor在执行sql的时候也是需要创建Statement对象的。 + + + +### StatementHandler + +在JDBC中,是调用Statement.executeXxx()来执行sql。在MyBatis,也是调用Statement.executeXxx()来执行sql,此时就不得不提StatementHandler,可以将其理解为一个工人,他的工作包括 + +- 对sql进行预处理 +- 调用statement.executeXxx()执行sql +- 将数据库返回的结果集进行对象转换(ORM) + + + +### ParameterHandler + + ParameterHandler的功能就是sql预处理后,进行设置参数。ParamterHandler有一个DefaultParameterHandler。 + + + +### ResultSetHandler + +当执行statement.execute()后,就可以通过statement.getResultSet()来获取结果集,获取到结果集之后,MyBatis会使用ResultSetHandler来将结果集的数据转换为Java对象(ORM映射)。ResultSetHandler有一个实现类,DefaultResultHandler,其重写handlerResultSets方法。 + + + +### TypeHandler + +TypeHandler主要用在两个地方: + +- 参数绑定,发生在ParameterHandler.setParamenters()中 + + MyBatis中,可以使用来定义结果的映射关系,包括每一个字段的类型。TypeHandler可以对某个字段按照xml中配置的类型进行设置值,比如设置sql的uid参数时,类型为INTEGER(jdbcType) + +- 获取结果集中的字段值,发生在ResultSetHandler处理结果集的过程中 \ No newline at end of file diff --git a/src/Middleware/1501.md b/src/Middleware/1501.md new file mode 100644 index 0000000..4157672 --- /dev/null +++ b/src/Middleware/1501.md @@ -0,0 +1,45 @@ +```shell +# CentOS +yum install nginx; +# Ubuntu +sudo apt-get install nginx; +# Mac +brew install nginx; +``` + +安装完成后,通过 `rpm \-ql nginx` 命令查看 `Nginx` 的安装信息: + +```shell +# Nginx配置文件 +/etc/nginx/nginx.conf # nginx 主配置文件 +/etc/nginx/nginx.conf.default + +# 可执行程序文件 +/usr/bin/nginx-upgrade +/usr/sbin/nginx + +# nginx库文件 +/usr/lib/systemd/system/nginx.service # 用于配置系统守护进程 +/usr/lib64/nginx/modules # Nginx模块目录 + +# 帮助文档 +/usr/share/doc/nginx-1.16.1 +/usr/share/doc/nginx-1.16.1/CHANGES +/usr/share/doc/nginx-1.16.1/README +/usr/share/doc/nginx-1.16.1/README.dynamic +/usr/share/doc/nginx-1.16.1/UPGRADE-NOTES-1.6-to-1.10 + +# 静态资源目录 +/usr/share/nginx/html/404.html +/usr/share/nginx/html/50x.html +/usr/share/nginx/html/index.html + +# 存放Nginx日志文件 +/var/log/nginx +``` + +主要关注的文件夹有两个: + +- `/etc/nginx/conf.d/` 是子配置项存放处, `/etc/nginx/nginx.conf` 主配置文件会默认把该文件夹中所有子配置项都引入 + +- `/usr/share/nginx/html/` 静态文件都放在这个文件夹,也可以根据你自己的习惯放在其他地方 \ No newline at end of file diff --git a/src/Middleware/1502.md b/src/Middleware/1502.md new file mode 100644 index 0000000..5e1ac9d --- /dev/null +++ b/src/Middleware/1502.md @@ -0,0 +1,48 @@ +一般可以在 `/etc/nginx/nginx.conf` 中配置。 + +### systemctl命令 + +```shell +# 开机配置 +# 开机自动启动 +systemctl enable nginx +# 关闭开机自动启动 +systemctl disable nginx + +# 启动Nginx +systemctl start nginx # 启动Nginx成功后,可以直接访问主机IP,此时会展示Nginx默认页面 +# 停止Nginx +systemctl stop nginx +# 重启Nginx +systemctl restart nginx +# 重新加载Nginx +systemctl reload nginx +# 查看 Nginx 运行状态 +systemctl status nginx + +# 查看Nginx进程 +ps -ef | grep nginx +# 杀死Nginx进程 +kill -9 pid # 根据上面查看到的Nginx进程号,杀死Nginx进程,-9 表示强制结束进程 +``` + + + +### Nginx应用命令 + +```shell +# 启动 +nginx -s start +# 向主进程发送信号,重新加载配置文件,热重启 +nginx -s reload +# 重启 Nginx +nginx -s reopen +# 快速关闭 +nginx -s stop +# 等待工作进程处理完成后关闭 +nginx -s quit +# 查看当前 Nginx 最终的配置 +nginx -T +# 检查配置是否有问题 +nginx -t +``` \ No newline at end of file diff --git a/src/Middleware/1503.md b/src/Middleware/1503.md new file mode 100644 index 0000000..5874fb6 --- /dev/null +++ b/src/Middleware/1503.md @@ -0,0 +1,89 @@ +Nginx层级结构如下: + +![Nginx层级结构](images/Middleware/Nginx层级结构.png) + +### URI匹配 + +```shell +location = / { + # 完全匹配 = + # 大小写敏感 ~ + # 忽略大小写 ~* +} +location ^~ /images/ { + # 前半部分匹配 ^~ + # 可以使用正则,如: + # location ~* \.(gif|jpg|png)$ { } +} +location / { + # 如果以上都未匹配,会进入这里 +} +``` + +### upstream + +```nginx +语法:upstream name { + ... +} +上下文:http +示例: +upstream back_end_server{ + server 192.168.100.33:8081 +} +``` + +`upstream` 用于定义上游服务器(指的就是后台提供的应用服务器)的相关信息。 + +```nginx +upstream test_server{ + # weight=3 权重值,默认为1 + # max_conns=1000 上游服务器的最大并发连接数 + # fail_timeout=10s 服务器不可用的判定时间 + # max_fails=2 服务器不可用的检查次数 + # backup 备份服务器,仅当其他服务器都不可用时才会启用 + # down 标记服务器长期不可用,离线维护; + server 127.0.0.1:8081 weight=3 max_conns=1000 fail_timeout=10s max_fails=2; + # 限制每个worker子进程与上游服务器空闲长连接的最大数量 + keepalive 16; + # 单个长连接可以处理的最多 HTTP 请求个数 + keepalive_requests 100; + # 空闲长连接的最长保持时间 + keepalive_timeout 60s; +} +``` + + + +### proxy_pass + +```nginx +语法:proxy_pass URL; +上下文:location、if、limit_except +示例: +proxy_pass http://127.0.0.1:8081 +proxy_pass http://127.0.0.1:8081/proxy +``` + +`URL` 参数原则: + +- `URL` 必须以 `http` 或 `https` 开头 + +- `URL` 中可以携带变量 + +- `URL` 中是否带 `URI` ,会直接影响发往上游请求的 `URL` + +`URL` 后缀带 `/` 和不带 `/` 的区别: + +- 不带 `/` 意味着 `Nginx` 不会修改用户 `URL` ,而是直接透传给上游的应用服务器 +- 带 `/` 意味着 `Nginx` 会修改用户 `URL` ,修改方法是将 `location` 后的 `URL` 从用户 `URL` 中删除 + +```nginx +# 用户请求 URL :/bbs/abc/test.html +location /bbs/{ + # 请求到达上游应用服务器的 URL :/bbs/abc/test.html + proxy_pass http://127.0.0.1:8080; + # 请求到达上游应用服务器的 URL :/abc/test.html + proxy_pass http://127.0.0.1:8080/; +} +``` \ No newline at end of file diff --git a/src/Middleware/1504.md b/src/Middleware/1504.md new file mode 100644 index 0000000..fd66ea0 --- /dev/null +++ b/src/Middleware/1504.md @@ -0,0 +1,328 @@ +### 反向代理 + +```nginx +# 1 /etc/nginx/conf.d/proxy.conf +# 1.1 模拟被代理服务 +server{ + listen 8080; + server_name localhost; + + location /proxy/ { + root /usr/share/nginx/html/proxy; + index index.html; + } +} +# 1.2 /usr/share/nginx/html/proxy/index.html +

121.42.11.34 proxy html

+ + +# 2 /etc/nginx/conf.d/proxy.conf +# 2.1 back server +upstream back_end { + server 121.42.11.34:8080 weight=2 max_conns=1000 fail_timeout=10s max_fails=3; + keepalive 32; + keepalive_requests 80; + keepalive_timeout 20s; +} +# 2.2 代理配置 +server { + listen 80; + # vim /etc/hosts进入配置文件,添加如下内容:121.5.180.193 proxy.lion.club + server_name proxy.lion.club; + location /proxy { + proxy_pass http://back_end/proxy; + } +} +``` + + + +### 负载均衡 + +```nginx +# 1 /etc/nginx/conf.d/balance.conf +# 1.1 模拟被代理服务1 +server{ + listen 8020; + location / { + return 200 'return 8020 \n'; + } +} +# 1.2 模拟被代理服务2 +server{ + listen 8030; + location / { + return 200 'return 8030 \n'; + } +} +# 1.3 模拟被代理服务3 +server{ + listen 8040; + location / { + return 200 'return 8040 \n'; + } +} + +# 2 /etc/nginx/conf.d/balance.conf +# 2.1 demo server list +upstream demo_server { + server 121.42.11.34:8020; + server 121.42.11.34:8030; + server 121.42.11.34:8040; +} + +# 2.2 代理配置 +server { + listen 80; + server_name balance.lion.club; + location /balance/ { + proxy_pass http://demo_server; + } +} +``` + + + +#### hash算法 + +```nginx +upstream demo_server { + hash $request_uri; + server 121.42.11.34:8020; + server 121.42.11.34:8030; + server 121.42.11.34:8040; +} + +server { + listen 80; + server_name balance.lion.club; + location /balance/ { + proxy_pass http://demo_server; + } +} +``` + +`hash $request_uri` 表示使用 `request_uri` 变量作为 `hash` 的 `key` 值,只要访问的 `URI` 保持不变,就会一直分发给同一台服务器。 + + + +#### ip_hash + +```nginx +upstream demo_server { + ip_hash; + server 121.42.11.34:8020; + server 121.42.11.34:8030; + server 121.42.11.34:8040; +} + +server { + listen 80; + server_name balance.lion.club; + + location /balance/ { + proxy_pass http://demo_server; + } +} +``` + +根据客户端的请求 `ip` 进行判断,只要 `ip` 地址不变就永远分配到同一台主机。它可以有效解决后台服务器 `session` 保持的问题。 + + + +#### 最少连接数算法 + +```nginx +upstream demo_server { + zone test 10M; # zone可以设置共享内存空间的名字和大小 + least_conn; + server 121.42.11.34:8020; + server 121.42.11.34:8030; + server 121.42.11.34:8040; +} + +server { + listen 80; + server_name balance.lion.club; + + location /balance/ { + proxy_pass http://demo_server; + } +} +``` + +各个 `worker` 子进程通过读取共享内存的数据,来获取后端服务器的信息。来挑选一台当前已建立连接数最少的服务器进行分配请求。 + + + +### 配置缓存 + +**① proxy_cache** + +存储一些之前被访问过、而且可能将要被再次访问的资源,使用户可以直接从代理服务器获得,从而减少上游服务器的压力,加快整个访问速度。 + +```nginx +语法:proxy_cache zone | off ; # zone 是共享内存的名称 +默认值:proxy_cache off; +上下文:http、server、location +``` + +**② proxy_cache_path** + +设置缓存文件的存放路径。 + +```nginx +语法:proxy_cache_path path [level=levels] ...可选参数省略,下面会详细列举 +默认值:proxy_cache_path off +上下文:http +``` + +参数含义: + +- `path` 缓存文件的存放路径 +- `level path` 的目录层级 +- `keys_zone` 设置共享内存 +- `inactive` 在指定时间内没有被访问,缓存会被清理,默认10分钟 + +**③ proxy_cache_key** + +设置缓存文件的 `key` 。 + +```nginx +语法:proxy_cache_key +默认值:proxy_cache_key $scheme$proxy_host$request_uri; +上下文:http、server、location +``` + +**④ proxy_cache_valid** + +配置什么状态码可以被缓存,以及缓存时长。 + +```nginx +语法:proxy_cache_valid [code...] time; +上下文:http、server、location +配置示例:proxy_cache_valid 200 304 2m;; # 说明对于状态为200和304的缓存文件的缓存时间是2分钟 +``` + +**⑤ proxy_no_cache** + +定义相应保存到缓存的条件,如果字符串参数的至少一个值不为空且不等于“ 0”,则将不保存该响应到缓存。 + +```nginx +语法:proxy_no_cache string; +上下文:http、server、location +示例:proxy_no_cache $http_pragma $http_authorization; +``` + +**⑥ proxy_cache_bypass** + +定义条件,在该条件下将不会从缓存中获取响应。 + +```nginx +语法:proxy_cache_bypass string; +上下文:http、server、location +示例:proxy_cache_bypass $http_pragma $http_authorization; +``` + +**⑦ upstream_cache_status 变量** + +它存储了缓存是否命中的信息,会设置在响应头信息中,在调试中非常有用。 + +```nginx +MISS: 未命中缓存 +HIT:命中缓存 +EXPIRED: 缓存过期 +STALE: 命中了陈旧缓存 +REVALIDDATED: Nginx验证陈旧缓存依然有效 +UPDATING: 内容陈旧,但正在更新 +BYPASS: X响应从原始服务器获取 +``` + +```nginx +proxy_cache_path /etc/nginx/cache_temp levels=2:2 keys_zone=cache_zone:30m max_size=2g inactive=60m use_temp_path=off; + +upstream cache_server{ + server 121.42.11.34:1010; + server 121.42.11.34:1020; +} + +server { + listen 80; + server_name cache.lion.club; + + # 场景一:实时性要求不高,则配置缓存 + location /demo { + proxy_cache cache_zone; # 设置缓存内存,上面配置中已经定义好的 + proxy_cache_valid 200 5m; # 缓存状态为200的请求,缓存时长为5分钟 + proxy_cache_key $request_uri; # 缓存文件的key为请求的URI + add_header Nginx-Cache-Status $upstream_cache_status # 把缓存状态设置为头部信息,响应给客户端 + proxy_pass http://cache_server; # 代理转发 + } + + # 场景二:实时性要求非常高,则配置不缓存 + # URI 中后缀为 .txt 或 .text 的设置变量值为 "no cache" + if ($request_uri ~ \.(txt|text)$) { + set $cache_name "no cache" + } + location /test { + proxy_no_cache $cache_name; # 判断该变量是否有值,如果有值则不进行缓存,如果没有值则进行缓存 + proxy_cache cache_zone; # 设置缓存内存 + proxy_cache_valid 200 5m; # 缓存状态为200的请求,缓存时长为5分钟 + proxy_cache_key $request_uri; # 缓存文件的key为请求的URI + add_header Nginx-Cache-Status $upstream_cache_status # 把缓存状态设置为头部信息,响应给客户端 + proxy_pass http://cache_server; # 代理转发 + } +} +``` + + + +### HTTPS + +下载证书的压缩文件,里面有个 `Nginx` 文件夹,把 `xxx.crt` 和 `xxx.key` 文件拷贝到服务器目录,再进行如下配置: + +```nginx +server { + listen 443 ssl http2 default_server; # SSL 访问端口号为 443 + server_name lion.club; # 填写绑定证书的域名(我这里是随便写的) + ssl_certificate /etc/nginx/https/lion.club_bundle.crt; # 证书地址 + ssl_certificate_key /etc/nginx/https/lion.club.key; # 私钥地址 + ssl_session_timeout 10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # 支持ssl协议版本,默认为后三个,主流版本是[TLSv1.2] + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } +} +``` + + + +### 开启gzip压缩 + +在 `/etc/nginx/conf.d/` 文件夹中新建配置文件 `gzip.conf` : + +```nginx +# 默认off,是否开启gzip +gzip on; +# 要采用 gzip 压缩的 MIME 文件类型,其中 text/html 被系统强制启用 +gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; +# ---- 以上两个参数开启就可以支持Gzip压缩了 ---- + + +# 默认 off,该模块启用后,Nginx 首先检查是否存在请求静态文件的 gz 结尾的文件,如果有则直接返回该 .gz 文件内容 +gzip_static on; +# 默认 off,nginx做为反向代理时启用,用于设置启用或禁用从代理服务器上收到相应内容 gzip 压缩 +gzip_proxied any; +# 用于在响应消息头中添加Vary:Accept-Encoding,使代理服务器根据请求头中的Accept-Encoding识别是否启用gzip压缩 +gzip_vary on; +# gzip 压缩比,压缩级别是 1-9,1 压缩级别最低,9 最高,级别越高压缩率越大,压缩时间越长,建议 4-6 +gzip_comp_level 6; +# 获取多少内存用于缓存压缩结果,16 8k 表示以 8k*16 为单位获得 +gzip_buffers 16 8k; +# 允许压缩的页面最小字节数,页面字节数从header头中的 Content-Length 中进行获取。默认值是 0,不管页面多大都压缩。建议设置成大于 1k 的字节数,小于 1k 可能会越压越大 +gzip_min_length 1k; +# 默认 1.1,启用 gzip 所需的 HTTP 最低版本 +gzip_http_version 1.1; +``` \ No newline at end of file diff --git a/src/Middleware/1505.md b/src/Middleware/1505.md new file mode 100644 index 0000000..d1d5650 --- /dev/null +++ b/src/Middleware/1505.md @@ -0,0 +1,144 @@ +### 侦听端口 + +```nginx +server { + # Standard HTTP Protocol + listen 80; + # Standard HTTPS Protocol + listen 443 ssl; + # For http2 + listen 443 ssl http2; + # Listen on 80 using IPv6 + listen [::]:80; + # Listen only on using IPv6 + listen [::]:80 ipv6only=on; +} +``` + + + +### 访问日志 + +```nginx +server { + # Relative or full path to log file + access_log /path/to/file.log; + # Turn 'on' or 'off' + access_log on; +} +``` + + + +### 域名 + +```nginx +server { + # Listen to yourdomain.com + server_name yourdomain.com; + # Listen to multiple domains server_name yourdomain.com www.yourdomain.com; + # Listen to all domains + server_name *.yourdomain.com; + # Listen to all top-level domains + server_name yourdomain.*; + # Listen to unspecified Hostnames (Listens to IP address itself) + server_name ""; +} +``` + + + +### 静态资源 + +```nginx +server { + listen 80; + server_name yourdomain.com; + location / { + root /path/to/website; + } +} +``` + + + +### 重定向 + +```nginx +server { + listen 80; + server_name www.yourdomain.com; + return 301 http://yourdomain.com$request_uri; +} + +server { + listen 80; + server_name www.yourdomain.com; + location /redirect-url { + return 301 http://otherdomain.com; + } +} +``` + + + +### 反向代理 + +```nginx +server { + listen 80; + server_name yourdomain.com; + location / { + proxy_pass http://0.0.0.0:3000; + # where 0.0.0.0:3000 is your application server (Ex: node.js) bound on 0.0.0.0 listening on port 3000 + } +} +``` + + + +### 负载均衡 + +```nginx +upstream node_js { + server 0.0.0.0:3000; + server 0.0.0.0:4000; + server 123.131.121.122; +} + +server { + listen 80; + server_name yourdomain.com; + location / { + proxy_pass http://node_js; + } +} +``` + + + +### SSL协议 + +```nginx +server { + listen 443 ssl; + server_name yourdomain.com; + ssl on; + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/privatekey.pem; + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /path/to/fullchain.pem; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_session_timeout 1h; + ssl_session_cache shared:SSL:50m; + add_header Strict-Transport-Security max-age=15768000; +} + +# Permanent Redirect for HTTP to HTTPS +server { + listen 80; + server_name yourdomain.com; + return 301 https://$host$request_uri; +} +``` \ No newline at end of file diff --git a/src/Middleware/1506.md b/src/Middleware/1506.md new file mode 100644 index 0000000..2577308 --- /dev/null +++ b/src/Middleware/1506.md @@ -0,0 +1,64 @@ +```shell +# 统计IP访问量 +awk '{print $1}' access.log | sort -n | uniq | wc -l +# 查看某一时间段的IP访问量(4-5点) +grep "07/Apr/2017:0[4-5]" access.log | awk '{print $1}' | sort | uniq -c| sort -nr | wc -l +# 查看访问最频繁的前100个IP +awk '{print $1}' access.log | sort -n |uniq -c | sort -rn | head -n 100 +# 查看访问100次以上的IP +awk '{print $1}' access.log | sort -n |uniq -c |awk '{if($1 >100) print $0}'|sort -rn +# 查询某个IP的详细访问情况,按访问频率排序 +grep '104.217.108.66' access.log |awk '{print $7}'|sort |uniq -c |sort -rn |head -n 100 + +# 页面访问统计 +# 查看访问最频的页面(TOP100) +awk '{print $7}' access.log | sort |uniq -c | sort -rn | head -n 100 +# 查看访问最频的页面([排除php页面】(TOP100) +grep -v ".php" access.log | awk '{print $7}' | sort |uniq -c | sort -rn | head -n 100 +# 查看页面访问次数超过100次的页面 +cat access.log | cut -d ' ' -f 7 | sort |uniq -c | awk '{if ($1 > 100) print $0}' | less +# 查看最近1000条记录,访问量最高的页面 +tail -1000 access.log |awk '{print $7}'|sort|uniq -c|sort -nr|less + +# 请求量统计 +# 统计每秒的请求数,top100的时间点(精确到秒) +awk '{print $4}' access.log |cut -c 14-21|sort|uniq -c|sort -nr|head -n 100 +# 统计每分钟的请求数,top100的时间点(精确到分钟) +awk '{print $4}' access.log |cut -c 14-18|sort|uniq -c|sort -nr|head -n 100 +# 统计每小时的请求数,top100的时间点(精确到小时) +awk '{print $4}' access.log |cut -c 14-15|sort|uniq -c|sort -nr|head -n 100 + +# 性能分析 +# 列出传输时间超过 3 秒的页面,显示前20条 +cat access.log|awk '($NF > 3){print $7}'|sort -n|uniq -c|sort -nr|head -20 +# 列出php页面请求时间超过3秒的页面,并统计其出现的次数,显示前100条 +cat access.log|awk '($NF > 1 && $7~/\.php/){print $7}'|sort -n|uniq -c|sort -nr|head -100 + +# 蜘蛛抓取统计 +# 统计蜘蛛抓取次数 +grep 'Baiduspider' access.log |wc -l +# 统计蜘蛛抓取404的次数 +grep 'Baiduspider' access.log |grep '404' | wc -l + +# TCP连接统计 +# 查看当前TCP连接数 +netstat -tan | grep "ESTABLISHED" | grep ":80" | wc -l +# 用tcpdump嗅探80端口的访问看看谁最高 +tcpdump -i eth0 -tnn dst port 80 -c 1000 | awk -F"." '{print $1"."$2"."$3"."$4}' | sort | uniq -c | sort -nr + +awk '{print $1}' $logpath |sort -n|uniq|wc -l +# 系统正在统计某一个时间段IP访问量为 +sed -n '/22\/Jun\/2017:1[5]/,/22\/Jun\/2017:1[6]/p' $logpath|awk '{print $1}'|sort -n|uniq|wc -l +# 访问100次以上的IP +awk '{print $1}' $logpath|sort -n|uniq -c|awk '{if($1>100) print $0}'|sort -rn +# 访问最频繁的请求(TOP50) +awk '{print $7}' $logpath |sort |uniq -c|sort -rn |head -n 50 +# 统计每秒的请求数(TOP50) +awk '{print $4}' $logpath|cut -c 14-21|sort |uniq -c|sort -nr|head -n 50 +# 统计每分钟的请求数(TOP50) +awk '{print $4}' $logpath|cut -c 14-18|sort|uniq -c|sort -nr|head -n 50 +# 统计每小时请求数(TOP50) +awk '{print $4}' $logpath|cut -c 14-15|sort|uniq -c|sort -nr|head -n 50 +# 传输时间超过1秒的请求(TOP20) +cat $logpath|awk '($NF > 1){print $7}'|sort -n|uniq -c|sort -nr|head -20 +``` \ No newline at end of file diff --git a/src/Middleware/1601.md b/src/Middleware/1601.md new file mode 100644 index 0000000..b759126 --- /dev/null +++ b/src/Middleware/1601.md @@ -0,0 +1 @@ +LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统。 \ No newline at end of file diff --git a/src/Middleware/1602.md b/src/Middleware/1602.md new file mode 100644 index 0000000..ed6ef33 --- /dev/null +++ b/src/Middleware/1602.md @@ -0,0 +1,256 @@ +LVS有三种常见的负载均衡的模式:**NAT模式(网络地址转换模式)、IP TUN(隧道模式)、DR(知己路由模式)**。阿里云还提供了两种模式:**full NAT模式、ENAT模式**。 + + + +**术语** + +- **DS**:Director Server,指的是前端负载均衡器 +- **RS**:Real Server,后端真实的工作服务器 +- **VIP**:Virtual Ip Address,向外部直接面向用户请求,作为用户请求的目标的IP地址 +- **DIP**:Director Server IP,主要用于和内部主机通讯的IP地址 +- **RIP**:Real Server IP,后端服务器的IP地址 +- **CIP**:Client IP,客户端主机IP地址 + +(假设 cip 是200.200.200.2, vip是200.200.200.1) + + + +### NAT模式(网络地址转换) + +**NAT模式(NetWork Address Translation-网络地址转换)**。客户发出请求,发送请求给链接调度器的VIP,调度器将请求报文中的目标Ip地址改为RIP。这样服务器RealServer将请求的内容发给调度器,调度器再将报文中的源IP地址改为VIP。 + +![LVS-NAT](images/Middleware/LVS-NAT.png) + +**原理** + +- client发出请求(sip 200.200.200.2,dip 200.200.200.1) +- 请求包到达lvs,lvs修改请求包为(sip 200.200.200.2, dip rip) +- 请求包到达rs, rs回复(sip rip,dip 200.200.200.2) +- 这个回复包不能直接给client,因为rip不是VIP会被reset掉 +- 但是因为lvs是网关,所以这个回复包先走到网关,网关有机会修改sip +- 网关修改sip为VIP,修改后的回复包(sip 200.200.200.1,dip 200.200.200.2)发给client + + + +![LVS-NAT-IP](images/Middleware/LVS-NAT-IP.png) + +**优点** + +- 配置简单 +- 支持端口映射(看名字就知道) +- RIP一般是私有地址,主要用户LVS和RS之间通信 + +**缺点** + +- LVS和所有RS必须在同一个vlan +- 进出流量都要走LVS转发 +- LVS容易成为瓶颈 +- 一般而言需要将VIP配置成RS的网关 + + + +**分析** + +- **为什么NAT要求lvs和RS在同一个vlan?** + + 因为回复包必须经过lvs再次修改sip为vip,client才认,如果回复包的sip不是client包请求的dip(也就是vip),那么这个连接会被reset掉。如果LVS不是网关,因为回复包的dip是cip,那么可能从其它路由就走了,LVS没有机会修改回复包的sip + + + +**NAT结构** + +- LVS修改进出包的(sip, dip)的时只改了其中一个(所以才有接下来的full NAT) +- NAT最大的缺点是要求LVS和RS必须在同一个vlan,这样限制了LVS集群和RS集群的部署灵活性 + +![LVS-NAT-STR](images/Middleware/LVS-NAT-STR.png) + + + +### IP TUN模型(IP隧道) + +**IP TUN模型(IP Tunneling-IP隧道)**。和DR模式差不多,但是比DR多了一个隧道技术以支持realserver不在同一个物理环境中。就是realserver一个在北京,一个工作在上海。在原有的IP报文外再次封装多一层IP首部,内部IP首部(源地址为CIP,目标IIP为VIP),外层IP首部(源地址为DIP,目标IP为RIP + +**原理** + +- 请求包到达LVS后,LVS将请求包封装成一个新的IP报文 +- 新的IP包的目的IP是某一RS的IP,然后转发给RS +- RS收到报文后IPIP内核模块解封装,取出用户的请求报文 +- 发现目的IP是VIP,而自己的tunl0网卡上配置了这个IP,从而愉快地处理请求并将结果直接发送给客户 + + + +**优点** + +- 集群节点可以跨vlan +- 跟DR一样,响应报文直接发给client + +**缺点** + +- RS上必须安装运行IPIP模块 +- 多增加了一个IP头 +- LVS和RS上的tunl0虚拟网卡上配置同一个VIP(类似DR) + + + +**分析** + +- **为什么IP TUN不要求同一个vlan?** + + 因为IP TUN中不是修改MAC来路由,所以不要求同一个vlan,只要求lvs和rs之间ip能通就行。DR模式要求的是lvs和RS之间广播能通 + +- **IP TUN性能** + + 回包不走LVS,但是多做了一次封包解包,不如DR好 + + + +**IP TUN结构** + +- 图中红线是再次封装过的包,ipip是操作系统的一个内核模块 +- DR可能在小公司用的比较多,IP TUN用的少一些 + +![LVS-TP-TUN-STR](images/Middleware/LVS-TP-TUN-STR.png) + + + +### DR模式(直接路由) + +**DR模式(Director Routing-直接路由)**。整个DR模式都是停留在第二层的数据链路层。直接修改MAC。实现报文的转发。 + +![LVS-DR](images/Middleware/LVS-DR.png) + +**原理** + +- 请求流量(sip 200.200.200.2, dip 200.200.200.1) 先到达LVS +- 然后LVS,根据负载策略挑选众多 RS中的一个,然后将这个网络包的MAC地址修改成这个选中的RS的MAC +- 然后丢给交换机,交换机将这个包丢给选中的RS +- 选中的RS看到MAC地址是自己的、dip也是自己的,愉快地手下并处理、回复 +- 回复包(sip 200.200.200.1, dip 200.200.200.2) +- 经过交换机直接回复给client了(不再走LVS) + + + +![LVS-DR-IP](images/Middleware/LVS-DR-IP.png) + +**优点** + +- DR模式是性能最好的一种模式,入站请求走LVS,回复报文绕过LVS直接发给Client + +**缺点** + +- 要求LVS和rs在同一个vlan +- RS需要配置vip同时特殊处理arp +- 不支持端口映射 + + + +**分析** + +- **为什么要求LVS和RS在同一个vlan(或说同一个二层网络里)?** + + 因为DR模式依赖多个RS和LVS共用同一个VIP,然后依据MAC地址来在LVS和多个RS之间路由,所以LVS和RS必须在一个vlan或者说同一个二层网络里 + +- **DR 模式为什么性能最好?** + + 因为回复包不走LVS了,大部分情况下都是请求包小,回复包大,LVS很容易成为流量瓶颈,同时LVS只需要修改进来的包的MAC地址。 + +- **DR 模式为什么回包不需要走LVS了?** + + 因为RS和LVS共享同一个vip,回复的时候RS能正确地填好sip为vip,不再需要LVS来多修改一次(后面讲的NAT、Full NAT都需要)。 + + + +**DR结构** + +- 绿色是请求包进来,红色是修改过MAC的请求包 + +![LVS-DR-STR](images/Middleware/LVS-DR-STR.png) + + + +### full NAT模式 + +**full NAT模式(full NetWork Address Translation-全部网络地址转换)**。 + +**原理(类似NAT)** + +- client发出请求(sip 200.200.200.2 dip 200.200.200.1) +- 请求包到达lvs,lvs修改请求包为**(sip 200.200.200.1, dip rip)** 注意这里sip/dip都被修改了 +- 请求包到达rs, rs回复(sip rip,dip 200.200.200.1) +- 这个回复包的目的IP是VIP(不像NAT中是 cip),所以LVS和RS不在一个vlan通过IP路由也能到达lvs +- lvs修改sip为vip, dip为cip,修改后的回复包(sip 200.200.200.1,dip 200.200.200.2)发给client + + + +**优点** + +- 解决了NAT对LVS和RS要求在同一个vlan的问题,适用更复杂的部署形式 + +**缺点** + +- RS看不到cip(NAT模式下可以看到) +- 进出流量还是都走的lvs,容易成为瓶颈(跟NAT一样都有这个问题) + + + +**分析** + +- **为什么full NAT解决了NAT中要求的LVS和RS必须在同一个vlan的问题?** + +因为LVS修改进来的包的时候把(sip, dip)都修改了(这也是full的主要含义吧),RS的回复包目的地址是vip(NAT中是cip),所以只要vip和rs之间三层可通就行,这样LVS和RS可以在不同的vlan了,也就是LVS不再要求是网关,从而LVS和RS可以在更复杂的网络环境下部署。 + +- **为什么full NAT后RS看不见cip了?** + +因为cip被修改掉了,RS只能看到LVS的vip,在阿里内部会将cip放入TCP包的Option中传递给RS,RS上一般部署自己写的toa模块来从Options中读取的cip,这样RS能看到cip了, 当然这不是一个开源的通用方案。 + + + +**full NAT结构** + +- full NAT解决了NAT的同vlan的要求,**基本上可以用于公有云** +- 但还没解决进出流量都走LVS的问题(LVS要修改进出的包) + +![LVS-full-NAT-STR](images/Middleware/LVS-full-NAT-STR.png) + + + +### ENAT模式 + +**ENAT模式(enhence NAT)**。ENAT模式在内部也会被称为 三角模式或者DNAT/SNAT模式。 + +**原理** + +- client发出请求(cip,vip) +- 请求包到达lvs,lvs修改请求包为(vip,rip),并将cip放入TCP Option中 +- 请求包根据ip路由到达rs, ctk模块读取TCP Option中的cip +- 回复包(RIP, vip)被ctk模块截获,并将回复包改写为(vip, cip) +- 因为回复包的目的地址是cip所以不需要经过lvs,可以直接发给client + + + +**优点** + +- 不要求LVS和RS在同一个vlan +- 出去的流量不需要走LVS,性能好 + +**缺点** + +- 集团实现的自定义方案,需要在所有RS上安装ctk组件(类似full NAT中的toa) + + + +**分析** + +- **为什么ENAT的回复包不需要走回LVS了?** + +因为full NAT模式下要走回去是需要LVS再次改写回复包的IP,而ENAT模式下,该事情在RS上被ctk模块提前做掉。 + +- **为什么ENAT的LVS和RS可以在不同的vlan?** + +跟full NAT一样。 + + + +**ENAT结构** + +![LVS-ENAT-STR](images/Middleware/LVS-ENAT-STR.png) \ No newline at end of file diff --git a/src/Middleware/1603.md b/src/Middleware/1603.md new file mode 100644 index 0000000..18f685a --- /dev/null +++ b/src/Middleware/1603.md @@ -0,0 +1,37 @@ +### 静态调度算法 + +只根据算法进行调度 而不考虑后端服务器的实际连接情况和负载情况。 + +- **RR:轮叫调度(Round Robin)** + 调度器通过”轮叫”调度算法将外部请求按顺序轮流分配到集群中的真实服务器上,它均等地对待每一台服务器,而不管服务器上实际的连接数和系统负载。 + +- **WRR:加权轮叫(Weight RR)** + 调度器通过“加权轮叫”调度算法根据真实服务器的不同处理能力来调度访问请求。这样可以保证处理能力强的服务器处理更多的访问流量。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。 + +- **DH:目标地址散列调度(Destination Hash )** + 根据请求的目标IP地址,作为散列键(HashKey)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。 + +- **SH:源地址 hash(Source Hash)** + 源地址散列”调度算法根据请求的源IP地址,作为散列键(HashKey)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。 + + + +### 动态调度算法 + +- **LC:最少链接(Least Connections)** + 调度器通过”最少连接”调度算法动态地将网络请求调度到已建立的链接数最少的服务器上。如果集群系统的真实服务器具有相近的系统性能,采用”最小连接”调度算法可以较好地均衡负载。 + +- **WLC:加权最少连接(默认)(Weighted Least Connections)** + 在集群系统中的服务器性能差异较大的情况下,调度器采用“加权最少链接”调度算法优化负载均衡性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。 + +- **SED:最短延迟调度(Shortest Expected Delay )** + 在WLC基础上改进,Overhead = (ACTIVE+1)*256/加权,不再考虑非活动状态,把当前处于活动状态的数目+1来实现,数目最小的,接受下次请求,+1的目的是为了考虑加权的时候,非活动连接过多缺陷:当权限过大的时候,会倒置空闲服务器一直处于无连接状态。 + +- **NQ永不排队/最少队列调度(Never Queue Scheduling NQ)** + 无需队列。如果有台 realserver的连接数=0就直接分配过去,不需要再进行sed运算,保证不会有一个主机很空间。在SED基础上无论+几,第二次一定给下一个,保证不会有一个主机不会很空闲着,不考虑非活动连接,才用NQ,SED要考虑活动状态连接,对于DNS的UDP不需要考虑非活动连接,而httpd的处于保持状态的服务就需要考虑非活动连接给服务器的压力。 + +- **LBLC:基于局部性的最少链接(locality-Based Least Connections)** + 基于局部性的最少链接”调度算法是针对目标IP地址的负载均衡,目前主要用于Cache集群系统。该算法根据请求的目标IP地址找出该目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;若服务器不存在,或者该服务器超载且有服务器处于一半的工作负载,则用“最少链接”的原则选出一个可用的服务器,将请求发送到该服务器。 + +- **LBLCR:带复制的基于局部性最少连接(Locality-Based Least Connections with Replication)** + 带复制的基于局部性最少链接”调度算法也是针对目标IP地址的负载均衡,目前主要用于Cache集群系统。它与LBLC算法的不同之处是它要维护从一个目标IP地址到一组服务器的映射,而LBLC算法维护从一个目标IP地址到一台服务器的映射。该算法根据请求的目标IP地址找出该目标IP地址对应的服务器组,按”最小连接”原则从服务器组中选出一台服务器,若服务器没有超载,将请求发送到该服务器;若服务器超载,则按“最小连接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,将请求发送到该服务器。同时,当该服务器组有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。 \ No newline at end of file diff --git a/src/Middleware/1604.md b/src/Middleware/1604.md new file mode 100644 index 0000000..f580bb1 --- /dev/null +++ b/src/Middleware/1604.md @@ -0,0 +1,63 @@ +### 脑裂 + +由于两台高可用服务器对之间,在指定时间内,无法相互检测到对方的心跳,而各自启动故障切换转移功能,取得资源服务及所有权,而此时的两台高可用服务器对都还或者,并且正在运行,这样就会导致同一个IP或服务在两段同时启动而发生冲突的严重问题,最严重的是两台主机占用同一个VIP,当用户写入数据的时候可能同时写在两台服务器上。 +**1)产生裂脑原因** + +- 心跳链路故障,导致无法通信 +- 开启防火墙阻挡心跳消息传输 +- 心跳网卡地址配置等不正确 +- 其他:心跳方式不同,心跳广播冲突,软件bug等1234 + +备注: + +- 心跳线坏了(故障或老化) +- 网卡相关驱动坏了,IP配置即冲突问题(直连) +- 心跳线间连接的设备故障(网卡及交换机) +- 仲裁机器出问题 + +**2)防止裂脑的方法** + +- 采用串行或以太网电缆连接,同时用两条心跳线路 +- 做好裂脑的监控报警,在问题发生时人为第一时间介入仲裁 +- 启用磁盘锁,即正在服务的一方只在发现心跳线全部断开时,才开启磁盘锁 +- fence设备(智能电源管理设备) +- 增加仲裁盘 +- 加冗余线路 + + + +### 负载不均 + +**原因分析** + +- lvs自身的会话保持参数设置。优化:使用cookie代替session +- lvs调度算法设置,例如rr、wrr +- 后端RS节点的会话保持参数,例如apache的keepalive参数 +- 访问量较少的情况下,不均衡的现象更加明显 +- 用户发送的请求时间长短和请求资源多少以及大小 + + + +### 排错 + +先检查客户端到服务端——>然后检查负载均衡到RS端——>最后检查客户端到LVS端。 + +- 调度器上lvs调度规则及IP正确性 +- RS节点上VIP和ARP抑制的检查 + + + +**生成思路** + +- 对绑定的VIP做实时监控,出问题报警或自动处理后报警 +- 把绑定的VIP做成配置文件,例如: vim /etc/sysconfig/network-scripts/lo:0 + + + +**ARP抑制的配置思路** + +- 如果是单个VIP,那么可以用stop传参设置0 +- 如果RS端有多个VIP绑定,此时,即使是停止VIP绑定也不一定不要置0. +- RS节点上自身提供服务的检查 +- 辅助排除工具有tcpdump、ping等 +- 负载均衡和反向代理三角形排查理论 \ No newline at end of file diff --git a/src/Middleware/1701.md b/src/Middleware/1701.md new file mode 100644 index 0000000..8891005 --- /dev/null +++ b/src/Middleware/1701.md @@ -0,0 +1 @@ +Keepalived是一个免费开源的,用C编写的类似于layer3, 4 & 7交换机制软件,具备我们平时说的第3层、第4层和第7层交换机的功能。主要提供loadbalancing(负载均衡)和 high-availability(高可用)功能,负载均衡实现需要依赖Linux的虚拟服务内核模块(ipvs),而高可用是通过VRRP协议实现多台机器之间的故障转移服务。 \ No newline at end of file diff --git a/src/Middleware/1702.md b/src/Middleware/1702.md new file mode 100644 index 0000000..4c65a81 --- /dev/null +++ b/src/Middleware/1702.md @@ -0,0 +1,35 @@ +![Keepalived体系结构](images/Middleware/Keepalived体系结构.jpg) + +Keepalived的所有功能是配置keepalived.conf文件来实现的。上图是Keepalived的功能体系结构,大致分两层: + +- **内核空间(kernel space)** + + 主要包括IPVS(IP虚拟服务器,用于实现网络服务的负载均衡)和NETLINK(提供高级路由及其他相关的网络功能)两个部份。 + +- **用户空间(user space)** + + - WatchDog:负载监控checkers和VRRP进程的状况 + - VRRP Stack:负载负载均衡器之间的失败切换FailOver,如果只用一个负载均稀器,则VRRP不是必须的 + - Checkers:负责真实服务器的健康检查healthchecking,是keepalived最主要的功能。换言之,可以没有VRRP Stack,但健康检查healthchecking是一定要有的 + - IPVS wrapper:用户发送设定的规则到内核ipvs代码 + - Netlink Reflector:用来设定vrrp的vip地址等 + + + +**重要功能** + +- 管理LVS负载均衡软件 +- 实现LVS集群节点的健康检查中 +- 作为系统网络服务的高可用性(failover) + + + +**高可用故障切换转移原理** + +Keepalived高可用服务对之间的故障切换转移,是通过 VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)来实现的。 + +在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活看,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。 + +**VRRP** + +全称Virtual Router Redundancy Protocol ,中文名为虚拟路由冗余协议 ,VRRP的出现就是为了解决静态踣甶的单点故障问题,VRRP是通过一种竞选机制来将路由的任务交给某台VRRP路由器的。 \ No newline at end of file diff --git a/src/Middleware/1703.md b/src/Middleware/1703.md new file mode 100644 index 0000000..661b7bc --- /dev/null +++ b/src/Middleware/1703.md @@ -0,0 +1,38 @@ +在高可用(HA)系统中,当联系2个节点的“心跳线”断开时,本来为一整体、动作协调的HA系统,就分裂成为2个独立的个体。由于相互失去了联系,都以为是对方出了故障。两个节点上的HA软件像“裂脑人”一样,争抢“共享资源”、争起“应用服务”,就会发生严重后果——或者共享资源被瓜分、2边“服务”都起不来了;或者2边“服务”都起来了,但同时读写“共享存储”,导致数据损坏(常见如数据库轮询着的联机日志出错)。 + +对付HA系统“裂脑”的对策,目前达成共识的的大概有以下几条: + +- 添加冗余的心跳线,例如:双线条线(心跳线也HA),尽量减少“裂脑”发生几率 +- 启用磁盘锁。正在服务一方锁住共享磁盘,“裂脑”发生时,让对方完全“抢不走”共享磁盘资源。但使用锁磁盘也会有一个不小的问题,如果占用共享盘的一方不主动“解锁”,另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃,就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在HA中设计了“智能”锁。即:正在服务的一方只在发现心跳线全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了 +- 设置仲裁机制。例如设置参考IP(如网关IP),当心跳线完全断开时,2个节点都各自ping一下参考IP,不通则表明断点就出在本端。不仅“心跳”、还兼对外“服务”的本端网络链路断了,即使启动(或继续)应用服务也没有用了,那就主动放弃竞争,让能够ping通参考IP的一端去起服务。更保险一些,ping不通参考IP的一方干脆就自我重启,以彻底释放有可能还占用着的那些共享资源 + + + +### 产生原因 + +一般来说,裂脑的发生,有以下几种原因: + +- 高可用服务器对之间心跳线链路发生故障,导致无法正常通信 + - 因心跳线坏了(包括断了,老化) + - 因网卡及相关驱动坏了,ip配置及冲突问题(网卡直连) + - 因心跳线间连接的设备故障(网卡及交换机) + - 因仲裁的机器出问题(采用仲裁的方案) +- 高可用服务器上开启了 iptables防火墙阻挡了心跳消息传输 +- 高可用服务器上心跳网卡地址等信息配置不正确,导致发送心跳失败 +- 其他服务配置不当等原因,如心跳方式不同,心跳广插冲突、软件Bug等 + +**提示:** Keepalived配置里同一 VRRP实例如果 virtual_router_id两端参数配置不一致也会导致裂脑问题发生。 + + + +### 解决方案 + +在实际生产环境中,我们可以从以下几个方面来防止裂脑问题的发生: + +- 同时使用串行电缆和以太网电缆连接,同时用两条心跳线路,这样一条线路坏了,另一个还是好的,依然能传送心跳消息 +- 当检测到裂脑时强行关闭一个心跳节点(这个功能需特殊设备支持,如Stonith、feyce)。相当于备节点接收不到心跳消患,通过单独的线路发送关机命令关闭主节点的电源 +- 做好对裂脑的监控报警(如邮件及手机短信等或值班).在问题发生时人为第一时间介入仲裁,降低损失。例如,百度的监控报警短倍就有上行和下行的区别。报警消息发送到管理员手机上,管理员可以通过手机回复对应数字或简单的字符串操作返回给服务器.让服务器根据指令自动处理相应故障,这样解决故障的时间更短. + + + +在实施高可用方案时,要根据业务实际需求确定是否能容忍这样的损失。对于一般的网站常规业务,这个损失是可容忍的。 \ No newline at end of file diff --git a/src/Middleware/1704.md b/src/Middleware/1704.md new file mode 100644 index 0000000..0b61aff --- /dev/null +++ b/src/Middleware/1704.md @@ -0,0 +1,174 @@ +**第一步:keepalived软件安装** + + `yum install keepalived -y ` + +``` +/etc/keepalived +/etc/keepalived/keepalived.conf #keepalived服务主配置文件 +/etc/rc.d/init.d/keepalived #服务启动脚本 +/etc/sysconfig/keepalived +/usr/bin/genhash +/usr/libexec/keepalived +/usr/sbin/keepalived +``` + + + +**第二步:配置文件说明** + +全局配置 + +```shell + global_defs { # 全局配置 + notification_email { # 定义报警邮件地址 + acassen@firewall.loc + failover@firewall.loc + sysadmin@firewall.loc + } + notification_email_from Alexandre.Cassen@firewall.loc # 定义发送邮件的地址 + smtp_server 192.168.200.1 # 邮箱服务器 + smtp_connect_timeout 30 # 定义超时时间 + router_id LVS_DEVEL # 定义路由标识信息,相同局域网唯一 + } +``` + +虚拟ip配置brrp + +```shell +vrrp_instance VI_1 { # 定义实例 + state MASTER # 状态参数 master/backup 只是说明 + interface eth0 # 虚IP地址放置的网卡位置 + virtual_router_id 51 # 同一家族要一直,同一个集群id一致 + priority 100 # 优先级决定是主还是备 越大越优先 + advert_int 1 # 主备通讯时间间隔 + authentication { + auth_type PASS + auth_pass 1111 # 认证 + } + virtual_ipaddress { + 192.168.200.16 # 设备之间使用的虚拟ip地址 + 192.168.200.17 + 192.168.200.18 + } +} +``` + + + +**第三步:最终配置文件** + +主负载均衡服务器配置 + +```shell +[root@lb01 conf]# cat /etc/keepalived/keepalived.conf +! Configuration File for keepalived + +global_defs { + router_id lb01 +} + +vrrp_instance VI_1 { + state MASTER + interface eth0 + virtual_router_id 51 + priority 150 + advert_int 1 + authentication { + auth_type PASS + auth_pass 1111 + } + virtual_ipaddress { + 10.0.0.3 + } +} +``` + +备负载均衡服务器配置 + +```shell +[root@lb02 ~]# cat /etc/keepalived/keepalived.conf +! Configuration File for keepalived + +global_defs { + router_id lb02 +} + +vrrp_instance VI_1 { + state BACKUP + interface eth0 + virtual_router_id 51 + priority 100 + advert_int 1 + authentication { + auth_type PASS + auth_pass 1111 + } + virtual_ipaddress { + 10.0.0.3 + } +} +``` + + + +**第四步:启动keepalived** + +```shell +[root@lb02 ~]# /etc/init.d/keepalived start +Starting keepalived: [ OK ] +``` + + + +**第五步:在进行访问测试之前要保证后端的节点都能够单独的访问** + +测试连通性. 后端节点 + +```shell +[root@lb01 conf]# curl -H host:www.etiantian.org 10.0.0.8 +web01 www +[root@lb01 conf]# curl -H host:www.etiantian.org 10.0.0.7 +web02 www +[root@lb01 conf]# curl -H host:www.etiantian.org 10.0.0.9 +web03 www +[root@lb01 conf]# curl -H host:bbs.etiantian.org 10.0.0.9 +web03 bbs +[root@lb01 conf]# curl -H host:bbs.etiantian.org 10.0.0.8 +web01 bbs +[root@lb01 conf]# curl -H host:bbs.etiantian.org 10.0.0.7 +web02 bbs +``` + + + +**第六步:查看虚拟ip状态** + +```shell +[root@lb01 conf]# ip a +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 + link/ether 00:0c:29:90:7f:0d brd ff:ff:ff:ff:ff:ff + inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0 + inet 10.0.0.3/24 scope global secondary eth0:1 + inet6 fe80::20c:29ff:fe90:7f0d/64 scope link + valid_lft forever preferred_lft forever +3: eth1: mtu 1500 qdisc pfifo_fast state UP qlen 1000 + link/ether 00:0c:29:90:7f:17 brd ff:ff:ff:ff:ff:ff + inet 172.16.1.5/24 brd 172.16.1.255 scope global eth1 + inet6 fe80::20c:29ff:fe90:7f17/64 scope link + valid_lft forever preferred_lft forever +``` + + + +**第七步:【总结】配置文件修改** + +Keepalived主备配置文件区别: + +- router_id 信息不一致 +- state 状态描述信息不一致 +- priority 主备竞选优先级数值不一致 \ No newline at end of file diff --git a/src/Middleware/1801.md b/src/Middleware/1801.md new file mode 100644 index 0000000..aedada5 --- /dev/null +++ b/src/Middleware/1801.md @@ -0,0 +1 @@ +HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代 理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案。HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会话保持或七层处理。HAProxy运行在当前的硬件上,完全可以支持数以万计的并发连接。并且它的运行模式使得它可以很简单安全的整合进您当前的架构中, 同时可以保护你的web服务器不被暴露到网络上。 \ No newline at end of file diff --git a/src/Middleware/1802.md b/src/Middleware/1802.md new file mode 100644 index 0000000..2af09cc --- /dev/null +++ b/src/Middleware/1802.md @@ -0,0 +1,5 @@ +所谓的四层就是ISO参考模型中的第四层。四层负载均衡也称为四层交换机,它主要是**通过分析IP层及TCP/UDP层的流量实现的基于IP加端口的负载均衡**。常见的基于四层的负载均衡器有**LVS、F5**等。 + + ![四层负载均衡](images/Middleware/四层负载均衡.png) + +以常见的TCP应用为例,负载均衡器在接收到第一个来自客户端的SYN请求时,会通过设定的负载均衡算法选择一个最佳的后端服务器,同时将报文中目标IP地址修改为后端服务器IP,然后直接转发给该后端服务器,这样一个负载均衡请求就完成了。从这个过程来看,一个TCP连接是客户端和服务器直接建立的,而负载均衡器只不过完成了一个类似路由器的转发动作。在某些负载均衡策略中,为保证后端服务器返回的报文可以正确传递给负载均衡器,在转发报文的同时可能还会对报文原来的源地址进行修改。 \ No newline at end of file diff --git a/src/Middleware/1803.md b/src/Middleware/1803.md new file mode 100644 index 0000000..9484005 --- /dev/null +++ b/src/Middleware/1803.md @@ -0,0 +1,11 @@ +七层负载均衡器也称为七层交换机,位于OSI的最高层,即应用层,此时负载均衡器支持多种应用协议,常见的有**HTTP、FTP、SMTP**等。七层负载均衡器**可以根据报文内容,再配合负载均衡算法来选择后端服务器**,因此也称为“内容交换器”。比如,对于Web服务器的负载均衡,七层负载均衡器不但可以根据“IP+端口”的方式进行负载分流,还可以根据网站的URL、访问域名、浏览器类别、语言等决定负载均衡的策略。例如,有两台Web服务器分别对应中英文两个网站,两个域名分别是A、B,要实现访问A域名时进入中文网站,访问B域名时进入英文网站,这在四层负载均衡器中几乎是无法实现的,而七层负载均衡可以根据客户端访问域名的不同选择对应的网页进行负载均衡处理。常见的七层负载均衡器有HAproxy、Nginx等。 + +![七层负载均衡](images/Middleware/七层负载均衡.png) + +这里仍以常见的TCP应用为例,由于负载均衡器要获取到报文的内容,因此只能先代替后端服务器和客户端建立连接,接着,才能收到客户端发送过来的报文内容,然后再根据该报文中特定字段加上负载均衡器中设置的负载均衡算法来决定最终选择的内部服务器。纵观整个过程,七层负载均衡器在这种情况下类似于一个代理服务器。整个过程如下图所示。 + + + +**四层和七层负载均衡** + +对比四层负载均衡和七层负载均衡运行的整个过程,可以看出,在七层负载均衡模式下,负载均衡器与客户端及后端的服务器会分别建立一次TCP连接,而在四层负载均衡模式下,仅建立一次TCP连接。由此可知,七层负载均衡对负载均衡设备的要求更高,而七层负载均衡的处理能力也必然低于四层模式的负载均衡。 \ No newline at end of file diff --git a/src/Middleware/1804.md b/src/Middleware/1804.md new file mode 100644 index 0000000..6368b2a --- /dev/null +++ b/src/Middleware/1804.md @@ -0,0 +1,15 @@ +- roundrobin:表示简单的轮询 +- static-rr:表示根据权重 +- leastconn:表示最少连接者先处理 +- source:表示根据请求的源IP,类似Nginx的IP_hash机制 +- uri:表示根据请求的URI +- url_param:表示根据HTTP请求头来锁定每一次HTTP请求 +- rdp-cookie(name):表示根据据cookie(name)来锁定并哈希每一次TCP请求 + + + +**常用的负载均衡算法** + +- 轮询算法:roundrobin +- 根据请求源IP算法:source +- 最少连接者先处理算法:lestconn \ No newline at end of file diff --git a/src/Middleware/1805.md b/src/Middleware/1805.md new file mode 100644 index 0000000..7876406 --- /dev/null +++ b/src/Middleware/1805.md @@ -0,0 +1,8 @@ +HAProxy负载均衡与LVS负载均衡的区别: + +- 两者都是软件负载均衡产品,但是LVS是基于Linux操作系统实现的一种软负载均衡,而HAProxy是基于第三应用实现的软负载均衡 +- LVS是基于四层的IP负载均衡技术,而HAProxy是基于四层和七层技术、可提供TCP和HTTP应用的负载均衡综合解决方案 +- LVS工作在ISO模型的第四层,因此其状态监测功能单一,而HAProxy在状态监测方面功能强大,可支持端口、URL、脚本等多种状态检测方式 +- HAProxy虽然功能强大,但是整体处理性能低于四层模式的LVS负载均衡,而LVS拥有接近硬件设备的网络吞吐和连接负载能力 + +综上所述,HAProxy和LVS各有优缺点,没有好坏之分,要选择哪个作为负载均衡器,要以实际的应用环境来决定。 \ No newline at end of file diff --git a/src/Middleware/1806.md b/src/Middleware/1806.md new file mode 100644 index 0000000..cb01a9b --- /dev/null +++ b/src/Middleware/1806.md @@ -0,0 +1,15 @@ +**第一步:安装依赖** + +```shell +[root@test ~] # yum -y install make gcc gcc-c++ openssl-devel +``` + +**第二步:安装haproxy** + +```shell +[root@test ~] # wget http://haproxy.1wt.eu/download/1.3/src/haproxy-1.3.20.tar.gz +[root@test ~] # tar zcvf haproxy-1.3.20.tar.gz +[root@test ~] # cd haproxy-1.3.20 +[root@test ~] # make TARGET=linux26 PREFIX=/usr/local/haproxy # 将haproxy安装到/usr/local/haproxy +[root@test ~] # make install PREFIX=/usr/local/haproxy +``` \ No newline at end of file diff --git a/src/Middleware/2.md b/src/Middleware/2.md new file mode 100644 index 0000000..f8c3a04 --- /dev/null +++ b/src/Middleware/2.md @@ -0,0 +1,16 @@ +JDK 中 提供了一个 `SPI` 的功能,核心类是 `java.util.ServiceLoader`。其作用就是,可以通过类名获取在 `META-INF/services/` 下的多个配置实现文件。为了解决上面的扩展问题,现在我们在`META-INF/services/`下创建一个`com.github.yu120.test.SuperLoggerConfiguration`文件(没有后缀)。文件中只有一行代码,那就是我们默认的`com.github.yu120.test.XMLConfiguration`(注意,一个文件里也可以写多个实现,回车分隔)。然后通过 ServiceLoader 获取我们的 SPI 机制配置的实现类: + +```java +// META-INF/services/com.github.test.test.SuperLoggerConfiguration: +com.github.yu120.test.XMLConfiguration + +ServiceLoader serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); +Iterator iterator = serviceLoader.iterator(); +SuperLoggerConfiguration configuration; +while(iterator.hasNext()) { + // 加载并初始化实现类 + configuration = iterator.next(); +} +// 对最后一个configuration类调用configure方法 +configuration.configure(configFile); +``` \ No newline at end of file diff --git a/src/Middleware/3.md b/src/Middleware/3.md new file mode 100644 index 0000000..d696693 --- /dev/null +++ b/src/Middleware/3.md @@ -0,0 +1,14 @@ +Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader类中,它的getExtensionLoader方法用于从缓存中获取与接口对应的ExtensionLoader,若缓存未命中,则创建一个新的实例。Dubbo SPI的核心思想其实很简单: + +- 通过配置文件,解耦拓展接口和拓展实现类 +- 通过IOC自动注入依赖的拓展实现类对象 +- 通过URL参数,在运行时确认真正的自定义拓展类对象 + +Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下(以下demo来自dubbo官方文档)。 + +```properties +optimusPrime = org.apache.spi.OptimusPrime +bumblebee = org.apache.spi.Bumblebee +``` + +与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI 注解。 \ No newline at end of file diff --git a/src/Middleware/301.md b/src/Middleware/301.md new file mode 100644 index 0000000..2ce6119 --- /dev/null +++ b/src/Middleware/301.md @@ -0,0 +1,11 @@ +RocketMQ 是阿里巴巴开源的分布式消息中间件。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如Group、Topic、Queue等。系统组成则由Producer、Consumer、Broker、NameServer等。 + + + +**功能优势** + +- **削峰填谷**:主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题 +- **应用解耦**:解决不同重要程度、不同能力级别系统之间依赖导致一死全死 +- **提升性能**:当存在一对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统 +- **蓄流压测**:线上有些链路不好压测,可以通过堆积一定量消息再放开来压测 +- **异步处理**:不需要同步执行的远程调用可以有效提高响应时间 \ No newline at end of file diff --git a/src/Middleware/302.md b/src/Middleware/302.md new file mode 100644 index 0000000..136dbe8 --- /dev/null +++ b/src/Middleware/302.md @@ -0,0 +1,64 @@ +### 部署模型 + +![img](images/Middleware/1090617-20190626233829426-1023022108.png) + + + +### 角色 + +#### Broker + +- 理解成RocketMQ本身 +- Broker主要用于Producer和Consumer接收和发送消息 +- Broker会定时向NameSrver提交自己的信息 +- 是消息中间件的消息存储、转发服务器 +- 每个Broker节点在启动时都会遍历NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报 + + + +#### NameServer + +- 理解成Zookeeper的效果,只是他没用zk,而是自己写了个NameServer来替代zk +- 底层由Netty实现,提供了路由管理、服务注册、服务发现的功能,是一个无状态节点 +- NameServer是服务发现者,集群中各个角色(Producer、Broker、Consumer等)都需要定时向NameServer上报自己的状态,以便互相发现彼此,超时不上报的话,NameServer会把它从列表中剔除 +- NameServer可以部署多个,当多个NameServer存在的时候,其他角色同时向他们上报信息,以保证高可用, +- NameServer集群间互不通信,没有主备的概念 +- NameServer内存式存储,NameServer中的Broker、Topic等信息默认不会持久化,所以他是无状态节点 + + + +#### Producer + +- 消息的生产者 +- 随机选择其中一个NameServer节点建立长连接,获得Topic路由信息(包括Topic下的Queue,这些Queue分布在哪些Broker上等等) +- 接下来向提供Topic服务的Master建立长连接(因为RocketMQ只有Master才能写消息),且定时向Master发送心跳 + + + +#### Consumer + +- 消息的消费者 +- 通过NameServer集群获得Topic的路由信息,连接到对应的Broker上消费消息 +- 由于Master和Slave都可以读取消息,因此Consumer会与Master和Slave都建立连接进行消费消息 + + + +### 核心流程 + +- Broker都注册到Nameserver上 +- Producer发消息的时候会从Nameserver上获取发消息的Topic信息 +- Producer向提供服务的所有Master建立长连接,且定时向Master发送心跳 +- Consumer通过NameServer集群获得Topic的路由信息 +- Consumer会与所有的Master和所有的Slave都建立连接进行监听新消息 + + + +### 实现原理 + +RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的: + +- Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳 +- Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息 +- Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费 + +![RocketMQ实现原理](images/Middleware/RocketMQ实现原理.jpg) \ No newline at end of file diff --git a/src/Middleware/303.md b/src/Middleware/303.md new file mode 100644 index 0000000..4ac2432 --- /dev/null +++ b/src/Middleware/303.md @@ -0,0 +1,83 @@ +### Message(消息) + +消息载体。Message发送或者消费的时候必须指定Topic。Message有一个可选的Tag项用于过滤消息,还可以添加额外的键值对。 + + + +### Topic(主题) + +消息的逻辑分类,发消息之前必须要指定一个topic才能发,就是将这条消息发送到这个topic上。消费消息的时候指定这个topic进行消费。就是逻辑分类。 + + + +### Queue(队列) + +1个Topic会被分为N个Queue,数量是可配置的。message本身其实是存储到queue上的,消费者消费的也是queue上的消息。多说一嘴,比如1个topic4个queue,有5个Consumer都在消费这个topic,那么会有一个consumer浪费掉了,因为负载均衡策略,每个consumer消费1个queue,5>4,溢出1个,这个会不工作。 + + + +### Tag(标签) + +Tag 是 Topic 的进一步细分,顾名思义,标签。每个发送的时候消息都能打tag,消费的时候可以根据tag进行过滤,选择性消费。 + + + +### 消费模式(Message Model) + +消息模型:集群(Clustering)和广播(Broadcasting) + +#### 集群模式(Clustering) + +生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。 + +![集群模式](images/Middleware/集群模式.jpg) + +- 每条消息只需要被处理一次,Broker只会把消息发送给消费集群中的一个消费者 +- 在消息重投时,不能保证路由到同一台机器上 +- 消费状态由Broker维护 + + + +#### 广播模式(Broadcasting) + +**为了解决一条消息能被多个消费者消费的问题**,发布/订阅模型就来了。该模型是将消息发往一个`Topic`即主题中,所有订阅了这个 `Topic` 的订阅者都能消费这条消息。 + +![广播模式](images/Middleware/广播模式.jpg) + +- 消费进度由Consumer维护 +- 保证每个消费者都消费一次消息 +- 消费失败的消息不会重投 + + + +### Message Order(消息顺序) + +消息顺序:顺序(Orderly)和并发(Concurrently) + +#### 顺序(Orderly) + + + +#### 并发(Concurrently) + + + +### Producer Group(生产组) + +消息生产者组。标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。若事务消息,如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其 他producer,确认这条消息应该commit还是rollback。但开源版本并不完全支持事务消息(阉割了事务回查的代码)。 + + + +### Consumer Group(消费组) + +消息消费者组。标识一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致。同一个Consumer Group下的各个实例将共同消费topic的消息,起到负载均衡的作用。消费进度以Consumer Group为粒度管理,不同Consumer Group之间消费进度彼此不受影响,即消息A被Consumer Group1消费过,也会再给Consumer Group2消费。 + +注: RocketMQ要求同一个Consumer Group的消费者必须要拥有相同的注册信息,即必须要听一样的topic(并且tag也一样)。 + + + +### Offset + +在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。这个消费位置是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。 + +![RocketMQ-Offset](images/Middleware/RocketMQ-Offset.png) \ No newline at end of file diff --git a/src/Middleware/304.md b/src/Middleware/304.md new file mode 100644 index 0000000..3a395fb --- /dev/null +++ b/src/Middleware/304.md @@ -0,0 +1,121 @@ +### 消息清理 + +Broker中的消息被消费后不会立即删除,每条消息都会持久化到CommitLog中,每个Consumer连接到Broker后会维持消费进度信息,当有消息消费后只是当前Consumer的消费进度(CommitLog的offset)更新了。默认48小时后会删除不再使用的CommitLog文件: + +- 检查这个文件最后访问时间 +- 判断是否大于过期时间 +- 指定时间删除,默认凌晨4点 + +```java +/** + * {@link org.apache.rocketmq.store.DefaultMessageStore.CleanCommitLogService#isTimeToDelete()} + */ +private boolean isTimeToDelete() { + // when = "04"; + String when = DefaultMessageStore.this.getMessageStoreConfig().getDeleteWhen(); + // 是04点,就返回true + if (UtilAll.isItTimeToDo(when)) { + return true; + } + // 不是04点,返回false + return false; +} + +/** + * {@link org.apache.rocketmq.store.DefaultMessageStore.CleanCommitLogService#deleteExpiredFiles()} + */ +private void deleteExpiredFiles() { + // isTimeToDelete()这个方法是判断是不是凌晨四点,是的话就执行删除逻辑。 + if (isTimeToDelete()) { + // 默认是72,但是broker配置文件默认改成了48,所以新版本都是48。 + long fileReservedTime = 48 * 60 * 60 * 1000; + deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(72 * 60 * 60 * 1000, xx, xx, xx); + } +} + +/** + * {@link org.apache.rocketmq.store.CommitLog#deleteExpiredFile()} + */ +public int deleteExpiredFile(xxx) { + // 这个方法的主逻辑就是遍历查找最后更改时间+过期时间,小于当前系统时间的话就删了(也就是小于48小时)。 + return this.mappedFileQueue.deleteExpiredFileByTime(72 * 60 * 60 * 1000, xx, xx, xx); +} +``` + + + +### push or pull + +RocketMQ没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是**长轮询机制**,即拉取方式。Broker端属性 `longPollingEnable` 标记是否开启长轮询,默认开启。源码如下: + +```java +// {@link org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage()} + +// 拉取消息,结果放到pullCallback里 +this.pullAPIWrapper.pullKernelImpl(pullCallback); +``` + +**为什么要主动拉取消息而不使用事件监听方式?** + +事件驱动方式是建立好长连接,由事件(发送数据)的方式来实时推送。如果broker主动推送消息的话有可能push速度快,消费速度慢的情况,那么就会造成消息在consumer端堆积过多,同时又不能被其他consumer消费的情况。而pull的方式可以根据当前自身情况来pull,不会造成过多的压力而造成瓶颈。所以采取了pull的方式。 + + + +### 负载均衡 + +RocketMQ通过Topic在多Broker中分布式存储实现。 + +#### Producer端 + +发送端指定message queue发送消息到相应的broker,来达到写入时的负载均衡 + +- 提升写入吞吐量,当多个producer同时向一个broker写入数据的时候,性能会下降 +- 消息分布在多broker中,为负载消费做准备 + + + +**默认策略是随机选择:** + +- producer维护一个index +- 每次取节点会自增 +- index向所有broker个数取余 +- 自带容错策略 + + + +**其他实现:** + +- SelectMessageQueueByHash + +- - hash的是传入的args + +- SelectMessageQueueByRandom + +- SelectMessageQueueByMachineRoom 没有实现 + +也可以自定义实现**MessageQueueSelector**接口中的select方法 + +```java +MessageQueue select(final List mqs, final Message msg, final Object arg); +``` + + + +#### Consumer端 + +采用的是平均分配算法来进行负载均衡。 + +**其他负载均衡算法** + +- 平均分配策略(默认)(AllocateMessageQueueAveragely) +- 环形分配策略(AllocateMessageQueueAveragelyByCircle) +- 手动配置分配策略(AllocateMessageQueueByConfig) +- 机房分配策略(AllocateMessageQueueByMachineRoom) +- 一致性哈希分配策略(AllocateMessageQueueConsistentHash) +- 靠近机房策略(AllocateMachineRoomNearby) + + + +**当消费负载均衡Consumer和Queue不对等的时候会发生什么?** + +Consumer和Queue会优先平均分配,如果Consumer少于Queue的个数,则会存在部分Consumer消费多个Queue的情况,如果Consumer等于Queue的个数,那就是一个Consumer消费一个Queue,如果Consumer个数大于Queue的个数,那么会有部分Consumer空余出来,白白的浪费了。 \ No newline at end of file diff --git a/src/Middleware/305.md b/src/Middleware/305.md new file mode 100644 index 0000000..0dd0ebf --- /dev/null +++ b/src/Middleware/305.md @@ -0,0 +1,60 @@ +### Producer + +- **Topic**:消息主题,通过Topic对不同的业务消息进行分类 + +- **Tag**:消息标签,用来进一步区分某个Topic下的消息分类,消息从生产者发出即带上的属性 + +- **key**:每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过 topic,key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key 尽可能唯一,这样可以避免潜在的哈希冲突 + +- **日志**:消息发送成功或者失败,要打印消息日志,务必要打印 send result 和key 字段 + +- **send**:send消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在sendResult里定义 + + - **SEND_OK**:消息发送成功 + - **FLUSH_DISK_TIMEOUT**:消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 + - **FLUSH_SLAVE_TIMEOUT**:消息发送成功,但是服务器同步到Slave时超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 + - **SLAVE_NOT_AVAILABLE**:消息发送成功,但是此时slave不可用,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 + +- **订阅关系一致** + + 多个Group ID订阅了多个Topic,并且每个Group ID里的多个消费者实例的订阅关系保持了一致。 + + ![RocketMQ消息正确订阅关系](images/Middleware/RocketMQ消息正确订阅关系.png) + + + +### Consumer + +- **消费幂等** + + 为了防止消息重复消费导致业务处理异常,消息队列RocketMQ版的消费者在接收到消息后,有必要根据业务上的唯一Key对消息做幂等处理。消息重复的场景如下: + + - **发送时消息重复** + - **投递时消息重复** + - **负载均衡时消息重复**(包括但不限于网络抖动、Broker重启以及消费者应用重启) + +- **日志**:消费时记录日志,以便后续定位问题 + +- **批量消费**:尽量使用批量方式消费方式,可以很大程度上提高消费吞吐量 + + + +### 事务消息 + +MQ与DB一致性原理(两方事务) + +![MQ与DB一致性原理](images/Middleware/MQ与DB一致性原理.png) + +事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。实现原理如下: + +- 生产者先发送一条半事务消息到MQ +- MQ收到消息后返回ack确认 +- 生产者开始执行本地事务 +- 如果事务执行成功发送commit到MQ,失败发送rollback +- 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查 +- 生产者查询事务执行最终状态 +- 根据查询事务状态再次提交二次确认 + +如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。 + +![RocketMQ事务消息](images/Middleware/RocketMQ事务消息.jpg) \ No newline at end of file diff --git a/src/Middleware/306.md b/src/Middleware/306.md new file mode 100644 index 0000000..8905c67 --- /dev/null +++ b/src/Middleware/306.md @@ -0,0 +1,50 @@ +RocketMQ的消息是存储到Topic的Queue里面的,Queue本身是FIFO(First Int First Out)先进先出队列。所以单个Queue是可以保证有序性的。 + +顺序消息(FIFO 消息)是 MQ 提供的一种严格按照顺序进行发布和消费的消息类型。顺序消息由两个部分组成: + +- **顺序发布** +- **顺序消费** + + + +顺序消息包含两种类型: + +- **分区顺序**:一个Partition内所有的消息按照先进先出的顺序进行发布和消费 +- **全局顺序**:一个Topic内所有的消息按照先进先出的顺序进行发布和消费 + + + +![img](images/Middleware/471426-20180519131211273-554395305.png) + +对于两个订单的消息的原始数据:a1、b1、b2、a2、a3、b3(绝对时间下发生的顺序): + +- 在发送时,a订单的消息需要保持a1、a2、a3的顺序,b订单的消息也相同,但是a、b订单之间的消息没有顺序关系,这意味着a、b订单的消息可以在不同的线程中被发送出去 +- 在存储时,需要分别保证a、b订单的消息的顺序,但是a、b订单之间的消息的顺序可以不保证 + + + +### 保持顺序发送 + +消息被发送时保持顺序。 + + + +### 保持顺序发送存储 + +消息被存储时保持和发送的顺序一致。 + + + +### 保持顺序消费 + +消息被消费时保持和存储的顺序一致。 + +#### MQPullConsumer + +MQPullConsumer由用户控制线程,主动从服务端获取消息,每次获取到的是一个MessageQueue中的消息。PullResult中的List msgFoundList自然和存储顺序一致,用户需要再拿到这批消息后自己保证消费的顺序。 + + + +#### MQPushConsumer + +对于PushConsumer,由用户注册MessageListener来消费消息,在客户端中需要保证调用MessageListener时消息的顺序性。 \ No newline at end of file diff --git a/src/Middleware/307.md b/src/Middleware/307.md new file mode 100644 index 0000000..e278148 --- /dev/null +++ b/src/Middleware/307.md @@ -0,0 +1,220 @@ +一条消息从生产到被消费,将会经历三个阶段: + +![Rocket消息丢失](images/Middleware/Rocket消息丢失.jpg) + +- 生产阶段:Producer 新建消息,然后通过网络将消息投递给 MQ Broker +- 存储阶段:消息将会存储在 Broker 端磁盘中 +- 消息阶段:Consumer 将会从 Broker 拉取消息 + +以上任一阶段都可能会丢失消息,我们只要找到这三个阶段丢失消息原因,采用合理的办法避免丢失,就可以彻底解决消息丢失的问题。 + + + +### 生产阶段 + +Producer通过网络将消息发送给Broker,这个发送可能会发生丢失,比如网络延迟不可达等。 + +失败会自动重试,即使重试N次也不行后,那客户端也会知道消息没成功,这也可以自己补偿等,不会盲目影响到主业务逻辑。再比如即使Broker挂了,那还有其他Broker再提供服务了,高可用,不影响。 + +总结:**同步发送+自动重试机制+多个Master节点** + + + +#### 同步发送 + +有三种send方法,同步发送、异步发送、单向发送,可以采取同步发送的方式进行发送消息。 + +- **同步发送**:发消息时会同步阻塞等待broker返回的结果,如果没成功,则不会收到SendResult,这种是最可靠的 +- **异步发送**:在回调方法里可以得知是否发送成功 +- **单向发送(OneWay)**:最不靠谱的一种发送方式,我们无法保证消息真正可达 + +```java +/** + * {@link org.apache.rocketmq.client.producer.DefaultMQProducer} + */ + +// 同步发送 +public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {} +// 异步发送,sendCallback作为回调 +public void send(Message msg,SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {} +// 单向发送,不关心发送结果,最不靠谱 +public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {} +``` + + + +#### 失败重试 + +发送消息如果失败或者超时了,则会自动重试。默认是重试3次,可以根据api进行更改,比如改为10次: + +```java +producer.setRetryTimesWhenSendFailed(10); +``` + +底层源码逻辑如下: + +```java +/** + * {@link org.apache.rocketmq.client.producer.DefaultMQProducer#sendDefaultImpl(Message, CommunicationMode, SendCallback, long)} + */ + +// 自动重试次数,this.defaultMQProducer.getRetryTimesWhenSendFailed()默认为2,如果是同步发送,默认重试3次,否则重试1次 +int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1; +int times = 0; +for (; times < timesTotal; times++) { + // 选择发送的消息queue + MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName); + if (mqSelected != null) { + try { + // 真正的发送逻辑,sendKernelImpl。 + sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); + switch (communicationMode) { + case ASYNC: + return null; + case ONEWAY: + return null; + case SYNC: + // 如果发送失败了,则continue,意味着还会再次进入for,继续重试发送 + if (sendResult.getSendStatus() != SendStatus.SEND_OK) { + if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) { + continue; + } + } + // 发送成功的话,将发送结果返回给调用者 + return sendResult; + default: + break; + } + } catch (RemotingException e) { + continue; + } catch (...) { + continue; + } + } +} +``` + + + +#### 故障切换 + +假设Broker宕机了,但是生产环境一般都是多M多S的,所以还会有其他Master节点继续提供服务,这也不会影响到我们发送消息,我们消息依然可达。因为比如恰巧发送到broker的时候,broker宕机了,producer收到broker的响应发送失败了,这时候producer会自动重试,这时候宕机的broker就被踢下线了, 所以producer会换一台broker发送消息。 + + + +### Broker存储阶段 + +若想很严格的保证Broker存储消息阶段消息不丢失,则需要如下配置,但是性能肯定远差于默认配置: + +```properties +# master 节点配置 +flushDiskType = SYNC_FLUSH +brokerRole=SYNC_MASTER + +# slave 节点配置 +brokerRole=slave +flushDiskType = SYNC_FLUSH +``` + + + +#### 设置Broker同步刷盘策略 + +**设置Broker同步刷盘策略**。默认情况下,消息只要到了 Broker 端,将会优先保存到内存中,然后立刻返回确认响应给生产者。随后 Broker 定期批量的将一组消息从内存异步刷入磁盘。这种方式减少 I/O 次数,可以取得更好的性能,但是如果发生机器断电,异常宕机等情况,消息还未及时刷入磁盘,就会出现丢失消息的情况。 + +若想保证 Broker 端不丢消息,保证消息的可靠性,我们需要将消息保存机制修改为同步刷盘方式,即消息**存储磁盘成功**,才会返回响应。修改 Broker 端配置如下: + +```properties +# 默认情况为 ASYNC_FLUSH +flushDiskType = SYNC_FLUSH +``` + +若 Broker 未在同步刷盘时间内(**默认为 5s**)完成刷盘,将会返回 `SendStatus.FLUSH_DISK_TIMEOUT` 状态给生产者。 + + + +#### 等待Master和Slave刷盘完 + +等待Master和Slave刷盘完。即使Broker设置了同步刷盘策略,但是Broker刷完盘后磁盘坏了,这会导致盘上的消息全丢了。但是如果即使是1主1从了,但是Master刷完盘后还没来得及同步给Slave就磁盘坏了,这会导致盘上的消息全丢了。所以我们还可以配置不仅是等Master刷完盘就通知Producer,而是等Master和Slave都刷完盘后才去通知Producer说消息ok了。 + +```properties +# 默认为 ASYNC_MASTER +brokerRole=SYNC_MASTER +``` + + + +### 消费阶段 + +消费失败了其实也是消息丢失的一种变体。 + +只有当消费模式为 **MessageModel.CLUSTERING(集群模式)** 时,Broker 才会自动进行重试,对于广播消息是不会重试的。对于一直无法消费成功的消息,RocketMQ 会在达到最大重试次数之后,将该消息投递至死信队列。然后我们需要关注死信队列,并对该死信消息业务做人工的补偿操作。 + +#### 手动ACK确认 + +消费者会先把消息拉取到本地,然后进行业务逻辑,业务逻辑完成后手动进行ack确认,这时候才会真正的代表消费完成。而不是说pull到本地后消息就算消费完了。举个例子 + +```java + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { + try{ + for (MessageExt msg : msgs) { + String str = new String(msg.getBody()); + System.out.println(str); + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } catch(Throwable t){ + log.error("消费异常:{}", msgs, t); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + } + }); +``` + + + +#### 消费异常自动重试 + +- 业务消费方返回 ConsumeConcurrentlyStatus.RECONSUME_LATER +- 业务消费方返回 null +- 业务消费方主动/被动抛出异常 + +针对以上3种情况下,Broker一般会进行重试(默认最大重试16次),RocketMQ 采用了“时间衰减策略”进行消息的重复投递,即重试次数越多,消息消费成功的可能性越小。我们可以在 RocketMQ 的 `broker.conf` 配置文件中配置 Consumer 侧重试次数及时间间隔(**距离第1次发送的时间间隔**), 配置如下: + +```properties + messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h +``` + +消费者客户端,首先判断消费端有没有显式设置最大重试次数 MaxReconsumeTimes, 如果没有,则设置默认重试次数为 16,否则以设置的最大重试次数为准。 + +```java +private int getMaxReconsumeTimes() { + // default reconsume times: 16 + if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) { + return 16; + } else { + return this.defaultMQPushConsumer.getMaxReconsumeTimes(); + } +} +``` + + + +#### 消费超时无线重试 + +如果是消费超时情况,MQ会无限制的发送给消费端。这种情况就是Consumer端没有返回`ConsumeConcurrentlyStatus. CONSUME_SUCCESS`,也没有返回`ConsumeConcurrentlyStatus.RECONSUME_LATER`。 + + + +#### 死信队列 + +死信的处理逻辑: + +- 首先判断消息当前重试次数是否大于等于 16,或者消息延迟级别是否小于 0 +- 只要满足上述的任意一个条件,设置新的 topic(死信 topic)为:**%DLQ%+consumerGroup** +- 进行前置属性的添加 +- 将死信消息投递到上述步骤 2 建立的死信 topic 对应的死信队列中并落盘,使消息持久化 + +最后单独启动一个死信队列的消费者进行消费,然后进行人工干预处理失败的消息。 \ No newline at end of file diff --git a/src/Middleware/308.md b/src/Middleware/308.md new file mode 100644 index 0000000..8d6c8b0 --- /dev/null +++ b/src/Middleware/308.md @@ -0,0 +1,7 @@ +在所有消息系统中消费消息有三种模式:`at-most-once`(最多一次)、`at-least-once`(最少一次)和 `exactly-only-once`(精确仅一次),分布式消息系统都是在三者间取平衡,前两者是可行的并且被广泛使用。 + +- `at-most-once`:消息投递后不论消息是否被消费成功,不会再重复投递,有可能会导致消息未被消费,RocketMQ 未使用该方式 +- `at-lease-once`:消息投递后,消费完成后,向服务器返回 ACK,没有消费则一定不会返回 ACK 消息。由于网络异常、客户端重启等原因,服务器未能收到客户端返回的 ACK,服务器则会再次投递,这就会导致可能重复消费,RocketMQ 通过 ACK 来确保消息至少被消费一次 +- `exactly-only-once`:在分布式系统环境下,如果要实现该模式,巨大的开销不可避免。RocketMQ 没有保证此特性,无法避免消息重复,由业务上进行幂等性处理。必须下面两个条件都满足,才能认为消息是"Exactly Only Once": + - 发送消息阶段,不允许发送重复消息 + - 消费消息阶段,不允许消费重复的消息 \ No newline at end of file diff --git a/src/Middleware/4.md b/src/Middleware/4.md new file mode 100644 index 0000000..0c20b53 --- /dev/null +++ b/src/Middleware/4.md @@ -0,0 +1,13 @@ +Motan使用SPI机制来实现模块间的访问,基于接口和name来获取实现类,降低了模块间的耦合。 + +Motan的SPI的实现在 `motan-core/com/weibo/api/motan/core/extension` 中。组织结构如下: + +```tex +motan-core/com.weibo.api.motan.core.extension + |-Activation:SPI的扩展功能,例如过滤、排序 + |-ActivationComparator:排序比较器 + |-ExtensionLoader:核心,主要负责SPI的扫描和加载 + |-Scope:模式枚举,单例、多例 + |-Spi:注解,作用在接口上,表明这个接口的实现可以通过SPI的形式加载 + |-SpiMeta:注解,作用在具体的SPI接口的实现类上,标注该扩展的名称 +``` \ No newline at end of file diff --git a/src/Middleware/401.md b/src/Middleware/401.md new file mode 100644 index 0000000..4a7014e --- /dev/null +++ b/src/Middleware/401.md @@ -0,0 +1 @@ +**下载地址:**http://www.apache.org/dist/zookeeper/ \ No newline at end of file diff --git a/src/Middleware/402.md b/src/Middleware/402.md new file mode 100644 index 0000000..39e78e6 --- /dev/null +++ b/src/Middleware/402.md @@ -0,0 +1,9 @@ +Zookeeper主要靠其 **分布式数据一致性** 为集群提供 **分布式协调服务**,即指在集群的节点中进行可靠的消息传递,来协调集群的工作。主要具有如下特点: + +- **最终一致性:**Client无论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能 +- **可靠性:**如果一个消息或事物被一台Server接受,那么它将被所有的服务器接受 +- **实时性:**Zookeeper不能保证强一致性,只保证顺序一致性和最终一致性,因此称为**伪实时性**。由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口 +- **原子性:**更新只能成功或者失败,没有中间状态 +- **顺序性:**包括 **全序(Total order)** 和 **因果顺序(Causal order)** + - **全序:**如果消息a在消息b之前发送,则所有Server应该看到相同的结果 + - **因果顺序:**如果消息a在消息b之前发生(a导致了b),并被一起发送,则a始终在b之前被执行 \ No newline at end of file diff --git a/src/Middleware/403.md b/src/Middleware/403.md new file mode 100644 index 0000000..36e913a --- /dev/null +++ b/src/Middleware/403.md @@ -0,0 +1,38 @@ +- **Server(服务端)** + - **Leader(领导者):**负责对所有对ZK状态变更的请求,将状态更新请求进行排序与编号,以保证集群内部消息处理的有序性 + - **Learner(学习者)** + - **Follower(追随者):**用于接收客户请求并向客户端返回结果,在选举过程中参与投票 + - **Observer(观察者):**其作用是为了扩展集群来提供读取速度,可以接收客户端连接,将写请求转发给Leader节点,但不参与投票,只同步Leader状态 +- **Client(客户端):**请求发起方 + +每个Server在工作过程中有三种状态: + +- **LOOKING:**当前Server不知道Leader是谁,正在搜寻 +- **LEADING:**当前Server即为选举出来的Leader +- **FOLLOWING:**Leader已经选举出来,当前Server与之同步 + +![Zookeeper中的角色](images/Middleware/Zookeeper中的角色.jpg) + +Zookeeper集群中,有Leader、Follower和Observer三种角色 + +- **领导者(Leader)**:负责进行投票的发起和决议,更新系统状态 + + Leader服务器是整个ZooKeeper集群工作机制中的核心,其主要工作: + + - 事务请求的唯一调度和处理者,保证集群事务处理的顺序性 + - 集群内部各服务的调度者 + +- **跟随者(Follower)**:用于接收客户端请求并给客户端返回结果,在选主过程中进行投票 + + Follower服务器是ZooKeeper集群状态的跟随者,其主要工作: + + - 处理客户端非事务请求,转发事务请求给Leader服务器 + - 参与事务请求Proposal的投票 + - 参与Leader选举投票 + +- **观察者(Observer)**:可以接受客户端连接,将写请求转发给 leader,但是observer 不参加投票的过程,只是为了扩展系统,提高读取的速度 + + Observer是3.3.0 版本开始引入的一个服务器角色,它充当一个观察者角色——观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来。其工作: + + - 处理客户端的非事务请求,转发事务请求给 Leader 服务器 + - 不参与任何形式的投票 \ No newline at end of file diff --git a/src/Middleware/404.md b/src/Middleware/404.md new file mode 100644 index 0000000..3cb290e --- /dev/null +++ b/src/Middleware/404.md @@ -0,0 +1,10 @@ +Zookeeper 的数据模型: + +- 层次化的目录结构,命名符合常规文件系统规范,类似于Linux +- 每个节点在zookeeper中叫做znode,并且其有一个唯一的路径标识 +- 节点Znode可以包含数据和子节点,但是EPHEMERAL类型的节点不能有子节点 +- Znode中的数据可以有多个版本,比如某一个路径下存有多个数据版本,那么查询这个路径下的数据就需要带上版本 +- 客户端应用可以在节点上设置监视器 +- 节点不支持部分读写,而是一次性完整读写 + +![Zookeeper的数据模型](images/Middleware/Zookeeper的数据模型.jpg) \ No newline at end of file diff --git a/src/Middleware/405.md b/src/Middleware/405.md new file mode 100644 index 0000000..28bebd4 --- /dev/null +++ b/src/Middleware/405.md @@ -0,0 +1,6 @@ +服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。 + +- LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态 +- FOLLOWING:跟随者状态。表明当前服务器角色是Follower +- LEADING:领导者状态。表明当前服务器角色是Leader +- OBSERVING:观察者状态。表明当前服务器角色是Observer \ No newline at end of file diff --git a/src/Middleware/406.md b/src/Middleware/406.md new file mode 100644 index 0000000..de82cbc --- /dev/null +++ b/src/Middleware/406.md @@ -0,0 +1,5 @@ +Zookeeper 有三种运行模式:单机模式、伪集群模式和集群模式。 + +- **单机模式**:这种模式一般适用于开发测试环境,一方面我们没有那么多机器资源,另外就是平时的开发调试并不需要极好的稳定性。 +- **集群模式**:一个 ZooKeeper 集群通常由一组机器组成,一般 3 台以上就可以组成一个可用的 ZooKeeper 集群了。组成 ZooKeeper 集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都会互相保持通信。 +- **伪集群模式**:这是一种特殊的集群模式,即集群的所有服务器都部署在一台机器上。当你手头上有一台比较好的机器,如果作为单机模式进行部署,就会浪费资源,这种情况下,ZooKeeper 允许你在一台机器上通过启动不同的端口来启动多个 ZooKeeper 服务实例,以此来以集群的特性来对外服务。 \ No newline at end of file diff --git a/src/Middleware/407.md b/src/Middleware/407.md new file mode 100644 index 0000000..de82cbc --- /dev/null +++ b/src/Middleware/407.md @@ -0,0 +1,5 @@ +Zookeeper 有三种运行模式:单机模式、伪集群模式和集群模式。 + +- **单机模式**:这种模式一般适用于开发测试环境,一方面我们没有那么多机器资源,另外就是平时的开发调试并不需要极好的稳定性。 +- **集群模式**:一个 ZooKeeper 集群通常由一组机器组成,一般 3 台以上就可以组成一个可用的 ZooKeeper 集群了。组成 ZooKeeper 集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都会互相保持通信。 +- **伪集群模式**:这是一种特殊的集群模式,即集群的所有服务器都部署在一台机器上。当你手头上有一台比较好的机器,如果作为单机模式进行部署,就会浪费资源,这种情况下,ZooKeeper 允许你在一台机器上通过启动不同的端口来启动多个 ZooKeeper 服务实例,以此来以集群的特性来对外服务。 \ No newline at end of file diff --git a/src/Middleware/408.md b/src/Middleware/408.md new file mode 100644 index 0000000..7243f16 --- /dev/null +++ b/src/Middleware/408.md @@ -0,0 +1,28 @@ +**① 节点组成** + +每个znode由4部分组成: + +- **path:**即节点名称,用于存放简单可视化的数据 +- **stat:**即状态信息,描述该znode的版本,权限等信息 +- **data:**与该znode关联的数据 +- **children:**该znode下的子节点 + + + +**② 节点类型** + +- **PERSISTENT(持久化目录节点)** + + 客户端与zookeeper断开连接后,该节点依旧存在 + +- **PERSISTENT_SEQUENTIAL(持久化顺序编号目录节点)** + + 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号 + +- **EPHEMERAL(临时目录节点)** + + 客户端与zookeeper断开连接后,该节点被删除 + +- **EPHEMERAL_SEQUENTIAL(临时顺序编号目录节点)** + + 客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号 \ No newline at end of file diff --git a/src/Middleware/409.md b/src/Middleware/409.md new file mode 100644 index 0000000..08b5499 --- /dev/null +++ b/src/Middleware/409.md @@ -0,0 +1,7 @@ +- **zxid(事务ID号)** + + ZooKeeper状态的每一次改变,都对应着一个递增的`Transaction id`,该id称为zxid。由于zxid的递增性质,如果zxid1小于zxid2,那么zxid1肯定先于zxid2发生。创建任意节点、更新任意节点的数据、删除任意节点,都会导致Zookeeper状态发生改变,从而导致zxid的值增加。 + +- **session(会话连接)** + + 在Client和Server通信之前,首先需要建立连接,该连接称为session。连接建立后,如果发生连接超时、授权失败或显式关闭连接,连接便处于CLOSED状态,此时session结束。 \ No newline at end of file diff --git a/src/Middleware/410.md b/src/Middleware/410.md new file mode 100644 index 0000000..904347e --- /dev/null +++ b/src/Middleware/410.md @@ -0,0 +1,18 @@ +Zookeeper 允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher通知状态和事件类型做出业务上的改变。 + +![Watcher监听机制的工作原理](images/Middleware/Watcher监听机制的工作原理.png) + +- ZooKeeper的Watcher机制主要包括客户端线程、客户端 WatcherManager、Zookeeper服务器三部分 +- 客户端向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中 +- 当zookeeper服务器触发watcher事件后,会向客户端发送通知, 客户端线程从 WatcherManager 中取出对应的 Watcher 对象来执行回调逻辑 + + + +**Watcher特性总结** + +- **「一次性:」** 一个Watch事件是一个一次性的触发器。一次性触发,客户端只会收到一次这样的信息 +- **「异步的:」** Zookeeper服务器发送watcher的通知事件到客户端是异步的,不能期望能够监控到节点每次的变化,Zookeeper只能保证最终的一致性,而无法保证强一致性 +- **「轻量级:」** Watcher 通知非常简单,它只是通知发生了事件,而不会传递事件对象内容 +- **「客户端串行:」** 执行客户端 Watcher 回调的过程是一个串行同步的过程 +- 注册 watcher用getData、exists、getChildren方法 +- 触发 watcher用create、delete、setData方法 \ No newline at end of file diff --git a/src/Middleware/411.md b/src/Middleware/411.md new file mode 100644 index 0000000..8d70f29 --- /dev/null +++ b/src/Middleware/411.md @@ -0,0 +1,8 @@ +![Zookeeper-ZXID](images/Middleware/Zookeeper-ZXID.png) + +ZXID有两部分组成: + +- 任期:完成本次选举后,直到下次选举前,由同一Leader负责协调写入 +- 事务计数器:单调递增,每生效一次写入,计数器加一 + +ZXID的低32位是计数器,所以同一任期内,ZXID是连续的,每个结点又都保存着自身最新生效的ZXID,通过对比新提案的ZXID与自身最新ZXID是否相差“1”,来保证事务严格按照顺序生效的。 \ No newline at end of file diff --git a/src/Middleware/412.md b/src/Middleware/412.md new file mode 100644 index 0000000..c30aeda --- /dev/null +++ b/src/Middleware/412.md @@ -0,0 +1,47 @@ +**① 客户端发起的操作的主要流程** + +Leader可以执行增删改查操作,而Follower只能进行查询操作。所有的更新操作都会被转交给Leader来处理,Leader批准的任务,再发送给Follower去执行来保证和Leader的一致性。由于网络是不稳定的,为了保证执行顺序的一致,所有的任务都会被赋予一个唯一的顺序的编号,一定是按照这个编号来执行任务,保证任务顺序的一致性。 + + + +**② 客户端的请求什么时候才能算处理成功?为什么说集群过半机器宕机后无法再工作?** + +Leader在收到客户端提交过来的任务后,会向集群中所有的Follower发送提案等待Follower的投票,Follower们收到这个提议后,会进行投票,同意或者不同意,Leader会回收Follower的投票,一旦受到过半的投票表示同意,则Leader认为这个提案通过,再发送命令要求所有的Follower都进行这个提案中的任务。由于需要过半的机器同意才能执行任务,所以一旦集群中过半的机器挂掉,整个集群就无法工作了。 + +- **Leader工作流程** + + Leader主要有三个功能: + + - 恢复数据 + - 维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型 + - Learner的消息类型主要有如下四种,根据不同的消息类型,进行不同的处理: + - **PING消息:**指Learner的心跳信息 + - **REQUEST消息:**Follower发送的提议信息,包括写请求及同步请求 + - **ACK消息:**Follower的对提议的回复,超过半数的Follower通过,则commit该提议 + - **REVALIDATE消息:**用来延长SESSION有效时间 + + +- **Follower工作流程** + + Follower主要有四个功能: + + - 向Leader发送消息请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息) + - 接收Leader消息并进行处理 + - 接收Client的请求,如果为写请求,发送给Leader进行投票 + - 返回Client结果 + + Follower的消息循环处理如下几种来自Leader的消息: + + - **PING消息:** 心跳消息 + +- **PROPOSAL消息:**Leader发起的提案,要求Follower投票 + + - **COMMIT消息:**服务器端最新一次提案的信息 + - **UPTODATE消息:**表明同步完成 + - **REVALIDATE消息:**根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息 + - **SYNC消息:**返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新 + + +- **Observer工作流程** + + 对于Observer的流程不再叙述,Observer流程和Follower的唯一不同的地方就是Observer不会参加Leader发起的投票。 \ No newline at end of file diff --git a/src/Middleware/413.md b/src/Middleware/413.md new file mode 100644 index 0000000..c65b202 --- /dev/null +++ b/src/Middleware/413.md @@ -0,0 +1,56 @@ +ZooKeeper并没有完全采用Paxos算法,而是使用一种称为ZooKeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法,简称ZAB协议。ZAB协议分为两种模式: + +- **崩溃恢复模式(Recovery):**当服务初次启动或Leader节点挂了时,系统就会进入恢复模式,直到选出了有合法数量Follower的新Leader,然后新Leader负责将整个系统同步到最新状态 +- **消息广播模式(Boardcast):**ZAB协议中,所有的写请求都由Leader来处理。正常工作状态下,Leader接收请求并通过广播协议来处理(如:广播提议投票、广播命令) + + + +**① 崩溃恢复模式(Recovery)** + +为了使Leader挂了后系统能正常工作,需要解决以下两个问题: + +- 已经被处理的消息不能丢 +- 被丢弃的消息不能再次出现 + + + +**② 消息广播模式(Boardcast)** + +广播的过程实际上是一个简化的二阶段提交过程: + +- Leader 接收到消息请求后,将消息赋予一个全局唯一的 64 位自增 id,叫做:zxid,通过 zxid 的大小比较即可实现因果有序这一特性 +- Leader 通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower +- 当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK +- 当 leader 接收到合法数量的 ACKs 后,leader 就向所有 follower 发送 COMMIT 命令,同事会在本地执行该消息 +- 当 follower 收到消息的 COMMIT 命令时,就会执行该消息 + + + +**总结** + +个人认为 Zab 协议设计的优秀之处有两点,一是简化二阶段提交,提升了在正常工作情况下的性能;二是巧妙地利用率自增序列,简化了异常恢复的逻辑,也很好地保证了顺序处理这一特性。值得注意的是,ZAB提交事务并不像2PC一样需要全部Follower都ACK,只需要得到quorum(超过半数的节点)的ACK就可以了。 + + + +**③ ZAB 的四个阶段** + +- **Leader election(选举阶段):**节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准Leader(只有完成ZAB的四个阶段,准Leader才会成为真正的Leader)。本阶段的目的是就是为了选出一个准Leader,然后进入下一个阶段 +- **Discovery(发现阶段):**在次阶段,Followers跟准Leader进行通信,同步Followers最近接收的事务提议,这个一阶段的主要目的是发现当前大多数节点接收的最新提议 +- **Synchronization(同步阶段):**同步阶段主要是利用Leader前一阶段获得的最新提议历史,同步集群中所有的副本。只有当quorum都同步完成,准Leader才会成为真正的Leader。Follower只会接收zxid比自己的lastZxid大的提议 +- **Broadcast(广播阶段):**到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且Leader可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步 + + + +**④ JAVA版ZAB协议** + +- **Fast Leader Election**:Fast Leader Election + + 前面提到 FLE 会选举拥有最新提议历史(lastZixd最大)的节点作为 leader,这样就省去了发现最新提议的步骤。这是基于拥有最新提议的节点也有最新提交记录的前提。成为 leader 的条件: + + - 选epoch最大的 + - epoch相等,选 zxid 最大的 + - epoch和zxid都相等,选择server id最大的(即配置zoo.cfg中的myid) + +- **Recovery Phase**:这一阶段Follower发送它们的 lastZixd 给Leader,Leader 根据 lastZixd 决定如何同步数据。这里的实现跟前面 Phase 2 有所不同:Follower 收到 TRUNC 指令会中止 L.lastCommittedZxid 之后的提议,收到 DIFF 指令会接收新的提议。 + +- **Broadcast Phase**:暂无 \ No newline at end of file diff --git a/src/Middleware/414.md b/src/Middleware/414.md new file mode 100644 index 0000000..542358d --- /dev/null +++ b/src/Middleware/414.md @@ -0,0 +1,15 @@ +最开始集群启动时,会选择xzid最小的机器作为leader。 + +当Leader崩溃或者Leader失去大多数的Follower,这时候ZK进入恢复模式,恢复模式需要重新选举出一个新的Leader,让所有的Server都恢复到一个正确的状态。ZK的选举算法使用**ZAB协议**: + +- 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server +- 选举线程首先向所有Server发起一次询问(包括自己) +- 选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中 +- 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server +- 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来 + + + +**分析结论** + +要使Leader获得多数Server的支持,则Server总数最好是奇数2n+1,且存活的Server的数目不得少于n+1。因为需要过半存活集群才能工作,所以2n个机器提供的集群可靠性其实和2n-1个机器提供的集群可靠性是一样的。 \ No newline at end of file diff --git a/src/Middleware/415.md b/src/Middleware/415.md new file mode 100644 index 0000000..4bcdcd9 --- /dev/null +++ b/src/Middleware/415.md @@ -0,0 +1,125 @@ +### 单机模式 + +**第一步:安装部署** + +```shell +# 下载解压 +wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.9/zookeeper-3.4.9.tar.gz +tar -zxvf zookeeper-3.4.9.tar.gz + +# 设置全局变量 +vim ~/.bash_profile +# 最后一行加入 +export ZOOKEEPER_HOME=/home/zookeeper/zookeeper-3.4.9 +export PATH=$ZOOKEEPER_HOME/bin:$PATH +# 使之生效 +source ~/.bash_profile + +# 复制配置文件 +cp /home/zookeeper/zookeeper-3.4.9/conf/zoo_sample.cfg /home/zookeeper/zookeeper-3.4.9/conf/zoo.cfg +``` + +**第二步:配置信息** + +```shell +# 心跳间隔 +tickTime=2000 +# 保存数据目录 +dataDir=/home/zookeeper/zookeeper-3.4.9/dataDir +# 保存日志目录 +dataLogDir=/home/zookeeper/zookeeper-3.4.9/dataLogDir +# 客户端连接Zookeeper的端口 +clientPort=2181 +# 允许follower连接并同步到leader的初始化连接时间(心跳倍数),超过则连接失败 +initLimit=5 +# 表示leader和follower之间发送消息时, 请求和应答的最大时间长度 +syncLimit=2 +``` + + + +### 集群模式 + +**第一步:安装部署** + +安装方式参考单机模式。 + +**第二步:配置信息** + +在dataDir根目录下新建myid文件,并向文件myid中写入值(如:1、2、3……) + +```shell +# 1表示当前集群的id号 +echo "1" > myid +``` + +在单机配置情况下,新增下述参数: + +```shell +# 格式:server.X=A:B:C +# X表示myid(范围1~255) +# A是该server所在的IP地址 +# B配置该server和集群中的leader交换消息所使用的端口,即数据同步端口 +# C配置选举leader时所使用的端口 +server.1=10.24.1.62:2888:3888 +server.2=10.24.1.63:2888:3888 +server.3=10.24.1.64:2888:3888 +``` + + + +### 运维命令 + +```shell +# 启动ZK服务器 +./zkServer.sh start +# 使用ZK Client连接指定服务器 +./zkCli.sh -server 127.0.0.1:2181 +# 查看ZK服务状态 +./zkServer.sh status +# 停止ZK服务 +./zkServer.sh stop +# 重启ZK服务 +./zkServer.sh restart +``` + + + +### zoo.cfg配置参数 + +```shell +# 客户端连接server的端口,默认值2181 +clientPort=2181 +# 存储快照文件snapshot的目录 +dataDir=/User/lry/zookeeper/data +# ZK中的最小时间单元,单位为毫秒 +tickTime=5000 +# 事务日志输出目录 +dataLogDir=/User/lry/zookeeper/datalog +# Server端最大允许的请求堆积数,默认值为1000 +globalOutstandingLimit=1000 +# 每个事务日志文件的大小,默认值64M +preAllocSize=64 +# 每进行snapCount次事务日志输出后,触发一次快照,默认值100000,实际代码中是随机范围触发,避免并发情况 +snapCount=100000 +# 单个客户端与单台服务器之间的连接数的限制,是ip级别的,默认是60,如果设置为0,那么表明不作任何限制 +maxClientCnxns=60 +# 对于多网卡的机器,可以为每个IP指定不同的监听端口。默认是所有IP都监听clientPort指定的端口 +clientPortAddress=10.24.22.56 +# Session超时时间限制,如果客户端设置的超时时间不在这个范围,那么会被强制设置为最大或最小时间。默认的Session超时时间是在2*tickTime~20*tickTime这个范围 +minSessionTimeoutmaxSessionTimeout +# Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许F在 initLimit 时间内完成这个工作。 +initLimit +# 在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。如果L发出心跳包在syncLimit之后,还没有从F那里收到响应,那么就认为这个F已经不在线了。 +syncLimit +# 默认情况下,Leader是会接受客户端连接,并提供正常的读写服务。但是,如果你想让Leader专注于集群中机器的协调,那么可以将这个参数设置为no,这样一来,会大大提高写操作的性能 +leaderServes=yes +# server.[myid]=[hostname]:[数据同步和其它通讯端口]:[选举投票通讯] +server.x=[hostname]:nnnnn[:nnnnn] +# 对机器分组和权重设置 +group.x=nnnnn[:nnnnn]weight.x=nnnnn +# Leader选举过程中,打开一次连接的超时时间,默认是5s +cnxTimeout +# 每个节点最大数据量,是默认是1M +jute.maxbuffer +``` \ No newline at end of file diff --git a/src/Middleware/5.md b/src/Middleware/5.md new file mode 100644 index 0000000..a8af3ce --- /dev/null +++ b/src/Middleware/5.md @@ -0,0 +1,20 @@ +Spring 的 SPI 配置文件是一个固定的文件 - `META-INF/spring.factories`,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单: + +```java +// 获取所有factories文件中配置的LoggingSystemFactory +List> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader); +``` + +下面是一段 Spring Boot 中 spring.factories 的配置 + +```properties +# Logging Systems +org.springframework.boot.logging.LoggingSystemFactory=\ +org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\ +org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory + +# PropertySource Loaders +org.springframework.boot.env.PropertySourceLoader=\ +org.springframework.boot.env.PropertiesPropertySourceLoader,\ +org.springframework.boot.env.YamlPropertySourceLoader +``` \ No newline at end of file diff --git a/src/Middleware/501.md b/src/Middleware/501.md new file mode 100644 index 0000000..e9e5752 --- /dev/null +++ b/src/Middleware/501.md @@ -0,0 +1,5 @@ +从功能上,流程可以分为服务启动、建立连接、读取数据、业务处理、发送数据、关闭连接以及关闭服务。整体流程如下所示(图中没有包含关闭的部分): + +![Netty整体流程](images/Middleware/Netty整体流程.png) + +![Netty线程模型](images/Middleware/Netty线程模型.png) \ No newline at end of file diff --git a/src/Middleware/502.md b/src/Middleware/502.md new file mode 100644 index 0000000..806363a --- /dev/null +++ b/src/Middleware/502.md @@ -0,0 +1,85 @@ +Netty中Reactor线程和worker线程所处理的事件: + +1、Server端NioEventLoop处理的事件: + +![Server端NioEventLoop处理的事件](images/Middleware/Server端NioEventLoop处理的事件.png) + +2、Client端NioEventLoop处理的事件 + +![Client端NioEventLoop处理的事件](images/Middleware/Client端NioEventLoop处理的事件.png) + + + +### 服务启动 + +服务启动时,以 example 代码中的 EchoServer 为例,启动的过程以及相应的源码类如下: + +1. `EchoServer#new NioEventLoopGroup(1)->NioEventLoop#provider.openSelector()` : 创建 selector +2. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()-> channelFactory.newChannel() / init(channel)` : 创建 serverSocketChannel 以及初始化 +3. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()-> config().group().register(channel)` :从 boss group 中选择一个 NioEventLoop 开始注册 serverSocketChannel +4. `EchoServer#b.bind(PORT).sync->AbstractBootStrap#doBind()->initAndRegister()->config().group().register(channel)->AbstractChannel#register0(promise)->AbstractNioChannel#javaChannel().register(eventLoop().unwrappedSelector(), 0, this)` : 将 server socket channel 注册到选择的 NioEventLoop 的 selector +5. `EchoServer#b.bind(PORT).sync()->AbstractBootStrap#doBind()->doBind0()->AbstractChannel#doBind(localAddress)->NioServerSocketChannel#javaChannel().bind(localAddress, config.getBacklog())` : 绑定地址端口开始启动 +6. `EchoServer#b.bind(PORT).sync()->AbstractBootStrap#doBind()->doBind0()->AbstractChannel#pipeline.fireChannelActive()->AbstractNioChannel#selectionKey.interestOps(interestOps|readInterestOp)`: 注册 OP_READ 事件 + +上述启动流程中,1、2、3 由我们自己的线程执行,即mainThread,4、5、6 是由Boss Thread执行。相应时序图如下: +![Netty流程-服务启动](images/Middleware/Netty流程-服务启动.jpg) + + + +### 建立连接 + +服务启动后便是建立连接的过程了,相应过程及源码类如下: + +1. `NioEventLoop#run()->processSelectedKey()` NioEventLoop 中的 selector 轮询创建连接事件(OP_ACCEPT) +2. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#read->NioServerSocketChannel#doReadMessages()->SocketUtil#accept(serverSocketChannel)` 创建 socket channel +3. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)` 从worker group 中选择一个 NioEventLoop 开始注册 socket channel +4. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)->AbstractChannel#register0(promise)-> AbstractNioChannel#javaChannel().register(eventLoop().unwrappedSelector(), 0, this)` 将 socket channel 注册到选择的 NioEventLoop 的 selector +5. `NioEventLoop#run()->processSelectedKey()->AbstractNioMessageChannel#fireChannelRead->ServerBootstrap#ServerBootstrapAcceptor#channelRead-> childGroup.register(child)->AbstractChannel#pipeline.fireChannelActive()-> AbstractNioChannel#selectionKey.interestOps(interestOps | readInterestOp)` 注册 OP_ACCEPT 事件 + +同样,上述流程中 1、2、3 的执行仍由 Boss Thread 执行,直到 4、5 由具体的 Work Thread 执行。 +![Netty流程-建立连接](images/Middleware/Netty流程-建立连接.jpg) + + + +### 读写与业务处理 + +连接建立完毕后是具体的读写,以及业务处理逻辑。以 EchoServerHandler 为例,读取数据后会将数据传播出去供业务逻辑处理,此时的 EchoServerHandler 代表我们的业务逻辑,而它的实现也非常简单,就是直接将数据写回去。我们将这块看成一个整条,流程如下: + +1. `NioEventLoop#run()->processSelectedKey() NioEventLoop 中的 selector` 轮询创建读取事件(OP_READ) +2. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()`nioSocketChannel 开始读取数据 +3. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->pipeline.fireChannelRead(byteBuf)`把读取到的数据传播出去供业务处理 +4. `AbstractNioByteChannel#pipeline.fireChannelRead->EchoServerHandler#channelRead`在这个例子中即 EchoServerHandler 的执行 +5. `EchoServerHandler#write->ChannelOutboundBuffer#addMessage` 调用 write 方法 +6. `EchoServerHandler#flush->ChannelOutboundBuffer#addFlush` 调用 flush 准备数据 +7. `EchoServerHandler#flush->NioSocketChannel#doWrite` 调用 flush 发送数据 + +在这个过程中读写数据都是由 Work Thread 执行的,但是业务处理可以由我们自定义的线程池来处理,并且一般我们也是这么做的,默认没有指定线程的情况下仍然由 Work Thread 代为处理。 +![Netty流程-读写与业务处理](images/Middleware/Netty流程-读写与业务处理.jpg) + + + +### 关闭连接 + +服务处理完毕后,单个连接的关闭是什么样的呢? + +1. `NioEventLoop#run()->processSelectedKey()` NioEventLoop 中的 selector 轮询创建读取事件(OP_READ),这里关闭连接仍然是读取事件 +2. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)`当字节<0 时开始执行关闭 nioSocketChannel +3. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->AbstractNioChannel#doClose()` 关闭 socketChannel +4. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->outboundBuffer.failFlushed/close` 清理消息:不接受新信息,fail 掉所有 queue 中消息 +5. `NioEventLoop#run()->processSelectedKey()->AbstractNioByteChannel#read()->closeOnRead(pipeline)->AbstractChannel#close->fireChannelInactiveAndDeregister->AbstractNioChannel#doDeregister eventLoop().cancel(selectionKey())` 关闭多路复用器的 key + +时序图如下: +![Netty流程-关闭连接.jpg](images/Middleware/Netty流程-关闭连接.jpg) + + + +### 关闭服务 + +最后是关闭整个 Netty 服务: + +1. `NioEventLoop#run->closeAll()->selectionKey.cancel/channel.close` 关闭 channel,取消 selectionKey +2. `NioEventLoop#run->confirmShutdown->cancelScheduledTasks` 取消定时任务 +3. `NioEventLoop#cleanup->selector.close()` 关闭 selector + +时序图如下,为了好画将 NioEventLoop 拆成了 2 块: +![Netty流程-关闭服务.jpg](images/Middleware/Netty流程-关闭服务.jpg) \ No newline at end of file diff --git a/src/Middleware/503.md b/src/Middleware/503.md new file mode 100644 index 0000000..f81154b --- /dev/null +++ b/src/Middleware/503.md @@ -0,0 +1,3 @@ +### 更多连接 + +### 更高QPS \ No newline at end of file diff --git a/src/Middleware/504.md b/src/Middleware/504.md new file mode 100644 index 0000000..a0da46b --- /dev/null +++ b/src/Middleware/504.md @@ -0,0 +1,42 @@ +Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。 + +### 单线程Reactor线程模型 + +下图演示了单线程reactor线程模型,之所以称之为单线程,还是因为只有一个accpet Thread接受任务,之后转发到reactor线程中进行处理。两个黄色框表示的是Reactor Thread Group,里面有多个Reactor Thread。一个Reactor Thread Group中的Reactor Thread功能都是相同的,例如第一个黄色框中的Reactor Thread都是处理拆分后的任务的第一阶段,第二个黄色框中的Reactor Thread都是处理拆分后的任务的第二步骤。任务具体要怎么拆分,要结合具体场景,下图只是演示作用。**一般来说,都是以比较耗时的操作(例如IO)为切分点**。 + +![单线程reactor线程模型](images/Middleware/单线程reactor线程模型.png) + +特别的,如果我们在任务处理的过程中,不划分为多个阶段进行处理的话,那么单线程reactor线程模型就退化成了并行工作和模型。**事实上,可以认为并行工作者模型,就是单线程reactor线程模型的最简化版本。** + + + +### 多线程Reactor线程模型 + +所谓多线程reactor线程模型,无非就是有多个accpet线程,如下图中的虚线框中的部分。 + +![多线程reactor线程模型](images/Middleware/多线程reactor线程模型.png) + + + +### 混合型Reactor线程模型 + +混合型reactor线程模型,实际上最能体现reactor线程模型的本质: + +- 将任务处理切分成多个阶段进行,每个阶段处理完自己的部分之后,转发到下一个阶段进行处理。不同的阶段之间的执行是异步的,可以认为每个阶段都有一个独立的线程池。 +- 不同的类型的任务,有着不同的处理流程,划分时需要划分成不同的阶段。如下图蓝色是一种任务、绿色是另一种任务,两种任务有着不同的执行流程 + +![混合型reactor线程模型](images/Middleware/混合型reactor线程模型.png) + + + +### Netty-Reactor线程模型 + +![Netty-Reactor](images/Middleware/Netty-Reactor.png) + +图中大致包含了5个步骤,而我们编写的服务端代码中可能并不能完全体现这样的步骤,因为Netty将其中一些步骤的细节隐藏了,笔者将会通过图形分析与源码分析相结合的方式帮助读者理解这五个步骤。这个五个步骤可以按照以下方式简要概括: + +- 设置服务端ServerBootStrap启动参数 +- 通过ServerBootStrap的bind方法启动服务端,bind方法会在parentGroup中注册NioServerScoketChannel,监听客户端的连接请求 +- Client发起连接CONNECT请求,parentGroup中的NioEventLoop不断轮循是否有新的客户端请求,如果有,ACCEPT事件触发 +- ACCEPT事件触发后,parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到childGroup中 +- childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理 \ No newline at end of file diff --git a/src/Middleware/505.md b/src/Middleware/505.md new file mode 100644 index 0000000..59c3458 --- /dev/null +++ b/src/Middleware/505.md @@ -0,0 +1,93 @@ +时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代表一段时间。并用一个链表保存在该格子上的计划任务,同时一个指针随着时间一格一格转动,并执行相应格子中的所有到期任务。任务通过时间取模决定放入那个格子。 + +![HashedWheelTimer](images/Middleware/HashedWheelTimer.png) + +在网络通信中管理上万的连接,每个连接都有超时任务,如果为每个任务启动一个Timer超时器,那么会占用大量资源。为了解决这个问题,可用Netty工具类HashedWheelTimer。 + +Netty 的时间轮 `HashedWheelTimer` 给出了一个**粗略的定时器实现**,之所以称之为粗略的实现是**因为该时间轮并没有严格的准时执行定时任务**,而是在每隔一个时间间隔之后的时间节点执行,并执行当前时间节点之前到期的定时任务。 + +当然具体的定时任务的时间执行精度可以通过调节 HashedWheelTimer 构造方法的时间间隔的大小来进行调节,在大多数网络应用的情况下,由于 IO 延迟的存在,并**不会严格要求具体的时间执行精度**,所以默认的 100ms 时间间隔可以满足大多数的情况,不需要再花精力去调节该时间精度。 + + + +**HashedWheelTimer的特点** + +- 从源码分析可以看出,其实 HashedWheelTimer 的时间精度并不高,误差能够在 100ms 左右,同时如果任务队列中的等待任务数量过多,可能会产生更大的误差 +- 但是 HashedWheelTimer 能够处理非常大量的定时任务,且每次定位到要处理任务的候选集合链表只需要 O(1) 的时间,而 Timer 等则需要调整堆,是 O(logN) 的时间复杂度 +- HashedWheelTimer 本质上是`模拟了时间的轮盘`,将大量的任务拆分成了一个个的小任务列表,能够有效`节省 CPU 和线程资源` + + + +### 源码解读 + +```java +public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, + int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts) { + ...... +} +``` + +- `threadFactory`:自定义线程工厂,用于创建线程对象 +- `tickDuration`:间隔多久走到下一槽(相当于时钟走一格) +- `unit`:定义tickDuration的时间单位 +- `ticksPerWheel`:一圈有多个槽 +- `leakDetection`:是否开启内存泄漏检测 +- `maxPendingTimeouts`:最多待执行的任务个数。0或负数表示无限制 + + + +### 优缺点 + +- **优点** + - 可以添加、删除、取消定时任务 + - 能高效的处理大批定时任务 +- **缺点** + - 对内存要求较高,占用较高的内存 + - 时间精度要求不高 + + + +### 定时任务方案 + +目前主流的一些定时任务方案: + +- Timer +- ScheduledExecutorService +- ThreadPoolTaskScheduler(基于ScheduledExecutorService) +- Netty的schedule(用到了PriorityQueue) +- Netty的HashedWheelTimer(时间轮) +- Kafka的TimingWheel(层级时间轮) + + + +### 使用案例 + +```java +// 构造一个 Timer 实例 +Timer timer = new HashedWheelTimer(); + +// 提交一个任务,让它在 5s 后执行 +Timeout timeout1 = timer.newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) { + System.out.println("5s 后执行该任务"); + } +}, 5, TimeUnit.SECONDS); + +// 再提交一个任务,让它在 10s 后执行 +Timeout timeout2 = timer.newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) { + System.out.println("10s 后执行该任务"); + } +}, 10, TimeUnit.SECONDS); + +// 取消掉那个 5s 后执行的任务 +if (!timeout1.isExpired()) { + timeout1.cancel(); +} + +// 原来那个 5s 后执行的任务,已经取消了。这里我们反悔了,我们要让这个任务在 3s 后执行 +// 我们说过 timeout 持有上、下层的实例,所以下面的 timer 也可以写成 timeout1.timer() +timer.newTimeout(timeout1.task(), 3, TimeUnit.SECONDS); +``` \ No newline at end of file diff --git a/src/Middleware/506.md b/src/Middleware/506.md new file mode 100644 index 0000000..462ff3b --- /dev/null +++ b/src/Middleware/506.md @@ -0,0 +1,311 @@ +### 工作流程 + +ByteBuf维护两个不同的索引:`读索引(readerIndex)` 和 `写索引(writerIndex)` 。如下图所示: + +![ByteBuf工作流程](images/Middleware/ByteBuf工作流程.png) + +- `ByteBuf` 维护了 `readerIndex` 和 `writerIndex` 索引 +- 当 `readerIndex > writerIndex` 时,则抛出 `IndexOutOfBoundsException` +- `ByteBuf`容量 = `writerIndex` +- `ByteBuf` 可读容量 = `writerIndex` - `readerIndex` +- `readXXX()` 和 `writeXXX()` 方法将会推进其对应的索引。自动推进 +- `getXXX()` 和 `setXXX()` 方法将对 `writerIndex` 和 `readerIndex` 无影响 + + + +### 使用模式 + +ByteBuf本质是一个由不同的索引分别控制读访问和写访问的字节数组。ByteBuf共有三种模式:`堆缓冲区模式(Heap Buffer)`、`直接缓冲区模式(Direct Buffer)` 和 `复合缓冲区模式(Composite Buffer)`。 + +#### 堆缓冲区模式(Heap Buffer) + +堆缓冲区模式又称为`支撑数组(backing array)`。将数据存放在JVM的堆空间,通过将数据存储在数组中实现。 + +- **优点**:由于数据存储在Jvm堆中可以快速创建和快速释放,并且提供了数组直接快速访问的方法 +- **缺点**:每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区 + +```java +public static void heapBuffer() { + // 创建Java堆缓冲区 + ByteBuf heapBuf = Unpooled.buffer(); + if (heapBuf.hasArray()) { // 是数组支撑 + byte[] array = heapBuf.array(); + int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); + int length = heapBuf.readableBytes(); + handleArray(array, offset, length); + } +} +``` + + + +#### 直接缓冲区模式(Direct Buffer) + +Direct Buffer属于堆外分配的直接内存,不会占用堆的容量。适用于套接字传输过程,避免了数据从内部缓冲区拷贝到直接缓冲区的过程,性能较好。对于涉及大量I/O的数据读写,建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer。 + +- **优点**: 使用Socket传递数据时性能很好,避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能 +- **缺点**: 相对于堆缓冲区而言,Direct Buffer分配内存空间和释放更为昂贵 + +```java +public static void directBuffer() { + ByteBuf directBuf = Unpooled.directBuffer(); + if (!directBuf.hasArray()) { + int length = directBuf.readableBytes(); + byte[] array = new byte[length]; + directBuf.getBytes(directBuf.readerIndex(), array); + handleArray(array, 0, length); + } +} +``` + + + +#### 复合缓冲区模式(Composite Buffer) + +Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图,可以根据需要添加和删除不同类型的ByteBuf。 + +- Composite Buffer是一个组合视图。它提供一种访问方式让使用者自由组合多个ByteBuf,避免了拷贝和分配新的缓冲区 +- Composite Buffer不支持访问其支撑数组。因此如果要访问,需要先将内容拷贝到堆内存中,再进行访问 + +下图是将两个ByteBuf:头部 + Body 组合在一起,没有进行任何复制过程。仅仅创建了一个视图: + +![CompositeBuffer](images/Middleware/CompositeBuffer.png) + +```java +public static void byteBufComposite() { + // 复合缓冲区,只是提供一个视图 + CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); + ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct + ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct + messageBuf.addComponents(headerBuf, bodyBuf); + messageBuf.removeComponent(0); // remove the header + for (ByteBuf buf : messageBuf) { + System.out.println(buf.toString()); + } +} +``` + + + +### 字节级操作 + +#### 随机访问索引 + +ByteBuf的索引与普通的Java字节数组一样。第一个字节的索引是0,最后一个字节索引总是capacity()-1。访问方式如下: + +- readXXX()和writeXXX()方法将会推进其对应的索引readerIndex和writerIndex。自动推进 +- getXXX()和setXXX()方法用于访问数据,对writerIndex和readerIndex无影响 + +```java +public static void byteBufRelativeAccess() { + ByteBuf buffer = Unpooled.buffer(); // get reference form somewhere + for (int i = 0; i < buffer.capacity(); i++) { + byte b = buffer.getByte(i); // 不改变readerIndex值 + System.out.println((char) b); + } +} +``` + + + +#### 顺序访问索引 + +Netty的ByteBuf同时具有读索引和写索引,但JDK的ByteBuffer只有一个索引,所以JDK需要调用flip()方法在读模式和写模式之间切换。ByteBuf被读索引和写索引划分成3个区域:**可丢弃字节区域**、**可读字节区域** 和 **可写字节区域** 。 + +![ByteBuf顺序访问索引](images/Middleware/ByteBuf顺序访问索引.png) + + + +#### 可丢弃字节区域 + +可丢弃字节区域是指:[0,readerIndex)之间的区域。可调用discardReadBytes()方法丢弃已经读过的字节。 + +- discardReadBytes()效果 ----- 将可读字节区域(CONTENT)[readerIndex, writerIndex)往前移动readerIndex位,同时修改读索引和写索引 +- discardReadBytes()方法会移动可读字节区域内容(CONTENT)。如果频繁调用,会有多次数据复制开销,对性能有一定的影响 + + + +#### 可读字节区域 + +可读字节区域是指:[readerIndex, writerIndex)之间的区域。任何名称以read和skip开头的操作方法,都会改变readerIndex索引。 + + + +#### 可写字节区域 + +可写字节区域是指:[writerIndex, capacity)之间的区域。任何名称以write开头的操作方法都将改变writerIndex的值。 + + + +#### 索引管理 + +- markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先备份当前的readerIndex,resetReaderIndex()则是将刚刚备份的readerIndex恢复回来。常用于dump ByteBuf的内容,又不想影响原来ByteBuf的readerIndex的值 +- readerIndex(int) ----- 设置readerIndex为固定的值 +- writerIndex(int) ----- 设置writerIndex为固定的值 +- clear() ----- 效果是: readerIndex=0, writerIndex(0)。不会清除内存 +- 调用clear()比调用discardReadBytes()轻量的多。仅仅重置readerIndex和writerIndex的值,不会拷贝任何内存,开销较小 + + + +#### 查找操作(indexOf) + +查找ByteBuf指定的值。类似于,String.indexOf("str")操作 + +- 最简单的方法 —— indexOf() +- 利用ByteProcessor作为参数来查找某个指定的值 + +```java +public static void byteProcessor() { + ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere + // 使用indexOf()方法来查找 + buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8); + // 使用ByteProcessor查找给定的值 + int index = buffer.forEachByte(ByteProcessor.FIND_CR); +} +``` + + + +#### 派生缓冲——视图 + +派生缓冲区为ByteBuf提供了一个访问的视图。视图仅仅提供一种访问操作,不做任何拷贝操作。下列方法,都会呈现给使用者一个视图,以供访问: + +- duplicate() +- slice() +- slice(int, int) +- Unpooled.unmodifiableBuffer(...) +- Unpooled.wrappedBuffer(...) +- order(ByteOrder) +- readSlice(int) + +**理解** + +- 上面的6中方法,都会返回一个新的ByteBuf实例,具有自己的读索引和写索引。但是,其内部存储是与原对象是共享的。这就是视图的概念 +- 请注意:如果你修改了这个新的ByteBuf实例的具体内容,那么对应的源实例也会被修改,因为其内部存储是共享的 +- 如果需要拷贝现有缓冲区的真实副本,请使用copy()或copy(int, int)方法 +- 使用派生缓冲区,避免了复制内存的开销,有效提高程序的性能 + +```java +public static void byteBufSlice() { + Charset utf8 = Charset.forName("UTF-8"); + ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); + ByteBuf sliced = buf.slice(0, 15); + System.out.println(sliced.toString(utf8)); + buf.setByte(0, (byte)'J'); + assert buf.getByte(0) == sliced.getByte(0); // return true +} + +public static void byteBufCopy() { + Charset utf8 = Charset.forName("UTF-8"); + ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); + ByteBuf copy = buf.copy(0, 15); + System.out.println(copy.toString(utf8)); + buf.setByte(0, (byte)'J'); + assert buf.getByte(0) != copy.getByte(0); // return true +} +``` + + + +#### 读/写操作 + +如上文所提到的,有两种类别的读/写操作: + +- get()和set()操作 ----- 从给定的索引开始,并且保持索引不变 +- read()和write()操作 ----- 从给定的索引开始,并且根据已经访问过的字节数对索引进行访问 +- 下图给出get()操作API,对于set()操作、read()操作和write操作可参考书籍或API + +![ByteBuf-get](images/Middleware/ByteBuf-get.png) + + + +#### 更多操作 + +![ByteBuf-更多操作](images/Middleware/ByteBuf-更多操作.png) + +下面的两个方法操作字面意思较难理解,给出解释: + +- **hasArray()**:如果ByteBuf由一个字节数组支撑,则返回true。通俗的讲:ByteBuf是堆缓冲区模式,则代表其内部存储是由字节数组支撑的 +- **array()**:如果ByteBuf是由一个字节数组支撑泽返回数组,否则抛出UnsupportedOperationException异常。也就是,ByteBuf是堆缓冲区模式 + + + +### ByteBuf分配 + +创建和管理ByteBuf实例的多种方式:**按序分配(ByteBufAllocator)**、**Unpooled缓冲区** 和 **ByteBufUtil类**。 + +#### 按序分配:ByteBufAllocator接口 + +Netty通过接口ByteBufAllocator实现了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator: + +- `ctx.channel().alloc().buffer()`:本质就是ByteBufAllocator.DEFAULT +- `ByteBufAllocator.DEFAULT.buffer()`:返回一个基于堆或者直接内存存储的Bytebuf。默认是堆内存 +- `ByteBufAllocator.DEFAULT`:有两种类型: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。对于Java程序,默认使用PooledByteBufAllocator(池化)。对于安卓,默认使用UnpooledByteBufAllocator(非池化) +- 可以通过BootStrap中的Config为每个Channel提供独立的ByteBufAllocator实例 + +![img](images/Middleware/ByteBufAllocator.png) + +解释: + +- 上图中的buffer()方法,返回一个基于堆或者直接内存存储的Bytebuf ----- 缺省是堆内存。源码: AbstractByteBufAllocator() { this(false); } +- ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。默认是池化(PooledByteBufAllocator.DEFAULT) + + + +#### Unpooled缓冲区——非池化 + +Unpooled提供静态的辅助方法来创建未池化的ByteBuf。 + +![Unpooled缓冲区](images/Middleware/Unpooled缓冲区.png) + +注意: + +- 上图的buffer()方法,返回一个未池化的基于堆内存存储的ByteBuf +- wrappedBuffer() ----- 创建一个视图,返回一个包装了给定数据的ByteBuf。非常实用 + +创建ByteBuf代码: + +```java + public void createByteBuf(ChannelHandlerContext ctx) { + // 1. 通过Channel创建ByteBuf + ByteBuf buf1 = ctx.channel().alloc().buffer(); + // 2. 通过ByteBufAllocator.DEFAULT创建 + ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(); + // 3. 通过Unpooled创建 + ByteBuf buf3 = Unpooled.buffer(); +} +``` + + + +#### ByteBufUtil类 + +ByteBufUtil类提供了用于操作ByteBuf的静态的辅助方法: hexdump()和equals + +- hexdump():以十六进制的表示形式打印ByteBuf的内容。非常有价值 +- equals():判断两个ByteBuf实例的相等性 + + + +### 引用计数 + +Netty4.0版本中为ButeBuf和ButeBufHolder引入了引用计数技术。请区别引用计数和可达性分析算法(jvm垃圾回收) + +- 谁负责释放:一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放 +- buffer.release():引用计数减1 +- buffer.retain():引用计数加1 +- buffer.refCnt():返回当前对象引用计数值 +- buffer.touch():记录当前对象的访问位置,主要用于调试 +- 引用计数并非仅对于直接缓冲区(direct Buffer)。ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值 + +```java +public static void releaseReferenceCountedObject(){ + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + // 引用计数加1 + buffer.retain(); + // 输出引用计数 + buffer.refCnt(); + // 引用计数减1 + buffer.release(); +} +``` \ No newline at end of file diff --git a/src/Middleware/507.md b/src/Middleware/507.md new file mode 100644 index 0000000..87d24e7 --- /dev/null +++ b/src/Middleware/507.md @@ -0,0 +1,89 @@ +**Netty** 的`Zero-copy` 体现在如下几个个方面: + +- **通过CompositeByteBuf实现零拷贝**:Netty提供了`CompositeByteBuf` 类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝 +- **通过wrap操作实现零拷贝**:通过`wrap`操作,可以将byte[]、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象,进而避免了拷贝操作 +- 通过slice操作实现零拷贝:ByteBuf支持`slice`操作,可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝 +- **通过FileRegion实现零拷贝**:通过 `FileRegion` 包装的`FileChannel.tranferTo` 实现文件传输,可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题 + +### 零拷贝操作 + +#### 通过CompositeByteBuf实现零拷贝 + +![CompositeByteBuf实现零拷贝](images/Middleware/CompositeByteBuf实现零拷贝.png) + +```java +ByteBuf header = null; +ByteBuf body = null; + +// 传统合并header和body:两次额外的数据拷贝 +ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes()); +allBuf.writeBytes(header); +allBuf.writeBytes(body); + +// 合并header和body:内部这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体 +CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); +compositeByteBuf.addComponents(true, header, body); +// 底层封装了 CompositeByteBuf 操作 +ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body); +``` + + + +#### 通过wrap操作实现零拷贝 + +```java +byte[] bytes = null; + +// 传统方式:直接将byte[]数组拷贝到ByteBuf中 +ByteBuf byteBuf = Unpooled.buffer(); +byteBuf.writeBytes(bytes); + +// wrap方式:将bytes包装成为一个UnpooledHeapByteBuf对象, 包装过程中, 是不会有拷贝操作的 +// 即最后我们生成的生成的ByteBuf对象是和bytes数组共用了同一个存储空间, 对bytes的修改也会反映到ByteBuf对象中 +ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); + +``` + + + +#### 通过slice操作实现零拷贝 + +slice 操作和 wrap 操作刚好相反,`Unpooled.wrappedBuffer` 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 **ByteBuf **`切片` 为多个共享一个存储区域的 ByteBuf 对象. ByteBuf 提供了两个 slice 操作方法: + +```java +public ByteBuf slice(); +public ByteBuf slice(int index, int length); +``` + +不带参数的`slice`方法等同于`buf.slice(buf.readerIndex(), buf.readableBytes())` 调用, 即返回 buf 中可读部分的切片. 而 `slice(int index, int length)`方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片。 + +用 `slice` 方法产生 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已,即: + +![slice操作实现零拷贝](images/Middleware/slice操作实现零拷贝.png) + + + +#### 通过FileRegion实现零拷贝 + +Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 **Java NIO** `FileChannel.transfer` 的零拷贝功能。当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 **Channel** 中, 而不需要像传统的做法: 拷贝文件内容到临时 **buffer**, 然后再将 **buffer** 写入 **Channel.** 通过这样的零拷贝操作, 无疑对传输大文件很有帮助。 + + + +### 传统IO的流程 + +![传统IO的流程Copy](images/Middleware/传统IO的流程Copy.png) + +![传统IO的流程](images/Middleware/传统IO的流程.png) + +- **「第一步」**:将文件通过 **「DMA」** 技术从磁盘中拷贝到内核缓冲区 +- **「第二步」**:将文件从内核缓冲区拷贝到用户进程缓冲区域中 +- **「第三步」**:将文件从用户进程缓冲区中拷贝到 socket 缓冲区中 +- **「第四步」**:将socket缓冲区中的文件通过 **「DMA」** 技术拷贝到网卡 + + + +### 零拷贝整体流程图 + +![零拷贝CPU](images/Middleware/零拷贝CPU.png) + +![零拷贝整体流程图](images/Middleware/零拷贝整体流程图.png) \ No newline at end of file diff --git a/src/Middleware/508.md b/src/Middleware/508.md new file mode 100644 index 0000000..e74d2eb --- /dev/null +++ b/src/Middleware/508.md @@ -0,0 +1,111 @@ +### 粘包拆包图解 + +![CP粘包拆包图解](images/Middleware/CP粘包拆包图解.png) + +假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下几种情况: + +- 服务端分两次读取到两个独立的数据包,分别是D1和D2,没有粘包和拆包 +- 服务端一次接收到了两个数据包,D1和D2粘在一起,发生粘包 +- 服务端分两次读取到数据包,第一次读取到了完整D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,发生拆包 +- 服务端分两次读取到数据包,第一次读取到部分D1包,第二次读取到剩余的D1包和全部的D2包 +- 当TCP缓存再小一点的话,会把D1和D2分别拆成多个包发送 + + + +### 产生原因 + +产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小: + +- 如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题 +- 如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送 + + + +### 解决方案 + +#### 固定长度 + +对于使用固定长度的粘包和拆包场景,可以使用: + +- `FixedLengthFrameDecoder`:每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。 + +```java + @Override +protected void initChannel(SocketChannel ch) throws Exception { + // 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20 + ch.pipeline().addLast(new FixedLengthFrameDecoder(20)); + // 将前一步解码得到的数据转码为字符串 + ch.pipeline().addLast(new StringDecoder()); + // 这里FixedLengthFrameEncoder是我们自定义的,用于将长度不足20的消息进行补全空格 + ch.pipeline().addLast(new FixedLengthFrameEncoder(20)); + // 最终的数据处理 + ch.pipeline().addLast(new EchoServerHandler()); +} +``` + + + +#### 指定分隔符 + +对于通过分隔符进行粘包和拆包问题的处理,Netty提供了两个编解码的类: + +- `LineBasedFrameDecoder`:通过换行符,即`\n`或者`\r\n`对数据进行处理 +- `DelimiterBasedFrameDecoder`:通过用户指定的分隔符对数据进行粘包和拆包处理 + +```java +@Override +protected void initChannel(SocketChannel ch) throws Exception { + String delimiter = "_$"; + // 将delimiter设置到DelimiterBasedFrameDecoder中,经过该解码一器进行处理之后,源数据将会 + // 被按照_$进行分隔,这里1024指的是分隔的最大长度,即当读取到1024个字节的数据之后,若还是未 + // 读取到分隔符,则舍弃当前数据段,因为其很有可能是由于码流紊乱造成的 + ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, + Unpooled.wrappedBuffer(delimiter.getBytes()))); + // 将分隔之后的字节数据转换为字符串数据 + ch.pipeline().addLast(new StringDecoder()); + // 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符 + ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter)); + // 最终处理数据并且返回响应的handler + ch.pipeline().addLast(new EchoServerHandler()); +} +``` + + + +#### 数据包长度字段 + +处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。 + +- `LengthFieldBasedFrameDecoder`:按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据。解码过程如下图所示: + + ![LengthFieldBasedFrameDecoder](images/Middleware/LengthFieldBasedFrameDecoder.png) + +- `LengthFieldPrepender`:在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。编码过程如下图所示: + + ![LengthFieldPrepender](images/Middleware/LengthFieldPrepender.png) + +```java +@Override +protected void initChannel(SocketChannel ch) throws Exception { + // 这里将LengthFieldBasedFrameDecoder添加到pipeline的首位,因为其需要对接收到的数据 + // 进行长度字段解码,这里也会对数据进行粘包和拆包处理 + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2)); + // LengthFieldPrepender是一个编码器,主要是在响应字节数据前面添加字节长度字段 + ch.pipeline().addLast(new LengthFieldPrepender(2)); + // 对经过粘包和拆包处理之后的数据进行json反序列化,从而得到User对象 + ch.pipeline().addLast(new JsonDecoder()); + // 对响应数据进行编码,主要是将User对象序列化为json + ch.pipeline().addLast(new JsonEncoder()); + // 处理客户端的请求的数据,并且进行响应 + ch.pipeline().addLast(new EchoServerHandler()); + } +``` + + + +#### 自定义粘包拆包器 + +可以通过实现`MessageToByteEncoder`和`ByteToMessageDecoder`来实现自定义粘包和拆包处理的目的。 + +- `MessageToByteEncoder`:作用是将响应数据编码为一个ByteBuf对象 +- `ByteToMessageDecoder`:将接收到的ByteBuf数据转换为某个对象数据 \ No newline at end of file diff --git a/src/Middleware/509.md b/src/Middleware/509.md new file mode 100644 index 0000000..2c452c9 --- /dev/null +++ b/src/Middleware/509.md @@ -0,0 +1,5 @@ +- **IO线程模型**:同步非阻塞,用最少的资源做更多的事 +- **内存零拷贝**:尽量减少不必要的内存拷贝,实现了更高效率的传输 +- **内存池设计**:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况 +- **串形化处理读写**:避免使用锁带来的性能开销 +- **高性能序列化协议**:支持 protobuf 等高性能序列化协议 \ No newline at end of file diff --git a/src/Middleware/510.md b/src/Middleware/510.md new file mode 100644 index 0000000..55b9f2b --- /dev/null +++ b/src/Middleware/510.md @@ -0,0 +1,63 @@ +### 文件描述符 + +- 设置系统最大文件句柄数 + +```bash +# 查看 +cat /proc/sys/fs/file-max +# 修改 +在/etc/sysctl.conf插入fs.file-max=1000000 +# 配置生效 +sysctl -p +``` + +- 设置单进程打开的最大句柄数 + +默认单进程打开的最大句柄数是 `1024`,通过 `ulimit -a` 可以查看相关参数,示例如下: + +```powershell +[root@test ~]# ulimit -a +core file size (blocks, -c) 0 +data seg size (kbytes, -d) unlimited +scheduling priority (-e) 0 +file size (blocks, -f) unlimited +pending signals (-i) 256324 +max locked memory (kbytes, -l) 64 +max memory size (kbytes, -m) unlimited +open files (-n) 1024 +...... +``` + +当并发接入的TCP连接数超过上限时,就会报“too many open files”,所有新的客户端接入将失败。通过 `vi /etc/security/limits.conf` 命令添加如下配置参数: + +```powershell +* soft nofile 1000000 +* hard nofile 1000000 +``` + +修改之后保存,注销当前用户,重新登录,通过 `ulimit -a` 查看修改的状态是否生效。 + +**注意**:尽管我们可以将单个进程打开的最大句柄数修改的非常大,但是当句柄数达到一定数量级之后,处理效率将出现明显下降,因此,需要根据服务器的硬件配置和处理能力进行合理设置。 + + + +### TCP/IP相关参数 + +需要重点调优的TCP IP参数如下: + +- net.ipv4.tcp_rmem:为每个TCP连接分配的读缓冲区内存大小。第一个值时socket接收缓冲区分配的最小字节数。 + + + +### 多网卡队列和软中断 + +- **TCP缓冲区** + + 根据推送消息的大小,合理设置以下两个参数,对于海量长连接,通常 32K 是个不错的选择: + + - **SO_SNDBUF**:发送缓冲区大小 + - **SO_RCVBUF**:接收缓冲区大小 + +- **软中断** + + 使用命令 `cat /proc/interrupts` 查看网卡硬件中断的运行情况,如果全部被集中在CPU0上处理,则无法并行执行多个软中断。Linux kernel内核≥2.6.35的版本,可以开启RPS,网络通信能力提升20%以上,RPS原理是:根据数据包的源地址、目的地址和源端口等,计算出一个Hash值,然后根据Hash值来选择软中断运行的CPU,即实现每个链接和CPU绑定,通过Hash值来均衡软中断运行在多个CPU上。 \ No newline at end of file diff --git a/src/Middleware/511.md b/src/Middleware/511.md new file mode 100644 index 0000000..8510a21 --- /dev/null +++ b/src/Middleware/511.md @@ -0,0 +1,159 @@ +### 设置合理的线程数 + +**boss线程池优化** + +对于Netty服务端,通常只需要启动一个监听端口用于端侧设备接入,但是如果集群实例较少,甚至是单机部署,那么在短时间内大量设备接入时,需要对服务端的监听方式和线程模型做优化,即服务端监听多个端口,利用主从Reactor线程模型。由于同时监听了多个端口,每个ServerSocketChannel都对应一个独立的Acceptor线程,这样就能并行处理,加速端侧设备的接人速度,减少端侧设备的连接超时失败率,提高单节点服务端的处理性能。 + + + +**work线程池优化(I/O工作线程池)** + +对于I/O工作线程池的优化,可以先采用系统默认值(cpu内核数*2)进行性能测试,在性能测试过程中采集I/O线程的CPU占用大小,看是否存在瓶颈,具体策略如下: + +- 通过执行 `ps -ef|grep java` 找到服务端进程pid +- 执行`top -Hp pid`查询该进程下所有线程的运行情况,通过“shift+p”对CPU占用大小做排序,获取线程的pid及对应的CPU占用大小 +- 使用`printf'%x\n' pid`将pid转换成16进制格式 +- 通过`jstack -f pid`命令获取线程堆栈,或者通过jvisualvm工具打印线程堆栈,找到I/O work工作线程,查看他们的CPU占用大小及线程堆栈,关键词:`SelectorImpl.lockAndDoSelect` + +**分析** + +- 如果连续采集几次进行对比,发现线程堆栈都停留在SelectorImpl.lockAndDoSelect处,则说明I/O线程比较空闲,无需对工作线程数做调整 +- 如果发现I/O线程的热点停留在读或写操作,或停留在ChannelHandler的执行处,则可以通过适当调大NioEventLoop线程的个数来提升网络的读写性能。调整方式有两种: + - 接口API指定:在创建NioEventLoopGroup实例时指定线程数 + - 系统参数指定:通过-Dio.netty.eventLoopThreads来指定NioEventLoopGroup线程池(不建议) + + + +### 心跳检测优化 + +心跳检测的目的就是确认当前链路是否可用,对方是否活着并且能够正常接收和发送消息。从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。 + +**海量设备接入的服务端心跳优化策略** + +- **要能够及时检测失效的连接,并将其剔除**。防止无效的连接句柄积压,导致OOM等问题 +- **设置合理的心跳周期**。防止心跳定时任务积压,造成频繁的老年代GC(新生代和老年代都有导致STW的GC,不过耗时差异较大),导致应用暂停 +- **使用Nety提供的链路空闲检测机制**。不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题 + +**心跳检测机制分为三个层面** + +- **TCP层的心跳检测**:即TCP的 Keep-Alive机制,它的作用域是整个TCP协议栈 +- **协议层的心跳检测**:主要存在于长连接协议中,例如MQTT +- **应用层的心跳检测**:它主要由各业务产品通过约定方式定时给对方发送心跳消息实现 + +**心跳检测机制分类** + +- **Ping-Pong型心跳**:由通信一方定时发送Ping消息,对方接收到Ping消息后立即返回Pong答应消息给对方,属于“请求-响应型”心跳 +- **Ping-Ping型心跳**:不区分心跳请求和答应,由通信双发按照约定时间向对方发送心跳Ping消息,属于”双向心跳“ + +**心跳检测机制策略** + +- **心跳超时**:连续N次检测都没有收到对方的Pong应答消息或Ping请求消息,则认为链路已经发生逻辑失效 +- **心跳失败**:在读取和发送心跳消息的时候,如果直接发生了IO异常,说明链路已经失效 + +**链路空闲检测机制** + +- **读空闲**:链路持续时间T没有读取到任何消息 +- **写空闲**:链路持续时间T没有发送任何消息 +- **读写空闲**:链路持续时间T没有接收或者发送任何消息 + + + +**案例分析** + +由于移动无线网络的特点,推送服务的心跳周期并不能设置的太长,否则长连接会被释放,造成频繁的客户端重连,但是也不能设置太短,否则在当前缺乏统一心跳框架的机制下很容易导致信令风暴(例如微信心跳信令风暴问题)。具体的心跳周期并没有统一的标准,180S 也许是个不错的选择,微信为 300S。 + +在 Netty 中,可以通过在 ChannelPipeline 中增加 IdleStateHandler 的方式实现心跳检测,在构造函数中指定链路空闲时间,然后实现空闲回调接口,实现心跳的发送和检测。拦截链路空闲事件并处理心跳: + +```java +public class MyHandler extends ChannelHandlerAdapter { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + // 心跳处理 + } + } + } +``` + +**心跳优化结论** + +- 对于百万级的服务器,一般不建议很长的心跳周期和超时时长 +- 心跳检测周期通常不要超过60s,心跳检测超时通常为心跳检测周期的2倍 +- 建议通过IdleStateHandler实现心跳,不要自己创建定时任务线程池,加重系统负担和增加潜在并发安全问题 +- 发生心跳超时或心跳失败时,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常 +- 链路空闲事件被触发后并没有关闭链路,而是触发IdleStateEvent事件,用户订阅IdleStateEvent事件,用于自定义逻辑处理。如关闭链路、客户端发起重新连接、告警和日志打印等 +- 链路空闲检测类库主要包括:IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler + + + +### 接收和发送缓冲区调优 + +对于长链接,每个链路都需要维护自己的消息接收和发送缓冲区,JDK 原生的 NIO 类库使用的是java.nio.ByteBuffer, 它实际是一个长度固定的byte[],无法动态扩容。 + +**场景**:假设单条消息最大上限为10K,平均大小为5K,为满足10K消息处理,ByteBuffer的容量被设置为10K,这样每条链路实际上多消耗了5K内存,如果长链接链路数为100万,每个链路都独立持有ByteBuffer接收缓冲区,则额外损耗的总内存Total(M) =1000000×5K=4882M + + + +Netty提供的ByteBuf支持容量动态调整,同时提供了两种接收缓冲区的内存分配器: + +- **FixedRecvByteBufAllocator**:固定长度的接收缓冲区分配器,它分配的ByteBuf长度是固定大小的,并不会根据实际数据报大小动态收缩。但如果容量不足,支持动态扩展 +- **AdaptiveRecvByteBufAllocator**:容量动态调整的接收缓冲区分配器,会根据之前Channel接收到的数据报大小进行计算,如果连续填充满接收缓冲区的可写空间,则动态扩展容量。如果连续2次接收到的数据报都小于指定值,则收缩当前的容量,以节约内存 + +相对于FixedRecvByteBufAllocator,使用AdaptiveRecvByteBufAllocator更为合理,可在创建客户端或者服务端的时候指定RecvByteBufAllocator。 + +```java +Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT) +``` + +**注意**:无论是接收缓冲区还是发送缓冲区,缓冲区的大小建议设置为消息的平均大小,不要设置成最大消息的上限,这会导致额外的内存浪费。 + + + +### 合理使用内存池 + +每个NioEventLoop线程处理N个链路。**链路处理流程**:开始处理A链路→**创建接收缓冲区(创建ByteBuf)**→消息解码→封装成POJO对象→提交至后台线程成Task→**释放接收缓冲区**→开始处理B链路。 + +如果使用内存池,则当A链路接收到新数据报后,从NioEventLoop的内存池中申请空闲的ByteBuf,解码完成后,调用release将ByteBuf释放到内存池中,供后续B链路继续使用。使用内存池优化后,单个NioEventLoop的ByteBuf申请和GC次数从原来的N=1000000/64= 15625次减少为最少0次(假设每次申请都有可用的内存)。 + +Netty默认不使用内存池,需要在创建客户端或者服务端的时候进行指定: + +```java +Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) +``` + +使用内存池之后,内存的申请和释放必须成对出现,即 retain() 和 release() 要成对出现,否则会导致内存泄露。值得注意的是,如果使用内存池,完成 ByteBuf 的解码工作之后必须显式的调用 ReferenceCountUtil.release(msg) 对接收缓冲区 ByteBuf 进行内存释放,否则它会被认为仍然在使用中,这样会导致内存泄露。 + + + +### 防止I/O线程被意外阻塞 + +通常情况不能在Netty的I/O线程上做执行时间不可控的操作,如访问数据库、调用第三方服务等,但有一些隐形的阻塞操作却容易被忽略,如打印日志。 + +生产环境中,一般需要实时打印接口日志,其它日志处于ERROR级别,当服务发生I/O异常后,会记录异常日志。如果磁盘的WIO比较高,可能会发生写日志文件操作被同步阻塞,阻塞时间无法预测,就会导致Netty的NioEventLoop线程被阻塞,Socket链路无法被及时管理,其它链路也无法进行读写操作等。 + +常用的log4j虽然支持异步写日志(AsyncAppender),但当日志队列满之后,它会同步阻塞业务线程,直到日志队列有空闲位置可用。 + +类似问题具有极强的隐蔽性,往往WIO高的时间持续非常短,或是偶现的,在测试环境中很难模拟此类故障,问题定位难度大。 + + + +### I/O线程与业务线程分离 + +- 如果服务端不做复杂业务逻辑操作,仅是简单内存操作和消息转发,则可通过调大NioEventLoop工作线程池的方式,直接在I/O线程中执行业务ChannelHandler,这样便减少了一次线上上下文切换,性能反而更高 +- 如果有复杂的业务逻辑操作,则建议I/O线程和业务线程分离。 + - 对于I/O线程,由于互相之间不存在锁竞争,可以创建一个大的NioEventLoopGroup线程组,所有Channel都共享一个线程池 + - 对于后端的业务线程池,则建议创建多个小的业务线程池,线程池可以与I/O线程绑定,这样既减少了锁竞争,又提升了后端的处理性能 + + + +### 服务端并发连接数流控 + +无论服务端的性能优化到多少,都需要考虑流控功能。当资源成为瓶颈,或遇到端侧设备的大量接入,需要通过流控对系统做保护。一般Netty主要考虑并发连接数的控制。 \ No newline at end of file diff --git a/src/Middleware/512.md b/src/Middleware/512.md new file mode 100644 index 0000000..5348e36 --- /dev/null +++ b/src/Middleware/512.md @@ -0,0 +1,28 @@ +### 疑似内存泄漏 + +**环境**:8C16G的Linux + +**描述**:boss为1,worker为6,其余分配给业务使用,保持10W用户长链接,2W用户并发做消息请求 + +**分析**:dump内存堆栈发现Netty的ScheduledFutureTask增加了9076%,达到110W个实例。通过业务代码分析发现用户使用了IdleStateHandler用于在链路空闲时进行业务逻辑处理,但空闲时间比较大,为15分钟。Netty 的 IdleStateHandler 会根据用户的使用场景,启动三类定时任务,分别是:ReaderIdleTimeoutTask、WriterIdleTimeoutTask 和 AllIdleTimeoutTask,它们都会被加入到 NioEventLoop 的 Task 队列中被调度和执行。由于超时时间过长,10W 个长链接链路会创建 10W 个 ScheduledFutureTask 对象,每个对象还保存有业务的成员变量,非常消耗内存。用户的持久代设置的比较大,一些定时任务被老化到持久代中,没有被 JVM 垃圾回收掉,内存一直在增长,用户误认为存在内存泄露,即小问题被放大而引出的问题。 + +**解决**:重新设计和反复压测之后将超时时间设置为45秒,内存可以实现正常回收。 + + + +### 当心CLOSE_WAIT + +由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭 socket,就会导致处于 close_wait 状态的链路过多。close_wait 状态的链路并不释放句柄和内存等资源,如果积压过多可能会导致系统句柄耗尽,发生“Too many open files”异常,新的客户端无法接入,涉及创建或者打开句柄的操作都将失败。 + +close_wait 是被动关闭连接是形成的,根据 TCP 状态机,服务器端收到客户端发送的 FIN,TCP 协议栈会自动发送 ACK,链接进入 close_wait 状态。但如果服务器端不执行 socket 的 close() 操作,状态就不能由 close_wait 迁移到 last_ack,则系统中会存在很多 close_wait 状态的连接。通常来说,一个 close_wait 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 close_wait 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。 + +导致 close_wait 过多的可能原因如下: + +- **程序处理Bug**:导致接收到对方的 fin 之后没有及时关闭 socket,这可能是 Netty 的 Bug,也可能是业务层 Bug,需要具体问题具体分析 +- **关闭socket不及时**:例如 I/O 线程被意外阻塞,或者 I/O 线程执行的用户自定义 Task 比例过高,导致 I/O 操作处理不及时,链路不能被及时释放 + +**解决方案** + +- **不要在 Netty 的 I/O 线程(worker线程)上处理业务(心跳发送和检测除外)** +- **在I/O线程上执行自定义Task要当心** +- **IdleStateHandler、ReadTimeoutHandler和WriteTimeoutHandler使用要当** \ No newline at end of file diff --git a/src/Middleware/601.md b/src/Middleware/601.md new file mode 100644 index 0000000..2fb1ea6 --- /dev/null +++ b/src/Middleware/601.md @@ -0,0 +1,128 @@ +在 RabbitMQ 官网上提供了 6 中工作模式:简单模式、工作队列模式、发布/订阅模式、路由模式、主题模式 和 RPC 模式。本篇只对前 5 种工作方式进行介绍。 + + + +### 简单模式与工作队列模式 + +之所以将这两种模式合并在一起介绍,是因为它们工作原理非常简单,由 3 个对象组成:生产者、队列、消费者。 + +[![img](images/Middleware/rabbitmq-work-01.png)](http://images.extlight.com/rabbitmq-work-01.png) + +生产者负责生产消息,将消息发送到队列中,消费者监听队列,队列有消息就进行消费。 + +[![img](images/Middleware/rabbitmq-work-02.png)](http://images.extlight.com/rabbitmq-work-02.png) + +当有多个消费者时,消费者平均消费队列中的消息。代码演示: + +生产者: + +```java +//1.获取连接 +Connection connection = ConnectionUtil.getConnection(); +//2.创建通道 +Channel channel = connection.createChannel(); +//3.申明队列 +channel.queueDeclare(QUEUE_NAME, false, false, false, null); +//4.发送消息 +channel.basicPublish("", QUEUE_NAME, null, "hello simple".getBytes()); + +System.out.println("发送成功"); +//5.释放连接 +channel.close(); +connection.close(); +``` + +消费者: + +```java +// 1.获取连接 +Connection connection = ConnectionUtil.getConnection(); +// 2.创建通道 +Channel channel = connection.createChannel(); +// 3.申明队列 +channel.queueDeclare(QUEUE_NAME, false, false, false, null); +// 4.监听消息 +channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, + byte[] body) throws IOException { + String message = new String(body, "UTF-8"); + System.out.println("接收:" + message); + } +}); +``` + + + +### 发布/订阅、路由与主题模式 + +这 3 种模式都使用到交换机。生产者不直接与队列交互,而是将消息发送到交换机中,再由交换机将消息放入到已绑定该交换机的队列中给消费者消费。常用的交换机类型有 3 种:fanout、direct、topic。工作原理图如下: + +[![img](images/Middleware/rabbitmq-work-03-1.png)](http://images.extlight.com/rabbitmq-work-03-1.png) + +**fanout**:不处理路由键。只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型交换机转发消息是最快的。 + +**其中,发布/订阅模式使用的是 fanout 类型的交换机。** + +[![img](images/Middleware/rabbitmq-work-04-1.png)](http://images.extlight.com/rabbitmq-work-04-1.png) + +**direct**:处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为 “dog” 的消息才被转发,不会转发 dog.puppy,也不会转发 dog.guard,只会转发dog。 + +**其中,路由模式使用的是 direct 类型的交换机。** + +[![img](images/Middleware/rabbitmq-work-05-1.png)](http://images.extlight.com/rabbitmq-work-05-1.png) + +**topic**:将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号 “#” 匹配一个或多个词,符号“\*”匹配不多不少一个词。因此“audit.#” 能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到 “audit.irs”。 + +**其中,主题模式使用的是 topic 类型的交换机。**代码演示: + +生产者: + +```java +// 1.获取连接 +Connection connection = ConnectionUtil.getConnection(); +// 2.创建通道 +Channel channel = connection.createChannel(); +// 3.申明交换机 +channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); +// 4.发送消息 +for (int i = 0; i < 100; i++) { + channel.basicPublish(EXCHANGE_NAME, "", null, ("hello ps" + i + "").getBytes()); +} + +System.out.println("发送成功"); +// 5.释放连接 +channel.close(); +connection.close(); +``` + +多个消费者: + +```java +// 1.获取连接 +Connection connection = ConnectionUtil.getConnection(); +// 2.创建通道 +Channel channel = connection.createChannel(); +// 3.申明交换机 +channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); +// 4.队列绑定交换机 +channel.queueDeclare(QUEUE_NAME, false, false, false, null); +channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); +// 5.消费消息 +channel.basicQos(1); +channel.basicConsume(QUEUE_NAME, false, new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + String message = new String(body, "UTF-8"); + System.out.println("recv1:" + message); + + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + channel.basicAck(envelope.getDeliveryTag(), false); + } +}); +``` \ No newline at end of file diff --git a/src/Middleware/701.md b/src/Middleware/701.md new file mode 100644 index 0000000..00d7055 --- /dev/null +++ b/src/Middleware/701.md @@ -0,0 +1,29 @@ +Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架。Apache Dubbo 提供了六大核心能力:**面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维**。 + +- **面向接口代理的高性能RPC调用** + + 提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。 + +- **智能负载均衡** + + 内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。 + +- **服务自动注册与发现** + + 支持多种注册中心服务,服务实例上下线实时感知。 + +- **高度可扩展能力** + + 遵循微内核+插件的设计原则,所有核心能力如Protocol、Transport、Serialization被设计为扩展点,平等对待内置实现和第三方实现。 + +- **运行期流量调度** + + 内置条件、脚本等路由策略,通过配置不同的路由规则,轻松实现灰度发布,同机房优先等功能。 + +- **可视化的服务治理与运维** + + 提供丰富服务治理、运维工具:随时查询服务元数据、服务健康状态及调用统计,实时下发路由策略、调整配置参数。 + +![DubboArchitecture](images/Middleware/DubboArchitecture.png) + +![ApacheDubbo](images/Middleware/ApacheDubbo.jpg) \ No newline at end of file diff --git a/src/Middleware/702.md b/src/Middleware/702.md new file mode 100644 index 0000000..5ead360 --- /dev/null +++ b/src/Middleware/702.md @@ -0,0 +1 @@ +Remote Procedure Call Protocol 既 **远程过程调用**,一种能让我们**像调用本地服务一样调用远程服务**,可以让调用者对网络通信这些细节**无感知**,比如服务消费方在执行 helloWorldService.sayHello("sowhat") 时,实质上调用的是远端的服务。这种方式其实就是**RPC**,**RPC**思想在各大互联网公司中被广泛使用,如阿里巴巴的**dubbo**、当当的**Dubbox** 、Facebook 的 **thrift**、Google 的**grpc**、Twitter的**finagle**等。 \ No newline at end of file diff --git a/src/Middleware/703.md b/src/Middleware/703.md new file mode 100644 index 0000000..cad1d3d --- /dev/null +++ b/src/Middleware/703.md @@ -0,0 +1,60 @@ +### Dubbo 简介 + +Dubbo 是阿里巴巴研发开源工具,主要分为2.6.x 跟 2.7.x 版本。是一款分布式、高性能、透明化的 RPC 服务框架,提供服务自动注册、自动发现等高效服务治理方案,可以和Spring 框架无缝集成,它提供了6大核心能力: + +- 面向接口代理的高性能RPC调用 +- 智能容错和负载均衡 +- 服务自动注册和发现 +- 高度可扩展能力 +- 运行期流量调度 +- 可视化的服务治理与运维 + + + +**调用过程**: + +- 服务提供者 **Provider** 启动然后向 **Registry** 注册自己所能提供的服务。 +- 服务消费者 Consumer 向**Registry**订阅所需服务,**Consumer** 解析**Registry**提供的元信息,从服务中通过负载均衡选择 **Provider**调用。 +- 服务提供方 **Provider** 元数据变更的话**Registry**会把变更推送给**Consumer**,以此保证**Consumer**获得最新可用信息。 + + + +**注意点**: + +- **Provider** 跟 **Consumer** 在内存中记录调用次数跟时间,定时发送统计数据到**Monitor**,发送的时候是**短**连接。 +- **Monitor** 跟 **Registry** 是可选的,可直接在配置文件中写好,**Provider** 跟 **Consumer**进行直连。 +- **Monitor** 跟 **Registry** 挂了也没事, **Consumer** 本地缓存了 **Provider** 信息。 +- **Consumer** 直接调用 **Provider** 不会经过 **Registry**。**Provider**、**Consumer**这俩到 **Registry**之间是长连接。 + + + +### Dubbo框架分层 + +![Dubbo框架分层](images/Middleware/Dubbo框架分层.jpg) + +如上图,总的而言 Dubbo 分为三层。 + +- **Busines**层:由用户自己来提供接口和实现还有一些配置信息。 +- **RPC**层:真正的RPC调用的核心层,封装整个RPC的调用过程、负载均衡、集群容错、代理。 +- **Remoting**层:对网络传输协议和数据转换的封装。 + + + +如果每一层再细分下去,一共有十层: + +1. 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现。 +2. 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心初始化配置。 +3. 服务代理层(Proxy):服务接口透明代理,Provider跟Consumer都生成代理类,使得服务接口透明,代理层实现服务调用跟结果返回。 +4. 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心。 +5. 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce。 +6. 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService。 +7. 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter。 +8. 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer。 +9. 网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec。 +10. 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool。 + + + +他们之间的调用关系直接看下面官网图即可: + +![Dubbo调用关系](images/Middleware/Dubbo调用关系.jpg) \ No newline at end of file diff --git a/src/Middleware/704.md b/src/Middleware/704.md new file mode 100644 index 0000000..4a6faac --- /dev/null +++ b/src/Middleware/704.md @@ -0,0 +1,156 @@ +**Dubbo** 采用 **微内核设计** + **SPI** 扩展技术来搭好核心框架,同时满足用户定制化需求。这里重点说下**SPI**。 + +### SPI 含义 + +**SPI** 全称为 **Service Provider Interface**,是一种**服务发现机制**。它约定在**ClassPath**路径下的**META-INF/services**文件夹查找文件,自动加载文件里所定义的类。 + + + +### Java SPI缺点 + +- 不能按需加载,Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源。 +- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。 +- 不支持AOP与依赖注入,JAVA SPI可能会丢失加载扩展点异常信息,导致追踪问题很困难。 + + + +### Dubbo SPI + +JDK自带的不好用Dubbo 就自己实现了一个 SPI,该SPI **可以通过名字实例化指定的实现类,并且实现了 IOC 、AOP 与 自适应扩展 SPI** 。 + +```properties +key = com.sowhat.value +``` + +Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。 + +- META-INF/services/ :该目录下 SPI 配置文件是为了用来兼容 Java SPI +- META-INF/dubbo/ :该目录存放用户自定义的 SPI 配置文件 +- META-INF/dubbo/internal/ :该目录存 Dubbo 内部使用的 SPI 配置文件 + + + +### Dubbo SPI源码追踪 + +**ExtensionLoader.getExtension** 方法的整个思路是 查找缓存是否存在,不存在则读取SPI文件,通过反射创建类,然后设置依赖注入这些东西,有包装类就包装下,执行流程如下图所示: + +![DubboSPI源码追踪](images/Middleware/DubboSPI源码追踪.png) + +### injectExtension IOC + +查找 set 方法,根据参数找到依赖对象则注入。 + + + +### WrapperClass AOP + +包装类,Dubbo帮你自动包装,只需某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类。 + + + +### Activate + +Active 有三个属性,group 表示修饰在哪个端,是 provider 还是 consumer,value 表示在 URL参数中出现才会被激活,order 表示实现类的顺序。 + + + +### Adaptive自适应扩展 + +**需求**:根据配置来进行 SPI 扩展的加载后不想在启动的时候让扩展被加载,想根据请求时候的参数来动态选择对应的扩展。**实现**:Dubbo用代理机制实现了自适应扩展,为用户想扩展的接口 通过JDK 或者 Javassist 编译生成一个代理类,然后通过反射创建实例。实例会根据本来方法的请求参数得知需要的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(name)来获取真正的实例来调用,看个官网样例。 + +```java +public interface WheelMaker { + Wheel makeWheel(URL url); +} +// WheelMaker 接口的自适应实现类 +public class AdaptiveWheelMaker implements WheelMaker { + public Wheel makeWheel(URL url) { + if (url == null) { + throw new IllegalArgumentException("url == null"); + } + // 1. 调用 url 的 getXXX 方法获取参数值 + String wheelMakerName = url.getParameter("Wheel.maker"); + if (wheelMakerName == null) { + throw new IllegalArgumentException("wheelMakerName == null"); + } + // 2. 调用 ExtensionLoader 的 getExtensionLoader 获取加载器 + // 3. 调用 ExtensionLoader 的 getExtension 根据从url获取的参数作为类名称加载实现类 + WheelMaker wheelMaker = ExtensionLoader.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName); + // 4. 调用实现类的具体方法实现调用。 + return wheelMaker.makeWheel(URL url); + } +} +``` + +查看Adaptive注解源码可知该注解可用在**类**或**方法**上,Adaptive 注解在类上或者方法上有不同的实现逻辑。 + + + +#### Adaptive注解在类上 + +Adaptive 注解在类上时,Dubbo 不会为该类生成代理类,Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory,表示拓展的加载逻辑由人工编码完成,这不是我们关注的重点。 + + + +#### Adaptive注解在方法上 + +Adaptive注解在方法上时,Dubbo则会为该方法生成代理逻辑,表示拓展的加载逻辑需由框架自动生成,实现机制如下: + +- 加载标注有 @Adaptive 注解的接口,如果不存在,则不支持 Adaptive 机制 +- 为目标接口按照一定的模板生成子类代码,并且编译生成的代码,然后通过反射生成该类的对象 +- 结合生成的对象实例,通过传入的URL对象,获取指定key的配置,然后加载该key对应的类对象,最终将调用委托给该类对象进行 + +```java +@SPI("apple") +public interface FruitGranter { + Fruit grant(); + @Adaptive + String watering(URL url); +} +--- +// 苹果种植者 +public class AppleGranter implements FruitGranter { + @Override + public Fruit grant() { + return new Apple(); + } + @Override + public String watering(URL url) { + System.out.println("watering apple"); + return "watering finished"; + } +} +--- +// 香蕉种植者 +public class BananaGranter implements FruitGranter { + @Override + public Fruit grant() { + return new Banana(); + } + @Override + public String watering(URL url) { + System.out.println("watering banana"); + return "watering success"; + } +} +``` + +调用方法实现: + +```java +public class ExtensionLoaderTest { + @Test + public void testGetExtensionLoader() { + // 首先创建一个模拟用的URL对象 + URL url = URL.valueOf("dubbo://192.168.0.1:1412?fruit.granter=apple"); + // 通过ExtensionLoader获取一个FruitGranter对象 + FruitGranter granter = ExtensionLoader.getExtensionLoader(FruitGranter.class) + .getAdaptiveExtension(); + // 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果 + String result = granter.watering(url); + System.out.println(result); + } +} +``` + +通过如上方式生成一个内部类。 \ No newline at end of file diff --git a/src/Middleware/705.md b/src/Middleware/705.md new file mode 100644 index 0000000..4ba7d29 --- /dev/null +++ b/src/Middleware/705.md @@ -0,0 +1,36 @@ +### 服务暴露总览 + +**Dubbo**框架是以**URL**为总线的模式,运行过程中所有的状态数据信息都可以通过**URL**来获取,比如当前系统采用什么序列化,采用什么通信,采用什么负载均衡等信息,都是通过**URL**的参数来呈现的,所以在框架运行过程中,运行到某个阶段需要相应的数据,都可以通过对应的**Key**从**URL**的参数列表中获取。**URL** 具体的参数如下: + +- protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http +- username/password:用户名/密码 +- host/port:主机/端口 +- path:接口的名称 +- parameters:参数键值对 + +```properties +protocol://username:password@host:port/path?k=v +``` + +服务暴露从代码流程看分为三部分: + +- 检查配置,最终组装成 **URL**。 +- 暴露服务到到本地服务跟远程服务。 +- 服务注册至注册中心。 + + + +服务暴露从对象构建转换看分为两步: + +- 将服务封装成**Invoker**。 +- 将**Invoker**通过协议转换为**Exporter**。 + + + +### 服务暴露源码追踪 + +- 容器启动,Spring IOC刷新完毕后调用 onApplicationEvent 开启服务暴露,ServiceBean +- export 跟 doExport 来进行拼接构建URL,为屏蔽调用的细节,统一暴露出一个可执行体,通过ProxyFactory 获取到 invoker +- 调用具体 Protocol 将把包装后的 invoker 转换成 exporter,此处用到了SPI +- 然后启动服务器server,监听端口,使用NettyServer创建监听服务器 +- 通过 RegistryProtocol 将URL注册到注册中心,使得consumer可获得provider信息 \ No newline at end of file diff --git a/src/Middleware/706.md b/src/Middleware/706.md new file mode 100644 index 0000000..f514d65 --- /dev/null +++ b/src/Middleware/706.md @@ -0,0 +1,38 @@ +Dubbo中一个可执行体就是一个invoker,所以 provider 跟 consumer 都要向 invoker 靠拢。通过上面demo可知为了无感调用远程接口,底层需要有个代理类包装 invoker。 + + + +**服务的引入时机有两种** + +- 饿汉式 + + 通过实现 Spring 的 InitializingBean 接口中的 afterPropertiesSet 方法,容器通过调用 **ReferenceBean**的 afterPropertiesSet 方法时引入服务。 + +- 懒汉式(默认) + + 懒汉式是只有当服务被注入到其他类中时启动引入流程。 + + + +**服务引用的三种方式** + +- **本地引入**:服务暴露时本地暴露,避免网络调用开销 +- **直接连接引入远程服务**:不启动注册中心,直接写死远程**Provider**地址 进行直连 +- **通过注册中心引入远程服务**:通过注册中心抉择如何进行负载均衡调用远程服务 + + + +**服务引用流程** + +- 检查配置构建map ,map 构建 URL ,通过URL上的协议利用自适应扩展机制调用对应的 protocol.refer 得到相应的 invoker ,此处 +- 想注册中心注册自己,然后订阅注册中心相关信息,得到provider的 ip 等信息,再通过共享的netty客户端进行连接。 +- 当有多个 URL 时,先遍历构建出 invoker 然后再由 **StaticDirectory** 封装一下,然后通过 cluster 进行合并,只暴露出一个 invoker 。 +- 然后再构建代理,封装 invoker 返回服务引用,之后 Comsumer 调用的就是这个代理类。 + + + +**调用方式** + +- oneway:不关心请求是否发送成功。 +- Async异步调用:Dubbo天然异步,客户端调用请求后将返回的 ResponseFuture 存到上下文中,用户可随时调用 future.get 获取结果。异步调用通过唯一**ID** 标识此次请求。 +- Sync同步调用:在 Dubbo 源码中就调用了 future.get,用户感觉方法被阻塞了,必须等结果后才返回。 \ No newline at end of file diff --git a/src/Middleware/707.md b/src/Middleware/707.md new file mode 100644 index 0000000..6ad08ff --- /dev/null +++ b/src/Middleware/707.md @@ -0,0 +1,25 @@ +**调用之前你可能需要考虑这些事** + +- consumer 跟 provider 约定好通讯协议,dubbo支持多种协议,比如dubbo、rmi、hessian、http、webservice等。默认走dubbo协议,连接属于**单一长连接**,**NIO异步通信**。适用传输数据量很小(单次请求在100kb以内),但是并发量很高。 +- 约定序列化模式,大致分为两大类,一种是字符型(XML或json 人可看懂 但传输效率低),一种是二进制流(数据紧凑,机器友好)。默认使用 hessian2作为序列化协议。 +- consumer 调用 provider 时提供对应接口、方法名、参数类型、参数值、版本号。 +- provider列表对外提供服务涉及到负载均衡选择一个provider提供服务。 +- consumer 跟 provider 定时向monitor 发送信息。 + + + +**调用大致流程** + +- 客户端发起请求来调用接口,接口调用生成的代理类。代理类生成RpcInvocation 然后调用invoke方法。 +- ClusterInvoker获得注册中心中服务列表,通过负载均衡给出一个可用的invoker。 +- 序列化跟反序列化网络传输数据。通过NettyServer调用网络服务。 +- 服务端业务线程池接受解析数据,从exportMap找到invoker进行invoke。 +- 调用真正的Impl得到结果然后返回。 + + + +**调用方式** + +- oneway:不关心请求是否发送成功,消耗最小。 +- sync同步调用:在 Dubbo 源码中就调用了 future.get,用户感觉方法被阻塞了,必须等结果后才返回。 +- Async 异步调用:Dubbo天然异步,客户端调用请求后将返回的 ResponseFuture 存到上下文中,用户可以随时调用future.get获取结果。异步调用通过**唯一ID**标识此次请求。 \ No newline at end of file diff --git a/src/Middleware/708.md b/src/Middleware/708.md new file mode 100644 index 0000000..78d3c5b --- /dev/null +++ b/src/Middleware/708.md @@ -0,0 +1,65 @@ +Dubbo 引入了**Cluster**、**Directory**、**Router**、**LoadBalance**、**Invoker**模块来保证Dubbo系统的稳健性,关系如下图: + +![Dubbo集群容错负载均衡](images/Middleware/Dubbo集群容错负载均衡.jpg) + +- 服务发现时会将多个多个远程调用放入**Directory**,然后通过**Cluster**封装成一个**Invoker**,该**invoker**提供容错功能 +- 消费者代用的时候从**Directory**中通过负载均衡获得一个可用**invoker**,最后发起调用 +- 你可以认为**Dubbo**中的**Cluster**对上面进行了大的封装,自带各种鲁棒性功能 + + + +### 集群容错 + +集群容错是在消费者端通过**Cluster**子类实现的,Cluster接口有10个实现类,每个**Cluster**实现类都会创建一个对应的**ClusterInvoker**对象。核心思想是**让用户选择性调用这个Cluster中间层,屏蔽后面具体实现细节**。 + +| Cluster | Cluster Invoker | 作用 | +| :--------------- | :---------------------- | :---------------------------- | +| FailoverCluster | FailoverClusterInvoker | 失败自动切换功能,**默认** | +| FailfastCluster | FailfastClusterInvoker | 一次调用,失败异常 | +| FailsafeCluster | FailsafeClusterInvoker | 调用出错则日志记录 | +| FailbackCluster | FailbackClusterInvoker | 失败返空,定时重试2次 | +| ForkingCluster | ForkingClusterInvoker | 一个任务并发调用,一个OK则OK | +| BroadcastCluster | BroadcastClusterInvoker | 逐个调用invoker,全可用才可用 | +| AvailableCluster | AvailableClusterInvoker | 哪个能用就用那个 | +| MergeableCluster | MergeableClusterInvoker | 按组合并返回结果 | + + + +### 智能容错之负载均衡 + +Dubbo中一般有4种负载均衡策略。 + +- **RandomLoadBalance**:加权随机,它的算法思想简单。假设有一组服务器 servers = [A, B, C],对应权重为 weights = [5, 3, 2],权重总和为10。现把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。**默认实现**。 +- **LeastActiveLoadBalance**:最少活跃数负载均衡,选择现在活跃调用数最少的提供者进行调用,活跃的调用数少说明它现在很轻松,而且活跃数都是从 0 加起来的,来一个请求活跃数+1,一个请求处理完成活跃数-1,所以活跃数少也能变相的体现处理的快。 +- **RoundRobinLoadBalance**:加权轮询负载均衡,比如现在有两台服务器 A、B,轮询的调用顺序就是 A、B、A、B,如果加了权重,A 比B 的权重是2:1,那现在的调用顺序就是 A、A、B、A、A、B。 +- **ConsistentHashLoadBalance**:一致性 Hash 负载均衡,将服务器的 IP 等信息生成一个 hash 值,将hash 值投射到圆环上作为一个节点,然后当 key 来查找的时候顺时针查找第一个大于等于这个 key 的 hash 值的节点。一般而言还会引入虚拟节点,使得数据更加的分散,避免数据倾斜压垮某个节点。如下图 Dubbo 默认搞了 160 个虚拟节点。 + +![ConsistentHashLoadBalance](images/Middleware/ConsistentHashLoadBalance.png) + + + +### 智能容错之服务目录 + +关于 服务目录Directory 你可以理解为是相同服务Invoker的集合,核心是RegistryDirectory类。具有三个功能。 + +- 从注册中心获得invoker列表 +- 监控着注册中心invoker的变化,invoker的上下线 +- 刷新invokers列表到服务目录 + + + +### 智能容错之服务路由 + +服务路由其实就是路由规则,它规定了服务消费者可以调用哪些服务提供者。条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则: + +```properties +host = 10.20.153.14 => host = 10.20.153.12 +``` + +该条规则表示 IP 为 10.20.153.14 的服务消费者只可调用 IP 为 10.20.153.12 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下: + +```properties +[服务消费者匹配条件] => [服务提供者匹配条件] +``` + +如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。 \ No newline at end of file diff --git a/src/Middleware/709.md b/src/Middleware/709.md new file mode 100644 index 0000000..c528ec2 --- /dev/null +++ b/src/Middleware/709.md @@ -0,0 +1,9 @@ +一个RPC框架大致需要以下功能: + +1. 服务的注册与发现 +2. 用动态代理 +3. 负载均衡(LoadBalance) +4. 通信协议 +5. 序列化与反序列化 +6. 网络通信(Netty) +7. Monitor \ No newline at end of file diff --git a/src/Middleware/801.md b/src/Middleware/801.md new file mode 100644 index 0000000..25dc5d0 --- /dev/null +++ b/src/Middleware/801.md @@ -0,0 +1,53 @@ +![Nacos架构图](images/Middleware/Nacos架构图.jpeg) + +### 服务 (Service) + +服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service。 + + + +### 服务注册中心 (Service Registry) + +服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。 + + + +### 服务元数据 (Service Metadata) + +服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据。 + + + +### 服务提供方 (Service Provider) + +是指提供可复用和可调用服务的应用方。 + + + +### 服务消费方 (Service Consumer) + +是指会发起对某个服务调用的应用方。 + + + +### 配置 (Configuration) + +在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。 + + + +### 配置管理 (Configuration Management) + +在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。 + + + +### 名字服务 (Naming Service) + +提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。 + + + +### 配置服务 (Configuration Service) + +在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。 \ No newline at end of file diff --git a/src/Middleware/802.md b/src/Middleware/802.md new file mode 100644 index 0000000..967e3c0 --- /dev/null +++ b/src/Middleware/802.md @@ -0,0 +1,31 @@ +![Nacos逻辑架构及其组件介绍](images/Middleware/Nacos逻辑架构及其组件介绍.png) + +- 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能 +- 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能 +- 元数据管理:提供元数据CURD 和打标能力 +- 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制 +- 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑 +- 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档 +- 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性 +- 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展 +- 推送通道:解决server与存储、server间、server与sdk间推送性能问题 +- 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性 +- 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制 +- 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具 +- 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI +- 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制 +- 存储模块:解决数据持久化、非持久化存储,解决数据分片问题 +- Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题 +- CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系 +- Metrics:暴露标准metrics数据,方便与三方监控系统打通 +- Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通 +- 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程 +- 用户管理:解决用户管理,登录,sso等问题 +- 权限管理:解决身份识别,访问控制,角色管理等问题 +- 审计系统:扩展接口方便与不同公司审计系统打通 +- 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更 +- OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成 +- Console:易用控制台,做服务管理、配置管理等操作 +- SDK:多语言sdk +- Agent:dns-f类似模式,或者与mesh等方案集成 +- CLI:命令行对产品进行轻量化管理,像git一样好用 \ No newline at end of file diff --git a/src/Middleware/803.md b/src/Middleware/803.md new file mode 100644 index 0000000..9401b7d --- /dev/null +++ b/src/Middleware/803.md @@ -0,0 +1,31 @@ +Nacos 的关键特性包括: + +- **服务发现和服务健康监测** + +- **动态配置服务** + +- **动态 DNS 服务** + +- **服务及其元数据管理** + + + +### 数据模型 + +Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。 + +![nacos_data_model](images/Middleware/nacos_data_model.jpeg) + + + +### Nacos-SDK类视图 + +![nacos_sdk_class_relation](images/Middleware/nacos_sdk_class_relation.jpeg) + + + +### 配置领域模型 + +围绕配置,主要有两个关联的实体,一个是配置变更历史,一个是服务标签(用于打标分类,方便索引),由 ID 关联。 + +![nacos_config_er](images/Middleware/nacos_config_er.jpeg) \ No newline at end of file diff --git a/src/Middleware/804.md b/src/Middleware/804.md new file mode 100644 index 0000000..50712e0 --- /dev/null +++ b/src/Middleware/804.md @@ -0,0 +1,94 @@ +### 下载源码或者安装包 + +**从 Github 上下载源码方式** + +```bash +git clone https://github.com/alibaba/nacos.git +cd nacos/ +mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U +ls -al distribution/target/ + +// change the $version to your actual path +cd distribution/target/nacos-server-$version/nacos/bin +``` + +**下载编译后压缩包方式** + +您可以从 [最新稳定版本](https://github.com/alibaba/nacos/releases) 下载 `nacos-server-$version.zip` 包。 + +```bash + unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz + cd nacos/bin +``` + + + +### 启动服务器 + +**Linux/Unix/Mac** + +启动命令(standalone代表着单机模式运行,非集群模式): + +```shell +sh startup.sh -m standalone +``` + +如果您使用的是ubuntu系统,或者运行脚本报错提示符号找不到,可尝试如下运行: + +```shell +bash startup.sh -m standalone +``` + +**Windows** + +启动命令(standalone代表着单机模式运行,非集群模式): + +```shell +startup.cmd -m standalone +``` + + + +### 服务测试 + +**服务注册** + +```shell +curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080' +``` + +**服务发现** + +```shell +curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos.naming.serviceName' +``` + +**发布配置** + +```shell +curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld" +``` + +**获取配置** + +```shell +curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test" +``` + + + +### 关闭服务器 + +**Linux/Unix/Mac** + +```shell +sh shutdown.sh +``` + +**Windows** + +```shell +shutdown.cmd +``` + +或者双击 `shutdown.cmd`运行文件。 \ No newline at end of file diff --git a/src/Middleware/805.md b/src/Middleware/805.md new file mode 100644 index 0000000..2ebe9c6 --- /dev/null +++ b/src/Middleware/805.md @@ -0,0 +1,317 @@ +### Spring Boot + +#### 启动配置管理 + +启动了 Nacos server 后,您就可以参考以下示例代码,为您的 Spring Boot 应用启动 Nacos 配置管理服务了。 + +**第一步**:添加依赖 + +```xml + + com.alibaba.boot + nacos-config-spring-boot-starter + ${latest.version} + +``` + +**注意**:版本 [0.2.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.boot/nacos-config-spring-boot-starter) 对应的是 Spring Boot 2.x 版本,版本 [0.1.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.boot/nacos-config-spring-boot-starter) 对应的是 Spring Boot 1.x 版本。 + +**第二步**:在 `application.properties` 中配置 Nacos server 的地址: + +```properties +nacos.config.server-addr=127.0.0.1:8848 +``` + +**第三步**:使用 `@NacosPropertySource` 加载 `dataId` 为 `example` 的配置源,并开启自动更新: + +```java +@SpringBootApplication +@NacosPropertySource(dataId = "example", autoRefreshed = true) +public class NacosConfigApplication { + public static void main(String[] args) { + SpringApplication.run(NacosConfigApplication.class, args); + } +} +``` + +**第四步**:通过 Nacos 的 `@NacosValue` 注解设置属性值。 + +```java +@Controller +@RequestMapping("config") +public class ConfigController { + + @NacosValue(value = "${useLocalCache:false}", autoRefreshed = true) + private boolean useLocalCache; + + @RequestMapping(value = "/get", method = GET) + @ResponseBody + public boolean get() { + return useLocalCache; + } +} +``` + +**第五步**:启动 `NacosConfigApplication`,调用 `curl http://localhost:8080/config/get`,返回内容是 `false`。 + +**第六步**:通过调用 [Nacos Open API](https://nacos.io/zh-cn/docs/open-api.html) 向 Nacos server 发布配置:dataId 为`example`,内容为`useLocalCache=true` + +```shell +curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=example&group=DEFAULT_GROUP&content=useLocalCache=true" +``` + +**第七步**:再次访问 `http://localhost:8080/config/get`,此时返回内容为`true`,说明程序中的`useLocalCache`值已经被动态更新了。 + + + +#### 启动服务发现 + +本节演示如何在您的 Spring Boot 项目中启动 Nacos 的服务发现功能。 + +**第一步**:添加依赖 + +```xml + + com.alibaba.boot + nacos-discovery-spring-boot-starter + ${latest.version} + +``` + +**注意**:版本 [0.2.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.boot/nacos-discovery-spring-boot-starter) 对应的是 Spring Boot 2.x 版本,版本 [0.1.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.boot/nacos-discovery-spring-boot-starter) 对应的是 Spring Boot 1.x 版本。 + +**第二步**:在 `application.properties` 中配置 Nacos server 的地址: + +```properties +nacos.discovery.server-addr=127.0.0.1:8848 +``` + +**第三步**:使用 `@NacosInjected` 注入 Nacos 的 `NamingService` 实例: + +```java +@Controller +@RequestMapping("discovery") +public class DiscoveryController { + + @NacosInjected + private NamingService namingService; + + @RequestMapping(value = "/get", method = GET) + @ResponseBody + public List get(@RequestParam String serviceName) throws NacosException { + return namingService.getAllInstances(serviceName); + } +} + +@SpringBootApplication +public class NacosDiscoveryApplication { + + public static void main(String[] args) { + SpringApplication.run(NacosDiscoveryApplication.class, args); + } +} +``` + +**第四步**:启动 `NacosDiscoveryApplication`,调用 `curl http://localhost:8080/discovery/get?serviceName=example`,此时返回为空 JSON 数组`[]`。 + +**第五步**:通过调用 [Nacos Open API](https://nacos.io/zh-cn/docs/open-api.html) 向 Nacos server 注册一个名称为 `example` 服务 + +```shell +curl -X PUT 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=example&ip=127.0.0.1&port=8080' +``` + +**第六步**:再次访问 `curl http://localhost:8080/discovery/get?serviceName=example`,此时返回内容为: + +```json +[ + { + "instanceId": "127.0.0.1-8080-DEFAULT-example", + "ip": "127.0.0.1", + "port": 8080, + "weight": 1.0, + "healthy": true, + "cluster": { + "serviceName": null, + "name": "", + "healthChecker": { + "type": "TCP" + }, + "defaultPort": 80, + "defaultCheckPort": 80, + "useIPPort4Check": true, + "metadata": {} + }, + "service": null, + "metadata": {} + } +] +``` + + + +### Spring Cloud + +#### 启动配置管理 + +启动了 Nacos server 后,您就可以参考以下示例代码,为您的 Spring Cloud 应用启动 Nacos 配置管理服务了。 + +**第一步**:添加依赖 + +```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + ${latest.version} + +``` + +**注意**:版本 [2.1.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-config) 对应的是 Spring Boot 2.1.x 版本。版本 [2.0.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-config) 对应的是 Spring Boot 2.0.x 版本,版本 [1.5.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-config) 对应的是 Spring Boot 1.5.x 版本。 + +**第二步**:在 `bootstrap.properties` 中配置 Nacos server 的地址和应用名 + +```properties +spring.cloud.nacos.config.server-addr=127.0.0.1:8848 +spring.application.name=example +``` + +说明:之所以需要配置 `spring.application.name` ,是因为它是构成 Nacos 配置管理 `dataId`字段的一部分。 + +**第三步**:在 Nacos Spring Cloud 中,`dataId` 的完整格式如下: + +```properties +${prefix}-${spring.profiles.active}.${file-extension} +``` + +- `prefix` 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix`来配置。 +- `spring.profiles.active` 即为当前环境对应的 profile,详情可以参考 [Spring Boot文档](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html#boot-features-profiles)。 **注意:当 `spring.profiles.active` 为空时,对应的连接符 `-` 也将不存在,dataId 的拼接格式变成 `${prefix}.${file-extension}`** +- `file-exetension` 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置。目前只支持 `properties` 和 `yaml` 类型。 + +**第四步**:通过 Spring Cloud 原生注解 `@RefreshScope` 实现配置自动更新: + +```java +@RestController +@RequestMapping("/config") +@RefreshScope +public class ConfigController { + + @Value("${useLocalCache:false}") + private boolean useLocalCache; + + @RequestMapping("/get") + public boolean get() { + return useLocalCache; + } +} +``` + +**第五步**:首先通过调用 [Nacos Open API](https://nacos.io/zh-cn/docs/open-api.html) 向 Nacos Server 发布配置:dataId 为`example.properties`,内容为`useLocalCache=true` + +```shell +curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=example.properties&group=DEFAULT_GROUP&content=useLocalCache=true" +``` + +**第六步**:运行 `NacosConfigApplication`,调用 `curl http://localhost:8080/config/get`,返回内容是 `true`。 + +**第七步**:再次调用 [Nacos Open API](https://nacos.io/zh-cn/docs/open-api.html) 向 Nacos server 发布配置:dataId 为`example.properties`,内容为`useLocalCache=false` + +```shell +curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=example.properties&group=DEFAULT_GROUP&content=useLocalCache=false" +``` + +**第八步**:再次访问 `http://localhost:8080/config/get`,此时返回内容为`false`,说明程序中的`useLocalCache`值已经被动态更新了。 + + + +#### 启动服务发现 + +本节通过实现一个简单的 `echo service` 演示如何在您的 Spring Cloud 项目中启用 Nacos 的服务发现功能,如下图示: + +![echo_service](images/Middleware/echo_service.png) + +**第一步**:添加依赖: + +```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + ${latest.version} + +``` + +**注意**:版本 [2.1.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery) 对应的是 Spring Boot 2.1.x 版本。版本 [2.0.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery) 对应的是 Spring Boot 2.0.x 版本,版本 [1.5.x.RELEASE](https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery) 对应的是 Spring Boot 1.5.x 版本。 + +**第二步**:配置服务提供者,从而服务提供者可以通过 Nacos 的服务注册发现功能将其服务注册到 Nacos server 上。 + +i. 在 `application.properties` 中配置 Nacos server 的地址: + +```properties +server.port=8070 +spring.application.name=service-provider +spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 +``` + +ii. 通过 Spring Cloud 原生注解 `@EnableDiscoveryClient` 开启服务注册发现功能: + +```java +@SpringBootApplication +@EnableDiscoveryClient +public class NacosProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(NacosProviderApplication.class, args); + } + + @RestController + class EchoController { + @RequestMapping(value = "/echo/{string}", method = RequestMethod.GET) + public String echo(@PathVariable String string) { + return "Hello Nacos Discovery " + string; + } + } +} +``` + +**第三步**:配置服务消费者,从而服务消费者可通过 Nacos 的服务注册发现功能从 Nacos server 上获取到它要调用的服务。 + +i. 在 `application.properties` 中配置 Nacos server 的地址: + +```properties +server.port=8080 +spring.application.name=service-consumer +spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 +``` + +ii. 通过 Spring Cloud 原生注解 `@EnableDiscoveryClient` 开启服务注册发现功能。给 [RestTemplate](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-resttemplate.html) 实例添加 `@LoadBalanced` 注解,开启 `@LoadBalanced` 与 [Ribbon](https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-ribbon.html) 的集成: + +```java +@SpringBootApplication +@EnableDiscoveryClient +public class NacosConsumerApplication { + + @LoadBalanced + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + public static void main(String[] args) { + SpringApplication.run(NacosConsumerApplication.class, args); + } + + @RestController + public class TestController { + + private final RestTemplate restTemplate; + + @Autowired + public TestController(RestTemplate restTemplate) {this.restTemplate = restTemplate;} + + @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET) + public String echo(@PathVariable String str) { + return restTemplate.getForObject("http://service-provider/echo/" + str, String.class); + } + } +} +``` + +**第四步**:启动 `ProviderApplication` 和 `ConsumerApplication` ,调用 `http://localhost:8080/echo/2018`,返回内容为 `Hello Nacos Discovery 2018`。 \ No newline at end of file diff --git a/src/Middleware/901.md b/src/Middleware/901.md new file mode 100644 index 0000000..9f8d9b5 --- /dev/null +++ b/src/Middleware/901.md @@ -0,0 +1,6 @@ +Sentinel(分布式系统的流量防卫兵)。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。Sentinel 具有以下特征: + +- **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等 +- **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况 +- **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel +- **完善的 SPI 扩展点**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等 \ No newline at end of file diff --git a/src/Middleware/902.md b/src/Middleware/902.md new file mode 100644 index 0000000..be924af --- /dev/null +++ b/src/Middleware/902.md @@ -0,0 +1,3 @@ +Sentinel 的主要特性: + +![Sentinel-features-overview](images/Middleware/Sentinel-features-overview.png) \ No newline at end of file diff --git a/src/Middleware/903.md b/src/Middleware/903.md new file mode 100644 index 0000000..c98a706 --- /dev/null +++ b/src/Middleware/903.md @@ -0,0 +1,8 @@ +Sentinel 的开源生态: + +![Sentinel-opensource-eco](images/Middleware/Sentinel-opensource-eco.png) + +Sentinel 分为两个部分: + +- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持 +- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器 \ No newline at end of file diff --git a/src/Middleware/904.md b/src/Middleware/904.md new file mode 100644 index 0000000..12d94fa --- /dev/null +++ b/src/Middleware/904.md @@ -0,0 +1,95 @@ +### 手动接入Sentinel以及控制台 + +下面例子将展示应用如何三步接入 Sentinel。同时,Sentinel 也提供所见即所得的控制台,可实时监控资源以及管理规则。 + +**STEP 1. 在应用中引入Sentinel Jar包** + +如果应用使用 pom 工程,则在 `pom.xml` 文件中加入以下代码即可: + +```xml + + com.alibaba.csp + sentinel-core + 1.8.1 + +``` + +注意: Sentinel仅支持JDK 1.8或者以上版本。如果未使用依赖管理工具,请到 [Maven Center Repository](https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-core) 直接下载JAR 包。 + + + +**STEP 2. 定义资源** + +接下来,我们把需要控制流量的代码用 Sentinel API `SphU.entry("HelloWorld")` 和 `entry.exit()` 包围起来即可。在下面的例子中,我们将 `System.out.println("hello world");` 这端代码作为资源,用 API 包围起来(埋点)。参考代码如下: + +```java +public static void main(String[] args) { + initFlowRules(); + while (true) { + Entry entry = null; + try { + entry = SphU.entry("HelloWorld"); + /*您的业务逻辑 - 开始*/ + System.out.println("hello world"); + /*您的业务逻辑 - 结束*/ + } catch (BlockException e1) { + /*流控逻辑处理 - 开始*/ + System.out.println("block!"); + /*流控逻辑处理 - 结束*/ + } finally { + if (entry != null) { + entry.exit(); + } + } + } +} +``` + +完成以上两步后,代码端的改造就完成了。当然,我们也提供了 [注解支持模块](https://github.com/alibaba/Sentinel/wiki/注解支持),可以以低侵入性的方式定义资源。 + + + +**STEP 3. 定义规则** + +接下来,通过规则来指定允许该资源通过的请求次数,如下面的代码定义了资源 `HelloWorld` 每秒最多只能通过 20 个请求。 + +```java +private static void initFlowRules(){ + List rules = new ArrayList<>(); + FlowRule rule = new FlowRule(); + rule.setResource("HelloWorld"); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + // Set limit QPS to 20. + rule.setCount(20); + rules.add(rule); + FlowRuleManager.loadRules(rules); +} +``` + +完成上面 3 步,Sentinel 就能够正常工作了。更多的信息可以参考 [使用文档](https://github.com/alibaba/Sentinel/wiki/如何使用)。 + + + +**STEP 4. 检查效果** + +Demo 运行之后,我们可以在日志 `~/logs/csp/${appName}-metrics.log.xxx` 里看到下面的输出: + +```shell +|--timestamp-|------date time----|-resource-|p |block|s |e|rt +1529998904000|2018-06-26 15:41:44|HelloWorld|20|0 |20|0|0 +1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728 +1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0 +1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0 +1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0 +1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0 +``` + +其中 `p` 代表通过的请求, `block` 代表被阻止的请求, `s` 代表成功执行完成的请求个数, `e` 代表用户自定义的异常, `rt` 代表平均响应时长。可以看到,这个程序每秒稳定输出 "hello world" 20 次,和规则中预先设定的阈值是一样的。 + + + +**STEP 5. 启动 Sentinel 控制台** + +您可以参考 [Sentinel 控制台文档](https://github.com/alibaba/Sentinel/wiki/控制台) 启动控制台,可以实时监控各个资源的运行情况,并且可以实时地修改限流规则。 + +![dashboard-monitoring](images/Middleware/dashboard-monitoring.png) \ No newline at end of file diff --git a/src/Middleware/README.md b/src/Middleware/README.md new file mode 100644 index 0000000..17859cb --- /dev/null +++ b/src/Middleware/README.md @@ -0,0 +1,5 @@ +
Middleware
+ +Introduction:收纳技术相关的基础知识 `Redis`、`RocketMQ`、`Zookeeper`、`Netty`、`Tomcat` 等总结! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/Middleware/_sidebar.md b/src/Middleware/_sidebar.md new file mode 100644 index 0000000..c771f8b --- /dev/null +++ b/src/Middleware/_sidebar.md @@ -0,0 +1,130 @@ +* [🏁SPI](src/Middleware/1 "SPI") + * [✍Java SPI](src/Middleware/2 "Java SPI") + * [✍Dubbo SPI](src/Middleware/3 "Dubbo SPI") + * [✍Motan SPI](src/Middleware/4 "Motan SPI") + * [✍SpringBoot SPI](src/Middleware/5 "SpringBoot SPI") +* [🏁Redis](src/Middleware/101 "Redis") + * [✍线程模型](src/Middleware/102 "线程模型") + * [✍数据类型](src/Middleware/103 "数据类型") + * [✍特殊数据结构](src/Middleware/104 "特殊数据结构") + * [✍底层数据结构](src/Middleware/105 "底层数据结构") + * [✍持久化机制](src/Middleware/106 "持久化机制") + * [✍过期策略](src/Middleware/107 "过期策略") + * [✍淘汰策略](src/Middleware/108 "淘汰策略") + * [✍部署架构](src/Middleware/109 "部署架构") + * [✍环境搭建](src/Middleware/110 "环境搭建") + * [✍拓展方案](src/Middleware/111 "拓展方案") + * [✍常见问题](src/Middleware/112 "常见问题") +* 🏁Kafka +* [🏁RocketMQ](src/Middleware/301 "RocketMQ") + * [✍架构设计](src/Middleware/302 "架构设计") + * [✍核心概念](src/Middleware/303 "核心概念") + * [✍核心设计](src/Middleware/304 "核心设计") + * [✍最佳实践](src/Middleware/305 "最佳实践") + * [✍保证顺序](src/Middleware/306 "保证顺序") + * [✍消息不丢失](src/Middleware/307 "消息不丢失") + * [✍消息幂等](src/Middleware/308 "消息幂等") +* [🏁Zookeeper](src/Middleware/401 "Zookeeper") + * [✍ZK特性](src/Middleware/402 "ZK特性") + * [✍ZK角色](src/Middleware/403 "ZK角色") + * [✍数据模型](src/Middleware/404 "数据模型") + * [✍Server工作状态](src/Middleware/405 "Server工作状态") + * [✍运行模式](src/Middleware/406 "运行模式") + * [✍Leader选举](src/Middleware/407 "Leader选举") + * [✍节点(znode)](src/Middleware/408 "节点(znode)") + * [✍关键词](src/Middleware/409 "关键词") + * [✍Watcher监听机制](src/Middleware/410 "Watcher监听机制") + * [✍ZXID](src/Middleware/411 "ZXID") + * [✍工作流程](src/Middleware/412 "工作流程") + * [✍ZAB协议](src/Middleware/413 "ZAB协议") + * [✍ZK选举过程](src/Middleware/414 "ZK选举过程") + * [✍Zookeeper安装](src/Middleware/415 "Zookeeper安装") +* 🏁Netty + * [✍Netty流程](src/Middleware/501 "Netty流程") + * [✍处理事件](src/Middleware/502 "处理事件") + * [✍长连接优化](src/Middleware/503 "长连接优化") + * [✍线程模型](src/Middleware/504 "线程模型") + * [✍HashedWheelTimer](src/Middleware/505 "HashedWheelTimer") + * [✍ByteBuf](src/Middleware/506 "ByteBuf") + * [✍Zero-Copy](src/Middleware/507 "Zero-Copy") + * [✍TCP粘包拆包](src/Middleware/508 "TCP粘包拆包") + * [✍高性能](src/Middleware/509 "高性能") + * [✍操作系统调优](src/Middleware/510 "操作系统调优") + * [✍Netty性能调优](src/Middleware/511 "Netty性能调优") + * [✍案例分析](src/Middleware/512 "案例分析") +* 🏁RabbitMQ + * [✍模式介绍](src/Middleware/601 "模式介绍") +* [🏁Dubbo](src/Middleware/701 "Dubbo") + * [✍RPC](src/Middleware/702 "RPC") + * [✍框架设计](src/Middleware/703 "框架设计") + * [✍SPI机制](src/Middleware/704 "SPI机制") + * [✍服务暴露流程](src/Middleware/705 "服务暴露流程") + * [✍服务引用流程](src/Middleware/706 "服务引用流程") + * [✍调用整体流程](src/Middleware/707 "调用整体流程") + * [✍集群容错负载均衡](src/Middleware/708 "集群容错负载均衡") + * [✍设计RPC](src/Middleware/709 "设计RPC") +* 🏁Nacos + * [✍基本架构](src/Middleware/801 "基本架构") + * [✍逻辑架构](src/Middleware/802 "逻辑架构") + * [✍功能特性](src/Middleware/803 "功能特性") + * [✍安装部署](src/Middleware/804 "安装部署") + * [✍开源案例](src/Middleware/805 "开源案例") +* [🏁Sentinel](src/Middleware/901 "Sentinel") + * [✍主要特性](src/Middleware/902 "主要特性") + * [✍开源生态](src/Middleware/903 "开源生态") + * [✍Quick Start](src/Middleware/904 "Quick Start") +* [🏁Influxdb](src/Middleware/1001 "Influxdb") + * [✍重要特性](src/Middleware/1002 "重要特性") + * [✍存储引擎](src/Middleware/1003 "存储引擎") + * [✍系统架构](src/Middleware/1004 "系统架构") +* [🏁Spring](src/Middleware/1101 "Spring") + * [✍相关概念](src/Middleware/1102 "相关概念") + * [✍Spring原理](src/Middleware/1103 "Spring原理") + * [✍IoC](src/Middleware/1104 "IoC") + * [✍AOP](src/Middleware/1105 "AOP") + * [✍过滤器(Filter)](src/Middleware/1106 "过滤器(Filter)") + * [✍拦截器(Interceptor)](src/Middleware/1107 "拦截器(Interceptor)") + * [✍Spring流程](src/Middleware/1108 "Spring流程") +* [🏁SpringBoot](src/Middleware/1201 "SpringBoot") + * [✍MVC](src/Middleware/1202 "MVC") + * [✍IoC](src/Middleware/1203 "IoC") + * [✍AOP](src/Middleware/1204 "AOP") + * [✍Others](src/Middleware/1205 "Others") +* [🏁SpringCloud](src/Middleware/1301 "SpringCloud") + * [✍Eurake](src/Middleware/1302 "Eurake") + * [✍Zuul](src/Middleware/1303 "Zuul") + * [✍Feign](src/Middleware/1304 "Feign") + * [✍Ribbon](src/Middleware/1305 "Ribbon") + * [✍Hystrix](src/Middleware/1306 "Hystrix") + * [✍Gateway](src/Middleware/1307 "Gateway") + * [✍Config](src/Middleware/1308 "Config") + * [✍Bus](src/Middleware/1309 "Bus") + * [✍OAuth2](src/Middleware/1310 "OAuth2") + * [✍Sleuth](src/Middleware/1311 "Sleuth") +* [🏁MyBatis](src/Middleware/1401 "MyBatis") + * [✍MyBatis架构](src/Middleware/1402 "MyBatis架构") + * [✍MyBatis流程](src/Middleware/1403 "MyBatis流程") + * [✍Dao接口工作原理](src/Middleware/1404 "Dao接口工作原理") + * [✍MyBatis缓存](src/Middleware/1405 "MyBatis缓存") + * [✍MyBatis主要组件](src/Middleware/1406 "MyBatis主要组件") +* 🏁Nginx + * [✍Nginx安装](src/Middleware/1501 "Nginx安装") + * [✍运维命令](src/Middleware/1502 "运维命令") + * [✍配置规则](src/Middleware/1503 "配置规则") + * [✍应用场景](src/Middleware/1504 "应用场景") + * [✍常用配置](src/Middleware/1505 "常用配置") + * [✍日志分析](src/Middleware/1506 "日志分析") +* [🏁LVS](src/Middleware/1601 "LVS") + * [✍工作模式](src/Middleware/1602 "工作模式") + * [✍调度算法](src/Middleware/1603 "调度算法") + * [✍常见问题分析](src/Middleware/1604 "常见问题分析") +* [🏁Keepalived](src/Middleware/1701 "Keepalived") + * [✍功能体系](src/Middleware/1702 "功能体系") + * [✍脑裂问题](src/Middleware/1703 "脑裂问题") + * [✍安装部署](src/Middleware/1704 "安装部署") +* [🏁HAProxy](src/Middleware/1801 "HAProxy") + * [✍四层负载均衡](src/Middleware/1802 "四层负载均衡") + * [✍七层负载均衡](src/Middleware/1803 "七层负载均衡") + * [✍负载均衡策略](src/Middleware/1804 "负载均衡策略") + * [✍HAProxy与LVS的区别](src/Middleware/1805 "HAProxy与LVS的区别") + * [✍安装配置](src/Middleware/1806 "安装配置") \ No newline at end of file diff --git a/src/Middleware/images/Middleware/1090617-20190626233829426-1023022108.png b/src/Middleware/images/Middleware/1090617-20190626233829426-1023022108.png new file mode 100644 index 0000000..60dd6f3 Binary files /dev/null and b/src/Middleware/images/Middleware/1090617-20190626233829426-1023022108.png differ diff --git a/src/Middleware/images/Middleware/471426-20180519131211273-554395305.png b/src/Middleware/images/Middleware/471426-20180519131211273-554395305.png new file mode 100644 index 0000000..301b2e3 Binary files /dev/null and b/src/Middleware/images/Middleware/471426-20180519131211273-554395305.png differ diff --git a/src/Middleware/images/Middleware/ApacheDubbo.jpg b/src/Middleware/images/Middleware/ApacheDubbo.jpg new file mode 100644 index 0000000..6892500 Binary files /dev/null and b/src/Middleware/images/Middleware/ApacheDubbo.jpg differ diff --git a/src/Middleware/images/Middleware/Bus介绍.png b/src/Middleware/images/Middleware/Bus介绍.png new file mode 100644 index 0000000..f915c00 Binary files /dev/null and b/src/Middleware/images/Middleware/Bus介绍.png differ diff --git a/src/Middleware/images/Middleware/ByteBuf-get.png b/src/Middleware/images/Middleware/ByteBuf-get.png new file mode 100644 index 0000000..429830d Binary files /dev/null and b/src/Middleware/images/Middleware/ByteBuf-get.png differ diff --git a/src/Middleware/images/Middleware/ByteBuf-更多操作.png b/src/Middleware/images/Middleware/ByteBuf-更多操作.png new file mode 100644 index 0000000..081fff0 Binary files /dev/null and b/src/Middleware/images/Middleware/ByteBuf-更多操作.png differ diff --git a/src/Middleware/images/Middleware/ByteBufAllocator.png b/src/Middleware/images/Middleware/ByteBufAllocator.png new file mode 100644 index 0000000..3314673 Binary files /dev/null and b/src/Middleware/images/Middleware/ByteBufAllocator.png differ diff --git a/src/Middleware/images/Middleware/ByteBuf工作流程.png b/src/Middleware/images/Middleware/ByteBuf工作流程.png new file mode 100644 index 0000000..ca25e48 Binary files /dev/null and b/src/Middleware/images/Middleware/ByteBuf工作流程.png differ diff --git a/src/Middleware/images/Middleware/ByteBuf顺序访问索引.png b/src/Middleware/images/Middleware/ByteBuf顺序访问索引.png new file mode 100644 index 0000000..a690be2 Binary files /dev/null and b/src/Middleware/images/Middleware/ByteBuf顺序访问索引.png differ diff --git a/src/Middleware/images/Middleware/CP粘包拆包图解.png b/src/Middleware/images/Middleware/CP粘包拆包图解.png new file mode 100644 index 0000000..393c903 Binary files /dev/null and b/src/Middleware/images/Middleware/CP粘包拆包图解.png differ diff --git a/src/Middleware/images/Middleware/Client端NioEventLoop处理的事件.png b/src/Middleware/images/Middleware/Client端NioEventLoop处理的事件.png new file mode 100644 index 0000000..5482b6f Binary files /dev/null and b/src/Middleware/images/Middleware/Client端NioEventLoop处理的事件.png differ diff --git a/src/Middleware/images/Middleware/CompositeBuffer.png b/src/Middleware/images/Middleware/CompositeBuffer.png new file mode 100644 index 0000000..e3c83e5 Binary files /dev/null and b/src/Middleware/images/Middleware/CompositeBuffer.png differ diff --git a/src/Middleware/images/Middleware/CompositeByteBuf实现零拷贝.png b/src/Middleware/images/Middleware/CompositeByteBuf实现零拷贝.png new file mode 100644 index 0000000..5cb8d9f Binary files /dev/null and b/src/Middleware/images/Middleware/CompositeByteBuf实现零拷贝.png differ diff --git a/src/Middleware/images/Middleware/Config介绍.png b/src/Middleware/images/Middleware/Config介绍.png new file mode 100644 index 0000000..a9cf94b Binary files /dev/null and b/src/Middleware/images/Middleware/Config介绍.png differ diff --git a/src/Middleware/images/Middleware/ConsistentHashLoadBalance.png b/src/Middleware/images/Middleware/ConsistentHashLoadBalance.png new file mode 100644 index 0000000..0b47622 Binary files /dev/null and b/src/Middleware/images/Middleware/ConsistentHashLoadBalance.png differ diff --git a/src/Middleware/images/Middleware/DubboArchitecture.png b/src/Middleware/images/Middleware/DubboArchitecture.png new file mode 100644 index 0000000..530fd53 Binary files /dev/null and b/src/Middleware/images/Middleware/DubboArchitecture.png differ diff --git a/src/Middleware/images/Middleware/DubboSPI源码追踪.png b/src/Middleware/images/Middleware/DubboSPI源码追踪.png new file mode 100644 index 0000000..785beb8 Binary files /dev/null and b/src/Middleware/images/Middleware/DubboSPI源码追踪.png differ diff --git a/src/Middleware/images/Middleware/Dubbo框架分层.jpg b/src/Middleware/images/Middleware/Dubbo框架分层.jpg new file mode 100644 index 0000000..17757af Binary files /dev/null and b/src/Middleware/images/Middleware/Dubbo框架分层.jpg differ diff --git a/src/Middleware/images/Middleware/Dubbo调用关系.jpg b/src/Middleware/images/Middleware/Dubbo调用关系.jpg new file mode 100644 index 0000000..94942a3 Binary files /dev/null and b/src/Middleware/images/Middleware/Dubbo调用关系.jpg differ diff --git a/src/Middleware/images/Middleware/Dubbo集群容错负载均衡.jpg b/src/Middleware/images/Middleware/Dubbo集群容错负载均衡.jpg new file mode 100644 index 0000000..6d7d519 Binary files /dev/null and b/src/Middleware/images/Middleware/Dubbo集群容错负载均衡.jpg differ diff --git a/src/Middleware/images/Middleware/Eurake.png b/src/Middleware/images/Middleware/Eurake.png new file mode 100644 index 0000000..164da6e Binary files /dev/null and b/src/Middleware/images/Middleware/Eurake.png differ diff --git a/src/Middleware/images/Middleware/Eurake介绍.png b/src/Middleware/images/Middleware/Eurake介绍.png new file mode 100644 index 0000000..14d31f0 Binary files /dev/null and b/src/Middleware/images/Middleware/Eurake介绍.png differ diff --git a/src/Middleware/images/Middleware/EurekaServer触发自我保护机制.png b/src/Middleware/images/Middleware/EurekaServer触发自我保护机制.png new file mode 100644 index 0000000..4e49f53 Binary files /dev/null and b/src/Middleware/images/Middleware/EurekaServer触发自我保护机制.png differ diff --git a/src/Middleware/images/Middleware/Eureka集群原理.png b/src/Middleware/images/Middleware/Eureka集群原理.png new file mode 100644 index 0000000..84daed6 Binary files /dev/null and b/src/Middleware/images/Middleware/Eureka集群原理.png differ diff --git a/src/Middleware/images/Middleware/Feign.png b/src/Middleware/images/Middleware/Feign.png new file mode 100644 index 0000000..385fc5d Binary files /dev/null and b/src/Middleware/images/Middleware/Feign.png differ diff --git a/src/Middleware/images/Middleware/Feign介绍.png b/src/Middleware/images/Middleware/Feign介绍.png new file mode 100644 index 0000000..e6472aa Binary files /dev/null and b/src/Middleware/images/Middleware/Feign介绍.png differ diff --git a/src/Middleware/images/Middleware/Feign动态代理.png b/src/Middleware/images/Middleware/Feign动态代理.png new file mode 100644 index 0000000..8867eeb Binary files /dev/null and b/src/Middleware/images/Middleware/Feign动态代理.png differ diff --git a/src/Middleware/images/Middleware/Feign参数编码整体流程.png b/src/Middleware/images/Middleware/Feign参数编码整体流程.png new file mode 100644 index 0000000..145c0cb Binary files /dev/null and b/src/Middleware/images/Middleware/Feign参数编码整体流程.png differ diff --git a/src/Middleware/images/Middleware/Feign整体装配流程.png b/src/Middleware/images/Middleware/Feign整体装配流程.png new file mode 100644 index 0000000..e400939 Binary files /dev/null and b/src/Middleware/images/Middleware/Feign整体装配流程.png differ diff --git a/src/Middleware/images/Middleware/Feign整体设计.png b/src/Middleware/images/Middleware/Feign整体设计.png new file mode 100644 index 0000000..db43f96 Binary files /dev/null and b/src/Middleware/images/Middleware/Feign整体设计.png differ diff --git a/src/Middleware/images/Middleware/Feign远程调用流程.png b/src/Middleware/images/Middleware/Feign远程调用流程.png new file mode 100644 index 0000000..bb3c87d Binary files /dev/null and b/src/Middleware/images/Middleware/Feign远程调用流程.png differ diff --git a/src/Middleware/images/Middleware/Gateway介绍.png b/src/Middleware/images/Middleware/Gateway介绍.png new file mode 100644 index 0000000..3e2105e Binary files /dev/null and b/src/Middleware/images/Middleware/Gateway介绍.png differ diff --git a/src/Middleware/images/Middleware/HashedWheelTimer.png b/src/Middleware/images/Middleware/HashedWheelTimer.png new file mode 100644 index 0000000..9e7416a Binary files /dev/null and b/src/Middleware/images/Middleware/HashedWheelTimer.png differ diff --git a/src/Middleware/images/Middleware/Hystrix.png b/src/Middleware/images/Middleware/Hystrix.png new file mode 100644 index 0000000..b57baa2 Binary files /dev/null and b/src/Middleware/images/Middleware/Hystrix.png differ diff --git a/src/Middleware/images/Middleware/Hystrix介绍.png b/src/Middleware/images/Middleware/Hystrix介绍.png new file mode 100644 index 0000000..bc724ee Binary files /dev/null and b/src/Middleware/images/Middleware/Hystrix介绍.png differ diff --git a/src/Middleware/images/Middleware/Hystrix熔断.png b/src/Middleware/images/Middleware/Hystrix熔断.png new file mode 100644 index 0000000..91a721d Binary files /dev/null and b/src/Middleware/images/Middleware/Hystrix熔断.png differ diff --git a/src/Middleware/images/Middleware/InfluxDB系统架构.jpg b/src/Middleware/images/Middleware/InfluxDB系统架构.jpg new file mode 100644 index 0000000..b11593b Binary files /dev/null and b/src/Middleware/images/Middleware/InfluxDB系统架构.jpg differ diff --git a/src/Middleware/images/Middleware/Keepalived体系结构.jpg b/src/Middleware/images/Middleware/Keepalived体系结构.jpg new file mode 100644 index 0000000..ce09c6f Binary files /dev/null and b/src/Middleware/images/Middleware/Keepalived体系结构.jpg differ diff --git a/src/Middleware/images/Middleware/LVS-DR-IP.png b/src/Middleware/images/Middleware/LVS-DR-IP.png new file mode 100644 index 0000000..cbe868f Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-DR-IP.png differ diff --git a/src/Middleware/images/Middleware/LVS-DR-STR.png b/src/Middleware/images/Middleware/LVS-DR-STR.png new file mode 100644 index 0000000..2ef3331 Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-DR-STR.png differ diff --git a/src/Middleware/images/Middleware/LVS-DR.png b/src/Middleware/images/Middleware/LVS-DR.png new file mode 100644 index 0000000..5ab7808 Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-DR.png differ diff --git a/src/Middleware/images/Middleware/LVS-ENAT-STR.png b/src/Middleware/images/Middleware/LVS-ENAT-STR.png new file mode 100644 index 0000000..a21ab23 Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-ENAT-STR.png differ diff --git a/src/Middleware/images/Middleware/LVS-NAT-IP.png b/src/Middleware/images/Middleware/LVS-NAT-IP.png new file mode 100644 index 0000000..0c39234 Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-NAT-IP.png differ diff --git a/src/Middleware/images/Middleware/LVS-NAT-STR.png b/src/Middleware/images/Middleware/LVS-NAT-STR.png new file mode 100644 index 0000000..50f733a Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-NAT-STR.png differ diff --git a/src/Middleware/images/Middleware/LVS-NAT.png b/src/Middleware/images/Middleware/LVS-NAT.png new file mode 100644 index 0000000..51d948c Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-NAT.png differ diff --git a/src/Middleware/images/Middleware/LVS-TP-TUN-STR.png b/src/Middleware/images/Middleware/LVS-TP-TUN-STR.png new file mode 100644 index 0000000..31a1863 Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-TP-TUN-STR.png differ diff --git a/src/Middleware/images/Middleware/LVS-full-NAT-STR.png b/src/Middleware/images/Middleware/LVS-full-NAT-STR.png new file mode 100644 index 0000000..381819f Binary files /dev/null and b/src/Middleware/images/Middleware/LVS-full-NAT-STR.png differ diff --git a/src/Middleware/images/Middleware/LengthFieldBasedFrameDecoder.png b/src/Middleware/images/Middleware/LengthFieldBasedFrameDecoder.png new file mode 100644 index 0000000..0f5c06b Binary files /dev/null and b/src/Middleware/images/Middleware/LengthFieldBasedFrameDecoder.png differ diff --git a/src/Middleware/images/Middleware/LengthFieldPrepender.png b/src/Middleware/images/Middleware/LengthFieldPrepender.png new file mode 100644 index 0000000..554be87 Binary files /dev/null and b/src/Middleware/images/Middleware/LengthFieldPrepender.png differ diff --git a/src/Middleware/images/Middleware/MQ与DB一致性原理.png b/src/Middleware/images/Middleware/MQ与DB一致性原理.png new file mode 100644 index 0000000..5099b17 Binary files /dev/null and b/src/Middleware/images/Middleware/MQ与DB一致性原理.png differ diff --git a/src/Middleware/images/Middleware/MyBatis-Executor.png b/src/Middleware/images/Middleware/MyBatis-Executor.png new file mode 100644 index 0000000..c0dcde3 Binary files /dev/null and b/src/Middleware/images/Middleware/MyBatis-Executor.png differ diff --git a/src/Middleware/images/Middleware/MyBatis执行sql流程.png b/src/Middleware/images/Middleware/MyBatis执行sql流程.png new file mode 100644 index 0000000..20dcd77 Binary files /dev/null and b/src/Middleware/images/Middleware/MyBatis执行sql流程.png differ diff --git a/src/Middleware/images/Middleware/MyBatis缓存机制.png b/src/Middleware/images/Middleware/MyBatis缓存机制.png new file mode 100644 index 0000000..ce0a368 Binary files /dev/null and b/src/Middleware/images/Middleware/MyBatis缓存机制.png differ diff --git a/src/Middleware/images/Middleware/Mybatis核心成员数据流.png b/src/Middleware/images/Middleware/Mybatis核心成员数据流.png new file mode 100644 index 0000000..37faff8 Binary files /dev/null and b/src/Middleware/images/Middleware/Mybatis核心成员数据流.png differ diff --git a/src/Middleware/images/Middleware/Mybatis流程.png b/src/Middleware/images/Middleware/Mybatis流程.png new file mode 100644 index 0000000..cf6a289 Binary files /dev/null and b/src/Middleware/images/Middleware/Mybatis流程.png differ diff --git a/src/Middleware/images/Middleware/Nacos架构图.jpeg b/src/Middleware/images/Middleware/Nacos架构图.jpeg new file mode 100644 index 0000000..16832c4 Binary files /dev/null and b/src/Middleware/images/Middleware/Nacos架构图.jpeg differ diff --git a/src/Middleware/images/Middleware/Nacos逻辑架构及其组件介绍.png b/src/Middleware/images/Middleware/Nacos逻辑架构及其组件介绍.png new file mode 100644 index 0000000..3de0f56 Binary files /dev/null and b/src/Middleware/images/Middleware/Nacos逻辑架构及其组件介绍.png differ diff --git a/src/Middleware/images/Middleware/Netty-Reactor.png b/src/Middleware/images/Middleware/Netty-Reactor.png new file mode 100644 index 0000000..bf1317a Binary files /dev/null and b/src/Middleware/images/Middleware/Netty-Reactor.png differ diff --git a/src/Middleware/images/Middleware/Netty整体流程.png b/src/Middleware/images/Middleware/Netty整体流程.png new file mode 100644 index 0000000..5484937 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty整体流程.png differ diff --git a/src/Middleware/images/Middleware/Netty流程-关闭服务.jpg b/src/Middleware/images/Middleware/Netty流程-关闭服务.jpg new file mode 100644 index 0000000..3e4feb0 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty流程-关闭服务.jpg differ diff --git a/src/Middleware/images/Middleware/Netty流程-关闭连接.jpg b/src/Middleware/images/Middleware/Netty流程-关闭连接.jpg new file mode 100644 index 0000000..0228484 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty流程-关闭连接.jpg differ diff --git a/src/Middleware/images/Middleware/Netty流程-建立连接.jpg b/src/Middleware/images/Middleware/Netty流程-建立连接.jpg new file mode 100644 index 0000000..7d815b0 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty流程-建立连接.jpg differ diff --git a/src/Middleware/images/Middleware/Netty流程-服务启动.jpg b/src/Middleware/images/Middleware/Netty流程-服务启动.jpg new file mode 100644 index 0000000..786a401 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty流程-服务启动.jpg differ diff --git a/src/Middleware/images/Middleware/Netty流程-读写与业务处理.jpg b/src/Middleware/images/Middleware/Netty流程-读写与业务处理.jpg new file mode 100644 index 0000000..3a67299 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty流程-读写与业务处理.jpg differ diff --git a/src/Middleware/images/Middleware/Netty线程模型.png b/src/Middleware/images/Middleware/Netty线程模型.png new file mode 100644 index 0000000..dbeaf30 Binary files /dev/null and b/src/Middleware/images/Middleware/Netty线程模型.png differ diff --git a/src/Middleware/images/Middleware/Nginx层级结构.png b/src/Middleware/images/Middleware/Nginx层级结构.png new file mode 100644 index 0000000..e2919bb Binary files /dev/null and b/src/Middleware/images/Middleware/Nginx层级结构.png differ diff --git a/src/Middleware/images/Middleware/OAuth2介绍.png b/src/Middleware/images/Middleware/OAuth2介绍.png new file mode 100644 index 0000000..027e525 Binary files /dev/null and b/src/Middleware/images/Middleware/OAuth2介绍.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF-Always.png b/src/Middleware/images/Middleware/Redis-AOF-Always.png new file mode 100644 index 0000000..2018959 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF-Always.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF-Everysec.png b/src/Middleware/images/Middleware/Redis-AOF-Everysec.png new file mode 100644 index 0000000..c814bda Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF-Everysec.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF-No.png b/src/Middleware/images/Middleware/Redis-AOF-No.png new file mode 100644 index 0000000..63fef42 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF-No.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF-后台重写.png b/src/Middleware/images/Middleware/Redis-AOF-后台重写.png new file mode 100644 index 0000000..74e7a83 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF-后台重写.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF.png b/src/Middleware/images/Middleware/Redis-AOF.png new file mode 100644 index 0000000..17d2796 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF.png differ diff --git a/src/Middleware/images/Middleware/Redis-AOF持久化流程.png b/src/Middleware/images/Middleware/Redis-AOF持久化流程.png new file mode 100644 index 0000000..0fb3351 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-AOF持久化流程.png differ diff --git a/src/Middleware/images/Middleware/Redis-BloomFilter.jpg b/src/Middleware/images/Middleware/Redis-BloomFilter.jpg new file mode 100644 index 0000000..cf396cd Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-BloomFilter.jpg differ diff --git a/src/Middleware/images/Middleware/Redis-Hash.png b/src/Middleware/images/Middleware/Redis-Hash.png new file mode 100644 index 0000000..c988718 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-Hash.png differ diff --git a/src/Middleware/images/Middleware/Redis-List.png b/src/Middleware/images/Middleware/Redis-List.png new file mode 100644 index 0000000..e4a57fd Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-List.png differ diff --git a/src/Middleware/images/Middleware/Redis-RDB-BgSave命令.png b/src/Middleware/images/Middleware/Redis-RDB-BgSave命令.png new file mode 100644 index 0000000..591f152 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-RDB-BgSave命令.png differ diff --git a/src/Middleware/images/Middleware/Redis-RDB-Save命令.png b/src/Middleware/images/Middleware/Redis-RDB-Save命令.png new file mode 100644 index 0000000..60f6c00 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-RDB-Save命令.png differ diff --git a/src/Middleware/images/Middleware/Redis-RDB-创建.png b/src/Middleware/images/Middleware/Redis-RDB-创建.png new file mode 100644 index 0000000..b492f8d Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-RDB-创建.png differ diff --git a/src/Middleware/images/Middleware/Redis-RDB-载入.png b/src/Middleware/images/Middleware/Redis-RDB-载入.png new file mode 100644 index 0000000..7deeda9 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-RDB-载入.png differ diff --git a/src/Middleware/images/Middleware/Redis-Set.png b/src/Middleware/images/Middleware/Redis-Set.png new file mode 100644 index 0000000..c800774 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-Set.png differ diff --git a/src/Middleware/images/Middleware/Redis-SortedSet.png b/src/Middleware/images/Middleware/Redis-SortedSet.png new file mode 100644 index 0000000..17fd1b5 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-SortedSet.png differ diff --git a/src/Middleware/images/Middleware/Redis-基本数据类型.png b/src/Middleware/images/Middleware/Redis-基本数据类型.png new file mode 100644 index 0000000..658c1da Binary files /dev/null and b/src/Middleware/images/Middleware/Redis-基本数据类型.png differ diff --git a/src/Middleware/images/Middleware/Redis主从复制模式(Replication).png b/src/Middleware/images/Middleware/Redis主从复制模式(Replication).png new file mode 100644 index 0000000..6b7fc20 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis主从复制模式(Replication).png differ diff --git a/src/Middleware/images/Middleware/Redis主从复制模式(Replication)优缺点.png b/src/Middleware/images/Middleware/Redis主从复制模式(Replication)优缺点.png new file mode 100644 index 0000000..4fd716b Binary files /dev/null and b/src/Middleware/images/Middleware/Redis主从复制模式(Replication)优缺点.png differ diff --git a/src/Middleware/images/Middleware/Redis全局hash字典.png b/src/Middleware/images/Middleware/Redis全局hash字典.png new file mode 100644 index 0000000..068f484 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis全局hash字典.png differ diff --git a/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel).png b/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel).png new file mode 100644 index 0000000..e0ecca8 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel).png differ diff --git a/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel)优缺点.png b/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel)优缺点.png new file mode 100644 index 0000000..d85d3b6 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis哨兵模式(Sentinel)优缺点.png differ diff --git a/src/Middleware/images/Middleware/Redis数据类型与底层数据结构关系.png b/src/Middleware/images/Middleware/Redis数据类型与底层数据结构关系.png new file mode 100644 index 0000000..2d2de41 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis数据类型与底层数据结构关系.png differ diff --git a/src/Middleware/images/Middleware/Redis文件事件处理器.png b/src/Middleware/images/Middleware/Redis文件事件处理器.png new file mode 100644 index 0000000..961c618 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis文件事件处理器.png differ diff --git a/src/Middleware/images/Middleware/Redis线程模型.png b/src/Middleware/images/Middleware/Redis线程模型.png new file mode 100644 index 0000000..2a2716d Binary files /dev/null and b/src/Middleware/images/Middleware/Redis线程模型.png differ diff --git a/src/Middleware/images/Middleware/Redis请求过程.png b/src/Middleware/images/Middleware/Redis请求过程.png new file mode 100644 index 0000000..9f74ce8 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis请求过程.png differ diff --git a/src/Middleware/images/Middleware/Redis集群模式(Cluster).png b/src/Middleware/images/Middleware/Redis集群模式(Cluster).png new file mode 100644 index 0000000..9f00fb2 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis集群模式(Cluster).png differ diff --git a/src/Middleware/images/Middleware/Redis集群模式(Cluster)优缺点.png b/src/Middleware/images/Middleware/Redis集群模式(Cluster)优缺点.png new file mode 100644 index 0000000..3402326 Binary files /dev/null and b/src/Middleware/images/Middleware/Redis集群模式(Cluster)优缺点.png differ diff --git a/src/Middleware/images/Middleware/Ribbon.png b/src/Middleware/images/Middleware/Ribbon.png new file mode 100644 index 0000000..2955386 Binary files /dev/null and b/src/Middleware/images/Middleware/Ribbon.png differ diff --git a/src/Middleware/images/Middleware/Ribbon介绍.png b/src/Middleware/images/Middleware/Ribbon介绍.png new file mode 100644 index 0000000..3b7c6e7 Binary files /dev/null and b/src/Middleware/images/Middleware/Ribbon介绍.png differ diff --git a/src/Middleware/images/Middleware/Ribbon规则.png b/src/Middleware/images/Middleware/Ribbon规则.png new file mode 100644 index 0000000..3c5ad1c Binary files /dev/null and b/src/Middleware/images/Middleware/Ribbon规则.png differ diff --git a/src/Middleware/images/Middleware/RocketMQ-Offset.png b/src/Middleware/images/Middleware/RocketMQ-Offset.png new file mode 100644 index 0000000..e5bf396 Binary files /dev/null and b/src/Middleware/images/Middleware/RocketMQ-Offset.png differ diff --git a/src/Middleware/images/Middleware/RocketMQ事务消息.jpg b/src/Middleware/images/Middleware/RocketMQ事务消息.jpg new file mode 100644 index 0000000..cba58d7 Binary files /dev/null and b/src/Middleware/images/Middleware/RocketMQ事务消息.jpg differ diff --git a/src/Middleware/images/Middleware/RocketMQ实现原理.jpg b/src/Middleware/images/Middleware/RocketMQ实现原理.jpg new file mode 100644 index 0000000..fd98817 Binary files /dev/null and b/src/Middleware/images/Middleware/RocketMQ实现原理.jpg differ diff --git a/src/Middleware/images/Middleware/RocketMQ消息正确订阅关系.png b/src/Middleware/images/Middleware/RocketMQ消息正确订阅关系.png new file mode 100644 index 0000000..6bfa51d Binary files /dev/null and b/src/Middleware/images/Middleware/RocketMQ消息正确订阅关系.png differ diff --git a/src/Middleware/images/Middleware/Rocket消息丢失.jpg b/src/Middleware/images/Middleware/Rocket消息丢失.jpg new file mode 100644 index 0000000..3c0aeaa Binary files /dev/null and b/src/Middleware/images/Middleware/Rocket消息丢失.jpg differ diff --git a/src/Middleware/images/Middleware/SDS简单动态字符.png b/src/Middleware/images/Middleware/SDS简单动态字符.png new file mode 100644 index 0000000..c0937ab Binary files /dev/null and b/src/Middleware/images/Middleware/SDS简单动态字符.png differ diff --git a/src/Middleware/images/Middleware/Sentinel-features-overview.png b/src/Middleware/images/Middleware/Sentinel-features-overview.png new file mode 100644 index 0000000..974649b Binary files /dev/null and b/src/Middleware/images/Middleware/Sentinel-features-overview.png differ diff --git a/src/Middleware/images/Middleware/Sentinel-opensource-eco.png b/src/Middleware/images/Middleware/Sentinel-opensource-eco.png new file mode 100644 index 0000000..2d5f6a5 Binary files /dev/null and b/src/Middleware/images/Middleware/Sentinel-opensource-eco.png differ diff --git a/src/Middleware/images/Middleware/Server端NioEventLoop处理的事件.png b/src/Middleware/images/Middleware/Server端NioEventLoop处理的事件.png new file mode 100644 index 0000000..82aefb5 Binary files /dev/null and b/src/Middleware/images/Middleware/Server端NioEventLoop处理的事件.png differ diff --git a/src/Middleware/images/Middleware/Sleuth介绍.png b/src/Middleware/images/Middleware/Sleuth介绍.png new file mode 100644 index 0000000..f344846 Binary files /dev/null and b/src/Middleware/images/Middleware/Sleuth介绍.png differ diff --git a/src/Middleware/images/Middleware/SpringCloud.png b/src/Middleware/images/Middleware/SpringCloud.png new file mode 100644 index 0000000..d9dfa61 Binary files /dev/null and b/src/Middleware/images/Middleware/SpringCloud.png differ diff --git a/src/Middleware/images/Middleware/SpringMVC工作原理.png b/src/Middleware/images/Middleware/SpringMVC工作原理.png new file mode 100644 index 0000000..ab52726 Binary files /dev/null and b/src/Middleware/images/Middleware/SpringMVC工作原理.png differ diff --git a/src/Middleware/images/Middleware/SpringMVC流程.jpg b/src/Middleware/images/Middleware/SpringMVC流程.jpg new file mode 100644 index 0000000..7e96021 Binary files /dev/null and b/src/Middleware/images/Middleware/SpringMVC流程.jpg differ diff --git a/src/Middleware/images/Middleware/Spring主要包.png b/src/Middleware/images/Middleware/Spring主要包.png new file mode 100644 index 0000000..10c99ea Binary files /dev/null and b/src/Middleware/images/Middleware/Spring主要包.png differ diff --git a/src/Middleware/images/Middleware/Spring关系.jpg b/src/Middleware/images/Middleware/Spring关系.jpg new file mode 100644 index 0000000..d6afc2b Binary files /dev/null and b/src/Middleware/images/Middleware/Spring关系.jpg differ diff --git a/src/Middleware/images/Middleware/Spring常用模块.png b/src/Middleware/images/Middleware/Spring常用模块.png new file mode 100644 index 0000000..ec60efe Binary files /dev/null and b/src/Middleware/images/Middleware/Spring常用模块.png differ diff --git a/src/Middleware/images/Middleware/Spring常用注解.png b/src/Middleware/images/Middleware/Spring常用注解.png new file mode 100644 index 0000000..a22b43a Binary files /dev/null and b/src/Middleware/images/Middleware/Spring常用注解.png differ diff --git a/src/Middleware/images/Middleware/Spring核心组件.png b/src/Middleware/images/Middleware/Spring核心组件.png new file mode 100644 index 0000000..451f163 Binary files /dev/null and b/src/Middleware/images/Middleware/Spring核心组件.png differ diff --git a/src/Middleware/images/Middleware/Unpooled缓冲区.png b/src/Middleware/images/Middleware/Unpooled缓冲区.png new file mode 100644 index 0000000..08ba702 Binary files /dev/null and b/src/Middleware/images/Middleware/Unpooled缓冲区.png differ diff --git a/src/Middleware/images/Middleware/Watcher监听机制的工作原理.png b/src/Middleware/images/Middleware/Watcher监听机制的工作原理.png new file mode 100644 index 0000000..e0cc1cd Binary files /dev/null and b/src/Middleware/images/Middleware/Watcher监听机制的工作原理.png differ diff --git a/src/Middleware/images/Middleware/ZipList压缩列表.png b/src/Middleware/images/Middleware/ZipList压缩列表.png new file mode 100644 index 0000000..7084537 Binary files /dev/null and b/src/Middleware/images/Middleware/ZipList压缩列表.png differ diff --git a/src/Middleware/images/Middleware/Zookeeper-ZXID.png b/src/Middleware/images/Middleware/Zookeeper-ZXID.png new file mode 100644 index 0000000..b5f61ab Binary files /dev/null and b/src/Middleware/images/Middleware/Zookeeper-ZXID.png differ diff --git a/src/Middleware/images/Middleware/Zookeeper中的角色.jpg b/src/Middleware/images/Middleware/Zookeeper中的角色.jpg new file mode 100644 index 0000000..50ede62 Binary files /dev/null and b/src/Middleware/images/Middleware/Zookeeper中的角色.jpg differ diff --git a/src/Middleware/images/Middleware/Zookeeper的数据模型.jpg b/src/Middleware/images/Middleware/Zookeeper的数据模型.jpg new file mode 100644 index 0000000..511fdf9 Binary files /dev/null and b/src/Middleware/images/Middleware/Zookeeper的数据模型.jpg differ diff --git a/src/Middleware/images/Middleware/Zuul.png b/src/Middleware/images/Middleware/Zuul.png new file mode 100644 index 0000000..15b2c35 Binary files /dev/null and b/src/Middleware/images/Middleware/Zuul.png differ diff --git a/src/Middleware/images/Middleware/Zuul介绍.png b/src/Middleware/images/Middleware/Zuul介绍.png new file mode 100644 index 0000000..b1c4cea Binary files /dev/null and b/src/Middleware/images/Middleware/Zuul介绍.png differ diff --git a/src/Middleware/images/Middleware/dashboard-monitoring.png b/src/Middleware/images/Middleware/dashboard-monitoring.png new file mode 100644 index 0000000..f1ab9b9 Binary files /dev/null and b/src/Middleware/images/Middleware/dashboard-monitoring.png differ diff --git a/src/Middleware/images/Middleware/echo_service.png b/src/Middleware/images/Middleware/echo_service.png new file mode 100644 index 0000000..dc43903 Binary files /dev/null and b/src/Middleware/images/Middleware/echo_service.png differ diff --git a/src/Middleware/images/Middleware/nacos_config_er.jpeg b/src/Middleware/images/Middleware/nacos_config_er.jpeg new file mode 100644 index 0000000..aaadc8e Binary files /dev/null and b/src/Middleware/images/Middleware/nacos_config_er.jpeg differ diff --git a/src/Middleware/images/Middleware/nacos_data_model.jpeg b/src/Middleware/images/Middleware/nacos_data_model.jpeg new file mode 100644 index 0000000..c8e3510 Binary files /dev/null and b/src/Middleware/images/Middleware/nacos_data_model.jpeg differ diff --git a/src/Middleware/images/Middleware/nacos_sdk_class_relation.jpeg b/src/Middleware/images/Middleware/nacos_sdk_class_relation.jpeg new file mode 100644 index 0000000..3040717 Binary files /dev/null and b/src/Middleware/images/Middleware/nacos_sdk_class_relation.jpeg differ diff --git a/src/Middleware/images/Middleware/quicklist.png b/src/Middleware/images/Middleware/quicklist.png new file mode 100644 index 0000000..f19735d Binary files /dev/null and b/src/Middleware/images/Middleware/quicklist.png differ diff --git a/src/Middleware/images/Middleware/rabbitmq-work-01.png b/src/Middleware/images/Middleware/rabbitmq-work-01.png new file mode 100644 index 0000000..6bd9597 Binary files /dev/null and b/src/Middleware/images/Middleware/rabbitmq-work-01.png differ diff --git a/src/Middleware/images/Middleware/rabbitmq-work-02.png b/src/Middleware/images/Middleware/rabbitmq-work-02.png new file mode 100644 index 0000000..d19e86d Binary files /dev/null and b/src/Middleware/images/Middleware/rabbitmq-work-02.png differ diff --git a/src/Middleware/images/Middleware/rabbitmq-work-03-1.png b/src/Middleware/images/Middleware/rabbitmq-work-03-1.png new file mode 100644 index 0000000..5a0c1cc Binary files /dev/null and b/src/Middleware/images/Middleware/rabbitmq-work-03-1.png differ diff --git a/src/Middleware/images/Middleware/rabbitmq-work-04-1.png b/src/Middleware/images/Middleware/rabbitmq-work-04-1.png new file mode 100644 index 0000000..650084c Binary files /dev/null and b/src/Middleware/images/Middleware/rabbitmq-work-04-1.png differ diff --git a/src/Middleware/images/Middleware/rabbitmq-work-05-1.png b/src/Middleware/images/Middleware/rabbitmq-work-05-1.png new file mode 100644 index 0000000..983233d Binary files /dev/null and b/src/Middleware/images/Middleware/rabbitmq-work-05-1.png differ diff --git a/src/Middleware/images/Middleware/skipList跳跃表.png b/src/Middleware/images/Middleware/skipList跳跃表.png new file mode 100644 index 0000000..0add25e Binary files /dev/null and b/src/Middleware/images/Middleware/skipList跳跃表.png differ diff --git a/src/Middleware/images/Middleware/slice操作实现零拷贝.png b/src/Middleware/images/Middleware/slice操作实现零拷贝.png new file mode 100644 index 0000000..5945f88 Binary files /dev/null and b/src/Middleware/images/Middleware/slice操作实现零拷贝.png differ diff --git a/src/Middleware/images/Middleware/zuul过滤器的生命周期.png b/src/Middleware/images/Middleware/zuul过滤器的生命周期.png new file mode 100644 index 0000000..1502b7a Binary files /dev/null and b/src/Middleware/images/Middleware/zuul过滤器的生命周期.png differ diff --git a/src/Middleware/images/Middleware/zuul限流参数.png b/src/Middleware/images/Middleware/zuul限流参数.png new file mode 100644 index 0000000..78c2df3 Binary files /dev/null and b/src/Middleware/images/Middleware/zuul限流参数.png differ diff --git a/src/Middleware/images/Middleware/七层负载均衡.png b/src/Middleware/images/Middleware/七层负载均衡.png new file mode 100644 index 0000000..ee6fbdb Binary files /dev/null and b/src/Middleware/images/Middleware/七层负载均衡.png differ diff --git a/src/Middleware/images/Middleware/传统IO的流程.png b/src/Middleware/images/Middleware/传统IO的流程.png new file mode 100644 index 0000000..bb61af0 Binary files /dev/null and b/src/Middleware/images/Middleware/传统IO的流程.png differ diff --git a/src/Middleware/images/Middleware/传统IO的流程Copy.png b/src/Middleware/images/Middleware/传统IO的流程Copy.png new file mode 100644 index 0000000..8615b92 Binary files /dev/null and b/src/Middleware/images/Middleware/传统IO的流程Copy.png differ diff --git a/src/Middleware/images/Middleware/单线程reactor线程模型.png b/src/Middleware/images/Middleware/单线程reactor线程模型.png new file mode 100644 index 0000000..f77beed Binary files /dev/null and b/src/Middleware/images/Middleware/单线程reactor线程模型.png differ diff --git a/src/Middleware/images/Middleware/四层负载均衡.png b/src/Middleware/images/Middleware/四层负载均衡.png new file mode 100644 index 0000000..294023d Binary files /dev/null and b/src/Middleware/images/Middleware/四层负载均衡.png differ diff --git a/src/Middleware/images/Middleware/多线程reactor线程模型.png b/src/Middleware/images/Middleware/多线程reactor线程模型.png new file mode 100644 index 0000000..37fed0c Binary files /dev/null and b/src/Middleware/images/Middleware/多线程reactor线程模型.png differ diff --git a/src/Middleware/images/Middleware/广播模式.jpg b/src/Middleware/images/Middleware/广播模式.jpg new file mode 100644 index 0000000..8f12fe9 Binary files /dev/null and b/src/Middleware/images/Middleware/广播模式.jpg differ diff --git a/src/Middleware/images/Middleware/服务器启动的Leader选举.png b/src/Middleware/images/Middleware/服务器启动的Leader选举.png new file mode 100644 index 0000000..8b340ac Binary files /dev/null and b/src/Middleware/images/Middleware/服务器启动的Leader选举.png differ diff --git a/src/Middleware/images/Middleware/服务器运行期间的Leader选举.png b/src/Middleware/images/Middleware/服务器运行期间的Leader选举.png new file mode 100644 index 0000000..54f4772 Binary files /dev/null and b/src/Middleware/images/Middleware/服务器运行期间的Leader选举.png differ diff --git a/src/Middleware/images/Middleware/混合型reactor线程模型.png b/src/Middleware/images/Middleware/混合型reactor线程模型.png new file mode 100644 index 0000000..db4c675 Binary files /dev/null and b/src/Middleware/images/Middleware/混合型reactor线程模型.png differ diff --git a/src/Middleware/images/Middleware/用户的普通Http请求执行顺序.jpg b/src/Middleware/images/Middleware/用户的普通Http请求执行顺序.jpg new file mode 100644 index 0000000..31359b8 Binary files /dev/null and b/src/Middleware/images/Middleware/用户的普通Http请求执行顺序.jpg differ diff --git a/src/Middleware/images/Middleware/过滤器、拦截器添加后的执行顺序.jpg b/src/Middleware/images/Middleware/过滤器、拦截器添加后的执行顺序.jpg new file mode 100644 index 0000000..228a93e Binary files /dev/null and b/src/Middleware/images/Middleware/过滤器、拦截器添加后的执行顺序.jpg differ diff --git a/src/Middleware/images/Middleware/集群模式.jpg b/src/Middleware/images/Middleware/集群模式.jpg new file mode 100644 index 0000000..7c1ca3e Binary files /dev/null and b/src/Middleware/images/Middleware/集群模式.jpg differ diff --git a/src/Middleware/images/Middleware/零拷贝CPU.png b/src/Middleware/images/Middleware/零拷贝CPU.png new file mode 100644 index 0000000..8c6a883 Binary files /dev/null and b/src/Middleware/images/Middleware/零拷贝CPU.png differ diff --git a/src/Middleware/images/Middleware/零拷贝整体流程图.png b/src/Middleware/images/Middleware/零拷贝整体流程图.png new file mode 100644 index 0000000..7bf623c Binary files /dev/null and b/src/Middleware/images/Middleware/零拷贝整体流程图.png differ diff --git a/src/OS/1.md b/src/OS/1.md new file mode 100644 index 0000000..4f33b0d --- /dev/null +++ b/src/OS/1.md @@ -0,0 +1,4 @@ +Linux/Unix常见IO模型:**阻塞(Blocking I/O)**、**非阻塞(Non-Blocking I/O)**、**IO多路复用(I/O Multiplexing)**、 **信号驱动 I/O(Signal Driven I/O)**(不常用)和**异步(Asynchronous I/O)**。网络IO操作主要涉及到**内核**和**进程**,其主要分为两个过程: + +- 内核等待数据可操作(可读或可写)——阻塞与非阻塞 +- 内核与进程之间数据的拷贝——同步与异步 \ No newline at end of file diff --git a/src/OS/10.md b/src/OS/10.md new file mode 100644 index 0000000..6656a73 --- /dev/null +++ b/src/OS/10.md @@ -0,0 +1,36 @@ +信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。 + +**信号驱动式I/O的整个过程图如下:** + +![信号驱动式IO](images/OS/信号驱动式IO.png) + +**第一阶段(非阻塞):** + +- ①:进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情 +- ②:内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序 + +**第二阶段(阻塞):** + +- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom) +- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成 +- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作 + + + +**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数 + +**优点** + +- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率; + +**缺点** + +- 在程序的实现上比较困难 +- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失 + + + +**信号通知机制** + +- **水平触发:**指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源 +- **边缘触发:**内核只会发送一次通知信号 \ No newline at end of file diff --git a/src/OS/101.md b/src/OS/101.md new file mode 100644 index 0000000..1b2fe12 --- /dev/null +++ b/src/OS/101.md @@ -0,0 +1,24 @@ +TCP是**面向连接的、可靠的、基于字节流**的传输层通信协议: + +![TCP协议](images/OS/TCP协议.jpg) + +- **面向连接**:一定是**一对一**才能连接,不能像UDP协议可以一个主机同时向多个主机发送消息,即一对多是无法做到的 + +- **可靠的**:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端 + +- **字节流**:消息是**没有边界**的,所以无论消息有多大都可以进行传输。并且消息是**有序的**,当**前一个**消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对**重复**的报文会自动丢弃 + + + +**TCP头部格式** + +![TCP头部格式](images/OS/TCP头部格式.jpg) + +- **序列号**:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。**用来解决网络包乱序问题** +- **确认应答号**:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。**用来解决不丢包的问题** +- **控制位:** + - **ACK**:该位为 `1` 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 `SYN` 包之外该位必须设置为 `1` + - **RST**:该位为 `1` 时,表示 TCP 连接中出现异常必须强制断开连接 + - **SYN**:该位为 `1` 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定 + - **FIN**:该位为 `1` 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 `FIN` 位置为 1 的 TCP 段 + diff --git a/src/OS/102.md b/src/OS/102.md new file mode 100644 index 0000000..823e616 --- /dev/null +++ b/src/OS/102.md @@ -0,0 +1,15 @@ +谈一谈你对 TCP/IP 四层模型,OSI 七层模型的理解? + +### OSI参考模型 + +OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。 + +![OSI参考模型](images/OS/OSI参考模型.png) + + + +### TCP/IP五层模型 + + TCP/IP五层协议和OSI的七层协议对应关系如下: + +![TCP/IP五层模型](images/OS/TCPIP五层模型.png) \ No newline at end of file diff --git a/src/OS/103.md b/src/OS/103.md new file mode 100644 index 0000000..b7dfdbc --- /dev/null +++ b/src/OS/103.md @@ -0,0 +1,88 @@ +![TCP状态](images/OS/TCP状态.png) + +- **CLOSED:** 表示初始状态 +- **LISTEN:** 表示服务器端的某个SOCKET处于监听状态,可以接受连接了 +- **SYN_RCVD:** 表示接收到了SYN报文 +- **SYN_SENT:** 表示客户端已发送SYN报文 +- **ESTABLISHED:**表示连接已经建立了 +- **TIME_WAIT:**表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了 +- **CLOSING:** 表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接 +- **CLOSE_WAIT:** 表示在等待关闭 + + + +**如何在 Linux 系统中查看 TCP 状态?** + +TCP 的连接状态查看,在 Linux 可以通过 `netstat -napt` 命令查看: + +![TCP连接状态查看](images/OS/TCP连接状态查看.jpg) + + + +### TIME_WAIT + +**① 为什么需要 TIME_WAIT 状态?** + +主动发起关闭连接的一方,才会有 `TIME-WAIT` 状态。需要 TIME-WAIT 状态,主要是两个原因: + +- 防止具有相同「四元组」的「旧」数据包被收到 +- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭 + + + +**② TIME_WAIT 过多有什么危害?** + +如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。过多的 TIME-WAIT 状态主要的危害有两种: + +- 第一是内存资源占用 +- 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口 + +第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 `32768~61000`,也可以通过如下参数设置指定 + +```shell +net.ipv4.ip_local_port_range +``` + +**如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。** + +客户端受端口资源限制: + +- 客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接 + +服务端受系统资源限制: + +- 由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口 但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接 + + + +**③ 如何优化 TIME_WAIT?** + +这里给出优化 `TIME-WAIT` 的几个方式,都是有利有弊: + +- 打开 `net.ipv4.tcp_tw_reuse` 和 `net.ipv4.tcp_timestamps` 选项 +- `net.ipv4.tcp_max_tw_buckets` +- 程序中使用`SO_LINGER`,应用强制使用`RST`关闭 + + + +**④ 为什么 TIME_WAIT 等待的时间是 2MSL?** + +`MSL` 是 Maximum Segment Lifetime,**报文最大生存时间**,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 `TTL` 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。 + +**MSL 与 TTL 的区别**: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 **MSL 应该要大于等于 TTL 消耗为 0 的时间**,以确保报文已被自然消亡。 + +TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以**一来一回需要等待 2 倍的时间**。 + +比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。 + +`2MSL` 的时间是从**客户端接收到 FIN 后发送 ACK 开始计时的**。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 **2MSL 时间将重新计时**。 + +在 Linux 系统里 `2MSL` 默认是 `60` 秒,那么一个 `MSL` 也就是 `30` 秒。**Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒**。 + +其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN: + +```shell +#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */ +``` + +如果要修改TIME_WAIT的时间长度,只能修改Linux内核代码里TCP_TIMEWAIT_LEN的值,并重新编译Linux内核。 \ No newline at end of file diff --git a/src/OS/104.md b/src/OS/104.md new file mode 100644 index 0000000..02fb8cd --- /dev/null +++ b/src/OS/104.md @@ -0,0 +1,144 @@ +![TCP连接的过程和状态变化](images/OS/TCP连接的过程和状态变化.jpg) + +参考文档:https://www.cnblogs.com/jojop/p/14111160.html + +### TCP三次握手 + +开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态: + +- 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SENT 状态 +- 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态 +- 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输 + +![三次握手](images/OS/三次握手.jpg) + +- 假设一开始客户端和服务端都处于`CLOSED`的状态。然后先是服务端主动监听某个端口,处于`LISTEN`状态 + +- 【第一个报文】:客户端会随机初始化序号(`client_isn`),将此序号置于 `TCP` 首部的「序号」字段中,同时把 `SYN` 标志位置为 `1` ,表示 `SYN` 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 `SYN-SENT` 状态 + + ![SYN报文](images/OS/SYN报文.jpg) + +- 【第二个报文】:服务端收到客户端的 `SYN` 报文后,首先服务端也随机初始化自己的序号(`server_isn`),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 `client_isn + 1`, 接着把 `SYN` 和 `ACK` 标志位置为 `1`。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 `SYN-RCVD` 状态 + + ![SYN+ACK报文](images/OS/SYN+ACK报文.jpg) + +- 【第三个报文】:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 `ACK` 标志位置为 `1` ,其次「确认应答号」字段填入 `server_isn + 1` ,最后把报文发送给服务端,**这次报文可以携带客户到服务器的数据**,之后客户端处于 `ESTABLISHED` 状态 + + ![ACK报文](images/OS/ACK报文.jpg) + +- 服务器收到客户端的应答报文后,也进入 `ESTABLISHED` 状态 + + + +**第三次握手是否可以携带数据?** + +**第三次握手是可以携带数据的,前两次握手是不可以携带数据的**。一旦完成三次握手,双方都处于 `ESTABLISHED` 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。假设第三次握手的报文的`seq`是`x+1`: + +- **如果有携带数据**:下次客户端发送的报文,`seq=服务器发回的ACK号` +- **如果没有携带数据**:第三次握手的报文不消耗`seq`,下次客户端发送的报文,`seq`序列号为`x+1` + +**① 服务端SYN-RECV流程** + +![TCP服务端-SYN_RECV流程](images/OS/TCP服务端-SYN_RECV流程.png) + +**② 客户端SYN-SEND流程** + +![TCP客户端-SYN_SEND流程](images/OS/TCP客户端-SYN_SEND流程.png) + +- **场景1:sk->sk_write_pending != 0** + + 这个值默认是0的,那什么情况会导致不为0呢?答案是协议栈发送数据的函数遇到socket状态不是ESTABLISHED的时候,会对这个变量做++操作,并等待一小会时间尝试发送数据。 + +- **场景2:icsk->icsk_accept_queue.rskq_defer_accept != 0** + + 客户端先bind到一个端口和IP,然后setsockopt(TCP_DEFER_ACCEPT),然后connect服务器,这个时候就会出现rskq_defer_accept=1的情况,这时候内核会设置定时器等待数据一起在回复ACK包。 + +- **场景3:icsk->icsk_ack.pingpong != 0** + + pingpong这个属性实际上也是一个套接字选项,用来表明当前链接是否为交互数据流,如其值为1,则表明为交互数据流,会使用延迟确认机制。 + + + +**为什么是三次握手?不是两次、四次?** + +TCP建立连接时,通过三次握手**能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号**。序列号能够保证数据包不重复、不丢弃和按序传输。不使用「两次握手」和「四次握手」的原因: + +- **两次握手**:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号 +- **四次握手**:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数 + +接下来以三个方面分析三次握手的原因: + +- 三次握手才可以阻止重复历史连接的初始化(主要原因) +- 三次握手才可以同步双方的初始序列号 +- 三次握手才可以避免资源浪费 + +**原因一:避免历史连接** + +客户端连续发送多次 SYN 建立连接的报文,在**网络拥堵**情况下: + +- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端 +- 那么此时服务端就会回一个 `SYN + ACK` 报文给客户端 +- 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 `RST` 报文给服务端,表示中止这一次连接 + +![三次握手避免历史连接](images/OS/三次握手避免历史连接.jpg) + +如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接: + +- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 `RST` 报文,以此中止历史连接 +- 如果不是历史连接,则第三次发送的报文是 `ACK` 报文,通信双方就会成功建立连接 + +所以,TCP 使用三次握手建立连接的最主要原因是**防止历史连接初始化了连接。** + +**原因二:同步双方初始序列号** + +TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用: + +- 接收方可以去除重复的数据 +- 接收方可以根据数据包的序列号按序接收 +- 可以标识发送出去的数据包中, 哪些是已经被对方收到的 + +可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 `SYN` 报文的时候,需要服务端回一个 `ACK` 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,**这样一来一回,才能确保双方的初始序列号能被可靠的同步。** + +![同步双方初始序列号](images/OS/同步双方初始序列号.jpg) + +四次握手其实也能够可靠的同步双方的初始化序号,但由于**第二步和第三步可以优化成一步**,所以就成了「三次握手」。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。 + +**原因三:避免资源浪费** + +如果只有「两次握手」,当客户端的 `SYN` 请求连接在网络中阻塞,客户端没有接收到 `ACK` 报文,就会重新发送 `SYN` ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 `ACK` 确认信号,所以每收到一个 `SYN` 就只能先主动建立一个连接,这会造成什么情况呢?如果客户端的 `SYN` 阻塞了,重复发送多次 `SYN` 报文,那么服务器在收到请求后就会**建立多个冗余的无效链接,造成不必要的资源浪费。** + +![避免资源浪费](images/OS/避免资源浪费.jpg) + +即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 `SYN` 报文,而造成重复分配资源。 + + + +### TCP四次挥手 + +- 第一次挥手:**FIN=1,seq=u**,发送完毕后客户端进入**FIN_WAIT_1** 状态 +- 第二次挥手:**ACK=1,seq =v,ack=u+1**,发送完毕后服务器端进入**CLOSE_WAIT** 状态,客户端接收到后进入 **FIN_WAIT_2** 状态 +- 第三次挥手:**FIN=1,ACK=1,seq=w,ack=u+1**,发送完毕后服务器端进入**LAST_ACK**状态,客户端接收到后进入 **TIME_WAIT**状态 +- 第四次挥手:**ACK=1,seq=u+1,ack=w+1**,客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,**2MSL**,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 **CLOSED** 状态 + +![四次挥手](images/OS/四次挥手.jpg) + +四次挥手过程: + +- 客户端打算关闭连接,此时会发送一个 TCP 首部 `FIN` 标志位被置为 `1` 的报文,也即 `FIN` 报文,之后客户端进入 `FIN_WAIT_1` 状态 +- 服务端收到该报文后,就向客户端发送 `ACK` 应答报文,接着服务端进入 `CLOSED_WAIT` 状态 +- 客户端收到服务端的 `ACK` 应答报文后,之后进入 `FIN_WAIT_2` 状态 +- 等待服务端处理完数据后,也向客户端发送 `FIN` 报文,之后服务端进入 `LAST_ACK` 状态 +- 客户端收到服务端的 `FIN` 报文后,回一个 `ACK` 应答报文,之后进入 `TIME_WAIT` 状态 +- 服务器收到了 `ACK` 应答报文后,就进入了 `CLOSE` 状态,至此服务端已经完成连接的关闭 +- 客户端在经过 `2MSL` 一段时间后,自动进入 `CLOSE` 状态,至此客户端也完成连接的关闭 + + + +**为什么挥手需要四次?** + +再来回顾下四次挥手双方发 `FIN` 包的过程,就能理解为什么需要四次了。 + +- 关闭连接时,客户端向服务端发送 `FIN` 时,仅仅表示客户端不再发送数据了但是还能接收数据。 +- 服务器收到客户端的 `FIN` 报文时,先回一个 `ACK` 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 `FIN` 报文给客户端来表示同意现在关闭连接。 + +从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 `ACK` 和 `FIN` 一般都会分开发送,从而比三次握手导致多了一次。 \ No newline at end of file diff --git a/src/OS/105.md b/src/OS/105.md new file mode 100644 index 0000000..40e5c4c --- /dev/null +++ b/src/OS/105.md @@ -0,0 +1,17 @@ +正确有效的使用TCP参数可以提高 TCP 性能。以下将从三个角度来阐述提升 TCP 的策略,分别是: + +### TCP三次握手优化 + +![优化TCP三次握手的策略](images/OS/优化TCP三次握手的策略.jpg) + + + +### TCP四次挥手优化 + +![优化TCP四次挥手的策略](images/OS/优化TCP四次挥手的策略.jpg) + + + +### TCP数据传输优化 + +![TCP数据传输的优化策略](images/OS/TCP数据传输的优化策略.jpg) \ No newline at end of file diff --git a/src/OS/106.md b/src/OS/106.md new file mode 100644 index 0000000..e298c4d --- /dev/null +++ b/src/OS/106.md @@ -0,0 +1,150 @@ +### TCP和UDP + +**TCP和UDP的区别?** + +- **连接** + - TCP 是面向连接的传输层协议,传输数据前先要建立连接 + - UDP 是不需要连接,即刻传输数据 + +- **服务对象** + - TCP 是一对一的两点服务,即一条连接只有两个端点 + - UDP 支持一对一、一对多、多对多的交互通信 + +- **可靠性** + - TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达 + - UDP 是尽最大努力交付,不保证可靠交付数据 + +- **拥塞控制、流量控制** + - TCP 有拥塞控制和流量控制机制,保证数据传输的安全性 + - UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率 + +- **首部开销** + - TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 `20` 个字节,如果使用了「选项」字段则会变长的 + - UDP 首部只有 8 个字节,并且是固定不变的,开销较小 + +- **传输方式** + - TCP 是流式传输,没有边界,但保证顺序和可靠 + - UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序 + +- **分片不同** + - TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片 + - UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应该小于 MTU + + + +### ISN + +**① 为什么客户端和服务端的初始序列号 ISN 是不相同的?** + +如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。 + + + +**② 初始序列号 ISN 是如何随机产生的?** + +起始 `ISN` 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。 + +**ISN = M + F (localhost, localport, remotehost, remoteport)** + +- `M` 是一个计时器,这个计时器每隔 4 毫秒加 1 +- `F` 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择 + + + +### UDP + +![UDP-TCP](images/OS/UDP-TCP.png) + +**总结** + +- TCP 向上层提供面向连接的可靠服务 ,UDP 向上层提供无连接不可靠服务 +- UDP 没有 TCP 传输可靠,但是可以在实时性要求搞的地方有所作为 +- 对数据准确性要求高,速度可以相对较慢的,可以选用TCP + + + +### TCP数据可靠性 + +一句话:通过`校验和`、`序列号`、`确认应答`、`超时重传`、`连接管理`、`流量控制`、`拥塞控制`等机制来保证可靠性。 + +**(1)校验和** + +在数据传输过程中,将发送的数据段都当做一个16位的整数,将这些整数加起来,并且前面的进位不能丢弃,补在最后,然后取反,得到校验和。 + +发送方:在发送数据之前计算校验和,并进行校验和的填充。接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方进行比较。 + +**(2)序列号** + +TCP 传输时将每个字节的数据都进行了编号,这就是序列号。序列号的作用不仅仅是应答作用,有了序列号能够将接收到的数据根据序列号进行排序,并且去掉重复的数据。 + +**(3)确认应答** + +TCP 传输过程中,每次接收方接收到数据后,都会对传输方进行确认应答,也就是发送 ACK 报文,这个 ACK 报文中带有对应的确认序列号,告诉发送方,接收了哪些数据,下一次数据从哪里传。 + +**(4)超时重传** + +在进行 TCP 传输时,由于存在确认应答与序列号机制,也就是说发送方发送一部分数据后,都会等待接收方发送的 ACK 报文,并解析 ACK 报文,判断数据是否传输成功。如果发送方发送完数据后,迟迟都没有接收到接收方传来的 ACK 报文,那么就对刚刚发送的数据进行重发。 + +**(5)连接管理** + +就是指三次握手、四次挥手的过程。 + +**(6)流量控制** + +如果发送方的发送速度太快,会导致接收方的接收缓冲区填充满了,这时候继续传输数据,就会造成大量丢包,进而引起丢包重传等等一系列问题。TCP 支持根据接收端的处理能力来决定发送端的发送速度,这就是流量控制机制。 + +具体实现方式:接收端将自己的接收缓冲区大小放入 TCP 首部的『窗口大小』字段中,通过 ACK 通知发送端。 + +**(7)拥塞控制** + +TCP 传输过程中一开始就发送大量数据,如果当时网络非常拥堵,可能会造成拥堵加剧。所以 TCP 引入了`慢启动机制`,在开始发送数据的时候,先发少量的数据探探路。 + + + +### TCP协议如何提高传输效率 + +一句话:TCP 协议提高效率的方式有`滑动窗口`、`快重传`、`延迟应答`、`捎带应答`等。 + +**(1)滑动窗口** + +如果每一个发送的数据段,都要收到 ACK 应答之后再发送下一个数据段,这样的话我们效率很低,大部分时间都用在了等待 ACK 应答上了。 + +为了提高效率我们可以一次发送多条数据,这样就能使等待时间大大减少,从而提高性能。窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。 + +**(2)快重传** + +`快重传`也叫`高速重发控制`。 + +那么如果出现了丢包,需要进行重传。一般分为两种情况: + +情况一:数据包已经抵达,ACK被丢了。这种情况下,部分ACK丢了并不影响,因为可以通过后续的ACK进行确认; + +情况二:数据包直接丢了。发送端会连续收到多个相同的 ACK 确认,发送端立即将对应丢失的数据重传。 + +**(3)延迟应答** + +如果接收数据的主机立刻返回ACK应答,这时候返回的窗口大小可能比较小。 + +- 假设接收端缓冲区为1M,一次收到了512K的数据;如果立刻应答,返回的窗口就是512K; +- 但实际上可能处理端处理速度很快,10ms之内就把512K的数据从缓存区消费掉了; +- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来; +- 如果接收端稍微等一会在应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M; + +窗口越大,网络吞吐量就越大,传输效率就越高;我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。 + +**(4)捎带应答** + +在延迟应答的基础上,很多情况下,客户端服务器在应用层也是一发一收的。这时候常常采用捎带应答的方式来提高效率,而ACK响应常常伴随着数据报文共同传输。如:三次握手。 + + + +### TCP如何处理拥塞 + +网络拥塞现象是指到达通信网络中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。 + +拥塞控制的四个阶段: + +- 慢启动 +- 拥塞避免 +- 快速重传 +- 快速恢复 \ No newline at end of file diff --git a/src/OS/107.md b/src/OS/107.md new file mode 100644 index 0000000..0ed0393 --- /dev/null +++ b/src/OS/107.md @@ -0,0 +1,60 @@ +基于TCP协议的客户端和服务器工作: + +![基于TCP协议的客户端和服务器工作](images/OS/基于TCP协议的客户端和服务器工作.jpg) + +- 服务端和客户端初始化 `socket`,得到文件描述符 +- 服务端调用 `bind`,将绑定在 IP 地址和端口 +- 服务端调用 `listen`,进行监听 +- 服务端调用 `accept`,等待客户端连接 +- 客户端调用 `connect`,向服务器端的地址和端口发起连接请求 +- 服务端 `accept` 返回用于传输的 `socket` 的文件描述符 +- 客户端调用 `write` 写入数据;服务端调用 `read` 读取数据 +- 客户端断开连接时,会调用 `close`,那么服务端 `read` 读取数据的时候,就会读取到了 `EOF`,待处理完数据后,服务端调用 `close`,表示连接关闭 + + + +> **listen 时候参数 backlog 的意义?** +> +> Linux内核中会维护两个队列: +> +> - 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态; +> - 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态; +> +> ![SYN队列与Accpet队列](images/OS/SYN队列与Accpet队列.jpg) +> +> SYN 队列 与 Accpet 队列 +> +> ```shell +> int listen (int socketfd, int backlog) +> ``` +> +> - 参数一 socketfd 为 socketfd 文件描述符 +> - 参数二 backlog,这参数在历史内环版本有一定的变化 +> +> 在早期Linux内核backlog是SYN队列大小,也就是未完成的队列大小。在Linux内核2.2之后,backlog变成accept队列,也就是已完成连接建立的队列长度,**所以现在通常认为backlog是accept队列**。但是上限值是内核参数somaxconn的大小,也就说accpet队列长度=min(backlog, somaxconn)。 + +> **accept 发送在三次握手的哪一步?** +> +> 我们先看看客户端连接服务端时,发送了什么? +> +> ![客户端连接服务端](images/OS/客户端连接服务端.jpg) +> +> - 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYNC_SENT 状态 +> - 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYNC_RCVD 状态 +> - 客户端协议栈收到 ACK 之后,使得应用程序从 `connect` 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1 +> - 应答包到达服务器端后,服务器端协议栈使得 `accept` 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态 +> +> 从上面的描述过程,我们可以得知**客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。** + +> **客户端调用 close 了,连接是断开的流程是什么?** +> +> 我们看看客户端主动调用了 `close`,会发生什么? +> +> ![客户端调用close过程](images/OS/客户端调用close过程.jpg) +> +> - 客户端调用 `close`,表明客户端没有数据需要发送了,则此时会向服务端发送FIN报文,进入FIN_WAIT_1状态 +> - 服务端接收到了 FIN 报文,TCP协议栈会为 FIN 包插入一个文件结束符 `EOF` 到接收缓冲区中,应用程序可以通过 `read` 调用来感知这个 FIN 包。这个 `EOF` 会被**放在已排队等候的其他已接收的数据之后**,这就意味着服务端需要处理这种异常情况,因为EOF表示在该连接上再无额外数据到达。此时服务端进入 CLOSE_WAIT 状态 +> - 接着,当处理完数据后,自然就会读到 `EOF`,于是也调用 `close` 关闭它的套接字,这会使得会发出一个 FIN 包,之后处于 LAST_ACK 状态 +> - 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态 +> - 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态 +> - 客户端进过 `2MSL` 时间之后,也进入 CLOSE 状态 \ No newline at end of file diff --git a/src/OS/108.md b/src/OS/108.md new file mode 100644 index 0000000..8b19c6e --- /dev/null +++ b/src/OS/108.md @@ -0,0 +1,659 @@ +### tcp_v4_connect() + +- 描述: 建立与服务器连接,发送SYN段 + +- 返回值: 0或错误码 + +- 代码关键路径: + + ```c + int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) + { + .....  + /* 设置目的地址和目标端口 */ + inet->dport = usin->sin_port; + inet->daddr = daddr; + .... + /* 初始化MSS上限 */ + tp->rx_opt.mss_clamp = 536; + + /* Socket identity is still unknown (sport may be zero). + * However we set state to SYN-SENT and not releasing socket + * lock select source port, enter ourselves into the hash tables and + * complete initialization after this. + */ + tcp_set_state(sk, TCP_SYN_SENT);/* 设置状态 */ + err = tcp_v4_hash_connect(sk);/* 将传输控制添加到ehash散列表中,并动态分配端口 */ + if (err) + goto failure; + .... + if (!tp->write_seq)/* 还未计算初始序号 */ + /* 根据双方地址、端口计算初始序号 */ + tp->write_seq = secure_tcp_sequence_number(inet->saddr, + inet->daddr, + inet->sport, + usin->sin_port); + + /* 根据初始序号和当前时间,随机算一个初始id */ + inet->id = tp->write_seq ^ jiffies; + + /* 发送SYN段 */ + err = tcp_connect(sk); + rt = NULL; + if (err) + goto failure; + + return 0; + } + ``` + + + +### sys_accept() + +- 描述: 调用tcp_accept(), 并把它返回的newsk进行连接描述符分配后返回给用户空间。 + +- 返回值: 连接描述符 + +- 代码关键路径: + + ```c + asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen) + { + struct socket *sock, *newsock; + ..... + sock = sockfd_lookup(fd, &err);/* 获得侦听端口的socket */ + ..... + if (!(newsock = sock_alloc()))/* 分配一个新的套接口,用来处理与客户端的连接 */ + ..... + /* 调用传输层的accept,对TCP来说,是inet_accept */ + err = sock->ops->accept(sock, newsock, sock->file->f_flags); + .... + if (upeer_sockaddr) {/* 调用者需要获取对方套接口地址和端口 */ + /* 调用传输层回调获得对方的地址和端口 */ + if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) { + } + /* 成功后复制到用户态 */ + err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen); + } + ..... + if ((err = sock_map_fd(newsock)) < 0)/* 为新连接分配文件描述符 */ + + return err; + } + ``` + + + +### tcp_accept() + +**[注]**: 在内核2.6.32以后对应函数为inet_csk_accept(). + +- 描述: 通过在规定时间内,判断tcp_sock->accept_queue队列非空,代表有新的连接进入. + +- 返回值: (struct sock *)newsk; + +- 代码关键路径: + + ```c + struct sock *tcp_accept(struct sock *sk, int flags, int *err) + { + .... + /* Find already established connection */ + if (!tp->accept_queue) {/* accept队列为空,说明还没有收到新连接 */ + long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);/* 如果套口是非阻塞的,或者在一定时间内没有新连接,则返回 */ + + if (!timeo)/* 超时时间到,没有新连接,退出 */ + goto out; + + /* 运行到这里,说明有新连接到来,则等待新的传输控制块 */ + error = wait_for_connect(sk, timeo); + if (error) + goto out; + } + + req = tp->accept_queue; + if ((tp->accept_queue = req->dl_next) == NULL) + tp->accept_queue_tail = NULL; + + newsk = req->sk; + sk_acceptq_removed(sk); + tcp_openreq_fastfree(req); + .... + + return newsk; + } + ``` + +### 三次握手 + +#### 客户端发送SYN段 + +- 由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()发送,并置为TCP_SYN_SENT. + +- 代码关键路径: + + ```c + /* 构造并发送SYN段 */ + int tcp_connect(struct sock *sk) + { + struct tcp_sock *tp = tcp_sk(sk); + struct sk_buff *buff; + + tcp_connect_init(sk);/* 初始化传输控制块中与连接相关的成员 */ + + /* 为SYN段分配报文并进行初始化 */ + buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation); + if (unlikely(buff == NULL)) + return -ENOBUFS; + + /* Reserve space for headers. */ + skb_reserve(buff, MAX_TCP_HEADER); + + TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; + TCP_ECN_send_syn(sk, tp, buff); + TCP_SKB_CB(buff)->sacked = 0; + skb_shinfo(buff)->tso_segs = 1; + skb_shinfo(buff)->tso_size = 0; + buff->csum = 0; + TCP_SKB_CB(buff)->seq = tp->write_seq++; + TCP_SKB_CB(buff)->end_seq = tp->write_seq; + tp->snd_nxt = tp->write_seq; + tp->pushed_seq = tp->write_seq; + tcp_ca_init(tp); + + /* Send it off. */ + TCP_SKB_CB(buff)->when = tcp_time_stamp; + tp->retrans_stamp = TCP_SKB_CB(buff)->when; + + /* 将报文添加到发送队列上 */ + __skb_queue_tail(&sk->sk_write_queue, buff); + sk_charge_skb(sk, buff); + tp->packets_out += tcp_skb_pcount(buff); + /* 发送SYN段 */ + tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL)); + TCP_INC_STATS(TCP_MIB_ACTIVEOPENS); + + /* Timer for repeating the SYN until an answer. */ + /* 启动重传定时器 */ + tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto); + return 0; + } + ``` + + + +#### 服务端发送SYN和ACK处理 + +服务端接收到SYN段后,发送SYN/ACK处理: + +- 由tcp_v4_do_rcv()->tcp_rcv_state_process()->tcp_v4_conn_request()->tcp_v4_send_synack(). + +- tcp_v4_send_synack() + + - tcp_make_synack(sk, dst, req); ** 根据路由、传输控制块、连接请求块中的构建SYN+ACK段 ** + + - ip_build_and_send_pkt(); * 生成IP数据报并发送出去 * + + +![服务端接收到SYN段后_发送SYN_ACK处理流程](images/OS/服务端接收到SYN段后_发送SYN_ACK处理流程.jpg) + + - 代码关键路径: + + ```c + /* 向客户端发送SYN+ACK报文 */ + static int tcp_v4_send_synack(struct sock *sk, struct open_request *req, + struct dst_entry *dst) + { + int err = -1; + struct sk_buff * skb; + + /* First, grab a route. */ + /* 查找到客户端的路由 */ + if (!dst && (dst = tcp_v4_route_req(sk, req)) == NULL) + goto out; + + /* 根据路由、传输控制块、连接请求块中的构建SYN+ACK段 */ + skb = tcp_make_synack(sk, dst, req); + + if (skb) {/* 生成SYN+ACK段成功 */ + struct tcphdr *th = skb->h.th; + + /* 生成校验码 */ + th->check = tcp_v4_check(th, skb->len, + req->af.v4_req.loc_addr, + req->af.v4_req.rmt_addr, + csum_partial((char *)th, skb->len, + skb->csum)); + + /* 生成IP数据报并发送出去 */ + err = ip_build_and_send_pkt(skb, sk, req->af.v4_req.loc_addr, + req->af.v4_req.rmt_addr, + req->af.v4_req.opt); + if (err == NET_XMIT_CN) + err = 0; + } + + out: + dst_release(dst); + return err; + } + ``` + + + +#### 客户端回复确认ACK段 + +- 由tcp_v4_do_rcv()->tcp_rcv_state_process().当前客户端处于TCP_SYN_SENT状态。 + +- tcp_rcv_synsent_state_process(); \* tcp_rcv_synsent_state_process处理SYN_SENT状态下接收到的TCP段 * + + - tcp_ack(); ** 处理接收到的ack报文 ** + + - tcp_send_ack(); * 在主动连接时,向服务器端发送ACK完成连接,并更新窗口 * + + - alloc_skb(); ** 构造ack段 ** + - tcp_transmit_skb(); ** 将ack段发出 ** + + - tcp_urg(sk, skb, th); ** 处理完第二次握手后,还需要处理带外数据 ** + + - tcp_data_snd_check(sk); \* 检测是否有数据需要发送 * + + - 检查sk->sk_send_head队列上是否有待发送的数据。 + - tcp_write_xmit(); ** 将TCP发送队列上的段发送出去 ** + +- 代码关键路径: + +```c +/* 在SYN_SENT状态下处理接收到的段,但是不处理带外数据 */ +static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, + struct tcphdr *th, unsigned len) +{ + struct tcp_sock *tp = tcp_sk(sk); + int saved_clamp = tp->rx_opt.mss_clamp; + + /* 解析TCP选项并保存到传输控制块中 */ + tcp_parse_options(skb, &tp->rx_opt, 0); + + if (th->ack) {/* 处理ACK标志 */ + /* rfc793: + * "If the state is SYN-SENT then + * first check the ACK bit + * If the ACK bit is set + * If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send + * a reset (unless the RST bit is set, if so drop + * the segment and return)" + * + * We do not send data with SYN, so that RFC-correct + * test reduces to: + */ + if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt) + goto reset_and_undo; + + if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr && + !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp, + tcp_time_stamp)) { + NET_INC_STATS_BH(LINUX_MIB_PAWSACTIVEREJECTED); + goto reset_and_undo; + } + + /* Now ACK is acceptable. + * + * "If the RST bit is set + * If the ACK was acceptable then signal the user "error: + * connection reset", drop the segment, enter CLOSED state, + * delete TCB, and return." + */ + + if (th->rst) {/* 收到ACK+RST段,需要tcp_reset设置错误码,并关闭套接口 */ + tcp_reset(sk); + goto discard; + } + + /* rfc793: + * "fifth, if neither of the SYN or RST bits is set then + * drop the segment and return." + * + * See note below! + * --ANK(990513) + */ + if (!th->syn)/* 在SYN_SENT状态下接收到的段必须存在SYN标志,否则说明接收到的段无效,丢弃该段 */ + goto discard_and_undo; + + /* rfc793: + * "If the SYN bit is on ... + * are acceptable then ... + * (our SYN has been ACKed), change the connection + * state to ESTABLISHED..." + */ + + /* 从首部标志中获取显示拥塞通知的特性 */ + TCP_ECN_rcv_synack(tp, th); + if (tp->ecn_flags&TCP_ECN_OK)/* 如果支持ECN,则设置标志 */ + sk->sk_no_largesend = 1; + + /* 设置与窗口相关的成员变量 */ + tp->snd_wl1 = TCP_SKB_CB(skb)->seq; + tcp_ack(sk, skb, FLAG_SLOWPATH); + + /* Ok.. it's good. Set up sequence numbers and + * move to established. + */ + tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; + tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1; + + /* RFC1323: The window in SYN & SYN/ACK segments is + * never scaled. + */ + tp->snd_wnd = ntohs(th->window); + tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq, TCP_SKB_CB(skb)->seq); + + if (!tp->rx_opt.wscale_ok) { + tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0; + tp->window_clamp = min(tp->window_clamp, 65535U); + } + + if (tp->rx_opt.saw_tstamp) {/* 根据是否支持时间戳选项来设置传输控制块的相关字段 */ + tp->rx_opt.tstamp_ok = 1; + tp->tcp_header_len = + sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; + tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; + tcp_store_ts_recent(tp); + } else { + tp->tcp_header_len = sizeof(struct tcphdr); + } + + /* 初始化PMTU、MSS等成员变量 */ + if (tp->rx_opt.sack_ok && sysctl_tcp_fack) + tp->rx_opt.sack_ok |= 2; + + tcp_sync_mss(sk, tp->pmtu_cookie); + tcp_initialize_rcv_mss(sk); + + /* Remember, tcp_poll() does not lock socket! + * Change state from SYN-SENT only after copied_seq + * is initialized. */ + tp->copied_seq = tp->rcv_nxt; + mb(); + tcp_set_state(sk, TCP_ESTABLISHED); + + /* Make sure socket is routed, for correct metrics. */ + tp->af_specific->rebuild_header(sk); + + tcp_init_metrics(sk); + + /* Prevent spurious tcp_cwnd_restart() on first data + * packet. + */ + tp->lsndtime = tcp_time_stamp; + + tcp_init_buffer_space(sk); + + /* 如果启用了连接保活,则启用连接保活定时器 */ + if (sock_flag(sk, SOCK_KEEPOPEN)) + tcp_reset_keepalive_timer(sk, keepalive_time_when(tp)); + + if (!tp->rx_opt.snd_wscale)/* 首部预测 */ + __tcp_fast_path_on(tp, tp->snd_wnd); + else + tp->pred_flags = 0; + + if (!sock_flag(sk, SOCK_DEAD)) {/* 如果套口不处于SOCK_DEAD状态,则唤醒等待该套接口的进程 */ + sk->sk_state_change(sk); + sk_wake_async(sk, 0, POLL_OUT); + } + + /* 连接建立完成,根据情况进入延时确认模式 */ + if (sk->sk_write_pending || tp->defer_accept || tp->ack.pingpong) { + /* Save one ACK. Data will be ready after + * several ticks, if write_pending is set. + * + * It may be deleted, but with this feature tcpdumps + * look so _wonderfully_ clever, that I was not able + * to stand against the temptation 8) --ANK + */ + tcp_schedule_ack(tp); + tp->ack.lrcvtime = tcp_time_stamp; + tp->ack.ato = TCP_ATO_MIN; + tcp_incr_quickack(tp); + tcp_enter_quickack_mode(tp); + tcp_reset_xmit_timer(sk, TCP_TIME_DACK, TCP_DELACK_MAX); + +discard: + __kfree_skb(skb); + return 0; + } else {/* 不需要延时确认,立即发送ACK段 */ + tcp_send_ack(sk); + } + return -1; + } + + /* No ACK in the segment */ + + if (th->rst) {/* 收到RST段,则丢弃传输控制块 */ + /* rfc793: + * "If the RST bit is set + * + * Otherwise (no ACK) drop the segment and return." + */ + + goto discard_and_undo; + } + + /* PAWS check. */ + /* PAWS检测失效,也丢弃传输控制块 */ + if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp && tcp_paws_check(&tp->rx_opt, 0)) + goto discard_and_undo; + + /* 在SYN_SENT状态下收到了SYN段并且没有ACK,说明是两端同时打开 */ + if (th->syn) { + /* We see SYN without ACK. It is attempt of + * simultaneous connect with crossed SYNs. + * Particularly, it can be connect to self. + */ + tcp_set_state(sk, TCP_SYN_RECV);/* 设置状态为TCP_SYN_RECV */ + + if (tp->rx_opt.saw_tstamp) {/* 设置时间戳相关的字段 */ + tp->rx_opt.tstamp_ok = 1; + tcp_store_ts_recent(tp); + tp->tcp_header_len = + sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; + } else { + tp->tcp_header_len = sizeof(struct tcphdr); + } + + /* 初始化窗口相关的成员变量 */ + tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; + tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1; + + /* RFC1323: The window in SYN & SYN/ACK segments is + * never scaled. + */ + tp->snd_wnd = ntohs(th->window); + tp->snd_wl1 = TCP_SKB_CB(skb)->seq; + tp->max_window = tp->snd_wnd; + + TCP_ECN_rcv_syn(tp, th);/* 从首部标志中获取显式拥塞通知的特性。 */ + if (tp->ecn_flags&TCP_ECN_OK) + sk->sk_no_largesend = 1; + + /* 初始化MSS相关的成员变量 */ + tcp_sync_mss(sk, tp->pmtu_cookie); + tcp_initialize_rcv_mss(sk); + + /* 向对端发送SYN+ACK段,并丢弃接收到的SYN段 */ + tcp_send_synack(sk); +#if 0 + /* Note, we could accept data and URG from this segment. + * There are no obstacles to make this. + * + * However, if we ignore data in ACKless segments sometimes, + * we have no reasons to accept it sometimes. + * Also, seems the code doing it in step6 of tcp_rcv_state_process + * is not flawless. So, discard packet for sanity. + * Uncomment this return to process the data. + */ + return -1; +#else + goto discard; +#endif + } + /* "fifth, if neither of the SYN or RST bits is set then + * drop the segment and return." + */ + +discard_and_undo: + tcp_clear_options(&tp->rx_opt); + tp->rx_opt.mss_clamp = saved_clamp; + goto discard; + +reset_and_undo: + tcp_clear_options(&tp->rx_opt); + tp->rx_opt.mss_clamp = saved_clamp; + return 1; +} +``` + + + +#### 服务端收到ACK段 + +- 由tcp_v4_do_rcv()->tcp_rcv_state_process().当前服务端处于TCP_SYN_RECV状态变为TCP_ESTABLISHED状态。 + +- 代码关键路径: + + ```c + /* 除了ESTABLISHED和TIME_WAIT状态外,其他状态下的TCP段处理都由本函数实现 */ + int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, + struct tcphdr *th, unsigned len) + { + struct tcp_sock *tp = tcp_sk(sk); + int queued = 0; + + tp->rx_opt.saw_tstamp = 0; + + switch (sk->sk_state) { + ..... + /* SYN_RECV状态的处理 */ + if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&/* 解析TCP选项,如果首部中存在时间戳选项 */ + tcp_paws_discard(tp, skb)) {/* PAWS检测失败,则丢弃报文 */ + if (!th->rst) {/* 如果不是RST段 */ + /* 发送DACK给对端,说明接收到的TCP段已经处理过 */ + NET_INC_STATS_BH(LINUX_MIB_PAWSESTABREJECTED); + tcp_send_dupack(sk, skb); + goto discard; + } + /* Reset is accepted even if it did not pass PAWS. */ + } + + /* step 1: check sequence number */ + if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {/* TCP段序号无效 */ + if (!th->rst)/* 如果TCP段无RST标志,则发送DACK给对方 */ + tcp_send_dupack(sk, skb); + goto discard; + } + + /* step 2: check RST bit */ + if(th->rst) {/* 如果有RST标志,则重置连接 */ + tcp_reset(sk); + goto discard; + } + + /* 如果有必要,则更新时间戳 */ + tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq); + + /* step 3: check security and precedence [ignored] */ + + /* step 4: + * + * Check for a SYN in window. + */ + if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {/* 如果有SYN标志并且序号在接收窗口内 */ + NET_INC_STATS_BH(LINUX_MIB_TCPABORTONSYN); + tcp_reset(sk);/* 复位连接 */ + return 1; + } + + /* step 5: check the ACK field */ + if (th->ack) {/* 如果有ACK标志 */ + /* 检查ACK是否为正常的第三次握手 */ + int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH); + + switch(sk->sk_state) { + case TCP_SYN_RECV: + if (acceptable) { + tp->copied_seq = tp->rcv_nxt; + mb(); + /* 正常的第三次握手,设置连接状态为TCP_ESTABLISHED */ + tcp_set_state(sk, TCP_ESTABLISHED); + sk->sk_state_change(sk); + + /* Note, that this wakeup is only for marginal + * crossed SYN case. Passively open sockets + * are not waked up, because sk->sk_sleep == + * NULL and sk->sk_socket == NULL. + */ + if (sk->sk_socket) {/* 状态已经正常,唤醒那些等待的线程 */ + sk_wake_async(sk,0,POLL_OUT); + } + + /* 初始化传输控制块,如果存在时间戳选项,同时平滑RTT为0,则需计算重传超时时间 */ + tp->snd_una = TCP_SKB_CB(skb)->ack_seq; + tp->snd_wnd = ntohs(th->window) << + tp->rx_opt.snd_wscale; + tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq, + TCP_SKB_CB(skb)->seq); + + /* tcp_ack considers this ACK as duplicate + * and does not calculate rtt. + * Fix it at least with timestamps. + */ + if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr && + !tp->srtt) + tcp_ack_saw_tstamp(tp, 0); + + if (tp->rx_opt.tstamp_ok) + tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; + + /* Make sure socket is routed, for + * correct metrics. + */ + /* 建立路由,初始化拥塞控制模块 */ + tp->af_specific->rebuild_header(sk); + + tcp_init_metrics(sk); + + /* Prevent spurious tcp_cwnd_restart() on + * first data packet. + */ + tp->lsndtime = tcp_time_stamp;/* 更新最近一次发送数据包的时间 */ + + tcp_initialize_rcv_mss(sk); + tcp_init_buffer_space(sk); + tcp_fast_path_on(tp);/* 计算有关TCP首部预测的标志 */ + } else { + return 1; + } + break; + ..... + } + } else + goto discard; + ..... + + /* step 6: check the URG bit */ + tcp_urg(sk, skb, th);/* 检测带外数据位 */ + + /* tcp_data could move socket to TIME-WAIT */ + if (sk->sk_state != TCP_CLOSE) {/* 如果tcp_data需要发送数据和ACK则在这里处理 */ + tcp_data_snd_check(sk); + tcp_ack_snd_check(sk); + } + + if (!queued) { /* 如果段没有加入队列,或者前面的流程需要释放报文,则释放它 */ + discard: + __kfree_skb(skb); + } + return 0; + } + ``` \ No newline at end of file diff --git a/src/OS/2.md b/src/OS/2.md new file mode 100644 index 0000000..a0b6497 --- /dev/null +++ b/src/OS/2.md @@ -0,0 +1,60 @@ +**① 阻塞(Blocking)和非阻塞(Non-blocking)** + +阻塞和非阻塞发生在内核等待数据可操作(可读或可写)时,指做事时是否需要等待应答。 + +- **阻塞:** 内核检查数据不可操作,则不立即返回 +- **非阻塞:** 内核检查数据不可操作,则立即返回 + +**② 同步(Synchronous)和异步(Asynchronous)** + +同步和异步发生在内核与进程交互时,进程触发IO操作后是否需要等待或轮询查看结果。 + +- **同步:** 触发IO操作 → 等待或轮询查看结果 +- **异步:** 触发IO操作 → 直接返回去做其它事,IO处理完后内核主动通知进程 + + + +### 阻塞I/O + +阻塞IO情况下,当用户调用`read`后,用户线程会被阻塞,等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后`read`才会返回。阻塞分两个阶段: + +- **等待CPU把数据从磁盘读到内核缓冲区** +- **等待CPU把数据从内核缓冲区拷贝到用户缓冲区** + +![阻塞IO](images/OS/阻塞IO.png) + + + +### 非阻塞I/O + +非阻塞的 `read` 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,询问数据是否准备好,当数据没有准备好时,内核立即返回EWOULDBLOCK错误。直到数据准备好后,内核将数据拷贝到应用程序缓冲区,`read` 请求才获取到结果。 + +**注意**:这里最后一次 `read` 调用获取数据的过程,是一个**同步的过程**,是需要等待的过程。这里的同步指的是**内核态的数据拷贝到用户程序的缓存区这个过程**。 + +![非阻塞IO](images/OS/非阻塞IO.png) + +注意,**这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。** + + + +### I/O多路复用 + +非阻塞情况下无可用数据时,应用程序每次轮询内核看数据是否准备好了也耗费CPU,能否不让它轮询,当内核缓冲区数据准备好了,以事件通知当机制告知应用进程数据准备好了呢?应用进程在没有收到数据准备好的事件通知信号时可以忙写其他的工作。此时**IO多路复用**就派上用场了。像**select、poll、epoll** 都是I/O多路复用的具体的实现。 + +![IO-多路复用](images/OS/IO-多路复用.png) + + + +### 同步I/O + +无论 `read` 和 `send` 是 `阻塞I/O`,还是 `非阻塞I/O` 都是同步调用。因为在 `read` 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,即这个过程是同步的,如果内核实现的拷贝效率不高,`read` 调用就会在这个同步过程中等待比较长的时间。 + + + +### 异步I/O + +真正的异步 I/O 是`内核数据准备好` 和 `数据从内核态拷贝到用户态` 这两个过程都不用等待。 + +当我们发起 `aio_read` (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图: + +![异步IO](images/OS/异步IO.png) \ No newline at end of file diff --git a/src/OS/201.md b/src/OS/201.md new file mode 100644 index 0000000..bf39079 --- /dev/null +++ b/src/OS/201.md @@ -0,0 +1 @@ +https://juejin.cn/post/6844903789078675469#heading-23 \ No newline at end of file diff --git a/src/OS/202.md b/src/OS/202.md new file mode 100644 index 0000000..4c5aea3 --- /dev/null +++ b/src/OS/202.md @@ -0,0 +1,153 @@ +**HTTP 缓存的好处?** + +- 减少亢余的数据传输,节约资源 +- 缓解服务器压力,提高网站性能 +- 加快客户加载网页的速度 + + + +**不想使用缓存的几种方式** + +- Ctrl + F5强制刷新,都会直接向服务器提取数据 +- 按F5刷新或浏览器的刷新按钮,默认加上Cache-Control:max-age=0,即会走协商缓存 + + + +**浏览器的缓存分类** + +- 强缓存 200 (from memory cache)和200 (from disk cache) +- 协商缓存 304 (Not Modified) + + + +**刷新操作的缓存策略** + +- 正常操作:强制缓存有效,协商缓存有效 +- 手动刷新:强制缓存失效,协商缓存有效 +- 强制刷新:强制缓存失效,协商缓存失效 + + + +### 缓存流程 + +强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存,主要过程如下: + +![HTTP缓存](images/OS/HTTP缓存.jpg) + + + +### 强制缓存 + +如果启用了强缓存,请求资源时不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network中看到请求返回的200状态码,并在状态码的后面跟着from disk cache 或者from memory cache关键字。两者的差别在于获取缓存的位置不一样。 + +- **Pragma**:在 http1.1 中被遗弃 +- **Cache-Control**:http1.1 时出现的header信息。设置过期时间(绝对时间、时间点),超过了这个时间点就代表资源过期。但是用户的本地时间是可以自行调整的,所以会出现问题。 + - **max-age=x(单位秒)**:缓存内容将在x秒后失效,如 `Cache-Control:max-age=36000` + - **no-cache**:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定 + - **no-store**:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存 + - **private**:所有内容只有客户端可以缓存,**Cache-Control的默认取值** + - **public**:所有内容都将被缓存(客户端和代理服务器都可缓存) +- **Expires**:是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。设置过期时长(相对时间、时间段),指定一个时间长度,跟本地时间无关,在这个时间段内缓存是有效的。 + - 同在Response Headers中 + - 同为控制缓存过期 + - 已被Cache-Control代替 + +**注意**:生效优先级为(从高到低):**Pragma > Cache-Control > Expires** 。 + + + +#### Pragma + +在 http1.1 中被遗弃。 + + + +#### Cache-Control + +![HTTP缓存-Cache-Control](images/OS/HTTP缓存-Cache-Control.png) + +**第一步**:浏览器首次发起请求,缓存为空,服务器响应: + +![HTTP缓存-Cache-Control-第一步](images/OS/HTTP缓存-Cache-Control-第一步.png) + +浏览器缓存此响应,缓存寿命为接收到此响应开始计时 100s 。 + +**第二步**:10s 过后,浏览器再次发起请求,检测缓存未过期,浏览器计算 Age: 10 ,然后直接使用缓存,这里是直接去内存中的缓存,from disk 是取磁盘上的缓存: + +![HTTP缓存-Cache-Control-第二步](images/OS/HTTP缓存-Cache-Control-第二步.png) + +**第三步**:100s 过后,浏览器再次发起请求,检测缓存过期,向服务器发起验证缓存请求。如果服务器对比文件已发生改变,则如 1;否则不返回文件数据报文,直接返回 304: + +![HTTP缓存-Cache-Control-第三步](images/OS/HTTP缓存-Cache-Control-第三步.png) + + + +#### Expires + +Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求的结果缓存的到期时间,即再次发送请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。 + +Expires是HTTP/1.0的字段,但是现在浏览器的默认使用的是HTTP/1.1,那么在HTTP/1.1中网页缓存还是否由Expires控制?到了HTTP/1.1,Expires已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用**客户端的时间**与**服务端返回的时间**做对比,如果客户端与服务端的时间由于某些原因(时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存直接失效,那么强制缓存存在的意义就毫无意义。 + + + +### 协商缓存 + +协商缓存(对比缓存)就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段。这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。 + +**注意**:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。 + + + +#### ETag/If-None-Match + +![ETag与If-None-Match](images/OS/ETag与If-None-Match.jpg) + +**Etag** + +Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下: + +![Etag](images/OS/Etag.png) + +**If-None-Match** + +If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200,如下: + +![If-None-Match](images/OS/If-None-Match.png) + +**注意**:Etag/If-None-Match优先级高于Last-Modified/If-Modified-Since,同时存在则只有Etag/If-None-Match生效。 + + + +#### Last-Modified/If-Modified-Since + +![Last-Modified与If-Modified-Since](images/OS/Last-Modified与If-Modified-Since.jpg) + +**Last-Modified** + +Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间,如下: + +![Last-Modified](images/OS/Last-Modified.png) + + + +**If-Modified-Since** + +If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件,如下: + +![If-Modified-Since](images/OS/If-Modified-Since.png) + + + +**常见问题** + +**问题一:为什么要有Etag?** + +你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题: + +> 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET; +> 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒); +> 某些服务器不能精确的得到文件的最后修改时间。 + +**问题二:如果什么缓存策略都没设置,那么浏览器会怎么处理?** + +如果什么缓存策略都没设置,没有Cache-Control也没有Expires,对于这种情况,浏览器会采用一个启发式的算法(LM-Factor),通常会取响应头中的 Date 减去 Last-Modified 值的 10% (不同的浏览器可能不一样)作为缓存时间。 \ No newline at end of file diff --git a/src/OS/203.md b/src/OS/203.md new file mode 100644 index 0000000..4416d4c --- /dev/null +++ b/src/OS/203.md @@ -0,0 +1,13 @@ +截止到HTTP1.1共有下面几种方法: + +| 方法 | 描述 | +| ------- | ------------------------------------------------------------ | +| GET | GET请求会显示请求指定的资源。一般来说GET方法应该只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。它期望的应该是而且应该是安全的和幂等的。这里的安全指的是,请求不会影响到资源的状态 | +| POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改 | +| PUT | PUT请求会身向指定资源位置上传其最新内容,PUT方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容 | +| PATCH | PATCH方法出现的较晚,它在2010年的RFC 5789标准中被定义。PATCH请求与PUT请求类似,同样用于资源的更新。二者有以下两点不同:1.PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新。2.当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已在资源进行更新 | +| DELETE | DELETE请求用于请求服务器删除所请求URI(统一资源标识符,Uniform Resource Identifier)所标识的资源。DELETE请求后指定资源会被删除,DELETE方法也是幂等的 | +| OPTIONS | 允许客户端查看服务器的性能 | +| CONNECT | HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 | +| HEAD | 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头 | +| TRACE | 回显服务器收到的请求,主要用于测试或诊断 | \ No newline at end of file diff --git a/src/OS/204.md b/src/OS/204.md new file mode 100644 index 0000000..e145240 --- /dev/null +++ b/src/OS/204.md @@ -0,0 +1,25 @@ +### 常见请求头 + +| 名称 | 作用 | +| ----------------- | ------------------------------------------------------------ | +| Authorization | 用于设置身份认证信息 | +| User-Agent | 用户标识,如:OS和浏览器的类型和版本 | +| If-Modified-Since | 值为上一次服务器返回的 `Last-Modified` 值,用于确认某个资源是否被更改过,没有更改过(304)就从缓存中读取 | +| If-None-Match | 值为上一次服务器返回的 ETag 值,一般会和`If-Modified-Since`一起出现 | +| Cookie | 已有的Cookie | +| Referer | 表示请求引用自哪个地址,比如你从页面A跳转到页面B时,值为页面A的地址 | +| Host | 请求的主机和端口号 | + + + +### 常见响应头 + +| 名称 | 作用 | +| ----------------- | ------------------------------------------------------------ | +| Date | 服务器的日期 | +| Last-Modified | 该资源最后被修改时间 | +| Transfer-Encoding | 取值为一般为chunked,出现在Content-Length不能确定的情况下,表示服务器不知道响应版体的数据大小,一般同时还会出现`Content-Encoding`响应头 | +| Set-Cookie | 设置Cookie | +| Location | 重定向到另一个URL,如输入浏览器就输入baidu.com回车,会自动跳到 https://www.baidu.com ,就是通过这个响应头控制的 | +| Server | 后台服务器 | + diff --git a/src/OS/205.md b/src/OS/205.md new file mode 100644 index 0000000..04a8e2f --- /dev/null +++ b/src/OS/205.md @@ -0,0 +1,11 @@ +HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型: + +| 分类 | 分类描述 | +| ---- | ---------------------------------------------- | +| 1xx | 信息,服务器收到请求,需要请求者继续执行操作 | +| 2xx | 成功,操作被成功接收并处理 | +| 3xx | 重定向,需要进一步的操作以完成请求 | +| 4xx | 客户端错误,请求包含语法错误或无法完成请求 | +| 5xx | 服务器错误,服务器在处理请求的过程中发生了错误 | + +一般我们只需要知道几个常见的就行,比如 200,400,401,403,404,500,502。 \ No newline at end of file diff --git a/src/OS/206.md b/src/OS/206.md new file mode 100644 index 0000000..78a1222 --- /dev/null +++ b/src/OS/206.md @@ -0,0 +1,7 @@ +说下浏览器请求一个网址的过程? + +- 首先通过DNS服务器把域名解析成IP地址,通过IP和子网掩码判断是否属于同一个子网 +- 构造应用层请求http报文,传输层添加TCP/UDP头部,网络层添加IP头部,数据链路层添加以太网协议头部 +- 数据经过路由器、交换机转发,最终达到目标服务器,目标服务器同样解析数据,最终拿到http报文,按照对应的程序的逻辑响应回去 + +![HTTP请求流程](images/OS/HTTP请求流程.jpg) \ No newline at end of file diff --git a/src/OS/207.md b/src/OS/207.md new file mode 100644 index 0000000..07b1f63 --- /dev/null +++ b/src/OS/207.md @@ -0,0 +1,61 @@ +### http1.1和http2的区别 + +**HTTP1.1** + +- 持久连接 +- 请求管道化 +- 增加缓存处理(新的字段如cache-control) +- 增加 Host 字段、支持断点传输等 + +**HTTP2.0** + +- 二进制分帧 +- 多路复用(或连接共享) +- 头部压缩 +- 服务器推送 + + + +### HTTP 和HTTPS的区别 + +(1)HTTPS 协议需要到 CA 申请证书,一般免费证书较少,因而需要一定费用。 + +(2)HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。 + +(3)HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。 + +(4)HTTP 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。 + + + +### 对称加密和非对称加密 + +对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即`如何安全地将密钥发给对方`; + +而非对称加密是指使用一对非对称密钥,即`公钥`和`私钥`,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。 + +由于非对称加密的方式不需要发送用来解密的私钥,所以可以`保证安全性`;但是和对称加密比起来,它比较`慢`,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。 + + + +### 常见状态码 + +1×× : 请求处理中,请求已被接受,正在处理 + +2×× : 请求成功,请求被成功处理 200 OK + +3×× : 重定向,要完成请求必须进行进一步处理 301 : 永久性转移 302 :暂时性转移 304 :已缓存 + +4×× : 客户端错误,请求不合法 400:Bad Request,请求有语法问题 403:拒绝请求 404:客户端所访问的页面不存在 + +5×× : 服务器端错误,服务器不能处理合法请求 500 :服务器内部错误 503 :服务不可用,稍等 + + + +### Session、Cookie 的区别 + +- session 在服务器端,cookie 在客户端(浏览器) +- session 默认被存储在服务器的一个文件里(不是内存) +- session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id) +- session 可以放在 文件、数据库、或内存中都可以。 +- 用户验证这种场合一般会用 session \ No newline at end of file diff --git a/src/OS/3.md b/src/OS/3.md new file mode 100644 index 0000000..8bbb85c --- /dev/null +++ b/src/OS/3.md @@ -0,0 +1,130 @@ +`Reactor 模式` 即 I/O 多路复用监听事件,收到事件后根据事件类型分配(Dispatch)给某个进程/线程。其主要由 `Reactor` 和 `处理资源池` 两个核心部分组成: + +- **Reactor**:负责监听和分发事件。事件类型包含连接事件、读写事件 +- **处理资源池**:负责处理事件。如:read -> 业务逻辑 -> send + +Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于: + +- Reactor 的数量可以只有一个,也可以有多个 +- 处理资源池可以是单个进程/线程,也可以是多个进程/线程 + + + +将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择: + +- **单 Reactor 单进程/线程** +- **单 Reactor 多进程/线程** +- **多 Reactor 单进程/线程**:相比 `单Reactor单进程/线程` 方案不仅复杂而且没有性能优势,因此可以忽略 +- **多 Reactor 多进程/线程** + + + +### 单Reactor单进程/单线程 + +一般来说,C 语言实现的是`单Reactor单进程`的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程。而 Java 语言实现的是「单 Reactor 单线程」的方案,因为 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有很多线程,我们写的 Java 程序只是其中的一个线程而已。以下是「`单 Reactor单进程`」的方案示意图: + + +![单Reactor单进程线程](images/OS/单Reactor单进程线程.png) + +可以看到进程里有 `Reactor`、`Acceptor`、`Handler` 这三个对象: + +- `Reactor` 对象的作用是监听和分发事件 +- `Acceptor` 对象的作用是获取连接 +- `Handler` 对象的作用是处理业务 + +对象里的 `select`、`accept`、`read`、`send` 是系统调用函数,`dispatch` 和 `业务处理` 是需要完成的操作,其中 `dispatch` 是分发事件操作。 + + + +**工作流程** + +- `Reactor` 对象通过 `select` (IO多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 +- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法 获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 +- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 + + + +**优缺点** + +- **优点** + - 因为全部工作都在同一个进程内完成,所以实现起来比较简单 + - 不需要考虑进程间通信,也不用担心多进程竞争 + +- **缺点** + - 因为只有一个进程,无法充分利用 多核 `CPU` 的性能 + - `Handler`对象在业务处理时,整个进程是无法处理其它连接事件,如果业务处理耗时比较长,那么就造成响应的延迟 + + + +**使用场景** + +单Reactor单进程的方案`不适用计算机密集型的场景`,`只适用于业务处理非常快速的场景`。如:Redis 是由 C 语言实现的,它采用的正是「单Reactor单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。 + + + +### 单Reactor多线程/多进程 + +如果要克服`单 Reactor 单线程/单进程`方案的缺点,那么就需要引入多线程/多进程,这样就产生了**单Reactor多线程/多进程**的方案。具体方案的示意图如下: + +![单Reactor多线程多进程](images/OS/单Reactor多线程多进程.png) + +**工作流程** + +- `Reactor` 对象通过 `select` (IO 多路复用接口) 监听事件,收到事件后通过 `dispatch` 进行分发,具体分发给 `Acceptor` 对象还是 `Handler` 对象,还要看收到的事件类型 +- 如果是连接建立的事件,则交由 `Acceptor` 对象进行处理,`Acceptor` 对象会通过 `accept` 方法获取连接,并创建一个 `Handler` 对象来处理后续的响应事件 +- 如果不是连接建立事件, 则交由当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象不再负责业务处理,只负责数据的接收和发送,`Handler` 对象通过 `read` 读取到数据后,会将数据发给子线程里的 `Processor` 对象进行业务处理 +- 子线程里的 `Processor` 对象就进行业务处理,处理完后,将结果发给主线程中的 `Handler` 对象,接着由 `Handler` 通过 `send` 方法将响应结果发送给 `client` + + + +**单Reator多线程** + +- **优势**:能够充分利用多核 `CPU` 的能力 + +- **缺点**:带来了多线程竞争资源问题(如需加互斥锁解决) + +**单Reactor多进程** + +- **缺点** + - 需要考虑子进程和父进程的双向通信 + - 进程间通信远比线程间通信复杂 + + + +另外,`单Reactor` 的模式还有个问题,因为一个 `Reactor` 对象承担所有事件的 `监听` 和 `响应` ,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能瓶颈。 + + + +### 多Reactor多进程/多线程 + +要解决 `单Reactor` 的问题,就是将 `单Reactor` 实现成 `多Reactor`,这样就产生了 **多Reactor多进程/线程** 方案。其方案的示意图如下(以线程为例): + +![多Reactor多进程线程](images/OS/多Reactor多进程线程.png) + +**工作流程** + +- 主线程中的 `MainReactor` 对象通过 `select` 监控连接建立事件,收到事件后通过 `Acceptor` 对象中的 `accept` 获取连接,将新的连接分配给某个子线程 +- 子线程中的 `SubReactor` 对象将 `MainReactor` 对象分配的连接加入 `select` 继续进行监听,并创建一个 `Handler` 用于处理连接的响应事件 +- 如果有新的事件发生时,`SubReactor` 对象会调用当前连接对应的 `Handler` 对象来进行响应 +- `Handler` 对象通过 `read` -> 业务处理 -> `send` 的流程来完成完整的业务流程 + + + +**方案优势** + +`多Reactor多线程` 的方案虽然看起来复杂的,但是实际实现时比 `单Reactor多线程`的方案要简单的多,原因如下: + +- **分工明确**:主线程只负责接收新连接,子线程负责完成后续的业务处理 +- **主线程和子线程的交互很简单**:主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端 + + + +**应用场景** + +- `多Reactor多线程`:开源软件 `Netty`、`Memcache` + +- `多Reactor多进程`:开源软件 `Nginx`。不过 Nginx 方案与标准的多Reactor多进程有些差异,具体差异: + - 主进程仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而由子进程的 Reactor 来 accept 连接 + - 通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程 \ No newline at end of file diff --git a/src/OS/301.md b/src/OS/301.md new file mode 100644 index 0000000..404517d --- /dev/null +++ b/src/OS/301.md @@ -0,0 +1,63 @@ +常见处理器有X86、ARM、MIPS、PowerPC四种。 + +### X86 + +X86架构是芯片巨头Intel设计制造的一种微处理器体系结构的统称。如果这样说你不理解,那么当我说出8086,80286等这样的词汇时,相信你肯定马上就理解了,正是基于此,X86架构这个名称被广为人知。 + +如今,我们所用的PC绝大部分都是X86架构。可见X86架构普及程度,这也和Intel的霸主地位密切相关。 + +x86采用CISC(Complex Instruction Set Computer,复杂指令集计算机)架构。与采用RISC不同的是,在CISC处理器中,程序的各条指令是按顺序串行执行的,每条指令中的各个操作也是按顺序串行执行的。顺序执行的优点是控制简单,但计算机各部分的利用率不高,执行速度慢。 + +- 优势 + - 速度快:单挑指令功能强大,指令数相对较少 + - 带宽要求低:还是因为指令数相对少,即使高频率运行也不需要很大的带宽往CPU传输指令 + - 由于X86采用CISC,因此指令均是按顺序串行执行的,每条指令中的各个操作也是按顺序串行执行的,而顺序执行的优点就是控制简单 + - 个人认为其最大的优势就是:产业化规模更大。目前觉大数据的CPU厂商(如AMD,Intel)生产的就是这种处理器 +- 劣势 + - 通用寄存器组——X86指令集只有8个通用寄存器。所以,CISC的CPU执行是大多数时间是在访问存储器中的数据,而不是寄存器中的。这就是拖慢了整个系统的速度。而RISC系统往往具有非常多的通用寄存器,并采用了重叠寄存器窗口和寄存器堆等技术使寄存器资源得到充分的利用。 + - 解码——这是X86 CPU才有的东西。其作用是把长度不定的X86指令转换为长度固定的类似于RISC的指令,并交给RISC内核。解码分为硬件解码和微解码,对于简单的X86指令只要硬件解码即可,速度较快,而遇到复杂的X86指令则需要进行微解码,并把它分为若干条简单指令,速度较慢且很复杂 + - 寻址范围小——约束了用户需要 + - 计算机各部分的利用率不高,执行速度慢 + + + +### ARM + +ARM是高级精简指令集的简称(Advanced RISC Machine),它是一个32位的精简指令集架构,但也配备16位指令集,一般来讲比等价32位代码节省达35%,却能保留32位系统的所有优势。 + +- 优势: + - 体积小、低功耗、低成本、高性能——ARM被广泛应用在嵌入式系统中的最重要的原因 + - 支持Thumb(16位)/ARM(32位)双指令集,能很好的兼容8位/16位器件 + - 大量使用寄存器,指令执行速度更快 + - 大多数数据操作都在寄存器中完成 + - 寻址方式灵活简单,执行效率高 + - 流水线处理方式 + - 指令长度固定,大多数是简单指定且都能在一个时钟周期内完成,易于设计超标量与流水线 +- 劣势 + - 性能差距稍大。ARM要在性能上接近X86,频率必须比X86处理器高很多,但是频率一高能耗就疯涨,抵消了ARM的优点 + + + +### MIPS + +MIPS架构(英语:MIPS architecture,为Microprocessor without interlocked piped stages architecture的缩写,亦为Millions of Instructions Per Second的相关语),是一种采取精简指令集(RISC)的处理器架构,1981年出现,由MIPS科技公司开发并授权,广泛被使用在许多电子产品、网络设备、个人娱乐装置与商业装置上。最早的MIPS架构是32位,最新的版本已经变成64位。 它的基本特点是: + +- 包含大量的寄存器、指令数和字符 +- 可视的管道延时时隙 + +这些特性使MIPS架构能够提供最高的每平方毫米性能和当今SoC设计中最低的能耗。 + +- 优势 + - MIPS支持64bit指令和操作,ARM目前只到32bit + - MIPS有专门的除法器,可以执行除法指令 + - MIPS的内核寄存器比ARM多一倍,所以同样的性能下MIPS的功耗会比ARM更低,同样功耗下性能比ARM更高 + - MIPS指令比ARM稍微多一点,稍微灵活一点 + - MIPS开放 +- 劣势 + - MIPS的内存地址起始有问题,这导致了MIPS在内存和Cache的支持方面都有限制,现在的MIPS处理器单内核面对高容量内存时有问题 + - MIPS今后的发展方向时并行线程,类似INTEL的超线程,而ARM未来的发展方向时物理多核,目前看来物理多核占优 + - MIPS虽然结构更加简单,但是到现在还是顺序单发射,ARM已经进化到了乱序双发射 + + + +### PowerPC \ No newline at end of file diff --git a/src/OS/302.md b/src/OS/302.md new file mode 100644 index 0000000..c3b5b53 --- /dev/null +++ b/src/OS/302.md @@ -0,0 +1,256 @@ +### 虚拟内存 + +单片机是没有操作系统的,所以每次写完代码,都需要借助工具把程序烧录进去,这样程序才能跑起来。另外,单片机的 CPU 是直接操作内存的「物理地址」。 + +![单片机内存](images/OS/单片机内存.png) + +在这种情况下,要想在内存中同时运行两个程序是不可能的。如果第一个程序在 2000 的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。 + + + +**操作系统的解决方案** + +![进程的中间层](images/OS/进程的中间层.png) + +**操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来,然后为每个进程分配独立的一套虚拟地址。**如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。于是,这里就引出了两种地址的概念: + +- 我们程序所使用的内存地址叫做:**虚拟内存地址**(Virtual Memory Address) +- 实际存在硬件里面的空间地址叫:**物理内存地址**(Physical Memory Address) + +操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示: + +![虚拟地址寻址](images/OS/虚拟地址寻址.png) + +操作系统管理虚拟地址与物理地址之间关系的方式: + +- **内存分段**(比较早提出) +- **内存分页** + + + +### 内存分段 + +程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。**不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。** + + + +**① 分段机制中的虚拟地址和物理地址映射** + +分段机制下的虚拟地址由两部分组成,**段选择因子**和**段内偏移量**。 + +![内存分段-寻址的方式](images/OS/内存分段-寻址的方式.png) + +- **段选择因子**就保存在段寄存器里面。段选择因子里面最重要的是**段号**,用作段表的索引。**段表**里面保存的是这个**段的基地址、段的界限和特权等级**等 +- 虚拟地址中的**段内偏移量**应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址 + + + +**② 程序虚拟地址映射** + +在上面,知道了虚拟地址是通过**段表**与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,如下图: + +![内存分段-虚拟地址与物理地址](images/OS/内存分段-虚拟地址与物理地址.png) + +如果要访问段 3 中偏移量 500 的虚拟地址,我们可以计算出物理地址为,段 3 基地址 7000 + 偏移量 500 = 7500。分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处: + +- **产生内存碎片** +- **内存交换效率低** + + + +**③ 内存碎片** + +内存碎片主要有两类问题: + +- 外部内存碎片:产生了多个不连续的小物理内存,导致新的程序无法被装载。 +- 内部内存碎片:程序所有内存都被装载到物理内存,但程序有部分的内存可能并不是很常使用,这也会导致内存浪费 + +**解决方案** + +- 解决外部内存碎片的问题就是:**内存交换**。先把占用内存的程序写到硬盘中,然后再从硬盘读到内存中(装在位置已变) + + + +**④ 内存交换效率低** + +对于多进程的系统来说,用分段的方式,内存碎片是很容易产生的,产生了内存碎片,那不得不重新 `Swap` 内存区域,这个过程会产生性能瓶颈。因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,**如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。** + + + +为了解决**内存分段**的**内存碎片**和**内存交换效率低**的问题,就出现了**内存分页**。 + + + +### 内存分页 + +分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。要解决这些问题,那么就要想出能少出现一些内存碎片的办法。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决问题了。这个办法,也就是**内存分页**(Paging)。 + + + +**① 内存分页(Paging)** + +**分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小**。这样一个连续并且尺寸固定的内存空间,我们叫**页**(`Page`)。在 Linux 下,每一页的大小为 `4KB`。虚拟地址与物理地址之间通过**页表**来映射,如下图: + +![内存映射](images/OS/内存映射.png) + +页表实际上存储在 CPU 的**内存管理单元** (MMU) 中,于是 CPU 就可以直接通过 MMU,找出要实际要访问的物理内存地址。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个**缺页异常**,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。 + + + +**② 分页解决分段问题** + +由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片的原因。而**采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。** + +如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为**换出**(Swap Out)。一旦需要的时候,再加载进来,称为**换入**(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,**内存交换的效率就相对比较高。** + +![换入换出](images/OS/换入换出.png) + +更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是**只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。** + + + +**③ 分页机制中虚拟地址和物理地址的映射** + +在分页机制下,虚拟地址分为两部分,**页号**和**页内偏移**。页号作为页表的索引,**页表**包含物理页每页所在**物理内存的基地址**,这个基地址与页内偏移的组合就形成了物理内存地址,见下图: + +![内存分页寻址](images/OS/内存分页寻址.png) + +总结一下,对于一个内存地址转换,其实就是这样三个步骤: + +- 把虚拟内存地址,切分成页号和偏移量 +- 根据页号,从页表里面,查询对应的物理页号 +- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址 + + + +**④ 简单分页缺陷** + +有空间上的缺陷。因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。 + +在 32 位的环境下,每个进程的虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 `4MB` 的内存来存储页表。 + +这 4MB 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。 + +那么,`100` 个进程的话,就需要 `400MB` 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。 + + + +**⑤ 多级页表** + +要解决上面的问题,就需要采用的是一种叫作**多级页表**(Multi-Level Page Table)的解决方案。 + +把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 `1024` 个页表(二级页表),每个表(二级页表)中包含 `1024` 个「页表项」,形成**二级分页**。如下图所示: + +![二级分页](images/OS/二级分页.png) + +如果使用了二级分页,一级页表就可以覆盖整个 4GB 虚拟地址空间,但**如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表**。做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= `0.804MB`,这对比单级页表的 `4MB` 是不是一个巨大的节约? + +那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以**页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项**(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。我们把二级分页再推广到多级页表,就会发现页表占用的内存空间更少了,这一切都要归功于对局部性原理的充分应用。 + +对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是: + +- 全局页目录项 PGD(Page Global Directory) +- 上层页目录项 PUD(Page Upper Directory) +- 中间页目录项 PMD(Page Middle Directory) +- 页表项 PTE(Page Table Entry) + +![64位页表四级目录](images/OS/64位页表四级目录.png) + + + +**⑥ TLB** + +多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。 + +我们就可以利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。 + +![页表项地址转换](images/OS/页表项地址转换.png) + +在 CPU 芯片里面,封装了内存管理单元(Memory Management Uni)芯片,它用来完成地址转换和 TLB 的访问与交互。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。 + + + +### 段页式内存管理 + +内存分段和内存分页并不是对立的,是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为**段页式内存管理**。 + +![段页式地址空间](images/OS/段页式地址空间.png) + +段页式内存管理实现的方式: + +- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制 +- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页 + +这样地址结构就由**段号、段内页号和页内位移**三部分组成。用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号,如图所示: + +![段页式管理中的段表、页表与内存的关系](images/OS/段页式管理中的段表、页表与内存的关系.png) + +段页式地址变换中要得到物理地址须经过三次内存访问: + +- 第一次访问段表,得到页表起始地址 +- 第二次访问页表,得到物理页号 +- 第三次将物理页号与页内位移组合,得到物理地址 + +可用软、硬件相结合的方法实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的利用率。 + + + +### Linux 内存管理 + +**① Intel处理器的发展历史** + +早期 Intel 的处理器从 80286 开始使用的是段式内存管理。但是很快发现,光有段式内存管理而没有页式内存管理是不够的,这会使它的 X86 系列会失去市场的竞争力。因此,在不久以后的 80386 中就实现了对页式内存管理。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时,还实现了页式内存管理。 + +但是这个 80386 的页式内存管理设计时,没有绕开段式内存管理,而是建立在段式内存管理的基础上,这就意味着,**页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上一层地址映射。** + +由于此时由段式内存管理映射而成的地址不再是“物理地址”了,Intel 就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址。 + +![IntelX86逻辑地址解析过程](images/OS/IntelX86逻辑地址解析过程.png) + +- **逻辑地址**:程序所使用的地址,通常是没被段式内存管理映射的地址。即段式内存管理转换前的地址 +- **线性地址**:也叫**虚拟地址**,通过段式内存管理映射的地址。页式内存管理转换前的地址 + + + +**② Linux管理内存** + +**Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理**。于是 Linux 就把所有段的基地址设为 `0`,每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。 + + + +**③ Linux的虚拟地址空间分布** + +在 Linux 操作系统中,虚拟地址空间的内部又被分为**内核空间和用户空间**两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示: + +![Linux虚拟地址空间分布](images/OS/Linux虚拟地址空间分布.png) + +- `32` 位系统的内核空间占用 `1G`,位于最高处,剩下的 `3G` 是用户空间 +- `64` 位系统的内核空间和用户空间都是 `128T`,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的 + + + +**④ 内核空间与用户空间** + +- 进程在用户态时,只能访问用户空间内存 +- 只有进入内核态后,才可以访问内核空间的内存; + +虽然每个进程都各自有独立的虚拟内存,但是**每个虚拟内存中的内核地址,其实关联的都是相同的物理内存**。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。 + +![每个进程的内核空间都是一致的](images/OS/每个进程的内核空间都是一致的.png) + + + +**⑤ 32位操作系统中的用户空间分布** + +用户空间内存,从**低到高**分别是 7 种不同的内存段: + +- 程序文件段,包括二进制可执行代码 +- 已初始化数据段,包括静态常量 +- 未初始化数据段,包括未初始化的静态变量 +- 堆段,包括动态分配的内存,从低地址开始向上增长 +- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关) +- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 `8 MB`。当然系统也提供了参数,以便我们自定义大小 + +![32位操作系统中的用户空间分布](images/OS/32位操作系统中的用户空间分布.png) + +在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 `malloc()` 或者 `mmap()` ,就可以分别在堆和文件映射段动态分配内存。 \ No newline at end of file diff --git a/src/OS/303.md b/src/OS/303.md new file mode 100644 index 0000000..7c8450c --- /dev/null +++ b/src/OS/303.md @@ -0,0 +1,103 @@ +### 进程 + +#### 单进程创建的线程数 + +**一个进程最多可以创建多少个线程?**这个问题跟两个东西有关系: + +- **进程的虚拟内存空间上限**。因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多 +- **系统参数限制**。虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数 + +**结论** + +- 32 位系统:用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程 +- 64 位系统:用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制 + + + +**① 在进程里创建一个线程需要消耗多少虚拟内存大小?** + +我们可以执行 ulimit -a 这条命令,查看进程创建线程时默认分配的栈空间大小,比如我这台服务器默认分配给线程的栈空间大小为 8M。 + +![创建线程时默认分配的栈空间大小](images/OS/创建线程时默认分配的栈空间大小.jpg) + + + +**② 32位Linux系统** + +一个进程的虚拟空间是 4G,内核分走了1G,**留给用户用的只有 3G**。那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。 + +![32位的系统创建线程](images/OS/32位的系统创建线程.png) + +如果想使得进程创建上千个线程,那么我们可以调整创建线程时分配的栈空间大小,比如调整为 512k: + +```shell +$ ulimit -s 512 +``` + + + +**③ 64位Linux系统** + +测试服务器的配置:64位系统、2G 物理内存、单核 CPU。 + +64 位系统意味着用户空间的虚拟内存最大值是 128T,这个数值是很大的,如果按创建一个线程需占用 10M 栈空间的情况来算,那么理论上可以创建 128T/10M 个线程,也就是 1000多万个线程,有点魔幻。所以按 64 位系统的虚拟内存大小,理论上可以创建无数个线程。事实上,肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。 + +比如下面这三个内核参数的大小,都会影响创建线程的上限: + +- `/proc/sys/kernel/threads-max`:表示系统支持的最大线程数,默认值是 `14553` +- `/proc/sys/kernel/pid_max`:表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 `32768` +- `/proc/sys/vm/max_map_count`:表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是 `65530` + +在这台服务器跑了前面的程序,其结果如下: + +![64位的系统创建线程](images/OS/64位的系统创建线程.png) + +可以看到,创建了 14374 个线程后,就无法在创建了,而且报错是因为资源的限制。前面我提到的 `threads-max` 内核参数,它是限制系统里最大线程数,默认值是 14553。我们可以运行那个测试线程数的程序后,看下当前系统的线程数是多少,可以通过 `top -H` 查看。 + +![top-H线程数](images/OS/top-H线程数.png) + +左上角的 Threads 的数量显示是 14553,与 `threads-max` 内核参数的值相同,所以我们可以认为是因为这个参数导致无法继续创建线程。那么,我们可以把 threads-max 参数设置成 `99999`: + +```shell +echo 99999 > /proc/sys/kernel/threads-max +``` + +设置完 threads-max 参数后,我们重新跑测试线程数的程序,运行后结果如下图: + +![64位的系统创建线程-不限制thread-max](images/OS/64位的系统创建线程-不限制thread-max.png) + +可以看到,当进程创建了 32326 个线程后,就无法继续创建里,且报错是无法继续申请内存。此时的上限个数很接近 `pid_max` 内核参数的默认值(32768),那么我们可以尝试将这个参数设置为 99999: + +```shell +echo 99999 > /proc/sys/kernel/pid_max +``` + +设置完 pid_max 参数后,继续跑测试线程数的程序,运行后结果创建线程的个数还是一样卡在了 32768 了。经过查阅资料发现,`max_map_count` 这个内核参数也是需要调大的,但是它的数值与最大线程数之间有什么关系,我也不太明白,只是知道它的值是会限制创建线程个数的上限。然后,我把 max_map_count 内核参数也设置成后 99999: + +```shell +echo 99999 > /proc/sys/kernel/pid_max +``` + +继续跑测试线程数的程序,结果如下图: + +![64位的系统创建线程-不限制](images/OS/64位的系统创建线程-不限制.png) + +当创建差不多 5 万个线程后,我的服务器就卡住不动了,CPU 都已经被占满了,毕竟这个是单核 CPU,所以现在是 CPU 的瓶颈了。 + +接下来,我们换个思路测试下,把创建线程时分配的栈空间调大,比如调大为 100M,在大就会创建线程失败。 + +```shell +ulimit -s 1024000 +``` + +设置完后,跑测试线程的程序,其结果如下: + +![64位的系统创建线程-大栈空间](images/OS/64位的系统创建线程-大栈空间.png) + +总共创建了 26390 个线程,然后就无法继续创建了,而且该进程的虚拟内存空间已经高达 25T,要知道这台服务器的物理内存才 2G。为什么物理内存只有 2G,进程的虚拟内存却可以使用 25T 呢?因为虚拟内存并不是全部都映射到物理内存的,程序是有**局部性的特性**,也就是某一个时间只会执行部分代码,所以只需要映射这部分程序就好。 + +你可以从上面那个 top 的截图看到,虽然进程虚拟空间很大,但是物理内存(RES)只有使用了 400M+。 + + + +### 线程 \ No newline at end of file diff --git a/src/OS/4.md b/src/OS/4.md new file mode 100644 index 0000000..402721a --- /dev/null +++ b/src/OS/4.md @@ -0,0 +1,34 @@ +## Proactor模式 + +**Reactor 和 Proactor 的区别** + +- **Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件** + - 在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 `read` 方法来完成数据的读取,也就是要应用进程主动将 `socket` 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据 + - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统通知应用进程,让应用进程来处理**(从驱动读取到内核以及从内核读取到用户空间) +- **Proactor 是异步网络模式, 感知的是已完成的读写事件** + - 在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据 + - 简单理解:**来了事件**(有新连接、有数据可读、有数据可写)**操作系统来处理**(从驱动读取到内核,从内核读取到用户空间),**处理完再通知应用进程** + +无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 **Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件**。 + + + +Proactor 模式的示意图如下: + +![Proactor模式](images/OS/Proactor模式.png) + +**工作流程** + +- Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 +- Asynchronous Operation Processor 注册到内核 +- Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作; +- Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor +- Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理 +- Handler 完成业务处理 + + + +**平台支持** + +- **Linux**:在 `Linux` 下的 `异步I/O` 是不完善的,`aio` 系列函数是由 `POSIX` 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步。并且仅仅支持基于本地文件的 `aio` 异步操作,网络编程中的 `socket` 是不支持的,这也使得基于 `Linux` 的高性能网络程序都是使用 `Reactor` 方案 +- **Windows** :在 `Windows` 下实现了一套完整的支持 `socket` 的异步编程接口,这套接口就是 `IOCP`,是由操作系统级别实现的 `异步I/O`,真正意义上 `异步I/O`,因此在 `Windows` 里实现高性能网络程序可以使用效率更高的 `Proactor` 方案 \ No newline at end of file diff --git a/src/OS/5.md b/src/OS/5.md new file mode 100644 index 0000000..32f780d --- /dev/null +++ b/src/OS/5.md @@ -0,0 +1,206 @@ + + +**注意**:**遍历**相当于查看所有的位置,**回调**相当于查看对应的位置。 + + + +### select + +![select工作流程](images/OS/select工作流程.jpg) + +`select` 本质上是通过设置或者检查存放 `fd` 标志位的数据结构来进行下一步处理。 + +**缺点** + +- **单个进程可监视的`fd`数量被限制**。能监听端口的数量有限,数值存在文件:`cat /proc/sys/fs/file-max` +- **需要维护一个用来存放大量fd的数据结构**。这样会使得用户空间和内核空间在传递该结构时复制开销大 +- **对fd进行扫描时是线性扫描**。`fd`剧增后,`IO`效率较低,因为每次调用都对`fd`进行线性扫描遍历,所以随着`fd`的增加会造成遍历速度慢的性能问题 +- **`select()`函数的超时参数在返回时也是未定义的**。考虑到可移植性,每次在超时之后在下一次进入到`select`之前都需要重新设置超时参数 + +**优点** + +- **`select()`的可移植性更好**。在某些`Unix`系统上不支持`poll()` +- **`select()`对于超时值提供了更好的精度:微秒**。而`poll`是毫秒 + + + +### poll + +![poll工作流程](images/OS/poll工作流程.jpg) + +poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 + +**缺点** + +- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义 +- 与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符 + +**优点** + +- poll() 不要求开发者计算最大文件描述符加一的大小 +- poll() 在应付大数目的文件描述符的时候速度更快,相比于select +- 它没有最大连接数的限制,原因是它是基于链表来存储的 + + + +### epoll + +![epoll工作流程](images/OS/epoll工作流程.jpg) + +epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。 + + + +**优点** + +- **支持一个进程打开大数目的socket描述符(FD)** + + select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024/2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 + +- **IO效率不随FD数目增加而线性下降** + + 传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在Linux内核。 + +- **使用mmap加速内核与用户空间的消息传递** + + 这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。 + + + +## BIO(同步阻塞I/O) + +每个客户端的Socket连接请求,服务端都会对应有个处理线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是`一个连接一个线程`。 + +用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 + +![同步阻塞IO](images/OS/同步阻塞IO.png) + +**特点:**I/O执行的两个阶段进程都是阻塞的。 + +- 使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚拟机造成一定压力 +- 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费 + + + +**优点** + +- 能够及时的返回数据,无延迟 +- 程序简单,进程挂起基本不会消耗CPU时间 + + + +**缺点** + +- I/O等待对性能影响较大 +- 每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用 + + + +## NIO(同步非阻塞I/O) + +服务器端保存一个Socket连接列表,然后对这个列表进行轮询: + +- 如果发现某个Socket端口上有数据可读时说明读就绪,则调用该Socket连接的相应读操作 +- 如果发现某个Socket端口上有数据可写时说明写就绪,则调用该Socket连接的相应写操作 +- 如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口 + +这样能充分利用服务器资源,效率得到了很大提高,在进行I/O操作请求时候再用个线程去处理,是`一个请求一个线程`。Java中使用Selector、Channel、Buffer来实现上述效果。 + +- `Selector`:Selector允许单线程处理多个Channel。如果应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用他的select方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等。 +- `Channel`:基本上所有的IO在NIO中都从一个Channel开始。Channel有点像流,数据可以从channel**读**到buffer,也可以从buffer**写**到channel。 +- `Buffer`:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由Buffer。 + +用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求过程中,虽然用户线程每次发起IO请求后可以立即返回,但为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。 + +![同步非阻塞IO](images/OS/同步非阻塞IO.png) + +**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。 + +**优点** + +- 进程在等待当前任务完成时,可以同时执行其他任务进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性 + +**缺点** + +- 不断轮询将占用大量CPU时间,系统资源利用率大打折扣,影响性能,整体数据吞吐量下降 +- 该模型不适用web服务器 + + + +## IO多路复用(异步阻塞I/O) + +通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 + +![IO多路复用](images/OS/IO多路复用.png) + +**特点:**通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。 + +**优点** + +- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接 +- 可以节省更多的系统资源 + +**缺点:** + +- 如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好 +- 可能延迟还更大,因为处理一个连接数需要发起两次system call + + + +## AIO(异步非阻塞I/O) + +AIO(异步非阻塞IO,即NIO.2)。异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。 + +![异步非阻塞IO](images/OS/异步非阻塞IO.png) + +**特点:**第一阶段和第二阶段都是有内核完成。 + +**优点** + +- 能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力 + +**缺点** + +- 在程序的实现上比较困难 +- 要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主 + + + +## 信号驱动式I/O + +信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。 + +**信号驱动式I/O的整个过程图如下:** + +![信号驱动式IO](images/OS/信号驱动式IO.png) + +**第一阶段(非阻塞):** + +- ①:进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情 +- ②:内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序 + +**第二阶段(阻塞):** + +- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom) +- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成 +- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作 + + + +**特点:**借助socket进行信号驱动I/O并建立SIGIO信号处理函数 + +**优点** + +- 线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率; + +**缺点** + +- 在程序的实现上比较困难 +- 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失 + + + +**信号通知机制** + +- **水平触发:**指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源 +- **边缘触发:**内核只会发送一次通知信号 \ No newline at end of file diff --git a/src/OS/6.md b/src/OS/6.md new file mode 100644 index 0000000..6e40441 --- /dev/null +++ b/src/OS/6.md @@ -0,0 +1,24 @@ +每个客户端的Socket连接请求,服务端都会对应有个处理线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是`一个连接一个线程`。 + +用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 + +![同步阻塞IO](images/OS/同步阻塞IO.png) + +**特点:**I/O执行的两个阶段进程都是阻塞的。 + +- 使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚拟机造成一定压力 +- 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源的浪费 + + + +**优点** + +- 能够及时的返回数据,无延迟 +- 程序简单,进程挂起基本不会消耗CPU时间 + + + +**缺点** + +- I/O等待对性能影响较大 +- 每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用 \ No newline at end of file diff --git a/src/OS/7.md b/src/OS/7.md new file mode 100644 index 0000000..5a52a50 --- /dev/null +++ b/src/OS/7.md @@ -0,0 +1,26 @@ +服务器端保存一个Socket连接列表,然后对这个列表进行轮询: + +- 如果发现某个Socket端口上有数据可读时说明读就绪,则调用该Socket连接的相应读操作 +- 如果发现某个Socket端口上有数据可写时说明写就绪,则调用该Socket连接的相应写操作 +- 如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口 + +这样能充分利用服务器资源,效率得到了很大提高,在进行I/O操作请求时候再用个线程去处理,是`一个请求一个线程`。Java中使用Selector、Channel、Buffer来实现上述效果。 + +- `Selector`:Selector允许单线程处理多个Channel。如果应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用他的select方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等。 +- `Channel`:基本上所有的IO在NIO中都从一个Channel开始。Channel有点像流,数据可以从channel**读**到buffer,也可以从buffer**写**到channel。 +- `Buffer`:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由Buffer。 + +用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求过程中,虽然用户线程每次发起IO请求后可以立即返回,但为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。 + +![同步非阻塞IO](images/OS/同步非阻塞IO.png) + +**特点:**non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。 + +**优点** + +- 进程在等待当前任务完成时,可以同时执行其他任务进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性 + +**缺点** + +- 不断轮询将占用大量CPU时间,系统资源利用率大打折扣,影响性能,整体数据吞吐量下降 +- 该模型不适用web服务器 \ No newline at end of file diff --git a/src/OS/8.md b/src/OS/8.md new file mode 100644 index 0000000..6a27a67 --- /dev/null +++ b/src/OS/8.md @@ -0,0 +1,15 @@ +通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 + +![IO多路复用](images/OS/IO多路复用.png) + +**特点:**通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。 + +**优点** + +- 可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接 +- 可以节省更多的系统资源 + +**缺点:** + +- 如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好 +- 可能延迟还更大,因为处理一个连接数需要发起两次system call \ No newline at end of file diff --git a/src/OS/9.md b/src/OS/9.md new file mode 100644 index 0000000..98b572a --- /dev/null +++ b/src/OS/9.md @@ -0,0 +1,14 @@ +AIO(异步非阻塞IO,即NIO.2)。异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。 + +![异步非阻塞IO](images/OS/异步非阻塞IO.png) + +**特点:**第一阶段和第二阶段都是有内核完成。 + +**优点** + +- 能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力 + +**缺点** + +- 在程序的实现上比较困难 +- 要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主 \ No newline at end of file diff --git a/src/OS/README.md b/src/OS/README.md new file mode 100644 index 0000000..07a3e6b --- /dev/null +++ b/src/OS/README.md @@ -0,0 +1,5 @@ +
OS
+ +Introduction:收纳技术相关的 `JDK Tools`、`Linux Tools`、`Git` 等总结! + +## 🚀点击左侧菜单栏开始吧! \ No newline at end of file diff --git a/src/OS/_sidebar.md b/src/OS/_sidebar.md new file mode 100644 index 0000000..8491a11 --- /dev/null +++ b/src/OS/_sidebar.md @@ -0,0 +1,29 @@ +* [🏁I/O](src/OS/1 "I/O") + * [✍基础概念](src/OS/2 "基础模式") + * [✍Reactor模式](src/OS/3 "Reactor模式") + * [✍Proactor模式](src/OS/4 "Proactor模式") + * [✍select/poll/epoll](src/OS/5 "select/poll/epoll") + * [✍BIO(同步阻塞I/O)](src/OS/6 "BIO(同步阻塞I/O)") + * [✍NIO(同步非阻塞I/O)](src/OS/7 "NIO(同步非阻塞I/O)") + * [✍IO多路复用(异步阻塞I/O)](src/OS/8 "IO多路复用(异步阻塞I/O)") + * [✍AIO(异步非阻塞I/O)](src/OS/9 "AIO(异步非阻塞I/O)") + * [✍信号驱动式I/O](src/OS/10 "信号驱动式I/O") +* [🏁TCP](src/OS/101 "TCP") + * [✍网络模型](src/OS/102 "网络模型") + * [✍TCP状态](src/OS/103 "TCP状态") + * [✍连接过程](src/OS/104 "连接过程") + * [✍TCP优化](src/OS/105 "TCP优化") + * [✍常见问题](src/OS/106 "常见问题") + * [✍Socket](src/OS/107 "Socket") + * [✍TCP源码](src/OS/108 "TCP源码") +* [🏁HTTP](src/OS/201 "HTTP") + * [✍HTTP缓存](src/OS/202 "HTTP缓存") + * [✍请求方法](src/OS/203 "请求方法") + * [✍头参数](src/OS/204 "头参数") + * [✍状态码](src/OS/205 "状态码") + * [✍请求流程](src/OS/206 "请求流程") + * [✍常见问题](src/OS/207 "常见问题") +* 🏁OS + * [✍处理器](src/OS/301 "处理器") + * [✍内存管理](src/OS/302 "内存管理") + * [✍进程和线程](src/OS/303 "进程和线程") \ No newline at end of file diff --git a/src/OS/images/OS/32位操作系统中的用户空间分布.png b/src/OS/images/OS/32位操作系统中的用户空间分布.png new file mode 100644 index 0000000..fd26c30 Binary files /dev/null and b/src/OS/images/OS/32位操作系统中的用户空间分布.png differ diff --git a/src/OS/images/OS/32位的系统创建线程.png b/src/OS/images/OS/32位的系统创建线程.png new file mode 100644 index 0000000..79ddec6 Binary files /dev/null and b/src/OS/images/OS/32位的系统创建线程.png differ diff --git a/src/OS/images/OS/64位的系统创建线程-不限制.png b/src/OS/images/OS/64位的系统创建线程-不限制.png new file mode 100644 index 0000000..44e59b4 Binary files /dev/null and b/src/OS/images/OS/64位的系统创建线程-不限制.png differ diff --git a/src/OS/images/OS/64位的系统创建线程-不限制thread-max.png b/src/OS/images/OS/64位的系统创建线程-不限制thread-max.png new file mode 100644 index 0000000..8606ff9 Binary files /dev/null and b/src/OS/images/OS/64位的系统创建线程-不限制thread-max.png differ diff --git a/src/OS/images/OS/64位的系统创建线程-大栈空间.png b/src/OS/images/OS/64位的系统创建线程-大栈空间.png new file mode 100644 index 0000000..5dcc33d Binary files /dev/null and b/src/OS/images/OS/64位的系统创建线程-大栈空间.png differ diff --git a/src/OS/images/OS/64位的系统创建线程.png b/src/OS/images/OS/64位的系统创建线程.png new file mode 100644 index 0000000..e5d582e Binary files /dev/null and b/src/OS/images/OS/64位的系统创建线程.png differ diff --git a/src/OS/images/OS/64位页表四级目录.png b/src/OS/images/OS/64位页表四级目录.png new file mode 100644 index 0000000..85b2826 Binary files /dev/null and b/src/OS/images/OS/64位页表四级目录.png differ diff --git a/src/OS/images/OS/ACK报文.jpg b/src/OS/images/OS/ACK报文.jpg new file mode 100644 index 0000000..8816ac1 Binary files /dev/null and b/src/OS/images/OS/ACK报文.jpg differ diff --git a/src/OS/images/OS/ETag与If-None-Match.jpg b/src/OS/images/OS/ETag与If-None-Match.jpg new file mode 100644 index 0000000..41fcb4e Binary files /dev/null and b/src/OS/images/OS/ETag与If-None-Match.jpg differ diff --git a/src/OS/images/OS/Etag.png b/src/OS/images/OS/Etag.png new file mode 100644 index 0000000..7d73160 Binary files /dev/null and b/src/OS/images/OS/Etag.png differ diff --git a/src/OS/images/OS/HTTP缓存-Cache-Control-第一步.png b/src/OS/images/OS/HTTP缓存-Cache-Control-第一步.png new file mode 100644 index 0000000..ac18dca Binary files /dev/null and b/src/OS/images/OS/HTTP缓存-Cache-Control-第一步.png differ diff --git a/src/OS/images/OS/HTTP缓存-Cache-Control-第三步.png b/src/OS/images/OS/HTTP缓存-Cache-Control-第三步.png new file mode 100644 index 0000000..79c09d9 Binary files /dev/null and b/src/OS/images/OS/HTTP缓存-Cache-Control-第三步.png differ diff --git a/src/OS/images/OS/HTTP缓存-Cache-Control-第二步.png b/src/OS/images/OS/HTTP缓存-Cache-Control-第二步.png new file mode 100644 index 0000000..066a115 Binary files /dev/null and b/src/OS/images/OS/HTTP缓存-Cache-Control-第二步.png differ diff --git a/src/OS/images/OS/HTTP缓存-Cache-Control.png b/src/OS/images/OS/HTTP缓存-Cache-Control.png new file mode 100644 index 0000000..d9bba6f Binary files /dev/null and b/src/OS/images/OS/HTTP缓存-Cache-Control.png differ diff --git a/src/OS/images/OS/HTTP缓存.jpg b/src/OS/images/OS/HTTP缓存.jpg new file mode 100644 index 0000000..ded3148 Binary files /dev/null and b/src/OS/images/OS/HTTP缓存.jpg differ diff --git a/src/OS/images/OS/HTTP请求流程.jpg b/src/OS/images/OS/HTTP请求流程.jpg new file mode 100644 index 0000000..6cded64 Binary files /dev/null and b/src/OS/images/OS/HTTP请求流程.jpg differ diff --git a/src/OS/images/OS/IO-多路复用.png b/src/OS/images/OS/IO-多路复用.png new file mode 100644 index 0000000..8efb49d Binary files /dev/null and b/src/OS/images/OS/IO-多路复用.png differ diff --git a/src/OS/images/OS/IO多路复用.png b/src/OS/images/OS/IO多路复用.png new file mode 100644 index 0000000..e6c6b5e Binary files /dev/null and b/src/OS/images/OS/IO多路复用.png differ diff --git a/src/OS/images/OS/If-Modified-Since.png b/src/OS/images/OS/If-Modified-Since.png new file mode 100644 index 0000000..7ed9a1a Binary files /dev/null and b/src/OS/images/OS/If-Modified-Since.png differ diff --git a/src/OS/images/OS/If-None-Match.png b/src/OS/images/OS/If-None-Match.png new file mode 100644 index 0000000..fde5e65 Binary files /dev/null and b/src/OS/images/OS/If-None-Match.png differ diff --git a/src/OS/images/OS/IntelX86逻辑地址解析过程.png b/src/OS/images/OS/IntelX86逻辑地址解析过程.png new file mode 100644 index 0000000..b3cdbee Binary files /dev/null and b/src/OS/images/OS/IntelX86逻辑地址解析过程.png differ diff --git a/src/OS/images/OS/Last-Modified.png b/src/OS/images/OS/Last-Modified.png new file mode 100644 index 0000000..c2ae11d Binary files /dev/null and b/src/OS/images/OS/Last-Modified.png differ diff --git a/src/OS/images/OS/Last-Modified与If-Modified-Since.jpg b/src/OS/images/OS/Last-Modified与If-Modified-Since.jpg new file mode 100644 index 0000000..09030c0 Binary files /dev/null and b/src/OS/images/OS/Last-Modified与If-Modified-Since.jpg differ diff --git a/src/OS/images/OS/Linux虚拟地址空间分布.png b/src/OS/images/OS/Linux虚拟地址空间分布.png new file mode 100644 index 0000000..8c2714b Binary files /dev/null and b/src/OS/images/OS/Linux虚拟地址空间分布.png differ diff --git a/src/OS/images/OS/OSI参考模型.png b/src/OS/images/OS/OSI参考模型.png new file mode 100644 index 0000000..e98e64e Binary files /dev/null and b/src/OS/images/OS/OSI参考模型.png differ diff --git a/src/OS/images/OS/Proactor模式.png b/src/OS/images/OS/Proactor模式.png new file mode 100644 index 0000000..af7b1fc Binary files /dev/null and b/src/OS/images/OS/Proactor模式.png differ diff --git a/src/OS/images/OS/SYN+ACK报文.jpg b/src/OS/images/OS/SYN+ACK报文.jpg new file mode 100644 index 0000000..5bbd094 Binary files /dev/null and b/src/OS/images/OS/SYN+ACK报文.jpg differ diff --git a/src/OS/images/OS/SYN报文.jpg b/src/OS/images/OS/SYN报文.jpg new file mode 100644 index 0000000..0ef0b24 Binary files /dev/null and b/src/OS/images/OS/SYN报文.jpg differ diff --git a/src/OS/images/OS/SYN队列与Accpet队列.jpg b/src/OS/images/OS/SYN队列与Accpet队列.jpg new file mode 100644 index 0000000..e4ee7a4 Binary files /dev/null and b/src/OS/images/OS/SYN队列与Accpet队列.jpg differ diff --git a/src/OS/images/OS/TCPIP五层模型.png b/src/OS/images/OS/TCPIP五层模型.png new file mode 100644 index 0000000..14497c8 Binary files /dev/null and b/src/OS/images/OS/TCPIP五层模型.png differ diff --git a/src/OS/images/OS/TCP协议.jpg b/src/OS/images/OS/TCP协议.jpg new file mode 100644 index 0000000..cdc6e6d Binary files /dev/null and b/src/OS/images/OS/TCP协议.jpg differ diff --git a/src/OS/images/OS/TCP头部格式.jpg b/src/OS/images/OS/TCP头部格式.jpg new file mode 100644 index 0000000..d6e2155 Binary files /dev/null and b/src/OS/images/OS/TCP头部格式.jpg differ diff --git a/src/OS/images/OS/TCP客户端-SYN_SEND流程.png b/src/OS/images/OS/TCP客户端-SYN_SEND流程.png new file mode 100644 index 0000000..3f93fa9 Binary files /dev/null and b/src/OS/images/OS/TCP客户端-SYN_SEND流程.png differ diff --git a/src/OS/images/OS/TCP数据传输的优化策略.jpg b/src/OS/images/OS/TCP数据传输的优化策略.jpg new file mode 100644 index 0000000..88a3b2a Binary files /dev/null and b/src/OS/images/OS/TCP数据传输的优化策略.jpg differ diff --git a/src/OS/images/OS/TCP服务端-SYN_RECV流程.png b/src/OS/images/OS/TCP服务端-SYN_RECV流程.png new file mode 100644 index 0000000..646465a Binary files /dev/null and b/src/OS/images/OS/TCP服务端-SYN_RECV流程.png differ diff --git a/src/OS/images/OS/TCP状态.png b/src/OS/images/OS/TCP状态.png new file mode 100644 index 0000000..969e52b Binary files /dev/null and b/src/OS/images/OS/TCP状态.png differ diff --git a/src/OS/images/OS/TCP连接状态查看.jpg b/src/OS/images/OS/TCP连接状态查看.jpg new file mode 100644 index 0000000..e73cd9e Binary files /dev/null and b/src/OS/images/OS/TCP连接状态查看.jpg differ diff --git a/src/OS/images/OS/TCP连接的过程和状态变化.jpg b/src/OS/images/OS/TCP连接的过程和状态变化.jpg new file mode 100644 index 0000000..8ebf495 Binary files /dev/null and b/src/OS/images/OS/TCP连接的过程和状态变化.jpg differ diff --git a/src/OS/images/OS/UDP-TCP.png b/src/OS/images/OS/UDP-TCP.png new file mode 100644 index 0000000..563989b Binary files /dev/null and b/src/OS/images/OS/UDP-TCP.png differ diff --git a/src/OS/images/OS/epoll工作流程.jpg b/src/OS/images/OS/epoll工作流程.jpg new file mode 100644 index 0000000..ed28026 Binary files /dev/null and b/src/OS/images/OS/epoll工作流程.jpg differ diff --git a/src/OS/images/OS/poll工作流程.jpg b/src/OS/images/OS/poll工作流程.jpg new file mode 100644 index 0000000..4b07886 Binary files /dev/null and b/src/OS/images/OS/poll工作流程.jpg differ diff --git a/src/OS/images/OS/select、poll、epoll对比.png b/src/OS/images/OS/select、poll、epoll对比.png new file mode 100644 index 0000000..586e2e9 Binary files /dev/null and b/src/OS/images/OS/select、poll、epoll对比.png differ diff --git a/src/OS/images/OS/select工作流程.jpg b/src/OS/images/OS/select工作流程.jpg new file mode 100644 index 0000000..30b45d8 Binary files /dev/null and b/src/OS/images/OS/select工作流程.jpg differ diff --git a/src/OS/images/OS/top-H线程数.png b/src/OS/images/OS/top-H线程数.png new file mode 100644 index 0000000..ddda0f4 Binary files /dev/null and b/src/OS/images/OS/top-H线程数.png differ diff --git a/src/OS/images/OS/三次握手.jpg b/src/OS/images/OS/三次握手.jpg new file mode 100644 index 0000000..d753258 Binary files /dev/null and b/src/OS/images/OS/三次握手.jpg differ diff --git a/src/OS/images/OS/三次握手避免历史连接.jpg b/src/OS/images/OS/三次握手避免历史连接.jpg new file mode 100644 index 0000000..ab6d5ca Binary files /dev/null and b/src/OS/images/OS/三次握手避免历史连接.jpg differ diff --git a/src/OS/images/OS/二级分页.png b/src/OS/images/OS/二级分页.png new file mode 100644 index 0000000..5a97507 Binary files /dev/null and b/src/OS/images/OS/二级分页.png differ diff --git a/src/OS/images/OS/优化TCP三次握手的策略.jpg b/src/OS/images/OS/优化TCP三次握手的策略.jpg new file mode 100644 index 0000000..d03f31e Binary files /dev/null and b/src/OS/images/OS/优化TCP三次握手的策略.jpg differ diff --git a/src/OS/images/OS/优化TCP四次挥手的策略.jpg b/src/OS/images/OS/优化TCP四次挥手的策略.jpg new file mode 100644 index 0000000..8827170 Binary files /dev/null and b/src/OS/images/OS/优化TCP四次挥手的策略.jpg differ diff --git a/src/OS/images/OS/信号驱动式IO.png b/src/OS/images/OS/信号驱动式IO.png new file mode 100644 index 0000000..284a00b Binary files /dev/null and b/src/OS/images/OS/信号驱动式IO.png differ diff --git a/src/OS/images/OS/内存分段-寻址的方式.png b/src/OS/images/OS/内存分段-寻址的方式.png new file mode 100644 index 0000000..47833ad Binary files /dev/null and b/src/OS/images/OS/内存分段-寻址的方式.png differ diff --git a/src/OS/images/OS/内存分段-虚拟地址与物理地址.png b/src/OS/images/OS/内存分段-虚拟地址与物理地址.png new file mode 100644 index 0000000..a2e7fc0 Binary files /dev/null and b/src/OS/images/OS/内存分段-虚拟地址与物理地址.png differ diff --git a/src/OS/images/OS/内存分页寻址.png b/src/OS/images/OS/内存分页寻址.png new file mode 100644 index 0000000..aa231af Binary files /dev/null and b/src/OS/images/OS/内存分页寻址.png differ diff --git a/src/OS/images/OS/内存映射.png b/src/OS/images/OS/内存映射.png new file mode 100644 index 0000000..b4deea9 Binary files /dev/null and b/src/OS/images/OS/内存映射.png differ diff --git a/src/OS/images/OS/创建线程时默认分配的栈空间大小.jpg b/src/OS/images/OS/创建线程时默认分配的栈空间大小.jpg new file mode 100644 index 0000000..9203bbf Binary files /dev/null and b/src/OS/images/OS/创建线程时默认分配的栈空间大小.jpg differ diff --git a/src/OS/images/OS/单Reactor单进程线程.png b/src/OS/images/OS/单Reactor单进程线程.png new file mode 100644 index 0000000..6616416 Binary files /dev/null and b/src/OS/images/OS/单Reactor单进程线程.png differ diff --git a/src/OS/images/OS/单Reactor多线程多进程.png b/src/OS/images/OS/单Reactor多线程多进程.png new file mode 100644 index 0000000..312f50d Binary files /dev/null and b/src/OS/images/OS/单Reactor多线程多进程.png differ diff --git a/src/OS/images/OS/单片机内存.png b/src/OS/images/OS/单片机内存.png new file mode 100644 index 0000000..b72ac9c Binary files /dev/null and b/src/OS/images/OS/单片机内存.png differ diff --git a/src/OS/images/OS/同步双方初始序列号.jpg b/src/OS/images/OS/同步双方初始序列号.jpg new file mode 100644 index 0000000..e1dbd01 Binary files /dev/null and b/src/OS/images/OS/同步双方初始序列号.jpg differ diff --git a/src/OS/images/OS/同步阻塞IO.png b/src/OS/images/OS/同步阻塞IO.png new file mode 100644 index 0000000..1ca8e73 Binary files /dev/null and b/src/OS/images/OS/同步阻塞IO.png differ diff --git a/src/OS/images/OS/同步非阻塞IO.png b/src/OS/images/OS/同步非阻塞IO.png new file mode 100644 index 0000000..cfab65b Binary files /dev/null and b/src/OS/images/OS/同步非阻塞IO.png differ diff --git a/src/OS/images/OS/四次挥手.jpg b/src/OS/images/OS/四次挥手.jpg new file mode 100644 index 0000000..f0bcb36 Binary files /dev/null and b/src/OS/images/OS/四次挥手.jpg differ diff --git a/src/OS/images/OS/基于TCP协议的客户端和服务器工作.jpg b/src/OS/images/OS/基于TCP协议的客户端和服务器工作.jpg new file mode 100644 index 0000000..6d173e6 Binary files /dev/null and b/src/OS/images/OS/基于TCP协议的客户端和服务器工作.jpg differ diff --git a/src/OS/images/OS/多Reactor多进程线程.png b/src/OS/images/OS/多Reactor多进程线程.png new file mode 100644 index 0000000..f2e1ca9 Binary files /dev/null and b/src/OS/images/OS/多Reactor多进程线程.png differ diff --git a/src/OS/images/OS/客户端调用close过程.jpg b/src/OS/images/OS/客户端调用close过程.jpg new file mode 100644 index 0000000..c954101 Binary files /dev/null and b/src/OS/images/OS/客户端调用close过程.jpg differ diff --git a/src/OS/images/OS/客户端连接服务端.jpg b/src/OS/images/OS/客户端连接服务端.jpg new file mode 100644 index 0000000..f5fc291 Binary files /dev/null and b/src/OS/images/OS/客户端连接服务端.jpg differ diff --git a/src/OS/images/OS/异步IO.png b/src/OS/images/OS/异步IO.png new file mode 100644 index 0000000..1d1a4cd Binary files /dev/null and b/src/OS/images/OS/异步IO.png differ diff --git a/src/OS/images/OS/异步非阻塞IO.png b/src/OS/images/OS/异步非阻塞IO.png new file mode 100644 index 0000000..be7de9c Binary files /dev/null and b/src/OS/images/OS/异步非阻塞IO.png differ diff --git a/src/OS/images/OS/换入换出.png b/src/OS/images/OS/换入换出.png new file mode 100644 index 0000000..a5f0552 Binary files /dev/null and b/src/OS/images/OS/换入换出.png differ diff --git a/src/OS/images/OS/服务端接收到SYN段后_发送SYN_ACK处理流程.jpg b/src/OS/images/OS/服务端接收到SYN段后_发送SYN_ACK处理流程.jpg new file mode 100644 index 0000000..1ea909a Binary files /dev/null and b/src/OS/images/OS/服务端接收到SYN段后_发送SYN_ACK处理流程.jpg differ diff --git a/src/OS/images/OS/段页式地址空间.png b/src/OS/images/OS/段页式地址空间.png new file mode 100644 index 0000000..5db4ada Binary files /dev/null and b/src/OS/images/OS/段页式地址空间.png differ diff --git a/src/OS/images/OS/段页式管理中的段表、页表与内存的关系.png b/src/OS/images/OS/段页式管理中的段表、页表与内存的关系.png new file mode 100644 index 0000000..f78d09f Binary files /dev/null and b/src/OS/images/OS/段页式管理中的段表、页表与内存的关系.png differ diff --git a/src/OS/images/OS/每个进程的内核空间都是一致的.png b/src/OS/images/OS/每个进程的内核空间都是一致的.png new file mode 100644 index 0000000..53c0c79 Binary files /dev/null and b/src/OS/images/OS/每个进程的内核空间都是一致的.png differ diff --git a/src/OS/images/OS/虚拟地址寻址.png b/src/OS/images/OS/虚拟地址寻址.png new file mode 100644 index 0000000..8e1c724 Binary files /dev/null and b/src/OS/images/OS/虚拟地址寻址.png differ diff --git a/src/OS/images/OS/进程的中间层.png b/src/OS/images/OS/进程的中间层.png new file mode 100644 index 0000000..98f7644 Binary files /dev/null and b/src/OS/images/OS/进程的中间层.png differ diff --git a/src/OS/images/OS/避免资源浪费.jpg b/src/OS/images/OS/避免资源浪费.jpg new file mode 100644 index 0000000..107041a Binary files /dev/null and b/src/OS/images/OS/避免资源浪费.jpg differ diff --git a/src/OS/images/OS/阻塞IO.png b/src/OS/images/OS/阻塞IO.png new file mode 100644 index 0000000..dc90054 Binary files /dev/null and b/src/OS/images/OS/阻塞IO.png differ diff --git a/src/OS/images/OS/非阻塞IO.png b/src/OS/images/OS/非阻塞IO.png new file mode 100644 index 0000000..08dea29 Binary files /dev/null and b/src/OS/images/OS/非阻塞IO.png differ diff --git a/src/OS/images/OS/页表项地址转换.png b/src/OS/images/OS/页表项地址转换.png new file mode 100644 index 0000000..ff750c4 Binary files /dev/null and b/src/OS/images/OS/页表项地址转换.png differ diff --git a/static/docsify-pagination/dist/docsify-pagination.min.js b/static/docsify-pagination/dist/docsify-pagination.min.js new file mode 100644 index 0000000..33bad0a --- /dev/null +++ b/static/docsify-pagination/dist/docsify-pagination.min.js @@ -0,0 +1 @@ +!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e():"function"==typeof define&&define.amd?define(e):e()}(0,function(){"use strict";var n,c=(function(n,e){function t(n,e){return e.querySelector(n)}(e=n.exports=function(n,e){return t(n,e=e||document)}).all=function(n,e){return(e=e||document).querySelectorAll(n)},e.engine=function(n){if(!n.one)throw new Error(".one callback required");if(!n.all)throw new Error(".all callback required");return t=n.one,e.all=n.all,e}}(n={exports:{}},n.exports),n.exports);c.all,c.engine;try{var o=c}catch(n){o=c}var e=Element.prototype,a=e.matches||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector,s=function(n,e){if(!n||1!==n.nodeType)return!1;if(a)return a.call(n,e);for(var t=o.all(e,n.parentNode),i=0;i*{line-height:1;vertical-align:middle}.pagination-item-label svg{height:.8em;width:auto;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1px}.pagination-item--next{margin-left:auto;text-align:right}.pagination-item--next svg{margin-left:.5em}.pagination-item--previous svg{margin-right:.5em}.pagination-item-title{font-size:1.6em}");var t=function(n,e){if(!(n instanceof e))throw new TypeError("Cannot call a class as a function")},r=function(){function i(n,e){for(var t=0;t'},inner:function(n,e){return[n.prev&&'\n \n ",n.next&&'\n \n "].filter(Boolean).join("")}};window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(n,e){var t=l({},u,e.config.pagination||{});function i(){var n=c("."+f);n&&(n.innerHTML=v.inner(function(n,e){try{var t=n.route.path,i=d(c.all(".sidebar li a")).filter(function(n){return!s(n,".section-link")}),o=i.find(m(t)),a=d((p(o,"ul")||{}).children).filter(function(n){return"LI"===n.tagName.toUpperCase()}),r=e?i.findIndex(m(t)):a.findIndex(function(n){var e=g(n);return e&&m(t,e)}),l=e?i:a;return{prev:new h(l[r-1]).toJSON(),next:new h(l[r+1]).toJSON()}}catch(n){return{}}}(e,t.crossChapter),t))}n.afterEach(function(n){return n+v.container()}),n.doneEach(function(){return i()})}].concat(window.$docsify.plugins||[])}); \ No newline at end of file diff --git a/static/docsify/lib/docsify.min.js b/static/docsify/lib/docsify.min.js new file mode 100644 index 0000000..013045b --- /dev/null +++ b/static/docsify/lib/docsify.min.js @@ -0,0 +1 @@ +!function(){function s(n){var r=Object.create(null);return function(e){var t=c(e)?e:JSON.stringify(e);return r[t]||(r[t]=n(e))}}var o=s(function(e){return e.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),l=Object.prototype.hasOwnProperty,d=Object.assign||function(e){for(var t=arguments,n=1;n=a.length)i(r);else if("function"==typeof e)if(2===e.length)e(r,function(e){r=e,o(t+1)});else{var n=e(r);r=void 0===n?r:n,o(t+1)}else o(t+1)};o(0)}var f=!0,m=f&&document.body.clientWidth<=600,g=f&&window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/),n={};function v(e,t){if(void 0===t&&(t=!1),"string"==typeof e){if(void 0!==window.Vue)return x(e);e=t?x(e):n[e]||(n[e]=x(e))}return e}var b=f&&document,y=f&&b.body,k=f&&b.head;function x(e,t){return t?e.querySelector(t):b.querySelector(e)}function w(e,t){return[].slice.call(t?e.querySelectorAll(t):b.querySelectorAll(e))}function _(e,t){return e=b.createElement(e),t&&(e.innerHTML=t),e}function S(e,t){return e.appendChild(t)}function A(e,t){return e.insertBefore(t,e.children[0])}function C(e,t,n){u(t)?window.addEventListener(e,t):e.addEventListener(t,n)}function E(e,t,n){u(t)?window.removeEventListener(e,t):e.removeEventListener(t,n)}function $(e,t,n){e&&e.classList[n?t:"toggle"](n||t)}var L,T,e=Object.freeze({getNode:v,$:b,body:y,head:k,find:x,findAll:w,create:_,appendTo:S,before:A,on:C,off:E,toggleClass:$,style:function(e){S(k,_("style",e))}});function R(e,t){if(void 0===t&&(t='
    {inner}
'),!e||!e.length)return"";var n="";return e.forEach(function(e){n+='
  • '+e.title+"
  • ",e.children&&(n+=R(e.children,t))}),t.replace("{inner}",n)}function r(e,t){return'

    '+t.slice(5).trim()+"

    "}function P(e){var t,n,r=e.loaded,i=e.total,a=e.step;!L&&((n=_("div")).classList.add("progress"),S(y,n),L=n),t=a?80<(t=parseInt(L.style.width||0,10)+a)?80:t:Math.floor(r/i*100),L.style.opacity=1,L.style.width=95<=t?"100%":t+"%",95<=t&&(clearTimeout(T),T=setTimeout(function(e){L.style.opacity=0,L.style.width="0%"},200))}var O={};function F(a,e,t){void 0===e&&(e=!1),void 0===t&&(t={});var o=new XMLHttpRequest,n=function(){o.addEventListener.apply(o,arguments)},r=O[a];if(r)return{then:function(e){return e(r.content,r.opt)},abort:p};for(var i in o.open("GET",a),t)l.call(t,i)&&o.setRequestHeader(i,t[i]);return o.send(),{then:function(r,i){if(void 0===i&&(i=p),e){var t=setInterval(function(e){return P({step:Math.floor(5*Math.random()+1)})},500);n("progress",P),n("loadend",function(e){P(e),clearInterval(t)})}n("error",i),n("load",function(e){var t=e.target;if(400<=t.status)i(t);else{var n=O[a]={content:t.response,opt:{updatedAt:o.getResponseHeader("last-modified")}};r(n.content,n.opt)}})},abort:function(e){return 4!==o.readyState&&o.abort()}}}function j(e,t){e.innerHTML=e.innerHTML.replace(/var\(\s*--theme-color.*?\)/g,t)}var N=/([^{]*?)\w(?=\})/g,z={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds"};var t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function i(e,t){return e(t={exports:{}},t.exports),t.exports}var M=i(function(m,e){!function(e){var y={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:d,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,nptable:d,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)|(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,table:d,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/,text:/^[^\n]+/};function l(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||f.defaults,this.rules=y.normal,this.options.pedantic?this.rules=y.pedantic:this.options.gfm&&(this.options.tables?this.rules=y.tables:this.rules=y.gfm)}y._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,y._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,y.def=t(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d+\.)/,y.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,y.item=t(y.item,"gm").replace(/bull/g,y.bullet).getRegex(),y.list=t(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=//,y.html=t(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=t(y.paragraph).replace("hr",y.hr).replace("heading",y.heading).replace("lheading",y.lheading).replace("tag",y._tag).getRegex(),y.blockquote=t(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=g({},y),y.gfm=g({},y.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\n? *\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),y.gfm.paragraph=t(y.paragraph).replace("(?!","(?!"+y.gfm.fences.source.replace("\\1","\\2")+"|"+y.list.source.replace("\\1","\\3")+"|").getRegex(),y.tables=g({},y.gfm,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),y.pedantic=g({},y.normal,{html:t("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/}),l.rules=y,l.lex=function(e,t){return new l(t).lex(e)},l.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},l.prototype.token=function(e,t){var n,r,i,a,o,s,l,c,u,p,h,d,g,f,m,v,b=this;for(e=e.replace(/^ +$/gm,"");e;)if((i=b.rules.newline.exec(e))&&(e=e.substring(i[0].length),1 ?/gm,""),b.token(i,t),b.tokens.push({type:"blockquote_end"});else if(i=b.rules.list.exec(e)){for(e=e.substring(i[0].length),l={type:"list_start",ordered:f=1<(a=i[2]).length,start:f?+a:"",loose:!1},b.tokens.push(l),n=!(c=[]),g=(i=i[0].match(b.rules.item)).length,h=0;h?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:d,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(href(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s])__(?!_)|^\*\*([^\s])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_)|^_([^\s_][\s\S]*?[^\s])_(?!_)|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:d,text:/^(`+|[^`])[\s\S]*?(?=[\\?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=t(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=t(n.tag).replace("comment",y._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/,n._href=/\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f\\]*\)|[^\s\x00-\x1f()\\])*?)/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=t(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=t(n.reflink).replace("label",n._label).getRegex(),n.normal=g({},n),n.pedantic=g({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:t(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:t(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=g({},n.normal,{escape:t(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:t(n.text).replace("]|","~]|").replace("|$","|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&'*+/=?^_`{\\|}~-]+@|$").getRegex()}),n.gfm.url=t(n.gfm.url).replace("email",n.gfm._extended_email).getRegex(),n.breaks=g({},n.gfm,{br:t(n.br).replace("{2,}","*").getRegex(),text:t(n.gfm.text).replace("{2,}","*").getRegex()}),c.rules=n,c.output=function(e,t,n){return new c(t,n).output(e)},c.prototype.output=function(e){for(var t,n,r,i,a,o,s=this,l="";e;)if(a=s.rules.escape.exec(e))e=e.substring(a[0].length),l+=a[1];else if(a=s.rules.autolink.exec(e))e=e.substring(a[0].length),r="@"===a[2]?"mailto:"+(n=p(s.mangle(a[1]))):n=p(a[1]),l+=s.renderer.link(r,null,n);else if(s.inLink||!(a=s.rules.url.exec(e))){if(a=s.rules.tag.exec(e))!s.inLink&&/^/i.test(a[0])&&(s.inLink=!1),!s.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(a[0])?s.inRawBlock=!0:s.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(a[0])&&(s.inRawBlock=!1),e=e.substring(a[0].length),l+=s.options.sanitize?s.options.sanitizer?s.options.sanitizer(a[0]):p(a[0]):a[0];else if(a=s.rules.link.exec(e))e=e.substring(a[0].length),s.inLink=!0,r=a[2],i=s.options.pedantic?(t=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r))?(r=t[1],t[3]):"":a[3]?a[3].slice(1,-1):"",r=r.trim().replace(/^<([\s\S]*)>$/,"$1"),l+=s.outputLink(a,{href:c.escapes(r),title:c.escapes(i)}),s.inLink=!1;else if((a=s.rules.reflink.exec(e))||(a=s.rules.nolink.exec(e))){if(e=e.substring(a[0].length),t=(a[2]||a[1]).replace(/\s+/g," "),!(t=s.links[t.toLowerCase()])||!t.href){l+=a[0].charAt(0),e=a[0].substring(1)+e;continue}s.inLink=!0,l+=s.outputLink(a,t),s.inLink=!1}else if(a=s.rules.strong.exec(e))e=e.substring(a[0].length),l+=s.renderer.strong(s.output(a[4]||a[3]||a[2]||a[1]));else if(a=s.rules.em.exec(e))e=e.substring(a[0].length),l+=s.renderer.em(s.output(a[6]||a[5]||a[4]||a[3]||a[2]||a[1]));else if(a=s.rules.code.exec(e))e=e.substring(a[0].length),l+=s.renderer.codespan(p(a[2].trim(),!0));else if(a=s.rules.br.exec(e))e=e.substring(a[0].length),l+=s.renderer.br();else if(a=s.rules.del.exec(e))e=e.substring(a[0].length),l+=s.renderer.del(s.output(a[1]));else if(a=s.rules.text.exec(e))e=e.substring(a[0].length),s.inRawBlock?l+=s.renderer.text(a[0]):l+=s.renderer.text(p(s.smartypants(a[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===a[2])r="mailto:"+(n=p(a[0]));else{for(;o=a[0],a[0]=s.rules._backpedal.exec(a[0])[0],o!==a[0];);n=p(a[0]),r="www."===a[1]?"http://"+n:n}e=e.substring(a[0].length),l+=s.renderer.link(r,null,n)}return l},c.escapes=function(e){return e?e.replace(c.rules._escapes,"$1"):e},c.prototype.outputLink=function(e,t){var n=t.href,r=t.title?p(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,p(e[1]))},c.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},c.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;i'+(n?e:p(e,!0))+"\n":"
    "+(n?e:p(e,!0))+"
    "},r.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return t&&(t=""+t+""),"\n\n"+e+"\n"+t+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(h(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return n}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return n}this.options.baseUrl&&!s.test(e)&&(e=a(this.options.baseUrl,e));try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return n}var i='
    "},r.prototype.image=function(e,t,n){this.options.baseUrl&&!s.test(e)&&(e=a(this.options.baseUrl,e));var r=''+n+'":">"},r.prototype.text=function(e){return e},i.prototype.strong=i.prototype.em=i.prototype.codespan=i.prototype.del=i.prototype.text=function(e){return e},i.prototype.link=i.prototype.image=function(e,t,n){return""+n},i.prototype.br=function(){return""},u.parse=function(e,t){return new u(t).parse(e)},u.prototype.parse=function(e){this.inline=new c(e.links,this.options),this.inlineText=new c(e.links,g({},this.options,{renderer:new i})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},u.prototype.next=function(){return this.token=this.tokens.pop()},u.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},u.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},u.prototype.tok=function(){var e=this;switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,h(this.inlineText.output(this.token.text)));case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var t,n,r,i,a="",o="";for(r="",t=0;t"']/,p.escapeReplace=/[&<>"']/g,p.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},p.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,p.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var o={},s=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function d(){}function g(e){for(var t,n,r=arguments,i=1;it)n.splice(t);else for(;n.lengthAn error occurred:

    "+p(e.message+"",!0)+"
    ";throw e}}d.exec=d,f.options=f.setOptions=function(e){return g(f.defaults,e),f},f.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},f.defaults=f.getDefaults(),f.Parser=u,f.parser=u.parse,f.Renderer=r,f.TextRenderer=i,f.Lexer=l,f.lexer=l.lex,f.InlineLexer=c,f.inlineLexer=c.output,f.parse=f,m.exports=f}(t||"undefined"!=typeof window&&window)}),a=i(function(e){var c="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},u=function(){var l=/\blang(?:uage)?-([\w-]+)\b/i,t=0,P=c.Prism={manual:c.Prism&&c.Prism.manual,disableWorkerMessageHandler:c.Prism&&c.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof o?new o(e.type,P.util.encode(e.content),e.alias):"Array"===P.util.type(e)?e.map(P.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof s)){if(g&&b!=t.length-1){if(p.lastIndex=y,!(C=p.exec(e)))break;for(var x=C.index+(d?C[1].length:0),w=C.index+C[0].length,_=b,S=y,A=t.length;_"+r.content+""},!c.document)return c.addEventListener&&(P.disableWorkerMessageHandler||c.addEventListener("message",function(e){var t=JSON.parse(e.data),n=t.language,r=t.code,i=t.immediateClose;c.postMessage(P.highlight(r,P.languages[n],n)),i&&c.close()},!1)),c.Prism;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return e&&(P.filename=e.src,P.manual||e.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(P.highlightAll):window.setTimeout(P.highlightAll,16):document.addEventListener("DOMContentLoaded",P.highlightAll))),c.Prism}();e.exports&&(e.exports=u),void 0!==t&&(t.Prism=u),u.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},u.languages.markup.tag.inside["attr-value"].inside.entity=u.languages.markup.entity,u.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),u.languages.xml=u.languages.markup,u.languages.html=u.languages.markup,u.languages.mathml=u.languages.markup,u.languages.svg=u.languages.markup,u.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},u.languages.css.atrule.inside.rest=u.languages.css,u.languages.markup&&(u.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:u.languages.css,alias:"language-css",greedy:!0}}),u.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:u.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:u.languages.css}},alias:"language-css"}},u.languages.markup.tag)),u.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},u.languages.javascript=u.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,function:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),u.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"},constant:/\b[A-Z][A-Z\d_]*\b/}),u.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${[^}]+}|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\${[^}]+}/,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}}}),u.languages.javascript["template-string"].inside.interpolation.inside.rest=u.languages.javascript,u.languages.markup&&u.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:u.languages.javascript,alias:"language-javascript",greedy:!0}}),u.languages.js=u.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(){var l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(document.querySelectorAll("pre[data-src]")).forEach(function(e){for(var t,n=e.getAttribute("data-src"),r=e,i=/\blang(?:uage)?-([\w-]+)\b/i;r&&!i.test(r.className);)r=r.parentNode;if(r&&(t=(e.className.match(i)||[,""])[1]),!t){var a=(n.match(/\.(\w+)$/)||[,""])[1];t=l[a]||a}var o=document.createElement("code");o.className="language-"+t,e.textContent="",o.textContent="Loading…",e.appendChild(o);var s=new XMLHttpRequest;s.open("GET",n,!0),s.onreadystatechange=function(){4==s.readyState&&(s.status<400&&s.responseText?(o.textContent=s.responseText,u.highlightElement(o)):400<=s.status?o.textContent="✖ Error "+s.status+" while fetching file: "+s.statusText:o.textContent="✖ Error: File does not exist or is empty")},s.send(null)}),u.plugins.toolbar&&u.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),r=document.createElement("a");return r.textContent=t.getAttribute("data-download-link-label")||"Download",r.setAttribute("download",""),r.href=n,r}})},document.addEventListener("DOMContentLoaded",self.Prism.fileHighlight))});function q(e,r){var i=[],a={};return e.forEach(function(e){var t=e.level||1,n=t-1;r?@[\]^`{|}~]/g;function B(e){return e.toLowerCase()}function U(e){if("string"!=typeof e)return"";var t=e.trim().replace(/[A-Z]+/g,B).replace(/<[^>\d]+>/g,"").replace(I,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),n=H[t];return n=l.call(H,t)?n+1:0,(H[t]=n)&&(t=t+"-"+n),t}function D(e,t){return''+t+''}U.clear=function(){H={}};var Z=decodeURIComponent,Y=encodeURIComponent;function W(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))&&e.split("&").forEach(function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&Z(t[1])}),n}function G(e,t){void 0===t&&(t=[]);var n=[];for(var r in e)-1=g.length))for(var t=0;t=g.length)break}}else n.content&&"string"!=typeof n.content&&f(n.content)}};f(p.tokens)}}}});var te={};function ne(e){void 0===e&&(e="");var r={};return e&&(e=e.replace(/^'/,"").replace(/'$/,"").replace(/(?:^|\s):([\w-]+)=?([\w-]+)?/g,function(e,t,n){return r[t]=n&&n.replace(/"/g,"")||!0,""}).trim()),{str:e,config:r}}var re={markdown:function(e){return{url:e}},mermaid:function(e){return{url:e}},iframe:function(e,t){return{html:'"}},video:function(e,t){return{html:'"}},audio:function(e,t){return{html:'"}},code:function(e,t){var n=e.match(/\.(\w+)$/);return"md"===(n=t||n&&n[1])&&(n="markdown"),{url:e,lang:n}}},ie=function(i,e){var a=this;this.config=i,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=i.externalLinkTarget||"_blank",this.contentBase=e.getBasePath();var o,t=this._initRenderer(),n=i.markdown||{};o=u(n)?n(M,t):(M.setOptions(d(n,{renderer:d(t,n.renderer)})),M),this._marked=o,this.compile=function(n){var r=!0,e=s(function(e){r=!1;var t="";return n?(t=c(n)?o(n):o.parser(n),t=i.noEmoji?t:t.replace(/<(pre|template|code)[^>]*?>[\s\S]+?<\/(pre|template|code)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(/:(\w+?):/gi,f&&window.emojify||D).replace(/__colon__/g,":"),U.clear(),t):n})(n),t=a.router.parse().file;return r?a.toc=a.cacheTOC[t]:a.cacheTOC[t]=[].concat(a.toc),e}};ie.prototype.compileEmbed=function(e,t){var n,r=ne(t),i=r.str,a=r.config;if(t=i,a.include){var o;if(X(e)||(e=K(this.contentBase,Q(this.router.getCurrentPath()),e)),a.type&&(o=re[a.type]))(n=o.call(this,e,t)).type=a.type;else{var s="code";/\.(md|markdown)/.test(e)?s="markdown":/\.mmd/.test(e)?s="mermaid":/\.html?/.test(e)?s="iframe":/\.(mp4|ogg)/.test(e)?s="video":/\.mp3/.test(e)&&(s="audio"),(n=re[s].call(this,e,t)).type=s}return n.fragment=a.fragment,n}},ie.prototype._matchNotCompileLink=function(e){for(var t=this.config.noCompileLinks||[],n=0;n
    '+r+""},t.code=e.code=function(e,t){return void 0===t&&(t=""),e=e.replace(/@DOCSIFY_QM@/g,"`"),'
    '+a.highlight(e,a.languages[t]||a.languages.markup)+"
    "},t.link=e.link=function(e,t,n){void 0===t&&(t="");var r="",i=ne(t),a=i.str,o=i.config;return t=a,X(e)||l._matchNotCompileLink(e)||o.ignore?r+=0===e.indexOf("mailto:")?"":' target="'+s+'"':(e===l.config.homepage&&(e="README"),e=u.toURL(e,null,u.getCurrentPath())),o.target&&(r+=" target="+o.target),o.disabled&&(r+=" disabled",e="javascript:void(0)"),t&&(r+=' title="'+t+'"'),'"+n+""},t.paragraph=e.paragraph=function(e){return/^!>/.test(e)?r("tip",e):/^\?>/.test(e)?r("warn",e):"

    "+e+"

    "},t.image=e.image=function(e,t,n){var r=e,i="",a=ne(t),o=a.str,s=a.config;t=o,s["no-zoom"]&&(i+=" data-no-zoom"),t&&(i+=' title="'+t+'"');var l=s.size;if(l){var c=l.split("x");c[1]?i+="width="+c[0]+" height="+c[1]:i+="width="+c[0]}return X(e)||(r=K(p,Q(u.getCurrentPath()),e)),''+n+'"},t.list=e.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",n&&1"+e+""},t.listitem=e.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},ie.prototype.sidebar=function(e,t){var n=this.toc,r=this.router.getCurrentPath(),i="";if(e)i=this.compile(e);else{for(var a=0;a{inner}"),this.cacheTree[r]=l}return i},ie.prototype.subSidebar=function(e){if(e){var t=this.router.getCurrentPath(),n=this.cacheTree,r=this.toc;r[0]&&r[0].ignoreAllSubs&&r.splice(0),r[0]&&1===r[0].level&&r.shift();for(var i=0;i=t||e.classList.contains("hidden")?$(y,"add","sticky"):$(y,"remove","sticky")}}function se(e,t,r,n){var i=[];null!=(t=v(t))&&(i=w(t,"a"));var a,o=decodeURI(e.toURL(e.getCurrentPath()));return i.sort(function(e,t){return t.href.length-e.href.length}).forEach(function(e){var t=e.getAttribute("href"),n=r?e.parentNode:e;0!==o.indexOf(t)||a?$(n,"remove","active"):(a=e,$(n,"add","active"))}),n&&(b.title=a?a.title||a.innerText+" - "+ae:ae),a}var le=function(){function r(e,t){for(var n=0;nthis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}}]),t}(),ue={},pe=!1,he=null,de=!0,ge=0;function fe(e){if(de){for(var t,n=v(".sidebar"),r=w(".anchor"),i=x(n,".sidebar-nav"),a=x(n,"li.active"),o=document.documentElement,s=(o&&o.scrollTop||document.body.scrollTop)-ge,l=0,c=r.length;ls){t||(t=u);break}t=u}if(t){var p=ue[me(decodeURIComponent(e),t.getAttribute("data-id"))];if(p&&p!==a&&(a&&a.classList.remove("active"),p.classList.add("active"),a=p,!pe&&y.classList.contains("sticky"))){var h=n.clientHeight,d=a.offsetTop+a.clientHeight+40,g=d-0=i.scrollTop&&d<=i.scrollTop+h?i.scrollTop:g?0:d-h;n.scrollTop=f}}}}function me(e,t){return e+"?id="+t}function ve(e,t){if(t){var n,r=x("#"+t);r&&(n=r,he&&he.stop(),de=!1,he=new ce({start:window.pageYOffset,end:n.getBoundingClientRect().top+window.pageYOffset,duration:500}).on("tick",function(e){return window.scrollTo(0,e)}).on("done",function(){de=!0,he=null}).begin());var i=ue[me(e,t)],a=x(v(".sidebar"),"li.active");a&&a.classList.remove("active"),i&&i.classList.add("active")}}var be=b.scrollingElement||b.documentElement;var ye={};function ke(e,i){var o=e.compiler,a=e.raw;void 0===a&&(a="");var t=e.fetch,n=ye[a];if(n){var r=n.slice();return r.links=n.links,i(r)}var s=o._marked,l=s.lexer(a),c=[],u=s.InlineLexer.rules.link,p=l.links;l.forEach(function(e,a){"paragraph"===e.type&&(e.text=e.text.replace(new RegExp(u.source,"g"),function(e,t,n,r){var i=o.compileEmbed(n,r);return i&&c.push({index:a,embed:i}),e}))});var h=0;!function(e,a){var t,n=e.embedTokens,o=e.compile,s=(e.fetch,0),l=1;if(!n.length)return a({});for(;t=n[s++];){var r=function(i){return function(e){var t;if(e)if("markdown"===i.embed.type)t=o.lexer(e);else if("code"===i.embed.type){if(i.embed.fragment){var n=i.embed.fragment,r=new RegExp("(?:###|\\/\\/\\/)\\s*\\["+n+"\\]([\\s\\S]*)(?:###|\\/\\/\\/)\\s*\\["+n+"\\]");e=((e.match(r)||[])[1]||"").trim()}t=o.lexer("```"+i.embed.lang+"\n"+e.replace(/`/g,"@DOCSIFY_QM@")+"\n```\n")}else"mermaid"===i.embed.type?(t=[{type:"html",text:'
    \n'+e+"\n
    "}]).links={}:(t=[{type:"html",text:e}]).links={};a({token:i,embedToken:t}),++l>=s&&a({})}}(t);t.embed.url?F(t.embed.url).then(r):r(t.embed.html)}}({compile:s,embedTokens:c,fetch:t},function(e){var t=e.embedToken,n=e.token;if(n){var r=n.index+h;d(p,t.links),l=l.slice(0,r).concat(t,l.slice(r+1)),h+=t.length-1}else ye[a]=l.concat(),l.links=ye[a].links=p,i(l)})}function xe(){var e=w(".markdown-section>script").filter(function(e){return!/template/.test(e.type)})[0];if(!e)return!1;var t=e.innerText.trim();if(!t)return!1;setTimeout(function(e){window.__EXECUTE_RESULT__=new Function(t)()},0)}function we(e,t,n){var r,i,a;return t="function"==typeof n?n(t):"string"==typeof n?(i=[],a=0,(r=n).replace(N,function(t,e,n){i.push(r.substring(a,n-1)),a=n+=t.length+1,i.push(function(e){return("00"+("string"==typeof z[t]?e[z[t]]():z[t](e))).slice(-t.length)})}),a!==r.length&&i.push(r.substring(a)),function(e){for(var t="",n=0,r=e||new Date;n'):""),t.coverpage&&(u+=(i=", 100%, 85%",'
    \x3c!--cover--\x3e
    ')),t.logo){var h=/^data:image/.test(t.logo),d=/(?:http[s]?:)?\/\//.test(t.logo),g=/^\./.test(t.logo);h||d||g||(t.logo=K(e.router.getBasePath(),t.logo))}u+=(r='',(m?r+"
    ":"
    "+r)+'
    \x3c!--main--\x3e
    '),e._renderTo(c,u,!0)}else e.rendered=!0;t.mergeNavbar&&m?p=x(".sidebar"):(l.classList.add("app-nav"),t.repo||l.classList.add("no-badge")),t.loadNavbar&&A(p,l),t.themeColor&&(b.head.appendChild(_("div",(o=t.themeColor,"")).firstElementChild),function(n){if(!(window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)"))){var e=w("style:not(.inserted),link");[].forEach.call(e,function(e){if("STYLE"===e.nodeName)j(e,n);else if("LINK"===e.nodeName){var t=e.getAttribute("href");if(!/\.css$/.test(t))return;F(t).then(function(e){var t=_("style",e);k.appendChild(t),j(t,n)})}})}}(t.themeColor)),e._updateRender(),$(y,"ready")}var Ae={};var Ce=function(e){this.config=e};function Ee(e){var t=location.href.indexOf("#");location.replace(location.href.slice(0,0<=t?t:0)+"#"+e)}Ce.prototype.getBasePath=function(){return this.config.basePath},Ce.prototype.getFile=function(e,t){void 0===e&&(e=this.getCurrentPath());var n,r,i=this.config,a=this.getBasePath(),o="string"==typeof i.ext?i.ext:".md";return e=i.alias?function e(t,n,r){var i=Object.keys(n).filter(function(e){return(Ae[e]||(Ae[e]=new RegExp("^"+e+"$"))).test(t)&&t!==r})[0];return i?e(t.replace(Ae[i],n[i]),n,t):t}(e,i.alias):e,n=e,r=o,e=(e=new RegExp("\\.("+r.replace(/^\./,"")+"|html)$","g").test(n)?n:/\/$/g.test(n)?n+"README"+r:""+n+r)==="/README"+o&&i.homepage||e,e=X(e)?e:K(a,e),t&&(e=e.replace(new RegExp("^"+a),"")),e},Ce.prototype.onchange=function(e){void 0===e&&(e=p),e()},Ce.prototype.getCurrentPath=function(){},Ce.prototype.normalize=function(){},Ce.prototype.parse=function(){},Ce.prototype.toURL=function(e,t,n){var r=n&&"#"===e[0],i=this.parse(ee(e));if(i.query=d({},i.query,t),e=(e=i.path+G(i.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=n.indexOf("?");e=(0([^<]*?)

    $');if(i){if("color"===i[2])n.style.background=i[1]+(i[3]||"");else{var a=i[1];$(n,"add","has-mask"),X(i[1])||(a=K(this.router.getBasePath(),i[1])),n.style.backgroundImage="url("+a+")",n.style.backgroundSize="cover",n.style.backgroundPosition="center center"}r=r.replace(i[0],"")}this._renderTo(".cover-main",r),oe()}else $(n,"remove","show")},Ne._updateRender=function(){!function(e){var t=v(".app-name-link"),n=e.config.nameLink,r=e.route.path;if(t)if(c(e.config.nameLink))t.setAttribute("href",n);else if("object"==typeof n){var i=Object.keys(n).filter(function(e){return-1":">",'"':""","'":"'","/":"/"};return String(e).replace(/[&<>"'/]/g,function(e){return n[e]})}function o(o,r){var e,n,t="auto"===o.paths,s=(e=o.namespace)?u.EXPIRE_KEY+"/"+e:u.EXPIRE_KEY,c=(n=o.namespace)?u.INDEX_KEY+"/"+n:u.INDEX_KEY,a=localStorage.getItem(s)l.length&&(o=l.length);var r="..."+h(l).substring(i,o).replace(t,''+e+"")+"...";c+=r}}),0\n

    '+e.title+"

    \n

    "+e.content+"

    \n\n"}),t.classList.add("show"),a.classList.add("show"),t.innerHTML=s||'

    '+d+"

    ",c.hideOtherSidebarContent&&(i.classList.add("hide"),o.classList.add("hide"))}function l(e){c=e}function r(e,n){var t,a,i,o,r=n.router.parse().query.s;l(e),Docsify.dom.style("\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0 7px;\n line-height: 36px;\n font-size: 14px;\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.search .clear-button {\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"),function(e){void 0===e&&(e="");var n='
    \n \n
    \n \n \n \n \n \n
    \n
    \n
    \n ',t=Docsify.dom.create("div",n),a=Docsify.dom.find("aside");Docsify.dom.toggleClass(t,"search"),Docsify.dom.before(a,t)}(r),a=Docsify.dom.find("div.search"),i=Docsify.dom.find(a,"input"),o=Docsify.dom.find(a,".input-wrap"),Docsify.dom.on(a,"click",function(e){return"A"!==e.target.tagName&&e.stopPropagation()}),Docsify.dom.on(i,"input",function(n){clearTimeout(t),t=setTimeout(function(e){return s(n.target.value.trim())},100)}),Docsify.dom.on(o,"click",function(e){"INPUT"!==e.target.tagName&&(i.value="",s())}),r&&setTimeout(function(e){return s(r)},500)}function p(e,n){l(e),function(e,n){var t=Docsify.dom.getNode('.search input[type="search"]');if(t)if("string"==typeof e)t.placeholder=e;else{var a=Object.keys(e).filter(function(e){return-1