├── .gitattributes ├── LICENSE ├── README.md ├── docs ├── booknote │ └── 高效阅读法.md ├── design │ ├── Spring、Guava框架如何设计观察者模式?.md │ ├── 图解设计模式:身份认证场景的应用.md │ ├── 摊牌了!策略模式在项目设计中用的最多.md │ ├── 春节期间,我用责任链模式重构了业务代码.md │ ├── 某厂面试:如何优雅使用SPI机制.md │ └── 火遍全网的Hutool,如何使用Builder模式创建线程池.md ├── distributed │ └── 彻底掌握分布式事务2PC、3PC模型.md ├── jvm │ └── 如果线上遇到了OOM,该如何解决?.md ├── scene │ ├── MySQL单表千万数据量如何深分页优化.md │ ├── Snowflake(雪花算法),生产环境生成重复ID.md │ ├── 如何保证使用MyBatis,查询千万数据量不发生内存溢出?.md │ └── 线上问题复盘,异常信息消失的罪魁祸首JVM-Fast-Throw.md └── sourcecode │ ├── Mybatis核心架构设计分享.md │ ├── 心态崩了呀!Mybatis动态代理这么玩?!.md │ ├── 花一个周末,掌握OpenFeign核心原理.md │ └── 花一个周末,掌握SpringCloud-Ribbon核心原理.md └── images └── 公众号.png /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-detectable=true 2 | *.md linguist-documentation=false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [👉 《小马哥的代码实战课》官方知识星球来啦!!!](https://xiaomage.info/knowledge-planet/) 2 | 3 | 如果大家想要实时关注 Framework 最新动态以及干货分享的话,可以关注我的公众号:**龙台的技术笔记** 4 | 5 | ![](https://github.com/acmenlt/framework/blob/main/images/公众号.png) 6 | 7 | ## 场景问题 8 | 9 | 1. [Snowflake(雪花算法),生产环境生成重复ID](https://github.com/acmenlt/framework/blob/main/docs/scene/Snowflake(雪花算法),生产环境生成重复ID.md) 10 | 2. [线上问题复盘,异常信息消失的罪魁祸首JVM-Fast-Throw](https://github.com/acmenlt/framework/blob/main/docs/scene/线上问题复盘,异常信息消失的罪魁祸首JVM-Fast-Throw.md) 11 | 3. [MySQL单表千万数据量,如何深分页优化?](https://github.com/acmenlt/framework/blob/main/docs/scene/MySQL单表千万数据量如何深分页优化.md) 12 | 4. [如何保证使用MyBatis,查询千万数据量不发生内存溢出?](https://github.com/acmenlt/framework/blob/main/docs/scene/如何保证使用MyBatis,查询千万数据量不发生内存溢出?.md) 13 | 14 | ## JVM 15 | 16 | 1. [如果线上遇到了OOM,该如何解决?](https://github.com/acmenlt/framework/blob/main/docs/jvm/如果线上遇到了OOM,该如何解决?.md) 17 | 18 | ## 设计模式 19 | 20 | 1. [春节期间,我用责任链模式重构了业务代码](https://github.com/acmenlt/framework/blob/main/docs/design/春节期间,我用责任链模式重构了业务代码.md) 21 | 2. [Spring、Guava框架如何设计观察者模式?](https://github.com/acmenlt/framework/blob/main/docs/design/Spring、Guava框架如何设计观察者模式?.md) 22 | 3. [摊牌了!策略模式在项目设计中用的最多](https://github.com/acmenlt/framework/blob/main/docs/design/摊牌了!策略模式在项目设计中用的最多.md) 23 | 4. [火遍全网的Hutool,如何使用Builder模式创建线程池?](https://github.com/acmenlt/framework/blob/main/docs/design/火遍全网的Hutool,如何使用Builder模式创建线程池.md) 24 | 5. [某厂面试:如何优雅使用SPI机制?](https://github.com/acmenlt/framework/blob/main/docs/design/某厂面试:如何优雅使用SPI机制.md) 25 | 6. [图解设计模式:身份认证场景的应用](https://github.com/acmenlt/framework/blob/main/docs/design/图解设计模式:身份认证场景的应用.md) 26 | 27 | ## 影子库压测 28 | 29 | TODO 30 | 31 | ## 蓝绿发布 32 | 33 | TODO 34 | 35 | ## 灰度发布 36 | 37 | TODO 38 | 39 | ## 分布式链路追踪 40 | 41 | TODO 42 | 43 | ## 异地多活 44 | 45 | TODO 46 | 47 | ## 分布式事务 48 | 49 | 1. [彻底掌握分布式事务2PC、3PC模型](https://github.com/acmenlt/framework/blob/main/docs/distributed/彻底掌握分布式事务2PC、3PC模型.md) 50 | 51 | ## 源码篇 52 | 53 | ### SpringCloud 54 | 55 | 1. [花一个周末,掌握OpenFeign核心原理](https://github.com/acmenlt/framework/blob/main/docs/sourcecode/花一个周末,掌握OpenFeign核心原理.md) 56 | 2. [花一个周末,掌握SpringCloud-Ribbon核心原理](https://github.com/acmenlt/framework/blob/main/docs/sourcecode/花一个周末,掌握SpringCloud-Ribbon核心原理.md) 57 | 58 | ### Mybatis 59 | 60 | 1. [心态崩了呀!Mybatis 动态代理这么玩?!](https://github.com/acmenlt/framework/blob/main/docs/sourcecode/心态崩了呀!Mybatis动态代理这么玩?!.md) 61 | 2. [Mybatis核心架构设计分享](https://github.com/acmenlt/framework/blob/main/docs/sourcecode/Mybatis核心架构设计分享.md) 62 | 63 | 64 | ## 读书笔记 65 | 66 | 1. [《高效阅读法》本田直之](https://github.com/acmenlt/framework/blob/main/docs/booknote/高效阅读法.md) 67 | -------------------------------------------------------------------------------- /docs/booknote/高效阅读法.md: -------------------------------------------------------------------------------- 1 | ## 《高效阅读法》- 本田直之 2 | 3 | 在使用金钱上,我认为再没有像书籍这样棒的投资对象了。因为书籍是最好的自我投资 4 | 5 | 读书与不读书就像“少劳而获” 和 “辛劳而获” 之间的差异;不读书的的话,就像 “辛劳而获” 一样,自己每次都要从零开始,要从各种错误中探索学习并实践,才能有所回报。但是籍由读书,你就能像 “少劳而获” 那样,靠着累积起来的 “个人资产”,用少少的力气,就能得到大大的回报 6 | 7 | ### 1 - 在商学院发现 “多读术” 005 8 | 9 | 意识到 “寻找问题的解决之道” 这个明确的目的了,所以不读多余的不重要的部分,因而加速了重点的掌握 10 | 11 | 带着目的去读书,可以选择跳着读,因为跳着读不仅能思考,还能正确地掌握其中的意义。书,就素那没有全部读完,也是可以的 12 | 13 | 秉持 “目的” 意识,进而采取 “舍弃不重要” 的读书方法,这样一来,一本书可以在短时间内就能读完,也因此逐渐变得会读书;只吸收必要的资讯,而且逐渐会读有用的新书。总之,就是提升对资讯的取舍能力 14 | 15 | ### 2 - 什么是杠杆阅读术 013 16 | 17 | “杠杆阅读术” 即书本的多度法则,所强调的是 “累计效果”。可以选择多读几本相同主题的书,并从中获取资讯,你就渐渐能掌握不同作者的想法。在这样的过程中,你将会选出结合自己的读物,这也是多读的优点所在 18 | 19 | 如何运用 “杠杆效益” 呢?读书,然后以自己独树一帜的方式应用书中所阐述的诀窍,活用在现实事务中 20 | 21 | 学习某个成功人士的做法,再加上自己独树一帜的应用法,这就是走向成功的捷径 22 | 23 | 没有实际经验的伟大老师所写的只是阐述理论,我不太买像这样的教科书。我会尽可能地读有实际经验的人写自己经验的书,实践出真知 24 | 25 | ### 3 - 商人读书就像运动员的练习 017 26 | 27 | 商人读书就好比运动员的练习。也就是说,不读书的商人就像是平时不练习而突然临时比赛的运动员一样 28 | 29 | 读书的方式应该是,先读 “目录” “后记”,据此判断是否对自己有所帮助,如果决定读这本书,就针对有用的地方做笔记 30 | 31 | ### 4 - 舍弃读书的 ”常识“ 019 32 | 33 | 这是一种有策略的、互动的行为。具备清楚的目的,精挑细选该读的书,没有用的部分迅速丢弃;这样的做法也是必要的 34 | 35 | 一般的观念是 ”不可弄脏书本“。但在杠杆阅读术中,在书本上标号、画线、记录、折页脚是理所当然的。此外,有的观念是 ”不能跳着阅读“,”从头到尾,一字不漏的阅读才是真正的读书“,但在多读术中,纵然你花钱买书,也不用读完一整本 36 | 37 | 基本上,一本书应该设定一个小时读完。然后,不是读一次就结束了。还要下功夫去找这本书的精华,向学以致用的方向进行 38 | 39 | ### 5 - 读书的目的要明确 100 40 | 41 | 开始读书之前,再一次确认读该书的目的,这样,你逐渐就能弄清楚书中重要的部分和不重要的部分 42 | 43 | 读书的流程: 44 | 45 | 1. 读书的目的要明确。看准该读的部分和不用读的部分 46 | 2. 设定时间限制。虽然是按照书的内容而定,但平均在一至两小时左右 47 | 3. 浏览一本书的内容。查看 “前言” “目录” “后记” 等,把整本书的结构体系映入脑海中 48 | 4. 开始读书。读书时要有轻重缓急之分 -- 重点的地方必须熟读,其它地方就略抓重点 -- 画线、标号、记录、折页角等做记号 49 | 50 | ### 6 - 何谓 “彩色浴” 103 51 | 52 | “彩色浴” 一词出自书籍 《考具》 53 | 54 | 熟练使用 “彩色浴” 效果,就算你快速翻阅书本,眼睛也能停在目的所在之处。所以,读书之前,必须先决定好目的 55 | 56 | > 这里我理解 “彩色浴” 是对自己种下的心里暗示 57 | 58 | ### 7 - 寻找适合自己的读书环境 106 59 | 60 | 好的书籍辉带给人勇气和干劲。在阅读之际,潜力逐渐被激发出来,冲劲也展现出来。如果早上看书,一大早潜力就被激发出来,则一整天保持着积极的情绪 61 | 62 | 早上读书,是每天激发潜力和调整工作步调的 “起搏器” 63 | 64 | ### 8 - 掌握 60% 就可以了 115 65 | 66 | 与其选出全部的一百项来读,而没有学到一项,还不如只选出重要的一项来实践,才能有所回报 67 | 68 | “假如舍弃的部分写到重点怎么办?” 69 | 70 | 这么想,就会感到不安。我非常了解这点。当然,有时会发生跳过重点的情况。然而,“这也是没办法的事情啊!” 最好这样豁达地去想 71 | 72 | ### 9 - 把 “80 / 20 法则” 应用在读书上 118 73 | 74 | 越用有效率的读书方法回报就越多。多读的第一步就是,首先要舍弃 “把书本从头到尾细读” 这种观念。从书本里得到 80% 的回报,只要靠读 20% 的内容就可获得 75 | 76 | ### 10 - 浏览一本书的内容 122 77 | 78 | 不要从第一页开始读。首先要开的,是位于封面勒口和版权页上的作者简介。其次,读书腰、封底。再之后看前言、目录 79 | 80 | 如果一开始就觉得没意思的书,马上就会放弃阅读。所谓的 “序言” 是以浅显易懂的形态,浓缩大体上的内容。“序言” 无趣的书,没有什么值得期待的 81 | 82 | ### 11 - 尽量利用空白处记录 133 83 | 84 | 读书作为一种投资,其要诀是设身处地地去读。一边阅读一边不断模拟 “如果是我的话,会怎么做?” 85 | 86 | 读书时,对于突然灵机一动想到的点子,或是对作者主张的自我看法,应该在空白处或空白页上全部记下来。另外准备便条纸和或笔记本来写,是非常没效率的 87 | 88 | ### 12 - 第三章摘要 145 89 | 90 | 读书前,先要在心里有个底,就是 “从这本书中学到什么?” 这样一来,重要很容易进入眼帘,而且其他没用的地方就不用去读,这样就可以在短时间内把一本书读完 91 | 92 | 把读书融入一天的作息习惯中。没有读书时间的概念,就老是抽不出时间读书。读书前,先决定 “几个小时内把书读完” 93 | 94 | 先把 “前言” “目录” “后记” 等过目一遍,以掌握整本书的概况。不要断了书的库存。没用的书立刻停止阅读 95 | 96 | 在重点上画线、折页角,并且边读边把想法全部记下来 97 | 98 | 读书的速度并不是固定,而是要有轻重缓急。一边读,一边设身处地去模拟书中描述的状况 99 | 100 | ### 13 - 把后续工作系统化 152 101 | 102 | 为准备考试而用功 103 | 104 | 不读一整本参考书,在开始读之前,从考试题中将考试的重点集中缩小范围 =》读书 =》在重要的地方画线 =》反复去读画线的地方=》解题看看 =》把记不住的部分写在卡片上 =》反复读卡片。随身携带,并利用空闲时间检测 =》记住后,丢掉卡片,只留下记不住的部分,再反复读 105 | 106 | 读商业书 107 | 108 | 找到自己面临的问题和目的 =〉》缩小该读的书的范围,拿到手并阅读 =〉》在重要的地方画线及标记好 =〉》从杠杆效益笔记中抽出要点,反复去读 =〉》在实践中尝试 =〉》恶补杠杆消息笔记,反复去读直到学会 =〉》实践并逐渐能以直觉反应般来应对处理 109 | 110 | ### 14 - 把重点内容累计并集中起来,输进电脑里 159 111 | 112 | 与其读完后马上整理,还不如搁置几天后再打进电脑里比较好。这是因为刚读完,不具有客观性的观点。稍微冷静之后再阅读时,你可能会认为有些内容没那么重要,或是内容有所重复 113 | 114 | ### 15 - 金科玉律的分享 168 115 | 116 | 当想要与同事或友人沟通,或是试图说服他人时,与其用我自己的话,还不如引用第三者所说的话,这样传达给对方时,更具有说服力 117 | 118 | ### 16 - 考实践活用知识 182 119 | 120 | 如果不去阅读,就始终没有开始,这是毋庸置疑的。但只是阅读,而没有实践的话,就会到此结束 121 | 122 | 只是加上经验,事情才算是 “行得通”。在此之前,只不过是 “了解” 而已。在现今资讯社会、知识偏重的时代里,认为 “只要了解,就可以行得通” 的人剧增,然而,这个想法是大错特错的。“行得通” 和 “了解” 之间,有一条既深且大的鸿沟。要填补这道鸿沟,就要靠实践的经验 123 | -------------------------------------------------------------------------------- /docs/design/Spring、Guava框架如何设计观察者模式?.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 今天讲解一篇行为型设计模式,什么是行为型?行为型主要负责设计 **类或对象之间的交互**。工作中常用的观察者模式就是一种行为型设计模式 4 | 5 | 最近在尝试重构之前写过的代码。在重新梳理过业务之后,发现已有的设计场景应该能够接入到设计模式,而且查看了代码的提交记录,更是坚定了此想法 6 | 7 | 保持之前的一贯作风,想要说明一个设计模式,需要三板斧支撑。什么是观察者模式?如何使用观察者模式?项目中应该如何应用? 8 | 9 | > 观察者设计模式大纲如下: 10 | > 11 | > 1. 什么是观察者模式 12 | > 2. 观察者模式代码如何写 13 | > 3. 如何使用观察者模式结合业务 14 | > 4. Guava EventBus 观察者模式 15 | > 5. Spring ApplicationEvent 事件模型 16 | > 6. 观察者模式最后的总结 17 | 18 | ## 什么是观察者模式 19 | 20 | **观察者模式** 是一种行为设计模式,允许定义一种订阅通知机制,可以在对象(被观察者)事件发生时通知多个 “观察” 该对象的观察者对象,所以也被称为 **发布订阅模式** 21 | 22 | 其实我个人而言,**不太喜欢使用文字去定义一种设计模式的语义**,因为这样总是难以理解。所以就有了下面生活中的例子,来帮助读者更好的去理解模式的语义。类图如下所示: 23 | 24 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210328185857885.png) 25 | 26 | 在举例说明前,先让我们熟悉下观察者模式中的 `角色类型` 以及代码示例。观察者模式由以下几部分角色组成,可以参考代码示例去理解,不要被文字描述带偏 27 | 28 | - **主题(被观察者)**(Subject):抽象主题角色把所有观察者对象保存在一个容器里,提供添加和移除观察者接口,并且提供出通知所有观察者对象接口(也有作者通过 `Observable` 描述) 29 | - **具体主题(具体被观察者)**(Concrete Subject):具体主题角色的职责就是`实现抽象目标角色的接口语义`,在被观察者状态更改时,给容器内所有注册观察者发送状态通知 30 | 31 | ```java 32 | public interface Subject { 33 | void register(Observer observer); // 添加观察者 34 | void remove(Observer observer); // 移除观察者 35 | void notify(String message); // 通知所有观察者事件 36 | } 37 | 38 | public class ConcreteSubject implements Subject { 39 | private static final List observers = new ArrayList(); 40 | 41 | @Override 42 | public void register(Observer observer) { observers.add(observer); } 43 | 44 | @Override 45 | public void remove(Observer observer) { observers.remove(observer); } 46 | 47 | @Override 48 | public void notify(String message) { observers.forEach(each -> each.update(message)); } 49 | } 50 | ``` 51 | 52 | - **抽象观察者**(Observer):抽象观察者角色是观察者的行为抽象,它定义了一个修改接口,当被观察者发出事件时通知自己 53 | - **具体观察者**(Concrete Observer):实现抽象观察者定义的更新接口,可以在被观察者发出事件时通知自己 54 | 55 | ```java 56 | public interface Observer { 57 | void update(String message); // String 入参只是举例, 真实业务不会限制 58 | } 59 | 60 | public class ConcreteObserverOne implements Observer { 61 | @Override 62 | public void update(String message) { 63 | // 执行 message 逻辑 64 | System.out.println("接收到被观察者状态变更-1"); 65 | } 66 | } 67 | 68 | public class ConcreteObserverTwo implements Observer { 69 | @Override 70 | public void update(String message) { 71 | // 执行 message 逻辑 72 | System.out.println("接收到被观察者状态变更-2"); 73 | } 74 | } 75 | ``` 76 | 77 | 我们跑一下上面的观察者模式示例,如果不出意外的话会将两个观察者执行逻辑中的日志打印输出。如果是平常业务逻辑,抽象观察者定义的入参是具有业务意义的,大家可以类比项目上使用到的 MQ Message 机制 78 | 79 | ```java 80 | public class Example { 81 | public static void main(String[] args) { 82 | ConcreteSubject subject = new ConcreteSubject(); 83 | subject.register(new ConcreteObserverOne()); 84 | subject.register(new ConcreteObserverTwo()); 85 | subject.notify("被观察者状态改变, 通知所有已注册观察者"); 86 | } 87 | } 88 | ``` 89 | 90 | ## 观察者模式结合业务 91 | 92 | 因为公司业务场景保密,所以下面我们通过【新警察故事】的电影情节,稍微篡改下剧情,模拟出我们的观察者模式应用场景 93 | 94 | 假设:目前我们有三个警察,分别是`龙哥、锋哥、老三`,他们受命跟进犯罪嫌疑人**阿祖**。如果发现犯罪嫌疑人阿祖有动静,龙哥、峰哥负责实施抓捕行动,老三向警察局摇人,流程图如下: 95 | 96 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210322195538070.png) 97 | 98 | 如果说使用常规代码写这套流程,是能够实现需求的,一把梭的逻辑可以实现一切需求。但是,如果说下次行动,龙哥让老三跟着自己实施抓捕,亦或者说龙哥团队扩张,来了老四、老五、老六... 99 | 100 | > 对比观察者模式角色定义,老四、老五、老六都是具体的观察者(Concrete Observer) 101 | 102 | 如果按照上面的设想,我们通过“一把梭”的方式把代码写出来会有什么问题呢?如下: 103 | 104 | 1. 首当其冲,**增加了代码的复杂性**。实现类或者说这个方法函数奇大无比,因为随着警员的扩张,代码块会越来越大 105 | 106 | 2. **违背了开闭原则**,因为会频繁改动不同警员的任务。每个警员的任务不是一成不变的,举个例子来说这次针对疑犯,让峰哥实施的抓捕行动,下次就可能是疏散民众,难道每次的更改都需要改动“一把梭”的代码 107 | 108 | 第一种我们可以通过,**大函数拆小函数** 或者 **大类拆分为小类** 的方式解决代码负责性问题。但是,开闭原则却不能避免掉,因为随着警员(观察者)的增多及减少,势必会面临频繁改动原函数的情况 109 | 110 | 当我们面对这种 **已知会变动**,并且可能会 **频繁变动不固定** 的代码,就要使用抽象思维来进行设计,进而保持代码的简洁、可维护 111 | 112 | 这里使用 Java SpringBoot 项目结构来书写观察者模式,代码最终推送到 Github 仓库。**读者可以先把仓库拉下来**,因为其中不止示例代码,还包括 Guava 和 Spring 的观察者模式实现,[GitHub 仓库地址](https://github.com/JavaSouce/design) 113 | 114 | 首先,定义观察者模式中的观察者角色,分别为抽象观察者接口以及三个具体观察者实现类。实际业务中,设计模式会和 Spring 框架相结合,所以示例代码中包含 Spring 相关注解及接口 115 | 116 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210328120411550.png) 117 | 118 | 其次,定义抽象被观察者接口以及具体被观察者实现类。同上,被观察者也需要成为 Spring Bean,托管于 IOC 容器管理 119 | 120 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210328123639110.png) 121 | 122 | 到这里,一个完整的观察者模式就完成了。但是,细心的读者会发现这样的观察者模式会有一个小问题,这里先不说明,继续往下看。接下来就需要实际操练一番,注册这些观察者,通过被观察者触发事件来通知观察者 123 | 124 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210328125454787.png) 125 | 126 | ### 如何实现开闭原则 127 | 128 | 看了应用的代码之后,函数体过大的问题已经被解决了,我们通过 **拆分成为不同的具体的观察者类** 来拆分总体逻辑。但是开闭原则问题呢?这就是上面所说的问题所在,我们目前是通过 **显示的引入具体观察者模式** 来进行添加到被观察者的通知容器中,如果后续添加警察老四、老五... 越来越多的警察时,还是需要改动原有代码,问题应该怎么解决呢 129 | 130 | 其实非常简单,平常 Web 项目基本都会使用 Spring 框架开发,那自然是要运用其中的特性解决场景问题。我们这里通过 **改造具体被观察者实现开闭原则** 131 | 132 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210328130024382.png) 133 | 134 | 如果看过之前作者写过的设计模式文章,对 `InitializingBean` 接口不会感到陌生,我们在 `afterPropertiesSet` 方法中,通过注入的 **IOC 容器获取到所有观察者对象** 并添加至被观察者通知容器中。这样的话,触发观察者事件,代码中只需要一行即可完成通知 135 | 136 | ```java 137 | @PostConstruct 138 | public void executor() { 139 | // 被观察者触发事件, 通知所有观察者 140 | subject.notify("阿祖有行动!"); 141 | } 142 | ``` 143 | 144 | 后续如果再有新的观察者类添加,只需要创建新的类实现抽象观察者接口即可完成需求。有时候,**能够被封装起来的不止是 DateUtil 类型的工具类**,一些设计模式也可以被封装,**继而更好的服务开发者灵活运用**。这里会分别介绍 `Guava#EventBus` 以及 `Spring#事件模型` 145 | 146 | ### 同步异步的概念 147 | 148 | 在介绍 `EventBus` 和 `Spring` 事件模型之前,有一道绕不过去的弯,那就是同步执行、异步执行的概念,以及在什么样的场景下使用同步、异步模型? 149 | 150 | - 同步执行:所谓同步执行,指的就是在发出一个请求后,**在没有获得调用结果之前,调用者就会等待在当前代码**。直到获取到调用方法的执行结果,才算是结束。总结一句话就是 **由调用者主动等待这个调用的结果,未返回之前不执行别的操作** 151 | 152 | - 异步执行:而异步执行恰恰相反,**发出调用请求后立即返回,并向下执行代码**。异步调用方法一般不会有返回结果,调用之后就可以执行别的操作,一般通过回调函数的方式通知调用者结果 153 | 154 | 这里给大家举个例子,能够很好的反应同步、异步的概念。比如说你想要给体检医院打电话预约体检,你说出自己想要预约的时间后,对面的小姐姐说:“稍等,我查一下时间是否可以”,这个时候如果你 **不挂电话,等着小姐姐查完告诉你** 之后才挂断电话,那这就是同步。如果她说稍等需要查一下,**你告诉她:“我先挂了,查到结果后再打过来”**,那这就是异步+回调 155 | 156 | 在我们上面写的示例代码上,毋庸置疑是通过同步的形式执行观察者模式,**那是否可以通过异步的方式执行观察者行为**?答案当然是可以。我们可以通过在 **观察者模式行为执行前创建一个线程**,那自然就是异步的。当然,不太建议你这么做,这样可能会牵扯出更多的问题。一起来看下 Guava 和 Spring 是如何封装观察者模式 157 | 158 | ## Guava EventBus 解析 159 | 160 | `EventBus` 是 `Google Guava` 提供的消息发布-订阅类库,是设计模式中的观察者模式(生产/消费者模型)的经典实现 161 | 162 | 具体代码已上传 GitHub 代码仓库,`EventBus` 实现中包含同步、异步两种方式,代码库中由同步方式实现观察者模式 163 | 164 | 因为 `EventBus` 并不是文章重点,所以这里只会对其原理进行探讨。首先 **EventBus 是一个同步类库**,如果需要使用异步的,那就创建时候指定 `AsyncEventBus` 165 | 166 | ```java 167 | // 创建同步 EventBus 168 | EventBus eventBus = new EventBus(); 169 | 170 | // 创建异步 AsyncEventBus 171 | EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(10)); 172 | ``` 173 | 174 | **注意一点**,创建 `AsyncEventBus` 需要指定线程池,其内部并没有默认指定。当然也别像上面代码直接用 `Executors` 创建,作者是为了图省事,如果从规范而言,还是消停的使用默认线程池构建方法创建 `new ThreadPoolExecutor(xxx);` 175 | 176 | `EventBus` 同步实现有一个比较有意思的点。观察者操作同步、异步行为时,均使用 `Executor` 去执行观察者内部代码,那如何保证 `Executor` 能同步执行呢。Guava 是这么做的:**实现 Executor 接口,重写执行方法,调用 run 方法** 177 | 178 | ```java 179 | enum DirectExecutor implements Executor { 180 | INSTANCE; 181 | 182 | @Override 183 | public void execute(Runnable command) { 184 | command.run(); 185 | } 186 | } 187 | ``` 188 | 189 | 大家有兴趣可以去看下 `EventBus` 源码,不是很难理解,工作使用上还是挺方便的。只不过也有不好的地方,因为 `EventBus` 属于进程内操作,如果使用异步 `AsyncEventBus` 执行业务,**存在丢失任务的可能** 190 | 191 | ## Spring 事件模型 192 | 193 | Spring 大拿设计的观察者模式抽象是作者看到的最优雅、最功能的设计 194 | 195 | 如果想要使用 `ApplicationEvent` 玩转观察者模式,只需要简单几步。总结:操作简单,功能强大 196 | 197 | 1. 创建业务相关的 `MyEvent`,需要继承 `ApplicationEvent`,重写有参构造函数 198 | 199 | 2. 定义不同的监听器(观察者)比如 `ListenerOne` 实现 `ApplicationListener` 接口,重写 `onApplicationEvent` 方法 200 | 201 | 3. 通过 `ApplicationContext#publishEvent` 方法发布具体事件 202 | 203 | Spring 事件与 Guava EventBus 一样,代码就不粘贴了,都已经存放到 Github 代码仓库。这里重点介绍下 Spring 事件模型的特点,以及使用事项 204 | 205 | Spring 事件同样支持异步编程,需要在具体 Listener 实现类上添加 `@Async` 注解。支持 Listener 订阅的顺序,比如说有 A、B、C 三个 Listener。可以通过 `@Order` 注解实现多个观察者顺序消费 206 | 207 | 作者建议读者朋友一定要跑下 `ApplicationEvent` 的 Demo,在使用框架的同时也 **要合理的运用框架提供的工具轮子**,因为被框架封装出的功能,一般而言要比自己写的功能更强大、出现问题的几率更少。同时,**切记不要造重复轮子**,除非功能点不满足的情况下,可以借鉴原有轮子的基础上开发自己功能 208 | 209 | 210 | ## 结言 211 | 212 | 文章通过图文并茂的方式帮助大家梳理了下观察者模式的实现方式,更是推出了进阶版的 `EventBus` 以及 `ApplicationEvent`,相信大家看完之后可以很愉快的在自己项目中玩耍设计模式了。切记哈,要在合理的场景下使用模式,一般而言观察者模式作用于 **观察者与被观察者之间的解耦合** 213 | 214 | 最后解答下最早提到的问题,项目中的观察者模式 **应该使用同步模型还是异步模型呢** 215 | 216 | 如果只是使用观察者模式拆分代码使其满足 **开闭原则、高内聚低耦合、职责单一** 等特性,那么自然是使用同步去做,因为这种方式是最为稳妥。而如果 **不关心观察者执行结果或者考虑性能** 等情况,则可以使用异步的方式,通过回调的方式满足业务返回需求 217 | 218 | -------------------------------------------------------------------------------- /docs/design/图解设计模式:身份认证场景的应用.md: -------------------------------------------------------------------------------- 1 | 今天和大家聊一聊,如何合理的将多种设计模式放到同一个业务场景中 2 | 3 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213134923162.png) 4 | 5 | ## 业务背景 6 | 7 | 最近接到一个认证的需求,C 端用户在购买公司保险时,需要先进行 **实名认证确认身份** 8 | 9 | 为了保证业务复用,单独将认证的逻辑拆分为微服务模块 10 | 11 | C 端用户下单购买保险的逻辑大致如下 12 | 13 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213134419177.png) 14 | 15 | 先说下关于认证相关的一些基本知识。简单来说,**你如何证明你是你自己** 16 | 17 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213133841960.png) 18 | 19 | 一些云服务厂商都会有关于验证身份的付费接口,接下来我们就以腾讯云姓名、身份证二要素认证为参考进行举例 20 | 21 | 说完认证知识,我们再来拆解下用户购买保险的步骤 22 | 23 | 1. 用户在前端发起认证行为 24 | 2. 请求经过网关调用保险服务,保险服务调用认证服务 25 | 3. 认证服务调用腾讯云认证付费 API,返回认证结果信息 26 | 27 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213134517571.png) 28 | 29 | ## 认证流程 30 | 31 | 在整个块认证流程中,我们会讲解三种设计模式,按照顺序分别是策略、责任链、模板模式 32 | 33 | ### 策略模式 34 | 35 | > 定义一组算法类,**将每个算法分别封装起来,让它们可以互相替换**。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化,客户端代指使用算法的代码 36 | 37 | 我们拿认证来说,定义一个认证接口,然后实现二、三、四要素以及人脸识别实现;将这些实现类放到一个 Map 容器中,并和业务规定好对应的标识 Key,通过标识 Key 获取对应的认证策略实现 38 | 39 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213182314757.png) 40 | 41 | 如果真的像上面这么简单,if-else 判断加上拆解几个认证函数就可以搞得定,还真的不一定需要策略模式 42 | 43 | 我们再延伸来看一种复杂场景:假设后续不满足于腾讯云的认证,为了保证可用性以及更多的流量,需要对接更多的认证平台 44 | 45 | >可用性:平台的接口不太可能保证全年百分百可用,需要有容灾降级或者替换方案 46 | > 47 | >更多的流量:腾讯云认证接口限流 100次 / S 48 | 49 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213183053611.png) 50 | 51 | 这个时候策略模式的优点就体现出来了,**简化代码的复杂性** 以及 **保证开闭原则,增加程序的健壮性以及可扩展性** 52 | 53 | 后续再增加三方认证平台和认证方式,都不需要改动原有逻辑,添加对应实现即可 54 | 55 | ### 责任链模式 56 | 57 | > 在责任链模式中,多个处理器(参照拦截器)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条,链条上的每个处理器 **各自承担各自的处理职责** 58 | 59 | 这里主要将责任链模式应用于,**规避无意义调用三方认证服务** 60 | 61 | 1. 已认证过的人员信息,在有效期内没必要再次调用 62 | 2. 调用认证结果错误,依然会扣钱,比如说名称中包含非中文,身份证格式错误等等 63 | 64 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20220213205418481.png) 65 | 66 | 我们可以将处理器尽量职责单一,方便后续其它认证方式的 **复用和编排** 67 | 68 | 69 | 70 | ### 模板方法 71 | 72 | > 模板方法模式在一个方法中定义一个 **算法骨架**,并将某些步骤推迟到 **子类中实现**。模板方法模式可以让子类在 **不改变算法整体结构的情况下,重新定义算法中的某些步骤** 73 | 74 | 75 | 76 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/format,png.png) 77 | 78 | 模版方法主要作用:**复用性** 和 **扩展性** 79 | 80 | - 复用性:核心思想就是 **父级定义公共实现**,**由子级进行调取使用** 81 | - 扩展性:**在不修改方法逻辑的前提下,变更其中的某些步骤** 82 | 83 | 通俗来讲 : 定义一个抽象类 `AbstractTemplate`,并定义一个或若干抽象方法 `abstractMethod`。代码大致如下: 84 | 85 | ```java 86 | public abstract class AbstractAuthenticationService { 87 | 88 | void before(T request) { 89 | } 90 | 91 | void after(T request) { 92 | } 93 | 94 | // 抽象方法 95 | protected abstract void practicalExecute(T request); 96 | 97 | public void authentication(T request) { 98 | // 前置拦截操作,包括不限于责任链模式调用 99 | before(request); 100 | // 策略模式实现,调用具体认证类,比如二要素认证或三要素认证 101 | practicalExecute(request); 102 | // 资源清理或记录认证完成信息 103 | after(request); 104 | } 105 | ``` 106 | 107 | 108 | 109 | 腾讯云二要素认证实现类,代码如下: 110 | 111 | ```java 112 | @Slf4j 113 | @Component 114 | @RequiredArgsConstructor 115 | // BaseAuthenticationStrategy 是策略模式实现,定义了 mark、execute 方法 116 | public class NameIdCardAuthenticationByTencentResolver extends AbstractAuthenticationService 117 | implements BaseAuthenticationStrategy { 118 | 119 | private static final String SUCCESS = "0"; 120 | 121 | // 责任链容器 122 | private final NameIdCardHandlerChain nameIdCardHandlerChain; 123 | 124 | @Override 125 | public String mark() { 126 | return AuthenticationEnum.TENCENT.name(); 127 | } 128 | 129 | @Override 130 | public void execute(NameIdCardAuthenticationReqDTO request) { 131 | authentication(request); 132 | } 133 | 134 | @Override 135 | public void before(NameIdCardAuthenticationReqDTO request) { 136 | // 责任链调用 137 | nameIdCardHandlerChain.doFilter(request); 138 | } 139 | 140 | @Override 141 | public void practicalExecute(NameIdCardAuthenticationReqDTO request) { 142 | // 腾讯云二要素认证具体行为 143 | } 144 | 145 | } 146 | ``` 147 | 148 | 149 | 150 | ## 最后总结 151 | 152 | 抛出一个老生常谈的问题,**学习设计模式有什么作用?** 153 | 154 | 设计模式主要是为了应对 **代码的复杂性**,让其满足 **开闭原则**,提高代码的 **扩展性**;合适的场景合理运用的设计模式,可以帮助代码实现 **高内聚、低耦合** 等的优点 155 | 156 | 你无法决定别人的代码,但你可以决定自己的。时间充足的情况下,**尽量以重构的方式去写每一行代码** 157 | 158 | 最后希望小伙伴读过文章后有所收获,祝好。 -------------------------------------------------------------------------------- /docs/design/摊牌了!策略模式在项目设计中用的最多.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 日常 Coding 过程中,设计模式三板斧:模版、构建者、策略,今天来说下第三板斧 **策略设计模式** 4 | 5 | 策略模式还是比较简单并且使用较多的,平常我们多运用策略模式用来消除 if-else、switch 等多重判断的代码,消除 if-else、switch 多重判断 **可以有效应对代码的复杂性** 6 | 7 | 如果分支判断会不断变化(增、删、改),那么可以使用别的技巧让其满足开闭原则,提高代码的扩展性 (策略模式场景主要负责解耦,开闭原则需要额外支持) 8 | 9 | 下文中会详细列举如何使用设计模式做个 Demo 、模式的真实场景以及策略模式的好处 10 | 11 | > 策略设计模式大纲如下: 12 | > 13 | > 1. 什么是策略模式 14 | > 2. Spring 项目中真实的应用场景 15 | > 3. 框架源码底层如何玩耍策略模式 16 | > 4. 策略模式总结 17 | 18 | ## 什么是策略模式 19 | 20 | 策略模式在 GoF 的《设计模式》一书中,是这样定义的: 21 | 22 | Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. 23 | 24 | 定义一组算法类,**将每个算法分别封装起来,让它们可以互相替换**。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化,客户端代指使用算法的代码 25 | 26 | 看到上面的介绍可能不太明白策略模式具体为何物,这里会从最基本的代码说起,一步一步彻底掌握此模式。下述代码可能大家都能联想出对应的业务,**根据对应的优惠类型,对价格作出相应的优惠** 27 | 28 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215131731566.png) 29 | 30 | 这段代码是能够满足项目中业务需求的,而且很多已上线生产环境的代码也有这类代码。但是,这一段代码存在存在两个弊端 31 | 32 | 1. 代码的复杂性,正常业务代码逻辑肯定会比这个代码块复杂很多,这也就 **导致了 if-else 的分支以及代码数量过多**。这种方式可以通过将代码拆分成独立函数或者拆分成类来解决 33 | 2. 开闭原则,价格优惠肯定会 **随着不同的时期作出不同的改变**,或许新增、删除或修改。如果在一个函数中修改无疑是件恐怖的事情,想想可能多个开发者分别进行开发,杂乱无章的注释,混乱的代码逻辑等情况十有八九会发生 34 | 35 | 如何运用策略模式优化上述代码,使程序设计看着简约、可扩展等特性 36 | 37 | 1. 简化代码的复杂性,将不同的优惠类型定义为不同的策略算法实现类 38 | 2. 保证开闭原则,增加程序的健壮性以及可扩展性 39 | 40 | ### 策略模式示例 41 | 42 | 将上述代码块改造为策略设计模式,大致需要三个步骤 43 | 44 | 1. 定义抽象策略接口,因为业务使用接口而不是具体的实现类的话,便可以灵活的替换不同的策略 45 | 2. 定义具体策略实现类,实现自抽象策略接口,其内部封装具体的业务实现 46 | 3. 定义策略工厂,封装创建策略实现(算法),对客户端屏蔽具体的创建细节 47 | 48 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215133551878.png) 49 | 50 | 目前把抽象策略接口、具体的策略实现类以及策略工厂都已经创建了,现在可以看一下客户端需要如何调用,又是如何对客户端屏蔽具体实现细节的 51 | 52 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215141732464.png) 53 | 54 | 根据代码块图片得知,具体策略类是从策略工厂中获取,确实是取消了 if-else 设计,**在工厂中使用 Map 存储策略实现**。获取到策略类后执行具体的优惠策略方法就可以获取优惠后的金额 55 | 56 | 通过分析大家得知,目前这种设计确实将应用代码的复杂性降低了。**如果新增一个优惠策略,只需要新增一个策略算法实现类即可**。但是,添加一个策略算法实现,**意味着需要改动策略工厂中的代码**,还是不符合开闭原则 57 | 58 | 如何完整实现符合开闭原则的策略模式,需要借助 Spring 的帮助,详细案例请继续往下看 59 | 60 | 61 | ## 项目中真实的应用场景 62 | 63 | 最近项目中设计的一个功能用到了策略模式,分为两类角色,笔者负责定义抽象策略接口以及策略工厂,不同的策略算法需要各个业务方去实现,可以联想到上文中的优惠券功能。因为是 Spring 项目,所以都是按照 Spring 的方式进行处理,话不多说,上代码 64 | 65 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215150711949.png) 66 | 67 | 可以看到,比对上面的示例代码,有两处明显的变化 68 | 69 | 1. 抽象策略接口中,新定义了 mark() 接口,此接口用来标示算法的唯一性 70 | 2. 具体策略实现类,使用 @Component 修饰,将对象本身交由 Spring 进行管理 71 | 72 | > 小贴士:为了阅读方便,mark() 返回直接使用字符串替代,读者朋友在返回标示时最好使用枚举定义 73 | 74 | 接下来继续查看抽象策略工厂如何改造,才能满足开闭原则 75 | 76 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215151332606.png) 77 | 78 | 和之前责任链模式相同 (TODO 添加链接),都是通过 InitializingBean 接口实现中调用 IOC 容器查找对应策略实现,随后将策略实现 mark() 方法返回值作为 key, 策略实现本身作为 value 添加到 Map 容器中等待客户端的调用 79 | 80 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215152451136.png) 81 | 82 | 这里使用的 SpringBoot 测试类,注入策略工厂 Bean,通过策略工厂选择出具体的策略算法类,继而通过算法获取到优惠后的价格。小插曲:如果不想把策略工厂注入 Spring 也可以,实现方法有很多 83 | 84 | 总结下本小节,我们通过和 Spring 结合的方式,通过策略设计模式对文初的代码块进行了两块优化:应对代码的复杂性,让其满足开闭原则。更具体一些呢就是 **通过抽象策略算法类减少代码的复杂性,继而通过 Spring 的一些特性同时满足了开闭原则**,现在来了新需求只要添加新的策略类即可,健壮易扩展 85 | 86 | ## 源码底层如何耍策略模式 87 | 88 | 自己用肯定觉得不够,必要时候还得看看设计开源框架源码的大佬们如何在代码中运用策略模式的 89 | 90 | 在作者了解中,JDK、Spring、SpringMvc、Mybatis、Dubbo 等等都运用了策略设计模式,这里就以 Mybatis 举例说明 91 | 92 | Mybatis 中 Executor 代表执行器,负责增删改查的具体操作。其中用到了两种设计模式,模版方法以及策略模式 93 | 94 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215175249443.png) 95 | 96 | Executor 代表了抽象策略接口,刚才说到的模版方法模式源自 BaseExecutor 97 | 98 | Configuration 代表策略工厂,负责创建具体的策略算法实现类 99 | 100 | SimpleExecuto、ReuseExecutor... 表示封装了具体的策略算法实现类 101 | 102 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215175633352.png) 103 | 104 | 上述代码块发生在 Configuration 类中创建执行器 Executor,通过 executorType 判断创建不同的策略算法。 105 | 106 | 上述代码块并没有彻底消除 if-else,因为 Mybatis 中执行器策略基本是固定的,也就是说它只会有这些 if-else 判断,基本不会新增或修改。如果非要消除 if-else,可以这么搞,这里写一下伪代码 107 | 108 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210215180629772.png) 109 | 110 | 这种方式叫做 **"查表法"**,通过策略工厂实现消除 if-else 分支。最后,Mybatis 太过详细的设计这里不再赘述,有兴趣的小伙伴可以去把源码下载啃一啃 111 | 112 | 到了这里可能有读者看出了问题,**策略模式就算消除了 if-else 但是如果添加新的策略类,不还是会违反开闭原则么?** 113 | 114 | 没错,因为 Mybatis 本身没有引入 Spring 依赖,**所以没有办法借助 IOC 容器实现开闭原则**。Spring 是一种开闭原则解决方式,那还有没有别的解决方式? 115 | 116 | 解决方式有很多,开闭原则核心就是 **对原有代码修改关闭,对新增代码开放**。可以通过扫描指定包下的自定义注解亦或者通过 instanceof 判断是否继承自某接口都可以。不过, 项目如果用了 Spring 还是消停的吧 117 | 118 | ## 结言 119 | 120 | 文章中图文并茂的方式介绍策略模式,通过价格优惠的场景,进而引用本文的重点:策略设计模式,相信小伙伴看完后都会有一定的收获 121 | 122 | **策略模式的本质依然是对代码设计解耦合**,通过三类角色贯穿整个策略模式:抽象策略接口、策略工厂以及具体的策略实现类。通过细粒度的策略实现类避免了主体代码量过多,减少了设计中的复杂性 123 | 124 | 作者听到过很多小伙伴觉得自己做的都是 CRUD 工作,没有挑战性没意思。其实,我想说的是:**业务代码一样牛逼,一样能体现程序员的水平**。不一定非要高并发、大数据等场景。颇有一屋不扫何以扫天下的意思 125 | 126 | 最后抛出一个问题:**出现 if-else 的代码,一定要使用策略模式优化么** 127 | 128 | **如果 if-else 判断分支不多并且是固定的**,后续不会出现新的分支,那我们完全 **可以通过抽函数的方式降低程序复杂性**;不要想法设法去除 if-else 语句,存在即合理。而且,使用策略模式会导致类增多,没有必要为了少量的判断分支引入策略模式 129 | 130 | 关于策略设计模式本文就讲到这里,后面会陆续输出工厂、原型、享元等模式;**如果文章对你有帮助那就点个关注支持下吧,祝好。** 131 | -------------------------------------------------------------------------------- /docs/design/春节期间,我用责任链模式重构了业务代码.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 文章开篇,抛出一个老生常谈的问题,**学习设计模式有什么作用?** 4 | 5 | > 设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性 6 | 7 | 另外,学习的设计模式 **一定要在业务代码中落实**,只有理论没有真正实施,是无法真正掌握并且灵活运用设计模式的 8 | 9 | 这篇文章主要说 **责任链设计模式**,认识此模式是在读 Mybatis 源码时, Interceptor 拦截器主要使用的就是责任链,当时读过后就留下了很深的印象(内心 OS:还能这样玩) 10 | 11 | 文章先从基础概念说起,另外分析一波 Mybatis 源码中是如何运用的,最后按照 "习俗",设计一个真实业务场景上的应用 12 | 13 | > 责任链设计模式大纲如下: 14 | > 15 | > 1. 什么是责任链模式 16 | > 2. 完成真实的责任链业务场景设计 17 | > 3. Mybatis Interceptor 底层实现 18 | > 4. 责任链模式总结 19 | 20 | ## 什么是责任链模式 21 | 22 | 举个例子,SpringMvc 中可以定义拦截器,并且可以定义多个。当一个用户发起请求时,顺利的话请求会经过所有拦截器,最终到达业务代码逻辑,SpringMvc 拦截器设计就是使用了责任链模式 23 | 24 | > 为什么说顺利的话会经过所有拦截器?因为请求不满足拦截器自定义规则会被打回,但这并不是责任链模式的唯一处理方式,继续往下看 25 | 26 | 在责任链模式中,多个处理器(参照上述拦截器)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条,链条上的每个处理器 **各自承担各自的处理职责** 27 | 28 | 责任链模式中多个处理器形成的处理器链在进行处理请求时,有两种处理方式: 29 | 30 | 1. 请求会被 **所有的处理器都处理一遍,不存在中途终止的情况**,这里参照 MyBatis 拦截器理解 31 | 2. 二则是处理器链执行请求中,某一处理器执行时,**如果不符合自制定规则的话,停止流程,并且剩下未执行处理器就不会被执行**,大家参照 SpringMvc 拦截器理解 32 | 33 | 这里通过代码的形式对两种处理方式作出解答,方便读者更好的理解。首先看下第一种,请求会经过所有处理器执行的情况 34 | 35 | 36 | ![图1 责任链模式一种实现](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210104156343.png) 37 | 38 | `IHandler` 负责抽象处理器行为,`handle()` 则是不同处理器具体需要执行的方法,`HandleA`、`HandleB` 为具体需要执行的处理器类,`HandlerChain` 则是将处理器串成一条链执行的处理器链 39 | 40 | ```java 41 | public class ChainApplication { 42 | public static void main(String[] args) { 43 | HandlerChain handlerChain = new HandlerChain(); 44 | handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB())); 45 | handlerChain.handle(); 46 | /** 47 | * 程序执行结果: 48 | * HandlerA打印:执行 HandlerA 49 | * HandlerB打印:执行 HandlerB 50 | */ 51 | } 52 | } 53 | ``` 54 | 55 | 这种责任链执行方式会将所有的 **处理器全部执行一遍**,不会被打断。Mybatis 拦截器用的正是此类型,这种类型 **重点在对请求过程中的数据或者行为进行改变** 56 | 57 | ![图2 参考Mybatis拦截器实现](https://images-machen.oss-cn-beijing.aliyuncs.com/责任链模式-迅捷PDF转换器.gif) 58 | 59 | 而另外一种责任链模式实现,则是会对请求有阻断作用,阻断产生的前置条件是在处理器中自定义的,代码中的实现较简单,读者可以联想 SpringMvc 拦截器的实现流程 60 | 61 | ![图3 责任链模式一种实现](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210104344505.png) 62 | 63 | 根据代码看的出来,在每一个 IHandler 实现类中会返回一个布尔类型的返回值,如果返回布尔值为 false,那么责任链发起类会中断流程,剩余处理器将不会被执行。就像我们定义在 SpringMvc 中的 Token 拦截器,如果 Token 失效就不能继续访问系统,处理器将请求打回 64 | 65 | ```java 66 | public class ChainApplication { 67 | public static void main(String[] args) { 68 | HandlerChain handlerChain = new HandlerChain(); 69 | handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB())); 70 | boolean resultFlag = handlerChain.handle(); 71 | if (!resultFlag) { 72 | System.out.println("责任链中处理器不满足条件"); 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | 读者可以自己在 IDEA 中实现两种不同的责任链模式,对比其中的不同,设想下业务中真实的应用场景,再或者可以跑 SpringBoot 项目,创建多个拦截器来佐证文中的说辞 79 | 80 | ![图4 参考SpringMvc拦截器实现](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210123719802.png) 81 | 82 | 本章节介绍了责任链设计模式的具体语义,以及不同责任链实现类型代码举例,并以 Mybatis、SpringMvc 拦截器为参照点,介绍各自不同的代码实现以及应用场景 83 | 84 | ## 责任链业务场景设计 85 | 86 | 趁热打铁,本小节对使用的真实业务场景进行举例说明。假设业务场景是这样的,我们 **系统处在一个下游服务**,因为业务需求,系统中所使用的 **基础数据需要从上游中台同步到系统数据库** 87 | 88 | 基础数据包含了很多类型数据,虽然数据在中台会有一定验证,但是 **数据只要是人为录入就极可能存在问题,遵从对上游系统不信任原则**,需要对数据接收时进行一系列校验 89 | 90 | 最初是要进行一系列验证原则才能入库的,后来因为工期问题只放了一套非空验证,趁着春节期间时间还算宽裕,把这套验证规则骨架放进去 91 | 92 | 从我们系统的接入数据规则而言,个人觉得需要支持以下几套规则 93 | 94 | 1. **必填项校验**,如果数据无法满足业务所必须字段要求,数据一旦落入库中就会产生一系列问题 95 | 2. **非法字符校验**,因为数据如何录入,上游系统的录入规则是什么样的我们都不清楚,这一项规则也是必须的 96 | 3. **长度校验**,理由同上,如果系统某字段长度限制 50,但是接入来的数据 500长度,这也会造成问题 97 | 98 | 为了让读者了解业务嵌入责任链模式的前因,这里列举了三套校验规则,当然真实中可能不止这三套。但是 **一旦将责任链模式嵌入数据同步流程**,就会 **完全符合文初所提的开闭原则,提高代码的扩展性** 99 | 100 | > 本案例设计模式中的开闭原则通过 Spring 提供支持,后续添加新的校验规则就可以不必修改原有代码 101 | 102 | 这里要再强调下,**设计模式的应用场景一定要灵活掌握**,只有这样才能在合适的业务场景合理运用对象的设计模式 103 | 104 | 既然设计模式场景说过了,最后说一下需要达成的业务需求。将一个批量数据经过处理器链的处理,**返回出符合要求的数据分类** 105 | 106 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210152432725.png) 107 | 108 | 定义顶级验证接口和一系列处理器实现类没什么难度,但是应该如何进行链式调用呢? 109 | 110 | 这一块代码需要有一定 Spring 基础才能理解,一起来看下 VerifyHandlerChain 如何将所有处理器串成一条链 111 | 112 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210152950772.png) 113 | 114 | VerifyHandlerChain 处理流程如下: 115 | 116 | 1. 实现自 InitializingBean 接口,在对应实现方法中获取 IOC 容器中类型为 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler、SexyVerifyHandler 117 | 2. 将 VerifyHandler 类型的 Bean 添加到处理器链容器中 118 | 3. 定义校验方法 verify(),对入参数据展开处理器链的全部调用,如果过程中发现已无需要验证的数据,直接返回 119 | 120 | 这里使用 SpringBoot 项目中默认测试类,来测试一下如何调用 121 | 122 | ```java 123 | @SpringBootTest 124 | class ChainApplicationTests { 125 | 126 | @Autowired 127 | private VerifyHandlerChain verifyHandlerChain; 128 | 129 | @Test 130 | void contextLoads() { 131 | List verify = verifyHandlerChain.verify(Lists.newArrayList("源码兴趣圈", "@龙台")); 132 | System.out.println(verify); 133 | } 134 | } 135 | ``` 136 | 137 | 这样的话,如果客户或者产品提校验相关的需求时,我们只需要实现 VerifyHandler 接口新建个校验规则实现类就 OK 了,这样符合了设计模式的原则:**满足开闭原则,提高代码的扩展性** 138 | 139 | 熟悉之前作者写过设计模式的文章应该知道,**强调设计模式重语意,而不是具体的实现过程**。所以,你看这个咱们这个校验代码,把责任链两种模式结合了使用 140 | 141 | 上面的代码只是示例代码,实际业务中的实现要比这复杂很多,比如: 142 | 143 | 1. **如何定义处理器的先后调用顺序**。比如说某一个处理器执行时间很长并且过滤数据很少,所以希望把它放到最后面执行 144 | 2. 这是为当前业务的所有数据类型进行过滤,**如何自定义单个数据类型过滤**。比如你接入学生数据,学号有一定校验规则,这种处理器类肯定只适合单一类型 145 | 146 | 147 | 还有很多的业务场景,**所以设计模式强调的应该是一种思想,而不是固定的代码写法**,需要结合业务场景灵活变通 148 | 149 | ### 责任链模式的好处 150 | 151 | 一定要使用责任链模式么?不使用能不能完成业务需求? 152 | 153 | 回答是肯定可以,设计模式只是帮助减少代码的复杂性,让其满足开闭原则,提高代码的扩展性。如果不使用同样可以完成需求 154 | 155 | 如果不使用责任链模式,上面说的真实同步场景面临两个问题 156 | 157 | 1. 如果把上述说的代码逻辑校验规则写到一起,**毫无疑问这个类或者说这个方法函数奇大无比**。减少代码复杂性一贯方法是:**将大块代码逻辑拆分成函数,将大类拆分成小类**,是应对代码复杂性的常用方法。如果此时说:可以把不同的校验规则拆分成不同的函数,不同的类,这样不也可以满足减少代码复杂性的要求么。这样拆分是能解决代码复杂性,但是这样就会面临第二个问题 158 | 2. 开闭原则:**添加一个新的功能应该是,在已有代码基础上扩展代码,而非修改已有代码**。大家设想一下,假设你写了三套校验规则,运行过一段时间,这时候领导让加第四套,是不是要在原有代码上改动 159 | 160 | 综上所述,在合适的场景运用适合的设计模式,能够让代码设计复杂性降低,变得更为健壮。朝更远的说也能让自己的编码设计能力有所提高,告别被人吐槽的烂代码... 161 | 162 | ## Mybatis Interceptor底层实现 163 | 164 | 上面说了那么多,框架底层源码是怎么设计并且使用责任链模式的?之前在看 Mybatis 3.4.x 源码时了解到 Interceptor 底层实现就是责任链模式,这里和读者分享 Interceptor 具体实现 165 | 166 | 开门见山,直接把视线聚焦到 Mybatis 源码,版本号 `3.4.7-SNAPSHOT` 167 | 168 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210162908484.png) 169 | 170 | 熟悉么?是不是和我们上面用到的责任链模式差不太多,有处理器集合 interceptors,有添加处理器方法 171 | 172 | Mybatis Interceptor 不仅用到了责任链,还用到了动态代理,服务于 Mybatis 四大 "护教法王",在创建对象时通过动态代理和责任链相结合组装而成插件模块 173 | 174 | 1. ParameterHandler 175 | 2. ResultSetHandler 176 | 3. StatementHandler 177 | 4. Executor 178 | 179 | 使用过 Mybatis 的读者应该知道,查询 SQL 的分页语句就是使用 Interceptor 实现,比如市场上的 PageHelper、Mybatis-Plus 分页插件再或者我们自实现的分页插件(应该没有项目组使用显示调用多条语句组成分页吧) 180 | 181 | 拿查询语句举例,如果定义了多个查询相关的拦截器,会先经过拦截器的代码加工,所有的拦截器执行完毕后才会走真正查询数据库操作 182 | 183 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210210174255039.png) 184 | 185 | 扯的话就扯远了,能够知道如何用、在哪用就可以了。通过 Interceptor 也能知道一点,**想要读框架源码,需要一定的设计模式基础**。如果对责任链、动态代理不清楚,那么就不能理解这一块的精髓 186 | 187 | 188 | 189 | ## 结言 190 | 191 | 文章通过图文并茂的方式帮助大家理解责任链设计模式,在两种类型示例代码以及举例实际业务场景下,相信小伙伴已经掌握了如何在合适的场景使用责任链设计模式 192 | 193 | 看完文章后可以结合 Mybatis、SpringMvc 拦截器更深入掌握责任链模式的应用场景以及使用手法。另外可以结合项目中实际业务场景灵活使用,相信真正使用后的你会对责任链模式产生更深入的了解 194 | 195 | 文章言论部分参考自专栏《设计模式之美:职责链模式》 196 | -------------------------------------------------------------------------------- /docs/design/某厂面试:如何优雅使用SPI机制.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 代码不多,文章可能有点长。朋友面试某厂问到的 SPI 机制,联想到自己项目最近写到的 SPI 场景,文章简要描述下 SPI 机制的发展历程 4 | 5 | ## 产出背景 6 | 7 | 因为最近项目中使用分库分表以及数据加密使用到了 ShardingSphere,所以决定这段时间看看源码实现。问我为什么要读源码?不看源码怎么提高逼格嘞,就是这么朴实无华~ 8 | 9 | 考虑到自己看微信文章的习惯,不喜欢代码太多的,看着逻辑有点不清晰。所以,以后的文章风格就是,**少贴代码,画图 + BB** 10 | 11 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210526224454128.png) 12 | 13 | ## Sharding-Jdbc SPI 14 | 15 | 看源码的历程,往往从点开 Jar 包的瞬间开始。好巧不巧,就看到源代码包下有个 SPI 包,处于好奇心就点了一点,嗯~ 代码果然很熟悉,还是那个配方原来的味道 16 | 17 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210524233941876.png) 18 | 19 | 看了许久,陷入深深的沉思。内心小九九:这玩意好像之前看过,但是在哪我忘了,这到底是个啥? 20 | 21 | 代码还是那个代码,只是它认识我,我不认识它了 22 | 23 | > 这一块的 SPI 接口是 shrding-jdbc 预留自定义加密器的接口 24 | 25 | 看到这里相信就遇到过绝大多数技术同学都会遇到的一个问题,那就是 **认为自己会了**,实际情况呢?不一定。所以,学习一门技术,**一定要多看几遍,尝试去理解记忆**。千万不要看一遍之后,眼高手低认为技术 so easy,然后隔十天半个月就啥都不记的 26 | 27 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/9150e4e5ly1g70ke8a35lj20cu0d9gm1.jpg) 28 | 29 | 继续回过头来说说今天的主角:SPI。首先回答这么一个问题,什么是 SPI 机制 30 | 31 | SPI 全称为 Service Provider Interface,**是一种服务发现机制**。为了被第三方实现或扩展的 API,它可以用于实现框架扩展或组件替换 32 | 33 | SPI 机制本质是将 **接口实现类的全限定名配置在文件中**,并由服务加载器读取配置文件,加载文件中的实现类,**这样运行时可以动态的为接口替换实现类** 34 | 35 | 看文字描述介绍总是枯燥无味且空洞的。简单一点来说,就是你在 `META-INF/services` 下面定义个文件,然后通过一个特殊的类加载器,启动的时候加载你定义文件中的类,这样就能扩展原有框架的功能 36 | 37 | 就这么简单,那可能有读者会问:我不定义在 `META-INF/services` 下面行不行?就想定义在别的地方 38 | 39 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/9150e4e5gy1g6o9410bdwj206o06owf8.jpg) 40 | 41 | 不行滴,**请遏制住这么危险的想法**,人家怎么定义你就怎么实现。这是 JDK 规定好的配置路径,你随便定义,类加载器怎么知道去哪里加载 42 | 43 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210525085841454.png) 44 | 45 | 看到这个 `PREFIX` 常量之后,想法比较活跃的小伙子不知道清醒点了么。简单画张图来描述下 SPI 的运行机制 46 | 47 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210525102811060.png) 48 | 49 | 有点 SPI 基础的同学看到图之后应该又开始自信了,这不就是我之前看过的那玩意么?是的,技术还是那个技术,可以继续往下看看,有没有自己不知道的 50 | 51 | 52 | ## 为什么要有 SPI 53 | 54 | 了解一项技术的前提,一定要知道它为了解决什么样的痛点而存在,JDK 作者也不会没屁事加点代码玩 55 | 56 | 引入了 SPI 机制后,**服务接口与服务实现就会达成分离的状态**,可以实现 **解耦以及程序可扩展机制**。服务提供者(比如 springboot starter)提供出 SPI 接口后,客户端(平常的 springboot 项目)就可以通过本地注册的形式,**将实现类注册到服务端**,轻松实现可插拔 57 | 58 | ### 数据加密举例 59 | 60 | 以实际项目举个例子,就拿 sharding-jdbc 数据加密模块来说,sharding-jdbc 本身支持 AES 和 MD5 两种加密方式。但是,如果客户端不想用内置的两种加密,偏偏想用 RSA 算法呢?难道每加一种算法,sharding-jdbc 就要发个版本么 61 | 62 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/u=4119827549,2634790401&fm=26&fmt=auto&gp=0.jpg) 63 | 64 | sharding-jdbc 可不会这么干,首先提供出 `Encryptor` 加密接口,并引入 SPI 的机制,做到服务接口与服务实现分离的效果。如果客户端想要使用新的加密算法,只需要在客户端项目 `META-INF/services` 目录下定义接口的全限定名称文件,并在文件内写上加密实现类的全限定名,就像这样式的 65 | 66 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210525222904572.png) 67 | 68 | 通过 SPI 的方式,就可以将客户端提供的加密算法加载到 sharding-jdbc 加密规则中,这样就可以在项目运行中选择自定义算法来对数据进行加密存储 69 | 70 | 通过 sharding-jdbc 的例子,可以很好的看出来,上面提到的 SPI 优点,都体现了出来 71 | 72 | 1. 客户端(自己的项目)提供了服务端(sharding-jdbc)的接口自定义实现,但是与服务端状态分离,只有在客户端提供了自定义接口实现时才会加载,其它并没有关联;客户端的新增或删除实现类不会影响服务端 73 | 74 | 2. 如果客户端不想要 RSA 算法,又想要使用内置的 AES 算法,那么可以随时删掉实现类,可扩展性强,插件化架构 75 | 76 | 配合实际案例理解 SPI 是不是很简单。为了防止有些小伙伴没有理解 sharding-jdbc 的例子,这里再举一个真实的例子 77 | 78 | ### 对象存储举例 79 | 80 | 假如你是一家集团公司里做公共架构开发的(可以把这个集团想大一点,几百家子公司的那种 🙃️ ),领导给你安排了个开发任务,**需要你开发一个对象存储服务**,让其它业务线的团队使用,**统一集团内部的对象存储** 81 | 82 | OK,开发诉求明白了,这个时候就该想想怎么去完成这个需求(主要想给领导留个好印象,升官发财 ing...)。首先应该考虑的是要兼容多套对象存储供应商,比如阿里 OSS、腾讯 COS、华为云 OBS,最基本的三连对吧 83 | 84 | 高高兴兴的封装了个 starter,告诉领导封装完成了,然后就下发到各项目组去用了。但是这个时候其中一个子公司负责人告诉你,说他们之前用的七牛云 Kodo 85 | 86 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210526214556720.png) 87 | 88 | 心态炸了呀,难道要给他再适配一个七牛云么?万一适配完这个,又一位大哥说项目自建 HDFS 咋整 89 | 90 | 聊到这,大家就明白了吧,SPI 的场景可不就出现了么。就是身为服务提供者,**在你无法形成绝对规范强制的时候,"放权" 往往是比较明智的选择**,适当让客户端去自定义实现 91 | 92 | 这个时候,回过头想一想最初的一个问题。为什么 sharding-jdbc 不多实现几套算法,而是提供出一个 SPI 接口呢 93 | 94 | 因为开发者明白,不论提供多少接口,**总有个别用户因各方面因素导致的个性化需求**。个性化这个事情是追摸不透的,就像 **女生的心思一样,永远不知道在想什么...**(重点都加黑加粗了,剩下的全靠自己领悟) 95 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/u=871215614,4212505177&fm=26&fmt=auto&gp=0.jpg) 96 | 97 | ## 实战讲解 98 | 99 | 都说到这了,不来个实战,感觉有点说不过去。**吹过的牛逼,负责到底**!就实现上面说的统一对象存储服务的代码 100 | 101 | 最简单的对象存储,只需要两个接口就可以实现功能,分别是 **上传和下载** 102 | 103 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/文件上传-1.png) 104 | 定义好上传、下载接口后,我们就要考虑,**如何让客户端项目可以选择底层的对象存储服务器**,以及如何通过 SPI 的方式将客户端自定义的文件存储组件加载到服务端 105 | 106 | 107 | 我们可以定义个对象存储容器,存放可以使用的对象存储服务,然后再 **使用 SPI 的机制加载客户端自定义组件放到容器**。对象存储服务放到容器中自然需要一个标识,那么就需要给文件接口加一个获取类型接口 108 | 109 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/文件上传-2.png) 110 | 111 | 定义好了接口,就要写具体的代码了。我们为 **对象存储服务提供出一个对外的门面**,所有访问对象存储的服务,必须访问门面对象进行文件的上传下载操作 112 | 113 | 下面这段代码将 **对象服务 bean 存储至容器**,并提供根据客户端的自定义配置,选择合适的对象存储服务 114 | 115 | 代码里用到的关键字 `var` 是 lombok 的注解,可以自动识别对象类型 116 | 117 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/文件上传-3.png) 118 | 119 | 因为是个示例 demo,所以将获取对象存储和具体的上传、下载耦合在了一起,如果小伙伴有类似需求,**一定要将不同行为拆分开,类职责尽量单一些** 120 | 121 | 这段代码整体逻辑不算复杂,所以也有点自信回头,就没跑单元测试,不过问题应该不大。解释一下其中具体逻辑: 122 | 123 | 1. `FileServiceFactory` 大家可以理解为文件服务对外的统一访问入口。实现了 spirng 初始化的一个接口,可以在 bean 初始化时进行代码逻辑操作 124 | 125 | 2. bean 初始化时,通过 `ServiceLoader` 类加载器负责加载对象存储接口,这样就能加载到客户端存放到 `META-INF/services` 中的自定义对象存储实现 126 | 127 | 3. 获取到自定义对象存储后,和服务端本身自带的对象存储一起存放至容器中,这样就可以根据项目中的 `fileStoreType` 获取对应的服务了 128 | 129 | 结合实际的项目场景,一个简简单单的 SPI 应用就完成了,自我感觉比 JDBC 装配的例子更好理解一些 130 | 131 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/u=1861188209,927395044&fm=26&fmt=auto&gp=0.jpg) 132 | 133 | 上面的业务只是为了让不理解 SPI 的小伙伴更好的掌握应用场景,**其实对象存储服务是一种可穷举的业务场景**,SPI 并不是唯一的解决思路。当然,为了省事使用 SPI 也没啥问题。最后提一句,**SPI 最合适的还是没有统一业务实现场景**,就像上面提到过的加密算法 134 | 135 | ## 深入解析 SPI 136 | 137 | 一篇技术解析文章,适当放一些源码解析感觉会更好一些。下面一起来看看 `ServiceLoader` 底层都做了什么事情 138 | 139 | 通过 ServiceLoader 的 load 方法创建一个新的 ServiceLoader,并实例化其中的成员变量 140 | 141 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/SPI-1.png) 142 | 143 | 应用程序通过迭代器接口获取对象实例,这里首先会判断 `providers` 对象中是否有实例对象 144 | 145 | 如果有实例,那么就返回;如果没有,执行类的装载步骤,具体类装载实现如下: 146 | 147 | 1. `LazyIterator#hasNextService` 读取 `META-INF/services` 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析 148 | 149 | 2. `LazyIterator#nextService` 负责实例化 `hasNextService()` 读到的实现类,并将实例化后的对象存放到 `providers` 集合中缓存 150 | 151 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210529104709028.png) 152 | 如果你不知道上面的一些 "黑话" 不要紧,因为都是 `ServiceLoader` 底层执行的方法,跟着下面这个程序敲一遍代码就懂了 153 | 154 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210528233500881.png) 155 | 156 | 这里为了跟源码,也是把上面对象存储的逻辑,简单写了个 SPI 示例,证明是没有问题的。如果小伙伴想真正了解,就需要跟下源码去看看,其它源码部分就不细说了 157 | 158 | ## 结言 159 | 160 | 上面说了很多关于 SPI 机制的优点以及应用场景,这里总结下关键内容 161 | 162 | 1. **SPI 机制优势就是解耦**。将接口的定义以及具体业务实现分离,而不是和业务端全部耦合在一端。可以实现 **运行时根据业务实际场景启用或者替换具体组件** 163 | 164 | 2. SPI 机制的场景就是 **没有统一实现标准的业务场景**。一般就是,服务端有标准的接口,但是没有统一的实现,需要业务方提供其具体实现。比如说 JDBC 的 `java.sql.Driver` 接口和不同云厂商提供的数据库实现包 165 | 166 | 167 | 每个事物都是既有优点,同时也伴随着缺点。要从两个方面去看,不能总盯着一方面。这里说一下 SPI 机制的缺点 168 | 169 | 1. **不能按需加载**。虽然 ServiceLoader 做了延迟加载,但是只能通过遍历的方式全部获取。如果其中某些实现类很耗时,而且你也不需要加载它,那么就形成了资源浪费 170 | 171 | 2. **获取某个实现类的方式不够灵活**,只能通过迭代器的形式获取。这两点可以参考 Dubbo SPI 实现方式进行业务优化 172 | 173 | 文章通过图文并茂的方式帮助大家重新梳理了一遍 **SPI 的场景、优势和缺点**,看完文章后相信大家对 SPI 机制有了更深入的认识 174 | 175 | 梳理出 SPI 的场景以及优势后,**小伙伴最好再去 Debug 源代码**,这样会大家对 SPI 的实现才能更加清楚。**只有对一个知识点真正掌握,才不至于事后很快遗忘** 176 | 177 | 另外可以通过项目中的场景,比如文中提到的加密、对象存储,**通过类比的方式结合项目逻辑去实现代码代入**,这样能够更好的去学习以及扩展相关的设计思路 178 | 179 | **创作不易,文章看到这里如果有所帮助,可以点个关注支持一下,祝好。我们下期见!** 180 | -------------------------------------------------------------------------------- /docs/design/火遍全网的Hutool,如何使用Builder模式创建线程池.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | Builder 设计模式也叫做 **构建者模式或者建造者模式**,名字只是一种叫法,当聊起三种名称的时候知道是怎么回事就行 4 | 5 | Builder 设计模式在作者编码过程中,**属于比较常用的模式之一**。优秀的设计模式总是会受到广大开发者的青睐,Hutool 也是其中之一 6 | 7 | 因为上周编写的业务需要用到线程池,就去 Hutool thread 包下看了看,还真有惊喜,学习到了一种之前编码中没用过的 Builder 模式实现 8 | 9 | 这里必须提一句:**设计模式重要的是思想**,一种设计模式可能不止一种实现方式 10 | 11 | Builder 模式文章大纲如下: 12 | 13 | > 1. Builder 模式应用场景 14 | > 2. Hutool 线程池如何应用 Builder 模式 15 | > 3. Builder 模式不同的实现方式 16 | > 4. Builder 模式总结 17 | 18 | ## Builder 模式应用场景 19 | 20 | Builder 模式作用域:**如果类的属性之间有一定的依赖关系或者约束条件(源自设计模式之美)**,那么就可以考虑使用 Builer 设计模式 21 | 22 | 我们依照线程池来举例,默认创建的线程池,构造方法最多有七个参数,核心线程数、最大线程数、阻塞队列、线程存活时间... 23 | 24 | 日常使用创建线程池时,大家想一下为什么要这么设计?一起来看下源码注释中如何解释此行为 25 | 26 | 27 | 28 | 线程池之所以设置如此之多的构造参数,**是因为对这些参数会有一定规则的校验**,如果不满足线程池的规则,将不允许创建线程池,**通过抛异常的方式终止程序** 29 | 30 | 终止规则大概有七点,这里列举一下: 31 | 32 | 1. 核心线程数不可以小于 0 33 | 2. 线程存活时间不可以小于 0 34 | 3. 最大线程数不可以小于等于 0,同时也不可以小于核心线程数 35 | 4. 阻塞队列、线程工厂、拒绝策略参数均不可为空 36 | 37 | 上述七点有两个作用,其一是为了让核心参数满足线程池运行流程,其二是为了保障运行时的稳定性 38 | 39 | 小伙伴想一哈线程池创建是不是灰常灰常适合 Builder 模式,**构造器函数过多以及属性之间存在依赖关系和约束条件** 40 | 41 | ## Hutool Builder 创建线程池 42 | 43 | Hutool 线程池相关使用 Builder 设计模式有两处,一个是创建线程池,另一个是创建线程工厂,我们重点围绕线程池说 44 | 45 | 创建 Hutool 线程池比较简单且优雅,笔者较喜欢这种链式风格,所以抽象公共业务时都会使用此模式,如图所示 46 | 47 | 48 | 49 | 这个时候跟下源码,先从 `ExecutorBuilder#create` 入手,小伙伴就明白 Hutool 是如何玩 Builder 模式了 50 | 51 | ```java 52 | public static ExecutorBuilder create() { 53 | return new ExecutorBuilder(); 54 | } 55 | ``` 56 | 57 | What? 自己创建自己?这是要搞啥子 58 | 59 | 小伙伴想一下,如果你想要对一个类中属性进行约束,前提是不是先应该把属性搞到手 60 | 61 | 没错,`ExecutorBuilder#create` 方法返回自己本身,然后通过 set 方法 **把数据填充到创建出来的对象上**,最后再进行依赖关系整理和条件约束 62 | 63 | 看一下 `ExecutorBuilder#build` 方法内部做了什么事情 64 | 65 | 66 | 67 | 这里有个知识点,也是B格之一,大家看到 build 方法上有 @Override 注解,证明它是实现了接口方法 68 | 69 | 70 | 71 | Hutool 定义了 Builder 接口,实现此接口即可完成 Builder 模式,泛型 T 代表需要返回的构造对象类型,比如刚才线程池 Builder 泛型就是 ThreadPoolExecutor 72 | 73 | 在实现 build 方法上调用真正管理依赖和约束的方法 build(ExecutorBuilder builder),将刚才创建好并且已经赋过值的构建对象传入 74 | 75 | 最后 build(ExecutorBuilder builder) 返回的就是我们所需要的线程池对象,这一块大家可以自己跟下源码,学会就可以套用自己写的业务代码 76 | 77 | > Hutool Version:5.0.6 78 | > 79 | > 源码包路径:cn.hutool.core.thread 80 | 81 | ## Builder 模式不同的实现方式 82 | 83 | 上文说过,设计模式重思想,就像 Builder 模式,强调的是 **管理依赖关系或者约束条件** 84 | 85 | 刚才 Hutool Builder 只是一种实现方式,之前还用过静态内部类的实现方式 86 | 87 | 代码经过精剪,并且为了阅读体验感,把部分缩进去除了。不过笔者测试过粘贴到 IDEA 中编译是可以的 88 | 89 | ```java 90 | @Getter 91 | public class HttpParameters { 92 | private Builder builder; 93 | public static Builder newBuilder() { return new Builder(); } 94 | private HttpParameters(Builder builder) { this.builder = builder; } 95 | 96 | @Getter 97 | public static class Builder { 98 | private String url; 99 | private Object parameter; 100 | private String httpType; 101 | public Builder parameter(Object parameter) { this.parameter = parameter; return this;} 102 | public Builder url(String url) { this.url = url; return this; } 103 | public Builder httpType(String httpType) { this.httpType = httpType; return this; } 104 | public HttpParameters build() { 105 | if (StringUtils.isBlank(url)) {throw new RuntimeException("URL不允许为空 "); } 106 | // ... 107 | return new HttpParameters(this); 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | 如果后面要获取 HttpParameters 参数就需要先获取 Builder 对象 114 | 115 | 可能有些小伙伴不习惯这种方式,也可以把 Builder 对象属性在 Parameters 里也定义一份,方式都很灵活 116 | 117 | ## 结言 118 | 119 | 本文通过创建线程池为引,讲述了 Builder 设计模式的场景以及实际用途,并引用 Hutool Builder 模式创建线程池进行讲解。相信大家看完之后对 Builder 模式的场景以及应用有了更深入的了解,另外我们可以将 Builder 模式引入到自己代码中,实际操练一下,相信你也会对它 "爱不释手" 120 | 121 | 另外,早之前笔者使用线程池都是自己封装,同时用到了 **Builder、模版方法** 两种模式,**并且重写了部分线程池方法**,使用以及排查问题都比较顺手。因为篇幅有限这里就不贴了,需要的小伙伴可以添加微信自取 122 | 123 | 关于 Builder 设计模式本文就讲到这里,后面会陆续输出策略、工厂、责任链等模式;**如果文章对你有帮助那就点个关注支持下吧,祝好。** 124 | -------------------------------------------------------------------------------- /docs/distributed/彻底掌握分布式事务2PC、3PC模型.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 工作中使用最多的是本地事务,但是在对单一项目拆分为 SOA、微服务之后,就会牵扯出分布式事务场景 4 | 5 | 文章以分布式事务为主线展开说明,并且针对 2PC、3PC 算法进行详细的讲解,最后通过一个 Demo 来更深入掌握分布式事务,文章目录结构如下 6 | 7 | > - 什么是事务 8 | > - 什么是分布式事务 9 | > - DTP 模型和 XA 规范 10 | > - 什么是 DTP 模型 11 | > - 什么是 XA 规范 12 | > - 2PC 一致性算法 13 | > - 2PC-准备阶段 14 | > - 2PC-提交阶段 15 | > - 2PC 算法优缺点 16 | > - 3PC 一致性算法 17 | > - JDBC 操作 MySQL XA 事务 18 | > - 结言 19 | 20 | ## 什么是事务 21 | 22 | 事务是`数据库操作的最小工作单元`,一组不可再分割的操作集合,是作为单个逻辑工作单元执行的一系列操作。这些操作作为一个整体一起向系统提交,`要么都执行、要么都不执行` 23 | 24 | 事务具有四个特征,分别是`原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)`,简称为事务的 ACID 特性 25 | 26 | **如何保证事务的 ACID 特性?** 27 | 28 | - 原子性(Atomicity):事务内 SQL 要么同时成功要么同时失败,基于撤销日志(undo 日志)实现 29 | 30 | - 一致性(Consistency):系统从一个正确态转移到另一个正确态,由应用通过 AID 来保证,可以说是事务的核心特性 31 | - 隔离性(Isolation):控制事务并发执行时数据的可见性,基于锁和多版本并发控制(mvcc)实现 32 | - 持久性(Durability):提交后一定存储成功不会丢失,基于重做日志(redo log)实现 33 | 34 | 文章主要是介绍分布式事务 `2PC 和 3PC`,关于 `redo、undo 日志、mvcc、锁`这块的内容后续再详细介绍 35 | 36 | 在早些时候,我们应用程序还是单体项目,所以操作的都是单一数据库,这种情况下我们称之为`本地事务`。本地事务的 ACID 一般都是由数据库层面支持的,比如我们工作中常用的 MySQL 数据库 37 | 38 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210304162720353.png) 39 | 40 | 平常我们在操作 MySQL 客户端时,MySQL 会隐式对事务做自动提交,所以日常工作不会涉及手动书写事务的创建、提交、回滚等操作。如果想要试验锁、MVCC等特性,可以创建多个会话,通过`begin、commit、rollback`等命令来试验下不同事务之间的数据,看执行结果和自己所想是否一致 41 | 42 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210304173646000.png) 43 | 44 | 我们平常开发项目代码时使用的是 Spring 封装好的事务,所以也不会手动编写对数据库事务的提交、回滚等方法(个别情况除外)。这里使用原生 JDBC 写一个示例代码,帮助大家理解如何通过事务保证 ACID 四大特性 45 | 46 | ```java 47 | Connection conn = ...; // 获取数据库连接 48 | conn.setAutoCommit(false); // 开启事务 49 | try { 50 | // ...执行增删改查sql 51 | conn.commit(); // 提交事务 52 | } catch (Exception e) { 53 | conn.rollback(); // 事务回滚 54 | } finally { 55 | conn.close(); // 关闭链接 56 | } 57 | ``` 58 | 59 | 设想一下,每次进行数据库操作,都要写重复的创建事务、提交、回滚等方法是不是挺痛苦的,那 Spring 如何自动帮助我们管理事务的呢?Spring 项目中我们一般使用两种方式来进行事务的管理,`编程式事务和声明式事务` 60 | 61 | 项目中使用 Spring 管理事务,要么在接口方法上添加注解 `@Transactional`,要么使用 AOP 配置切面事务。其实这两种方式大同小异,只不过 `@Transactional` 的粒度更细一些,实现原理上都是依赖 AOP,举例说明下 62 | 63 | ```java 64 | @Service 65 | public class TransactionalService { 66 | @Transactional 67 | public void save() { 68 | // 业务操作 69 | } 70 | } 71 | ``` 72 | 73 | `TransactionalService` 会被 Spring 创建一个代理对象放入到容器中,创建后的代理对象相当于下述类 74 | 75 | ```java 76 | public class TransactionalServiceProxy { 77 | private TransactionalService transactionalService; 78 | public TransactionalServiceProxy(TransactionalService transactionalService) { 79 | this.transactionalService = transactionalService; 80 | } 81 | 82 | public void save() { 83 | try { 84 | // 开启事务操作 85 | transactionalService.save(); 86 | } catch (Exception e) { 87 | // 出现异常则进行回滚 88 | } 89 | // 提交事务 90 | } 91 | } 92 | ``` 93 | 94 | 示例代码看着简洁明了,但是真正的代码生成代码对比要复杂很多。关于事务管理器,Spring 提供了接口 `PlatformTransactionManager`,其内部包含两个重要实现类 95 | 96 | - `DataSourceTransactionManager`:支持本地事务,内部通过`java.sql.Connection`来开启、提交和回滚事务 97 | 98 | - `JtaTransactionManager`:用于支持分布式事务,其实现了 JTA 规范,使用 XA 协议进行两阶段提交 99 | 100 | 通过这两个实现类得知,平常我们使用的`编程式事务和声明式事务`依赖于本地事务管理实现,`Spring 同时也支持分布式事务`,关于 JTA 分布式事务的支持网上资料挺多的,就不在这里赘述了 101 | 102 | 103 | ## 什么是分布式事务 104 | 105 | 日常业务代码中的本地事务我们一直都在用,理解起来并不困难。但是随着`服务化(SOA)、微服务`的流行,平常我们的单一业务系统被拆分成为了多个系统,为了迎合业务系统的变更,数据库也结合业务进行了拆分 106 | 107 | 比如以学校管理系统举例说明,可能就会拆分为学生服务、课程服务、老师服务等,数据库也拆分为多个库。当这种情况,把不同的服务部署到服务器,就会有可能面临下述的服务调用 108 | 109 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210305105346584.png) 110 | 111 | ServiceA 服务需要操作数据库执行本地事务,同时需要调用 ServiceB 和 ServiceC 服务发起事务调用,`如何保证三个服务的事务要么一起成功或者一起失败`,如何保证用户发起请求的事务 ACID 特性呢?无疑这就是分布式事务场景,`三个服务的单一本地事务都无法保证整个请求的事务` 112 | 113 | 分布式事务场景有很多种解决方案,以不同分类来看,`强一致性解决方案、最终一致性解决方案`,细分其中的方案包括`2PC、3PC、TCC、可靠消息...` 114 | 115 | 业界中使用较多的像阿里的 `RocketMQ 事务消息`、`Seata XA模式`、`可靠消息模型`这几种解决方案。不过,分布式事务无一例外都是会直接或间接操作多个数据库,`而且使用了分布式事务同时也会带来新的挑战,那就是性能问题`。如果为了保证强一致性分布式事务亦或者补偿方案的最终一致性,导致了性能的下降,对于正常业务而言,无疑是得不偿失的 116 | 117 | ## DTP 模型和 XA 规范 118 | 119 | X/Open 组织定义了分布式事务的模型(DTP)和 分布式事务协议(XA),DTP 由以下几个模型元素组成 120 | 121 | - `AP(Application 应用程序)`:用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作 122 | - `TM(Transaction Manager 事务管理器)`:负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等 123 | - `RM(Resource Manager 资源管理器)`:如数据库、文件系统等,并提供访问资源的方式 124 | - CRM(Communication Resource Manager 通信资源管理器):控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信 125 | - CP(Communication Protocol 通信协议):提供CRM提供的分布式应用节点之间的底层通信服务 126 | 127 | 在 DTP 分布式事务模型中,基本组成需要涵盖 **AP、TM、RMS**(不需要 CRM、CP 也是可以的),如下图所示 128 | 129 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306135942749.png) 130 | 131 | ### XA 规范 132 | 133 | XA 规范最重要的作用就是定义 RM(资源管理器)与 TM(事务管理器)之间的交互接口。另外,XA 规范除了定义 2PC 之间的交互接口外,同时对 2PC 进行了优化 134 | 135 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306135753269.png) 136 | 137 | **梳理下 DTP、XA、2PC 之间的关系** 138 | 139 | `DTP 规定了分布式事务中的角色模型`,并在其中指定了全局事务的控制需要使用 2PC 协议来保证数据的一致性 140 | 141 | 2PC 是 Two-Phase Commit 的缩写,即二阶段提交,是计算机网络尤其是数据库领域内,为了保证分布式系统架构下所有节点在进行事务处理过程中能够`保证原子性和一致性`而设计的一种算法。同时,2PC 也被认为是一种一致性协议,`用来保证分布式系统数据的一致性` 142 | 143 | XA 规范是 X/Open 组织提出的分布式事务处理规范,`XA 规范定义了 2PC(两阶段提交协议)中需要用到的接口`,也就是上图中 RM 和 TM 之间的交互。2PC 和 XA 两者最容易混淆,可以这么理解,DTP 模型定义 TM 和 RM 之间通讯的接口规范叫 XA,然后 **关系数据库(比如MySQL)基于 X/Open 提出的的 XA 规范(核心依赖于 2PC 算法)被称为 XA 方案** 144 | 145 | ## 2PC 一致性算法 146 | 147 | 当应用程序(AP)发起一个事务操作需要`跨越多个分布式节点`的时候,每一个`分布式节点(RM)`知道自己进行事务操作的结果是成功或是失败,但是却`不能获取到其它分布式节点的操作结果`。为了保证事务处理的 ACID 特性,就需要引入称为`"协调者"的组件(TM)`来进行统一调度分布式的执行逻辑 148 | 149 | `协调者负责调度参与整体事务的分布式节点的行为`,并最终决定这些分布式节点要把事务进行提交还是回滚。所以,基于这种思想下,衍生出了`二阶段提交和三阶段提交`两种分布式一致性算法协议。二阶段指的是**准备阶段和提交阶段**,下面我们先看准备阶段都做了什么事情 150 | 151 | ### 2PC-准备阶段 152 | 153 | 二阶段提交中第一阶段也叫做`"投票阶段"`,即各参与者投票表明自身是否继续执行接下来的事务提交步骤 154 | 155 | - **事务询问**:协调者向所有参与本次分布式事务的参与者发送事务内容,询问是否可以执行事务提交操作,然后开始等待各个参与者的响应 156 | 157 | - **执行事务**:参与者收到协调者的事务请求,执行对应的事务,并将内容写入 Undo 和 Redo 日志 158 | 159 | - **返回响应**:如果各个参与者执行了事务,那么反馈协调者 Yes 响应;如果各个参与者没有能够成功执行事务,那么就会返回协调者 No 响应 160 | 161 | 如果第一阶段全部参与者返回成功响应,那么进入事务提交步骤,反之本次分布式事务以失败返回。以 MySQL 数据库为例,在第一阶段,事务管理器(TM)向所有涉及到的数据库(RM)发出 **prepare(准备提交)** 请求,数据库收到请求后执行数据修改和日志记录处理,处理完成后把事务的状态修改为 "可提交",最终将结果返回给事务处理器 162 | 163 | ### 2PC-提交阶段 164 | 165 | 提交阶段分为两个流程,一个是各参与者正常执行事务提交流程,并返回 Ack 响应,表示各参与者投票成功;一个是各参与者当中有执行失败返回 No 响应或超时情况,将触发全局回滚,表示分布式事务执行失败 166 | 167 | - 执行事务提交 168 | 169 | - 中断事务 170 | 171 | #### 执行事务提交 172 | 173 | 假设协调者从所有的参与者获得的反馈都是 Yes 响应,那么就会执行事务提交操作 174 | 175 | - 事务提交:协调者向所有参与者节点发出 Commit 请求,各个参与者接收到 Commit 请求后,将本地事务进行提交操作,并在完成提交之后释放事务执行周期内占用的事务资源 176 | 177 | - 完成事务:各个参与者完成事务提交之后,向协调者发送 Ack 响应,协调者接收到响应后完成本次分布式事务 178 | 179 | 180 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306164157428.png) 181 | 182 | #### 中断事务 183 | 184 | 假设任意一个事务参与者节点向协调者反馈了 No 响应(**注意这里的 No 响应指的是第一阶段**),或者在等待超时之后,协调者没有接到所有参与者的反馈响应,那么就会进行事务中断流程 185 | 186 | - 事务回滚:协调者向所有参与者发出 Rollback 请求,参与者接收到回滚请求后,使用第一阶段写入的 undo log 执行事务的回滚,并在完成回滚事务之后释放占用的资源 187 | 188 | - 中断事务:参与者在完成事务回滚之后,向协调者发送 Ack 消息,协调者接收到事务参与者的 Ack 消息之后,完成事务中断 189 | 190 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306164501746.png) 191 | 192 | ### 2PC 优缺点 193 | 194 | 2PC 提交将事务的处理过程分为了投票和执行两个阶段,核心思想就是对每个事务都采用先尝试后提交的方式处理。2PC 优点显而易见,那就是 **原理简单,实现方便**。简单也意味着很多地方不能尽善尽美,这里梳理三个比较核心的缺陷 195 | 196 | 1. 同步阻塞:无论是在第一阶段的过程中,还是在第二阶段,所有的`参与者资源和协调者资源都是被锁住的`,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。`这样的过程会比较漫长,对性能影响比较大` 197 | 198 | 2. 单点故障:`如果协调者出现问题,那么整个二阶段提交流程将无法运转`。另外,如果协调者是在第二阶段出现了故障,那么其它参与者将会处于锁定事务资源的状态中 199 | 200 | 3. 数据不一致性:当协调者在第二阶段向所有参与者发送 Commit 请求后,发生了`局部网络异常或者协调者在尚未发送完 Commit 请求之前自身发生了崩溃`,导致只有部分参与者接收到 Commit 请求,那么`接收到的参与者就会进行提交事务`,进而形成了数据不一致性 201 | 202 | 由于 2PC 的简单方便,所以会产生上述的同步阻塞、单点故障、数据不一致等情况,所以在 2PC 的基础上做了改进,推行出了三阶段提交(3PC) 203 | 204 | > 使用 2PC 存在诸多限制,首先就是数据库需要支持 XA 规范,而且性能与数据一致性数据均不友好,所以 Seata 中虽然支持 XA 模式,但是主推的还是 AT 模式 205 | 206 | ## 3PC 一致性算法 207 | 208 | 三阶段提交(3PC)是二阶段提交(2PC)的一个改良版本,引入了两个新的特性 209 | 210 | 1. `协调者和参与者均引入超时机制`,通过超时机制来解决 2PC 的同步阻塞问题,避免事务资源被永久锁定 211 | 212 | 2. 把二阶段演变为三阶段,`二阶段提交协议中的第一阶段"准备阶段"一分为二`,形成了新的 CanCommit、PreCommit、do Commit 三个阶段组成事务处理协议 213 | 214 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306182315718.png) 215 | 216 | 这里将不再赘述 3PC 的详细提交过程,3PC 相比较于 2PC 最大的优点就是`降低了参与者的阻塞范围`,并且能够在协调者出现单点故障后继续达成一致 217 | 218 | 虽然通过超时机制解决了资源永久阻塞的问题,但是 3PC 依然存在数据不一致的问题。`当参与者接收到 PreCommit 消息后`,如果网络出现分区,此时协调者与参与者无法进行正常通信,这种情况下,`参与者依然会进行事务的提交` 219 | 220 | 通过了解 2PC 和 3PC 之后,我们可以知道这两者都无法彻底解决分布式下的数据一致性 221 | 222 | ## JDBC 操作 MySQL XA 事务 223 | 224 | MySQL 从 5.0.3 开始支持 XA 分布式事务,且只有 InnoDB 存储引擎支持。MySQL Connector/J 从5.0.0 版本之后开始直接提供对 XA 的支持 225 | 226 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210306183250966.png) 227 | 228 | 在 DTP 模型中,MySQL 属于 RM 资源管理器,所以这里就不再演示 MySQL 支持 XA 事务的语句,因为它执行的只是自己单一事务分支,我们通过 `JDBC 来演示如何通过 TM 来控制多个 RM 完成 2PC 分布式事务` 229 | 230 | 这里先来说明需要引入 GAV 的 Maven 版本,因为高版本 8.x 移除了对 XA 分布式事务的支持(*可能也是觉得没人会用吧*) 231 | 232 | ```xml 233 | 234 | 235 | 236 | mysql 237 | mysql-connector-java 238 | 5.1.38 239 | 240 | 241 | ``` 242 | 243 | 这里为了保证在公众号阅读的舒适性,通过 IDEA 将多行代码合并为一行了,如果小伙伴需要粘贴到 IDEA 中,格式化一下就好了 244 | 245 | 因为 XA 协议的基础是 2PC 一致性算法,所以小伙伴在看代码时可以对照上面文章讲的 DTP 模型和 2PC 来进行理解以及模拟错误和执行结果 246 | 247 | ```java 248 | import com.mysql.jdbc.jdbc2.optional.MysqlXAConnection;import com.mysql.jdbc.jdbc2.optional.MysqlXid;import javax.sql.XAConnection;import javax.transaction.xa.XAException;import javax.transaction.xa.XAResource;import javax.transaction.xa.Xid;import java.sql.*; 249 | 250 | public class MysqlXAConnectionTest { 251 | public static void main(String[] args) throws SQLException { 252 | // true 表示打印 XA 语句, 用于调试 253 | boolean logXaCommands = true; 254 | // 获得资源管理器操作接口实例 RM1 255 | Connection conn1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn1, logXaCommands);XAResource rm1 = xaConn1.getXAResource(); 256 | // 获得资源管理器操作接口实例 RM2 257 | Connection conn2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn2, logXaCommands);XAResource rm2 = xaConn2.getXAResource(); 258 | // AP(应用程序)请求 TM(事务管理器) 执行一个分布式事务, TM 生成全局事务 ID 259 | byte[] gtrid = "distributed_transaction_id_1".getBytes();int formatId = 1; 260 | try { 261 | // ============== 分别执行 RM1 和 RM2 上的事务分支 ==================== 262 | // TM 生成 RM1 上的事务分支 ID 263 | byte[] bqual1 = "transaction_001".getBytes();Xid xid1 = new MysqlXid(gtrid, bqual1, formatId); 264 | // 执行 RM1 上的事务分支 265 | rm1.start(xid1, XAResource.TMNOFLAGS);PreparedStatement ps1 = conn1.prepareStatement("INSERT into user(name) VALUES ('jack')");ps1.execute();rm1.end(xid1, XAResource.TMSUCCESS); 266 | // TM 生成 RM2 上的事务分支 ID 267 | byte[] bqual2 = "transaction_002".getBytes();Xid xid2 = new MysqlXid(gtrid, bqual2, formatId); 268 | // 执行 RM2 上的事务分支 269 | rm2.start(xid2, XAResource.TMNOFLAGS);PreparedStatement ps2 = conn2.prepareStatement("INSERT into user(name) VALUES ('rose')");ps2.execute();rm2.end(xid2, XAResource.TMSUCCESS); 270 | // =================== 两阶段提交 ================================ 271 | // phase1: 询问所有的RM 准备提交事务分支 272 | int rm1_prepare = rm1.prepare(xid1);int rm2_prepare = rm2.prepare(xid2); 273 | // phase2: 提交所有事务分支 274 | if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) { 275 | // 所有事务分支都 prepare 成功, 提交所有事务分支 276 | rm1.commit(xid1, false);rm2.commit(xid2, false); 277 | } else { 278 | // 如果有事务分支没有成功, 则回滚 279 | rm1.rollback(xid1);rm1.rollback(xid2); 280 | } 281 | } catch (XAException e) { e.printStackTrace(); } }} 282 | ``` 283 | 284 | ## 结言 285 | 286 | 本文通过图文并茂的方式讲解了如何保证本地事务的四大特性,分布式事务的产出背景,以及 2PC、3PC 为何不能解决分布式情况下的数据一致性,最后通过 JDBC 演示了 2PC 的执行流程。相信大家看过后也对分布式事务有了较深的印象,同时对 DTP、XA、2PC 这几种比较容易混淆的概念有了清楚的认识 287 | 288 | 这是《分布式事务》专栏的第一章开篇,后面陆续完成通过消息中间件、可靠消息模型、Seata XA模型完成分布式事务的文章,并对不同的实现方式进行总结利弊,挑选出合适场景使用不同的分布式事务解决方案 289 | 290 | 作者认为最好的学习方式那就是实战,如果没有接触过分布式事务的小伙伴,可以通过自己正在写的项目,模拟出分布式事务的业务场景,加深印象的同时也能够更好理解分布式事务解决方案相关设计思路 291 | 292 | **创作不易,文章看到这里如果有所帮助,可以点个关注支持一下,祝好。我们下期见** 293 | 294 |
295 | 296 | **站在巨人的肩膀** 297 | 298 | - 《从Paxos到Zookeeper分布式一致性原理与实践》 299 | 300 | - 《田守枝Java技术博客》 301 | -------------------------------------------------------------------------------- /docs/jvm/如果线上遇到了OOM,该如何解决?.md: -------------------------------------------------------------------------------- 1 | OOM 意味着程序存在着漏洞,可能是代码或者 JVM 参数配置引起的。这篇文章和读者聊聊,Java 进程触发了 OOM 后如何排查 2 | 3 | 常说对生产环境保持敬畏之心,快速解决问题也是一种敬畏的表现 4 | 5 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/03cdefc08f92ce634f971a414e26f52a.png) 6 | 7 | ## 为什么会 OOM 8 | 9 | OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误 10 | 11 | 为什么会出现 OOM,一般由这些问题引起 12 | 13 | 1. 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理 14 | 2. 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽 15 | 16 | **内存泄漏**:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用 17 | 18 | **内存溢出**:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出 19 | 20 | 内存泄漏持续存在,最后一定会溢出,两者是因果关系 21 | 22 | 23 | 24 | ## 常见的 OOM 25 | 26 | 比较常见的 OOM 类型有以下几种 27 | 28 | **java.lang.OutOfMemoryError: PermGen space** 29 | 30 | Java7 永久代(方法区)溢出,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。每当一个类初次加载的时候,元数据都会存放到永久代 31 | 32 | 一般出现于大量 Class 对象或者 JSP 页面,或者采用 CgLib 动态代理技术导致 33 | 34 | 我们可以通过 `-XX:PermSize` 和 `-XX:MaxPermSize` 修改方法区大小 35 | 36 | > Java8 将永久代变更为元空间,报错:java.lang.OutOfMemoryError: Metadata space,元空间内存不足默认进行动态扩展 37 | 38 | 39 | 40 | **java.lang.StackOverflowError** 41 | 42 | **虚拟机栈溢出**,一般是由于程序中存在 **死循环或者深度递归调用** 造成的。如果栈大小设置过小也会出现溢出,可以通过 `-Xss` 设置栈的大小 43 | 44 | 虚拟机抛出栈溢出错误,可以在日志中定位到错误的类、方法 45 | 46 | **java.lang.OutOfMemoryError: Java heap space** 47 | 48 | **Java 堆内存溢出**,溢出的原因一般由于 JVM 堆内存设置不合理或者内存泄漏导致 49 | 50 | 如果是内存泄漏,可以通过工具查看泄漏对象到 GC Roots 的引用链。掌握了泄漏对象的类型信息以及 GC Roots 引用链信息,就可以精准地定位出泄漏代码的位置 51 | 52 | 如果不存在内存泄漏,就是内存中的对象确实都还必须存活着,那就应该检查虚拟机的堆参数(-Xmx 与 -Xms),查看是否可以将虚拟机的内存调大些 53 | 54 | 小结:方法区和虚拟机栈的溢出场景不在本篇过多讨论,下面主要讲解常见的 Java 堆空间的 OOM 排查思路 55 | 56 | 57 | 58 | ## 查看 JVM 内存分布 59 | 60 | 假设我们 Java 应用 PID 为 15162,输入命令查看 JVM 内存分布 `jmap -heap 15162` 61 | 62 | ```java 63 | [xxx@xxx ~]# jmap -heap 15162 64 | Attaching to process ID 15162, please wait... 65 | Debugger attached successfully. 66 | Server compiler detected. 67 | JVM version is 25.161-b12 68 | 69 | using thread-local object allocation. 70 | Mark Sweep Compact GC 71 | 72 | Heap Configuration: 73 | MinHeapFreeRatio = 40 # 最小堆使用比例 74 | MaxHeapFreeRatio = 70 # 最大堆可用比例 75 | MaxHeapSize = 482344960 (460.0MB) # 最大堆空间大小 76 | NewSize = 10485760 (10.0MB) # 新生代分配大小 77 | MaxNewSize = 160759808 (153.3125MB) # 最大新生代可分配大小 78 | OldSize = 20971520 (20.0MB) # 老年代大小 79 | NewRatio = 2 # 新生代比例 80 | SurvivorRatio = 8 # 新生代与 Survivor 比例 81 | MetaspaceSize = 21807104 (20.796875MB) # 元空间大小 82 | CompressedClassSpaceSize = 1073741824 (1024.0MB) # Compressed Class Space 空间大小限制 83 | MaxMetaspaceSize = 17592186044415 MB # 最大元空间大小 84 | G1HeapRegionSize = 0 (0.0MB) # G1 单个 Region 大小 85 | 86 | Heap Usage: # 堆使用情况 87 | New Generation (Eden + 1 Survivor Space): # 新生代 88 | capacity = 9502720 (9.0625MB) # 新生代总容量 89 | used = 4995320 (4.763908386230469MB) # 新生代已使用 90 | free = 4507400 (4.298591613769531MB) # 新生代剩余容量 91 | 52.56726495150862% used # 新生代使用占比 92 | Eden Space: 93 | capacity = 8454144 (8.0625MB) # Eden 区总容量 94 | used = 4029752 (3.8430709838867188MB) # Eden 区已使用 95 | free = 4424392 (4.219429016113281MB) # Eden 区剩余容量 96 | 47.665996699370154% used # Eden 区使用占比 97 | From Space: # 其中一个 Survivor 区的内存分布 98 | capacity = 1048576 (1.0MB) 99 | used = 965568 (0.92083740234375MB) 100 | free = 83008 (0.07916259765625MB) 101 | 92.083740234375% used 102 | To Space: # 另一个 Survivor 区的内存分布 103 | capacity = 1048576 (1.0MB) 104 | used = 0 (0.0MB) 105 | free = 1048576 (1.0MB) 106 | 0.0% used 107 | tenured generation: # 老年代 108 | capacity = 20971520 (20.0MB) 109 | used = 10611384 (10.119804382324219MB) 110 | free = 10360136 (9.880195617675781MB) 111 | 50.599021911621094% used 112 | 113 | 10730 interned Strings occupying 906232 bytes. 114 | ``` 115 | 116 | 通过查看 JVM 内存分配以及运行时使用情况,可以判断内存分配是否合理 117 | 118 | 另外,可以在 JVM 运行时查看最耗费资源的对象,`jmap -histo:live 15162 | more` 119 | 120 | JVM 内存对象列表按照对象所占内存大小排序 121 | 122 | - instances:实例数 123 | - bytes:单位 byte 124 | - class name:类名 125 | 126 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210921195500700.png) 127 | 128 | 明显看到 `CustomObjTest` 对象实例以及占用内存过多 129 | 130 | 可惜的是,方案存在局限性,因为它只能排查对象占用内存过高问题 131 | 132 | 其中 "[" 代表数组,例如 "[C" 代表 Char 数组,"[B" 代表 Byte 数组。如果数组内存占用过多,我们不知道哪些对象持有它,所以就需要 Dump 内存进行离线分析 133 | 134 | > `jmap -histo:live` 执行此命令,JVM 会先触发 GC,再统计信息 135 | 136 | ## Dump 文件分析 137 | 138 | Dump 文件是 Java 进程的内存镜像,其中主要包括 **系统信息**、**虚拟机属性**、**完整的线程 Dump**、**所有类和对象的状态** 等信息 139 | 140 | 当程序发生内存溢出或 GC 异常情况时,怀疑 JVM 发生了 **内存泄漏**,这时我们就可以导出 Dump 文件分析 141 | 142 | JVM 启动参数配置添加以下参数 143 | 144 | - -XX:+HeapDumpOnOutOfMemoryError 145 | - -XX:HeapDumpPath=./(参数为 Dump 文件生成路径) 146 | 147 | > 当 JVM 发生 OOM 异常自动导出 Dump 文件,文件名称默认格式:`java_pid{pid}.hprof` 148 | 149 | 上面配置是在应用抛出 OOM 后自动导出 Dump,或者可以在 JVM 运行时导出 Dump 文件 150 | 151 | ```shell 152 | jmap -dump:file=[文件路径] [pid] 153 | 154 | # 示例 155 | jmap -dump:file=./jvmdump.hprof 15162 156 | ``` 157 | 158 | 在本地写一个测试代码,验证下 OOM 以及分析 Dump 文件 159 | 160 | ```java 161 | 设置 VM 参数:-Xms3m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ 162 | 163 | public static void main(String[] args) { 164 | List oomList = Lists.newArrayList(); 165 | // 无限循环创建对象 166 | while (true) { 167 | oomList.add(new Object()); 168 | } 169 | } 170 | ``` 171 | 172 | 通过报错信息得知,`java heap space` 表示 OOM 发生在堆区,并生成了 hprof 二进制文件在当前文件夹下 173 | 174 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210922172300122.png) 175 | 176 | **JvisualVM 分析** 177 | 178 | Dump 分析工具有很多,相对而言 **JvisualVM**、**JProfiler**、**Eclipse Mat**,使用人群更多一些。下面以 JvisualVM 举例分析 Dump 文件 179 | 180 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210922175322692.png) 181 | 182 | 183 | 184 | 列举两个常用的功能,第一个是能看到触发 OOM 的线程堆栈,清晰得知程序溢出的原因 185 | 186 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210924084323314.png) 187 | 188 | 189 | 190 | 第二个就是可以查看 JVM 内存里保留大小最大的对象,可以自由选择排查个数 191 | 192 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210924084605594.png) 193 | 194 | 点击对象还可以跳转具体的对象引用详情页面 195 | 196 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210924084719190.png) 197 | 198 | 文中 Dump 文件较为简单,而正式环境出错的原因五花八门,所以不对该 Dump 文件做深度解析 199 | 200 | **注意**:JvisualVM 如果分析大 Dump 文件,可能会因为内存不足打不开,需要调整默认的内存 201 | 202 | ## 总结回顾 203 | 204 | 线上如遇到 JVM 内存溢出,可以分以下几步排查 205 | 206 | 1. `jmap -heap` 查看是否内存分配过小 207 | 2. `jmap -histo` 查看是否有明显的对象分配过多且没有释放情况 208 | 3. `jmap -dump` 导出 JVM 当前内存快照,使用 JDK 自带或 MAT 等工具分析快照 209 | 210 | 如果上面还不能定位问题,那么需要排查应用是否在不断创建资源,比如网络连接或者线程,都可能会导致系统资源耗尽 211 | -------------------------------------------------------------------------------- /docs/scene/MySQL单表千万数据量如何深分页优化.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 优化项目代码过程中发现一个千万级数据深分页问题,缘由是这样的 4 | 5 | 库里有一张耗材 MCS_PROD 表,~~通过同步外部数据中台多维度数据,在系统内部组装为单一耗材产品~~,最终同步到 ES 搜索引擎 6 | 7 | MySQL 同步 ES 流程如下: 8 | 9 | 1. 通过定时任务的形式触发同步,比如间隔半天或一天的时间频率 10 | 2. 同步的形式为增量同步,根据更新时间的机制,比如第一次同步查询 >= 1970-01-01 00:00:00.0 11 | 3. 记录最大的更新时间进行存储,下次更新同步以此为条件 12 | 4. 以分页的形式获取数据,当前页数量加一,循环到最后一页 13 | 14 | 在这里问题也就出现了,**MySQL 查询分页 OFFSET 越深入,性能越差**,初步估计线上 MCS_PROD 表中记录在 1000w 左右 15 | 16 | 如果按照每页 10 条,OFFSET 值会拖垮查询性能,进而形成一个 **"性能深渊"** 17 | 18 | 同步类代码针对此问题有两种优化方式: 19 | 20 | 1. **采用游标、流式方案进行优化** 21 | 2. **优化深分页性能,文章围绕这个题目展开** 22 | 23 | > 文章目录如下: 24 | > 25 | > - 软硬件说明 26 | > - 重新认识 MySQL 分页 27 | > - 深分页优化 28 | > - 子查询优化 29 | > - 延迟关联 30 | > - 书签记录 31 | > - ORDER BY 巨坑,慎踩 32 | > - ORDER BY 索引失效举例 33 | > - 结言 34 | 35 | ## 软硬件说明 36 | 37 | **MySQL VERSION** 38 | 39 | ```sql 40 | mysql> select version(); 41 | +-----------+ 42 | | version() | 43 | +-----------+ 44 | | 5.7.30 | 45 | +-----------+ 46 | 1 row in set (0.01 sec) 47 | ``` 48 | 49 | **表结构说明** 50 | 51 | 借鉴公司表结构,字段、长度以及名称均已删减 52 | 53 | ```sql 54 | mysql> DESC MCS_PROD; 55 | +-----------------------+--------------+------+-----+---------+----------------+ 56 | | Field | Type | Null | Key | Default | Extra | 57 | +-----------------------+--------------+------+-----+---------+----------------+ 58 | | MCS_PROD_ID | int(11) | NO | PRI | NULL | auto_increment | 59 | | MCS_CODE | varchar(100) | YES | | | | 60 | | MCS_NAME | varchar(500) | YES | | | | 61 | | UPDT_TIME | datetime | NO | MUL | NULL | | 62 | +-----------------------+--------------+------+-----+---------+----------------+ 63 | 4 rows in set (0.01 sec) 64 | ``` 65 | 66 | 通过测试同学帮忙造了 500w 左右数据量 67 | 68 | ```sql 69 | mysql> SELECT COUNT(*) FROM MCS_PROD; 70 | +----------+ 71 | | count(*) | 72 | +----------+ 73 | | 5100000 | 74 | +----------+ 75 | 1 row in set (1.43 sec) 76 | ``` 77 | 78 | **SQL 语句如下** 79 | 80 | 因为功能需要满足 **增量拉取的方式**,所以会有数据更新时间的条件查询,以及相关 **查询排序(此处有坑)** 81 | 82 | ```sql 83 | SELECT 84 | MCS_PROD_ID, 85 | MCS_CODE, 86 | MCS_NAME, 87 | UPDT_TIME 88 | FROM 89 | MCS_PROD 90 | WHERE 91 | UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY UPDT_TIME 92 | LIMIT xx, xx 93 | ``` 94 | 95 | ## 重新认识 MySQL 分页 96 | 97 | LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接收一个或两个数字参数,参数必须是一个整数常量 98 | 99 | 如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数 100 | 101 | 举个简单的例子,分析下 SQL 查询过程,掌握深分页性能为什么差 102 | 103 | ```sql 104 | mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1; 105 | +-------------+-------------------------+------------------+---------------------+ 106 | | MCS_PROD_ID | MCS_CODE | MCS_NAME | UPDT_TIME | 107 | +-------------+-------------------------+------------------+---------------------+ 108 | | 181789 | XA601709733186213015031 | 尺、桡骨LC-DCP骨板 | 2020-10-19 16:22:19 | 109 | +-------------+-------------------------+------------------+---------------------+ 110 | 1 row in set (3.66 sec) 111 | 112 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1; 113 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 114 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 115 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 116 | | 1 | SIMPLE | MCS_PROD | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using index condition | 117 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 118 | 1 row in set, 1 warning (0.01 sec) 119 | ``` 120 | 121 | 简单说明下上面 SQL 执行过程: 122 | 123 | 1. 首先查询了表 MCS_PROD,进行过滤 UPDT_TIME 条件,查询出展示列(涉及回表操作)进行排序以及 LIMIT 124 | 2. LIMIT 100000, 1 的意思是扫描满足条件的 100001 行,**然后扔掉前 100000 行** 125 | 126 | MySQL 耗费了 **大量随机 I/O 在回表查询聚簇索引的数据上**,而这 100000 次随机 I/O 查询数据不会出现在结果集中 127 | 128 | 如果系统并发量稍微高一点,每次查询扫描超过 100000 行,性能肯定堪忧,另外 **LIMIT 分页 OFFSET 越深,性能越差(多次强调)** 129 | 130 | ![图1 数据仅供参考](https://imagES-machen.oss-cn-beijing.aliyuncs.com/image-20201223204520344.png) 131 | 132 | ## 深分页优化 133 | 134 | 关于 MySQL 深分页优化常见的大概有以下三种策略: 135 | 136 | 1. 子查询优化 137 | 2. 延迟关联 138 | 3. 书签记录 139 | 140 | 上面三点都能大大的提升查询效率,**核心思想就是让 MySQL 尽可能扫描更少的页面**,获取需要访问的记录后再根据关联列回原表查询所需要的列 141 | 142 | ### 子查询优化 143 | 144 | 子查询深分页优化语句如下: 145 | 146 | ```sql 147 | mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID >= ( SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) LIMIT 1; 148 | +-------------+-------------------------+------------------------+ 149 | | MCS_PROD_ID | MCS_CODE | MCS_NAME | 150 | +-------------+-------------------------+------------------------+ 151 | | 3021401 | XA892010009391491861476 | 金属解剖型接骨板T型接骨板A | 152 | +-------------+-------------------------+------------------------+ 153 | 1 row in set (0.76 sec) 154 | 155 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID >= ( SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) LIMIT 1; 156 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+ 157 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 158 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+ 159 | | 1 | PRIMARY | MCS_PROD | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 2296653 | 100.00 | Using where | 160 | | 2 | SUBQUERY | m1 | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using where; Using index | 161 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+ 162 | 2 rows in set, 1 warning (0.77 sec) 163 | ``` 164 | 165 | 根据执行计划得知,子查询 table m1 查询是用到了索引。首先在 **索引上拿到了聚集索引的主键 ID 省去了回表操作**,然后第二查询直接根据第一个查询的 ID 往后再去查 10 个就可以了 166 | 167 | ![图2 数据仅供参考](https://imagES-machen.oss-cn-beijing.aliyuncs.com/image-20201223205050818.png) 168 | 169 | ### 延迟关联 170 | 171 | "延迟关联" 深分页优化语句如下: 172 | 173 | ```sql 174 | mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD INNER JOIN (SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) AS MCS_PROD2 USING(MCS_PROD_ID); 175 | +-------------+-------------------------+------------------------+ 176 | | MCS_PROD_ID | MCS_CODE | MCS_NAME | 177 | +-------------+-------------------------+------------------------+ 178 | | 3021401 | XA892010009391491861476 | 金属解剖型接骨板T型接骨板A | 179 | +-------------+-------------------------+------------------------+ 180 | 1 row in set (0.75 sec) 181 | 182 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD INNER JOIN (SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) AS MCS_PROD2 USING(MCS_PROD_ID); 183 | +----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+ 184 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 185 | +----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+ 186 | | 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 2296653 | 100.00 | NULL | 187 | | 1 | PRIMARY | MCS_PROD | NULL | eq_ref | PRIMARY | PRIMARY | 4 | MCS_PROD2.MCS_PROD_ID | 1 | 100.00 | NULL | 188 | | 2 | DERIVED | m1 | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using where; Using index | 189 | +----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+ 190 | 3 rows in set, 1 warning (0.00 sec) 191 | ``` 192 | 193 | 思路以及性能与子查询优化一致,只不过采用了 JOIN 的形式执行 194 | 195 | ### 书签记录 196 | 197 | 关于 LIMIT 深分页问题,核心在于 OFFSET 值,它会 **导致 MySQL 扫描大量不需要的记录行然后抛弃掉** 198 | 199 | 我们可以先使用书签 **记录获取上次取数据的位置**,下次就可以直接从该位置开始扫描,这样可以 **避免使用 OFFEST** 200 | 201 | 假设需要查询 3000000 行数据后的第 1 条记录,查询可以这么写 202 | 203 | ```sql 204 | mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID < 3000000 ORDER BY UPDT_TIME LIMIT 1; 205 | +-------------+-------------------------+---------------------------------+ 206 | | MCS_PROD_ID | MCS_CODE | MCS_NAME | 207 | +-------------+-------------------------+---------------------------------+ 208 | | 127 | XA683240878449276581799 | 股骨近端-1螺纹孔锁定板(纯钛)YJBL01 | 209 | +-------------+-------------------------+---------------------------------+ 210 | 1 row in set (0.00 sec) 211 | 212 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID < 3000000 ORDER BY UPDT_TIME LIMIT 1; 213 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+ 214 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 215 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+ 216 | | 1 | SIMPLE | MCS_PROD | NULL | index | PRIMARY | MCS_PROD_1 | 5 | NULL | 2 | 50.00 | Using where | 217 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+ 218 | 1 row in set, 1 warning (0.00 sec) 219 | ``` 220 | 221 | 好处是很明显的,查询速度超级快,**性能都会稳定在毫秒级**,从性能上考虑碾压其它方式 222 | 223 | 不过这种方式局限性也比较大,需要一种类似连续自增的字段,以及业务所能包容的连续概念,视情况而定 224 | 225 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201224145609132.png) 226 | 227 | 上图是阿里云 OSS Bucket 桶内文件列表,大胆猜测是不是可以采用书签记录的形式完成 228 | 229 | ## ORDER BY 巨坑, 慎踩 230 | 231 | 以下言论可能会打破你对 order by 所有 **美好 YY** 232 | 233 | 先说结论吧,当 LIMIT OFFSET 过深时,会使 **ORDER BY 普通索引失效**(联合、唯一这些索引没有测试) 234 | 235 | ```sql 236 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1; 237 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 238 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 239 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 240 | | 1 | SIMPLE | MCS_PROD | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using index condition | 241 | +----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+ 242 | 1 row in set, 1 warning (0.00 sec) 243 | ``` 244 | 245 | 先来说一下这个 ORDER BY 执行过程: 246 | 247 | 1. 初始化 SORT_BUFFER,放入 MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME 四个字段 248 | 2. 从索引 UPDT_TIME 找到满足条件的主键 ID,回表查询出四个字段值存入 SORT_BUFFER 249 | 3. 从索引处继续查询满足 UPDT_TIME 条件记录,继续执行步骤 2 250 | 4. 对 SORT_BUFFER 中的数据按照 UPDT_TIME 排序 251 | 5. 排序成功后取出符合 LIMIT 条件的记录返回客户端 252 | 253 | 按照 UPDT_TIME 排序可能在内存中完成,也可能需要使用外部排序,取决于排序所需的内存和参数 SORT_BUFFER_SIZE 254 | 255 | **SORT_BUFFER_SIZE 是 MySQL 为排序开辟的内存**。如果排序数据量小于 SORT_BUFFER_SIZE,排序会在内存中完成。如果数据量过大,内存放不下,**则会利用磁盘临时文件排序** 256 | 257 | > 针对 SORT_BUFFER_SIZE 这个参数在网上查询到有用资料比较少,大家如果测试过程中存在问题,可以加微信一起沟通 258 | 259 | #### ORDER BY 索引失效举例 260 | 261 | OFFSET 100000 时,通过 key Extra 得知,没有使用磁盘临时文件排序,这个时候把 OFFSET 调整到 500000 262 | 263 | 一首凉凉送给写这个 SQL 的同学,发现了 Using filesort 264 | 265 | ```sql 266 | mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 500000, 1; 267 | +----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+ 268 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 269 | +----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+ 270 | | 1 | SIMPLE | MCS_PROD | NULL | ALL | MCS_PROD_1 | NULL | NULL | NULL | 4593306 | 50.00 | Using where; Using filesort | 271 | +----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+ 272 | 1 row in set, 1 warning (0.00 sec) 273 | ``` 274 | 275 | Using filesort 表示在索引之外,**需要额外进行外部的排序动作**,性能必将受到严重影响 276 | 277 | 所以我们应该 **结合相对应的业务逻辑避免常规 LIMIT OFFSET**,采用 **# 深分页优化** 章节进行修改对应业务 278 | 279 | ## 结言 280 | 281 | **最后有一点需要声明下,MySQL 本身并不适合单表大数据量业务** 282 | 283 | 因为 MySQL 应用在企业级项目时,针对库表查询并非简单的条件,可能会有更复杂的联合查询,亦或者是大数据量时存在频繁新增或更新操作,维护索引或者数据 ACID 特性上必然存在性能牺牲 284 | 285 | 如果设计初期能够预料到库表的数据增长,理应构思合理的重构优化方式,比如 ES 配合查询、分库分表、TiDB 等解决方式 286 | 287 | **参考资料:** 288 | 289 | 1. 《高性能 MySQL 第三版》 290 | 2. 《MySQL 实战 45 讲》 291 | 292 | -------------------------------------------------------------------------------- /docs/scene/Snowflake(雪花算法),生产环境生成重复ID.md: -------------------------------------------------------------------------------- 1 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210912002234335.png) 2 | 3 | 分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的 4 | 5 | 有些时候我们希望能使用一种简单些的 ID,并且希望 ID 能够按照时间有序生成 6 | 7 | ## 什么是雪花算法 8 | 9 | Snowflake 中文的意思是雪花,所以常被称为雪花算法,是 Twitter 开源的分布式 ID 生成算法 10 | 11 | Twitter 雪花算法生成后是一个 64bit 的 long 型的数值,组成部分引入了时间戳,基本保持了自增 12 | 13 | **SnowFlake 算法的优点:** 14 | 15 | 1. 高性能高可用:生成时不依赖于数据库,完全在内存中生成 16 | 2. 高吞吐:每秒钟能生成数百万的自增 ID 17 | 3. ID 自增:存入数据库中,索引效率高 18 | 19 | **SnowFlake 算法的缺点:** 20 | 21 | 依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成 ID 冲突或者重复 22 | 23 | ### 雪花算法组成 24 | 25 | snowflake 结构如下图所示: 26 | 27 | 28 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210912123815743.png) 29 | 30 | 包含四个组成部分 31 | 32 | **不使用**:1bit,最高位是符号位,0 表示正,1 表示负,固定为 0 33 | 34 | **时间戳**:41bit,毫秒级的时间戳(41 位的长度可以使用 69 年) 35 | 36 | **标识位**:5bit 数据中心 ID,5bit 工作机器 ID,两个标识位组合起来最多可以支持部署 1024 个节点 37 | 38 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210911125105248.png) 39 | 40 | **序列号**:12bit 递增序列号,表示节点毫秒内生成重复,通过序列号表示唯一,12bit 每毫秒可产生 4096 个 ID 41 | 42 | > 通过序列号 1 毫秒可以产生 4096 个不重复 ID,则 1 秒可以生成 4096 * 1000 = 409w ID 43 | 44 | 默认的雪花算法是 64 bit,具体的长度可以自行配置。如果希望运行更久,**增加时间戳的位数**;如果需要支持更多节点部署,**增加标识位长度**;如果并发很高,**增加序列号位数** 45 | 46 | **总结**:雪花算法并不是一成不变的,可以根据系统内具体场景进行定制 47 | 48 | ### 雪花算法适用场景 49 | 50 | 因为雪花算法有序自增,保障了 MySQL 中 B+ Tree 索引结构插入高性能 51 | 52 | 所以,日常业务使用中,雪花算法更多是被应用在数据库的主键 ID 和业务关联主键 53 | 54 | ## 雪花算法生成 ID 重复问题 55 | 56 | **假设**:一个订单微服务,通过雪花算法生成 ID,共部署三个节点,标识位一致 57 | 58 | 此时有 200 并发,均匀散布三个节点,三个节点同一毫秒同一序列号下生成 ID,那么就会产生重复 ID 59 | 60 | 通过上述假设场景,可以知道雪花算法生成 ID 冲突存在一定的前提条件 61 | 62 | 1. 服务通过集群的方式部署,其中部分机器标识位一致 63 | 2. 业务存在一定的并发量,没有并发量无法触发重复问题 64 | 3. 生成 ID 的时机:同一毫秒下的序列号一致 65 | 66 | 67 | 68 | ### 标识位如何定义 69 | 70 | 如果能保证标识位不重复,那么雪花 ID 也不会重复 71 | 72 | 通过上面的案例,知道了 ID 重复的必要条件。如果要避免服务内产生重复的 ID,那么就需要从标识位上动文章 73 | 74 | 我们先看看开源框架中使用雪花算法,如何定义标识位 75 | 76 | Mybatis-Plus v3.4.2 雪花算法实现类 Sequence,提供了两种构造方法:无参构造,自动生成 dataCenterId 和 workerId;有参构造,创建 Sequence 时明确指定标识位 77 | 78 | > Hutool v5.7.9 参照了 Mybatis-Plus dataCenterId 和 workerId 生成方案,提供了默认实现 79 | 80 | 一起看下 Sequence 的创建默认无参构造,如何生成 dataCenterId 和 workerId 81 | 82 | ```java 83 | public static long getDataCenterId(long maxDatacenterId) { 84 | long id = 1L; 85 | final byte[] mac = NetUtil.getLocalHardwareAddress(); 86 | if (null != mac) { 87 | id = ((0x000000FF & (long) mac[mac.length - 2]) 88 | | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6; 89 | id = id % (maxDatacenterId + 1); 90 | } 91 | 92 | return id; 93 | } 94 | ``` 95 | 96 | 入参 `maxDatacenterId` 是一个固定值,代表数据中心 ID 最大值,默认值 31 97 | 98 | > 为什么最大值要是 31?因为 5bit 的二进制最大是 11111,对应十进制数值 31 99 | 100 | 获取 dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L;另一种不为空,通过 Mac 地址获取 dataCenterId 101 | 102 | 可以得知,dataCenterId 的取值与 Mac 地址有关 103 | 104 | 接下来再看看 workerId 105 | 106 | ```java 107 | public static long getWorkerId(long datacenterId, long maxWorkerId) { 108 | final StringBuilder mpid = new StringBuilder(); 109 | mpid.append(datacenterId); 110 | try { 111 | mpid.append(RuntimeUtil.getPid()); 112 | } catch (UtilException igonre) { 113 | //ignore 114 | } 115 | return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1); 116 | } 117 | ``` 118 | 119 | 入参 maxWorkderId 也是一个固定值,代表工作机器 ID 最大值,默认值 31;datacenterId 取自上述的 getDatacenterId 方法 120 | 121 | name 变量值为 `PID@IP`,所以 name 需要根据 `@` 分割并获取下标 0,得到 PID 122 | 123 | 通过 MAC + PID 的 hashcode 获取16个低位,进行运算,最终得到 workerId 124 | 125 | ### 分配标识位 126 | 127 | Mybatis-Plus 标识位的获取依赖 Mac 地址和进程 PID,虽然能做到尽量不重复,但仍有小几率 128 | 129 | 标识位如何定义才能不重复?有两种方案:**预分配和动态分配** 130 | 131 | **预分配** 132 | 133 | 应用上线前,统计当前服务的节点数,人工去申请标识位 134 | 135 | 这种方案,没有代码开发量,在服务节点固定或者项目少可以使用,但是解决不了服务节点动态扩容性问题 136 | 137 | 138 | 139 | **动态分配** 140 | 141 | 通过将标识位存放在 Redis、Zookeeper、MySQL 等中间件,在服务启动的时候去请求标识位,请求后标识位更新为下一个可用的 142 | 143 | 通过存放标识位,延伸出一个问题:雪花算法的 ID 是 **服务内唯一还是全局唯一** 144 | 145 | 以 Redis 举例,如果要做服务内唯一,存放标识位的 Redis 节点使用自己项目内的就可以;如果是全局唯一,所有使用雪花算法的应用,要用同一个 Redis 节点 146 | 147 | 两者的区别仅是 **不同的服务间是否公用 Redis**。如果没有全局唯一的需求,最好使 ID 服务内唯一,因为这样可以避免单点问题 148 | 149 | > 服务的节点数超过 1024,则需要做额外的扩展;可以扩展 10 bit 标识位,或者选择开源分布式 ID 框架 150 | 151 | 152 | 153 | **动态分配实现方案** 154 | 155 | Redis 存储一个 Hash 结构 Key,包含两个键值对:dataCenterId 和 workerId 156 | 157 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210911225934411.png) 158 | 159 | 160 | 161 | 在应用启动时,通过 Lua 脚本去 Redis 获取标识位。dataCenterId 和 workerId 的获取与自增在 Lua 脚本中完成,调用返回后就是可用的标示位 162 | 163 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210911234823512.png) 164 | 165 | 具体 Lua 脚本逻辑如下: 166 | 167 | 1. 第一个服务节点在获取时,Redis 可能是没有 snowflake_work_id_key 这个 Hash 的,应该先判断 Hash 是否存在,不存在初始化 Hash,dataCenterId、workerId 初始化为 0 168 | 2. 如果 Hash 已存在,判断 dataCenterId、workerId 是否等于最大值 31,满足条件初始化 dataCenterId、workerId 设置为 0 返回 169 | 3. dataCenterId 和 workerId 的排列组合一共是 1024,在进行分配时,先分配 workerId 170 | 4. 判断 workerId 是否 != 31,条件成立对 workerId 自增,并返回;如果 workerId = 31,自增 dataCenterId 并将 workerId 设置为 0 171 | 172 | dataCenterId、workerId 是一直向下推进的,总体形成一个环状。通过 **Lua 脚本的原子性**,保证 1024 节点下的雪花算法生成不重复。如果标识位等于 1024,则从头开始继续循环推进 173 | 174 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210913172111610.png) 175 | 176 | 177 | 178 | ## 开源分布式 ID 框架 179 | 180 | Leaf 和 Uid 都有实现雪花算法,Leaf 额外提供了号段模式生成 ID 181 | 182 | 美团 Leaf:`https://github.com/Meituan-Dianping/Leaf` 183 | 184 | 百度 Uid:`https://github.com/baidu/uid-generator` 185 | 186 | 雪花算法可以满足大部分场景,如无必要,**不建议引入开源方案增加系统复杂度** 187 | 188 | ## 回顾总结 189 | 190 | 文章通过图文并茂的方式帮助读者梳理了一遍什么是雪花算法,以及如何解决雪花算法生成 ID 冲突的问题 191 | 192 | 关于雪环算法生成 ID 冲突问题,文中给了一种方案:**分配标示位**;通过分配雪花算法的组成标识位,来达到默认 1024 节点下 ID 生成唯一 193 | 194 | > 可以去看 Hutool 或者 Mybatis-Plus 雪花算法的具体实现,帮助大家更好的理解 195 | 196 | 雪花算法不是万能的,并不能适用于所有场景。**如果 ID 要求全局唯一并且服务节点超出 1024 节点**,可以选择修改算法本身的组成,即扩展标识位,或者选择开源方案:LEAF、UID 197 | 198 | 创作不易,文章看完有帮助,**点关注支持一下**,祝好 199 | -------------------------------------------------------------------------------- /docs/scene/如何保证使用MyBatis,查询千万数据量不发生内存溢出?.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 由于现在 ORM 框架的成熟运用,很多小伙伴对于 JDBC 的概念有些薄弱,**ORM 框架底层其实是通过 JDBC 操作的 DB** 4 | 5 | **JDBC**(JavaDataBase Connectivity)是 Java 数据库连接, 说的直白点就是使用 **Java 语言操作数据库** 6 | 7 | **由 SUN 公司提供出一套访问数据库的规范 API**, 并提供相对应的连接数据库协议标准, 然后 **各厂商根据规范提供一套访问自家数据库的 API 接口** 8 | 9 | 文章大数据量操作核心围绕 JDBC 展开,目录结构如下: 10 | 11 | > - MySQL JDBC 大数据量操作 12 | > - 常规查询 13 | > - 流式查询 14 | > - 游标查询 15 | > - JDBC RowData 16 | > - JDBC 通信原理 17 | > - 流式游标内存分析 18 | > - 单次调用内存使用 19 | > - 并发调用内存使用 20 | > - MyBatis 如何使用流式查询 21 | > - 结言 22 | 23 | ## MySql JDBC 大数据量操作 24 | 25 | 整篇文章以大数据量操作为议题,通过开发过程中的需求引出相关知识点 26 | 27 | 1. 迁移数据 28 | 2. 导出数据 29 | 3. 批量处理数据 30 | 31 | 一般而言笔者认为在 Java Web 程序里,能够被称为大数据量的,几十万到千万不等,再高的话 Java(WEB 应用)处理就不怎么合适了 32 | 33 | 举个例子,现在业务系统需要从 MySQL 数据库里读取 500w 数据行进行处理,应该怎么做 34 | 35 | 1. 常规查询,一次性读取 500w 数据到 JVM 内存中,或者分页读取 36 | 2. 流式查询,建立长连接,利用服务端游标,每次读取一条加载到 JVM 内存 37 | 3. 游标查询,和流式一样,通过 fetchSize 参数,控制一次读取多少条数据 38 | 39 | ### 常规查询 40 | 41 | 默认情况下,完整的检索结果集会将其存储在内存中。在大多数情况下,这是最有效的操作方式,并且由于 MySQL 网络协议的设计,因此更易于实现 42 | 43 | 假设单表 500w 数据量,没有人会一次性加载到内存中,一般会采用分页的方式 44 | 45 | ```java 46 | @SneakyThrows 47 | @Override 48 | public void pageQuery() { 49 | @Cleanup Connection conn = dataSource.getConnection(); 50 | @Cleanup Statement stmt = conn.createStatement(); 51 | long start = System.currentTimeMillis(); 52 | long offset = 0; 53 | int size = 100; 54 | while (true) { 55 | String sql = String.format("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE LIMIT %s, %s", offset, size); 56 | @Cleanup ResultSet rs = stmt.executeQuery(sql); 57 | long count = loopResultSet(rs); 58 | if (count == 0) break; 59 | offset += size; 60 | } 61 | 62 | log.info(" 🚀🚀🚀 分页查询耗时 :: {} ", System.currentTimeMillis() - start); 63 | } 64 | ``` 65 | 66 | 上述方式比较简单,但是在不考虑 LIMIT 深分页优化情况下,线上数据库服务器就凉了,**亦或者你能等个几天时间检索数据** 67 | 68 | > [MySQL 千万数据量深分页优化, 拒绝线上故障!](https://mp.weixin.qq.com/s/i3wLeCSxqWKrTwgtfelumQ) 69 | 70 | ### 流式查询 71 | 72 | 如果你正在使用具有大量数据行的 ResultSet,并且无法在 JVM 中为其分配所需的内存堆空间,则可以告诉驱动程序从结果流中返回一行 73 | 74 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201229104940364.png) 75 | 76 | 流式查询有一点需要注意:**必须先读取(或关闭)结果集中的所有行**,然后才能对连接发出任何其他查询,否则将引发异常 77 | 78 | 使用流式查询,则要保持对产生结果集的语句所引用的表的并发访问,因为其 **查询会独占连接,所以必须尽快处理** 79 | 80 | ```java 81 | @SneakyThrows 82 | public void streamQuery() { 83 | @Cleanup Connection conn = dataSource.getConnection(); 84 | @Cleanup Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); 85 | stmt.setFetchSize(Integer.MIN_VALUE); 86 | 87 | long start = System.currentTimeMillis(); 88 | @Cleanup ResultSet rs = stmt.executeQuery("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE"); 89 | loopResultSet(rs); 90 | log.info(" 🚀🚀🚀 流式查询耗时 :: {} ", (System.currentTimeMillis() - start) / 1000); 91 | } 92 | ``` 93 | 94 | 流式查询库表数据量 500w 单次调用时间消耗:**≈ 6s** 95 | 96 | ### 游标查询 97 | 98 | SpringBoot 2.x 版本默认连接池为 HikariPool,连接对象是 HikariProxyConnection,所以下述设置游标方式就不可行了 99 | 100 | ```java 101 | ((JDBC4Connection) conn).setUseCursorFetch(true); 102 | ``` 103 | 104 | 需要在数据库连接信息里拼接 **&useCursorFetch=true**。其次设置 Statement 每次读取数据数量,比如一次读取 1000 105 | 106 | ```java 107 | @SneakyThrows 108 | public void cursorQuery() { 109 | @Cleanup Connection conn = dataSource.getConnection(); 110 | @Cleanup Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); 111 | stmt.setFetchSize(1000); 112 | 113 | long start = System.currentTimeMillis(); 114 | @Cleanup ResultSet rs = stmt.executeQuery("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE"); 115 | loopResultSet(rs); 116 | log.info(" 🚀🚀🚀 游标查询耗时 :: {} ", (System.currentTimeMillis() - start) / 1000); 117 | } 118 | ``` 119 | 120 | 游标查询库表数据量 500w 单次调用时间消耗:**≈ 18s** 121 | 122 | ### JDBC RowData 123 | 124 | 上面都使用到了方法 loopResultSet,方法内部只是进行了 while 循环,常规、流式、游标查询的核心点在于 next 方法 125 | 126 | ```java 127 | @SneakyThrows 128 | private Long loopResultSet(ResultSet rs) { 129 | while (rs.next()) { 130 | // 业务操作 131 | } 132 | return xx; 133 | } 134 | ``` 135 | 136 | ResultSet.next() 的逻辑是实现类 ResultSetImpl 每次都从 RowData 获取下一行的数据。RowData 是一个接口,实现关系图如下 137 | 138 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201230102449708.png) 139 | 140 | 默认情况下 ResultSet 会使用 RowDataStatic 实例,在生成 RowDataStatic 对象时就会把 ResultSet 中所有记录读到内存里,之后通过 next() 再一条条从内存中读 141 | 142 | RowDataCursor 的调用为批处理,然后进行内部缓存,流程如下: 143 | 144 | 1. 首先会查看自己内部缓冲区是否有数据没有返回,如果有则返回下一行 145 | 2. 如果都读取完毕,向 MySQL Server 触发一个新的请求读取 fetchSize 数量结果 146 | 3. 并将返回结果缓冲到内部缓冲区,然后返回第一行数据 147 | 148 | 当采用流式处理时,**ResultSet 使用的是 RowDataDynamic 对象**,而这个对象 next() 每次调用都会发起 IO 读取单行数据 149 | 150 | 总结来说就是,**默认的 RowDataStatic 读取全部数据到客户端内存中**,也就是我们的 JVM;**RowDataCursor 一次读取 fetchSize 行**,消费完成再发起请求调用;**RowDataDynamic 每次 IO 调用读取一条数据** 151 | 152 | ### JDBC 通信原理 153 | 154 | #### 普通查询 155 | 156 | 在 JDBC 与 MySQL 服务端的交互是通过 Socket 完成的,对应到网络编程,可以把 **MySQL 当作一个 SocketServer**,因此一个完整的请求链路应该是: 157 | 158 | > JDBC 客户端 -> 客户端 Socket -> MySQL -> 检索数据返回 -> MySQL 内核 Socket 缓冲区 -> 网络 -> 客户端 Socket Buffer -> JDBC 客户端 159 | 160 | 普通查询的方式在查询大数据量时,所在 JVM 可能会凉凉,原因如下: 161 | 162 | 1. MySQL Server 会将检索出的 SQL 结果集通过输出流写入到内核对应的 Socket Buffer 163 | 2. **内核缓冲区通过 JDBC 发起的 TCP 链路进行回传数据**,此时数据会先进入 JDBC 客户端所在内核缓冲区 164 | 3. JDBC 发起 SQL 操作后,程序会被阻塞在输入流的 read 操作上,当缓冲区有数据时,程序会被唤醒进而将缓冲区数据读取到 JVM 内存中 165 | 4. MySQL Server 会不断发送数据,JDBC 不断读取缓冲区数据到 Java 内存中,**虽然此时数据已到 JDBC 所在程序本地,但是 JDBC 还没有对 execute 方法调用处进行响应**,因为需要等到对应数据读取完毕才会返回 166 | 5. 弊端就显而易见了,如果查询数据量过大,会不断经历 GC,然后就是内存溢出 167 | 168 | #### 游标查询 169 | 170 | 通过上文得知,游标可以解决普通查询大数据量的内存溢出问题,但是 171 | 172 | 小伙伴有没有思考过这么一个问题,**MySQL 不知道客户端程序何时消费完成,此时另一连接对该表造成 DML 写入操作应该如何处理?** 173 | 174 | 其实,在我们使用游标查询时,MySQL 需要建立一个临时空间来存放需要被读取的数据,**所以不会和 DML 写入操作产生冲突** 175 | 176 | 但是游标查询会引发以下现象: 177 | 178 | 1. **IOPS 飙升**,因为需要返回的数据需要写入到临时空间中,**存在大量的 IO 读取和写入**,此流程可能会引起其它业务的写入抖动 179 | 2. **磁盘空间飙升**,因为写入临时空间的数据是在原表之外的,如果表数据过大,**极端情况下可能会导致数据库磁盘写满**,这时网络输出时没有变化的。而写入临时空间的数据会在 **读取完成或客户端发起 ResultSet#close 操作时由 MySQL 回收** 180 | 3. 客户端 JDBC 发起 SQL 查询,可能会有长时间等待 SQL 响应,这段时间为服务端准备数据阶段。但是 **普通查询等待时间与游标查询等待时间原理上是不一致的**,前者是一致在读取网络缓冲区的数据,没有响应到业务层面;后者是 MySQL 在准备临时数据空间,没有响应到 JDBC 181 | 4. 数据准备完成后,进行到传输数据阶段,**网络响应开始飙升,IOPS 由"读写"转变为"读取"** 182 | 183 | 采用游标查询的方式 **通信效率比较低**,因为客户端消费完 fetchSize 行数据,就需要发起请求到服务端请求,在数据库前期准备阶段 **IOPS 会非常高,占用大量的磁盘空间以及性能** 184 | 185 | #### 流式查询 186 | 187 | 当客户端与 MySQL Server 端建立起连接并且交互查询时,MySQL Server 会通过输出流将 SQL 结果集返回输出,也就是 **向本地的内核对应的 Socket Buffer 中写入数据**,然后将内核中的数据通过 TCP 链路回传数据到 JDBC 对应的服务器内核缓冲区 188 | 189 | 1. JDBC 通过输入流 read 方法去读取内核缓冲区数据,因为开启了流式读取,每次业务程序接收到的数据只有一条 190 | 2. MySQL 服务端会向 JDBC 代表的客户端内核源源不断的输送数据,直到客户端请求 Socket 缓冲区满,这时的 MySQL 服务端会阻塞 191 | 3. 对于 JDBC 客户端而言,数据每次读取都是从本机器的内核缓冲区,所以性能会更快一些,一般情况不必担心本机内核无数据消费(除非 MySQL 服务端传递来的数据,在客户端不做任何业务逻辑,拿到数据直接放弃,会发生客户端消费比服务端超前的情况) 192 | 193 | 看起来,流式要比游标的方式更好一些,**但是事情往往不像表面上那么简单** 194 | 195 | 1. 相对于游标查询,**流式对数据库的影响时间要更长一些** 196 | 2. 另外流式查询依赖网络,**导致网络拥塞可能性较大** 197 | 198 | ## 流式游标内存分析 199 | 200 | 表数据量:500w 201 | 202 | 内存查看工具:JDK 自带 Jvisualvm 203 | 204 | 设置 JVM 参数: -Xmx512m -Xms512m 205 | 206 | ### 单次调用内存使用 207 | 208 | 流式查询内存性能报告如下 209 | 210 | ![图1 数据仅供参考](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201230141437876.png) 211 | 212 | 游标查询内存性能报告如下 213 | 214 | ![图2 数据仅供参考](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201230141536134.png) 215 | 216 | 根据内存占用情况来看,游标查询和流式查询都 **能够很好的防止 OOM** 217 | 218 | ### 并发调用内存使用 219 | 220 | 并发调用:Jmete 1 秒 10 个线程并发调用 221 | 222 | 流式查询内存性能报告如下 223 | 224 | ![图3 数据仅供参考](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201230140330532.png) 225 | 226 | 并发调用对于内存占用情况也很 OK,**不存在叠加式增加** 227 | 228 | 流式查询并发调用时间平均消耗:**≈ 55s** 229 | 230 | 游标查询内存性能报告如下 231 | 232 | ![图4 数据仅供参考](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20201230141036691.png) 233 | 234 | 游标查询并发调用时间平均消耗:**≈ 83s** 235 | 236 | 因为设备限制,以及部分情况只会在极端下产生,所以没有进行生产、测试多环境验证,小伙伴感兴趣可以自行测试 237 | 238 | ## MyBatis 如何使用流式查询 239 | 240 | 上文都是在描述如何使用 JDBC 原生 API 进行查询,ORM 框架 Mybatis 也针对流式查询进行了封装 241 | 242 | ResultHandler 接口只包含 handleResult 方法,可以获取到已转换后的 Java 实体类 243 | 244 | ```java 245 | @Slf4j 246 | @Service 247 | public class MyBatisStreamService { 248 | @Resource 249 | private MyBatisStreamMapper myBatisStreamMapper; 250 | 251 | public void mybatisStreamQuery() { 252 | long start = System.currentTimeMillis(); 253 | myBatisStreamMapper.mybatisStreamQuery(new ResultHandler() { 254 | @Override 255 | public void handleResult(ResultContext resultContext) { } 256 | }); 257 | log.info(" 🚀🚀🚀 MyBatis查询耗时 :: {} ", System.currentTimeMillis() - start); 258 | } 259 | } 260 | ``` 261 | 262 | 除了下述注解式的应用方式,也可以使用 .xml 文件的形式 263 | 264 | ```java 265 | @Mapper 266 | public interface MyBatisStreamMapper { 267 | @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE) 268 | @ResultType(YOU_TABLE_DO.class) 269 | @Select("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE") 270 | void mybatisStreamQuery(ResultHandler handler); 271 | } 272 | ``` 273 | 274 | Mybatis 流式查询调用时间消耗:**≈ 18s** 275 | 276 | JDBC 流式与 MyBatis 封装的流式读取对比 277 | 278 | 1. MyBatis 相对于原生的流式还是慢上了不少,但是考虑到底层的封装的特性,这点性能还是可以接受的 279 | 2. 从内存占比而言,两者波动相差无几 280 | 3. MyBatis 相对于原生 JDBC 更为的方便,因为封装了回调函数以及序列化对象等特性 281 | 282 | 两者具体的使用,可以针对项目实际情况而定,**没有最好的,只有最适合的** 283 | 284 | ## 结言 285 | 286 | 流式查询、游标查询可以避免 OOM,**数据量大可以考虑此方案**。但是这两种方式会占用数据库连接,使用中不会释放,所以线上针对大数据量业务用到游标和流式操作,**一定要进行并发控制** 287 | 288 | 另外针对 JDBC 原生流式查询,Mybatis 中也进行了封装,虽然会慢一些,但是 **功能以及代码的整洁程度会好上不少** 289 | 290 | **参考文章:** 291 | 292 | 1. https://blog.csdn.net/xieyuooo/article/details/83109971 293 | -------------------------------------------------------------------------------- /docs/scene/线上问题复盘,异常信息消失的罪魁祸首JVM-Fast-Throw.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 首先,这是一个 **悲伤的故事**,涉及到JVM 底层优化的知识点。想到第一次碰到这种问题时的懵逼,应了句老话:**书到用时方恨少!** 4 | 5 | 负责的消息中台在 **晚上八点左右**,运维群里反馈大量用户接收不到短信消息。登陆 Kibana 查找对应的 Error 日志,发现出现了 **大量的下标越界异常** 6 | 7 | 当时更...,线上问题得到了修复。但是,出现问题可不得找到问题的产出原因,不然下次有可能还会出现 8 | 9 | 因为在 ELK 上进行 **日志分析不太方便**,难以根据对应异常进行不同纬度上的统计分析,所以联系运维同学将故障产生当天的 **Info、Error 日志** 拉下来进行线下分析 10 | 11 | 经过日志分析得知,异常的产出有两种,**一种是有堆栈信息**,比如: 12 | 13 | ```java 14 | java.lang.ArrayIndexOutOfBoundsException: -1 15 | ... 省略堆栈信息 16 | ``` 17 | 18 | 另外一种,就比较诡异,**只有异常,没有对应的堆栈信息** 19 | 20 | ```java 21 | java.lang.ArrayIndexOutOfBoundsException: null 22 | ``` 23 | 24 | 第一种问题比较好定位,根据 **异常堆栈信息**,定位到了具体代码,直接进行了修复,难就难在第二种 25 | 26 | **其实这两个是一个异常**,往后看小伙伴就明白了。后面做的所有事情,都是为了搞清楚两件事情 27 | 28 | - 为什么异常 message 会输出 null 29 | - 为什么堆栈信息没有输出打印 30 | 31 | 32 | 33 | ## JVM Fast Throw 34 | 35 | **什么是 Fast Throw?** 36 | 37 | 大白话一点来说,就是:当一些异常类型(空指针、下标越界、算术运算等...)在代码里的固定位置被抛出多次,虚拟机(HotSpot VM)会直接 **抛出一个事先分配好、类型匹配的异常对象**。此异常对象的 **message 和 stack trace 都为空** 38 | 39 | 看到这里相信读者朋友已经明白了为什么同一种异常,**打印出来的日志却是不一样内容** 了吧。就是因为某一个异常在同一个地方多次被抛出,JVM 抛出一个预分配异常,那么 **message、stack trace 相当于被吞掉了** 40 | 41 | > The compiler in the server VM now provides correct stack backtraces for all "cold" built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: **-XX:-OmitStackTraceInFastThrow.** 42 | 43 | JDK 1.5 的发布文档介绍中描述了此情况,出现这种优化方案的原因是 **为了提高性能**。当同一种异常在相同的位置被抛出多次,**编译器就会重新编译此方法**。重编译后,编译器可能会 **使用不提供堆栈跟踪的预分配异常** 来选择更快的策略 44 | 45 | 如果想要关闭这种预分配异常的机制,可以使用 **-XX:-OmitStackTraceInFastThrow**。感兴趣的读者朋友可以看一下发布说明:`https://sourl.cn/PMzVkC` 46 | 47 | 另外通过 JVM 的源码得知,Fast Throw 机制目前支持五种异常情况,截图如下 48 | 49 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/34bf1837-6d11-4228-9e76-f6d0e7bfeef7.png) 50 | 51 | ## 模拟 Fast Throw 52 | 53 | 上面说的都是理论部分,这个章节使用代码来实战下 54 | 55 | ```java 56 | List list = new ArrayList(); 57 | for (int j = 0; j < 10000; j++) { 58 | try { 59 | list.get(-1); 60 | } catch (Exception ex) { 61 | int length = ex.getStackTrace().length; 62 | System.out.println(String.format("报错异常 :: %s, 堆栈长度 :: %s", ex, length)); 63 | } 64 | } 65 | ``` 66 | 67 | 上面程序跑在了 Java8 的环境中,通过运行程序结果可以看出来,Fast Throw 在 Java 8 中依然生效 68 | 69 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/96b65751-42fd-4ea1-8d83-5a5b3cbd71cf.png) 70 | 71 | **如果没有特别情况,最好不要关闭此特性**。因为如果并发量大的接口,因为程序的 BUG 导致大量的请求在同一代码处抛出异常,**Fast Throw 机制可以节省很多性能损耗**。通过单线程跑测试 Demo 得知,异常调用情况越多,性能差别越大 72 | 73 | | | 开启 Fast Throw | 关闭 Fast Throw | 74 | | ----- | ---------------- | --------------- | 75 | | 10w | 1004ms | 3547ms | 76 | | 100 w | 6193ms | 30928ms | 77 | | 500w | 37492ms | ... | 78 | 79 | 如果线上环境触发了 Fast Throw 机制,可以通过 **向前追溯相同位置、相同异常的日志** 来定位问题的产出原因 80 | 81 | ## 结言 82 | 83 | 千言万语汇成一句话就是,***重构有风险,上线需谨慎*** 84 | 85 | 针对公共功能的重构,**需要包含全量的测试用例**,要将可能会出现的问题产出背景考虑到 **极致**,亦或者和身边同事说明需求背景,大家一起想下,可以极大程度避免极端问题的产出 86 | 87 | **必要的压力测试** 是很重要的,这一点可以很好的将 **流量大才能显现的问题** 提前暴露出来 88 | 89 | 故障的产生带来的意义,有好有坏,坏的点大家都懂得;好的点自然是 **积累了线上问题故障排查的经验** 90 | -------------------------------------------------------------------------------- /docs/sourcecode/Mybatis核心架构设计分享.md: -------------------------------------------------------------------------------- 1 | ## Mybatis 架构设计分享 2 | 3 | > 本次分享内容依据 Mybatis-3-3.4.x 源码 4 | 5 | ## 1、带着问题思考本次分享 6 | 7 | 1. Mybatis 与 JDBC 的关系 8 | 2. .xml 文件定义 SQL 语句如何解析 9 | 3. Mybatis 中 Mapper 接口的存储与实现 10 | 4. Mybatis SQL 的执行过程 11 | 5. Mybatis 中分页如何实现 12 | 13 | ## 2、持久层的那些事 14 | 15 | ### 2.1 JDBC 16 | 17 | #### 什么是 JDBC 18 | 19 | JDBC(JavaDataBase Connectivity)就是 Java 数据库连接, 说的直白点就是 **使用 Java 语言操作数据库** 20 | 21 | 本来我们是通过控制台或客户端操作的数据库, JDBC 是用 Java 语言来发送 SQL 语句 22 | 23 | #### JDBC 原理 24 | 25 | 最初 SUN 公司希望提供一套 **能够适用所有数据库的 API**, 但是在实际操作中却发现这是项基本不可能完成的任务 26 | 27 | 因为各个厂商所提供的 **数据库差异实在太大**, 所以 SUN 公司与数据库厂商讨论出的就是:由 **SUN 公司提供出一套访问数据库的规范 API**, 并提供相对应的连接数据库协议标准, 然后各厂商根据规范提供一套访问自家数据库的 API 接口 28 | 29 | 最终:SUN 公司提供的规范 API 称之为 **JDBC**, 各厂商提供的自家数据库 API 接口称之为 **驱动** 30 | 31 | ![JDBC 架构图](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200818124614542.png) 32 | 33 | ### 2.2 Mybatis 34 | 35 | #### 什么是 Mybatis 36 | 37 | Mybatis 是一款优秀的 ORM(持久层)框架,使用 Java 语言 编写 38 | 39 | 前身是 apache 的一个开源项目 iBatis,2010 年迁移到 google code 并正式改名为 Mybatis 40 | 41 | ORM 持久层 指的是 : **将业务数据存储到磁盘,也具备长期存储能力,只要磁盘不损坏,如果在断电情况下,重启系统仍然可以读取数据** 42 | 43 | #### Mybatis 与 JDBC 的关系 44 | 45 | 在没有持久层框架之前, 想要代码中操作数据库都必须通过 JDBC 来操作, 接下来一个例子来说明两者之间的关系 46 | 47 | **JDBC 操作数据库** 48 | 49 | ![JDBC](https://images-machen.oss-cn-beijing.aliyuncs.com/carbon_jdbc.png) 50 | 51 | 相信大家都在实际项目中使用过 Mybatis, 可以联想一下, 平常我们工作中, 是否做过以下事情: 52 | 53 | - 是否装载过数据库驱动? 54 | - 是否从驱动中获取数据库连接? 55 | - 是否创建过执行 SQL 的 Statement? 56 | - 是否自行将数据库返回结果转换成 Java 对象? 57 | - 是否关闭过 finally 块中的三个对象? 58 | 59 | 看到上面的灵魂拷问, 就可以对本次分享的第一个问题作出解答: 60 | 61 | **Mybatis 针对 JDBC 中重复操作做了封装, 同时扩展并优化部分功能** 62 | 63 | ## 3、Mybatis 关键词说明 64 | 65 | > 📖 如果在阅读文章前没有接触过 Mybatis 源码相关的内容, 建议将下述名词多看几遍再向下阅读 66 | 67 | ### SqlSession 68 | 69 | 负责执行 **select、insert、update、delete** 等命令, 同时负责获取映射器和管理事务; 其底层封装了与 JDBC 的交互, 可以说是 mybatis 最核心的接口之一 70 | 71 | ### SqlSessionFactory 72 | 73 | 负责创建 **SqlSession** 的工厂, 一旦被创建就应该在应用运行期间一直存在, **不需要额外再进行创建** 74 | 75 | ### SqlSessionFactoryBuilder 76 | 77 | 主要是负责创建 **SqlSessionFactory** 的构造器类, 其中使用到了构建者设计模式; 仅负责创建 **SqlSessionFactory** 78 | 79 | ### Configuration 80 | 81 | Mybatis 最重要的配置类, 没有之一, 存储了大量的对象配置, 可以看源码感受一下 82 | 83 | ### MappedStatement 84 | 85 | MappedStatement 是保存 SQL 语句的数据结构, 其中的类属性都是由解析 .xml 文件中的 SQL 标签转化而成 86 | 87 | ### Executor 88 | 89 | SqlSession 对象对应一个 Executor, Executor 对象作用于 **增删改查方法** 以及 **事务、缓存** 等操作 90 | 91 | ### ParameterHandler 92 | 93 | Mybatis 中的 **参数处理器**, 类关系比较简单 94 | 95 | ### StatementHandler 96 | 97 | StatementHandler 是 Mybatis 负责 **创建 Statement 的处理器**, 根据不同的业务创建不同功能的 Statement 98 | 99 | ### ResultSetHandler 100 | 101 | ResultSetHandler 是 Mybatis 负责将 JDBC 返回数据进行解析, 并包装为 Java 中对应数据结构的处理器 102 | 103 | ### Interceptor 104 | 105 | Interceptor 为 Mybatis 中定义公共拦截器的接口, 其中定义了相关实现方法 106 | 107 | ## 4、Mybatis 架构设计 108 | 109 | ### 4.1 架构图 110 | 111 | ![Mybatis 分层架构图](https://images-machen.oss-cn-beijing.aliyuncs.com/Mybatis架构分析-Mybatis架构图_2.png) 112 | 113 | ### 4.2 基础支持层 114 | 115 | #### 反射模块 116 | 117 | 反射在 Java 中的应用可以说是相当广泛了, 同时也是一把双刃剑。 Mybatis 框架本身 **封装出了反射模块**, 提供了比原生反射更 **简洁易用的 API 接口**, 以及对 **类的元数据增加缓存, 提高反射的性能** 118 | 119 | #### 类型转换 120 | 121 | 类型转换模块最重要的功能就是在为 SQL 语句绑定实参时, 将 **Java 类型转为 JDBC 类型**, 在映射结果集时再由 **JDBC 类型转为 Java 类型** 122 | 123 | 另外一个功能就是提供别名机制, 简化了配置文件的定义 124 | 125 | #### 日志模块 126 | 127 | 日志对于系统的作用不言而喻, 尤其是测试、生产环境上查看信息及排查错误等都非常重要。主流的日志框架包括 Log4j、Log4j2、S l f4j 等, Mybatis 的日志模块作用就是 **集成这些日志框架** 128 | 129 | #### 资源加载 130 | 131 | Mybatis 对类加载器进行了封装, 用来确定类加载器的使用顺序, 用来记载类文件以及其它资源文件, 感兴趣可以参考 **ClassLoaderWrapper** 132 | 133 | #### 解析器模块 134 | 135 | 解析器模块主要提供了两个功能, 一个是封装了 XPath 类, 在 Mybatis 初始化时解析 Mybatis-config.xml 配置文件以及映射配置文件提供功能, 另一点就是处理动态 SQL 语句的占位符提供帮助 136 | 137 | #### ... 138 | 139 | ### 4.3 核心处理层 140 | 141 | #### 配置解析 142 | 143 | 在 Mybatis 初始化时, 会加载 Mybatis-config.xml 文件中的配置信息, 解析后的配置信息会 **转换成 Java 对象添加到 Configuration 对象** 144 | 145 | > 📖 比如说在 .xml 中定义的 resultMap 标签, 会被解析为 ResultMap 对象 146 | 147 | #### SQL 解析 148 | 149 | 大家如果手动拼写过复杂 SQL 语句, 就会明白会有多痛苦。Mybatis 提供出了动态 SQL, 加入了许多判断循环型标签, 比如 : if、where、foreach、set 等, 帮助开发者节约了大量的 SQL 拼写时间 150 | 151 | SQL 解析模块的作用就是将 Mybatis 提供的动态 SQL 标签解析为带占位符的 SQL 语句, 并在后期将实参对占位符进行替换 152 | 153 | #### SQL 执行 154 | 155 | SQL 的执行过程涉及几个比较重要的对象, **Executor、StatementHandler、ParameterHandler、ResultSetHandler** 156 | 157 | Executor 负责维护 **一级、二级缓存以及事务提交回滚操作**, 举个查询的例子, 查询请求会由 Executor 交给 StatementHandler 完成 158 | 159 | StatementHandler 通过 ParameterHandler 完成 **SQL 语句的实参绑定**, 通过 java.sql.Statement 执行 SQL 语句并拿到对应的 **结果集映射** 160 | 161 | 最后交由 ResultSetHandler 对结果集进行解析, 将 JDBC 类型转换为程序自定义的对象 162 | 163 | #### 插件 164 | 165 | 插件模块是 Mybatis 提供的一层扩展, 可以针对 SQL 执行的四大对象进行 **拦截并执行自定义插件** 166 | 167 | 插件编写需要很熟悉 Mybatis 运行机制, 这样才能控制编写的插件安全、高效 168 | 169 | ### 4.4 接口层 170 | 171 | 接口层只是 Mybatis **提供给调用端的一个接口 SqlSession**, 调用端在进行调用接口中方法时, 会调用核心处理层相对应的模块来完成数据库操作 172 | 173 | ## 5、问题答疑 174 | 175 | ### 5.1 .xml 文件定义 Sql 语句如何解析 176 | 177 | Mybatis 在创建 SqlSessionFactory 时, XMLConfigBuilder 会解析 Mybatis-config.xml 配置文件 178 | 179 | #### Mybatis 相关解析器 180 | 181 | Mybatis 解析器模块中定义了相关解析器的抽象类 BaseBuilder, 不同的子类负责实现解析不同的功能, 使用了 Builder 设计模式 182 | 183 | ![BaseBuilder](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200902131259623.png) 184 | 185 | XMLConfigBuilder 负责解析 mybatis-config.xml 配置文件 186 | 187 | XMLMapperBuilder 负责解析业务产生的 xxxMapper.xml 188 | 189 | ... 190 | 191 | #### mybatis-config.xml 解析 192 | 193 | XMLConfigBuilder 解析 mybatis-config.xml 内容参考代码 : 194 | 195 | ![parseConfiguration](https://images-machen.oss-cn-beijing.aliyuncs.com/parseConfiguration.png) 196 | 197 | **XMLConfifigBuilder#parseConfiguration()** 方法将 mybatis-config.xml 中定义的标签进行相关解析并填充到 Configuration 对象中 198 | 199 | #### xxxMapper.xml 解析 200 | 201 | **XMLConfifigBuilder#mapperElement()** 中解析配置的 mappers 标签, 找到具体的 .xml 文件, 并将其中的 select、insert、update、delete、resultMap 等标签解析为 Java 中的对象信息 202 | 203 | 具体解析 xxxMapper.xml 的对象为 XMLMapperBuilder, 具体的解析方法为 parse() 204 | 205 | ![parse](https://images-machen.oss-cn-beijing.aliyuncs.com/parse.png) 206 | 207 | 到这里就可以对当前问题作出答复了 208 | 209 | Mybatis 创建 **SqlSessionFactory** 会解析 **mybatis-config.xml**, 然后 **解析 configuration 标签下的子标签**, 解析 mappers 标签时, 会根据相关配置读取到 .xml 文件, 继而解析 .xml 中各个标签 210 | 211 | 具体的 select、insert、update、delete 标签定义为 **MappedStatement** 对象, .xml 文件中的其余标签也会根据不同映射解析为 Java 对象 212 | 213 | #### MappedStatement 214 | 215 | 这里重点说明下 MappedStatement 对象, 一起看一下类中的属性和 SQL 有何关联呢 216 | 217 | ![MappedStatement](https://images-machen.oss-cn-beijing.aliyuncs.com/MappedStatement.png) 218 | 219 | MappedStatement 对象中 **提供的属性与 .xml 文件中定义的 SQL 语句** 是能够对应上的, 用来 **控制每条 SQL 语句的执行行为** 220 | 221 | ### 5.2 Mapper 接口的存储与实现 222 | 223 | 在平常我们写的 SSM 框架中, 定义了 Mapper 接口与 .xml 对应的 SQL 文件, 在 Service 层直接注入 xxxMapper 就可以了 224 | 225 | 也没有看到像 JDBC 操作数据库的操作, Mybatis 在中间是如何为我们省略下这些重复繁琐的操作呢 226 | 227 | 这里使用 Mybatis 源码中的测试类进行验证, 首先定义 Mapper 接口, 省事直接注解定义 SQL 228 | 229 | ![AutoConstructorMapper](https://images-machen.oss-cn-beijing.aliyuncs.com/AutoConstructorMapper_new.png) 230 | 231 | 这里使用 SqlSession 来获取 Mapper 操作数据库, 测试方法如下 232 | 233 | ![primitiveSubjects](https://images-machen.oss-cn-beijing.aliyuncs.com/primitiveSubjects.png) 234 | 235 | #### 创建 SqlSession 236 | 237 | #1 从 SqlSessionFactory 中打开一个 新的 SqlSession 238 | 239 | #### 获取 Mapper 实例 240 | 241 | #2 就存在一个疑问点, 定义的 AutoConstructorMapper 明明是个接口, **为什么可以实例化为对象?** 242 | 243 | #### 动态代理方法调用 244 | 245 | #3 通过创建的对象调用类中具体的方法, 这里具体聊一下 #2 操作 246 | 247 | SqlSession 是一个接口, 有一个 **默认的实现类 DefaultSqlSession**, 类中包含了 Configuration 属性 248 | 249 | Mapper 接口的信息以及 .xml 中 SQL 语句是在 Mybatis **初始化时添加** 到 Configuration 的 **MapperRegistry** 属性中的 250 | 251 | ![MapperRegistry#addMapper](https://images-machen.oss-cn-beijing.aliyuncs.com/addMapper.png) 252 | 253 | #2 中的 getMapper 就是从 MapperRegistry 中获取 Mapper 254 | 255 | 看一下 MapperRegistry 的类属性都有什么 256 | 257 | ![MapperRegistry](https://images-machen.oss-cn-beijing.aliyuncs.com/MapperRegistry.png) 258 | 259 | config 为 **保持全局唯一** 的 Configuration 对象引用 260 | 261 | **knownMappers** 中 Key-Class 是 Mapper 对象, Value-MapperProxyFactory 是通过 Mapper 对象衍生出的 **Mapper 代理工厂** 262 | 263 | 再看一下 MapperProxyFactory 类的结构信息 264 | 265 | ![MapperProxyFactory](https://images-machen.oss-cn-beijing.aliyuncs.com/MapperProxyFactory.png) 266 | 267 | mapperInterface 属性是 Mapper 对象的引用, methodCache 的 key 是 Mapper 中的方法, value 是 Mapper 解析对应 SQL 产生的 MapperMethod 268 | 269 | > 📖 Mybatis 设计 methodCache 属性时使用到了 **懒加载机制**, 在初始化时不会增加对应 Method, 而是在 **第一次调用时新增** 270 | 271 | ![cachedMapperMethod](https://images-machen.oss-cn-beijing.aliyuncs.com/cachedMapperMethod.png) 272 | 273 | MapperMethod 运行时数据如下, 比较容易理解 274 | 275 | ![MapperMethod 运行状态](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200902133450941.png) 276 | 277 | 278 | 279 | 通过一个实际例子帮忙理解一下 MapperRegistry 类关系, Mapper 初始化第一次调用的对象状态, 可以看到 methodCache 容量为0 280 | 281 | ![MapperRegistry 运行状态](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200821191913092.png) 282 | 283 | 我们目前已经知道 MapperRegistry 的类关系, 回头继续看一下第二步的 **MapperRegistry#getMapper**() 处理步骤 284 | 285 | ![getMapper](https://images-machen.oss-cn-beijing.aliyuncs.com/getMapper.png) 286 | 287 | 核心处理在 MapperProxyFactory#newInstance() 方法中, 继续跟进 288 | 289 | ![newInstance](https://images-machen.oss-cn-beijing.aliyuncs.com/newInstance.png) 290 | 291 | MapperProxy **继承了 InvocationHandler 接口**, 通过 newInstance() 最终返回的是由 **Java Proxy 动态代理返回的动态代理实现类** 292 | 293 | 看到这里就清楚了步骤二中接口为什么能够被实例化, 返回的是 **接口的动态代理实现类** 294 | 295 | ### 5.3 Mybatis Sql 的执行过程 296 | 297 | 根据 Mybatis SQL 执行流程图进一步了解 298 | 299 | ![Mybatis-SQL执行流程](https://images-machen.oss-cn-beijing.aliyuncs.com/Mybatis-SQL执行流程.png) 300 | 301 | 大致可以分为以下几步操作: 302 | 303 | > 📖 在前面的内容中, 知道了 Mybatis Mapper 是动态代理的实现, 查看 SQL 执行过程, 就需要紧跟实现了 InvocationHandler 的 MapperProxy 类 304 | 305 | #### 执行增删改查 306 | 307 | ```java 308 | @Select(" SELECT * FROM SUBJECT WHERE ID = #{id}") 309 | PrimitiveSubject getSubject(@Param("id") final int id); 310 | ``` 311 | 312 | 我们以上述方法举例, 调用方通过 SqlSession 获取 Mapper 动态代理对象, 执行 Mapper 方法时会通过 **InvocationHandler 进行代理** 313 | 314 | ![MapperProxy](https://images-machen.oss-cn-beijing.aliyuncs.com/MapperProxy.png) 315 | 316 | 在 MapperMethod#execute 中, 根据 MapperMethod -> SqlCommand -> **SqlCommandType** 来确定增、删、改、查方法 317 | 318 | > 📖 SqlCommandType 是一个枚举类型, 对应五种类型 UNKNOWN、INSERT、UPDATE、DELETE、SELECT、FLUSH 319 | 320 | ![execute](https://images-machen.oss-cn-beijing.aliyuncs.com/execute.png) 321 | 322 | #### 参数处理 323 | 324 | 查询操作对应 SELECT 枚举值, if else 中判断为返回值是否集合、无返回值、单条查询等, 这里以查询单条记录作为入口 325 | 326 | ```java 327 | Object param = method.convertArgsToSqlCommandParam(args); 328 | result = sqlSession.selectOne(command.getName(), param); 329 | ``` 330 | 331 | ![convertArgsToSqlCommandParam_new](https://images-machen.oss-cn-beijing.aliyuncs.com/convertArgsToSqlCommandParam_new.png) 332 | 333 | ![参数解析](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200902124629730.png) 334 | 335 | > 📖 这里能够解释一个之前困扰我的问题, 那就是为什么方法入参只有单个 `@Param("id")`, 但是参数 param 对象会存在两个键值对 336 | 337 | 继续查看 **SqlSession#selectOne** 方法, sqlSession 是一个接口, 具体还是要看实现类 **DefaultSqlSession** 338 | 339 | ![selectOne](https://images-machen.oss-cn-beijing.aliyuncs.com/selectOne.png) 340 | 341 | 因为单条和查询多条以及分页查询都是走的一个方法, 所以在查询的过程中, 会将分页的参数进行添加 342 | 343 | ![selectList](https://images-machen.oss-cn-beijing.aliyuncs.com/selectList_new.png) 344 | 345 | #### 执行器处理 346 | 347 | 在 Mybatis 源码中, 创建的执行器默认是 **CachingExecutor,** 使用了装饰者模式, 在类中保持了 **Executor** 接口的引用, **CachingExecutor** 在持有的执行器基础上增加了缓存的功能 348 | 349 | ![CachingExecutor#query](https://images-machen.oss-cn-beijing.aliyuncs.com/CachingExecutor-query-new.png) 350 | 351 | **delegate.query** 就是在具体的执行器了, 默认 **SimpleExecutor,** query 方法统一在抽象父类 **BaseExecutor** 中维护 352 | 353 | ![BaseExecutor#query](https://images-machen.oss-cn-beijing.aliyuncs.com/BaseExecutor_query.png) 354 | 355 | **BaseExecutor#queryFromDatabase** 方法执行了缓存占位符以及执行具体方法, 并将查询返回数据添加至缓存 356 | 357 | ![queryFromDatabase](https://images-machen.oss-cn-beijing.aliyuncs.com/queryFromDatabase.png) 358 | 359 | **BaseExecutor#doQuery** 方法是由具体的 SimpleExecutor 实现 360 | 361 | ![doQuery](https://images-machen.oss-cn-beijing.aliyuncs.com/doQuery.png) 362 | 363 | #### 执行 SQL 364 | 365 | 因为我们 SQL 中使用了参数占位符, 使用的是 **PreparedStatementHandler** 对象, 执行预编译SQL的 Handler, 实际使用 **PreparedStatement** 进行 SQL 调用 366 | 367 | ![PreparedStatementHandler_query](https://images-machen.oss-cn-beijing.aliyuncs.com/PreparedStatementHandler_query.png) 368 | 369 | #### 返回数据解析 370 | 371 | 将 JDBC 返回类型转换为 Java 类型, 根据 resultSets 和 resultMap 进行转换 372 | 373 | ![handleResultSets](https://images-machen.oss-cn-beijing.aliyuncs.com/handleResultSets.png) 374 | 375 | ### 5.4 Mybatis 中分页如何实现 376 | 377 | 通过 Mybatis 执行分页 SQL 有两种实现方式, 一种是编写 SQL 时添加 LIMIT, 一种是全局处理 378 | 379 | #### SQL 分页 380 | 381 | ```sql 382 | 385 | ``` 386 | 387 | #### 拦截器分页 388 | 389 | 上文说到, Mybatis 支持了插件扩展机制, 可以拦截到具体对象的方法以及对应入参级别 390 | 391 | 我们添加插件时需要实现 **Interceptor** 接口, 然后将插件写在 mybatis-config.xml 配置文件中或者添加相关注解, Mybatis 初始化时解析才能在项目启动时添加到插件容器中 392 | 393 | ![pluginElement](https://images-machen.oss-cn-beijing.aliyuncs.com/pluginElement.png) 394 | 395 | 由一个 List 结构存储项目中全部拦截器, 通过 **Configuration#addInterceptor** 方法添加 396 | 397 | ![InterceptorChain](https://images-machen.oss-cn-beijing.aliyuncs.com/InterceptorChain.png) 398 | 399 | 重点需要关注 **Interceptor#pluginAll** 中 plugin 方法, Interceptor 只是一个接口, plugin 方法只能由其实现类完成 400 | 401 | ![ExamplePlugin](https://images-machen.oss-cn-beijing.aliyuncs.com/ExamplePlugin.png) 402 | 403 | Plugin 可以理解为是一个工具类, **Plugin#wrap** 返回的是一个动态代理类  404 | 405 | ![wrap](https://images-machen.oss-cn-beijing.aliyuncs.com/wrap.png) 406 | 407 | 这里使用一个测试的 Demo 看一下方法运行时的参数 408 | 409 | ![AlwaysMapPlugin](https://images-machen.oss-cn-beijing.aliyuncs.com/AlwaysMapPlugin.png) 410 | 411 | 虽然是随便写的 Demo, 但是与正式使用的插件并无实际区别 412 | 413 | ![插件运行状态](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20200902103355292.png) 414 | 415 | ## 6、相关链接 416 | 417 | [源码下载地址](https://github.com/Mybatis/Mybatis-3 "源码下载地址") 418 | 419 | [代码生成图片](https://carbon.now.sh/) 420 | 421 | [多功能在线Markdown编辑器](https://www.mdnice.com/) 422 | -------------------------------------------------------------------------------- /docs/sourcecode/心态崩了呀!Mybatis动态代理这么玩?!.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 假如有人问你这么几个问题,看能不能答上来 4 | 5 | 1. Mybatis Mapper 接口没有实现类,怎么实现的 SQL 查询 6 | 2. JDK 动态代理为什么不能对类进行代理(充话费送的问题) 7 | 3. 抽象类可不可以进行 JDK 动态代理(附加问题) 8 | 9 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/你说啥呢-表情包.jpeg?x-oss-process=image/resize,h_200,w_200) 10 | 11 | 答不上来的铁汁,证明 Proxy、Mybatis 源码还没看到位。不过没有关系,继续往下看就明白了 12 | 13 | ## 动态代理实战 14 | 15 | 众所周知哈,Mybatis 底层封装使用的 JDK 动态代理。说 Mybatis 动态代理之前,先来看一下平常我们写的动态代理 Demo,抛砖引玉 16 | 17 | 一般来说定义 JDK 动态代理分为三个步骤,如下所示 18 | 19 | 1. 定义代理接口 20 | 2. 定义代理接口实现类 21 | 3. 定义动态代理调用处理器 22 | 23 | 三步代码如下所示,玩过动态代理的小伙伴看过就能明白 24 | 25 | ```java 26 | public interface Subject { // 定义代理接口 27 | String sayHello(); 28 | } 29 | 30 | public class SubjectImpl implements Subject { // 定义代理接口实现类 31 | @Override 32 | public String sayHello() { 33 | System.out.println(" Hello World"); 34 | return "success"; 35 | } 36 | } 37 | 38 | public class ProxyInvocationHandler implements InvocationHandler { // 定义动态代理调用处理器 39 | private Object target; 40 | 41 | public ProxyInvocationHandler(Object target) { 42 | this.target = target; 43 | } 44 | 45 | @Override 46 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 47 | System.out.println(" 🧱 🧱 🧱 进入代理调用处理器 "); 48 | return method.invoke(target, args); 49 | } 50 | } 51 | ``` 52 | 53 | 写个测试程序,运行一下看看效果,同样是分三步 54 | 55 | 1. 创建被代理接口的实现类 56 | 2. 创建动态代理类,说一下三个参数 57 | - 类加载器 58 | - 被代理类所实现的接口数组 59 | - 调用处理器(调用被代理类方法,每次都经过它) 60 | 3. 被代理实现类调用方法 61 | 62 | ```java 63 | public class ProxyTest { 64 | public static void main(String[] args) { 65 | Subject subject = new SubjectImpl(); 66 | Subject proxy = (Subject) Proxy 67 | .newProxyInstance( 68 | subject.getClass().getClassLoader(), 69 | subject.getClass().getInterfaces(), 70 | new ProxyInvocationHandler(subject)); 71 | 72 | proxy.sayHello(); 73 | /** 74 | * 打印输出如下 75 | * 调用处理器:🧱 🧱 🧱 进入代理调用处理器 76 | * 被代理实现类:Hello World 77 | */ 78 | } 79 | } 80 | ``` 81 | 82 | Demo 功能实现了,大致运行流程也清楚了,下面要针对原理实现展开分析 83 | 84 | ## 动态代理原理分析 85 | 86 | 从原理的角度上解析一下,上面动态代理测试程序是如何执行的 87 | 88 | 第一步简单明了,**创建了 Subject 接口的实现类**,也是我们常规的实现 89 | 90 | 第二步是创建被代理对象的动态代理对象。这里有朋友就问了,怎么证明这是个动态代理对象?如图所示 91 | 92 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210111133425968.png) 93 | 94 | JDK 动态代理对象名称是有规则的,凡是经过 Proxy 类生成的动态代理对象,前缀必然是 **\$Proxy**,后面的数字也是名称组成部分 95 | 96 | 如果有小伙伴想要一探究竟,**关注 Proxy 内部类 ProxyClassFactory**,这里会有想要的答案 97 | 98 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210111134232414.png) 99 | 100 | 回归正题,继续看一下 ProxyInvocationHandler,**内部保持了被代理接口实现类的引用**,invoke 方法内部使用反射调用被代理接口实现类方法 101 | 102 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112101502099.png) 103 | 104 | 可以看出生成的动态代理类,继承了 Proxy 类,然后对 Subject 接口进行了实现,而实现方法 sayHello 中实际调用的是 ProxyInvocationHandler 的 invoke 方法 105 | 106 | > 一不小心发现了 JDK 动态代理不能对类进行代理的原因 ^ ^ 107 | 108 | 也就是说,当我们调用 `Subject#sayHello` 时,方法调用链是这样的 109 | 110 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210111140427269.png) 111 | 112 | 但是,Demo 里有被代理接口的实现类,Mybatis Mapper 没有,这要怎么玩 113 | 114 | 不知道不要紧,知道了估计也看不到这了,一起看下 mybatis 源码是怎么玩的 115 | 116 | > mybatis version:3.4.x 117 | 118 | ## Mybatis 源码实现 119 | 120 | 不知道大家考没考虑过这么一个问题,**Mapper Mapper 为什么不需要实现类?** 121 | 122 | 假如说,我们项目使用的三层设计,Controller 控制请求接收,Service 负责业务处理,Mapper 负责数据库交互 123 | 124 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210111153124692.png) 125 | 126 | Mapper 层也就是我们常说的数据库映射层,负责对数据库的操作,比如对数据的查询或者新增、删除等 127 | 128 | 大胆设想下,项目没有使用 Mybatis,需要在 Mapper 实现层写数据库交互,会写一些什么内容? 129 | 130 | 会写一些常规的 JDBC 操作,比如: 131 | 132 | ```java 133 | // 装载Mysql驱动 134 | Class.forName(driveName); 135 | // 获取连接 136 | con = DriverManager.getConnection(url, user, pass); 137 | // 创建Statement 138 | Statement state = con.createStatement(); 139 | // 构建SQL语句 140 | String stuQuerySqlStr = "SELECT * FROM student"; 141 | // 执行SQL返回结果 142 | ResultSet result = state.executeQuery(stuQuerySqlStr); 143 | ... 144 | ``` 145 | 146 | 如果项目中所有 Mapper 实现层都要这么玩,那岂不是很想打人... 147 | 148 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/没意思.png?x-oss-process=image/resize,h_200,w_200) 149 | 150 | 所以 Mybatis 结合项目痛点,应运而生,怎么做的呢 151 | 152 | 1. 将所有和 JDBC 交互的操作,底层采用 JDK 动态代理封装,使用者只需要自定义 Mapper 和 .xml 文件 153 | 2. SQL 语句定义在 .xml 文件或者 Mapper 中,项目启动时通过解析器解析 SQL 语句组装为 Java 中的对象 154 | 155 | > 解析器分为多种,因为 Mybatis 中不仅有静态语句,同时也包含动态 SQL 语句 156 | 157 | 这也就是为什么 Mapper 接口不需要实现类,**因为都已经被 Mybatis 通过动态代理封装了,如果每个 Mapper 都来一个实现类,臃肿且无用**。经过这一顿操作,展示给我们的就是项目里用到的 Mybatis 框架 158 | 159 | 上面铺垫这么久,终于要到主角了,**为什么 Mybatis Mapper 接口没有实现类也可以实现动态代理** 160 | 161 | > 想要严格按照先后顺序介绍 Mybatis 动态代理流程,而不超前引用未介绍过的术语,这几乎是不可能的,笔者尽量说的通俗易懂 162 | 163 | ## 无实现类完成动态代理 164 | 165 | 核心点来了,拿起小本本坐板正了 166 | 167 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/快闪开,他要开始装逼了.jpg?x-oss-process=image/resize,h_300,w_300) 168 | 169 | 我们先来看下普通动态代理有没有可能不用实现类,仅靠接口完成 170 | 171 | ```java 172 | public interface Subject { 173 | String sayHello(); 174 | } 175 | 176 | public class ProxyInvocationHandler implements InvocationHandler { 177 | 178 | @Override 179 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 180 | System.out.println(" 🧱 🧱 🧱 进入代理调用处理器 "); 181 | return "success"; 182 | } 183 | } 184 | ``` 185 | 186 | 根据代码可以看到,我们并没有实现接口 Subject,继续看一下怎么实现动态代理 187 | 188 | ```java 189 | public class ProxyTest { 190 | public static void main(String[] args) { 191 | Subject proxy = (Subject) Proxy 192 | .newProxyInstance( 193 | subject.getClass().getClassLoader(), 194 | new Class[]{Subject.class}, 195 | new ProxyInvocationHandler()); 196 | 197 | proxy.sayHello(); 198 | /** 199 | * 打印输出如下 200 | * 调用处理器:🧱 🧱 🧱 进入代理调用处理器 201 | */ 202 | } 203 | } 204 | ``` 205 | 206 | 可以看到,对比文初的 Demo,这里对 `Proxy.newProxyInstance` 方法的参数作出了变化 207 | 208 | 之前是通过实现类获取所实现接口的 Class 数组,而这里是把接口本身放到 Class 数组中,殊归同途 209 | 210 | 有实现接口和无实现接口产生的动态代理类有什么区别 211 | 212 | 1. 有实现接口是对 `InvocationHandler#invoke` 方法调用,invoke 方法通过反射调用被代理对象(SubjectImpl)方法(sayHello) 213 | 2. 无实现接口则是仅对 `InvocationHandler#invoke` 产生调用。**所以有接口实现返回的是被代理对象接口返回值,而无实现接口返回的仅是 invoke 方法返回值** 214 | 215 | `InvocationHandler#invoke` 方法返回值是 success 字符串,定义个字符串变量,是否能成功返回 216 | 217 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112222532385.png) 218 | 219 | 现在第一个问题答案已经浮现,**Mapper 没有实现类,所有调用 JDBC 等操作都是在 Mybatis InvocationHandler 实现的** 220 | 221 | 问题既然已经得到了解决,给人一种感觉,好像没那么难,但是你不好奇,Mybatis 底层怎么做的么? 222 | 223 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/难搞啊.jpg) 224 | 225 | 先抛出一个问题,然后带着问题去看源码,可能让你记忆 Double 倍深刻 226 | 227 | 咱们 Demo 里的接口是固定的,Mybatis Mapper 可是不固定的,怎么搞? 228 | 229 | 230 | 231 | **Mybatis 是这么说的** 232 | 233 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/我不觉得这是个问题.jpeg?x-oss-process=image/resize,h_300,w_300) 234 | 235 | 看看 Mybatis 底层它怎么实现的动态接口代理,小伙伴只需要关注标记处的代码即可 236 | 237 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112130314572.png) 238 | 239 | 和我们的 Demo 代码很像,核心点在于 **mapperInterface** 它是怎么赋值的 240 | 241 | 先来说一下 Mybatis 代理工厂中具体生成动态代理类具体逻辑 242 | 243 | 1. 根据 .xml 上关联的 namespace, 通过 `Class#forName` 反射的方式返回 Class 对象(不止 .xml namespace 一种方式) 244 | 2. 将得到的 Class 对象(实际就是接口对象)传递给 Mybatis 代理工厂生成代理对象,也就是刚才 mapperInterface 属性 245 | 246 | 谜底揭晓,Mybatis 使用接口全限定名通过 `Class#forName` 生成 Class 对象,这个 Class 对象类型就是接口 247 | 248 | 为了方便大家理解,通过 Mybatis 源码提供的测试类举例。假设已有接口 AutoConstructorMapper 以及对应的 .xml 如下 249 | 250 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112132212106.png) 251 | 252 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112132257008.png) 253 | 254 | 执行第一步,根据 .xml namespace 得到 Class 对象 255 | 256 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112132433980.png) 257 | 258 | 1. 首先第一步获取 .xml 上 mapper 标签 namespace 属性,得到 mapper 接口全限定信息 259 | 2. 根据 mapper 全限定信息获取 Class 对象 260 | 3. 添加到对应的映射器容器中,等待生成动态代理对象 261 | 262 | 如果此时调用生成动态代理对象,代理工厂 newInstance 方法如下: 263 | 264 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112131357019.png) 265 | 266 | 至此,文初提的 Proxy、Mybatis 动态代理相关问题已全部答疑 267 | 268 | ## 抽象类能否 JDK 动态代理 269 | 270 | 说代码前结论先行,**不能!** 271 | 272 | ```java 273 | public abstract class AbstractProxy { 274 | abstract void sayHello(); 275 | } 276 | 277 | AbstractProxy proxyInterface = (AbstractProxy) Proxy 278 | .newProxyInstance( 279 | ProxyTest.class.getClassLoader(), 280 | new Class[]{AbstractProxy.class}, 281 | new ProxyInvocationHandler()); 282 | proxyInterface.sayHello(); 283 | ``` 284 | 285 | 毫无疑问,报错是必然的,JDK 是不能对类进行代理的 286 | 287 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112133350848.png) 288 | 289 | 带着小疑惑我们看一下 Proxy 源码报错位置,JDK 动态代理在生成代理类的过程代码中,会有是否接口验证 290 | 291 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210112133513069.png) 292 | 293 | 抽象类终归是类,加个 abstract 也成不了接口(就像我,虽然胖了 60 斤,但依然是帅哥) 294 | 295 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/嘻嘻嘻.jpg) 296 | 297 | 下次面试官如果有问这问题的,**斩钉截铁一点**,就是不能 298 | 299 | ## 结言 300 | 301 | 结合 Mybatis 使用 JDK 动态代理相关的问题,展开了文章的讲述,这里总结下 302 | 303 | **Q:JDK 动态代理能否对类代理?** 304 | 305 | > 因为 JDK 动态代理生成的代理类,会继承 Proxy 类,由于 Java 无法多继承,所以无法对类进行代理 306 | 307 | **Q:抽象类是否可以 JDK 动态代理?** 308 | 309 | > 不可以,抽象类本质上也是类,Proxy 生成代理类过程中,会校验传入 Class 是否接口 310 | 311 | **Q:Mybatis Mapper 接口没有实现类,怎么实现的动态代理?** 312 | 313 | > Mybatis 会通过 `Class#forname` 得到 Mapper 接口 Class 对象,生成对应的动态代理对象,核心业务处理都会在 `InvocationHandler#invoke` 进行处理 314 | 315 | 316 | 317 | 希望读过的小伙伴都能有所收获,如果对于文章内容有所疑惑,可以通过留言或者添加作者好友的方式沟通,祝好! 318 | 319 | -------------------------------------------------------------------------------- /docs/sourcecode/花一个周末,掌握OpenFeign核心原理.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 现在的微服务在互联网圈子里应用已经相关广泛了,SpringCloud 是微服务领域当之无愧的 "头牌" 4 | 5 | 加上现在的一些轮子项目,新建一个全套的 SpringCloud 项目分分钟的事情,而我们要做的事情,就是不把认知停留在使用层面,所以要深入到源码中去理解 SpringCloud 6 | 7 | **为什么要选择 OpenFien?** 因为它足够的 "小",符合我们的标题:**一个周末搞定** 8 | 9 | Feign 的源代码中,Java 代码才 3w 多行,放眼现在热门的开源项目,包括不限于 Dubbo、Naocs、Skywalking 中 Java 代码都要 30w 行起步 10 | 11 | 通过本篇文章,希望读者朋友可以掌握如下知识 12 | 13 | - 什么是 Feign 14 | - Feign 和 Openfeign 的区别 15 | - OpenFeign 的启动原理 16 | - OpenFeign 的工作原理 17 | - OpenFeign 如何负载均衡 18 | 19 | > spring-cloud-starter-openfeign version:2.2.6.RELEASE 20 | 21 | ## 什么是 Feign 22 | 23 | Feign 是声明式 Web 服务客户端,它使编写 Web 服务客户端更加容易 24 | 25 | Feign 不做任何请求处理,通过处理注解相关信息生成 Request,并对调用返回的数据进行解码,从而实现 **简化 HTTP API 的开发** 26 | 27 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117103547767.png) 28 | 29 | 如果要使用 Feign,需要创建一个接口并对其添加 Feign 相关注解,另外 Feign **还支持可插拔编码器和解码器**,致力于打造一个轻量级 HTTP 客户端 30 | 31 | ## Feign 和 Openfeign 的区别 32 | 33 | Feign 最早是由 **Netflix 公司进行维护的**,后来 Netflix 不再对其进行维护,最终 **Feign 由社区进行维护**,更名为 Openfeign 34 | 35 | > 为了少打俩字,下文简称 Opefeign 为 Feign 36 | 37 | 并将原项目迁移至新的仓库,所以我们在 Github 上看到 Feign 的坐标如下 38 | 39 | ```xml 40 | io.github.openfeign 41 | parent 42 | ... 43 | ``` 44 | 45 | ### Starter Openfeign 46 | 47 | 当然了,基于 SpringCloud 团队对 Netflix 的情有独钟,你出了这么好用的轻量级 HTTP 客户端,我这老大哥不得支持一下,所以就有了基于 Feign 封装的 Starter 48 | 49 | ```xml 50 | 51 | org.springframework.cloud 52 | spring-cloud-starter-openfeign 53 | 54 | ``` 55 | 56 | Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用 Spring Web 中默认使用的相同 HttpMessageConverters 57 | 58 | 另外,Spring Cloud 老大哥同时集成了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 HTTP 客户端 59 | 60 | > 针对于注册中心的支持,包含但不限于 Eureka,比如 Consul、Naocs 等注册中心均支持 61 | 62 | 在我们 SpringCloud 项目开发过程中,使用的大多都是这个 Starter Feign 63 | 64 | ## 环境准备 65 | 66 | 为了方便大家理解,这里写出对应的生产方、消费方 Demo 代码,以及使用的注册中心 67 | 68 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117200427532.png) 69 | 70 | 注册中心使用的 Nacos,生产、消费方代码都比较简单。另外为了阅读体验感,**文章原则是少放源码**,更多的是给大家梳理核心逻辑 71 | 72 | ### 生产者服务 73 | 74 | 添加 Nacos 服务注册发现注解以及发布出 HTTP 接口服务 75 | 76 | ```java 77 | @EnableDiscoveryClient @SpringBootApplication 78 | public class NacosProduceApplication { 79 | public static void main(String[] args) { 80 | SpringApplication.run(NacosProduceApplication.class, args); 81 | } 82 | @RestController 83 | static class TestController { 84 | @GetMapping("/hello") 85 | public String hello(@RequestParam("name") String name) { 86 | return "hello " + name; 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ### 消费者服务 93 | 94 | 定义 FeignClient 消费服务接口 95 | 96 | ```java 97 | @FeignClient(value = "nacos-produce") 98 | public interface DemoFeignClient { 99 | @RequestMapping(value = "/hello", method = RequestMethod.GET) 100 | String sayHello(@RequestParam("name") String name); 101 | } 102 | ``` 103 | 104 | 因为生产者使用 Nacos,所以消费者除了开启 Feign 注解,同时也要开启 Naocs 服务注册发现 105 | 106 | ```java 107 | @RestController @EnableFeignClients 108 | @EnableDiscoveryClient @SpringBootApplication 109 | public class NacosConsumeApplication { 110 | public static void main(String[] args) { 111 | SpringApplication.run(NacosConsumeApplication.class, args); 112 | } 113 | 114 | @Autowired private DemoFeignClient demoFeignClient; 115 | 116 | @GetMapping("/test") 117 | public String test() { 118 | String result = demoFeignClient.sayHello("公号-源码兴趣圈"); 119 | return result; 120 | } 121 | } 122 | ``` 123 | 124 | ## Feign 的启动原理 125 | 126 | 我们在 SpringCloud 的使用过程中,如果想要启动某个组件,一般都是 **@Enable...** 这种方式注入,Feign 也不例外,我们需要在类上标记此注解 `@EnableFeignClients` 127 | 128 | ```java 129 | @EnableFeignClients 130 | @SpringBootApplication 131 | public class Application { 132 | public static void main(String[] args) { 133 | SpringApplication.run(Application.class, args); 134 | } 135 | } 136 | ``` 137 | 138 | 继续深入看一下注解内部都做了什么。注解内部的方法就不说明了,不加会有默认的配置,感兴趣可以跟下源码 139 | 140 | ```java 141 | @Retention(RetentionPolicy.RUNTIME) 142 | @Target(ElementType.TYPE) 143 | @Documented 144 | @Import(FeignClientsRegistrar.class) 145 | public @interface EnableFeignClients {...} 146 | ``` 147 | 148 | 前三个注解看着平平无奇,重点在第四个 @Import 上,一般使用此注解都是想要动态注册 Spring Bean 的 149 | 150 | ### 注入@Import 151 | 152 | 通过名字也可以大致猜出来,这是 Feign 注册 Bean 使用的,使用到了 Spring 相关的接口,一起看下起了什么作用 153 | 154 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210116215150734.png) 155 | 156 | ResourceLoaderAware、EnvironmentAware 为 FeignClientsRegistrar 中两个属性 **resourceLoader、environment** 赋值,对 Spring 了解的小伙伴理解问题不大 157 | 158 | ImportBeanDefinitionRegistrar 负责动态注入 IOC Bean,分别注入 Feign 配置类、FeignClient Bean 159 | 160 | ```java 161 | // 资源加载器,可以加载 classpath 下的所有文件 162 | private ResourceLoader resourceLoader; 163 | // 上下文,可通过该环境获取当前应用配置属性等 164 | private Environment environment; 165 | 166 | @Override 167 | public void setEnvironment(Environment environment) { 168 | this.environment = environment; 169 | } 170 | 171 | @Override 172 | public void setResourceLoader(ResourceLoader resourceLoader) { 173 | this.resourceLoader = resourceLoader; 174 | } 175 | 176 | @Override 177 | public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 178 | // 注册 @EnableFeignClients 提供的自定义配置类中的相关 Bean 实例 179 | registerDefaultConfiguration(metadata,registry); 180 | // 扫描 packge,注册被 @FeignClient 修饰的接口类为 IOC Bean 181 | registerFeignClients(metadata, registry); 182 | } 183 | ``` 184 | 185 | ### 添加全局配置 186 | 187 | registerDefaultConfiguration 方法流程如下 188 | 189 | 1. 获取 @EnableFeignClients 注解上的属性以及对应 Value 190 | 2. 生成 **FeignClientSpecification**(存储 Feign 中的配置类) 对应的构造器 BeanDefinitionBuilder 191 | 3. FeignClientSpecification Bean 名称为 default. + @EnableFeignClients 修饰类全限定名称 + FeignClientSpecification 192 | 4. @EnableFeignClients defaultConfiguration 默认为 {},如果没有相关配置,`默认使用 FeignClientsConfiguration` 并结合 name 填充到 FeignClientSpecification,最终注册为 IOC Bean 193 | 194 | ### 注册 FeignClient 接口 195 | 196 | 将重点放在 registerFeignClients 上,该方法主要就是将修饰了 @FeignClient 的接口注册为 IOC Bean 197 | 198 | 1. 扫描 @EnableFeignClients 注解,如果有 clients,则加载指定接口,为空则根据 scanner 规则扫描出修饰了 @FeignClient 的接口 199 | 2. 获取 @FeignClient 上对应的属性,根据 configuration 属性去创建接口级的 **FeignClientSpecification** 配置类 IOC Bean 200 | 3. 将 @FeignClient 的属性设置到 **FeignClientFactoryBean** 对象上,并注册 IOC Bean 201 | 202 | @FengnClient 修饰的接口实际上使用了 Spring 的代理工厂生成代理类,所以这里会把修饰了 @FeignClient 接口的 BeanDefinition 设置为 FeignClientFactoryBean 类型,而 **FeignClientFactoryBean 继承自 FactoryBean** 203 | 204 | 也就是说,当我们定义 @FeignClient 修饰接口时,注册到 IOC 容器中 Bean 类型变成了 FeignClientFactoryBean 205 | 206 | 在 Spring 中,FactoryBean 是一个工厂 Bean,用来创建代理 Bean。**工厂 Bean 是一种特殊的 Bean**,对于需要获取 Bean 的消费者而言,它是不知道 Bean 是普通 Bean 或是工厂 Bean 的。**工厂 Bean 返回的实例不是工厂 Bean 本身**,而是会返回执行了工厂 Bean 中 `FactoryBean#getObject` 逻辑的实例 207 | 208 | ## Feign 的工作原理 209 | 210 | 说 Feign 的工作原理,核心点围绕在被 @FeignClient 修饰的接口,如何发送及接收 HTTP 网络请求 211 | 212 | 上面说到 @FeignClient 修饰的接口最终填充到 IOC 容器的类型是 FeignClientFactoryBean,先来看下它是什么 213 | 214 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117084610770.png) 215 | 216 | ### FactoryBean 接口特征 217 | 218 | 这里说一下 FeignClientFactoryBean 都有哪些特征 219 | 220 | 1. 它会在类初始化时执行一段逻辑,依据 Spring **InitializingBean** 接口 221 | 2. 如果它被别的类 @Autowired 进行注入,返回的不是它本身,而是 `FactoryBean#getObject` 返回的类,依据 Spring **FactoryBean** 接口 222 | 3. 它能够获取 Spring 上下文对象,依据 Spring **ApplicationContextAware** 接口 223 | 224 | 先来看它的初始化逻辑都执行了什么 225 | 226 | ```java 227 | @Override 228 | public void afterPropertiesSet() { 229 | Assert.hasText(contextId, "Context id must be set"); 230 | Assert.hasText(name, "Name must be set"); 231 | } 232 | ``` 233 | 234 | 没有特别的操作,只是使用断言工具类判断两个字段不为空。ApplicationContextAware 也没什么说的,获取上下文对象赋值到对象的局部变量里,重点以及关键就是 `FactoryBean#getObject` 方法 235 | 236 | ```java 237 | @Override 238 | public Object getObject() throws Exception { 239 | return getTarget(); 240 | } 241 | ``` 242 | 243 | getTarget 源码方法还是挺长的,这里采用分段的形式展示 244 | 245 | ```java 246 | T getTarget() { 247 | // 从 IOC 容器获取 FeignContext 248 | FeignContext context = applicationContext.getBean(FeignContext.class); 249 | // 通过 context 创建 Feign 构造器 250 | Feign.Builder builder = feign(context); 251 | ... 252 | } 253 | ``` 254 | 255 | 这里提出一个疑问?FeignContext 什么时候、在哪里被注入到 Spring 容器里的? 256 | 257 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117124204635.png) 258 | 259 | 看到图片小伙伴就明了了,用了 SpringBoot 怎么会不使用自动装配的功能呢,FeignContext 就是在 FeignAutoConfiguration 中被成功创建 260 | 261 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117135517855.png) 262 | 263 | ### 初始化父子容器 264 | 265 | feign 方法里日志工厂、编码、解码等类均是通过 get(...) 方法得到 266 | 267 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117140151921.png) 268 | 269 | 这里涉及到 Spring 父子容器的概念,**默认子容器 Map 为空**,获取不到服务名对应 Context 则新建 270 | 271 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117140635516.png) 272 | 273 | 从下图中看到,注册了一个 **FeignClientsConfiguration** 类型的 Bean,我们上述方法 feign 中的获取的编码、解码器等组件都是从此类中获取默认 274 | 275 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117141800121.png) 276 | 277 | 默认注册如下,FeignClientsConfiguration 是由创建 FeignContext 调用父类 Super 构造方法传入的 278 | 279 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117142442921.png) 280 | 281 | 关于父子类容器对应关系,以及提供 @FeignClient 服务对应子容器的关系(每一个服务对应一个子容器实例) 282 | 283 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117144335432.png) 284 | 285 | 回到 getInstance 方法,子容器此时已加载对应 Bean,直接通过 getBean 获取 **FeignLoggerFactory** 286 | 287 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117142639778.png) 288 | 289 | 如法炮制,Feign.Builder、Encoder、Decoder、Contract 都可以通过子容器获取对应 Bean 290 | 291 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117145054652.png) 292 | 293 | configureFeign 方法主要进行一些配置赋值,**比如超时、重试、404 配置等**,就不再细说赋值代码了 294 | 295 | 到这里有必要总结一下创建 Spring 代理工厂的前半场代码 296 | 297 | 1. 注入@FeignClient 服务时,其实注入的是 `FactoryBean#getObject` 返回代理工厂对象 298 | 2. 通过 IOC 容器获取 FeignContext 上下文 299 | 3. 创建 Feign.Builder 对象时会创建 Feign 服务对应的子容器 300 | 4. 从子容器中获取日志工厂、编码器、解码器等 Bean 301 | 5. 为 Feign.Builder 设置配置,比如超时时间、日志级别等属性,每一个服务都可以个性化设置 302 | 303 | ### 动态代理生成 304 | 305 | 继续嗑,上面都是开胃菜,接下来是最最最重要的地方了,小板凳坐板正了.. 306 | 307 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117160135560.png) 308 | 309 | 因为我们在 @FeignClient 注解是使用 name 而不是 url,所以会执行负载均衡策略的分支 310 | 311 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117165403532.png) 312 | 313 | Client: Feign 发送请求以及接收响应等都是由 Client 完成,该类默认 Client.Default,另外支持 HttpClient、OkHttp 等客户端 314 | 315 | 代码中的 Client、Targeter 在自动装配时注册,配合上文中的父子容器理论,这两个 Bean 在父容器中存在 316 | 317 | 因为我们并没有对 Hystix 进行设置,所以走入此分支 318 | 319 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117165809307.png) 320 | 321 | 创建反射类 ReflectiveFeign,然后执行创建实例类 322 | 323 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117170636104.png) 324 | 325 | newInstance 方法对 @FeignClient 修饰的接口中 SpringMvc 等配置进行解析转换,对接口类中的方法进行归类,生成动态代理类 326 | 327 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117180843295.png) 328 | 329 | 可以看出 Feign 创建动态代理类的方式和 Mybatis Mapper 处理方式是一致的,因为两者都没有实现类 330 | 331 | 根据 newInstance 方法按照行为大致划分,共做了四件事 332 | 333 | 1. 处理 @FeignCLient 注解(SpringMvc 注解等)封装为 **MethodHandler** 包装类 334 | 2. 遍历接口中所有方法,过滤 Object 方法,并将默认方法以及 FeignClient 方法分类 335 | 3. 创建动态代理对应的 **InvocationHandler** 并创建 Proxy 实例 336 | 4. 接口内 default 方法 **绑定动态代理类** 337 | 338 | MethodHandler 将方法参数、方法返回值、参数集合、请求类型、请求路径进行解析存储 339 | 340 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117181818113.png) 341 | 342 | 到这里我们也就可以 Feign 的工作方式了。前面那么多封装铺垫,封装个性化配置等等,最终确定收尾的是创建动态代理类 343 | 344 | 也就是说在我们调用 @FeignClient 接口时,会被 `FeignInvocationHandler#invoke` 拦截,并在动态代理方法中执行下述逻辑 345 | 346 | 1. 接口注解信息封装为 HTTP Request 347 | 2. 通过 Ribbon 获取服务列表,并对服务列表进行负载均衡调用(**服务名转换为 ip+port**) 348 | 3. 请求调用后,将返回的数据封装为 HTTP Response,继而转换为接口中的返回类型 349 | 350 | 既然已经明白了调用流程,那就正儿八经的试一哈,试过才知有没有... 351 | 352 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117185730122.png) 353 | 354 | RequestTemplate:构建 Request 模版类 355 | 356 | Options:存放连接、超时时间等配置类 357 | 358 | Retryer:失败重试策略类 359 | 360 | > 重试这一块逻辑看了很多遍,但是怎么看,一个 continue 关键字放到 while 的最后面都有点多余... 361 | 362 | 执行远端调用逻辑中使用到了 **Rxjava (响应式编程)**,可以看到通过底层获取 server 后将服务名称转变为 ip+port 的方式 363 | 364 | 这种响应式编程的方式在 SpringCloud 中很常见,**Hystix 源码底层也有使用** 365 | 366 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117192026553.png) 367 | 368 | 网络调用默认使用 **HttpURLConnection**,可以配置使用 HttpClient 或者 OkHttp 369 | 370 | 调用远端服务后,再将返回值解析正常返回,到这里一个完成的 Feign 调用链就聊明白了 371 | 372 | ![图片参考@疯狂创客圈](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117221147470.png) 373 | 374 | ## Feign 如何负载均衡 375 | 376 | 一般而言,我们生产者注册多个服务,消费者调用时需要使用负载均衡从中 **选取一个健康并且可用的生产者服务** 377 | 378 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117211229930.png) 379 | 380 | 因为 Feign 内部集成 Ribbon,所以也支持此特性,一起看下它是怎么做的 381 | 382 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117201548333.png) 383 | 384 | 我们在 Nacos 上注册了两个服务,端口号 8080、8081。在获取负载均衡器时就可以获取服务集合 385 | 386 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117202046064.png) 387 | 388 | 然后通过 chooseServer 方法选择一个健康实例返回,后面会新出一篇文章对 Ribbon 的负载均衡详细说明 389 | 390 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210117202719314.png) 391 | 392 | 通过返回的 Server 替换 URL 中的服务名,最后使用网络调用服务进行远端调用,完美的一匹 393 | 394 | ## 结语 395 | 396 | 文章从最基础的知识介绍什么是 Feign?继而从源码的角度上说明 Feign 的底层原理,总结如下: 397 | 398 | 1. 通过 @EnableFeignCleints 注解启动 Feign Starter 组件 399 | 2. Feign Starter 在项目启动过程中注册全局配置,扫描包下所有的 @FeignClient 接口类,并进行注册 IOC 容器 400 | 3. @FeignClient 接口类被注入时,通过 `FactoryBean#getObject` 返回动态代理类 401 | 4. 接口被调用时被动态代理类逻辑拦截,将 @FeignClient 请求信息通过编码器生成 Request 402 | 5. 交由 Ribbon 进行负载均衡,挑选出一个健康的 Server 实例 403 | 6. 继而通过 Client 携带 Request 调用远端服务返回请求响应 404 | 7. 通过解码器生成 Response 返回客户端,将信息流解析成为接口返回数据 405 | 406 | 虽然 Feign 体量相对小,但是想要一篇文章完全描述,也不太现实,所以这里都是挑一些核心点讲解,没有写到的地方还请见谅 407 | 408 | 另外,由于作者水平有限, 欢迎大家能够反馈指正文章中错误不正确的地方, 感谢 409 | 410 |
411 | 412 | **参考文章:** 413 | 414 | - https://blog.csdn.net/forezp/article/details/73480304 415 | - https://www.cnblogs.com/yangxiaohui227/p/12965340.html 416 | - https://www.cnblogs.com/crazymakercircle/p/11965726.html 417 | -------------------------------------------------------------------------------- /docs/sourcecode/花一个周末,掌握SpringCloud-Ribbon核心原理.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 继 SpringCloud Feign 之后的第二篇分布式框架文章,同样秉承单周末一个 SpringCloud 组件的大目标为原则 4 | 5 | 如果想看 Feign 的小伙伴,[猛戳这里](https://mp.weixin.qq.com/s/zYfhJDyHji58Skw3IqIWzg),Feign 核心原理与你不期而遇 6 | 7 | 在平常使用 SpringCloud 中,一般会使用 Feign,因为 Feign 内部集成了 Ribbon 8 | 9 | 但是 Ribbon 又是一个不可忽视的知识点,并且比 Feign 要难很多。列举文章大纲主题 10 | 11 | > 1. 如何获取注册中心服务实例 12 | > 2. 非健康服务实例如何下线 13 | > 3. Ribbon 底层原理实现 14 | > 4. 自定义 Ribbon 负载均衡策略 15 | 16 | 文章使用 SpringCloud Ribbon 源代码 Hoxton.SR9 版本:**2.2.6.RELEASE** 17 | 18 | _另外在文章结尾,说了一些看源码过程中的感想,以及 Ribbon 中笔者实现不合理的流程说明_ 19 | 20 | ## 概念小贴士 21 | 22 | ### 负载均衡 23 | 24 | 负载均衡是指通过负载均衡策略分配到多个执行单元上,常见的负载均衡方式有两种 25 | 26 | - 独立进程单元,通过负载均衡策略,将请求进行分发到不同执行上,类似于 Nginx 27 | - 客户端行为,将负载均衡的策略绑定到客户端上,客户端会维护一份服务提供者列表,通过客户端负载均衡策略分发到不同的服务提供者 28 | 29 | ### Ribbon 30 | 31 | Ribbon 是 Netflix 公司开源的一款负载均衡组件,负载均衡的行为在客户端发生,所以属于上述第二种 32 | 33 | 一般而言,SpringCloud 构建以及使用时,会使用 Ribbon 作为客户端负载均衡工具。但是不会独立使用,而是结合 RestTemplate 以及 Feign 使用,Feign 底层集成了 Ribbon,不用额外的配置,开箱即用 34 | 35 | 文章为了更贴切 Ribbon 主题,所以使用 RestTemplate 充当网络调用工具 36 | 37 | RestTemplate 是 Spring Web 下提供访问第三方 RESTFul Http 接口的网络框架 38 | 39 | ## 环境准备 40 | 41 | 注册中心选用阿里 Nacos,创建两个服务,生产者集群启动,消费者使用 RestTemplate + Ribbon 调用,调用总体结构如下 42 | 43 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210122182633581.png) 44 | 45 | 生产者代码如下,将服务注册 Nacos,并对外暴露 Http Get 服务 46 | 47 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210123112352094.png) 48 | 49 | 消费者代码如下,将服务注册 Nacos,通过 RestTemplate + Ribbon 发起远程负载均衡调用 50 | 51 | RestTemplate 默认是没有负载均衡的,所以需要添加 @LoadBalanced 52 | 53 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210123112532490.png) 54 | 55 | 启动三个生产者实例注册 Nacos,启动并且注册成功如下所示 56 | 57 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210123123103613.png) 58 | 59 | 想要按严格的先后顺序介绍框架原理,而不超前引用尚未介绍过的术语,**这几乎是不可能的**,笔者尽可能介绍明白 60 | 61 | ## 如何获取注册中心服务实例 62 | 63 | 先来看一下 Ribbon 是如何在客户端获取到注册中心运行实例的,这个点在之前是我比较疑惑的内容 64 | 65 | > 服务注册相关的知识点,会放到 Nacos 源码解析说明 66 | 67 | 先来举个例子,当我们执行一个请求时,肯定要进行负载均衡对吧,这个时候代码跟到负载均衡获取服务列表源码的地方 68 | 69 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124134853454.png) 70 | 71 | 解释一下上面标黄色框框的地方: 72 | 73 | - RibbonLoadBalancerClient:负责负载均衡的请求处理 74 | - ILoadBalancer:接口中定义了一系列实现负载均衡的方法,相当于一个路由的作用,Ribbon 中默认实现类 ZoneAwareLoadBalancer 75 | - unknown:ZoneAwareLoadBalancer 是多区域负载均衡器,这个 unkonwn 代表默认区域的意思 76 | - allServerList:代表了从 Nacos 注册中心获取的接口服务实例,upServerList 代表了健康实例 77 | 78 | 现在想要知道 Ribbon 是如何获取服务实例的就需要跟进 **getLoadBalancer()** 79 | 80 | ### getLoadBalancer 81 | 82 | 首先声明一点,getLoadBalancer() 方法的语意是从 Ribbon 父子上下文容器中获取名称为 **ribbon-produce**,类型为 **ILoadBalancer.class** 的 Spring Bean 83 | 84 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124141655558.png) 85 | 86 | 之前在讲 Feign 的时候说过,Ribbon 会为每一个服务提供者创建一个 Spring 父子上下文,这里会从子上下文中获取 Bean 87 | 88 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124142007712.png) 89 | 90 | 看到这里并没有解决我们的疑惑,以为方法里会有拉取服务列表的代码,然鹅只是返回一个包含了服务实例的 Bean,所以我们只能去跟下这个 Bean 的上下文 91 | 92 | 我们需要从负载均衡客户端着手,因为默认是 ZoneAwareLoadBalancer,那我们需要跟进它何时被创建,初始化都做了什么事情 93 | 94 | ### ZoneAwareLoadBalancer 95 | 96 | ZoneAwareLoadBalancer 是一个根据区域(Zone)来进行负载均衡器,因为如果不同机房跨区域部署服务列表,跨区域的方式访问会产生更高的延迟,ZoneAwareLoadBalancer 就是为了解决此类问题,不过默认都是同一区域 97 | 98 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124171342217.png) 99 | 100 | ZoneAwareLoadBalancer 很重要,或者说它代表的 **负载均衡路由角色** 很重要。进行服务调用前,会使用该类根据负载均衡算法获取可用 Server 进行远程调用,所以我们要掌握创建这个负载均衡客户端时都做了哪些 101 | 102 | ZoneAwareLoadBalancer 是在服务第一次被调用时通过子容器创建 103 | 104 | ```java 105 | @Bean @ConditionalOnMissingBean // RibbonClientConfiguration 被加载,从 IOC 容器中获取对应实例填充到 ZoneAwareLoadBalancer 106 | public ILoadBalancer ribbonLoadBalancer(IClientConfig config, 107 | ServerList serverList, ServerListFilter serverListFilter, 108 | IRule rule, IPing ping, ServerListUpdater serverListUpdater) { 109 | ... 110 | return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, 111 | serverListFilter, serverListUpdater); 112 | } 113 | 114 | public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule, 115 | IPing ping, ServerList serverList, ServerListFilter filter, 116 | ServerListUpdater serverListUpdater) { 117 | // 调用父类构造方法 118 | super(clientConfig, rule, ping, serverList, filter, serverListUpdater); 119 | } 120 | ``` 121 | 122 | 123 | 在 DynamicServerListLoadBalancer 中调用了父类 BaseLoadBalancer 初始化了一部分配置以及方法,另外自己也初始化了 Server 服务列表等元数据 124 | 125 | ```java 126 | public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, 127 | ServerList serverList, ServerListFilter filter, 128 | ServerListUpdater serverListUpdater) { 129 | // 调用父类 BaseLoadBalancer 初始化一些配置,包括 Ping(检查服务是否可用)Rule(负载均衡规则) 130 | super(clientConfig, rule, ping); 131 | // 较重要,获取注册中心服务的接口 132 | this.serverListImpl = serverList; 133 | this.filter = filter; 134 | this.serverListUpdater = serverListUpdater; 135 | if (filter instanceof AbstractServerListFilter) { 136 | ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats()); 137 | } 138 | // 初始化步骤分了两步走,第一步在上面,这一步就是其余的初始化 139 | restOfInit(clientConfig); 140 | } 141 | ``` 142 | 143 | 先来说一下 BaseLoadBalancer 中初始化的方法,这里主要对一些重要参数以及 Ping、Rule 赋值,另外根据 IPing 实现类执行定时器,下面介绍 Ping 和 Rule 是什么 144 | 145 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124173636638.png) 146 | 147 | 方法大致做了以下几件事情: 148 | 149 | 1. 设置客户端配置对象、名称等关键参数 150 | 2. 获取每次 Ping 的间隔以及 Ping 的最大时间 151 | 3. 设置具体负载均衡规则 IRule,默认 ZoneAvoidanceRule,根据 server 和 zone 区域来轮询 152 | 4. 设置具体 Ping 的方式,默认 DummyPing,直接返回 True 153 | 5. 根据 Ping 的具体实现,执行定时任务 Ping Server 154 | 155 | 这里会介绍被填入的 IPing 和 IRule 是什么东东,并且都有哪些实现 156 | 157 | ### IPing 服务探测 158 | 159 | IPing 接口负责向 Server 实例发送 ping 请求,判断 Server 是否有响应,以此来判断 Server 是否可用 160 | 161 | 接口只有一个方法 isAlive,通过实现类完成探测 ping 功能 162 | 163 | ```java 164 | public interface IPing { 165 | public boolean isAlive(Server server); 166 | } 167 | ``` 168 | 169 | IPing 实现类如下: 170 | 171 | - PingUrl:通过 ping 的方式,发起网络调用来判断 Server 是否可用(一般而言创建 PingUrl 需要指定路径,默认是 IP + Port) 172 | - PingConstant:固定返回某服务是否可用,默认返回 True,表示可用 173 | - NoOpPing:没有任何操作,直接返回 True,表示可用 174 | - DummyPing:默认的类,直接返回 True,实现了 initWithNiwsConfig 方法 175 | 176 | ### IRule 负载均衡 177 | 178 | IRule 接口负责根据不用的算法和逻辑处理负载均衡的策略,自带的策略有 7 种,默认 ZoneAvoidanceRule 179 | 180 | 1. BestAvailableRule:选择服务列表中最小请求量的 Server 181 | 2. RandomRule:服务列表中随机选择 Server 182 | 3. RetryRule:根据轮询的方式重试 Server 183 | 4. ZoneAvoidanceRule:根据 Server 的 Zone 区域和可用性轮询选择 Server 184 | 5. ... 185 | 186 | 上面说过,会有两个初始化步骤,刚才只说了一个,接下来说一下 这个其余初始化方法 `restOfInit`,虽然取名叫其余初始化,但是就重要性而言,那是相当重要 187 | 188 | ```java 189 | void restOfInit(IClientConfig clientConfig) { 190 | boolean primeConnection = this.isEnablePrimingConnections(); 191 | // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList() 192 | this.setEnablePrimingConnections(false); 193 | // 初始化服务列表,并启用定时器,对服务列表作出更新 194 | enableAndInitLearnNewServersFeature(); 195 | // 更新服务列表,enableAndInitLearnNewServersFeature 中定时器的执行的就是此方法 196 | updateListOfServers(); 197 | if (primeConnection && this.getPrimeConnections() != null) { 198 | this.getPrimeConnections() 199 | .primeConnections(getReachableServers()); 200 | } 201 | this.setEnablePrimingConnections(primeConnection); 202 | LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString()); 203 | } 204 | ``` 205 | 206 | 207 | 获取服务列表以及定时更新服务列表的代码都在此处,值得仔细看着源码。关注其中更新服务列表方法就阔以了 208 | 209 | ```java 210 | public void updateListOfServers() { 211 | List servers = new ArrayList(); 212 | if (serverListImpl != null) { 213 | // 获取服务列表数据 214 | servers = serverListImpl.getUpdatedListOfServers(); 215 | LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", 216 | getIdentifier(), servers); 217 | 218 | if (filter != null) { 219 | servers = filter.getFilteredListOfServers(servers); 220 | LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", 221 | getIdentifier(), servers); 222 | } 223 | } 224 | // 更新所有服务列表 225 | updateAllServerList(servers); 226 | } 227 | ``` 228 | 229 | 第一个问题兜兜转转,终于要找到如何获取的服务列表了,**serverListImpl 实现自 ServerList**,因为我们使用的 Nacos 注册中心,所以 ServerList 的具体实现就是 **NacosServerList** 230 | 231 | ```java 232 | public interface ServerList { 233 | public List getInitialListOfServers(); 234 | public List getUpdatedListOfServers(); 235 | } 236 | ``` 237 | 238 | ServerList 中只有两个接口方法,分别是 **获取初始化服务列表集合、获取更新的服务列表集合**,Nacos 实现中两个调用都是一个实现方法,可能设计如此 239 | 240 | 相当于 Ribbon 提供出接口 ServerList,注册中心开发者们谁想和 Ribbon 集成,那你就实现这个接口吧,到时候 Ribbon 负责调用 ServerList 实现类中的方法实现 241 | 242 | > Ribbon 和各服务注册中心之间,这种实现方式和 JDBC 与各数据库之间很像 243 | 244 | 兜兜转转中问题已经明朗,一起总结下注册中心获取服务实例这块内容 245 | 246 | 1. 负载均衡客户端在初始化时向 **Nacos 注册中心获取服务注册列表信息** 247 | 2. 根据不同的 IPing 实现,向获取到的服务列表 **串行发送 ping**,以此来判断服务的可用性。没错,就是串行,如果你的实例很多,可以 **考虑重写 ping 这一块的逻辑** 248 | 3. 如果服务的可用性 **发生了改变或者被人为下线**,那么重新拉取或更新服务列表 249 | 4. 当负载均衡客户端有了这些服务注册类列表,自然就可以进行 **IRule 负载均衡策略** 250 | 251 | ## 非健康服务实例如何下线 252 | 253 | 首先笔者做了两个 **"大胆" 的实验**,第一次是对生产者 SpringBoot 项目执行关闭流程,这时候 Nacos 注册中心是 **实时感知到并将此服务下该实例删除** 254 | 255 | 证明 Nacos 客户端是有 **类似于钩子函数的存在**,感知项目停止就会向 Nacos 服务端注销实例。但是这个时候要考虑一件事情,那就是在 **暴力 Kill 或者执行关闭操作** 的情况下,**存在于 Ribbon 客户端服务列表缓存能不能感知** 256 | 257 | 第二次我这边测试流程是这样的,可以极大程度还原生产上使用 Ribbon 可能会遇到的问题 258 | 259 | 1. 改变客户端负载均衡策略为 **随机负载 RandomRule**,大家自己可以进行测试,不固定负载规则 260 | 2. 注册三个生产者服务实例到 Nacos 上,检查 **确保服务组下实例正常注册** 261 | 3. 操作重点来了,先通过消费方实例请求下对应的生产者接口,保证 **Ribbon 将对应 Server 缓存到客户端** 262 | 4. 停掉一个生产者服务,此时 **马上使用 Jmeter 调用**,Jmeter 线程组发起请求 100 次(一定要赶到更新 Server 缓存之前发起 Jmeter 请求) 263 | 5. 这时就会看到会发生随机失败,也就是说停掉一个服务后,**最坏结果会有 30 秒的生产服务不可用**,这个时间可配置,后面会讲到为什么 30 秒 264 | 265 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210125103040301.png) 266 | 267 | ### 服务列表定时维护 268 | 269 | 针对于服务列表的维护,在 Ribbon 中有两种方式,都是通过定时任务的形式维护客户端列表缓存 270 | 271 | 1. 使用 IPing 的实现类 PingUrl,**每隔 10 秒会去 Ping 服务地址**,如果返回状态不是 200,那么默认该实例下线 272 | 2. Ribbon 客户端内置的扫描,**默认每隔 30 秒去拉取 Nacos 也就是注册中心的服务实例**,如果已下线实例会在客户端缓存中剔除 273 | 274 | 这一块源码都不贴了,放两个源代码位置,感兴趣自己看看就行了 275 | 276 | ```diff 277 | + DynamicServerListLoadBalancer#enableAndInitLearnNewServersFeature 278 | + BaseLoadBalancer#setupPingTask 279 | ``` 280 | 281 | 如果你面试的时候,面试官问了本小节相关内容,把这两个点都能答出来,基本上 SpringCloud 源码就差不多了 282 | 283 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210125180520015.png) 284 | 285 | ## Ribbon 底层原理实现 286 | 287 | 底层原理实现这一块内容,会先说明使用 Ribbon **负载均衡调用远端请求的全过程**,然后着重看一下 RandomRule **负载策略底层是如何实现** 288 | 289 | 1. 创建 ILoadBalancer 负载均衡客户端,初始化 Ribbon 中所需的 **定时器和注册中心上服务实例列表** 290 | 2. 从 ILoadBalancer 中,通过 **负载均衡选择出健康服务列表中的一个 Server** 291 | 3. **将服务名(ribbon-produce)替换为 Server 中的 IP + Port**,然后生成 HTTP 请求进行调用并返回数据 292 | 293 | 上面已经说过,ILoadBalancer 是负责负载均衡路由的,内部会使用 IRule 实现类进行负载调用 294 | 295 | ```java 296 | public interface ILoadBalancer { 297 | public Server chooseServer(Object key); 298 | ... 299 | } 300 | ``` 301 | 302 | chooseServer 流程中调用的就是 IRule 负载策略中的 choose 方法,在方法内部获取一个健康 Server 303 | 304 | ```java 305 | public Server choose(ILoadBalancer lb, Object key) { 306 | ... 307 | Server server = null; 308 | while (server == null) { 309 | ... 310 | List upList = lb.getReachableServers(); // 获取服务列表健康实例 311 | List allList = lb.getAllServers(); // 获取服务列表全部实例 312 | int serverCount = allList.size(); // 全部实例数量 313 | if (serverCount == 0) { // 全部实例数量为空,返回 null,相当于错误返回 314 | return null; 315 | } 316 | int index = chooseRandomInt(serverCount); // 考虑到效率问题,使用多线程 ThreadLocalRandom 获取随机数 317 | server = upList.get(index); // 获取健康实例 318 | if (server == null) { 319 | // 作者认为出现获取 server 为空,证明服务列表正在调整,但是!这只是暂时的,所以当前释放出了 CPU 320 | Thread.yield(); 321 | continue; 322 | } 323 | if (server.isAlive()) { // 服务为健康,返回 324 | return (server); 325 | } 326 | ... 327 | } 328 | return server; 329 | } 330 | ``` 331 | 332 | 简单说一下随机策略 choose 中流程 333 | 334 | 1. 获取到全部服务、健康服务列表,**判断全部实例数量是否等于 0**,是则返回 null,相当于发生了错误 335 | 2. 从全部服务列表里获取下标索引,然后去 **健康实例列表获取 Server** 336 | 3. 如果获取到的 Server 为空会放弃 CPU,然后再来一遍上面的流程,**相当于一种重试机制** 337 | 4. 如果获取到的 Server 不健康,设置 Server 等于空,再歇一会,继续走一遍上面的流程 338 | 339 | 比较简单,有小伙伴可能就问了,如果健康实例小于全部实例怎么办?这种情况下存在两种可能 340 | 341 | 1. 运气比较好,从全部实例数量中随机了比较小的数,刚好健康实例列表有这个数,那么返回 Server 342 | 2. 运气比较背,从全部实例数量中随机了某个数,健康实例列表数量为空或者小于这个数,直接会下标越界异常 343 | 344 | 留下一个思考题: 345 | 346 | **为什么不直接从健康实例中选择实例呢** 347 | 348 | 如果直接从健康实例列表选择,就能规避下标越界异常,为什么作者要先从全部实例中获取 Server 下标? 349 | 350 | ## 自定义 Ribbon 负载均衡策略 351 | 352 | 这种自定义策略,在框架中都支持的比较友好,根据上面提的问题,我们自定义一款策略 353 | 354 | ```java 355 | @Slf4j 356 | public class MyRule extends AbstractLoadBalancerRule { 357 | @Override 358 | public Server choose(Object key) { 359 | ILoadBalancer loadBalancer = getLoadBalancer(); 360 | while (true && ) { 361 | Server server = null; 362 | // 获取已启动并且可访问的服务列表 363 | List reachableServers = loadBalancer.getReachableServers(); 364 | if (CollectionUtils.isEmpty(reachableServers)) return null; 365 | int idx = ThreadLocalRandom.current().nextInt(reachableServers.size()); 366 | server = reachableServers.get(idx); 367 | if (server == null || server.isAlive()) { 368 | log.warn("Ribbon 服务实例异常, 获取为空 || 状态不健康"); 369 | Thread.yield(); 370 | continue; 371 | } 372 | return server; 373 | } 374 | } 375 | 376 | ... initWithNiwsConfig 不用实现 377 | } 378 | ``` 379 | 380 | 说一下我们自己实现的 MyRule 负载的逻辑: 381 | 382 | 1. **IRule 获取服务列表没有在调用方实现**,而是抽象 AbstractLoadBalancerRule,所以我们要获取服务列表继承就好了 383 | 2. 和随机负载规则大致相似,只不过这里简化了流程,**直接从健康的服务实例列表获取 Server 实例** 384 | 3. **确定 Server 不为空并且节点健康后返回**,如果不符合则打印日志,睡一会再重复 385 | 4. 如果保险起见,**最好在 while 中加入一个循环次数的条件**,避免死循环 386 | 387 | 然后把 MyRule 注册到 SPring IOC 容器中就好了,在初始化时就会代替默认的 Rule 负载规则 388 | 389 | 390 | ## 关于 IPing 的思考 391 | 392 | 在阅读 Ribbon Ping 这一块源代码时,发现了两处个人认为不太合理的地方 393 | 394 | 1. **setPingInterval** 设置 Ping 间隔时执行设置 Ping 任务无意义 395 | 2. **BaseLoadBalancer** 构造函数中 ping 为 null,**又再次调用了 setPingInterval**,结果只会返回空 396 | 397 | setPingInterval 和 setPing 两个方法发生在 BaseLoadBalancer 初始化时,相当于接着上面逻辑继续。先说明执行逻辑,再看下不合理的地方 398 | 399 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124185708642.png) 400 | 401 | setupPingTask 用于定期执行对 Server 的 ping 任务,也就是检测 Server 是否可用 402 | 403 | 个人觉得在 setPingInterval 中没有必要执行 setupPingTask 方法 404 | 405 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124190554174.png) 406 | 407 | 作出上述结论有以下依据: 408 | 409 | 1. 第一次执行 setPingInterval 时,ping 必然为空,那么会在 canSkipPing 中返回 True,继而直接结束 setPingInterval 方法 410 | 2. 后来想了下,会不会在别的地方引用,需要强制刷新,然鹅全局搜索了下引用,只有在此次初始化时调用,当然不排除别的依赖包会使用此方法 411 | 3. 综上所述,setPingInterval 执行设置 Ping 任务的方法无意义 412 | 413 | 另外还有一点,作者感觉代码中调用的方法没有实际意义。和上述类似,都是在 ping 为空时执行了 setPingInterval 方法 414 | 415 | ![](https://images-machen.oss-cn-beijing.aliyuncs.com/image-20210124192423863.png) 416 | 417 | 以上这两点是笔者跟源码时,发现不妥当的地方,所以在这里占了一些篇幅说明,主要是想表达两点自己的想法给读者朋友 418 | 419 | 1. **不要对源码有敬畏之心,应该有敬畏之心的是生产环境!** 不要感觉看框架源码是一件高不可攀的事情,其实有时候你理解不了的代码,可能只是多人维护后,混乱的产物,条件允许的情况下,还是要多跟进源码去看一看 420 | 2. **直言说出自己的见解**,如果只有自己去想,那么很可能没有答案,通过文章间接的方式让更多小伙伴看到,指正错误言论亦或者得到肯定 421 | 422 | ## 结言 423 | 424 | 整体来看,文章更 **注重表达设计思想以及源码分析**,所以阅读文章需要一定的源码功底。同时文章是针对问题而展开叙述,哪怕源码不理解也能有所收获 425 | 426 | Ribbon 这块内容从初始化负载均衡客户端 ILoadBalancer 说起,讲述了初始化过程中具体的内容,包括如何开启 IPing 定时器以及服务列表更新定时器 427 | 428 | 另外通过源码查看到 Ribbon 的服务列表其实是向 **Nacos 提供的接口发起服务调用** 获取并保存到本地缓存,继而牵引出如何保证不健康实例下线:**IPing 定时器和服务更新定时器** 429 | 430 | 文末章节说了下请求 Ribbon 负载均衡的全链路以及如何自己定义一个负载均衡算法。最最后面也说了下自己看源码过程中对 SpringCloud IPing 感觉无意义的代码,*当然,不排除是为了别的包集成而留下的* -------------------------------------------------------------------------------- /images/公众号.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magestacks/framework/c80dd655f10eef1c84602fad9729e953b44a4c0b/images/公众号.png --------------------------------------------------------------------------------