├── .gitignore ├── 2017-08 └── Hello World.md ├── 2017-09 ├── [剑指offer-007]用两个栈实现队列.md └── coding-interviews │ └── stack2queue │ └── src │ └── Solution.java ├── 2017-10 ├── [剑指offer-003]二维数组中的查找.md ├── [剑指offer-005]从尾到头打印链表.md ├── [剑指offer-030]最小的k个数.md ├── assets │ ├── complete_tree.png │ ├── heap_order.png │ └── nowcoder_data.png ├── coding-interviews │ ├── find-in-two-dimensional-array │ │ └── src │ │ │ └── Solution.java │ ├── least-numbers │ │ └── src │ │ │ └── Solution.java │ └── print-list-from-tail-to-head │ │ └── src │ │ └── Solution.java └── 秋招面经.md ├── 2017-11 ├── assets │ ├── cmder_setting.png │ └── start_up_setting.png └── wsl使用全记录.md ├── 2018-03 ├── lambda │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── oneone1995 │ │ ├── FilteringApples.java │ │ ├── FunctionInterfaceDemo.java │ │ ├── domain │ │ └── Apple.java │ │ └── strategy │ │ ├── AppleGreenPredicate.java │ │ ├── AppleHeavyPredicate.java │ │ └── AppleStrategy.java └── 从匿名内部类与策略模式到Java8的Lambda.md ├── 2018-12 ├── assets │ ├── JedisSentinelPool.png │ └── exception_stack.png └── 从redis实例迁移导致应用无法获得连接简单说说jedis的sentinel机制.md ├── 2019-03 ├── hikari-monitor │ ├── .gitignore │ ├── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── readme.md │ ├── settings.gradle │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── dianwoda │ │ │ │ └── open │ │ │ │ └── monitor │ │ │ │ ├── annotation │ │ │ │ └── EnableHikariMonitor.java │ │ │ │ ├── config │ │ │ │ ├── HikariDataSourceBeanProcessor.java │ │ │ │ └── HikariMonitorConfiguration.java │ │ │ │ ├── serivice │ │ │ │ └── HikariMonitorService.java │ │ │ │ └── task │ │ │ │ └── HikariMonitorTask.java │ │ └── resources │ │ │ └── META-INF │ │ │ └── spring.factories │ │ └── test │ │ ├── java │ │ └── com │ │ │ └── dianwoda │ │ │ └── open │ │ │ └── hikari │ │ │ └── monitor │ │ │ └── MonitorTest.java │ │ └── resources │ │ └── application.properties └── 数据库连接池初探.md ├── 2020-10 ├── MySQL是怎样连接的-笔记(1).md ├── MySQL是怎样连接的-笔记(2).md ├── assets │ ├── b+_tree.png │ ├── compact_row_format.png │ ├── index_page.png │ ├── innodb_row_format_record_header.png │ ├── mysql_flow.png │ └── record_in_index_page.png ├── mybatis-spring-demo │ ├── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── mybatis-mapper │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── oneone1995 │ │ │ └── mybatis │ │ │ ├── domain │ │ │ └── User.java │ │ │ └── mapper │ │ │ └── UserMapper.java │ ├── mybatis-mapperscan │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── mapperscan │ │ │ ├── MapperScanRunner.java │ │ │ └── config │ │ │ └── AppConfig.java │ ├── mybatis-with-spring │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── oneone1995 │ │ │ └── mybatis │ │ │ ├── Main.java │ │ │ └── config │ │ │ └── AppConfig.java │ ├── mybatis-without-spring │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── oneone1995 │ │ │ └── mybatis │ │ │ ├── H2Test.java │ │ │ └── WithoutSpringRunner.java │ ├── settings.gradle │ └── testDB.mv.db └── 从Spring与Mybatis整合看Spring的扩展点.md ├── 2020-11 ├── Redisson分布式限流器RRateLimiter原理解析.md └── redisson-rratelimiter-demo │ ├── build.gradle │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── oneone1995 │ │ └── demo │ │ └── redisson │ │ └── rratelimiter │ │ └── Main.java │ └── resources │ └── logback.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | **/out 4 | !gradle/wrapper/gradle-wrapper.jar 5 | /logs/ 6 | /monitor/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | nbproject/private/ 25 | build/ 26 | nbbuild/ 27 | dist/ 28 | nbdist/ 29 | .nb-gradle/ 30 | ### Gradle template 31 | 32 | # Ignore Gradle GUI config 33 | gradle-app.setting 34 | 35 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 36 | !gradle-wrapper.jar 37 | 38 | # Cache of project 39 | .gradletasknamecache 40 | 41 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 42 | # gradle/wrapper/gradle-wrapper.properties 43 | ### Java template 44 | # Compiled class file 45 | *.class 46 | 47 | # Log file 48 | *.log 49 | 50 | # BlueJ files 51 | *.ctxt 52 | 53 | # Mobile Tools for Java (J2ME) 54 | .mtj.tmp/ 55 | 56 | # Package Files # 57 | *.war 58 | *.ear 59 | *.zip 60 | *.tar.gz 61 | *.rar 62 | 63 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 64 | hs_err_pid* 65 | /env-office.dev_ns-gw,zk-gw_master.json 66 | /order-mapping_office.dev.gw,dwd-dev_master.json 67 | /env-office.dev_ns,zk_master.json 68 | -------------------------------------------------------------------------------- /2017-08/Hello World.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 这是提交的第一个文章 3 | 4 | hello world!! 5 | 6 | (完 2017年8月31日) -------------------------------------------------------------------------------- /2017-09/[剑指offer-007]用两个栈实现队列.md: -------------------------------------------------------------------------------- 1 | # 用两个栈实现队列 2 | ## 题目描述 3 | 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 4 | 5 | ```java 6 | import java.util.Stack; 7 | 8 | public class Solution { 9 | Stack stack1 = new Stack(); 10 | Stack stack2 = new Stack(); 11 | 12 | public void push(int node) { 13 | //write code here 14 | } 15 | 16 | public int pop() { 17 | //write code here 18 | } 19 | } 20 | ``` 21 | [牛客网OJ链接](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) 22 | 23 | --- 24 | ## 解题思路 25 | 题目的最终目的是要实现一个队列,但是只给了我们两个栈。我们都知道队列的特点是先进先出,而栈的特点是先进后出,两者在某种意义上来说达到的效果是相反的,队列可以看作是栈的倒置。想到这里,我们其实就可以很容易的将题意化解为如何将一个先进后出的栈倒置。 26 | 27 | 我们假设有x1,x2,x3三个元素需要先后入栈stack1,很明显在x2元素入栈前x1已经在栈中,我们要想达到倒置的目的就必须先把x1先pop出来等x2入栈后再将x1压回。x3元素同理。而结合题目,我们的思路是可行的,因为题目友好地为我们提供了两个栈,第二个栈stack2刚好可以用来存放我们需要临时pop出来的元素。 28 | 29 | 以上我们已经完成了一个栈的倒置,即完成了一个先进先出的队列。虽然这部分只完成了跟题意中需要我们完成的Push操作,但我们知道我们的stack1实际上已经是一个队列,相应的Pop操作只需要调用stack1本身的 30 | pop()方法即可。 31 | 32 | ## 参考代码 33 | ```java 34 | import java.util.Stack; 35 | 36 | public class Solution { 37 | Stack stack1 = new Stack(); 38 | Stack stack2 = new Stack(); 39 | 40 | public void push(int node) { 41 | //压栈之前先将栈里的元素全部pop出来并push到另一个栈 42 | while (!stack1.empty()) { 43 | stack2.push(stack1.pop()); 44 | } 45 | //压入新元素 46 | stack1.push(node); 47 | 48 | //将之前pop出去的元素再压回来 49 | while (!stack2.empty()) { 50 | stack1.push(stack2.pop()); 51 | } 52 | } 53 | 54 | public int pop() { 55 | return stack1.pop(); 56 | } 57 | } 58 | ``` 59 | 60 | >说明:代码在牛客网的OJ是通过运行的,如果使用其它OJ可能需要更改。另外防止我手滑粘贴错导致不能运行的,原工程在2017-09/coding-interviews/stack2queue 61 | 62 | (完 2017年9月3日) -------------------------------------------------------------------------------- /2017-09/coding-interviews/stack2queue/src/Solution.java: -------------------------------------------------------------------------------- 1 | import java.util.Stack; 2 | 3 | public class Solution { 4 | Stack stack1 = new Stack(); 5 | Stack stack2 = new Stack(); 6 | 7 | public void push(int node) { 8 | //压栈之前先将栈里的元素全部pop出来并push到另一个栈 9 | while (!stack1.empty()) { 10 | stack2.push(stack1.pop()); 11 | } 12 | //压入新元素 13 | stack1.push(node); 14 | 15 | //将之前pop出去的元素再压回来 16 | while (!stack2.empty()) { 17 | stack1.push(stack2.pop()); 18 | } 19 | } 20 | 21 | public int pop() { 22 | return stack1.pop(); 23 | } 24 | 25 | /* 26 | //调试时使用,粘贴到各OJ时请注释 27 | public static void main(String[] args) { 28 | Solution solution = new Solution(); 29 | solution.push(1); 30 | solution.push(2); 31 | solution.pop(); 32 | solution.push(3); 33 | solution.push(4); 34 | solution.pop(); 35 | } 36 | */ 37 | } 38 | -------------------------------------------------------------------------------- /2017-10/[剑指offer-003]二维数组中的查找.md: -------------------------------------------------------------------------------- 1 | # 二维数组中的查找 2 | ## 题目描述 3 | 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 4 | 5 | ```java 6 | public class Solution { 7 | public boolean Find(int target, int [][] array) { 8 | //write code here 9 | } 10 | } 11 | ``` 12 | [牛客网OJ链接](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 13 | 14 | --- 15 | ## 解题思路 16 | 遍历的方法这里就不说了,两层for循环遍历查找即可。这里说另外一种方法,根据题目的意思我们可以知道任何一个数x左边的数都比x小,x下边的数都比x大。因此我们可以通过比较x和查找目标的大小来缩小查找范围,并且为了方便移动坐标我们将x的出发点设置在右上角,此时如果target比x小,target必定在x左边,即将x的横坐标向左走一步,如果target比x大,target必定在下边,即将x的纵坐标向下走一步。 17 | 18 | 按以上方法不停的缩小范围即能得到查找结果是否存在。唯一要考虑的只剩下循环的终止条件。这个终止条件很容易想出,因为我们是从右上角开始缩小范围(这里的缩小范围指的是x横坐标减小,纵坐标增大),所以当x的横坐标小于0或者纵坐标大于二维数组的```length - 1```即结束查找。 19 | 20 | --- 21 | ## 参考代码 22 | ```java 23 | public class Solution { 24 | public boolean Find(int target, int [][] array) { 25 | //先定位到右上角元素,这个位置的元素左边的都比它小,下边的都比它大 26 | int row = 0; 27 | int col = array[0].length - 1; 28 | 29 | while (row <= array.length - 1 && col >= 0) { 30 | int current = array[row][col]; 31 | if (current == target) { 32 | return true; 33 | } else if (current > target) { 34 | //当前元素x比目标元素大,则目标元素在x左边 35 | col--; 36 | } else { 37 | //当前元素x比目标元素小,则目标元素在x下边 38 | row++; 39 | } 40 | } 41 | return false; 42 | } 43 | } 44 | ``` 45 | >说明:代码在牛客网的OJ是通过运行的,如果使用其它OJ可能需要更改。另外防止我手滑粘贴错导致不能运行的,原工程在2017-10/coding-interviews/find-in-two-dimensional-array 46 | 47 | 48 | (完 2017年10月1日) -------------------------------------------------------------------------------- /2017-10/[剑指offer-005]从尾到头打印链表.md: -------------------------------------------------------------------------------- 1 | # 从尾到头打印链表 2 | ## 题目描述 3 | 输入一个链表,从尾到头打印链表每个节点的值。 4 | 5 | ```java 6 | /** 7 | * public class ListNode { 8 | * int val; 9 | * ListNode next = null; 10 | * 11 | * ListNode(int val) { 12 | * this.val = val; 13 | * } 14 | * } 15 | * 16 | */ 17 | import java.util.ArrayList; 18 | public class Solution { 19 | public ArrayList printListFromTailToHead(ListNode listNode) { 20 | // write code here 21 | } 22 | } 23 | ``` 24 | [牛客网OJ链接](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 25 | 26 | --- 27 | ## 解题思路 28 | 简单的递归可以解决这个题目。用一个全局的```ArrayList```来保存最后输出的结果,我们只要将往```ArrayList```中添加元素的操作放在递归操作的后面即可,这样就先可以通过递归走到链表的最后一个节点,碰到空节点后递归开始返回,返回过程中往list中丢数据。这样list中保存的结果便是链表元素的逆序。 29 | 30 | --- 31 | ## 参考代码 32 | ```java 33 | public class Solution { 34 | 35 | ArrayList arrayList = new ArrayList<>(); 36 | 37 | public ArrayList printListFromTailToHead(ListNode listNode) { 38 | if (listNode == null) { 39 | //这里不返回null是牛客网OJ要求链表为空时输出空的list而不是null 40 | //因为我们的arrayList是全局的,因此在方法返回值没有要求的情况下这里只要能告诉递归可以开始返回就行。 41 | return arrayList; 42 | } 43 | printListFromTailToHead(listNode.next); 44 | //添加元素的代码会一直等待递归开始返回后才执行 45 | arrayList.add(listNode.val); 46 | 47 | return arrayList; 48 | } 49 | 50 | } 51 | ``` 52 | >说明:代码在牛客网的OJ是通过运行的,如果使用其它OJ可能需要更改。另外防止我手滑粘贴错导致不能运行的,原工程在2017-10/coding-interviews/print-list-from-tail-to-head 53 | 54 | 55 | (完 2017年10月2日) -------------------------------------------------------------------------------- /2017-10/[剑指offer-030]最小的k个数.md: -------------------------------------------------------------------------------- 1 | # 最小的k个数 2 | ## 题目描述 3 | 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 4 | 5 | ```java 6 | public class Solution { 7 | public ArrayList GetLeastNumbers_Solution(int [] input, int k) { 8 | //write code here 9 | } 10 | } 11 | ``` 12 | [牛客网OJ链接](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 13 | 14 | --- 15 | ## 解题思路 16 | 既然是求最小的k个数,很明显需要是要对数组进行排序。而题目又要求是求出前k个数因此不需要对整个数组进行全排序,能做到只排序前K个数的排序算法是堆排序。这里有基于堆排序的两种方案: 17 | 1. 普通堆排序 18 | 由于只需要求最小的前k个数,因此我们只需要先将原数组通过"下沉"方式堆有序化(此问题中是指任一节点都比自己的子节点小,即构造小顶堆),此时我们可以知道堆中最小的元素即堆顶元素input[0],因此我们只需要将堆顶元素与堆中未被排序的最后一个元素交换(视作将堆顶元素从堆中移除),交换操作会破坏原来堆有序的状态,因此需要重新通过"下沉"方式调整堆至堆有序状态,如此操作k次便能得到最小的k个数,每次都将移除的堆顶元素保存在ArrayList中即可完成题目要求。 19 | - 由上至下的堆有序化(下沉) 20 | 21 | 首先,二叉堆是一棵完全二叉树。我们可以将输入数组按层次有序构建一棵完全二叉树。如图所示,数组a[i]在对应的完全二叉树中的两个子节点分别为a[i * 2 + 1]与a[i * 2 + 2]: 22 | 23 | ![complete_tree](assets/complete_tree.png) 24 | 25 | 针对本例牛客网给出的数据对应的完全二叉树为: 26 | ![nowcoder_data](assets/nowcoder_data.png) 27 | 28 | 接着我们需要由上至下的将堆有序化,本例中我们是要构建一个小顶堆,因此需要将位置为k上的节点与比它小的子节点交换,将节点向下移动直到它的子节点都比它更大或者到达了堆的底部。这部分代码为: 29 | ```java 30 | /** 31 | * 下沉方式建堆 32 | * @param a 堆对应的数组 33 | * @param k 数组中第k个元素 34 | * @param n 堆大小 35 | */ 36 | private static void sink(int[] a, int k, int n) { 37 | //当前节点的左子节点 >= 堆大小时候说明当前节点已经下沉到堆底了 38 | while (2 * k + 1 < n) { 39 | //记录左子节点 40 | int j = 2 * k + 1; 41 | 42 | //左子节点和右子节点比较大小,如果右子更小那么需要与右子节点交换来完成下沉 43 | //这里先判断左子节点是不是堆中最后一个元素了,即防止当前节点只有左子节点而造成后面判断数组越界 44 | if (j != n - 1 && a[j] > a[j + 1]) { 45 | j++; 46 | } 47 | //如果当前节点已经比自己的子节点小了,那么直接跳出循环,当前节点已经符合堆有序的定义 48 | if (a[k] < a[j]) { 49 | break; 50 | } 51 | //否则和子节点中更小的那个交换 52 | swap(a, k, j); 53 | //更改需要下沉的节点的位置,即更新为子节点的位置 54 | k = j; 55 | } 56 | } 57 | ``` 58 | 59 | 另外通过观察我们发现```array.length / 2```位置上以及之后的节点都为叶子节点,已经在堆的底部了因此无需参与下沉建堆的步骤。因此只需要将```0 ~ array.length / 2 - 1```位置上的节点依次下沉便能构建一个堆有序的结构。这部分代码很简单,只需要循环调用下沉建堆的方法即可: 60 | ```java 61 | for (int i = n / 2 - 1; i >= 0; i--) { 62 | sink(input, i, n); 63 | } 64 | ``` 65 | 执行完建堆步骤,如果对上面说的不理解的话可以手动一步步画一下。我们来看看```下沉```操作后的完全二叉树被调整成了什么样子: 66 | ![heap_order](assets/heap_order.png) 67 | 从图中我们可以看到经过我们的```下沉```步骤,这棵完全二叉树已经符合堆有序的定义了,即每一个节点都比它的两个子节点要小。那接下来的工作只剩下依次取出最小的k个数了。 68 | - 对前k个数下沉排序 69 | 70 | 说了这么多,我们还是没有说到怎么取出最小的k个数。但是我们已经构造出了一个小顶堆了,观察这个小顶堆你会发现最小的数在堆顶,因此我们将堆顶元素与堆底最后一个元素交换视为将最小的元素从堆中移除,同时因为这次交换操作导致堆有序状态被破坏,因此需要调用一次```sink```方法来调整堆。这里我们很容易想到因为最小的元素已经被我们从堆中移除,那么重新调整后的堆的堆顶元素便是原数组中第二小的数了。重复k次,每次调整都将从堆中移除的元素保存在容器中,便能得到符合题目要求的答案了。若k为数组长度便相当于对原数组进行了一次全排序。代码像下面这样: 71 | ```java 72 | //n为数组长度,每次移除一个元素堆大小都会-1 73 | for (int i = 0; i < k; i++) { 74 | //交换堆顶元素和数组最后一个元素,视作将堆顶元素从堆中移除。第一次n为数组长度。 75 | swap(input, 0, --n); 76 | sink(input, 0, n); 77 | result.add(input[n]); 78 | } 79 | ``` 80 | 81 | 2. 优先队列: 82 | 优先队列也是基于堆结构实现的,但是应用场景在于数据输入量非常巨大,甚至是无限的。只用一个堆来保存最大或最小的几个数,以实现对数级别的删除最大元素和插入元素操作,且不需要等到所有数据输入完成才来做统一排序。但是此例中并不适用,题目条件下,输入量势必是可以接受的。如果想使用优先队列,java有内置的API,我这里贴一个牛客网上该题讨论区的答案: 83 | ```java 84 | public class Solution { 85 | public ArrayList GetLeastNumbers_Solution(int[] input, int k) { 86 | ArrayList res = new ArrayList<>(); 87 | if (input == null || k <= 0 || k > input.length) { 88 | return res; 89 | } 90 | Queue queue = new PriorityQueue<>(k, Collections.reverseOrder()); 91 | 92 | for (int i = 0; i < input.length; i++) { 93 | 94 | if (queue.size() < k) { 95 | queue.add(input[i]); 96 | } else { 97 | if (input[i] < queue.peek()) { 98 | queue.remove(); 99 | queue.add(input[i]); 100 | } 101 | } 102 | } 103 | while (!queue.isEmpty()) { 104 | res.add(queue.remove()); 105 | } 106 | return res; 107 | } 108 | } 109 | ``` 110 | 111 | --- 112 | ## 参考代码 113 | ```java 114 | import java.util.ArrayList; 115 | import java.util.Arrays; 116 | 117 | public class Solution { 118 | public ArrayList GetLeastNumbers_Solution(int[] input, int k) { 119 | ArrayList result = new ArrayList<>(); 120 | //建堆 121 | int n = input.length; 122 | 123 | //所求的前k个数大于数组长度直接返回空集合 124 | if (k > n) { 125 | return result; 126 | } 127 | 128 | //将0 ~ n / 2 - 1位置上的元素按下沉方式建堆 129 | for (int i = n / 2 - 1; i >= 0; i--) { 130 | sink(input, i, n); 131 | } 132 | 133 | //调整k次堆,分别将排序前k个数从堆中移除并加到容器ArrayList中 134 | for (int i = 0; i < k; i++) { 135 | swap(input, 0, --n); 136 | sink(input, 0, n); 137 | result.add(input[n]); 138 | } 139 | return result; 140 | } 141 | 142 | /** 143 | * 下沉方式建堆 144 | * @param a 堆对应的数组 145 | * @param k 数组中第k个元素 146 | * @param n 堆大小 147 | */ 148 | private static void sink(int[] a, int k, int n) { 149 | //当前节点的左子节点 >= 堆大小时候说明当前节点已经下沉到堆底了 150 | while (2 * k + 1 < n) { 151 | //记录左子节点 152 | int j = 2 * k + 1; 153 | 154 | //左子节点和右子节点比较大小,如果右子更小那么需要与右子节点交换来完成下沉 155 | //这里先判断左子节点是不是堆中最后一个元素了,即防止当前节点只有左子节点而造成后面判断数组越界 156 | if (j != n - 1 && a[j] > a[j + 1]) { 157 | j++; 158 | } 159 | //如果当前节点已经比自己的子节点小了,那么直接跳出循环,当前节点已经符合堆有序的定义 160 | if (a[k] < a[j]) { 161 | break; 162 | } 163 | //否则和子节点中更小的那个交换 164 | swap(a, k, j); 165 | //更改需要下沉的节点的位置,即更新为子节点的位置 166 | k = j; 167 | } 168 | } 169 | 170 | /** 171 | * 用于交换数组中两个数的辅助方法 172 | * @param a 要交换的数所在的数组 173 | * @param i 第一个数在数组中的索引 174 | * @param j 第二个数在数组中的索引 175 | */ 176 | private static void swap(int[] a, int i, int j) { 177 | int t = a[i]; 178 | a[i] = a[j]; 179 | a[j] = t; 180 | } 181 | 182 | //调试时使用,粘贴到各OJ时请注释 183 | public static void main(String[] args) { 184 | Solution solution = new Solution(); 185 | int[] a = new int[]{4, 5, 1, 6, 2, 7, 3, 8}; 186 | System.out.println(solution.GetLeastNumbers_Solution(a, 8)); 187 | Arrays.stream(a).forEach(System.out::println); 188 | } 189 | } 190 | ``` 191 | >说明:代码在牛客网的OJ是通过运行的,如果使用其它OJ可能需要更改。另外防止我手滑粘贴错导致不能运行的,原工程在2017-10/coding-interviews/least-numbers 192 | 193 | >ps: 终于找到一个好的画图工具了..本文中的图都是用```graphviz```画的 194 | 195 | (完 2017年11月8日) -------------------------------------------------------------------------------- /2017-10/assets/complete_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2017-10/assets/complete_tree.png -------------------------------------------------------------------------------- /2017-10/assets/heap_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2017-10/assets/heap_order.png -------------------------------------------------------------------------------- /2017-10/assets/nowcoder_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2017-10/assets/nowcoder_data.png -------------------------------------------------------------------------------- /2017-10/coding-interviews/find-in-two-dimensional-array/src/Solution.java: -------------------------------------------------------------------------------- 1 | public class Solution { 2 | public boolean Find(int target, int[][] array) { 3 | //先定位到右上角元素,这个位置的元素左边的都比它小,下边的都比它大 4 | int row = 0; //横坐标 5 | int col = array[0].length - 1; //纵坐标 6 | 7 | while (row <= array.length - 1 && col >= 0) { 8 | int current = array[row][col]; 9 | if (current == target) { 10 | return true; 11 | } else if (current > target) { 12 | //当前元素x比目标元素大,则目标元素在x左边 13 | col--; 14 | } else { 15 | //当前元素x比目标元素小,则目标元素在x下边 16 | row++; 17 | } 18 | } 19 | return false; 20 | } 21 | 22 | //遍历的方法 23 | @Deprecated 24 | public boolean findByTraverse(int target, int[][] array) { 25 | for (int i = 0; i < array.length; i++) { 26 | for (int j = 0; j < array[i].length; j++) { 27 | if (array[i][j] == target) { 28 | return true; 29 | } 30 | } 31 | } 32 | return false; 33 | } 34 | 35 | 36 | //调试时使用,粘贴到各OJ时请注释 37 | public static void main(String[] args) { 38 | Solution solution = new Solution(); 39 | int[][] array = new int[][]{{1, 2, 8, 9}, {2, 4, 9, 12}, {4, 7, 10, 13}, {6, 8, 11, 15}}; 40 | System.out.println(solution.Find(8, array)); 41 | System.out.println(solution.findByTraverse(4, array)); 42 | } 43 | } -------------------------------------------------------------------------------- /2017-10/coding-interviews/least-numbers/src/Solution.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | import java.util.Arrays; 3 | 4 | public class Solution { 5 | public ArrayList GetLeastNumbers_Solution(int[] input, int k) { 6 | ArrayList result = new ArrayList<>(); 7 | //建堆 8 | int n = input.length; 9 | 10 | //所求的前k个数大于数组长度直接返回空集合 11 | if (k > n) { 12 | return result; 13 | } 14 | 15 | //将0 ~ n / 2 - 1位置上的元素按下沉方式建堆 16 | for (int i = n / 2 - 1; i >= 0; i--) { 17 | sink(input, i, n); 18 | } 19 | 20 | //调整k次堆,分别将排序前k个数从堆中移除并加到容器ArrayList中 21 | for (int i = 0; i < k; i++) { 22 | swap(input, 0, --n); 23 | sink(input, 0, n); 24 | result.add(input[n]); 25 | } 26 | return result; 27 | } 28 | 29 | /** 30 | * 下沉方式建堆 31 | * @param a 堆对应的数组 32 | * @param k 数组中第k个元素 33 | * @param n 堆大小 34 | */ 35 | private static void sink(int[] a, int k, int n) { 36 | //当前节点的左子节点 >= 堆大小时候说明当前节点已经下沉到堆底了 37 | while (2 * k + 1 < n) { 38 | //记录左子节点 39 | int j = 2 * k + 1; 40 | 41 | //左子节点和右子节点比较大小,如果右子更小那么需要与右子节点交换来完成下沉 42 | //这里先判断左子节点是不是堆中最后一个元素了,即防止当前节点只有左子节点而造成后面判断数组越界 43 | if (j != n - 1 && a[j] > a[j + 1]) { 44 | j++; 45 | } 46 | //如果当前节点已经比自己的子节点小了,那么直接跳出循环,当前节点已经符合堆有序的定义 47 | if (a[k] < a[j]) { 48 | break; 49 | } 50 | //否则和子节点中更小的那个交换 51 | swap(a, k, j); 52 | //更改需要下沉的节点的位置,即更新为子节点的位置 53 | k = j; 54 | } 55 | } 56 | 57 | /** 58 | * 用于交换数组中两个数的辅助方法 59 | * @param a 要交换的数所在的数组 60 | * @param i 第一个数在数组中的索引 61 | * @param j 第二个数在数组中的索引 62 | */ 63 | private static void swap(int[] a, int i, int j) { 64 | int t = a[i]; 65 | a[i] = a[j]; 66 | a[j] = t; 67 | } 68 | 69 | //调试时使用,粘贴到各OJ时请注释 70 | public static void main(String[] args) { 71 | Solution solution = new Solution(); 72 | int[] a = new int[]{4, 5, 1, 6, 2, 7, 3, 8}; 73 | System.out.println(solution.GetLeastNumbers_Solution(a, 8)); 74 | Arrays.stream(a).forEach(System.out::println); 75 | } 76 | } -------------------------------------------------------------------------------- /2017-10/coding-interviews/print-list-from-tail-to-head/src/Solution.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | 3 | class ListNode { 4 | int val; 5 | ListNode next = null; 6 | 7 | ListNode(int val) { 8 | this.val = val; 9 | } 10 | } 11 | 12 | public class Solution { 13 | 14 | ArrayList arrayList = new ArrayList<>(); 15 | 16 | public ArrayList printListFromTailToHead(ListNode listNode) { 17 | if (listNode == null) { 18 | //这里不返回null是牛客网OJ要求链表为空时输出空的list而不是null 19 | //因为我们的arrayList是全局的,因此在方法返回值没有要求的情况下这里只要能告诉递归可以开始返回就行。 20 | return arrayList; 21 | } 22 | printListFromTailToHead(listNode.next); 23 | //添加元素的代码会一直等待递归开始返回后才执行 24 | arrayList.add(listNode.val); 25 | 26 | return arrayList; 27 | } 28 | 29 | //调试时使用,粘贴到各OJ时请注释 30 | public static void main(String[] args) { 31 | Solution solution = new Solution(); 32 | ListNode listNode = new ListNode(1); 33 | listNode.next = new ListNode(2); 34 | listNode.next.next = new ListNode(3); 35 | listNode.next.next.next = new ListNode(4); 36 | listNode.next.next.next.next = new ListNode(5); 37 | 38 | System.out.println(solution.printListFromTailToHead(listNode)); 39 | } 40 | } -------------------------------------------------------------------------------- /2017-10/秋招面经.md: -------------------------------------------------------------------------------- 1 | # 秋招面经 2 | 9月中旬从实习单位正式离职开始准备秋招,早出晚归复习,直到10月1日没有一个offer。这段时间心态十分爆炸,生活一团糟,国庆放假回家睡了两天,和朋友爸妈聊了很多稍微舒服了一点。大概生活还要继续。说是面经其实到现在为止面试机会都寥寥无几,写下来也作为自己不懂问题的集合,可以为了以后的面试好好准备。哼最想去的51信用卡结果笔试都挂了。 3 | 4 | --- 5 | ## 数立信息(一面挂) 6 | 一家很小的公司,把我挂了之后心态瞬间爆炸,觉得自己还没有菜到这种地步。 7 | 8 | - 项目,爬虫策略,直接打开网页让我说怎么爬。(项目涉及爬虫) 9 | - 数据结构,哪块熟悉,说了查找和树。从二分查找讲到查找树、平衡树、红黑树 10 | - MySQL索引,B树。画B树 11 | - 问红黑树在java中的应用。说了HashMap。 12 | - HashMap源码,put操作、hashcode怎么计算,为什么这么计算、hash冲突、初始化数组为什么是2的幂次 13 | - HashMap是否是线程安全,如何变成线程安全,并发情况下会产生什么问题 14 | - ConcurrentHashMap如何解决这个问题 15 | - **session过期概念**,这一块答炸了 16 | - session保证一个时间点只能一个浏览器登录。这个也炸了.. 17 | - 分布式session 18 | - **spring bean生命周期**。忘了。 19 | - SpringMVC运行流程 20 | - 过滤器和监听器,以及各自应用场景。过滤器我回答了解决编码问题,貌似不满意 21 | - TCP协议、HTTP协议。 22 | - MySQL分表策略。给自己挖的坑,只用过当当sharding,答不上来 23 | - 对dubbo的理解,负载均衡算法知道哪些。这个想骂人,负载均衡我只带到一句,也是给自己挖坑跪了。 24 | - 最近在读什么书,近半年打算 25 | 26 | --- 27 | ## 招银网络电面一面 28 | - 回忆智力题,我猜的.. 29 | - 快排最差复杂度怎么算 30 | - 面向对象理解 31 | - 上一个问题说到封装、继承、多态,因此问怎么理解封装 32 | - 集合。有哪些接口,各个区别是啥。漏了Queue 33 | - jvm虚拟机,垃圾回收算法介绍 34 | - 项目 35 | - 分表策略。上次的坑这回填上了.. 36 | - 设计模式 37 | - SpringMVC运行流程和springMVC中用到的设计模式。事后回想漏了单例... 38 | - 数据库事务四大特性 39 | - 事务隔离级别 40 | 41 | --- 42 | ## 百世科技(offer) 43 | - websocket和http的区别 44 | - 手写一个多叉树 45 | - 上面那个多叉树的遍历,将所有节点打印出来,分别用DFS和BFS 46 | - DFS中用了递归,便问了递归的应用场景和注意事项 47 | - DFS写的是先序遍历,便问了后序遍历的输出结果 48 | - 多叉树写的是数组加链表,问为什么这么写。我回答了参考HashMap,其实是想将面试官引到HashMap的问题上去 49 | - 果然是HashMap源码,put过程、为什么再hash、如何散列、红黑树 50 | - ArrayList和LinkedList区别 51 | - 手写单例 52 | - 为什么双重校验锁 53 | - 为什么用volatile 54 | - 我的单例没有问题了已经,但是有人用我的单例类创建出了两个实例对象,怎么做到的。(这个不会,涉及到ClassLoader) 55 | - 聊聊dubbo 56 | - dubbo中有个序列化协议,那不知道你了解过protobuf。protobuf中有个加字段的问题如果让你解决你会怎么办。大概是服务端是很多客户端的服务提供者,现在有某几个客户端要求加字段,不能所有客户端一起停下来。(完全没有接触过,答不出来) 57 | - 聊聊spring的Ioc 58 | - RabbitMQ模型 59 | - 如何学习,看过哪些书 60 | 61 | --- 62 | ## 点我达(offer) 63 | - 项目 64 | - restful怎么理解 65 | - websocket 66 | - http长连接和tcp长连接的区别,两者的keep-alive的区别(之前没听说过) 67 | - oauth2协议 68 | - jwt 69 | - 爬虫数据量很大情况下的存储数据库是怎么优化的。 70 | - 为什么觉得mongodb在大量数据处理中比MySQL好 71 | - 线程池的几个核心参数,以及他们是如何协作的 72 | - ArrayList如何扩容 73 | - HashMap源码看过吗?介绍一下 74 | - HashMap和ConcurrentHashMap有什么区别 75 | - ConcurrentHashMap源码看过吗?介绍一下 76 | - 介绍一下重入锁 77 | - volatile关键字有什么作用 78 | - java内存模型 79 | - spring怎么理解 80 | - AOP怎么实现 81 | - 代理了解哪些 82 | - CGLib代理出来的源码有没有看过 83 | - 那AOP有没有遇到什么坑?没有的话我举个例子问你。如果基于同一个注解打切面打印日志。a方法和b方法都有注解,但是a调用了b,问有几行日志(没有踩过这个坑的我根本答不出来) 84 | - 看过哪些源码?答了集合的源码都看过,便问了那看过哪些框架的源码。不好意思,没看过 85 | - 说说redis吧 86 | - 希望得到什么样的工作环境 87 | 88 | 感冒晕乎乎的,有些知道的也没答好。面试官也是个急性子,我擅长的部分说到一半就不让我说了。虽然还是让我过了一面给了HR面,但是一般来说是凉了,总之非常可惜,非常想去的一家公司。 89 | 90 | >更新:已经给我发offer了,点我达让我抓住了秋招的尾巴。 91 | 92 | --- 93 | ## 九阳研发(offer) 94 | 虽然是传统行业,但看校招做的挺不错便投了简历。笔试只有行测,没有专业性题目笔试,但好像刷了很多人。午睡没睡醒本来没打算去面试,结果hr打了电话过来才过去。总共2面,都是技术面。 95 | 96 | ### 技术一面 97 | - 聊聊项目 98 | - 项目中redis的应用 99 | - 聊聊mongodb与MySQL相比区别与优势在哪里 100 | - websocket和http的区别 101 | - 如果不用websocket聊天怎么做 102 | - xmpp与mqtt区别 103 | - xmpp为什么更费流量 104 | - tcp三次握手 105 | - 集合线性表知道哪些 106 | - ArrayList与LinkedList区别,效率比较 107 | - spring怎么理解,说说ioc和aop 108 | - 聊聊dubbo和rpc 109 | 110 | ### 技术二面 111 | - 挨个介绍项目 112 | - 实习干了什么 113 | - 有什么要问他的 114 | 115 | 总的来说我感觉面试难度并不大,最后进入签约座谈会的人却很少。我感觉自己并没有多强,但可能面试次数多知道了面试官套路。投九阳的行政岗的小姐姐跟我抱怨说行测笔试被刷我还表示惊讶。签约座谈会也做的很棒,自己做了蛋糕,体验了九阳的空气炸锅炸的鸡翅。无奈在互联网公司的诱惑下还是选择放弃。 116 | 117 | --- 118 | ## 嘉云数据(offer) 119 | 收到点我达offer通知后的第二天面试,因为之前答应了去面试,感觉不去不太礼貌。总共1轮笔试3轮面试,两轮技术一轮HR。二面技术面是技术部门的负责人,被怼的有点难受,后来发展成我跟她互怼。 120 | 121 | ### 技术一面 122 | - 笔试做的怎么样感觉 123 | - 有一个特定情况下,插排、堆排、快排、归并四种排序算法中的一种能达到O(n)的时间复杂度。是哪种情况,并实现?(想不出来) 124 | - 最差时间复杂度和时间复杂度怎么理解 125 | - 怎么理解排序稳定性 126 | - 堆排是什么逻辑 127 | - 爬虫怎么做 128 | - 怎么看网络请求,用什么工具,主要关注哪些数据的传输 129 | - http了解嘛,说一说http以及报文格式 130 | - 用哪个头参数控制不留缓存 131 | - 能知道http响应报文长度嘛 132 | - TCP三次握手 133 | - java集合了不了解(以为又到了表演的时刻可以秀hashmap..) 134 | - hashset实现(套路满满) 135 | - ArrayList扩容,何时扩容,怎么指定默认数组大小,能修改默认大小吗? 136 | - 聊聊数据库索引 137 | - B树和B+树的区别 138 | - 为什么IO请求次数多就要用B树或者B+树 139 | - jdbc熟悉吗(...) 140 | - mybatis和jdbc的区别 141 | - mongodb和mysql区别 142 | - redis怎么使用的 143 | - springboot说说 144 | - 你对python的看法是什么 145 | 146 | ### 技术二面 147 | - 手写一个将字符串转成类似json的带缩进的格式。(我说了实现逻辑让我写,因为后面判断逻辑我觉得差不多我就不写了,而且写的有点乱涂涂改改她看不清,我给她说了接下来都差不多只是没写了,她准备给我张白纸重写,我直接放弃了) 148 | - 数据库了不了解 149 | - MySQL和mongodb除了索引上的差别还有什么(说了事务) 150 | - 事务的ACID 151 | - 事务的目的是什么(我说了保证能得到正确的结果,期望的结果,并不满意和我说是保证数据的一致性,我说一致性也是我们定义的,就是用我们期望的结果去定义这个一致性,认同我观点,但还是说我给她的答案中的正确性和满足期待太过暧昧) 152 | - 出了个题,临时出的。想让我答出读锁 153 | - 事务隔离级别 154 | - TCP怎么保证传输可靠 155 | - 又出了个题,两个有序数组求交集。 156 | - 其他一些职业规划问题 157 | 158 | 总结的话二面面的非常不舒服,面试官一口一个英文单词有点不习惯。我应该也给她留下了很差的印象。这大概是秋招最后一站,竟然被搞成这样,几乎全程在怼。后来HR加了微信问是否愿意转python,之前技术面试的时候也有这个意思在里面。他们应该是web方向都是python做,java主要是爬虫。 159 | 160 | --- 161 | ## 总结 162 | 秋招所有面试应该都已经结束,虽然没有去成最想去的51信用卡,但在秋招的尾巴上抓住了点我达也算是满意:离家近、薪资到位、互联网公司。不算是成功,但也不算失败,至少我自己已经满意,所以也许我的经历能给你带来帮助。 163 | 164 | 如果你有幸读到这篇文章,我用自己的经验来说的话最后悔的事情是实习辞职的太晚,我9月中旬才从实习公司离职,那时候很多大厂甚至都已经结束了,虽然很难进大厂,但是早个一两个月开始准备的话应该早就结束秋招了。最好的状态也许是大三下3月份就去实习待到6月底,实习三个月时间便可以离职回校准备秋招了。除非你已经确定会留在实习的单位。还有便是每个人能力都不一样,希望能正确定位自己,我一开始看到别人高的薪资很是羡慕觉得自己找的不满意,但自己是个双非本科,别人是双985硕士。 165 | 166 | 秋招主要准备的方向[牛客网](https://www.nowcoder.com)的面经已经遍地都是。我这里整理一份我准备秋招过程中看的书单和一些有参考价值的博客。 167 | 168 | ### 书单 169 | 下面是我准备秋招过程中看的书,如果有时间会写一点读书笔记。 170 | 171 | - [《算法》](https://www.amazon.cn/%E5%9B%BE%E7%81%B5%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E4%B8%9B%E4%B9%A6-%E7%AE%97%E6%B3%95-%E5%A1%9E%E5%A5%87%E5%A8%81%E5%85%8B/dp/B009OCFQ0O/ref=sr_1_1?ie=UTF8&qid=1509461845&sr=8-1&keywords=%E7%AE%97%E6%B3%95) 172 | 173 | java描述的数据结构基础,都是基础数据结构,然后加上一些字符串处理的高级算法。另外红黑树一章节逆天,是我看过写红黑树最好的一本书。 174 | - [《Java并发编程实战》](https://www.amazon.cn/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98-%E7%9B%96%E8%8C%A8/dp/B0077K9XHW/ref=sr_1_1?ie=UTF8&qid=1509461950&sr=8-1&keywords=java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98) 175 | 176 | 这本书我看的很痛苦,前几章写的还不错比较好懂,主要问题是书中给的示例代码都不能自己敲来实现,书名起的实战不知道意义何在。能知道一些并发的基础概念,像线程池、并发包、锁等等。不过仅仅只能知道概念,我并不推荐刚入门的直接看这本书,至少我看不懂。 177 | - [《深入理解java虚拟机》](https://www.amazon.cn/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA-JVM%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E4%B8%8E%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-%E5%91%A8%E5%BF%97%E6%98%8E/dp/B00D2ID4PK/ref=sr_1_1?ie=UTF8&qid=1509462164&sr=8-1&keywords=%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E8%99%9A%E6%8B%9F%E6%9C%BA) 178 | 179 | 神书,相见恨晚的一本书。介绍了JVM相关的知识,包括Java方法运行模型、垃圾回收算法、垃圾收集器、类加载、JMM、多线程、锁、并发等知识点。写的也是深入浅出,没有难懂的地方,而且书中有的例子都可以自己运行。除了第4、5章虚拟机优化和后面的10、11章我没有看以为,这本书看完不仅对面试有所帮助(能解决大部分jvm这块的面试题),对自身想要学习jvm知识的也非常有好处。并发相关章节的内容个人感觉比上面那本```java并发实战```都要写得好。 180 | 181 | - [《spring实战》](https://www.amazon.cn/Spring%E5%AE%9E%E6%88%98-Craig-Walls-%E6%B2%83%E5%B0%94%E6%96%AF/dp/B01DN3VR6G/ref=sr_1_1?ie=UTF8&qid=1509493130&sr=8-1&keywords=spring%E5%AE%9E%E6%88%98) 182 | 183 | 并不只是为了准备秋招看的书,实习期间我就开始读这本书。我读了前7章,对spring知识有个大概了解,书中鼓励javaconfig的方式来搭建spring应用,我觉得值得一试。书中给的例子也能实际运行,对```spring bean```、自动装备、IOC、AOP也有描述 184 | 185 | - [《redis实战》](https://www.amazon.cn/Redis%E5%AE%9E%E6%88%98-%E7%BA%A6%E8%A5%BF%E4%BA%9A-L-%E5%8D%A1%E5%B0%94%E6%A3%AE/dp/B016YLS2LM/ref=sr_1_1?ie=UTF8&qid=1509493291&sr=8-1&keywords=redis%E5%AE%9E%E6%88%98) 186 | 187 | 读了前4章,感觉并不是特别好。看完知道redis基本数据结构和常用命令、持久化策略。我真正使用redis的时候全在上网查。 188 | 189 | - [《高性能MySQL》](https://www.amazon.cn/%E9%AB%98%E6%80%A7%E8%83%BDMySQL-%E6%96%BD%E7%93%A6%E8%8C%A8/dp/B00C1W58DE/ref=sr_1_4?ie=UTF8&qid=1509493291&sr=8-4&keywords=redis%E5%AE%9E%E6%88%98) 190 | 191 | 实在太厚,挑了章节看的。我看了前两章以及后面索引的部分。 192 | 193 | - [《java8实战》](https://www.amazon.cn/Java-8%E5%AE%9E%E6%88%98-%E5%8E%84%E9%A9%AC/dp/B01ER75QC8/ref=sr_1_1?s=books&ie=UTF8&qid=1509493657&sr=1-1&keywords=java8%E5%AE%9E%E6%88%98) 194 | 195 | 介绍java8新特性,看到网上其他人面经是有问java8的,但是我还没真正面到过问题。idea现在能检测可以替换成java8新特性的方法并给出提示智能替换,所以也跟着学了学。这本书讲的java8新特性也很好懂,可能是翻译的好的原因。主要包括Lambda表达式、方法引用、流、默认方法、Optional、CompletableFuture以及新的日期和时间API等等。 196 | 197 | - [《编程之美》](https://www.amazon.cn/gp/product/B0016K8XGQ/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1) 198 | 199 | 面试题集,部分题目和《剑指offer》冲突,本想刷完剑指offer之后写完题解的,因为找不到一个好的画图工具而一直被耽搁。和《剑指offer》搭配着看,并在牛客网上练习。可惜我刷了很多现在随着秋招的结束也忘了很多,还是不够厉害呀。 200 | 201 | 其它一些别人推荐,买了之后还没看的书: 202 | - [《Effective Java》](https://www.amazon.cn/gp/product/B001PTGR52/ref=ox_sc_sfl_title_2?ie=UTF8&psc=1&smid=A33E54V2WD1T67) 203 | - [《redis设计与实现》](https://www.amazon.cn/Redis%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0-%E9%BB%84%E5%81%A5%E5%AE%8F-%E8%91%97/dp/B00LZNV5B4/ref=sr_1_1?s=books&ie=UTF8&qid=1509493608&sr=1-1&keywords=redis%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0) 204 | - [《计算机网络:自顶向下方法》](https://www.amazon.cn/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C-%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E6%96%B9%E6%B3%95-%E5%BA%93%E7%BD%97%E6%96%AF/dp/B00OB1AODW/ref=sr_1_1?s=books&ie=UTF8&qid=1509494150&sr=1-1&keywords=%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C) 205 | 206 | ### 博客以及一些博文 207 | 算法和海量数据处理: 208 | - [结构之法 算法之道](blog.csdn.net/v_JULY_v/article/list/1) 209 | 210 | Java基础: 211 | - [Java那些事儿](https://zhuanlan.zhihu.com/easyJava)(集合源码分析相当不错) 212 | - [Java中的字符串连接符(+)](http://www.jianshu.com/p/849f1d443b3a) 213 | - [深入Java集合学习系列:HashMap的实现原理](http://zhangshixi.iteye.com/blog/672697) 214 | - [从ConcurrentHashMap的演进看Java多线程核心技术 ](http://www.jasongj.com/java/concurrenthashmap/) 215 | - [concurrentHashMap原理(JDK1.6和1.8的比对)](http://blog.csdn.net/qq_24236769/article/details/75194247) 216 | - [尽量把CyclicBarrier和CountDownLatch的区别说通俗点](http://aaron-han.iteye.com/blog/1591755) 217 | - [Java nio 2.0 AIO](http://lxy2330.iteye.com/blog/1122849) 218 | - [Java JDK代理、CGLIB、AspectJ代理分析比较](xiezhaodong.me/2017/03/31/Java-JDK%E4%BB%A3%E7%90%86%E3%80%81CGLIB%E3%80%81AspectJ%E4%BB%A3%E7%90%86%E5%88%86%E6%9E%90%E6%AF%94%E8%BE%83/) 219 | 220 | Java内存相关: 221 | - [成神之路](http://www.hollischuang.com/archives/1003) 222 | 223 | MySQL: 224 | - [漫画算法:什么是B树](http://blog.jobbole.com/111757/) 225 | - [数据库索引的实现原理](http://blog.csdn.net/kennyrose/article/details/7532032) 226 | 227 | 其它: 228 | - [理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) 229 | - [如何正确地写出单例模式 ](wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/) 230 | - [通俗大白话来理解TCP协议的三次握手和四次分手](https://github.com/jawil/blog/issues/14) 231 | - [WebSocket 是什么原理?为什么可以实现持久连接?](https://zhihu.com/question/20215561/answer/40316953) 232 | - [怎样理解阻塞非阻塞与同步异步的区别?](https://zhihu.com/question/19732473/answer/20851256) 233 | 234 | 综合性总结以及面经: 235 | - [个人知识小仓库](https://github.com/pzxwhc/MineKnowContainer) 236 | - [Java面试题集](http://blog.csdn.net/dd864140130/article/details/55833087) 237 | - [Java方向如何准备BAT技术面试答案(汇总版)](https://www.nowcoder.com/discuss/31667) 238 | - [IT-Interviews-Sharing](https://github.com/lanxuezaipiao/IT-Interviews-Sharing) 239 | 240 | --- 241 | (完 2017年10月31日) -------------------------------------------------------------------------------- /2017-11/assets/cmder_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2017-11/assets/cmder_setting.png -------------------------------------------------------------------------------- /2017-11/assets/start_up_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2017-11/assets/start_up_setting.png -------------------------------------------------------------------------------- /2017-11/wsl使用全记录.md: -------------------------------------------------------------------------------- 1 | # Windows Subsystem for Linux使用全记录 2 | 微软大概为了和```OS X```竞争,在```windows```环境下加入了一个```linux```子系统,据说是目前最好用的Linux发行版(滑稽)。我个人看来的话是有学习```linux```或者某些业务场景需要使用但不想装虚拟机的话```wsl```确实是一个不错的选择,事实上也确实比虚拟机流畅。 3 | 4 | --- 5 | ## 安装 6 | 在我安装的时候需要在设置中的```针对开发人员```选项中勾选```开发人员模式```,然后在控制面板中```启用或关闭Windows功能```中勾选```适用于Linux的Windows子系统```,最后在cmd中输入```bash```命令等待下载完成即可。最新的windows周年更新后貌似可以直接在应用商店下载(搜索Ubuntu),我没有尝试过。 7 | 8 | --- 9 | ## 使用Cmder 10 | 当然你不用也没有关系,wsl自带了一个终端,我觉得它太丑,所以找到了cmder。可以在[cmder官网](http://cmder.net/)下载解压后直接可以使用。cmder默认终端是cmd,因此你可以直接输入```bash```命令来进入wsl,如果觉得这样比较麻烦,你可以在设置中新增一个task来快速打开,就像下面这样: 11 | 12 | ![cmder_setting](assets/cmder_setting.png) 13 | 14 | 需要在右边输shell启动命令的地方输入 15 | ```cmd 16 | %windir%\system32\bash.exe ~ -cur_console:p:n 17 | ``` 18 | 这样设置完之后在cmder中点击```create new console```那个绿色的加号旁边的三角便能看到刚刚新增的wsl条目。如果你想将wsl在你打开cmder之后便立即启动则需要在下图的```start up```中设置: 19 | 20 | ![start_up_setting](assets/start_up_setting.png) 21 | 22 | 到这里之后你每次启动cmder便会自动进入wsl 23 | 24 | --- 25 | ## 安装zsh 26 | 这部分网上教程比较多,和其它任意linux发行版安装方式都类似。不再多说,给两个我安装时的参考链接。 27 | - [在ubuntu中安装与配置zsh与oh-my-zsh](http://www.jianshu.com/p/546effd99c35) 28 | - [解决Bash On Ubuntu On Window安装Zsh无效问题附安装说明](http://www.jianshu.com/p/9a575dda0eff) 29 | 30 | --- 31 | ## 服务自启动 32 | wsl中的服务目前貌似是不会自启动的,比如你每次启动wsl都需要手动去启动redis服务。这个坑我在网上搜索之后找到一个曲线救国的方式来解决,即每次启动wsl时候调用一个启动服务的脚本,下面是一个例子: 33 | 在wsl主目录下新建一个```autostart.sh```来作为启动服务的脚本,例子中是一个启动redis服务的命令: 34 | ```bash 35 | #!/bin/bash 36 | sudo service redis-server start 37 | bash 38 | ``` 39 | 然后修改上文中提到的cmder启动wsl的命令在后面加上一个参数: 40 | ```cmd 41 | %windir%\system32\bash.exe ~ -cur_console:p:n -c ~/autostart.sh 42 | ``` 43 | 这样便能在wsl启动时候去执行主目录下的```autostart.sh```服务启动脚本,但是这里还有一个坑,如果仅仅这么做每次启动都会让你输入密码,这样还是会稍显麻烦,因此我们还需要将sudo的密码给去掉。像下面这么做 44 | ```bash 45 | cd /etc 46 | sudo vim sudoers 47 | ``` 48 | 然后```sudoers```的结尾加上 49 | ```bash 50 | # username是你的用户名,不是root,比如我是wangl,则我这里是wangl ALL=(ALL) NOPASSWD:ALL 51 | username ALL=(ALL) NOPASSWD:ALL 52 | ``` 53 | 我这里是去掉了所有sudo的密码,如果你觉得这样做有安全问题那么可以针对特定的命令来设置不使用密码,这个自己去找资料。 54 | 55 | 到这里应该就可以像任意linux发行版一样正常使用。像redis、mongo、nginx之类的服务都可以在wsl里面跑,我甚至有段时间直接用它作为ssh工具连接服务器。唯一不方便的地方是不能后台,一旦关闭cmder或者其它终端工具的窗口wsl便被关闭了。这里另一个曲线救国的方式是利用win10的多桌面,在另一个桌面开着cmder窗口,做一个假后台,但实际上如果强迫症没有特别严重也没什么必要。 56 | 57 | --- 58 | ## 总结 59 | 随着微软爸爸不停的更新,以及社区的活跃,wsl势必是会越来越好的,我室友刚开始使用的时候全是bug,等我使用的时候已经几乎没有什么特别严重的bug了。如果你遇到什么问题可以先查查应该已经有不少人在你之前踩过坑,知乎相关话题、Google、Baidu都能查到不少,也可以去github上提issue。 60 | 61 | --- 62 | ## 参考 63 | - [Linux Bash on Win10 (WSL)在cmder下使用vim时方向键失灵问题解决](http://www.cnblogs.com/hujq1029/p/6549073.html) 64 | - [在ubuntu中安装与配置zsh与oh-my-zsh](http://www.jianshu.com/p/546effd99c35) 65 | - [解决Bash On Ubuntu On Window安装Zsh无效问题附安装说明](http://www.jianshu.com/p/9a575dda0eff) 66 | - [使用 Windows Subsystem for Linux 进行 Python 开发](https://blog.kdwycz.com/archives/%E4%BD%BF%E7%94%A8Windows%20Subsystem%20for%20Linux/) 67 | 68 | 69 | (完 2017年11月2日) -------------------------------------------------------------------------------- /2018-03/lambda/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.oneone1995 8 | lambda 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.projectlombok 14 | lombok 15 | 1.16.20 16 | provided 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/FilteringApples.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995; 2 | 3 | import com.github.oneone1995.domain.Apple; 4 | import com.github.oneone1995.strategy.AppleGreenPredicate; 5 | import com.github.oneone1995.strategy.AppleStrategy; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class FilteringApples { 11 | public static void main(String[] args) { 12 | //苹果列表 13 | List inventory = Apple.inventory; 14 | 15 | //调用绿色苹果筛选器 16 | System.out.println("普通绿色苹果筛选器:"); 17 | System.out.println(filterGreenApples(inventory)); 18 | //调用重量大于150克的苹果筛选器 19 | System.out.println("普通重量苹果筛选器:"); 20 | System.out.println(filterHeavyApples(inventory)); 21 | //调用将颜色作为参数的苹果筛选器 22 | System.out.println("将颜色作为参数的苹果筛选器:"); 23 | System.out.println(filterApplesByColor(inventory, "red")); 24 | //使用策略设计模式的苹果筛选器,这里具体传入绿色苹果筛选策略 25 | System.out.println("使用了策略模式的筛选器,传入绿色苹果筛选策略:"); 26 | System.out.println(filterApples(inventory, new AppleGreenPredicate())); 27 | //使用匿名内部类代替具体的策略对象 28 | System.out.println("使用了策略模式的筛选器,传入匿名内部类:"); 29 | System.out.println(filterApples(inventory, new AppleStrategy() { 30 | @Override 31 | public boolean predicate(Apple apple) { 32 | return "green".equals(apple.getColor()); 33 | } 34 | })); 35 | System.out.println(filterApples(inventory, new AppleStrategy() { 36 | @Override 37 | public boolean predicate(Apple apple) { 38 | return apple.getWeight() > 150; 39 | } 40 | })); 41 | //使用Lambda表达式 42 | System.out.println("使用了Lambda表达式:"); 43 | System.out.println(filterApples(inventory, apple -> apple.getWeight() > 150)); 44 | } 45 | 46 | /** 47 | * 第一个需求,筛选出绿色的苹果 48 | * @param inventory 待筛选的苹果列表 49 | * @return 绿色苹果 50 | */ 51 | private static List filterGreenApples(List inventory) { 52 | List result = new ArrayList<>(inventory.size()); 53 | for (Apple apple : inventory) { 54 | if ("green".equals(apple.getColor())) { 55 | result.add(apple); 56 | } 57 | } 58 | return result; 59 | } 60 | 61 | /** 62 | * 第二个需求,筛选出重量大于150克的苹果 63 | * @param inventory 待筛选的苹果列表 64 | * @return 重量大于150克的苹果 65 | */ 66 | private static List filterHeavyApples(List inventory) { 67 | List result = new ArrayList<>(inventory.size()); 68 | for (Apple apple : inventory) { 69 | if (apple.getWeight() > 150) { 70 | result.add(apple); 71 | } 72 | } 73 | return result; 74 | } 75 | 76 | /** 77 | * 将颜色作为参数的过滤器 78 | * @param inventory 待筛选的苹果列表 79 | * @param color 需要筛选什么颜色 80 | * @return 符合要求的苹果列表 81 | */ 82 | private static List filterApplesByColor(List inventory, String color) { 83 | List result = new ArrayList<>(inventory.size()); 84 | for (Apple apple : inventory) { 85 | if (color.equals(apple.getColor())) { 86 | result.add(apple); 87 | } 88 | } 89 | return result; 90 | } 91 | 92 | /** 93 | * 使用策略设计模式改进后的筛选器,因为AppleStrategy为一个函数式接口,因此这里同样适用于传入Lambda表达式作为参数 94 | * @param inventory 待筛选的苹果列表 95 | * @param strategy 封装了具体筛选策略的策略对象,也可以是一个Lambda表达式 96 | * @return 符合筛选策略要求的苹果列表 97 | */ 98 | private static List filterApples(List inventory, AppleStrategy strategy) { 99 | List result = new ArrayList<>(inventory.size()); 100 | for (Apple apple : inventory) { 101 | if (strategy.predicate(apple)) { 102 | result.add(apple); 103 | } 104 | } 105 | return result; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/FunctionInterfaceDemo.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995; 2 | 3 | import com.github.oneone1995.domain.Apple; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | 12 | /** 13 | * Java8自带的函数式接口演示 14 | */ 15 | public class FunctionInterfaceDemo { 16 | public static void main(String[] args) { 17 | //1. 通过Java8自带函数式接口Predicate实现苹果筛选 18 | System.out.println("通过Java8自带函数式接口Predicate实现苹果筛选:"); 19 | List inventory = Apple.inventory; 20 | System.out.println(filterApples(inventory, apple -> apple.getWeight() > 100)); 21 | 22 | //2. 使用Consumer接口打印一个List中的所有元素 23 | System.out.println("使用Consumer接口打印一个List中的所有元素"); 24 | List integerList = Arrays.asList(1, 2, 3, 4, 5); 25 | forEach(integerList, integer -> System.out.println(integer)); 26 | 27 | //3. 使用Function接口映射一个列表元素中的某些属性到另一个列表 28 | System.out.println("使用Function接口映射一个String列表的长度到另一个列表:"); 29 | List stringList = Arrays.asList("lambda", "in", "action"); 30 | List mapResult = map(stringList, s -> s.length()); 31 | System.out.println(mapResult); 32 | } 33 | 34 | /** 35 | * 使用更为通用的Java8自带函数式接口Predicate来作为筛选器参数 36 | * @param inventory 待筛选的苹果列表 37 | * @param predicate Predicate接口实例,可以是一个Lambda表达式 38 | * @return 符合筛选要求的苹果列表 39 | */ 40 | private static List filterApples(List inventory, Predicate predicate) { 41 | List result = new ArrayList<>(inventory.size()); 42 | for (Apple apple : inventory) { 43 | if (predicate.test(apple)) { 44 | result.add(apple); 45 | } 46 | } 47 | return result; 48 | } 49 | 50 | /** 51 | * 使用Consumer接口打印一个List中的所有元素 52 | * @param list 需要操作的list 53 | * @param consumer 对list中的元素做特定操作的函数式接口对象实例,可以是一个Lambda表达式 54 | * @param list中元素的类型泛型 55 | */ 56 | private static void forEach(List list, Consumer consumer) { 57 | for (T t : list) { 58 | consumer.accept(t); 59 | } 60 | } 61 | 62 | /** 63 | * 使用Function接口映射一个列表元素中的某些属性到另一个列表 64 | * @param list 需要操作得list 65 | * @param function 对list中的元素进行转换的函数式接口对象实例,可以是一个Lambda表达式 66 | * @param function接收的对象 67 | * @param function转换后的对象 68 | * @return 存储了元素经过function转换后的list 69 | */ 70 | private static List map(List list, Function function) { 71 | List result = new ArrayList<>(list.size()); 72 | 73 | for (T t : list) { 74 | result.add(function.apply(t)); 75 | } 76 | return result; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/domain/Apple.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | @Data 12 | @ToString 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class Apple { 16 | private Integer weight; 17 | private String color; 18 | 19 | public static final List inventory = Arrays.asList(new Apple(80, "green"), 20 | new Apple(155, "green"), 21 | new Apple(120, "red")); 22 | } 23 | -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/strategy/AppleGreenPredicate.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.strategy; 2 | 3 | import com.github.oneone1995.domain.Apple; 4 | 5 | public class AppleGreenPredicate implements AppleStrategy { 6 | @Override 7 | public boolean predicate(Apple apple) { 8 | return apple.getWeight() > 150; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/strategy/AppleHeavyPredicate.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.strategy; 2 | 3 | import com.github.oneone1995.domain.Apple; 4 | 5 | public class AppleHeavyPredicate implements AppleStrategy { 6 | @Override 7 | public boolean predicate(Apple apple) { 8 | return apple.getWeight() > 150; 9 | } 10 | } -------------------------------------------------------------------------------- /2018-03/lambda/src/main/java/com/github/oneone1995/strategy/AppleStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.strategy; 2 | 3 | import com.github.oneone1995.domain.Apple; 4 | 5 | /** 6 | * 封装了苹果属性判断条件的策略 7 | * 因为只有一个抽象方法,它同时也是一个函数式接口 8 | */ 9 | @FunctionalInterface 10 | public interface AppleStrategy { 11 | boolean predicate(Apple apple); 12 | } -------------------------------------------------------------------------------- /2018-03/从匿名内部类与策略模式到Java8的Lambda.md: -------------------------------------------------------------------------------- 1 | # 从匿名内部类与策略模式到Java8的Lambda 2 | 3 | > 说明:本文为[《Java8实战》](https://book.douban.com/subject/26772632/)第一部分(基础知识)的读书笔记。按书的思路以及自己的理解整理了```Lambda表达式```与函数式编相关的内容,而关于Java8的整体概览则没有阐述。 4 | 5 | --- 6 | ## 农民的苹果问题 7 | 本书用一个农夫不断变更关于筛选苹果的需求的例子来引出函数式编程。 8 | 9 | 苹果有重量和颜色两个属性,有个农夫最初希望筛选出库存中绿色的苹果。所以你写了一个苹果颜色筛选器,这很容易实现。只需要遍历库存列表,将符合颜色属性为绿色这个条件的苹果添加到一个新的容器就行了: 10 | ```java 11 | private static List filterGreenApples(List inventory) { 12 | List result = new ArrayList<>(inventory.size()); 13 | for (Apple apple : inventory) { 14 | if ("green".equals(apple.getColor())) { 15 | result.add(apple); 16 | } 17 | } 18 | return result; 19 | } 20 | ``` 21 | 但是过了一周,农民又想要筛选出重量大于150克的苹果,所以你又写了一个苹果重量筛选器,这还是很容易实现。同样的只需要遍历库存列表,将符合重量属性大于150克这个条件的苹果添加到一个新的容器就行了: 22 | ```java 23 | private static List filterHeavyApples(List inventory) { 24 | List result = new ArrayList<>(inventory.size()); 25 | for (Apple apple : inventory) { 26 | if (apple.getWeight() > 150) { 27 | result.add(apple); 28 | } 29 | } 30 | return result; 31 | } 32 | ``` 33 | 我想你已经发现问题所在了,两个筛选器只有```if```的苹果属性的判断条件不同,却写了两个几乎一模一样的筛选器,代码就变得十分的啰嗦。如果这时候你还没有觉得繁琐,那当苹果的属性不只有重量和颜色,又多出了产地、销量、形状等等条件,这位不断更改需求的农民又要求你对这些不同的属性都做筛选,甚至要求你做组合筛选,便会出现无数个相似的```filter```方法,这肯定是不合理的。 34 | 35 | --- 36 | ## 行为参数化 37 | 我们来看这样一个问题,如果农夫的需要变更是从筛选```绿色```的苹果改成筛选```红色```的苹果,我们很容易想到为了不再反复得去修改```if```的颜色判断条件,我们会将颜色作为筛选器的参数在调用时动态的传入,使代码具有灵活性和扩展性,此时便能应付筛选各种不同颜色苹果的需求: 38 | ```java 39 | private static List filterApplesByColor(List inventory, String color) { 40 | List result = new ArrayList<>(inventory.size()); 41 | for (Apple apple : inventory) { 42 | if (color.equals(apple.getColor())) { 43 | result.add(apple); 44 | } 45 | } 46 | return result; 47 | } 48 | ``` 49 | 类比过来,在苹果的颜色、产地、重量、销量的多个不同属性的筛选器中不同的仅仅是筛选器中```if```的判断条件,也就是这个判断的行为。我们如果可以将这个行为,或者说将整个判断条件当作筛选器的参数在调用时动态的传入,那我们就能和仅仅是筛选不同颜色的筛选器```filterApplesByColor```那样在代码实际运行时根据传入的判断条件做到灵活的筛选了。 50 | 51 | --- 52 | ## 策略设计模式 53 | 我们的筛选判断条件想要作为参数在筛选器的方法中传入就应该将其封装在一个类中,并将类作为参数传递到方法中去。书中提到这其实是策略设计模式相关的,因为之前没有接触过设计模式,去翻了[《大话设计模式》](https://book.douban.com/subject/2334288/)```策略模式```这一章,也比较简单。 54 | 55 | 在```《大话设计模式》```中是用商场收银时采取的不同促销方式来举例的,写的也很生动: 56 | ``` 57 | 商场收银时如何促销,用打折还水返利,其实都是一种算法。用工厂来生成算法对象,这没有错,但算法本身是一种策略,最重要的是这些算法是随时都可能相互替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。 58 | ``` 59 | 这实质上和本文中提到的苹果筛选其实没什么不同。我们首先应该有一个苹果筛选```算法族```的抽象类或者接口来作为筛选器的参数进行传递,它里面应该封装了一个根据苹果某些属性(颜色是否是绿色、重量是否大于150克)来返回一个```boolean```值的```算法```: 60 | ```java 61 | public interface AppleStrategy { 62 | boolean predicate(Apple apple); 63 | } 64 | ``` 65 | 此时我们只需要一个筛选器就能应付所有的筛选需求了: 66 | ```java 67 | private static List filterApples(List inventory, AppleStrategy strategy) { 68 | List result = new ArrayList<>(inventory.size()); 69 | for (Apple apple : inventory) { 70 | if (strategy.predicate(apple)) { 71 | result.add(apple); 72 | } 73 | } 74 | return result; 75 | } 76 | ``` 77 | ```predicate(Apple apple)```方法是各种判断条件的抽象,想要向筛选器中传入不同的筛选条件只需要写一个类去实现```AppleStrategy```接口,用具体的判断条件重写```predicate(Apple apple)```方法就可以了。此处用筛选绿色苹果举例: 78 | 79 | 1. 写一个判断苹果是否为绿色的策略 80 | ```java 81 | public class AppleGreenPredicate implements AppleStrategy { 82 | @Override 83 | public boolean predicate(Apple apple) { 84 | return "green".equals(apple.getColor()); 85 | } 86 | } 87 | ``` 88 | 2. 在调用筛选器时将这个判断苹果是否为绿色的策略作为筛选器参数传入 89 | ```java 90 | List greenApples = filterApplesByStrategy(inventory, new AppleGreenPredicate()); 91 | ``` 92 | 其他判断条件也是类似的,只需要根据需求先写一个策略,看起来解决方案很不错,至少大幅度减少了重复的代码。但是值得注意的一点是,虽然我们将筛选器做了封装,减少到了一个,但是项目中却多了无数的策略类,每要实现一次筛选的需求就要创建一个实现了```AppleStrategy```接口的策略类。 93 | 94 | --- 95 | ## 匿名内部类 96 | 为了解决上述问题,你会想到既然不想出现这么多策略类,那我们用```匿名内部类```就好了嘛。我们可以借助匿名内部类同时声明并实例化一个具体的策略,改善了一个接口声明好几个实体类的问题: 97 | ```java 98 | //筛选绿色的苹果 99 | List greenApples = filterApples(inventory, new AppleStrategy() { 100 | @Override 101 | public boolean predicate(Apple apple) { 102 | return "green".equals(apple.getColor()); 103 | } 104 | }); 105 | 106 | //筛选重量大于150的苹果 107 | List heavyApples = filterApples(inventory, new AppleStrategy() { 108 | @Override 109 | public boolean predicate(Apple apple) { 110 | return apple.getWeight() > 150; 111 | } 112 | }); 113 | ``` 114 | 这样做的好处是我们终于不用再创建很多个具体的策略类、也不用根据不同的需求来写很多个筛选器就能应付多变的需求了。不好的地方是匿名内部类的代码仍然非常的啰嗦并且变得冗长不好读。我们需要在灵活性和简洁性之间找一个最佳的平衡点。 115 | 116 | --- 117 | ## Lambda表达式 118 | 好了好了,终于轮到```Lambda表达式```出场了。先来看看用Lambda表达式能怎样改写上述代码: 119 | ```java 120 | //筛选绿色的苹果 121 | List greenApples = filterApples(inventory, apple -> "green".equals(apple.getColor())); 122 | 123 | //筛选重量大于150的苹果 124 | List heavyApples = filterApples(inventory, apple -> apple.getWeight() > 150); 125 | ``` 126 | 这是非常大的一次改进了,我们在```行为参数化```一节中提到过将判断条件作为筛选器的参数传递进去,这里你会发现```Lambda表达式```真的这样做到了,没有啰嗦的代码,不再需要一个创建一个实现了策略接口的类,除却了语法规定的格式以外,Lambda的主体部分和筛选器中```if```的判断条件一模一样。 127 | 128 | 接着我们来讲讲怎么使用```Lambda表达式```: 129 | 130 | - Lambda表达式语法 131 | ```java 132 | (Apple apple) -> "green".equals(apple.getColor()) 133 | ``` 134 | Lambda表达式由参数、箭头和主体组成,箭头前部分为参数,后部分为主体。 135 | 1. 参数部分 136 | ```java 137 | //和Java的函数参数格式相同,为一个逗号分隔,小括号包围的形参集合。 138 | //可以忽略掉参数的数据类型;如果只有一个参数,你还可以忽略小括号;如果形参列表为空,则不能省略括号 139 | apple -> "green".equals(apple.getColor()) 140 | 141 | () -> System.out.println("Hello Lambda"); 142 | ``` 143 | 2. 主体部分 144 | ```java 145 | //要么是一个表达式,要么是一个由大括号包围语句块。 146 | //如果 Lambda 表达式主体只有一条语句,那么可以忽略大括号。 147 | apple -> { 148 | System.out.println(apple.getColor()); 149 | System.out.println(apple.getWeight()); 150 | } 151 | ``` 152 | - Lambda表达式如何使用 153 | 154 | 我们知道在```Java8```之前想要作为函数的参数就必须是一个基本数据类型或者一个实例对象,而我们现在可以直接将一个```Lambda表达式```作为参数传递。但这并不是直接就可以这么干的,在使用```Lambda表达式```之前首先需要定义一个```函数式接口```,而我们的```Lambda表达式```可以看作那个```函数式接口```的一个实例,这样就能理解了,原来```Lambda表达式```仍然是一个实例对象。 155 | ```java 156 | //函数式接口就是只有一个抽象方法的接口 157 | //我们在策略模式中设计的算法族便是一个函数式接口 158 | //@FunctionalInterface注解和@Override的注解类似,此处是标注了接口为一个函数式接口,如果接口定义了多个抽象方法则会在编译期报错 159 | @FunctionalInterface 160 | public interface AppleStrategy { 161 | boolean predicate(Apple apple); 162 | } 163 | ``` 164 | 定义完函数式接口后,我们先来看下```Lambda表达式```和这个```函数式接口```的关系: 165 | ```java 166 | (Apple apple) -> "green".equals(apple.getColor()) 167 | ``` 168 | 这个```Lambda表达式```的主体部分是一个表达式,表达式的返回值类型和```函数式接口```中定义的抽象方法返回值类型是一致的,```Lambda表达式```的参数类型即是定义的抽象方法的参数类型。整个表达式就是这个抽象方法的具体实现。接下来我们只需要将这个接口作为筛选器的方法参数,在使用筛选器时就可以使用Lambda表达式来作为参数传入了。 169 | 170 | --- 171 | ## 函数式接口 172 | 实际上大部分情况下不需要我们自己去定义函数式接口,拿苹果筛选器中用到的这个```AppleStrategy```接口来说,```Java8```有一个抽象层次更高的接口```Predicate```,它定义了一个```test(T t)```的抽象方法,接收泛型T对象,并返回一个```boolean```。在你需要表示一个设计类型T的布尔表达式时就可以使用这个接口,我们的苹果筛选器就可以改成下面这样: 173 | ```java 174 | private static List filterApples(List inventory, Predicate predicate) { 175 | List result = new ArrayList<>(inventory.size()); 176 | for (Apple apple : inventory) { 177 | if (predicate.test(apple)) { 178 | result.add(apple); 179 | } 180 | } 181 | return result; 182 | } 183 | ``` 184 | 除此之外,```Java API```还提供了更多的```函数式接口```,例如: 185 | - Consumer 186 | ```java 187 | @FunctionalInterface 188 | public interface Consumer { 189 | void accept(T t); 190 | } 191 | ``` 192 | Consumer接口中定义了一个接收泛型对象T,没有返回值的```accept(T t)```方法。适用于想访问某个对象并对其做操作且不需要返回的场景。比如接收一个List,并需要打印输出其中的每个元素时就可以用这个接口。 193 | ```java 194 | /** 195 | * 使用Consumer接口打印一个List中的所有元素 196 | * @param list 需要操作的list 197 | * @param consumer 对list中的元素做特定操作的函数式接口对象实例,可以是一个Lambda表达式 198 | * @param list中元素的类型泛型 199 | */ 200 | private static void forEach(List list, Consumer consumer) { 201 | for (T t : list) { 202 | consumer.accept(t); 203 | } 204 | } 205 | 206 | forEach(integerList, integer -> System.out.println(integer)); 207 | ``` 208 | - Function 209 | ```java 210 | @FunctionalInterface 211 | public interface Function { 212 | R apply(T t); 213 | } 214 | ``` 215 | Function接口中定义了一个接收泛型对象T,返回泛型R的```apply(T t)```方法。适用于将输入对象的信息映射到输出的场景,或者说是对输入对象进行转化。比如接收一个String列表,输出一个这个String列表中每个元素长度的Integer列表。 216 | ```java 217 | /** 218 | * 使用Function接口映射一个列表元素中的某些属性到另一个列表 219 | * @param list 需要操作得list 220 | * @param function 对list中的元素进行转换的函数式接口对象实例,可以是一个Lambda表达式 221 | * @param function接收的对象 222 | * @param function转换后的对象 223 | * @return 存储了元素经过function转换后的list 224 | */ 225 | private static List map(List list, Function function) { 226 | List result = new ArrayList<>(list.size()); 227 | 228 | for (T t : list) { 229 | result.add(function.apply(t)); 230 | } 231 | return result; 232 | } 233 | 234 | List mapResult = map(stringList, s -> s.length()); 235 | ``` 236 | 237 | 除了本文列出的三个```函数式接口```以外,```Java8```还提供了更多的函数式接口,这里不再多说,菜鸟教程的[Java 8 函数式接口](http://www.runoob.com/java/java8-functional-interfaces.html)总结的```java.util.function```包下所有的函数式接口。 238 | 239 | --- 240 | ## 总结 241 | 虽然别的语言早就引入了```Lambda表达式```,而且也比```Java8```的```Lambda```更强大,但其作为```Java8```的新语法,给```Java```在语法层面上带来了革命性的改进,还是非常值得学习的,毕竟```Java10```都快出了。。 242 | 243 | 本来还想再写写```方法引用```的内容,但是写的实在太长了,也不方便阅读还是单开一篇来写```方法引用```有关的笔记吧。 244 | 245 | 最后,本文所有例子在2018-03/lambda目录下,有需要的可以clone之后查看。 246 | 247 | (完 2018年3月11日) 248 | -------------------------------------------------------------------------------- /2018-12/assets/JedisSentinelPool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2018-12/assets/JedisSentinelPool.png -------------------------------------------------------------------------------- /2018-12/assets/exception_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2018-12/assets/exception_stack.png -------------------------------------------------------------------------------- /2018-12/从redis实例迁移导致应用无法获得连接简单说说jedis的sentinel机制.md: -------------------------------------------------------------------------------- 1 | # 从redis实例迁移导致应用无法获得连接简单说说jedis的sentinel机制 2 | 3 | ## 现象 4 | 5 | 前段时间,因为运维同学在做redis的实例迁移导致使用那个master-name的服务都无法获取到redis连接并报错。报错信息如下: 6 | 7 | ![异常栈](assets/exception_stack.png) 8 | 9 | 奇怪的问题是服务在进行`重启`操作之后便又能拿到redis连接,服务正常了。另外观察到出现此现象的服务使用的redis客户端均为jedis。 10 | 11 | ## 原因 12 | 13 | 看到这个现象第一反应是jedis在服务端出现停机断线的情况后不会进行重连,在服务端恢复后客户端这边没进行重连操作导致。这也很好解释为什么重启之后便能恢复故障的现象。 14 | 15 | 但按理来说正常的实例迁移操作是不会造成服务都不可用的问题的,不然生产上redis服务端进行一次闪断,所有使用jedis的服务全会不可用。所以和运维同学一起做了个实验复现了当时的问题,便找到了原因。 16 | 17 | ### 运维操作步骤复现 18 | 19 | #### 第一次实验 20 | 21 | 1. 删哨兵信息 22 | 2. 停master节点 23 | 3. 重启master节点 24 | 25 | 按照我最开始的想法,服务端出现闪断,jedis又没有重连机制,服务应该不可用才对。但奇怪的是问题并没有复现。此时我们开始怀疑redis服务端版本问题、spring-data版本问题。但我们把各个系统环境都和当时出问题的环境保持一致的时候按上述操作步骤仍然没有复现。而且发现了一个新的问题,在master节点没有停机的情况下即使把sentinel中的注册信息删掉,客户端仍然可以连上服务端。 26 | 27 | #### 第二次实验 28 | 29 | 1. 删哨兵信息 30 | 2. 停master节点 31 | 3. 注册新的节点信息到哨兵 32 | 33 | 在一番尝试之后,运维同学决定和他那天进行实例迁移的操作步骤保持完全一致再来尝试一次。这两次实验的区别是在停掉master节点后的操作,第一次是重启了原来的master节点,第二次实验则是开了另一台实例上的master并把新的地址注册到sentinel中去。 34 | 35 | 按这次的操作步骤真的复现了当时出现的错误! 36 | 37 | ### JedisSentinelPool 38 | 39 | 结合两次实验便能大致猜出原因是什么了,之前的服务端断线jedis不会重连的想法是有问题的,大方向应该是jedis在启动的时候从sentinel拿到master节点的信息并缓存在内存里,并且之后再也不去sentinel中拿了。这也解释了在master节点没挂的情况下即便服务器上sentinel里已经没有注册信息了,但是客户端还是能正常的读写;同时也解释了为啥在做了实例迁移以后新的master节点起来了但是客户端仍然连不上,因为它拿的还是原来那台已经被关掉的节点地址。 40 | 41 | 去找了jedis连接部分的源码来看果不其然。Jedis对Sentinel的操作主要都在`JedisSentinelPool.java`这个类里,这个类的结构如下: 42 | 43 | ![JedisSentinelPool](assets/JedisSentinelPool.png) 44 | 45 | 其中通过sentinel来找master节点的代码都在`initsSentinel()`方法中,这里简单贴一下: 46 | 47 | ```java 48 | private HostAndPort initSentinels(Set sentinels, final String masterName) { 49 | 50 | HostAndPort master = null; 51 | boolean sentinelAvailable = false; 52 | 53 | log.info("Trying to find master from available Sentinels..."); 54 | 55 | //从配置的多个sentinel中遍历,其实并不需要遍历,一般都在某一个sentinel中能找到master信息了,遍历多个是防止其中一个sentinel挂掉导致连不上主节点。 56 | for (String sentinel : sentinels) { 57 | final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":"))); 58 | 59 | log.fine("Connecting to Sentinel " + hap); 60 | 61 | Jedis jedis = null; 62 | try { 63 | //jedis客户端先连接到sentinel 64 | jedis = new Jedis(hap.getHost(), hap.getPort()); 65 | 66 | //根据masterName得到master的地址,master有多个节点,所以是list,从这里可以看到其实一个sentinel上就能得到全部的信息了。 67 | List masterAddr = jedis.sentinelGetMasterAddrByName(masterName); 68 | 69 | // connected to sentinel... 70 | sentinelAvailable = true; 71 | 72 | if (masterAddr == null || masterAddr.size() != 2) { 73 | log.warning("Can not get master addr, master name: " + masterName 74 | + ". Sentinel: " + hap + "."); 75 | continue; 76 | } 77 | 78 | master = toHostAndPort(masterAddr); 79 | log.fine("Found Redis master at " + master); 80 | //如果在任何一个sentinel中找到了便不用再继续遍历查找了 81 | break; 82 | } catch (JedisConnectionException e) { 83 | log.warning("Cannot connect to sentinel running @ " + hap 84 | + ". Trying next one."); 85 | } finally { 86 | if (jedis != null) { 87 | jedis.close(); 88 | } 89 | } 90 | } 91 | 92 | //如果执行到这里,master仍为null,要么所有的sentinel都不可用,要么master节点没有被存活的sentinel监控到 93 | if (master == null) { 94 | if (sentinelAvailable) { 95 | // can connect to sentinel, but master name seems to not 96 | // monitored 97 | throw new JedisException("Can connect to sentinel, but " + masterName 98 | + " seems to be not monitored..."); 99 | } else { 100 | throw new JedisConnectionException( 101 | "All sentinels down, cannot determine where is " + masterName 102 | + " master is running..."); 103 | } 104 | } 105 | 106 | log.info("Redis master running at " + master + ", starting Sentinel listeners..."); 107 | 108 | for (String sentinel : sentinels) { 109 | final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":"))); 110 | MasterListener masterListener = new MasterListener(masterName, hap.getHost(), 111 | hap.getPort()); 112 | masterListeners.add(masterListener); 113 | masterListener.start(); 114 | } 115 | 116 | return master; 117 | } 118 | ``` 119 | 120 | 这个initsSentinel()方法看了下只在JedisSentinelPool的构造方法里用到,也就是说大概率上是jedis只在启动创建连接池的时候会连上sentinel去查master节点的地址信息并缓存在内存里,从今以后只要服务还在拿到的永远都是启动时拿到的master节点。这和我们看到的现象是一致的。 121 | 122 | ### Failover 123 | 124 | 这个时候你可能会想,这不对呀!sentinel模式是redis的高可用模型啊,如果是这样,那发生master节点down掉的情况slave节点顶上,但如果我客户端拿到的注册信息还是原来的那不就相当于这种机制废了嘛。 125 | 126 | 但实际上注意看initsSentinel()方法最后,每一个sentinel都注册了一个`MasterListener`,这个`MasterListener`也很简单,订阅了sentinel上关于master节点地址改变的PUB消息。收到变更消息后,调用initPool()方法,这个方法会重建整个连接池,自然也会获得sentinel中变更后的节点信息从而来实现failover。 127 | 128 | 原来正常情况下的主从替换是可以处理的,只是测试环境报错那天的情况比较特殊,主从节点全挂了,运维才做了删除sentinel中注册信息并进行了实例迁移的操作。 129 | 130 | ## 再多说一点 131 | 132 | 写这个文章的时候我才想到一个问题,为什么在slave没挂master挂掉的情况下,slave会顶上来这时候会修改sentinel中的信息,此时我客户端订阅了你这个消息便会发生变更。那为啥slave和master都挂掉的情况,做实例迁移,手动修改sentinel中的信息便收不到这个pub消息了? 133 | 134 | 这一点确实不是很清楚,只能用一般注册中心的机制来类比推断了,例如zk,假设注册在zk上dubbo消费者没有重连机制(实际上有),当我把zk的node节点删了,客户端和zk之间的订阅关系理论上也被删了,此时当一个注册在zk上的dubbo提供者发生变更,但是由于消费者与zk之间的订阅关系已经不存在,zk自然也无法通知到。如果zk是这样,那sentinel会不会也是如此呢? 135 | 136 | 这个疑问在问了运维同学之后,得到的回复是"主从都挂掉,再重新注册,就相当于是一个新的东西。不会对外发消息,只有发生变化的时候才会发生变化"。 137 | 138 | 在文档资料和运维同学的帮助下这个问题到这里就算是解决了,另外在实验过程中不仅测试了jedis,还测试了另一个redis客户端[lettuce](https://github.com/lettuce-io/lettuce-core)。发现lettuce客户端有重连处理器避免了jedis的这个问题。关于lettuce的处理以及两个客户端的对比后面另外再写一篇来说吧。 -------------------------------------------------------------------------------- /2019-03/hikari-monitor/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | **/out 4 | !gradle/wrapper/gradle-wrapper.jar 5 | /logs/ 6 | /monitor/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | nbproject/private/ 25 | build/ 26 | nbbuild/ 27 | dist/ 28 | nbdist/ 29 | .nb-gradle/ 30 | ### Gradle template 31 | 32 | # Ignore Gradle GUI config 33 | gradle-app.setting 34 | 35 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 36 | !gradle-wrapper.jar 37 | 38 | # Cache of project 39 | .gradletasknamecache 40 | 41 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 42 | # gradle/wrapper/gradle-wrapper.properties 43 | ### Java template 44 | # Compiled class file 45 | *.class 46 | 47 | # Log file 48 | *.log 49 | 50 | # BlueJ files 51 | *.ctxt 52 | 53 | # Mobile Tools for Java (J2ME) 54 | .mtj.tmp/ 55 | 56 | # Package Files # 57 | *.war 58 | *.ear 59 | *.zip 60 | *.tar.gz 61 | *.rar 62 | 63 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 64 | hs_err_pid* 65 | /env-office.dev_ns-gw,zk-gw_master.json 66 | /order-mapping_office.dev.gw,dwd-dev_master.json 67 | /env-office.dev_ns,zk_master.json 68 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | 4 | group 'com.dianwoda.open' 5 | version '1.1-SNAPSHOT' 6 | 7 | sourceCompatibility = 1.8 8 | 9 | repositories { 10 | mavenLocal() 11 | maven { url "http://maven.aliyun.com/nexus/content/groups/public/" } 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | compileOnly 'org.projectlombok:lombok:1.18.6' 17 | //这个jar是公司内部的工具包,实际上就打了日志 18 | compileOnly 'com.dianwoba.monitor:monitor-client:2.5.2' 19 | 20 | //因为版本问题,下面两个包使用compile 21 | compile 'io.micrometer:micrometer-core:1.0.1' 22 | compile 'com.zaxxer:HikariCP:3.2.0' 23 | 24 | compileOnly 'org.springframework.boot:spring-boot-configuration-processor:1.5.19.RELEASE' 25 | compileOnly "org.springframework.boot:spring-boot-starter-log4j2:1.5.19.RELEASE" 26 | compileOnly 'org.springframework.boot:spring-boot-autoconfigure:1.5.19.RELEASE' 27 | 28 | testCompile 'com.dianwoba.monitor:monitor-client:2.5.2' 29 | testCompile group: 'junit', name: 'junit', version: '4.12' 30 | testCompile "org.springframework.boot:spring-boot-starter-test:1.5.19.RELEASE" 31 | testCompile "org.springframework.boot:spring-boot-starter-jdbc:1.5.19.RELEASE" 32 | testCompile 'mysql:mysql-connector-java:8.0.13' 33 | } 34 | 35 | apply plugin: 'maven' 36 | apply plugin: 'java' 37 | 38 | configurations { 39 | deployerJars 40 | } 41 | 42 | dependencies { 43 | deployerJars 'org.apache.maven.wagon:wagon-http:2.2' 44 | } 45 | 46 | // sources及javadoc 47 | javadoc { 48 | failOnError = false 49 | options { 50 | encoding 'UTF-8' 51 | charSet 'UTF-8' 52 | } 53 | options.memberLevel = JavadocMemberLevel.PRIVATE 54 | } 55 | 56 | task sourcesJar(type: Jar, dependsOn: classes) { 57 | classifier = 'sources' 58 | from sourceSets.main.allSource 59 | } 60 | task javadocJar(type: Jar, dependsOn: javadoc) { 61 | classifier = 'javadoc' 62 | from javadoc.destinationDir 63 | } 64 | artifacts { 65 | archives sourcesJar 66 | archives javadocJar 67 | } 68 | tasks.withType(JavaCompile) { 69 | options.encoding = "UTF-8" 70 | } 71 | 72 | // 上传 jar 包到自己的 maven 服务器 73 | uploadArchives { 74 | repositories.mavenDeployer { 75 | //beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 76 | configuration = configurations.deployerJars 77 | repository(url: "http://nexus.example.com/nexus/content/repositories/snapshots") { 78 | authentication(userName: "XXX", password: "XXX") 79 | } 80 | repository(url: "http://nexus.example.com/nexus/content/repositories/snapshots") { 81 | authentication(userName: "XXX", password: "XXX") 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2019-03/hikari-monitor/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /2019-03/hikari-monitor/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/readme.md: -------------------------------------------------------------------------------- 1 | # hikari监控工具 2 | 3 | ## 引入 4 | 5 | ```groovy 6 | compile 'com.dianwoda.open:connection-pool-monitor:1.1-SNAPSHOT' 7 | compile 'com.zaxxer:HikariCP:3.2.0' 8 | ``` 9 | 10 | 引入monitor-client包 11 | 12 | >注意:如果你以前的项目已经引入了HikariCP的包,则需要将版本升级到3.2.0 13 | 14 | ## 使用 15 | 16 | ### 配置 17 | 18 | - 使用spring原生配置方式 19 | 20 | ```properties 21 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 22 | spring.datasource.type=com.zaxxer.hikari.HikariDataSource 23 | spring.datasource.url=jdbc:mysql://localhost:3306/learn?serverTimezone=GMT 24 | spring.datasource.username=root 25 | spring.datasource.password=E815C***m 26 | ``` 27 | 28 | - 主类上使用`@EnableHikariMonitor`注解 29 | 30 | - 特别说明: 31 | 32 | 1. 默认情况下数据库连接池大小初始化为10,如果需要自定义,添加配置项`spring.datasource.hikari.maximum-pool-size=`,建议配置成20 33 | 34 | 2. 默认情况下连接池为fixed,即连接池最大连接和最小空闲连接一致,如果需要自定义,添加配置项`spring.datasource.hikari.max-pool-size`和`spring.datasource.hikari.min-idle`。建议使用默认值 35 | 36 | 37 | ### 上线后去Grafana配置监控项 38 | 39 | 各个监控项的key如下: 40 | 41 | | key | 描述 | 42 | |-------------------------------|--------------------------------------------| 43 | | hikaricp.connections.min |minIdle,最小空闲数量 | 44 | | hikaricp.connections |连接池当前所有连接数 | 45 | | hikaricp.connections.idle |当前空闲连接数量 | 46 | | hikaricp.connections.max |maxPoolSize,连接池最大连接数 | 47 | | hikaricp.connections.creation |创建一个新的连接所需的时间 | 48 | | hikaricp.connections.active |活跃连接数 | 49 | | hikaricp.connections.pending |当前排队获取连接的线程数 | 50 | | hikaricp.connections.acquire |获取连接时间 | 51 | | hikaricp.connections.usage |一个事务执行耗时。即连接被租用到归还的耗时 | 52 | | hikaricp.connections.timeout |从池中获取连接超时的数量 | 53 | 54 | ## 未完善的功能 55 | 56 | - [] 多数据源的时候处理 57 | - [] 和第三方框架整合时的功能,利如sharding-jdbc 58 | - [] 一些细节 -------------------------------------------------------------------------------- /2019-03/hikari-monitor/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'hikari-monitor' 2 | 3 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/java/com/dianwoda/open/monitor/annotation/EnableHikariMonitor.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.monitor.annotation; 2 | 3 | import com.dianwoda.open.monitor.config.HikariMonitorConfiguration; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.*; 7 | 8 | @Inherited 9 | @Documented 10 | @Target(ElementType.TYPE) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Import({HikariMonitorConfiguration.class}) 13 | public @interface EnableHikariMonitor { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/java/com/dianwoda/open/monitor/config/HikariDataSourceBeanProcessor.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.monitor.config; 2 | 3 | import com.zaxxer.hikari.HikariDataSource; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 6 | import org.springframework.beans.BeansException; 7 | import org.springframework.beans.factory.config.BeanPostProcessor; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | @ConditionalOnClass(HikariDataSource.class) 13 | public class HikariDataSourceBeanProcessor implements BeanPostProcessor { 14 | private MeterRegistry meterRegistry = new SimpleMeterRegistry(); 15 | 16 | @Override 17 | public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { 18 | return bean; 19 | } 20 | 21 | @Override 22 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 23 | if (bean instanceof HikariDataSource) { 24 | if (((HikariDataSource) bean).getMetricRegistry() == null) { 25 | ((HikariDataSource) bean).setMetricRegistry(meterRegistry); 26 | } 27 | } 28 | return bean; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/java/com/dianwoda/open/monitor/config/HikariMonitorConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.monitor.config; 2 | 3 | import com.dianwoba.monitor.client.MonitorUtil; 4 | import com.dianwoba.monitor.client.MonitorUtilImpl; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | 11 | @Configuration 12 | @EnableScheduling 13 | @ComponentScan(basePackages = {"com.dianwoda.open"}) 14 | @ConditionalOnClass(name = "com.dianwoba.monitor.client.MonitorUtil") 15 | public class HikariMonitorConfiguration { 16 | 17 | @Bean 18 | public MonitorUtil monitorUtil() { 19 | return new MonitorUtilImpl(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/java/com/dianwoda/open/monitor/serivice/HikariMonitorService.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.monitor.serivice; 2 | 3 | import com.dianwoba.monitor.client.MonitorPoint; 4 | import com.dianwoba.monitor.client.MonitorUtil; 5 | import io.micrometer.core.instrument.*; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.annotation.PostConstruct; 9 | import javax.annotation.Resource; 10 | import java.net.InetAddress; 11 | import java.net.UnknownHostException; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @Service 18 | public class HikariMonitorService { 19 | private String ip; 20 | private String applicationName; 21 | 22 | @Resource 23 | private MonitorUtil monitorUtil; 24 | 25 | public void doMonitor(MeterRegistry meterRegistry) { 26 | for (Meter m : meterRegistry.getMeters()) { 27 | if (m instanceof Timer) { 28 | writeTimer((Timer) m); 29 | } 30 | if (m instanceof DistributionSummary) { 31 | writeSummary((DistributionSummary) m); 32 | } 33 | if (m instanceof FunctionTimer) { 34 | writeTimer((FunctionTimer) m); 35 | } 36 | if (m instanceof TimeGauge) { 37 | writeGauge(m.getId(), ((TimeGauge) m).value(getBaseTimeUnit())); 38 | } 39 | if (m instanceof Gauge) { 40 | writeGauge(m.getId(), ((Gauge) m).value()); 41 | } 42 | if (m instanceof FunctionCounter) { 43 | writeCounter(m.getId(), ((FunctionCounter) m).count()); 44 | } 45 | if (m instanceof Counter) { 46 | writeCounter(m.getId(), ((Counter) m).count()); 47 | } 48 | if (m instanceof LongTaskTimer) { 49 | writeLongTaskTimer((LongTaskTimer) m); 50 | } 51 | } 52 | } 53 | 54 | 55 | private void writeLongTaskTimer(LongTaskTimer timer) { 56 | Meter.Id id = timer.getId(); 57 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 58 | .addTag(tags2Map(id.getTags())) 59 | .addTag("application", applicationName) 60 | .addTag("host", ip) 61 | .addField("active_tasks", timer.activeTasks()) 62 | .addField("duration", timer.duration(getBaseTimeUnit())) 63 | .build(); 64 | monitorUtil.writePoint(point); 65 | } 66 | 67 | private void writeCounter(Meter.Id id, double count) { 68 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 69 | .addTag(tags2Map(id.getTags())) 70 | .addTag("application", applicationName) 71 | .addTag("host", ip) 72 | .addField("value", count) 73 | .build(); 74 | monitorUtil.writePoint(point); 75 | } 76 | 77 | private void writeGauge(Meter.Id id, Double value) { 78 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 79 | .addTag(tags2Map(id.getTags())) 80 | .addTag("application", applicationName) 81 | .addTag("host", ip) 82 | .addField("value", value) 83 | .build(); 84 | monitorUtil.writePoint(point); 85 | } 86 | 87 | private void writeTimer(FunctionTimer timer) { 88 | Meter.Id id = timer.getId(); 89 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 90 | .addTag(tags2Map(id.getTags())) 91 | .addTag("application", applicationName) 92 | .addTag("host", ip) 93 | .addField("sum", timer.totalTime(getBaseTimeUnit())) 94 | .addField("count", timer.count()) 95 | .addField("mean", timer.mean(getBaseTimeUnit())) 96 | .build(); 97 | monitorUtil.writePoint(point); 98 | } 99 | 100 | private void writeTimer(Timer timer) { 101 | Meter.Id id = timer.getId(); 102 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 103 | .addTag(tags2Map(id.getTags())) 104 | .addTag("application", applicationName) 105 | .addTag("host", ip) 106 | .addField("sum", timer.totalTime(getBaseTimeUnit())) 107 | .addField("count", timer.count()) 108 | .addField("mean", timer.mean(getBaseTimeUnit())) 109 | .addField("upper", timer.max(getBaseTimeUnit())) 110 | .addField("p99", timer.percentile(0.99, getBaseTimeUnit())) 111 | .addField("p95", timer.percentile(0.95, getBaseTimeUnit())) 112 | .addField("p90", timer.percentile(0.90, getBaseTimeUnit())) 113 | .build(); 114 | monitorUtil.writePoint(point); 115 | } 116 | 117 | private void writeSummary(DistributionSummary summary) { 118 | Meter.Id id = summary.getId(); 119 | MonitorPoint point = MonitorPoint.monitorKey(id.getName()) 120 | .addTag(tags2Map(id.getTags())) 121 | .addTag("application", applicationName) 122 | .addTag("host", ip) 123 | .addField("sum", summary.totalAmount()) 124 | .addField("count", summary.count()) 125 | .addField("mean", summary.mean()) 126 | .addField("max", summary.max()) 127 | .build(); 128 | monitorUtil.writePoint(point); 129 | } 130 | 131 | private TimeUnit getBaseTimeUnit() { 132 | return TimeUnit.MILLISECONDS; 133 | } 134 | 135 | /** 136 | * 获取本地IP地址 137 | */ 138 | private String getLocalAddr() { 139 | try { 140 | InetAddress addr = InetAddress.getLocalHost(); 141 | return addr.getHostAddress().toString(); //获取本机ip 142 | } catch (UnknownHostException e) { 143 | e.printStackTrace(); 144 | } 145 | return "unknown"; 146 | } 147 | 148 | /** 149 | * 将Tags转变为Map 150 | */ 151 | private Map tags2Map(List tags) { 152 | Map map = new HashMap<>(); 153 | for (Tag tag : tags) { 154 | map.put(tag.getKey(), tag.getValue()); 155 | } 156 | return map; 157 | } 158 | 159 | 160 | @PostConstruct 161 | public void init() { 162 | this.ip = getLocalAddr(); 163 | this.applicationName = System.getProperty("project.name", "unknown"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/java/com/dianwoda/open/monitor/task/HikariMonitorTask.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.monitor.task; 2 | 3 | import com.dianwoda.open.monitor.serivice.HikariMonitorService; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import io.micrometer.core.instrument.MeterRegistry; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Slf4j 14 | @Component 15 | @ConditionalOnClass(HikariDataSource.class) 16 | public class HikariMonitorTask { 17 | @Resource 18 | private HikariMonitorService hikariMonitorService; 19 | 20 | @Resource 21 | private HikariDataSource dataSource; 22 | 23 | @Scheduled(fixedRate = 1000) 24 | public void monitor() { 25 | hikariMonitorService.doMonitor((MeterRegistry) dataSource.getMetricRegistry()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | # Auto Configure 2 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 3 | com.dianwoda.open.monitor.config.HikariMonitorConfiguration -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/test/java/com/dianwoda/open/hikari/monitor/MonitorTest.java: -------------------------------------------------------------------------------- 1 | package com.dianwoda.open.hikari.monitor; 2 | 3 | import com.dianwoda.open.monitor.annotation.EnableHikariMonitor; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import javax.annotation.Resource; 12 | import java.sql.Connection; 13 | import java.sql.PreparedStatement; 14 | import java.sql.ResultSet; 15 | import java.sql.SQLException; 16 | 17 | @RunWith(SpringRunner.class) 18 | @SpringBootTest(classes = TestApplicationConfiguration.class) 19 | public class MonitorTest { 20 | 21 | @Resource 22 | private HikariDataSource dataSource; 23 | 24 | @Test 25 | public void test() throws SQLException { 26 | System.out.println(dataSource.getMetricRegistry()); 27 | String sql = "select * from `user` where id = 1"; 28 | Connection connection = dataSource.getConnection(); 29 | PreparedStatement preparedStatement = connection.prepareStatement(sql); 30 | ResultSet resultSet = preparedStatement.executeQuery(); 31 | while (resultSet.next()) { 32 | System.out.println(String.format("id=%s,name=%s,sex=%s", resultSet.getLong("id"), resultSet.getString("name"), resultSet.getString("sex"))); 33 | } 34 | } 35 | } 36 | 37 | @EnableHikariMonitor 38 | @SpringBootApplication 39 | class TestApplicationConfiguration { 40 | } 41 | -------------------------------------------------------------------------------- /2019-03/hikari-monitor/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 2 | spring.datasource.type=com.zaxxer.hikari.HikariDataSource 3 | spring.datasource.url=jdbc:mysql://localhost:3306/learn?serverTimezone=GMT 4 | spring.datasource.username=root 5 | spring.datasource.password=root 6 | spring.datasource.hikari.maximum-pool-size= -------------------------------------------------------------------------------- /2019-03/数据库连接池初探.md: -------------------------------------------------------------------------------- 1 | # 数据库连接池初探 2 | 3 | ## 为什么需要连接池 4 | 5 | ### MySQL连接原理 6 | 7 | 所谓的数据库连接操作实际上是MySQL客户端与MySQL服务端进行通信,再细化一点便是连接进程与MySQL服务进程之间的进程通信。常用的进程通信方式有`管道`、`共享内存`、`TCP socket`、`unix domain socket`,而MySQL提供的连接方式也大抵如此。 8 | 9 | - TCP socket 10 | 11 | 这应该是使用最普遍的一种MySQL连接方式,也是我们日常开发过程中使用的,随着微服务化的流行,数据库RDS通常与应用服务器分离,数据库连接几乎都采用这种在TCP连接上建立一个基于网络的连接请求来完成任务。这种连接方式的连接过程如下: 12 | 13 | 1. 应用数据层向DataSource请求数据库连接 14 | 2. DataSource使用数据库Driver打开数据库连接 15 | 3. 创建数据库连接,内部可能创建线程,打开TCP socket 16 | 4. 应用读/写数据库 17 | 5. 如果该连接不再需要就关闭连接 18 | 6. 关闭socket 19 | 20 | - 其它连接方式 21 | 22 | 当客户端和服务端都在同一台服务器上的时候,还可以使用`命名管道`、`共享内存`和`Unix域套接字`来进行连接。这些方式都是本地通信,不经过网卡,不需要进行TCP握手与挥手,性能自然也比TCP Socket的方式要快的多,但我们用不上。 23 | 24 | ### 连接的资源占用 25 | 26 | 在使用TCP Socket的连接方式下,我们来看看MySQL连接需要占用的资源有哪些。 27 | 28 | 1. TCP连接 29 | 30 | tcp连接和断开资源的消耗,如三次握手四次挥手所消耗的时间、协议栈的内存分配等等。 31 | 32 | 2. 线程分配 33 | 34 | MySQL内部会为每个连接创建一个线程,为每个连接分配连接缓冲区和结果缓冲区。虽然内部维护了一个非常类似线程池的`Threads_catched`来避免频繁的创建线程和销毁线程,但它仅仅只是将用过的空闲连接给缓存下来放到池中,在连接不够的时候仍然存在创建连接消耗资源的操作。 35 | 36 | ### 资源池化 37 | 38 | 我们根据MySQL连接过程可以看到,如果每一个请求都需要创建新的数据库连接的话,那么每次DataSource都要通过驱动程序去创建一个新的MySQL连接。这么做的话将非常消耗资源。如果我们提前创建好这些连接,并把连接放在一个集合容器内,然后需要用去取连接,这样不同的请求便可以复用已经创建好的连接。这便是数据库连接池的基本思想,池化资源复用来节省系统资源消耗和降低请求时间。 39 | 40 | ## 连接池初探 41 | 42 | ### 从Class.forName到DataSource 43 | 44 | 在说连接池之前,先来了解一下JDBC驱动中一个非常重要的API。在比较老式的JDBC编程中,通常使用下面的模板代码来获得一个数据库连接: 45 | 46 | ```java 47 | /** 48 | * 连接数据库 49 | * @return 50 | */ 51 | public static Connection getConnection(){ 52 | Connection connection = null; 53 | String url = "jdbc:mysql://localhost:3306/chartroom"; 54 | String user = "root"; 55 | String password = "root"; 56 | try { 57 | Class.forName("com.mysql.jdbc.Driver"); 58 | connection = DriverManager.getConnection(url, user, password); 59 | } catch (SQLException e) { 60 | e.printStackTrace(); 61 | } catch (ClassNotFoundException e) { 62 | e.printStackTrace(); 63 | } 64 | return connection; 65 | } 66 | ``` 67 | 68 | 在老式的JDBC编程中,都是通过`DriverManager`类来连接到数据源并提供Connection类用以操作数据库的。但在`JDBC 2.0`的 API 中新增了`DataSource`接口,它提供了连接到数据源的另一种方法。使用 `DataSource`对象现在已经是连接到数据源的首选方法。 69 | 70 | 而众多数据库连接池的实现,无论是`tomcat`连接池还是`hikariCP`,其实都是`DataSource`的一种具体实现,可以看作是一个代理。只不过各个连接池在对数据库连接管理的实现层面上有不同的逻辑。 71 | 72 | ## 连接池最佳实践之HikariCP 73 | 74 | `HikariCP`可谓是性能极致的数据库连接池,目前在`spring-boot 2.0`中已被当作是默认连接池来使用。本文的目的在于分享对连接池的了解以及统一团队内的连接池选择,因此主要来说说`HikariCP`如何配置和如何监控,不探讨HikariCP内部实现细节,有时间的话可以再细说。 75 | 76 | ### 配置项 77 | 78 | 一些不常用的配置,比如`isAllowPoolSuspension`、`isIsolateInternalQueries`之类的这里不再提及。只说说几个经常使用以及争议比较大的配置。 79 | 80 | | name | 构造器默认值 | 默认配置validate之后的值 | 81 | |-------------------|--------------------------------|--------------------------| 82 | | minIdle | -1 | 10 | 83 | | maxPoolSize | -1 | 10 | 84 | | maxLifetime | MINUTES.toMillis(30) = 1800000 | 1800000 | 85 | | connectionTimeout | SECONDS.toMillis(30) = 30000 | 30000 | 86 | | idleTimeout | MINUTES.toMillis(10) = 600000 | 600000 | 87 | 88 | - 关于pool-size 89 | 90 | 首先给出结论,连接池的大小并不是越大越好的。作者在项目wiki中有一篇关于连接池大小配置的文章,有兴趣可以读一读,[About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)。大意便是尽可能在CPU线程切换的消耗与io阻塞等待消耗中寻找平衡。其中有一点很让人思考,随着SSD硬盘的普及,磁盘IO的时间将大大缩短,当然也意味着更频繁的切换线程。文章中还给了一些测试报告,包括Oracle的实验和某PG项目的测试,结论是前者将连接池大小从2048逐渐减少到96时响应时间从100ms降到了2ms,而作者认为大小为96的连接池仍然太大了。后者则用一个大小为9的连接池运行在一台小型四核i7服务器上可以轻松处理3000个前端用户在6000 TPS下的简单查询, 91 | 92 | 那么连接池大小究竟该设置为多少呢?作者给出了一个"经验性"的结论是`connections = ((core_count * 2) + effective_spindle_count)`,core_count为CPU的核心数、effective_spindle_count为数据库服务器磁盘列阵中的硬盘数。我觉得可以在公式的基础上做压测进行实验,然后根据实验结果的情况进行调整,但怎么看都不要过度配置连接池。 93 | 94 | - 关于fixed-pool-design 95 | 96 | hikari作者比较倾向于连接池是固定大小的,一方面是因为如果将minIdle与maxPoolSize设置成不同的话,在流量激增的时候,请求会阻塞在`getConnection()`方法上会造成性能损失;另一方面作者认为即便设置成一样空闲的连接对整体的性能并不会有多大的影响,觉得设置min和max在必要的时候可以释放连接以释放内存给其他功能用的逻辑不成立,因为在高峰时仍然会达到maxPoolSize的量级消耗掉这部分内存,既然高峰时都能牺牲掉省下的这部分内存为何在空闲时又会需要。 97 | 98 | 我是比较赞同作者的观点的,尤其是在当下微服务盛行,每个服务连的数据库都是不同的,不会有多个服务的多个连接加起来导致数据库连接有压力。把min和max设置成一样当作固定大小的连接池来使用我觉得是十分合理的,不会在流量激增的时候出现阻塞导致拿不到连接抛出异常的问题,同时也提高了系统性能。 99 | 100 | - 其它配置 101 | 102 | 1. hikari多了一个`maxLifetime`的配置,该配置用来指定所有连接的存活时间,即所有的连接在maxLifetime之后都得重连一次,保证连接池的活性。 103 | 104 | 2. idleTimeout仅仅在连接池不是fixed的时候才生效,用来消除高峰流量激增时生成的多余空闲连接。 105 | 106 | ### 监控项 107 | 108 | HikariCP本身设计以性能为主,也经常在网上被用来和阿里的`Druid`进行比较,后者号称为监控而生。但实际上`HikariCP`在拥有极佳性能的前提下,对监控的支持也并不算弱。`HikariCP`自身提供了`IMetricsTracker`接口,并给了`micrometer`、`dropwizard`、`prometheus`三种度量统计收集器的实现,并在运行时将连接池的各个状态值上传到初始化连接池时设置的`MeterRegistry`(可以看作是一个度量数据收集中心),想要监控只需要起个定时任务定时的从`MeterRegistry`把上传进去的值给取出来写到influxDB里结合监控系统就行了。 109 | 110 | 以下是`hikari`提供的监控指标: 111 | 112 | | 监控指标 | meter类型 | 描述 | 113 | |-------------------------------|-----------|--------------------------------------------| 114 | | hikaricp.connections.min | Gauge | minIdle,最小空闲数量 | 115 | | hikaricp.connections | Gauge | 连接池当前所有连接数 | 116 | | hikaricp.connections.idle | Gauge | 当前空闲连接数量 | 117 | | hikaricp.connections.max | Gauge | maxPoolSize,连接池最大连接数 | 118 | | hikaricp.connections.creation | Timer | 创建一个新的连接所需的时间 | 119 | | hikaricp.connections.active | Gauge | 活跃连接数 | 120 | | hikaricp.connections.pending | Gauge | 当前排队获取连接的线程数 | 121 | | hikaricp.connections.acquire | Timer | 获取连接时间 | 122 | | hikaricp.connections.usage | Timer | 一个事务执行耗时。即连接被租用到归还的耗时 | 123 | | hikaricp.connections.timeout | Counter | 从池中获取连接超时的数量 | 124 | 125 | 这些指标均会随着程序运行而产生不同的值。这里先对各个meter类型稍作解释: 126 | 127 | - Gauge 可以理解为上下浮动的度量,但存在上下界。 128 | - Counter 无限自增的度量,不存在上届,只要数据没被清除就会一直增加。 129 | - Timer 与时间有关的度量,比如请求的耗时之类。 130 | 131 | 这些指标中,我们尤其要关注当前排队获取连接的线程数,因为我们鼓励将连接池设置成fixed的,那么我们就要做好等待线程数的监控,以免真的连接池容量配的太少导致等待线程数过多而发生请求超时。 132 | 133 | 监控项目在2019-03/hikari-monitor目录下,有需要的可以clone之后查看。下面给出监控打点代码片段, 134 | 135 | ```java 136 | /** 137 | * 从meterRegistry中取出上传的merter数量写到influxdb中去 138 | */ 139 | public void doMonitor() { 140 | for (Meter m : meterRegistry.getMeters()) { 141 | if (m instanceof Timer) { 142 | writeTimer((Timer) m); 143 | } 144 | if (m instanceof Gauge) { 145 | writeGauge(m.getId(), ((Gauge) m).value()); 146 | } 147 | if (m instanceof Counter) { 148 | writeCounter(m.getId(), ((Counter) m).count()); 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | ## 参考文章 155 | 156 | - [About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing) 157 | - [浅析数据库连接池(一)](https://blog.csdn.net/wwh578867817/article/details/46451193) 158 | - [再次了解JDBC(上)- 从Class.forName到DataSource](https://benweizhu.github.io/blog/2014/07/06/learning-jdbc-from-class-forname-to-datasource/) 159 | - [聊聊hikari连接池的fixed pool design](https://segmentfault.com/a/1190000013164535) -------------------------------------------------------------------------------- /2020-10/MySQL是怎样连接的-笔记(1).md: -------------------------------------------------------------------------------- 1 | # MySQL是怎样连接的-笔记(1) 2 | 3 | > 本文为[MySQL是怎样运行的](https://juejin.im/book/5bffcbc9f265da614b11b731)的读书笔记。按书的思路以及自己的理解整理了MySQL总体的内容。 4 | 5 | ## 启动与配置 6 | 7 | MySQL是典型的C/S架构程序。`mysqld`为服务端程序,`mysql`为客户端程序。我们有可能并不直接运行`mysqld`,例如运行`mysql.server`、`mysqld_safe`,这些命令或脚本最终都调用了`mysqld`来启动MySQL服务,但他们更强大,在启动服务的同时提供了如自动重启等高级功能。 8 | 9 | MySQL启动的时候可以通过在启动命令中添加配置参数来配置某些东西,例如`--host(-h)`、`--user(-u)`、`--default-storage-engine`,但更推荐的做法是将常用的配置写到配置文件中去,这样就不需要每次启动服务的时候都写一串命令。 10 | 以类unix操作系统为例,MySQL会按照下列路径依次读取配置文件: 11 | 12 | 1. /etc/my.cnf 13 | 2. /etc/mysql/my.cnf 14 | 3. SYSCONFDIR/my.cnf 15 | 4. $MYSQL_HOME/my.cnf 16 | 5. defaults-extra-file 17 | 6. ~/.my.cnf 18 | 7. ~/.mylogin.cnf 19 | 20 | 另外,配置文件中用`[]`的形式将配置分组,其中,`[server]`组下边的启动选项将作用于所有的服务器程序。比如上文提到的`mysql.server`、`mysqld_safe`因为最终调用的都是`mysqld`,所以他们在启动时都会读取`[mysqld]`中的配置,客户端相关配置也可以按此规律推导。 21 | 22 | 配置文件的优先级按照后面的覆盖前面的逻辑生效。无论是不同的配置文件还是同一个文件中的不同分组,都将以最后一个出现的启动选项为准。 23 | 24 | 最后,很多配置在运行时可以改变。甚至可以针对不同的某个客户端连接设置不同的配置,例如存储引擎。可以将某个客户端连接设置成`MyISAM`,将别的客户端连接设置为`InnoDB`。 25 | 26 | 你可能会纳闷,`default-storage-engine`这个配置项明明是服务端的,但为什么可以通过客户端的启动命令进行改变,那是因为MySQL的配置选项分为`GLOBLAE`与`SESSION`两种作用域,这个例子中的`efault-storage-engine`便是一个`SESSION`级别的配置项,因此也仅仅只对当前连接生效。 27 | 28 | ## 请求与处理 29 | 30 | C/S架构的程序交互都可以抽象成“客户端与服务端建立连接,客户端发送请求 服务端处理请求,返回响应给客户端”。下图是MySQL运行大致的过程: 31 | 32 | ![MySQL运行总览](assets/mysql_flow.png) 33 | 34 | 下面也从这个过程来分步说明一次sql语句的执行过程。 35 | 36 | ### 连接 37 | 38 | 客户端首先要和服务端建立数据库连接,才能向服务端发送增删改查的请求。 39 | 40 | 我们知道MySQL客户端和服务端其实是`mysql`和`mysqld`两个进程,而数据库连接操作实际上是MySQL客户端与MySQL服务端进行通信,那所谓的连接其实就是进程间通信。常用的进程通信方式有管道、共享内存、TCP socket、unix domain socket,而MySQL提供的连接方式也大抵如此: 41 | 42 | - TCP Socket 43 | 这应该是使用最普遍的一种MySQL连接方式,也是我们日常开发过程中使用的,随着微服务化的流行,数据库RDS通常与应用服务器分离,数据库连接几乎都采用这种在TCP连接上建立一个基于网络的连接请求来完成任务。 44 | - 其它连接方式 45 | 当客户端和服务端都在同一台服务器上的时候,还可以使用`命名管道`、`共享内存`和`Unix域套接字`来进行连接。前两种在`Windows`上使用,第三种在`unix`上使用这些方式都是本地通信,不经过网卡,不需要进行TCP握手与挥手,性能自然也比TCP Socket的方式要快的多,但我们用不上。 46 | 47 | ### MySQL服务处理请求 48 | 49 | - MySQL服务端收到客户端的连接后,会为每一个连接创建一个线程来与之交互,并且这个线程在客户端断开连接后会被缓存(MySQL服务端内部的连接池)。 50 | - 在继续下一步之前,MySQL服务器会先去缓存中查询。这个缓存稍微有一点弱,他只会查询与客户端曾经发起的请求数据完全一致的请求。而维护这个缓存又要付出额外的开销,因此这个功能在`MySQL-8`中已经被移除。 51 | - 对客户端发送的请求数据进行解析,将收到的文本数据按MySQL的语法进行解析,得到要什么,根据什么去查,去哪里查等其能识别的内容。在识别之后还会对客户端的查询语句进行优化,例如子查询转连接,外连接转内连接,表达式优化等等。这个优化得到的结果就叫做我们平时说的`执行计划`。 52 | - 在知道去哪里读数据、读什么数据和用什么条件读数据之后,就要开始真正的去执行这个动作了。而这个操作实际上是一个物理磁盘的操作,具体怎么去表里读数据或是具体怎么把数据写到物理存储器中在MySQL中是由`存储引擎`来完成的。`MySQL`的设计也是将`存储引擎`和`MySQL服务器`分开,存储引擎做了与物理磁盘的交互,而将插数据、读索引等具体的动作封装成不同的api供`MySQL服务器`调用。 53 | 54 | ## 请求过程的编码转换 55 | 56 | MySQL从发送请求到接收结果过程中会发生多次字符集转换,转换的过程中涉及对字符的编码和解码,如果编码和解码使用的字符集不一样便会造成乱码。简单记录一下这个过程: 57 | 58 | 1. 客户端使用操作系统的默认字符集对请求语句进行编码发到服务端。 59 | 2. 服务端使用`character_set_client`对应的字符集对客户端发送的请求进行解码,再使用`character_set_connection`对应的字符集对刚解码的字符进行编码。 60 | 3. 如果 `character_set_connection`对应的字符集和即将操作的`列`字符集不一致则还需要进一步转换。 61 | 4. 将从`列`获取到的字节串从该列使用的字符集转换为`character_set_results`对应的字符集后发送到客户端。 62 | 5. 客户端使用操作系统的字符集对结果进行解析。 -------------------------------------------------------------------------------- /2020-10/MySQL是怎样连接的-笔记(2).md: -------------------------------------------------------------------------------- 1 | # MySQL是怎样连接的-笔记(2) 2 | 3 | > 本文为[MySQL是怎样运行的](https://juejin.im/book/5bffcbc9f265da614b11b731)的读书笔记第二篇。按书的思路以及自己的理解整理了MySQL索引原理的相关内容。 4 | 5 | ## Innodb索引页与行格式 6 | 要说清楚索引,首先需要说明白的是`Innodb索引页`和`Innodb行格式`。`页`是MySQL存储数据的基本单位,一个页的大小一般是16KB。MySQL中有各种各样的`页`,而和数据存储相关的便是`索引页`。索引页的示意图如下: 7 | 8 | ![索引页](assets/index_page.png) 9 | 10 | 我们每次插入记录,都是从索引页的`Free Space`中分配内存(分配得到的内存便是`User Records`部分了),然后按照特定的`行格式`将记录存在`User Records`中。 11 | 12 | 那什么是`行格式`?所谓的InnoDB行格式就是记录在磁盘上的存放形式,以`COMPACT`行格式举例,其示意图如下: 13 | 14 | ![compact行格式示意图](assets/compact_row_format.png) 15 | 16 | 因为本文只介绍索引相关的知识,并不需要对`InnoDB行格式`了解的太多,如果对`InnoDB行格式`有兴趣的同学可以去看作者原文。从`COMPACT`行格式示意图可以看到,一条记录除了我们记录的真实信息以外还有一些额外信息,我们需要对`记录头信息`这部分重点关注。 17 | 18 | ![记录头信息](assets/innodb_row_format_record_header.png) 19 | 20 | 其中最最重要的两个字段是`n_owned`和`next_record`两个字段。`n_owned`表示索引页对页内数据分组后每组有多少条记录,至于什么是分组,我们后面再详细说。`next_record`表示当前记录到下一条记录的距离,由此可见记录和记录之间通过`next_record`形成了一条单向链表。 21 | 22 | ## 索引页中的分组与槽 23 | 再来仔细看看,索引页中的数据究竟是如何存储的。 24 | 25 | ![索引页中的记录](assets/record_in_index_page.png) 26 | 27 | 这个图里没有将`next_record`的箭头画出,这里就自行想象一下吧。接下来我们来解释一下这张图。 28 | 29 | 1. 最小记录和最大记录:innodb索引页中规定了一个`页`中有两条固定的记录,分别是最小记录和最大记录。 30 | 2. 分组与槽:索引页按照主键大小将`页`里面的数据分成多个组,每组的记录条数在4-8条之间,如果一直往一个组内添加记录直到超过了8条那么则分裂出第二个组。然后将每一组中最大的那条记录单独摘出来放在`页目录`中称之为`槽`。 31 | 32 | 当我们搞清楚`槽`和`页目录`的概念后我们就能知道如何在一`页`中查找一条记录了。因为槽与槽是按顺序存在页目录中的,他们代表的记录也是按主键值排序的。那么可以使用二分查找确定待查询记录所在分组对应的槽,然后按`next_record`遍历槽对应的组中的各个记录。因为这个记录很少,所以查询速度极快。 33 | 34 | 回头想一想,假设没有`槽`和`页目录`的设计,我们又该如何查找呢?那就得从`页`的最小记录开始依次往后遍历,直到找到待查记录,当页内记录非常多时,查询效率就变得非常低下。 35 | 36 | ## 索引 37 | 38 | 聊完索引页中的分组和槽,我们只能解决在一个`页`中的根据主键查询的问题。如果记录数多到有好多页,那我们就需要先定位到对应的页才能根据槽去查询;又或者我们并不以主键作为条件去查询记录,而上述的高效率查找方式是仅适用于主键查找的。这时候就需要`索引`来解决这两个问题了。 39 | 40 | ### 记录数有很多页怎么办 41 | 42 | 从`索引页`的存储结构(槽与页目录)中得到灵感,直接为`索引页`本身建立一个目录不就好了吗?而MySQL也确实是这么做的。将每一页的最小用户记录的`主键`和`页号`单独摘出来放在一个单独的地方,那我们就可以使用二分查找快速定位到记录所在的页,然后在一页中采用之前说的方案去定位记录就好了。 43 | 44 | 然后要解决的问题就是这个单独存放`主键`和`页号`用来索引`页`的结构应该长啥样,这个问题的难点一个在于记录越来越多时需要很大的存储空间,另一个在于记录存在增删改时对应的索引结构也需要发生变化。仔细想一想,这里两个难点是不是跟存储记录时的问题是一样的?既然是一样的,那直接复用存储记录的`索引页`便可以了(当然了,有一个行格式中的record_type字段区分了两种页的类型)。我们来看一下图。 45 | 46 | ![b+树索引](assets/b+_tree.png) 47 | 48 | 在这种结构下实现快速定位记录就需要先找到存储`用户记录页`页号的页,然后在存储`用户记录页`页号的页当中确定用户记录页,再根据槽和页目录找到记录。你很快就会发现当数据越来越多的时候,定位存储`用户记录页`页号的页效率也变得很低,那解决方案自然是往上再加一层索引。这个数据结构就叫`B+树`,这棵以主键构造的`B+树`被叫做`聚簇索引`,它的叶子节点存储的是完整的用户记录。 49 | 50 | ### 不以主键为条件的查找怎么办 51 | 52 | 解决这个问题的方案是以别的条件再建一颗甚至多棵`B+树`,当然这里新建的树的排序规则就得是对应字段的排序规则。不同于主键索引的B+树,以其他条件建立的树的叶子节点中只存了当前索引字段和主键的值。如果你想得到完整的用户记录,那么得拿着这棵树中得到的主键值再去主键索引中查询一次,这个过程叫做`回表`,也正因为如此,以非主键字段构建的B+树索引成为`二级索引`。 53 | 54 | ### 索引别的知识点 55 | 56 | 1. B+树的根节点随着索引创建而创建,在插入记录后,会把用户记录存储到根节点中,在根节点的可用空间被存满后,会新分配一个页,从根节点复制所有数据并完成页分裂,然后将分裂后的索引数据存到根节点中。根节点自身的页号会被存储到某个地方,这个便是`数据字典`。 57 | 58 | 2. 二级索引中不仅会存索引列的字段值,同时也会存主键的值,在索引列值相同时使用主键排序。 59 | 60 | 3. 联合索引的排序规则为先按第一个索引列进行排序,如果第一个索引列的值相同才进行第二个索引列的排序。 61 | -------------------------------------------------------------------------------- /2020-10/assets/b+_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/b+_tree.png -------------------------------------------------------------------------------- /2020-10/assets/compact_row_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/compact_row_format.png -------------------------------------------------------------------------------- /2020-10/assets/index_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/index_page.png -------------------------------------------------------------------------------- /2020-10/assets/innodb_row_format_record_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/innodb_row_format_record_header.png -------------------------------------------------------------------------------- /2020-10/assets/mysql_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/mysql_flow.png -------------------------------------------------------------------------------- /2020-10/assets/record_in_index_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/assets/record_in_index_page.png -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/build.gradle: -------------------------------------------------------------------------------- 1 | subprojects { 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | group 'com.github.oneone1995' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | maven {url 'http://maven.aliyun.com/nexus/content/groups/public/'} 10 | mavenCentral() 11 | } 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/mybatis-spring-demo/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapper/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compileOnly 'org.projectlombok:lombok:1.18.16' 3 | compile 'org.mybatis:mybatis:3.5.6' 4 | } -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapper/src/main/java/com/github/oneone1995/mybatis/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis.domain; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString 8 | public class User { 9 | private Long id; 10 | 11 | private String name; 12 | 13 | private Integer age; 14 | } 15 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapper/src/main/java/com/github/oneone1995/mybatis/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis.mapper; 2 | 3 | import com.github.oneone1995.mybatis.domain.User; 4 | import org.apache.ibatis.annotations.Select; 5 | 6 | import java.util.List; 7 | 8 | public interface UserMapper { 9 | @Select("select * from user") 10 | List findAll(); 11 | } 12 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapperscan/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | compile project(':mybatis-mapper') 4 | 5 | compileOnly 'org.projectlombok:lombok:1.18.16' 6 | 7 | compile group: 'com.h2database', name: 'h2', version: '1.4.200' 8 | compile 'org.mybatis:mybatis:3.5.6' 9 | compile 'org.mybatis:mybatis-spring:2.0.5' 10 | compile 'org.springframework:spring-jdbc:5.2.9.RELEASE' 11 | compile 'org.springframework:spring-context:5.2.9.RELEASE' 12 | 13 | testCompile group: 'junit', name: 'junit', version: '4.12' 14 | } -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapperscan/src/main/java/mapperscan/MapperScanRunner.java: -------------------------------------------------------------------------------- 1 | package mapperscan; 2 | 3 | import com.github.oneone1995.mybatis.mapper.UserMapper; 4 | import mapperscan.config.AppConfig; 5 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 6 | 7 | public class MapperScanRunner { 8 | public static void main(String[] args) { 9 | AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); 10 | UserMapper userMapper = applicationContext.getBean(UserMapper.class); 11 | userMapper.findAll().forEach(System.out::println); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-mapperscan/src/main/java/mapperscan/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package mapperscan.config; 2 | 3 | import org.apache.ibatis.session.SqlSessionFactory; 4 | import org.h2.jdbcx.JdbcConnectionPool; 5 | import org.mybatis.spring.SqlSessionFactoryBean; 6 | import org.mybatis.spring.annotation.MapperScan; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import javax.sql.DataSource; 11 | 12 | @Configuration 13 | @MapperScan("com.github.oneone1995.mybatis.mapper") 14 | public class AppConfig { 15 | 16 | @Bean 17 | public DataSource dataSource() { 18 | return JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 19 | } 20 | 21 | @Bean 22 | public SqlSessionFactory sqlSessionFactory() throws Exception { 23 | SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); 24 | factoryBean.setDataSource(dataSource()); 25 | return factoryBean.getObject(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-with-spring/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | compile project(':mybatis-mapper') 4 | 5 | compileOnly 'org.projectlombok:lombok:1.18.16' 6 | 7 | compile group: 'com.h2database', name: 'h2', version: '1.4.200' 8 | compile 'org.mybatis:mybatis:3.5.6' 9 | compile 'org.mybatis:mybatis-spring:2.0.5' 10 | compile 'org.springframework:spring-jdbc:5.2.9.RELEASE' 11 | compile 'org.springframework:spring-context:5.2.9.RELEASE' 12 | 13 | testCompile group: 'junit', name: 'junit', version: '4.12' 14 | } -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-with-spring/src/main/java/com/github/oneone1995/mybatis/Main.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis; 2 | 3 | import com.github.oneone1995.mybatis.config.AppConfig; 4 | import com.github.oneone1995.mybatis.mapper.UserMapper; 5 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 6 | 7 | public class Main { 8 | public static void main(String[] args) { 9 | AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); 10 | UserMapper userMapper = applicationContext.getBean(UserMapper.class); 11 | userMapper.findAll().forEach(System.out::println); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-with-spring/src/main/java/com/github/oneone1995/mybatis/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis.config; 2 | 3 | import com.github.oneone1995.mybatis.mapper.UserMapper; 4 | import org.apache.ibatis.session.Configuration; 5 | import org.apache.ibatis.session.SqlSessionFactory; 6 | import org.h2.jdbcx.JdbcConnectionPool; 7 | import org.mybatis.spring.SqlSessionFactoryBean; 8 | import org.mybatis.spring.mapper.MapperFactoryBean; 9 | import org.springframework.context.annotation.Bean; 10 | 11 | import javax.sql.DataSource; 12 | 13 | @org.springframework.context.annotation.Configuration 14 | public class AppConfig { 15 | 16 | @Bean 17 | public DataSource dataSource() { 18 | return JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 19 | } 20 | 21 | @Bean 22 | public SqlSessionFactory sqlSessionFactory() throws Exception { 23 | SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); 24 | factoryBean.setDataSource(dataSource()); 25 | return factoryBean.getObject(); 26 | } 27 | 28 | @Bean 29 | public UserMapper userMapperFactory() throws Exception { 30 | MapperFactoryBean userMapperFactory = new MapperFactoryBean<>(UserMapper.class); 31 | userMapperFactory.setSqlSessionFactory(sqlSessionFactory()); 32 | Configuration configuration = sqlSessionFactory().getConfiguration(); 33 | configuration.addMapper(UserMapper.class); 34 | return userMapperFactory.getObject(); 35 | } 36 | 37 | /* 38 | @Bean 39 | public XXXMapper xxxMapperFactory() throws Exception { 40 | MapperFactoryBean xxxMapperFactory = new MapperFactoryBean<>(XXXMapper.class); 41 | xxxMapperFactory.setSqlSessionFactory(sqlSessionFactory()); 42 | Configuration configuration = sqlSessionFactory().getConfiguration(); 43 | configuration.addMapper(XXXMapper.class); 44 | return xxxMapperFactory.getObject(); 45 | } 46 | */ 47 | } 48 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-without-spring/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | compile project(':mybatis-mapper') 4 | compile group: 'com.h2database', name: 'h2', version: '1.4.200' 5 | 6 | 7 | testCompile group: 'junit', name: 'junit', version: '4.12' 8 | } -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-without-spring/src/main/java/com/github/oneone1995/mybatis/H2Test.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis; 2 | 3 | import java.sql.Connection; 4 | import java.sql.DriverManager; 5 | import java.sql.ResultSet; 6 | import java.sql.Statement; 7 | 8 | public class H2Test { 9 | 10 | /** 11 | * 以嵌入式(本地)连接方式连接H2数据库 12 | */ 13 | private static final String JDBC_URL = "jdbc:h2:file:./testDB"; 14 | private static final String DRIVER_CLASS = "org.h2.Driver"; 15 | private static final String USER = "root"; 16 | private static final String PASSWORD = "root"; 17 | 18 | public static void main(String[] args) throws Exception { 19 | //与数据库建立连接 20 | Class.forName(DRIVER_CLASS); 21 | Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD); 22 | Statement statement = conn.createStatement(); 23 | //创建表 24 | statement.execute("CREATE TABLE if not exists `user`(id INT(50) PRIMARY KEY, name VARCHAR(50) NOT NULL, age int(50) NOT NULL)"); 25 | 26 | //插入数据 27 | statement.executeUpdate("INSERT INTO USER VALUES(1, '程咬金', 20) "); 28 | statement.executeUpdate("INSERT INTO USER VALUES(2, '孙尚香', 21) "); 29 | statement.executeUpdate("INSERT INTO USER VALUES(3, '猴子', 22) "); 30 | 31 | //查询数据 32 | ResultSet resultSet = statement.executeQuery("select * from `user`"); 33 | while (resultSet.next()) { 34 | System.out.println(resultSet.getInt("id") + ", " + resultSet.getString("name") + ", " + resultSet.getString("age")); 35 | } 36 | //关闭连接 37 | statement.close(); 38 | conn.close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/mybatis-without-spring/src/main/java/com/github/oneone1995/mybatis/WithoutSpringRunner.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.mybatis; 2 | 3 | import com.github.oneone1995.mybatis.mapper.UserMapper; 4 | import org.apache.ibatis.mapping.Environment; 5 | import org.apache.ibatis.session.Configuration; 6 | import org.apache.ibatis.session.SqlSession; 7 | import org.apache.ibatis.session.SqlSessionFactory; 8 | import org.apache.ibatis.session.SqlSessionFactoryBuilder; 9 | import org.apache.ibatis.transaction.TransactionFactory; 10 | import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; 11 | import org.h2.jdbcx.JdbcConnectionPool; 12 | 13 | import javax.sql.DataSource; 14 | 15 | public class WithoutSpringRunner { 16 | 17 | public static void main(String[] args) { 18 | DataSource dataSource = JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 19 | 20 | TransactionFactory transactionFactory = 21 | new JdbcTransactionFactory(); 22 | Environment environment = 23 | new Environment("development", transactionFactory, dataSource); 24 | Configuration configuration = new Configuration(environment); 25 | configuration.addMapper(UserMapper.class); 26 | SqlSessionFactory sqlSessionFactory = 27 | new SqlSessionFactoryBuilder().build(configuration); 28 | 29 | try (SqlSession session = sqlSessionFactory.openSession()) { 30 | UserMapper userMapper = session.getMapper(UserMapper.class); 31 | userMapper.findAll().forEach(System.out::println); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mybatis-spring-demo' 2 | include 'mybatis-without-spring' 3 | include 'mybatis-with-spring' 4 | include 'mybatis-mapper' 5 | include 'mybatis-mapperscan' 6 | -------------------------------------------------------------------------------- /2020-10/mybatis-spring-demo/testDB.mv.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-10/mybatis-spring-demo/testDB.mv.db -------------------------------------------------------------------------------- /2020-10/从Spring与Mybatis整合看Spring的扩展点.md: -------------------------------------------------------------------------------- 1 | # 从Spring与Mybatis整合看Spring的两个扩展点 2 | 3 | ## 一、mybatis核心原理 4 | 5 | 我们都知道,实际开发中我们只需要写Mapper接口,并不需要提供对应的实现类,那为什么能直接注入并运行?要解答这个问题,我们从不与spring整合的mybatis下手,这样能方便我们研究源码。首先我们来写一个demo 6 | 7 | ```java 8 | public class WithoutSpringRunner { 9 | 10 | public static void main(String[] args) { 11 | DataSource dataSource = JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 12 | 13 | TransactionFactory transactionFactory = 14 | new JdbcTransactionFactory(); 15 | Environment environment = 16 | new Environment("development", transactionFactory, dataSource); 17 | Configuration configuration = new Configuration(environment); 18 | configuration.addMapper(UserMapper.class); 19 | SqlSessionFactory sqlSessionFactory = 20 | new SqlSessionFactoryBuilder().build(configuration); 21 | 22 | try (SqlSession session = sqlSessionFactory.openSession()) { 23 | UserMapper userMapper = session.getMapper(UserMapper.class); 24 | //这里能够执行findAll说明userMapper是一个实例,那一定是在getMapper中发生了实例化 25 | userMapper.findAll().forEach(System.out::println); 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 从上面的例子的`getMapper()`方法中点进去,一直往里跟,最后会走到`org.apache.ibatis.binding.MapperRegistry#getMapper`这个方法。 32 | 33 | ```java 34 | public T getMapper(Class type, SqlSession sqlSession) { 35 | final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); 36 | if (mapperProxyFactory == null) { 37 | throw new BindingException("Type " + type + " is not known to the MapperRegistry."); 38 | } 39 | try { 40 | return mapperProxyFactory.newInstance(sqlSession); 41 | } catch (Exception e) { 42 | throw new BindingException("Error getting mapper instance. Cause: " + e, e); 43 | } 44 | } 45 | ``` 46 | 47 | 这里可以看到我们的UserMapper是从`knownMappers`这个map里拿出来的,那必然是有地方放进去的,发现是通过`configuration.addMapper(UserMapper.class);`这行代码将UserMapper放到knownMappers中。这里实际调用的是`org.apache.ibatis.binding.MapperRegistry#addMapper`。 48 | 49 | ```java 50 | public void addMapper(Class type) { 51 | if (type.isInterface()) { 52 | if (hasMapper(type)) { 53 | throw new BindingException("Type " + type + " is already known to the MapperRegistry."); 54 | } 55 | boolean loadCompleted = false; 56 | try { 57 | knownMappers.put(type, new MapperProxyFactory<>(type)); 58 | // It's important that the type is added before the parser is run 59 | // otherwise the binding may automatically be attempted by the 60 | // mapper parser. If the type is already known, it won't try. 61 | MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); 62 | parser.parse(); 63 | loadCompleted = true; 64 | } finally { 65 | if (!loadCompleted) { 66 | knownMappers.remove(type); 67 | } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | 可以看到mybatis为我们的mapper封装了一个`MapperProxyFactory`对象,我们的mapper接口信息最终存在了这个对象的`mapperInterface`属性中,在`getMapper()`时,通过jdk动态代理生成代理对象,这个代理对象里面完成了对JDBC的封装,执行了真正的数据库操作。 74 | 75 | ## 二、如何和spring整合 76 | 77 | 从上文可以知道,mybatis会为每个mapper生成一个代理对象,那么问题来了,如果要和spring整合,怎么才能把这个代理对象交给spring管理?有一种方式是通过@Bean注解将生成的代理对象交给spring。 78 | 79 | ```java 80 | @org.springframework.context.annotation.Configuration 81 | public class AppConfig { 82 | 83 | @Bean 84 | public DataSource dataSource() { 85 | return JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 86 | } 87 | 88 | @Bean 89 | public SqlSessionFactory sqlSessionFactory() throws Exception { 90 | SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); 91 | factoryBean.setDataSource(dataSource()); 92 | return factoryBean.getObject(); 93 | } 94 | 95 | @Bean 96 | public UserMapper userMapperFactory() throws Exception { 97 | MapperFactoryBean userMapperFactory = new MapperFactoryBean<>(UserMapper.class); 98 | userMapperFactory.setSqlSessionFactory(sqlSessionFactory()); 99 | Configuration configuration = sqlSessionFactory().getConfiguration(); 100 | configuration.addMapper(UserMapper.class); 101 | return userMapperFactory.getObject(); 102 | } 103 | 104 | /* 105 | @Bean 106 | public XXXMapper xxxMapperFactory() throws Exception { 107 | MapperFactoryBean xxxMapperFactory = new MapperFactoryBean<>(XXXMapper.class); 108 | xxxMapperFactory.setSqlSessionFactory(sqlSessionFactory()); 109 | Configuration configuration = sqlSessionFactory().getConfiguration(); 110 | configuration.addMapper(XXXMapper.class); 111 | return xxxMapperFactory.getObject(); 112 | } 113 | */ 114 | } 115 | ``` 116 | 117 | 到此我们就完成了mybatis和spring的整合,只是还存在一点缺陷,这样做的问题就是,以后又来了一个XXXMapper、YYYMapper的时候都需要写一遍。 118 | 119 | ## 三、@MapperScan原理分析 120 | 121 | 为了解决第二章中出现的问题,避免每加一个mapper接口都要用@Bean去注册一波,mybatis-spring提供了@MapperScan的注解来解决这个问题。 122 | > 注意: 其实也可以通过org.apache.ibatis.session.Configuration#addMappers(java.lang.String)的API来解决这个问题,但本文其实想说的是Spring的扩展点,所以这里就不讨论addMappers的实现方案了。 123 | 124 | 只需要在spring的配置类上加`@MapperScan`的注解就可以完成所有mapper的扫描,并将mapper对应的代理对象加到spring环境中去。 125 | 126 | ```java 127 | @Configuration 128 | @MapperScan("com.github.oneone1995.mybatis.mapper") 129 | public class AppConfig { 130 | 131 | @Bean 132 | public DataSource dataSource() { 133 | return JdbcConnectionPool.create("jdbc:h2:file:./testDB", "root", "root"); 134 | } 135 | 136 | @Bean 137 | public SqlSessionFactory sqlSessionFactory() throws Exception { 138 | SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); 139 | factoryBean.setDataSource(dataSource()); 140 | return factoryBean.getObject(); 141 | } 142 | } 143 | ``` 144 | 145 | 点进去@MapperScan这个注解。 146 | 147 | ```java 148 | @Retention(RetentionPolicy.RUNTIME) 149 | @Target(ElementType.TYPE) 150 | @Documented 151 | @Import(MapperScannerRegistrar.class) 152 | @Repeatable(MapperScans.class) 153 | public @interface MapperScan { 154 | //....省略 155 | } 156 | ``` 157 | 158 | 可以看到这个注解上加了@Import(MapperScannerRegistrar.class),这里可以理解成将`MapperScannerRegistrar`这个类导入到当前配置类,即让`MapperScannerRegistrar`也生效。再来看`MapperScannerRegistrar`的代码。 159 | 160 | ```java 161 | public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware{ 162 | @Override 163 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 164 | AnnotationAttributes mapperScanAttrs = AnnotationAttributes 165 | .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); 166 | if (mapperScanAttrs != null) { 167 | registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,generateBaseBeanName(importingClassMetadata, 0)); 168 | } 169 | } 170 | 171 | void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, 172 | BeanDefinitionRegistry registry, String beanName) { 173 | 174 | BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); 175 | 176 | registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); 177 | 178 | } 179 | } 180 | ``` 181 | 182 | ### ImportBeanDefinitionRegistra扩展点 183 | 184 | 首先`MapperScannerRegistrar`实现了`ImportBeanDefinitionRegistrar`接口,复写了`registerBeanDefinitions()`方法。这个方法是干什么的呢? 185 | 186 | 这里简单说一下spring bean初始化的流程,简单地说spring会根据特定的规则去扫描包路径,并将符合规则的类转化成对应的`BeanDefinition`对象,放在`BeanDefinition Map`中,后续遍历这个map来实例化。而`ImportBeanDefinitionRegistrar`接口则是提供了一个供程序员手动注册`BeanDefinition`到`BeanDefinition Map`中的扩展点。这里多啰嗦一句,你直接写这么一个类,spring自然是不会执行你这个类里的方法的。。所以这里还需要把这个类通过@Import注解和配置类整合在一起,这样的话spring在做包扫描处理配置类(@Configuration)的时候会将配置类上使用@Import导入的`ImportBeanDefinitionRegistrar`的子类收集到一个map中,并执行这个子类的Aware方法(这里不懂的话就直接跳过这句话好了,在2.0.5这个版本里是没有任何真实的Aware方法的),最终会遍历这个map并执行`ImportBeanDefinitionRegistrar`的方法。 187 | 188 | 在mybatis-spring的这个例子中,便是执行MapperScannerRegistrar的重写方法,最终目的是将`MapperScannerConfigurer`转换成BeanDefinition并放到bd map中。这里跟我们想的不太一样,按正常人的思维,这里只要遍历来自注解中配置的mapper路径,将mapper接口对应的代理对象都注册成BeanDefinition就可以了。但是mybatis-spring这里仅仅是将`MapperScannerConfigurer`这个类给放到bd map中。 189 | 190 | > 其实在mybatis-spring 较老版本中的确就是按我们的想法去完成mapper扫描的。本文的版本为2.0.5。据我所知2.0.0的版本就是按上述在`registerBeanDefinitions()`方法中直接扫描Mapper的方式。 191 | 192 | > 插曲: 别的框架也利用了这个扩展点来做实现自己的组件被spring加载。例如dubbo的@DubboScan 193 | 194 | ### BeanDefinitionRegistryPostProcessor扩展点 195 | 196 | 说到这里,我们仍然没说到mybatis-spring是如何把Mapper交给spring管理的,在`ImportBeanDefinitionRegistra`扩展点中被放到`BeanDefinition Map`中的`MapperScannerConfigurer`到底是什么,他到底干了什么事?我们这里继续点开源码。 197 | 198 | ```java 199 | public class MapperScannerConfigurer 200 | implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { 201 | /** 202 | * {@inheritDoc} 203 | */ 204 | @Override 205 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { 206 | // left intentionally blank 207 | } 208 | 209 | /** 210 | * {@inheritDoc} 211 | * 212 | * @since 1.0.2 213 | */ 214 | @Override 215 | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { 216 | if (this.processPropertyPlaceHolders) { 217 | processPropertyPlaceHolders(); 218 | } 219 | 220 | ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); 221 | scanner.setAddToConfig(this.addToConfig); 222 | scanner.setAnnotationClass(this.annotationClass); 223 | scanner.setMarkerInterface(this.markerInterface); 224 | scanner.setSqlSessionFactory(this.sqlSessionFactory); 225 | scanner.setSqlSessionTemplate(this.sqlSessionTemplate); 226 | scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); 227 | scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); 228 | scanner.setResourceLoader(this.applicationContext); 229 | scanner.setBeanNameGenerator(this.nameGenerator); 230 | scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass); 231 | if (StringUtils.hasText(lazyInitialization)) { 232 | scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization)); 233 | } 234 | scanner.registerFilters(); 235 | scanner.scan( 236 | StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); 237 | } 238 | 239 | /* 240 | * BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors. This means that 241 | * PropertyResourceConfigurers will not have been loaded and any property substitution of this class' properties will 242 | * fail. To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class' bean 243 | * definition. Then update the values. 244 | */ 245 | private void processPropertyPlaceHolders() { 246 | Map prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class, 247 | false, false); 248 | 249 | //如果是AnnotationConfigApplicationContext(我们的例子就是),这里是不会进这个if条件的 250 | //他方法里的beanFactory也是在初始化spring context的时候就已经存在了。 251 | if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) { 252 | BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory() 253 | .getBeanDefinition(beanName); 254 | 255 | // PropertyResourceConfigurer does not expose any methods to explicitly perform 256 | // property placeholder substitution. Instead, create a BeanFactory that just 257 | // contains this mapper scanner and post process the factory. 258 | DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); 259 | factory.registerBeanDefinition(beanName, mapperScannerBean); 260 | 261 | for (PropertyResourceConfigurer prc : prcs.values()) { 262 | prc.postProcessBeanFactory(factory); 263 | } 264 | 265 | PropertyValues values = mapperScannerBean.getPropertyValues(); 266 | 267 | this.basePackage = updatePropertyValue("basePackage", values); 268 | this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values); 269 | this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values); 270 | this.lazyInitialization = updatePropertyValue("lazyInitialization", values); 271 | } 272 | this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null); 273 | this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName) 274 | .map(getEnvironment()::resolvePlaceholders).orElse(null); 275 | this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName) 276 | .map(getEnvironment()::resolvePlaceholders).orElse(null); 277 | this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders) 278 | .orElse(null); 279 | } 280 | } 281 | 282 | ``` 283 | 284 | 这个`MapperScannerConfigurer`又利用了spring的另一个重要扩展点`BeanDefinitionRegistryPostProcessor`。 285 | 286 | 这个扩展点接口继承自`BeanFactoryPostProcessor`,在`BeanFactoryPostProcessor`的基础上新增了`postProcessBeanDefinitionRegistry()`方法。Spring完成bean扫描功能的类(`ConfigurationClassPostProcessor`)就是实现了这个接口,并在该方法内部实现了bean扫描的逻辑。也包括了上文中提到的处理@Import之类等等逻辑。这个扩展点的执行机制特别早,spring内部通过代码控制了`BeanDefinitionRegistryPostProcessor`优先于`BeanFactoryPostProcessor`执行,且spring自身提供的`BeanDefinitionRegistryPostProcessor`扩展优先级最高。这里仔细想一下就很好理解,因为总是要先完成Bean的扫描,才能对扫描出来的Bean做其他的处理。 287 | 288 | > 之后再专门写一篇spring容器初始化的文章来专门解释`ConfigurationClassPostProcessor`这个类 289 | 290 | 我们以mybatis提供的`MapperScannerConfigurer`如何被执行为例,从源码角度来看一下`BeanDefinitionRegistryPostProcessor`扩展点的具体的加载时机与工作流程。 291 | 292 | 1. 容器初始化,走到refresh()方法,调用`invokeBeanFactoryPostProcessors()`方法来执行Bean工厂的后置处理器。spring初始化容器都是通过`BeanFactory的后置处理器`这个概念来完成的,包括Bean的扫描、对Bean完成CGlib代理。所谓的后置处理器就是干预`BeanFactory`的工作流程。 293 | 294 | ```java 295 | public void refresh() throws BeansException, IllegalStateException { 296 | synchronized (this.startupShutdownMonitor) { 297 | //...省略无关代码... 298 | // Invoke factory processors registered as beans in the context. 299 | //这行代码最终会调到 PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()方法 300 | invokeBeanFactoryPostProcessors(beanFactory); 301 | //...省略无关代码... 302 | } 303 | } 304 | ``` 305 | 306 | 2. invokeBeanFactoryPostProcessors()细节,这里直接看我写的注释吧。 307 | 308 | ```java 309 | public static void invokeBeanFactoryPostProcessors( 310 | ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { 311 | 312 | // Invoke BeanDefinitionRegistryPostProcessors first, if any. 313 | Set processedBeans = new HashSet<>(); 314 | 315 | //这里在当前项目下一定会进这个if,beanFactory是DefaultListableBeanFactory 316 | if (beanFactory instanceof BeanDefinitionRegistry) { 317 | BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; 318 | List regularPostProcessors = new ArrayList<>(); 319 | List registryProcessors = new ArrayList<>(); 320 | 321 | //这里不会进,beanFactoryPostProcessors如果没有手动add的话必然是空的 322 | for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { 323 | if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { 324 | BeanDefinitionRegistryPostProcessor registryProcessor = 325 | (BeanDefinitionRegistryPostProcessor) postProcessor; 326 | registryProcessor.postProcessBeanDefinitionRegistry(registry); 327 | registryProcessors.add(registryProcessor); 328 | } 329 | else { 330 | regularPostProcessors.add(postProcessor); 331 | } 332 | } 333 | //临时保存了正在执行的后置处理器 334 | List currentRegistryProcessors = new ArrayList<>(); 335 | 336 | // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. 337 | //第一次执行BeanDefinitionRegistryPostProcessors扩展点 338 | //这里一定是只有一个的,也就是ConfigurationClassPostProcessor 339 | //关于这个类又是什么时候被放进去的呢。。。就不在本文讨论了 340 | String[] postProcessorNames = 341 | beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); 342 | for (String ppName : postProcessorNames) { 343 | if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { 344 | currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); 345 | processedBeans.add(ppName); 346 | } 347 | } 348 | sortPostProcessors(currentRegistryProcessors, beanFactory); 349 | registryProcessors.addAll(currentRegistryProcessors); 350 | //执行完ConfigurationClassPostProcessor的扩展方法,实际上就是完成Bean扫描,包括我们上面的分析 351 | invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); 352 | //注意这里把正在执行的处理器集合给clear了 353 | currentRegistryProcessors.clear(); 354 | 355 | // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. 356 | //第二次执行BeanDefinitionRegistryPostProcessor扩展点 357 | //因为ConfigurationClassPostProcessor完成了Bean扫描 358 | //所以容器中(准确说应该是bd map)有可能又有了新的实现了BeanDefinitionRegistryPostProcessor扩展点的类 359 | 360 | //mybatis-spring实现扩展点的类MapperScannerConfigurer在这时已经被扫描出来放在了bd map中 361 | //所以执行完这行代码后,postProcessorNames中应该包括了MapperScannerConfigurer了 362 | postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); 363 | //但是这里有一个判断,实现的扩展点必须还要实现Ordered接口才会执行,所以这里还是不会执行 364 | for (String ppName : postProcessorNames) { 365 | if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { 366 | currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); 367 | processedBeans.add(ppName); 368 | } 369 | } 370 | sortPostProcessors(currentRegistryProcessors, beanFactory); 371 | registryProcessors.addAll(currentRegistryProcessors); 372 | invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); 373 | currentRegistryProcessors.clear(); 374 | 375 | // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. 376 | //最后一次执行BeanDefinitionRegistryPostProcessor扩展点 377 | //这里面没有任何要求,所以我们的MapperScannerConfigurer自然也在这里被执行 378 | boolean reiterate = true; 379 | while (reiterate) { 380 | reiterate = false; 381 | postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); 382 | for (String ppName : postProcessorNames) { 383 | if (!processedBeans.contains(ppName)) { 384 | currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); 385 | processedBeans.add(ppName); 386 | reiterate = true; 387 | } 388 | } 389 | sortPostProcessors(currentRegistryProcessors, beanFactory); 390 | registryProcessors.addAll(currentRegistryProcessors); 391 | invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); 392 | currentRegistryProcessors.clear(); 393 | } 394 | 395 | // Now, invoke the postProcessBeanFactory callback of all processors handled so far. 396 | invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); 397 | invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); 398 | } 399 | //....省略了一些无关代码....... 400 | //....省略了一些无关代码....... 401 | //....省略了一些无关代码....... 402 | } 403 | ``` 404 | 3. 代码跳回`MapperScannerConfigurer`类,借助`ClassPathMapperScanner`完成了扫描,将指定package下的Mapper交给spring管理。 405 | 406 | 自此整个过程分析完毕。 407 | 408 | > 我目前看主流框架源码中,只有mybatis-spring实现了这个扩展点来实现Mapper扫描的功能。可能作者是为了炫技吧。。不过这种方式来实现扫描的话确实更符合spring的设计初衷。因为spring 409 | 410 | 411 | ## 四、总结 412 | 413 | 本文以mybatis与spring的整合作为切入点,从源码级别分析了mybatis-spring的原理,从而学习了spring的ImportBeanDefinitionRegistra与BeanDefinitionRegistryPostProcessor两个扩展点。项目源码在mybatis-spring-demo中。 -------------------------------------------------------------------------------- /2020-11/Redisson分布式限流器RRateLimiter原理解析.md: -------------------------------------------------------------------------------- 1 | # Redisson分布式限流器RRateLimiter原理解析 2 | 3 | redisson就不多做介绍了,它提供的分布式锁非常强大,一般公司都会选择它在生产环境中使用。但其提供的其他分布式工具就不是那么有名了,比如其提供的分布式限流器`RRateLimiter`网上几乎没有分析它的文章,本文也基于此目的记录一下学习`RRateLimiter`的心得。如有不对,请多指正。 4 | 5 | 6 | ## 简单使用 7 | 8 | 很简单,相信大家都看得懂。 9 | 10 | ```java 11 | public class Main { 12 | public static void main(String[] args) throws InterruptedException { 13 | RRateLimiter rateLimiter = createLimiter(); 14 | 15 | 16 | int allThreadNum = 20; 17 | 18 | CountDownLatch latch = new CountDownLatch(allThreadNum); 19 | 20 | long startTime = System.currentTimeMillis(); 21 | for (int i = 0; i < allThreadNum; i++) { 22 | new Thread(() -> { 23 | rateLimiter.acquire(1); 24 | System.out.println(Thread.currentThread().getName()); 25 | latch.countDown(); 26 | }).start(); 27 | } 28 | latch.await(); 29 | System.out.println("Elapsed " + (System.currentTimeMillis() - startTime)); 30 | } 31 | 32 | public static RRateLimiter createLimiter() { 33 | Config config = new Config(); 34 | config.useSingleServer() 35 | .setTimeout(1000000) 36 | .setAddress("redis://127.0.0.1:6379"); 37 | 38 | RedissonClient redisson = Redisson.create(config); 39 | RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter"); 40 | // 初始化 41 | // 最大流速 = 每1秒钟产生1个令牌 42 | rateLimiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS); 43 | return rateLimiter; 44 | } 45 | } 46 | ``` 47 | 48 | ## 源码分析 49 | 50 | 1. 创建限流器源码 51 | 52 | ```lua 53 | redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]); 54 | redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]); 55 | return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]); 56 | ``` 57 | 58 | 很简单,就是把速率和模式(单机or集群)放到hash中就结束了。 59 | 60 | 2. 获取令牌 61 | 62 | 通过Demo的代码示例点进去,最后可以看到执行lua脚本拿令牌的代码在`org.redisson.RedissonRateLimiter#tryAcquireAsync(org.redisson.client.protocol.RedisCommand, java.lang.Long)`这个方法里,我把它的lua脚本拷出来写了注释 63 | 64 | ```lua 65 | 66 | -- 速率 67 | local rate = redis.call("hget", KEYS[1], "rate") 68 | -- 时间区间(ms) 69 | local interval = redis.call("hget", KEYS[1], "interval") 70 | local type = redis.call("hget", KEYS[1], "type") 71 | assert(rate ~= false and interval ~= false and type ~= false, "RateLimiter is not initialized") 72 | 73 | -- {name}:value 分析后面的代码,这个key记录的是当前令牌桶中的令牌数 74 | local valueName = KEYS[2] 75 | 76 | -- {name}:permits 这个key是一个zset,记录了请求的令牌数,score则为请求的时间戳 77 | local permitsName = KEYS[4] 78 | 79 | -- 单机限流才会用到,集群模式不用关注 80 | if type == "1" then 81 | valueName = KEYS[3] 82 | permitsName = KEYS[5] 83 | end 84 | 85 | -- 原版本有bug(https://github.com/redisson/redisson/issues/3197),最新版将这行代码提前了 86 | -- rate为1 arg1这里是 请求的令牌数量(默认是1)。rate必须比请求的令牌数大 87 | assert(tonumber(rate) >= tonumber(ARGV[1]), "Requested permits amount could not exceed defined rate") 88 | 89 | -- 第一次执行这里应该是null,会进到else分支 90 | -- 第二次执行到这里由于else分支中已经放了valueName的值进去,所以第二次会进if分支 91 | local currentValue = redis.call("get", valueName) 92 | if currentValue ~= false then 93 | -- 从第一次设的zset中取数据,范围是0 ~ (第二次请求时间戳 - 令牌生产的时间) 94 | -- 可以看到,如果第二次请求时间距离第一次请求时间很短(小于令牌产生的时间),那么这个差值将小于上一次请求的时间,取出来的将会是空列表。反之,能取出之前的请求信息 95 | -- 这里作者将这个取出来的数据命名为expiredValues,可认为指的是过期的数据 96 | local expiredValues = redis.call("zrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval) 97 | local released = 0 98 | -- lua迭代器,遍历expiredValues,如果有值,那么released等于之前所有请求的令牌数之和,表示应该释放多少令牌 99 | for i, v in ipairs(expiredValues) do 100 | local random, permits = struct.unpack("fI", v) 101 | released = released + permits 102 | end 103 | 104 | -- 没有过期请求的话,released还是0,这个if不会进,有过期请求才会进 105 | if released > 0 then 106 | -- 移除zset中所有元素,重置周期 107 | redis.call("zrem", permitsName, unpack(expiredValues)) 108 | currentValue = tonumber(currentValue) + released 109 | redis.call("set", valueName, currentValue) 110 | end 111 | 112 | -- 这里简单分析下上面这段代码: 113 | -- 1. 只有超过了1个令牌生产周期后的请求,expiredValues才会有值。 114 | -- 2. 以rate为3举例,如果之前发生了两个请求那么现在released为2,currentValue为1 + 2 = 3 115 | -- 以此可以看到,redisson的令牌桶放令牌操作是通过请求时间窗来做的,如果距离上一个请求的时间已经超过了一个令牌生产周期时间,那么令牌桶中的令牌应该得到重置,表示生产rate数量的令牌。 116 | 117 | -- 如果当前令牌数 < 请求的令牌数 118 | if tonumber(currentValue) < tonumber(ARGV[1]) then 119 | -- 从zset中找到距离当前时间最近的那个请求,也就是上一次放进去的请求信息 120 | local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); 121 | local random, permits = struct.unpack("fI", nearest[1]) 122 | -- 返回 上一次请求的时间戳 - (当前时间戳 - 令牌生成的时间间隔) 这个值表示还需要多久才能生产出足够的令牌 123 | return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval) 124 | else 125 | -- 如果当前令牌数 ≥ 请求的令牌数,表示令牌够多,更新zset 126 | redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1])) 127 | -- valueName存的是当前总令牌数,-1表示取走一个 128 | redis.call("decrby", valueName, ARGV[1]) 129 | return nil 130 | end 131 | else 132 | -- set一个key-value数据 记录当前限流器的令牌数 133 | redis.call("set", valueName, rate) 134 | -- 建了一个以当前限流器名称相关的zset,并存入 以score为当前时间戳,以lua格式化字符串{当前时间戳为种子的随机数、请求的令牌数}为value的值。 135 | -- struct.pack第一个参数表示格式字符串,f是浮点数、I是长整数。所以这个格式字符串表示的是把一个浮点数和长整数拼起来的结构体。我的理解就是往zset里记录了最后一次请求的时间戳和请求的令牌数 136 | redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1])) 137 | -- 从总共的令牌数 减去 请求的令牌数。 138 | redis.call("decrby", valueName, ARGV[1]) 139 | return nil 140 | end 141 | 142 | ``` 143 | 144 | 总结一下,redisson用了`zset`来记录请求的信息,这样可以非常巧妙的通过比较score,也就是请求的时间戳,来判断当前请求距离上一个请求有没有超过一个令牌生产周期。如果超过了,则说明令牌桶中的令牌需要生产,之前用掉了多少个就生产多少个,而之前用掉了多少个令牌的信息也在zset中保存了。 145 | 146 | 然后比较当前令牌桶中令牌的数量,如果足够多就返回了,如果不够多则返回到下一个令牌生产还需要多少时间。这个返回值特别重要。 147 | 148 | 149 | 接下来就是回到java代码,各个API入口点进去,最后都会调到`org.redisson.RedissonRateLimiter#tryAcquireAsync(long, org.redisson.misc.RPromise, long)`这个方法。我也拷出来做了简单的注释。 150 | ```java 151 | private void tryAcquireAsync(long permits, RPromise promise, long timeoutInMillis) { 152 | long s = System.currentTimeMillis(); 153 | RFuture future = tryAcquireAsync(RedisCommands.EVAL_LONG, permits); 154 | future.onComplete((delay, e) -> { 155 | if (e != null) { 156 | promise.tryFailure(e); 157 | return; 158 | } 159 | 160 | if (delay == null) { 161 | //delay就是lua返回的 还需要多久才会有令牌 162 | promise.trySuccess(true); 163 | return; 164 | } 165 | 166 | //没有手动设置超时时间的逻辑 167 | if (timeoutInMillis == -1) { 168 | //延迟delay时间后重新执行一次拿令牌的动作 169 | commandExecutor.getConnectionManager().getGroup().schedule(() -> { 170 | tryAcquireAsync(permits, promise, timeoutInMillis); 171 | }, delay, TimeUnit.MILLISECONDS); 172 | return; 173 | } 174 | 175 | //el 请求redis拿令牌的耗时 176 | long el = System.currentTimeMillis() - s; 177 | //如果设置了超时时间,那么应该减去拿令牌的耗时 178 | long remains = timeoutInMillis - el; 179 | if (remains <= 0) { 180 | //如果那令牌的时间比设置的超时时间还要大的话直接就false了 181 | promise.trySuccess(false); 182 | return; 183 | } 184 | //比如设置的的超时时间为1s,delay为1500ms,那么1s后告知失败 185 | if (remains < delay) { 186 | commandExecutor.getConnectionManager().getGroup().schedule(() -> { 187 | promise.trySuccess(false); 188 | }, remains, TimeUnit.MILLISECONDS); 189 | } else { 190 | long start = System.currentTimeMillis(); 191 | commandExecutor.getConnectionManager().getGroup().schedule(() -> { 192 | //因为这里是异步的,所以真正再次拿令牌之前再检查一下过去了多久时间。如果过去的时间比设置的超时时间大的话,直接false 193 | long elapsed = System.currentTimeMillis() - start; 194 | if (remains <= elapsed) { 195 | promise.trySuccess(false); 196 | return; 197 | } 198 | //再次拿令牌 199 | tryAcquireAsync(permits, promise, remains - elapsed); 200 | }, delay, TimeUnit.MILLISECONDS); 201 | } 202 | }); 203 | } 204 | ``` 205 | 206 | 再次总结一下,Java客户端拿到redis返回的`下一个令牌生产完成还需要多少时间`,也就是`delay`字段。如果这个delay为null,则表示成功获得令牌,如果没拿到,则过delay时间后通过异步线程再次发起拿令牌的动作。这里也可以看到,redisson的RateLimiter是非公平的,多个线程同时拿不到令牌的话并不保证先请求的会先拿到令牌。 207 | 208 | ## 总结 209 | 210 | 因为公司的开放网关的限流模块就是基于Redisson开发的,之前看的版本源码与最新的已经有很大的不同,趁着整理知识点的机会下了最新版的源码看了一遍。限流这个说简单也简单,说复杂也复杂。不知道是不是我看的东西太少,我觉得redisson的限流器设计非常精巧,感觉把redis玩穿了。 -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.github.oneone1995.demo' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | maven { url "http://maven.aliyun.com/nexus/content/groups/public/" } 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | compile 'org.redisson:redisson:3.13.6' 15 | 16 | compile 'ch.qos.logback:logback-classic:1.2.3' 17 | 18 | testCompile group: 'junit', name: 'junit', version: '4.12' 19 | } 20 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneone1995/blog/b3d6ccaf7a3def6011b044b3c8873793ceb617b1/2020-11/redisson-rratelimiter-demo/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'redisson-rratelimiter-demo' 2 | 3 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/src/main/java/com/github/oneone1995/demo/redisson/rratelimiter/Main.java: -------------------------------------------------------------------------------- 1 | package com.github.oneone1995.demo.redisson.rratelimiter; 2 | 3 | import org.redisson.Redisson; 4 | import org.redisson.api.RRateLimiter; 5 | import org.redisson.api.RateIntervalUnit; 6 | import org.redisson.api.RateType; 7 | import org.redisson.api.RedissonClient; 8 | import org.redisson.config.Config; 9 | 10 | import java.util.concurrent.CountDownLatch; 11 | 12 | public class Main { 13 | public static void main(String[] args) throws InterruptedException { 14 | RRateLimiter rateLimiter = createLimiter(); 15 | 16 | 17 | int allThreadNum = 20; 18 | 19 | CountDownLatch latch = new CountDownLatch(allThreadNum); 20 | 21 | long startTime = System.currentTimeMillis(); 22 | for (int i = 0; i < allThreadNum; i++) { 23 | new Thread(() -> { 24 | rateLimiter.acquire(1); 25 | System.out.println(Thread.currentThread().getName()); 26 | latch.countDown(); 27 | }).start(); 28 | } 29 | latch.await(); 30 | System.out.println("Elapsed " + (System.currentTimeMillis() - startTime)); 31 | } 32 | 33 | public static RRateLimiter createLimiter() { 34 | Config config = new Config(); 35 | config.useSingleServer() 36 | .setTimeout(1000000) 37 | .setAddress("redis://127.0.0.1:6379"); 38 | 39 | RedissonClient redisson = Redisson.create(config); 40 | RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter"); 41 | // 初始化 42 | // 最大流速 = 每1秒钟产生1个令牌 43 | rateLimiter.trySetRate(RateType.OVERALL, 3, 1, RateIntervalUnit.SECONDS); 44 | return rateLimiter; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /2020-11/redisson-rratelimiter-demo/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog 2 | 3 | > 因为以前也没写过博客,所以希望将一些学到的小知识记下来,不是特别想折腾github pages,所以就打算以issue为载体,将相应markdown及相关工程项目代码按日期归档在文件夹供翻阅,并以此readme文件作为文章分类目录。欢迎watch,尽量保持周更(工作后实在太牛马了。。断更好几年),欢迎在issue下拍砖。 4 | > 5 | 6 | 分类目录 7 | 8 | --- 9 | 10 | ## 剑指offer 11 | 12 | 个人没有ACM经验,算法题一直是弱项,但也希望能坚持做完。从牛客网上通过率高的题目开始做,所以这里的目录现在看起来是残缺的 13 | 14 | - [[剑指offer-003]二维数组中的查找](https://github.com/oneone1995/blog/issues/3) 15 | - [[剑指offer-005]从尾到头打印链表](https://github.com/oneone1995/blog/issues/5) 16 | - [[剑指offer-007]用两个栈实现队列](https://github.com/oneone1995/blog/issues/2) 17 | - [[剑指offer-030]最小的k个数](https://github.com/oneone1995/blog/issues/7) (堆排序的一种实现方案也在这里) 18 | 19 | --- 20 | 21 | ## 读书笔记 22 | 23 | - [[Java8实战读书笔记-001] 从匿名内部类与策略模式到Java8的Lambda](https://github.com/oneone1995/blog/issues/8) 24 | - [MySQL是怎样连接的-笔记(1)](https://github.com/oneone1995/blog/issues/11) 25 | - [MySQL是怎样连接的-笔记(2)](https://github.com/oneone1995/blog/issues/14) 26 | 27 | ## 其他 28 | 29 | --- 30 | 31 | - [Hello World](https://github.com/oneone1995/blog/issues/1) 32 | - [秋招面经](https://github.com/oneone1995/blog/issues/4) 33 | - [Windows Subsystem for Linux使用全记录](https://github.com/oneone1995/blog/issues/6) 34 | - [从redis实例迁移导致应用无法获得连接简单说说jedis的sentinel机制](https://github.com/oneone1995/blog/issues/9) 35 | - [数据库连接池初探](https://github.com/oneone1995/blog/issues/10) 36 | - [从Spring与Mybatis整合看Spring的两个扩展点](https://github.com/oneone1995/blog/issues/12) 37 | - [Redisson分布式限流器RRateLimiter原理解析](https://github.com/oneone1995/blog/issues/13) 38 | --------------------------------------------------------------------------------