From e0933143fa8f04d721608eff5084c568031522b5 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月10日 20:31:19 +0800 Subject: [PATCH 01/29] =?UTF-8?q?Update=20Java-=E8=99=9A=E6=8B=9F=E6=9C=BA?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...a-350円231円232円346円213円237円346円234円272円.md" | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git "a/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" "b/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" index 0026a1b..768d022 100644 --- "a/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" +++ "b/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" @@ -465,6 +465,8 @@ System.out.println("40=i5+i6 " + (40 == i5 + i6)); //输出 40=i5+i6 true ## 五、垃圾收集 +[垃圾回收的脑图](http://naotu.baidu.com/file/1eb8ce88025d3d160c2efbf03c7b62b5?token=64d1a334774221b1) + 垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。 ### 判断一个对象是否可被回收 @@ -534,6 +536,13 @@ Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般 当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。 +> Object 的finalize()方法的作用是否与C++的析构函数作用相同? + +- 与C++的析构函数不同,析构函数调用确定,而finalize()方法是不确定的; +- 当垃圾回收器要宣告一个对象死亡时,至少要经历两次标记过程。如果对象在进行可达性分析以后,没有与GC Root直接相连接的引用量,就会被第一次标记,并且判断是否执行finalize()方法;如果这个对象覆盖了finalize()方法,并且未被引用,就会被放置于F-Queue队列,稍后由虚拟机创建的一个低优先级的finalize()线程去执行触发finalize()方法; +- 由于线程的优先级比较低,执行过程随时可能会被终止; +- 给予对象最后一次重生的机会 + ### 引用类型 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 @@ -604,7 +613,7 @@ obj = null; | 弱引用 | 在垃圾回收的时候 | 对象缓存 | GC运行后终止 | | 虚引用 | Unknown | 标记、哨兵 | Unknown | -> 引用队列(ReferenceQueue):当GC(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。无实际的存储结构,存储逻辑依赖于内部节点之间的关系来表达。存储关联的且被GC的软引用,弱引用以及虚引用 +> 引用队列(ReferenceQueue):当GC(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。**如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了**。无实际的存储结构,存储逻辑依赖于内部节点之间的关系来表达。 ### 垃圾收集算法 @@ -785,6 +794,8 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 ## 六、内存分配与回收策略 +[内存回收脑图](http://naotu.baidu.com/file/488e2f0745f7cfff1b03eb1c3d81fe3e?token=df2309819db31dde) + ### Minor GC 和 Full GC - Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 @@ -859,19 +870,6 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 | -XX:NewRatio | 老年代和年轻代内存大小的比例 | | -XX:MaxTenuringThreshold | 对象从年轻代晋升到老年代经过的GC次数的最大阈值 | -### 常见GC相关面试题 - -Object 的finalize()方法的作用是否与C++的析构函数作用相同 - -- 与C++的析构函数不同,析构函数调用确定,而finalize()方法是不确定的; -- 当垃圾回收器要宣告一个对象死亡时,至少要经历两次标记过程。如果对象在进行可达性分析以后,没有与GC Root直接相连接的引用量,就会被第一次标记,并且判断是否执行finalize()方法;如果这个对象覆盖了finalize()方法,并且未被引用,就会被放置于F-Queue队列,稍后由虚拟机创建的一个低优先级的finalize()线程去执行触发finalize()方法; -- 由于线程的优先级比较低,执行过程随时可能会被终止; -- 给予对象最后一次重生的机会 - - - - - ## 七、类加载机制 类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。 @@ -1066,6 +1064,8 @@ System.out.println(ConstClass.HELLOWORLD); 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 +可以避免多份同样的字节码的加载 + 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。 #### 3. 实现 @@ -1083,13 +1083,14 @@ public abstract class ClassLoader { protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { - // First, check if the class has already been loaded + // 首先,自底向上地检查请求的类是否已经被加载过 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { + //自顶向下尝试加载该类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { @@ -1098,8 +1099,7 @@ public abstract class ClassLoader { } if (c == null) { - // If still not found, then invoke findClass in order - // to find the class. + // 在父加载器中无法加载再尝试自己加载 c = findClass(name); } } From 7ef93a2fe403d7f3fc48b55979607e351c653602 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月12日 08:30:43 +0800 Subject: [PATCH 02/29] Java-Notes --- ...235円242円350円257円225円351円242円230円346円225円264円347円220円206円.md" | 2 ++ 1 file changed, 2 insertions(+) diff --git "a/docs/351円235円242円350円257円225円351円242円230円346円225円264円347円220円206円.md" "b/docs/351円235円242円350円257円225円351円242円230円346円225円264円347円220円206円.md" index b8ffca6..34ac080 100644 --- "a/docs/351円235円242円350円257円225円351円242円230円346円225円264円347円220円206円.md" +++ "b/docs/351円235円242円350円257円225円351円242円230円346円225円264円347円220円206円.md" @@ -1,2 +1,4 @@ # ⭐️ 面试题整理 +- [vivo 提前批凉经](https://www.nowcoder.com/discuss/197384?type=0&order=0&pos=24&page=1) + From 98db72158ab26080e80bfb44498c09ee5ef294a6 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月12日 14:54:36 +0800 Subject: [PATCH 03/29] Java-Notes --- ...60347円273円204円351円227円256円351円242円230円.md" | 2760 +++++++++++++++++ ...7346円263円225円-347円233円256円345円275円225円.md" | 2 +- 2 files changed, 2761 insertions(+), 1 deletion(-) create mode 100644 "docs/LeetCode/346円225円260円347円273円204円351円227円256円351円242円230円.md" diff --git "a/docs/LeetCode/346円225円260円347円273円204円351円227円256円351円242円230円.md" "b/docs/LeetCode/346円225円260円347円273円204円351円227円256円351円242円230円.md" new file mode 100644 index 0000000..1f23f83 --- /dev/null +++ "b/docs/LeetCode/346円225円260円347円273円204円351円227円256円351円242円230円.md" @@ -0,0 +1,2760 @@ +# 数组问题 + +## 一、数组基础 + +### 1、移动零(283) + +[283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) + +给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。 + +**示例:** + +``` +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +``` + +**说明**: + +1. 必须在原数组上操作,不能拷贝额外的数组。 +2. 尽量减少操作次数。 + +```java +//思路一: +//1、引入另外一个指针k,用于指向数组中非0元素(原有一个遍历数组的指针i),很显然k <= nums.length-1 +//2、[0,k)中元素是非0元素,i指向非0元素,就与k指向的元素交换,这样保证元素的相对顺序 +public void moveZeroes(int[] nums) { + int k = 0; + for(int i=0;i findDisappearedNumbers(int[] nums) { + List res = new ArrayList(); + if(nums== null || nums.length==0){ + return res; + } + + + for(int i=0;i findDuplicates(int[] nums) { + List res = new ArrayList(); + + for(int i=0;i mid) h = mid - 1; + else l = mid + 1; + } + return l; +} +``` + +```java +//思路二:双指针解法,类似于有环链表中找出环的入口 +//参考 141 、142 +//思路:注意说明里面的要求 +public int findDuplicate(int[] nums) { + // nums 的长度是(n+1),元素的范围在[1,n]之间 + int slow = nums[0]; + int fast = nums[0]; + + while(true){ + slow = nums[slow]; + fast = nums[nums[fast]]; + if(slow == fast){ + break; + } + } + + slow = nums[0]; + while(slow != fast){ + slow = nums[slow]; + fast = nums[fast]; + } + return slow; +} +``` + +### 7、数组的度(697) + +[697. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/) + +给定一个非空且只包含非负数的整数数组 `nums`, 数组的度的定义是指数组里任一元素出现频数的最大值。 + +你的任务是找到与 `nums` 拥有相同大小的度的最短连续子数组,返回其长度。 + +**示例 1:** + +``` +输入: [1, 2, 2, 3, 1] +输出: 2 +解释: +输入数组的度是2,因为元素1和2的出现频数最大,均为2. +连续子数组里面拥有相同度的有如下所示: +[1, 2, 2, 3, 1], [1, 2, 2, 3], [2, 2, 3, 1], [1, 2, 2], [2, 2, 3], [2, 2] +最短连续子数组[2, 2]的长度为2,所以返回2. +``` + +**示例 2:** + +``` +输入: [1,2,2,3,1,4,2] +输出: 6 +``` + +**注意:** + +- `nums.length` 在1到50,000区间范围内。 +- `nums[i]` 是一个在0到49,999范围内的整数。 + +```java +public int findShortestSubArray(int[] nums) { + //统计 nums 中数字出现的次数 + HashMap freq = new HashMap(); + // + HashMap numLastIndex = new HashMap(); + // + HashMap numFirstIndex = new HashMap(); + + for(int i=0;itarget){ + r = mid-1; + }else{ + l = mid+1; + } + } + return -1; +} +``` + +```java +//写法二:在区间 [l,r)中查找target元素 +public int search(int[] nums, int target) { + int l = 0; + int r = nums.length; + while(ltarget){ + r = mid; + }else{ + l = mid+1; + } + } + return -1; +} +``` + + + +### 2、第一个错误版本(278) + +[278. 第一个错误的版本(Easy)](https://leetcode-cn.com/problems/first-bad-version/) + +问题描述:你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 `n` 个版本 `[1, 2, ..., n]`,你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用 `bool isBadVersion(version)` 接口来判断版本号 `version` 是否在单元测试中出错。 + +实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。 + +```html +示例: + +给定 n = 5,并且 version = 4 是第一个错误的版本。 + +调用 isBadVersion(3) -> false +调用 isBadVersion(5) -> true +调用 isBadVersion(4) -> true + +所以,4 是第一个错误的版本。 +/* The isBadVersion API is defined in the parent class VersionControl. + boolean isBadVersion(int version); */ +``` + +```java +public class Solution extends VersionControl { + //思路: + //如果第 m 个版本出错(即 isisBadVersion(mid) == true), + //则表示第一个错误的版本在 [l, m] 之间,令 r = m ; + //否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。 + //注意:这里判断条件 l < r + public int firstBadVersion(int n) { + int l = 1; + int r = n; + while(l mid){ + l = mid + 1; + }else{ + assert sqrt < mid; + r = mid - 1; + } + } + //循环结束时 l> r,这里忽略小数部分。 + return r; + } +} +``` + +### 4、寻找旋转排序数组中的最小值(153) + +[153 Find Minimum in Rotated Sorted Array](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) + +问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。请找出其中最小的元素。 + +```html +示例 1: +输入: [3,4,5,1,2] +输出: 1 +示例 2: +输入: [4,5,6,7,0,1,2] +输出: 0 +``` + +```java + +/** +* 思路: +* 第一个小于前一个元素的元素,就是最小值。 +* 我们通过二分查找的,进行优化。 +*/ +class Solution { + public int findMin(int[] nums) { + if(nums.length == 1){ + return nums[0]; + } + if(nums.length == 2){ + return Math.min(nums[0],nums[1]); + } + int l = 0; + int h = nums.length -1; + while(l<=h){ + if(l==h){ + return nums[l]; + } + int mid = (h-l)/2 + l; + if(nums[mid]> nums[mid+1]){ + return nums[mid+1]; + } + if(nums[mid] < nums[h]){ + h = mid; + }else if(nums[mid]> nums[h]){ + l = mid; + } + } + return nums[l]; + } +} +``` + +### 5、搜索旋转排序数组(33) + +[33 Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/description/) + +问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。 + +```html +示例 1: +输入: nums = [4,5,6,7,0,1,2], target = 0 +输出: 4 +示例 2: +输入: nums = [4,5,6,7,0,1,2], target = 3 +输出: -1 +``` + +```java +//思路:非常重要的性质,对于数组nums [0,1,2,3,4,5,6,7],有7种旋转方法,包括原来的数组 +//[0 1 2 {3} 4 5 6 7] +//[1 2 3 {4} 5 6 7 0] +//[2 3 4 {5} 6 7 0 1] +//[3 4 5 {6} 7 0 1 2] +//[5 6 7 {0} 1 2 3 4] +//[6 7 0 {1} 2 3 4 5] +//[7 0 1 {2} 3 4 5 6] +//可以看出 +//当 nums[mid]> nums[r] 时,左半部分是有序的,可以使用二分查找 +//当 nums[mid] < nums[r] 时,右半部分是有序的,可以使用二分查找 +class Solution { + public int search(int[] nums, int target) { + int l = 0; + int r = nums.length -1; + while(l<=r){ + int mid = l + (r-l)/2; + if(nums[mid] == target){ + return mid; + }else if(nums[mid]> nums[r]){ //左半部分[l,mid-1]是有序的。 + if(target>= nums[l] && target < nums[mid]){ //判断target是否在[l,mid-1] + r = mid - 1; + }else{ + l = mid + 1; + } + }else{ //右半部分[mid+1,r]是有序的。 + if(target> nums[mid] && target <= nums[r]){ //判断target是否在[mid+1,,r] + l = mid + 1; + }else{ + r = mid - 1; + } + } + } + return -1; + } +} +``` + +### 6、在排序数组中查找元素的第一个和最后一个位置(34) + +[34 Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/description/) + +问题描述:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。如果数组中不存在目标值,返回 [-1, -1]。 + +```html +示例 1: +输入: nums = [5,7,7,8,8,10], target = 8 +输出: [3,4] +示例 2: +输入: nums = [5,7,7,8,8,10], target = 6 +输出: [-1,-1] +``` + +```java + +class Solution { + public int[] searchRange(int[] nums, int target) { + int first = binarySearchFirstTarget(nums,target); + int last = binarySearchLastTarget(nums,target); + return new int[]{first,last}; + } + + //查找第一个target元素的下标 + private int binarySearchFirstTarget(int[] nums,int target){ + int l = 0; + int r = nums.length-1; + int res = -1; + while(l<=r){ + int mid = l + (r-l)/2; + if( target <= nums[mid]){ + r = mid -1; + }else{ + l = mid + 1; + } + if(target == nums[mid]){ + res = mid; + } + } + return res; + } + + //查找最后一个target元素的小标 + private int binarySearchLastTarget(int[] nums,int target){ + int l = 0; + int r = nums.length-1; + int res = -1; + while(l<=r){ + int mid = l + (r-l)/2; + /*if( target <= nums[mid]){ + r = mid -1; + }else{ + l = mid + 1; + }*/ + if(target>= nums[mid]){ + l = mid + 1; + }else{ + r = mid - 1; + } + if(target == nums[mid]){ + res = mid; + } + } + return res; + } +} +``` + +### 7、爱吃香蕉的珂珂(875) + +[875 Koko Eating Bananas](https://leetcode.com/problems/koko-eating-bananas/description/) + +问题描述:珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。 +如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。 + +```html +示例 1: +输入: piles = [3,6,7,11], H = 8 +输出: 4 +示例 2: +输入: piles = [30,11,23,4,20], H = 5 +输出: 30 +示例 3: +输入: piles = [30,11,23,4,20], H = 6 +输出: 23 +``` + +```java +// 思路:珂珂吃香蕉的最快速度就是 N 个堆中香蕉数目最多的堆中的数目。 +// 可以在 H 小时内吃掉所有香蕉的最小速度 K。就是求他在警卫刚好回来的时候,刚好吃完所有的香蕉。 +class Solution { + public int minEatingSpeed(int[] piles, int H) { + if (piles.length> H ) { + return -1; + } + //maxSpeed KOKO吃香蕉的最快速度 + int maxSpeed=piles[0]; + for(int i=0;iH说明吃的慢了,速度要快起来 + l=mid+1; + } + } + return l; + } + + //以spped速度吃香蕉所花费的时间 + private int hours(int[] piles,int speed){ + int time=0; + for(int pile:piles){ + time += Math.ceil(pile*1.0/speed); + } + return time; + } +} +``` + +### 8、有序数组中的单一元素(540) + +[540. 有序数组中的单一元素(Medium)](https://leetcode-cn.com/problems/single-element-in-a-sorted-array/) + +问题描述:给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。 + +``` +示例 1: +输入: [1,1,2,3,3,4,4,8,8] +输出: 2 +示例 2: +输入: [3,3,7,7,10,11,11] +输出: 10 +``` + +**注意:** 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。 + +```java +//思路: index 为 Single Element 在数组中的位置。 +//如果 m 为偶数, +//m + 1 < index,那么 nums[m] == nums[m + 1]; +//m + 1>= index,那么 nums[m] != nums[m + 1]。 +//从上面的规律可以知道, +//如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2; +//如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。 +//注意循环条件 l < r +public int singleNonDuplicate(int[] nums) { + int l = 0; + int h = nums.length - 1; + while(l len2){ + //这里保持 len1 <= len2,方便后面的操作 + return findKth(nums2,start2,end2,nums1,start1,end1,k); + }else if(len1 == 0){ + //nums1[start1,start2]数组长度为0,则就是在nums2[start2,end2]中第 k 小的元素 + return nums2[start2+k-1]; + }else if(k == 1){ + //合并后的数组的第1个元素,显然是nums1[start1,end1]和nums2[start2,end2]中第一个元素的较小值 + return Math.min(nums1[start1],nums2[start2]); + } + //分治 + int i = Math.min(k/2,len1); //在nums1[start1,end1]中第 i 小元素 + int j = k - i; //在nums2[start2,end2]中第 j 小元素 + if(nums1[start1+i-1]> nums2[start2+j-1]){ //此时 nums2[start2,end2]中就要舍弃前面j个元素 + /** + * 使用反证法证明: + * 证:当k/2>= len1 时,而我们要找的k就在nums2[start2,end2]的前 k/2元素中。 + * 我们假设 k 所在的数组下标记为p,那么nums2[start2,end2]中含有的属于后数组前k个元素的元素有(p+1)个。 + * 显然,nums1[start1,end1]中必然含有另外 k-(p+1)个元素。 + * 由此,得到如下不等式: + * p <= k/2 - 1 (k th 实际所处位置为p,在nums2[start2,end2]的前k/2个元素里。-1是因为现在算的是数组下标,从0开始) + * ==> p + 1 <= k/2; + * ==> k - (p+1)>= k - k/2。 + 显然,len1>= k - (p+1)>= k/2 ,这与上面的假设,k/2>= len1是矛盾的。 + */ + return findKth(nums1,start1,end1,nums2,start2+j,end2,k-j); //此时就是求第(k-j)小元素 + }else if(nums1[start1+i-1] < nums2[start2+j-1]){ //此时 nums1[start1,end1]中就要舍弃前面i个元素 + return findKth(nums1,start1+i,end1,nums2,start2,end2,k-i); + }else{ + return nums1[start1+i-1]; + } +} +``` + +## 三、对撞指针 + +### 1、两数之和-输出有序数组(167) + +[167 Two Sum II - Input array is sorted](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) + +问题描述:给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 +函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 + +说明: +返回的下标值(index1 和 index2)不是从零开始的。 +你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 + +``` +示例: +输入: numbers = [2, 7, 11, 15], target = 9 +输出: [1,2] +解释: +2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 +``` + +``` +//几个问题 +//1、**如果没有解如何处理**? 题目保证有解 +//2、**如果有多个个如何处理**? 返回任意一组即可 +//3、**索引是从0开始还是从1开始**? 索引从1开始 +``` + +``` +//思路一: +// 数组有序,首先想到二分查找,对于nums[i],如果数组中存在两个元素和为target, +// 则必然在nums[i+1...n-1]中存在元素target-nums[i]。 +public int[] twoSum(int[] numbers, int target) { + for(int i=0;i=0 && j>=0){ + if(nums1[i]> nums2[j]){ + nums1[index] = nums1[i--]; + }else{ + nums1[index] = nums2[j--]; + } + index--; + } + // nums1任然有未合并的元素,此时该操作可以省略,因为我们是使用num1存储结果的 + //while(i>=0){ + // nums1[index--] = nums1[i--]; + //} + + // nums2任然有未合并的元素 + while(j>=0){ + nums1[index--] = nums2[j--]; + } + } +``` + + + +### 3、平方数之和(633) + +[633. 平方数之和](https://leetcode-cn.com/problems/sum-of-square-numbers/) + +问题描述:给定一个非负整数 `c` ,你要判断是否存在两个整数 `a` 和 `b`,使得 a^2 + b^2 = c。 + +``` +示例1: +输入: 5 +输出: True +解释: 1 * 1 + 2 * 2 = 5 +``` + +``` +示例2: +输入: 3 +输出: False +``` + +```java +public boolean judgeSquareSum(int c) { + if(c==0){ + return true; + } + + int a = 0; + int b = (int)Math.sqrt(1.0*c); + + while(a<=b){ + int C = a*a + b*b; + if(c == C){ + return true; + }else if(c>C){ + a++; + }else{ + assert c d) { + String longestWord = ""; //当前在字典中最长的单词 + for(String word : d){ + int l1 = longestWord.length(); + int l2 = word.length(); + if((l1>l2) || + (l1==l2 && longestWord.compareTo(word)<0)){ + //longWord 单词已经比 word 单词长了,不考虑该 word + //longWord 和 word 一样长,但是 longWord 字典顺序更小,也不考虑该 word + continue; + } + if(isValid(s,word)){ + longestWord = word; + } + } + return longestWord; + } + + //TODO:判断 word 是否通过删除 s 的某些字符来得到。 + private boolean isValid(String s,String word){ + int i=0,j=0; + while(i max){ + max = tmp; + } + if(height[l]=s){ + ret=Math.min(ret,(r-l+1)); + } + } + //TODO:不能忽视无解的情况 + if(ret==n+1){ + //表示没有找到结果,使得 sum>=s + ret=0; + } + return ret; + } +} + +``` + +### 2、无重复字符的最长子串(3) + +[3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + +问题描述:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 + +``` +示例 1: +输入: "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + +``` + +``` +示例 2: +输入: "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 + +``` + +``` +示例 3: +输入: "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 +请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + +``` + +```java +//思路一:暴力解法,但是超过时间限制 +public int lengthOfLongestSubstring(String s) { + int res = 0; + + for(int i=0;i set = new HashSet(); + for(char c : buf){ + if(set.contains(c)){ + return false; + } + set.add(c); + } + return true; +} + +``` + +``` +//思路二:滑动窗口解法 +//1、l=0,r=-1,[l,r]是滑动窗口,freq[256]数组用于判断该滑动窗口是否存在重复元素: +//2、当加入的元素不是重复元素时,r+1,扩展该窗口 +//3、其他情况,缩小该窗口,l+1 +public int lengthOfLongestSubstring(String s) { + int l = 0, r = -1; + int[] freq = new int[256]; + + int n =s.length(); + //记录最长长度 + int res = 0; + while(l findAnagrams(String s, String p) { + List res = new ArrayList(); + + int l = 0; + int r = p.length(); + //[l,r) 范围内字符,组成字符串 + + while (r<=s.length()){ + String newP = s.substring(l,r); + if(isAnagram(newP,p)){ + res.add(l); + } + l++; + r++; + } + return res; + } + + //判断两个字符串是否是Anagram + //先判断长度是否相同,不相同,直接返回false + //统计 s1字符串中每个小写字母出现的频率,根据s2是否出现相同的字母以及出现的字母的频率是否相同 + private boolean isAnagram(String word1,String word2){ + if(word1.length() != word2.length()){ + return false; + } + + int[] freq = new int[26]; + + for(int i=0;i findAnagrams(String s, String p) { + List ret = new ArrayList(); + if (s == null || s == "") { + return ret; + } + //统计字符串p中出现的小写字符的频率 + int[] pFreq=new int[256]; + //count是p中的字符数 + int count=p.length(); + + for(int i=0;i=1){ + //每次有一个p中字符进入窗口,扩展窗口,并且count–1 + count--; + } + if(count==0){ + //当count == 0的时候,表明我们的窗口中包含了p中的全部字符,得到一个结果。 + ret.add(l); + } + + if (r-l == p.length()) { + //当窗口包含一个结果以后,为了进一步遍历,我们需要缩小窗口使窗口不再包含全部的p, + //同样,如果pFreq[char]>=0,表明一个在p中的字符就要从窗口移动到p字符串中,那么count ++ + if (pFreq[s.charAt(l++)]++>= 0) { + count++; // one more needed to match + } + } + } + return ret; + } +} + +``` + +### 4、最小覆盖子串(76) + +[76 Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/description) + +问题描述:给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。 + +``` +示例: +输入: S = "ADOBECODEBANC", T = "ABC" +输出: "BANC" + +``` + +说明: +如果 S 中不存这样的子串,则返回空字符串 ""。 +如果 S 中存在这样的子串,我们保证它是唯一的答案。 + +```java +/** + * 思路: + * 我们可以考虑哈希表,其中key是T中的字符,value是该字符出现的次数。 + + - 我们最开始先扫描一遍T,把对应的字符及其出现的次数存到哈希表中。 + + - 然后开始遍历S,遇到T中的字符,就把对应的哈希表中的value减一, + 直到包含了T中的所有的字符,纪录一个字串并更新最小字串值。 + + - 将子窗口的左边界向右移,略掉不在T中的字符, + 如果某个在T中的字符出现的次数大于哈希表中的value,则也可以跳过该字符。 + */ +public String minWindow(String s, String t) { + if(s.length() map=new HashMap(); + for(int i=0;i=0){ + count++; + } + //s中出现的字符数刚好包含了t中所有的字符 + while (count == t.length()) { + //[l...r]窗口就是最字符串短 + if (r - l + 1 < minLen) { + minLen = r - l + 1; + ret = s.substring(l, r + 1); + } + //缩小窗口 + if (map.containsKey(s.charAt(l))) { + int sfreq = map.get(s.charAt(l)); + map.put(s.charAt(l), ++sfreq); + if (sfreq> 0) { + --count; + } + } + ++l; + } + } + r++; + } + return ret; +} + +``` + +### 5、乘积小于k的子数组(713) + +[713 Subarray Product Less Than K](https://leetcode.com/problems/subarray-product-less-than-k/description/) + +问题描述:给定一个正整数数组 nums。找出该数组内乘积小于 k 的连续的子数组的个数。 + +``` +示例 1: +输入: nums = [10,5,2,6], k = 100 +输出: 8 +解释: 8个乘积小于100的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。 +需要注意的是 [10,5,2] 并不是乘积小于100的子数组。 + +``` + +说明: +0 < nums.length <= 50000 +0 < nums[i] < 1000 +0 <= k < 10^6 + +```java +/** +* 思路: +* 采用滑动窗口的解法:维护一个数字乘积刚好小于k的滑动窗口窗口, +* 用变量l来记录其左边界的位置,右边界r就是当前遍历到的位置。 +* 遍历原数组,用product乘上当前遍历到的数字, +* 然后进行while循环,如果product大于等于k, +* 则滑动窗口的左边界需要向右移动一位,删除最左边的数字,那么少了一个数字,乘积就会改变, +* 所以用product除以最左边的数字,然后左边右移一位,即l自增1。 +* 当我们确定了窗口的大小后,就可以统计子数组的个数了,就是窗口的大小。 + +* 为什么子数组的个数就是窗口的大小? +* 比如[5 2 6]这个窗口,k还是100,右边界刚滑到6这个位置, +* 这个窗口的大小就是包含6的子数组乘积小于k的个数,即[6], [2 6], [5 2 6],正好是3个。 +* 所以窗口每次向右增加一个数字,然后左边去掉需要去掉的数字后, +* 窗口的大小就是新的子数组的个数,每次加到结果res中即可。 +* +* 注意: +* 这里要求子集的乘积值必须小于k +*/ +class Solution { + public int numSubarrayProductLessThanK(int[] nums, int k) { + if(k<=1){ + return 0; + } + + int l=0; + int r=0; + int res=0; + //[l..r]表示的是乘积和=k){ + product/=nums[l]; + l++; + } + //r-l+1表示的就是[l...r]窗口的长度 + res+=(r-l+1); + r++; + } + return res; + } +} + +``` + + + +## 五、矩阵 + +### 1、重塑矩阵(566) + +[566.Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/) + +在MATLAB中,有一个非常有用的函数 `reshape`,它可以将一个矩阵重塑为另一个大小不同的新矩阵,但保留其原始数据。 + +给出一个由二维数组表示的矩阵,以及两个正整数`r`和`c`,分别表示想要的重构的矩阵的行数和列数。 + +重构后的矩阵需要将原始矩阵的所有元素以相同的**行遍历顺序**填充。 + +如果具有给定参数的`reshape`操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。 + +**示例 1:** + +``` +输入: +nums = +[[1,2], + [3,4]] +r = 1, c = 4 +输出: +[[1,2,3,4]] +解释: +行遍历nums的结果是 [1,2,3,4]。新的矩阵是 1 * 4 矩阵, 用之前的元素值一行一行填充新矩阵。 + +``` + +**示例 2:** + +``` +输入: +nums = +[[1,2], + [3,4]] +r = 2, c = 4 +输出: +[[1,2], + [3,4]] +解释: +没有办法将 2 * 2 矩阵转化为 2 * 4 矩阵。 所以输出原矩阵。 + +``` + +**注意:** + +1. 给定矩阵的宽和高范围在 [1, 100]。 +2. 给定的 r 和 c 都是正数。 + +```java +public int[][] matrixReshape(int[][] nums, int r, int c) { + int m = nums.length; + if(m == 0){ + return null; + } + int n = nums[0].length; + if(m * n != r * c){ //不符合条件,就输出原矩阵 + return nums; + } + + int[][] res = new int[r][c]; + int index = 0; // index 在[0,m*n-1] 范围内 + for(int i=0;i= 0 && j target; + i--; + } + } + + return false; +} + +``` + +### 3、有序矩阵的第 K 小元素 (378) + +[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) + +给定一个 *n x n* 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素。 +请注意,它是排序后的第k小元素,而不是第k个元素。 + +**示例:** + +``` +matrix = [ + [ 1, 5, 9], + [10, 11, 13], + [12, 13, 15] +], +k = 8, + +返回 13。 + +``` + +**说明:** +你可以假设 k 的值永远是有效的, 1 ≤ k ≤ n2 。 + +解题参考:[Share my thoughts and Clean Java Code](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173) + +二分查找解法: + +```java +// 参考 287 题 +public int kthSmallest(int[][] matrix, int k) { + int n = matrix.length; + + int lo = matrix[0][0]; + int hi = matrix[n-1][n-1]; + while (lo <= hi){ + int mid = lo + (hi-lo)/2; + int cnt = 0; + //TODO:统计矩阵中 <= mid 的元素个数 + //(实际上是用来进行切分的) + for(int i=0;i pq = new PriorityQueue(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2-o1; + } + }); + + for(int i=0;i k){ + pq.poll(); + } + } + } + return pq.peek(); +} + +``` + +### 4、螺旋矩阵(54) + +[54. 螺旋矩阵](https://leetcode-cn.com/problems/spiral-matrix/) + +给定一个包含 *m* x *n* 个元素的矩阵(*m* 行, *n* 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。 + +**示例 1:** + +``` +输入: +[ + [ 1, 2, 3 ], + [ 4, 5, 6 ], + [ 7, 8, 9 ] +] +输出: [1,2,3,6,9,8,7,4,5] + +``` + +**示例 2:** + +``` +输入: +[ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9,10,11,12] +] +输出: [1,2,3,4,8,12,11,10,9,5,6,7] + +``` + +```java +public List spiralOrder(int[][] matrix) { + List res = new ArrayList(); + if(matrix == null){ + return res; + } + int m = matrix.length; + if(m == 0){ + return res; + } + int n = matrix[0].length; + + int top = 0 , down = m-1; + int left = 0, right = n-1; + + while(top<=down && left<=right){ + // top == down 针对奇数行的情况;left == right 针对奇数列的情况。 + //从左向右遍历 + for(int j = left; j <= right ; j++){ + res.add(matrix[top][j]); + } + top++; + //从上向下遍历 + for(int i = top; i<= down; i++){ + res.add(matrix[i][right]); + } + right--; + //从右向左遍历 + if(top <= down){ + //因为之前 top++,top 值发生了变化,如果 top> down,就不需要遍历了 + for(int j = right;j>=left;j--){ + res.add(matrix[down][j]); + } + } + down--; + //从下往上遍历 + if(left <= right){ + //因为之前 right--,right 值发生了变化,如果 left> right,就不需要遍历了 + for(int i=down;i>=top;i--){ + res.add(matrix[i][left]); + } + } + left++; + } + return res; +} + +``` + +### 5、螺旋矩阵II(59) + +[59. 螺旋矩阵 II](https://leetcode-cn.com/problems/spiral-matrix-ii/) + +给定一个正整数 *n*,生成一个包含 1 到 *n*2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 + +**示例:** + +``` +输入: 3 +输出: +[ + [ 1, 2, 3 ], + [ 8, 9, 4 ], + [ 7, 6, 5 ] +] + +``` + +```java +public int[][] generateMatrix(int n) { + int[][] res = new int[n][n]; + + int k = 1; + int top = 0 , down = n-1; + int left = 0, right = n-1; + + while(top<=down && left<=right){ + // top == down 针对奇数行的情况;left == right 针对奇数列的情况。 + //从左向右遍历 + for(int j = left; j <= right ; j++){ + res[top][j] = k++; + } + top++; + //从上向下遍历 + for(int i = top; i<= down; i++){ + res[i][right] = k++; + } + right--; + //从右向左遍历 + if(top <= down){ + //因为之前 top++,top 值发生了变化,如果 top> down,就不需要遍历了 + for(int j = right;j>=left;j--){ + res[down][j] = k++; + } + } + down--; + //从下往上遍历 + if(left <= right){ + //因为之前 right--,right 值发生了变化,如果 left> right,就不需要遍历了 + for(int i=down;i>=top;i--){ + res[i][left] = k++; + } + } + left++; + } + return res; +} + +``` + +### 6、螺旋矩阵III(885) + +[885. 螺旋矩阵 III](https://leetcode-cn.com/problems/spiral-matrix-iii/) + +在 `R` 行 `C` 列的矩阵上,我们从 `(r0, c0)` 面朝东面开始 + +这里,网格的西北角位于第一行第一列,网格的东南角位于最后一行最后一列。 + +现在,我们以顺时针按螺旋状行走,访问此网格中的每个位置。 + +每当我们移动到网格的边界之外时,我们会继续在网格之外行走(但稍后可能会返回到网格边界)。 + +最终,我们到过网格的所有 `R * C` 个空间。 + +按照访问顺序返回表示网格位置的坐标列表。 + +**示例 1:** + +``` +输入:R = 1, C = 4, r0 = 0, c0 = 0 +输出:[[0,0],[0,1],[0,2],[0,3]] + +``` + + + +**示例 2:** + +``` +输入:R = 5, C = 6, r0 = 1, c0 = 4 +输出:[[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]] + +``` + + + +**提示:** + +1. `1 <= R <= 100` +2. `1 <= C <= 100` +3. `0 <= r0 < R` +4. `0 <= c0 < C` + +```java +public int[][] spiralMatrixIII(int R, int C, int r0, int c0) { + int[][] res = new int[R*C][2]; + + int k = 1; + int step=1; //每次螺旋的步长,第一次是1,第二次就是2 + int posx=r0,posy=c0; //(posx,posy)是每次螺旋的起始位置 + + //(posx,posy) 就是第一个元素的位置 + res[0][0] = posx; + res[0][1] = posy; + + int curD=0; //curD是螺旋的方向,curD初始值为0,表示是从右开始的 + while(k 下 -> 左 -> 上 + curD=(curD+1)%4; + } + step++; + } + return res; +} + +//注意:这里不是坐标,是该二维数组下标。 +private int[][] d={ + {0,1}, //向右 + {1,0}, //向下 + {0,-1}, //向左 + {-1,0}, //向上 +}; + +//判断下标是否在该矩阵内 +private boolean inArea(int R,int C,int x,int y){ + return (x>=0 && x=0 && y= matrix.length || col>= matrix[0].length) { + return true; + } + if (matrix[row][col] != expectValue) { + return false; + } + return check(matrix, expectValue, row + 1, col + 1); +} + +``` + diff --git "a/docs/347円256円227円346円263円225円-347円233円256円345円275円225円.md" "b/docs/347円256円227円346円263円225円-347円233円256円345円275円225円.md" index 9d63113..70a4d56 100644 --- "a/docs/347円256円227円346円263円225円-347円233円256円345円275円225円.md" +++ "b/docs/347円256円227円346円263円225円-347円233円256円345円275円225円.md" @@ -32,7 +32,7 @@ ### LeetCode 题解 - [复杂度分析](./LeetCode/00复杂度分析.md) -- [数组问题](./LeetCode/01数组问题.md) +- [数组问题](./LeetCode/数组问题.md) - [查找问题](./LeetCode/02查找问题.md) - [链表问题](./LeetCode/03链表问题.md) - [栈、队列、优先队列](./LeetCode/04栈_队列_优先队列.md) From 00d931729de44477c8664328ea1d8a251904a400 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月12日 20:16:43 +0800 Subject: [PATCH 04/29] Java-Nortes --- ...76350円241円250円351円227円256円351円242円230円.md" | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100755 "docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" diff --git "a/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" "b/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" new file mode 100755 index 0000000..5216ce5 --- /dev/null +++ "b/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" @@ -0,0 +1,492 @@ +# 链表问题 + +链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 + +## 一、基础链表问题 + +### *1、链表反转(206) + +[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) + +```java +//思路: +//1、准备三个指针,指向pre指向当前节点的前一个节点、cur指向当前节点、next指向当前节点的后一个节点 +//2、要实现链表的反转,则 cur 指向 pre,同时 pre、cur、next都要相应的指向下一个位置。 +``` + + + +递归 + +```java +public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode next = head.next; + ListNode newHead = reverseList(next); + next.next = head; + head.next = null; + return newHead; +} +``` + +头插法 + +```java +public ListNode reverseList(ListNode head) { + ListNode newHead = new ListNode(-1); + while (head != null) { + ListNode next = head.next; + head.next = newHead.next; + newHead.next = head; + head = next; + } + return newHead.next; +} +``` + +### 2、找出两个链表的交点(160) + +[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) + +```html +A: a1 → a2 + ↘ + c1 → c2 → c3 + ↗ +B: b1 → b2 → b3 +``` + +要求:时间复杂度为 O(N),空间复杂度为 O(1) + +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 + +当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode l1 = headA, l2 = headB; + while (l1 != l2) { + l1 = (l1 == null) ? headB : l1.next; + l2 = (l2 == null) ? headA : l2.next; + } + return l1; +} +``` + +如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: + +- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; +- 或者直接比较两个链表的最后一个节点是否相同。 + +### 3、归并两个有序的链表(21) + +[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/) + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } +} +``` + +### 4、从有序链表中删除重复节点(83) + +[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) + +```html +Given 1->1->2, return 1->2. +Given 1->1->2->3->3, return 1->2->3. +``` + +```java +public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; +} +``` + +### 5、删除链表的倒数第 n 个节点(19) + +[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) + +```html +Given linked list: 1->2->3->4->5, and n = 2. +After removing the second node from the end, the linked list becomes 1->2->3->5. +``` + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n--> 0) { + fast = fast.next; + } + if (fast == null) return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return head; +} +``` + +### 6、交换链表中的相邻结点(24) + +[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) + +```html +Given 1->2->3->4, you should return the list as 2->1->4->3. +``` + +题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 + +```java +public ListNode swapPairs(ListNode head) { + ListNode node = new ListNode(-1); + node.next = head; + ListNode pre = node; + while (pre.next != null && pre.next.next != null) { + ListNode l1 = pre.next, l2 = pre.next.next; + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + + pre = l1; + } + return node.next; +} +``` + +### 7、链表求和(445) + +[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) + +```html +Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +Output: 7 -> 8 -> 0 -> 7 +``` + +题目要求:不能修改原始链表。 + +```java +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Stack l1Stack = buildStack(l1); + Stack l2Stack = buildStack(l2); + ListNode head = new ListNode(-1); + int carry = 0; + while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { + int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); + int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); + int sum = x + y + carry; + ListNode node = new ListNode(sum % 10); + node.next = head.next; + head.next = node; + carry = sum / 10; + } + return head.next; +} + +private Stack buildStack(ListNode l) { + Stack stack = new Stack(); + while (l != null) { + stack.push(l.val); + l = l.next; + } + return stack; +} +``` + +### 8、回文链表(234) + +[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) + +题目要求:以 O(1) 的空间复杂度来求解。 + +切成两半,把后半段反转,然后比较两半是否相等。 + +```java +public boolean isPalindrome(ListNode head) { + if (head == null || head.next == null) return true; + ListNode slow = head, fast = head.next; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 + cut(head, slow); // 切成两个链表 + return isEqual(head, reverse(slow)); +} + +private void cut(ListNode head, ListNode cutNode) { + while (head.next != cutNode) { + head = head.next; + } + head.next = null; +} + +private ListNode reverse(ListNode head) { + ListNode newHead = null; + while (head != null) { + ListNode nextNode = head.next; + head.next = newHead; + newHead = head; + head = nextNode; + } + return newHead; +} + +private boolean isEqual(ListNode l1, ListNode l2) { + while (l1 != null && l2 != null) { + if (l1.val != l2.val) return false; + l1 = l1.next; + l2 = l2.next; + } + return true; +} +``` + +### 9、分隔链表(725) + +[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) + +```html +Input: +root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 +Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] +Explanation: +The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. +``` + +题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 + +```java +public ListNode[] splitListToParts(ListNode root, int k) { + int N = 0; + ListNode cur = root; + while (cur != null) { + N++; + cur = cur.next; + } + int mod = N % k; + int size = N / k; + ListNode[] ret = new ListNode[k]; + cur = root; + for (int i = 0; cur != null && i < k; i++) { + ret[i] = cur; + int curSize = size + (mod--> 0 ? 1 : 0); + for (int j = 0; j < curSize - 1; j++) { + cur = cur.next; + } + ListNode next = cur.next; + cur.next = null; + cur = next; + } + return ret; +} +``` + +### 10、链表元素按奇偶聚集(328) + +[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) + +```html +Example: +Given 1->2->3->4->5->NULL, +return 1->3->5->2->4->NULL. +``` + +```java +public ListNode oddEvenList(ListNode head) { + if (head == null) { + return head; + } + ListNode odd = head, even = head.next, evenHead = even; + while (even != null && even.next != null) { + odd.next = odd.next.next; + odd = odd.next; + even.next = even.next.next; + even = even.next; + } + odd.next = evenHead; + return head; +} +``` + +## 二、进阶 + +## 三、链表与双指针 + + + +## 四、Floyd 环检测算法 + +弗洛伊德(Floyd )使用了两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的,只要一个比另一个快就行。但是如果移动步数增加,算法的复杂度可能增加)。如果两者在链表头以外(不包含开始情况)的某一点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了链表的结尾(如果存在结尾,肯定无环),那么说明没环。 + +### 环形链表(141) + +[141 Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/description/) + +给定一个链表,判断链表中是否有环。 + +为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos` 是 `-1`,则在该链表中没有环。 + + + +**示例 1:** + +``` +输入:head = [3,2,0,-4], pos = 1 +输出:true +解释:链表中有一个环,其尾部连接到第二个节点。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png) + +**示例 2:** + +``` +输入:head = [1,2], pos = 0 +输出:true +解释:链表中有一个环,其尾部连接到第一个节点。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) + +**示例 3:** + +``` +输入:head = [1], pos = -1 +输出:false +解释:链表中没有环。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test3.png) + + + +**进阶:** + +你能用 *O(1)*(即,常量)内存解决此问题吗? + +```java +//思路:Floyd 环检测算法 +public boolean hasCycle(ListNode head) { + if(head == null){ // 空链表是无环的 + return false; + } + if(head.next == null){ + return false; // 链表只有一个节点,也是无环的 + } + + ListNode slow = head; + ListNode fast = head; + while(slow !=null && fast!=null){ + slow = slow.next; + fast = fast.next; + if(fast !=null){ + fast = fast.next; + } + if(slow == fast){ //相遇,说明有环 + return true; + } + } + return false; +} +``` + +### 环形链表 II(142) + +[142 Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/description/) + +给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。 + +为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos` 是 `-1`,则在该链表中没有环。 + +**说明:**不允许修改给定的链表。 + + + +**示例 1:** + +``` +输入:head = [3,2,0,-4], pos = 1 +输出:tail connects to node index 1 +解释:链表中有一个环,其尾部连接到第二个节点。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png) + +**示例 2:** + +``` +输入:head = [1,2], pos = 0 +输出:tail connects to node index 0 +解释:链表中有一个环,其尾部连接到第一个节点。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) + +**示例 3:** + +``` +输入:head = [1], pos = -1 +输出:no cycle +解释:链表中没有环。 +``` + +![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test3.png) + + + +**进阶:** +你是否可以不用额外空间解决此题? + +```java +//思路:Floyd 环检测算法,尤其要注意后面 TODO: 部分 +public ListNode detectCycle(ListNode head) { + if(head == null || head.next == null){ + //空链表,或者只有一个节点的链表,返回 null + return null; + } + + ListNode slow = head; + ListNode fast = head; + + while(slow != null && fast != null){ + slow = slow.next; + fast = fast.next; + if(fast != null){ + fast = fast.next; + } + if(fast == slow){ + break; + } + } + + slow = head; + + //TODO:判断是否有进行后面操作的必要,后面会涉及到 fast 、fast.next + if(fast == null || fast.next == null){ + return null; + } + + while(slow != fast){ + slow = slow.next; + fast = fast.next; + } + return slow; +} +``` + From fc1587e1e2e27c6f4d3d2b0421c3ea964cdae9f9 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月12日 21:36:04 +0800 Subject: [PATCH 05/29] Java-Notes --- ...76350円241円250円351円227円256円351円242円230円.md" | 159 +++++++++++++++--- 1 file changed, 140 insertions(+), 19 deletions(-) diff --git "a/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" "b/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" index 5216ce5..d15def8 100755 --- "a/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" +++ "b/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" @@ -4,6 +4,51 @@ ## 一、基础链表问题 +链表节点: + +```java +public class ListNode { + int val; + ListNode next; + ListNode(int x) { + val = x; + } +} +``` + +链表工具类: + +```java +public class LinkedListUtils { + //根据数组创建链表 + public static ListNode createLinkedList(int[] arr){ + if(arr.length==0){ + return null; + } + ListNode head=new ListNode(arr[0]); + + ListNode curNode=head; + for(int i=1;i"); + curNode=curNode.next; + } + System.out.println("NULL"); + } +} +``` + + + ### *1、链表反转(206) [206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) @@ -14,35 +59,111 @@ //2、要实现链表的反转,则 cur 指向 pre,同时 pre、cur、next都要相应的指向下一个位置。 ``` +```java +//写法一:非递归方式 +public ListNode reverseList(ListNode head) { + if(head==null || head.next==null){ + return head; + } + + ListNode pre = null; + ListNode cur = head; + + while(cur!=null){ + ListNode next = cur.next; + + cur.next = pre; + + pre = cur ; + cur = next; + } + return pre; +} + +@Test +public void test(){ + int[] arr = {1,2,3,4,5}; + ListNode head = LinkedListUtils.createLinkedList(arr); + LinkedListUtils.printList(head); + head = reverseList(head); + LinkedListUtils.printList(head); +} +``` -递归 ```java +//写法二:递归方式 public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) { - return head; - } - ListNode next = head.next; - ListNode newHead = reverseList(next); - next.next = head; - head.next = null; - return newHead; + if(head==null || head.next==null){ + return head; + } + + // last 节点就是以 head.next 为头结点的链表翻转后得到的最后一个节点 + ListNode last = head.next; + + //翻转后 head 是最后一个节点 + head.next = null; + + //以 head.next 为头结点的链表翻转后得到的新链表,头节点是 newHead + ListNode newHead = reverseList(last); + last.next = head; + + return newHead; +} + +@Test +public void test(){ + int[] arr = {5,4,3,2,1}; + ListNode head = LinkedListUtils.createLinkedList(arr); + LinkedListUtils.printList(head); + head = reverseList(head); + LinkedListUtils.printList(head); } ``` -头插法 +### *2、反转链表 II(92) + +[92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) ```java -public ListNode reverseList(ListNode head) { - ListNode newHead = new ListNode(-1); - while (head != null) { - ListNode next = head.next; - head.next = newHead.next; - newHead.next = head; - head = next; - } - return newHead.next; +public ListNode reverseBetween(ListNode head, int m, int n) { + // m,n 表示的是第m,n个节点 + if(m>= n){ //不需要翻转 + return head; + } + + //引入虚拟头结点,是方便处理 m=1,即头结点无前驱节点的情况 + ListNode dummyHead = new ListNode(-1); + dummyHead.next = head; + ListNode pre = dummyHead; // pre 用于定位 m 节点的前驱节点 + for(int i=1;i Date: 2019年6月24日 10:13:14 +0800 Subject: [PATCH 06/29] Update Zookeeper.md --- docs/BigData/Zookeeper.md | 40 +++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/BigData/Zookeeper.md b/docs/BigData/Zookeeper.md index 411d38b..618b7bc 100644 --- a/docs/BigData/Zookeeper.md +++ b/docs/BigData/Zookeeper.md @@ -1,6 +1,4 @@ -## Zookeeper - -### Zookeeper 简介 +# Zookeeper 简介 Zookeeper 最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发人员就试图开发一个通用的无单点问题的**分布式协调框架**,以便让开发人员将精力集中在处理业务逻辑上。 @@ -16,7 +14,7 @@ Zookeeper 作为一个分布式的服务框架,主要用来解决**分布式 简单的说,**zookeeper = 文件系统 + 通知机制**。 -### Zookeeper 数据模型 +## Zookeeper 数据模型 - 树形层次结构 @@ -115,7 +113,7 @@ ZooKeeper 中 Watcher 特性总结: WatchedEvent 是 Zookeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分的内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会**告诉客户端发生了事件,而不会说明事件的具体内容**。例如针对 NodeDataChanged 事件,Zookeeper 的 Watcher 只会通知客户指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据,这也是 Zookeeper 的 Watcher 机制的一个非常重要的特性。 -### Zookeeper 特点 +## Zookeeper 特点 **1、顺序一致性** @@ -133,7 +131,7 @@ WatchedEvent 是 Zookeeper 整个 Watcher 通知机制的最小通知单元, 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 -### Zookeeper 设计目标 +## Zookeeper 设计目标 #### 1、简单的数据模型 @@ -163,7 +161,7 @@ Zookeeper 允许分布式进程通过共享的层次结构命名空间进行相 **Zookeeper 是高性能的**。 在"读"多于"写"的应用程序中尤其地高性能,因为"写"会导致所有的服务器间同步状态。("读"多于"写"是协调服务的典型场景)。 -### Zookeeper 集群中的角色 +## Zookeeper 集群中的角色 在 Zookeeper 中引入了**Leader**、**Follower** 和 **Observer** 三种角色。如下图: @@ -185,7 +183,7 @@ Zookeeper 集群中的所有机器通过一个 Leader 选举过程来选定一 4、广播阶段:Zookeeper 集群正式对外提供事务服务,并且 Leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 -### ZAB 协议 +# ZAB 协议 ZAB(ZooKeeper Atomic Broadcast,原子广播) 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 Zookeeper 中,主要依赖 **ZAB 协议来实现分布式数据一致性**,基于该协议,Zookeeper 实现了一种主备模式的系统架构来**保持集群中各个副本之间的数据一致性**。 @@ -193,21 +191,39 @@ ZAB(ZooKeeper Atomic Broadcast,原子广播) 协议是为分布式协调 ZAB 协议两种基本的模式:崩溃恢复和消息广播。 -#### 崩溃恢复 +## 崩溃恢复 当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入**恢复模式**并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指**数据同步**,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。 -#### 消息广播 +## 消息广播 当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进人**消息广播模式**了。当一台同样遵守 ZAB 协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,**Zookeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理**。Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器。 -### Zookeeper 的功能 +# Zookeeper 的应用场景 -- 统一配置管理 +- 统一配置 - 统一命名管理 - 分布式锁 - 集群管理 +## 数据发布/订阅 + +数据发布/订阅系统,即所谓的配置中心,就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。 + +常见的是统一配置管理,将配置信息存放到ZooKeeper上进行集中管理。在通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时在该节点上注册一个Watcher监听。这样,一旦配置信息发生变化,服务端就会通知所有订阅的客户端,从而达到实时获取最新配置信息的目的。 + +由于系统的配置信息通常具有数据量较小、数据内容在运行时会动态变化、集群中各机器共享等特性,这样特别适合使用ZooKeeper来进行统一配置管理。 + +## 统一命名服务 + +ZooKeeper提供了一套分布式全局唯一ID的分配机制,所谓ID,就是一个能够唯一标识某个对象的标识符。 + +## 分布式协调/通知 + +ZooKeeper中持有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发送变化,那么所有订阅者都能够收到相应的Watcher通知,并做出相应处理。 + + + > 补充资料: ## 参考资料 From 1eecccb8fd3ff3d4f5b7e5654743eb1d24e24711 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月25日 09:09:02 +0800 Subject: [PATCH 07/29] Java-Notes --- docs/DataBase/Redis.md | 3 +- ...04345円210円222円345円237円272円347円241円200円.md" | 4 +- ...76350円241円250円351円227円256円351円242円230円.md" | 613 ------------------ ...45222円214円post346円257円224円350円276円203円.md" | 22 +- docs/Spring/06SpringMVC.md | 46 +- 5 files changed, 28 insertions(+), 660 deletions(-) delete mode 100755 "docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index dfe7a1a..a2550dc 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -1283,4 +1283,5 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 - [redis之分布式算法原理](https://www.jianshu.com/p/af7d933439a3) - [面试必备:什么是一致性Hash算法?](https://zhuanlan.zhihu.com/p/34985026) - [redis缓存与数据库一致性问题解决](https://blog.csdn.net/qq_27384769/article/details/79499373) -- [面试前必知Redis面试题—缓存雪崩+穿透+缓存与数据库双写一致问题](https://www.jianshu.com/p/09eb2babf175) \ No newline at end of file +- [面试前必知Redis面试题—缓存雪崩+穿透+缓存与数据库双写一致问题](https://www.jianshu.com/p/09eb2babf175) +- [高性能网站设计之缓存更新的套路](https://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323) \ No newline at end of file diff --git "a/docs/LeetCode/07345円212円250円346円200円201円350円247円204円345円210円222円345円237円272円347円241円200円.md" "b/docs/LeetCode/07345円212円250円346円200円201円350円247円204円345円210円222円345円237円272円347円241円200円.md" index 939e460..92a83a8 100644 --- "a/docs/LeetCode/07345円212円250円346円200円201円350円247円204円345円210円222円345円237円272円347円241円200円.md" +++ "b/docs/LeetCode/07345円212円250円346円200円201円350円247円204円345円210円222円345円237円272円347円241円200円.md" @@ -2272,9 +2272,9 @@ private boolean isGood(int num,boolean flag){ ### 动态规划方程 用edit[i][j]表示A串和B串的编辑距离。 - + edit[i][j]表示**A串从第0个字符开始到第i个字符**和**B串从第0个字符开始到第j个字符**,这两个字串的编辑距离。 - + 注意:字符串的下标从1开始。 diff --git "a/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" "b/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" deleted file mode 100755 index d15def8..0000000 --- "a/docs/LeetCode/351円223円276円350円241円250円351円227円256円351円242円230円.md" +++ /dev/null @@ -1,613 +0,0 @@ -# 链表问题 - -链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 - -## 一、基础链表问题 - -链表节点: - -```java -public class ListNode { - int val; - ListNode next; - ListNode(int x) { - val = x; - } -} -``` - -链表工具类: - -```java -public class LinkedListUtils { - //根据数组创建链表 - public static ListNode createLinkedList(int[] arr){ - if(arr.length==0){ - return null; - } - ListNode head=new ListNode(arr[0]); - - ListNode curNode=head; - for(int i=1;i"); - curNode=curNode.next; - } - System.out.println("NULL"); - } -} -``` - - - -### *1、链表反转(206) - -[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) - -```java -//思路: -//1、准备三个指针,指向pre指向当前节点的前一个节点、cur指向当前节点、next指向当前节点的后一个节点 -//2、要实现链表的反转,则 cur 指向 pre,同时 pre、cur、next都要相应的指向下一个位置。 -``` - -```java -//写法一:非递归方式 -public ListNode reverseList(ListNode head) { - if(head==null || head.next==null){ - return head; - } - - ListNode pre = null; - ListNode cur = head; - - while(cur!=null){ - ListNode next = cur.next; - - cur.next = pre; - - pre = cur ; - cur = next; - } - return pre; -} - -@Test -public void test(){ - int[] arr = {1,2,3,4,5}; - ListNode head = LinkedListUtils.createLinkedList(arr); - LinkedListUtils.printList(head); - head = reverseList(head); - LinkedListUtils.printList(head); -} -``` - - - -```java -//写法二:递归方式 -public ListNode reverseList(ListNode head) { - if(head==null || head.next==null){ - return head; - } - - // last 节点就是以 head.next 为头结点的链表翻转后得到的最后一个节点 - ListNode last = head.next; - - //翻转后 head 是最后一个节点 - head.next = null; - - //以 head.next 为头结点的链表翻转后得到的新链表,头节点是 newHead - ListNode newHead = reverseList(last); - last.next = head; - - return newHead; -} - -@Test -public void test(){ - int[] arr = {5,4,3,2,1}; - ListNode head = LinkedListUtils.createLinkedList(arr); - LinkedListUtils.printList(head); - head = reverseList(head); - LinkedListUtils.printList(head); -} -``` - -### *2、反转链表 II(92) - -[92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) - -```java -public ListNode reverseBetween(ListNode head, int m, int n) { - // m,n 表示的是第m,n个节点 - if(m>= n){ //不需要翻转 - return head; - } - - //引入虚拟头结点,是方便处理 m=1,即头结点无前驱节点的情况 - ListNode dummyHead = new ListNode(-1); - dummyHead.next = head; - ListNode pre = dummyHead; // pre 用于定位 m 节点的前驱节点 - for(int i=1;i1->2, return 1->2. -Given 1->1->2->3->3, return 1->2->3. -``` - -```java -public ListNode deleteDuplicates(ListNode head) { - if (head == null || head.next == null) return head; - head.next = deleteDuplicates(head.next); - return head.val == head.next.val ? head.next : head; -} -``` - -### 5、删除链表的倒数第 n 个节点(19) - -[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) - -```html -Given linked list: 1->2->3->4->5, and n = 2. -After removing the second node from the end, the linked list becomes 1->2->3->5. -``` - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - ListNode fast = head; - while (n--> 0) { - fast = fast.next; - } - if (fast == null) return head.next; - ListNode slow = head; - while (fast.next != null) { - fast = fast.next; - slow = slow.next; - } - slow.next = slow.next.next; - return head; -} -``` - -### 6、交换链表中的相邻结点(24) - -[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) - -```html -Given 1->2->3->4, you should return the list as 2->1->4->3. -``` - -题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 - -```java -public ListNode swapPairs(ListNode head) { - ListNode node = new ListNode(-1); - node.next = head; - ListNode pre = node; - while (pre.next != null && pre.next.next != null) { - ListNode l1 = pre.next, l2 = pre.next.next; - ListNode next = l2.next; - l1.next = next; - l2.next = l1; - pre.next = l2; - - pre = l1; - } - return node.next; -} -``` - -### 7、链表求和(445) - -[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) - -```html -Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) -Output: 7 -> 8 -> 0 -> 7 -``` - -题目要求:不能修改原始链表。 - -```java -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - Stack l1Stack = buildStack(l1); - Stack l2Stack = buildStack(l2); - ListNode head = new ListNode(-1); - int carry = 0; - while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { - int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); - int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); - int sum = x + y + carry; - ListNode node = new ListNode(sum % 10); - node.next = head.next; - head.next = node; - carry = sum / 10; - } - return head.next; -} - -private Stack buildStack(ListNode l) { - Stack stack = new Stack(); - while (l != null) { - stack.push(l.val); - l = l.next; - } - return stack; -} -``` - -### 8、回文链表(234) - -[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) - -题目要求:以 O(1) 的空间复杂度来求解。 - -切成两半,把后半段反转,然后比较两半是否相等。 - -```java -public boolean isPalindrome(ListNode head) { - if (head == null || head.next == null) return true; - ListNode slow = head, fast = head.next; - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 - cut(head, slow); // 切成两个链表 - return isEqual(head, reverse(slow)); -} - -private void cut(ListNode head, ListNode cutNode) { - while (head.next != cutNode) { - head = head.next; - } - head.next = null; -} - -private ListNode reverse(ListNode head) { - ListNode newHead = null; - while (head != null) { - ListNode nextNode = head.next; - head.next = newHead; - newHead = head; - head = nextNode; - } - return newHead; -} - -private boolean isEqual(ListNode l1, ListNode l2) { - while (l1 != null && l2 != null) { - if (l1.val != l2.val) return false; - l1 = l1.next; - l2 = l2.next; - } - return true; -} -``` - -### 9、分隔链表(725) - -[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) - -```html -Input: -root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 -Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] -Explanation: -The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. -``` - -题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 - -```java -public ListNode[] splitListToParts(ListNode root, int k) { - int N = 0; - ListNode cur = root; - while (cur != null) { - N++; - cur = cur.next; - } - int mod = N % k; - int size = N / k; - ListNode[] ret = new ListNode[k]; - cur = root; - for (int i = 0; cur != null && i < k; i++) { - ret[i] = cur; - int curSize = size + (mod--> 0 ? 1 : 0); - for (int j = 0; j < curSize - 1; j++) { - cur = cur.next; - } - ListNode next = cur.next; - cur.next = null; - cur = next; - } - return ret; -} -``` - -### 10、链表元素按奇偶聚集(328) - -[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) - -```html -Example: -Given 1->2->3->4->5->NULL, -return 1->3->5->2->4->NULL. -``` - -```java -public ListNode oddEvenList(ListNode head) { - if (head == null) { - return head; - } - ListNode odd = head, even = head.next, evenHead = even; - while (even != null && even.next != null) { - odd.next = odd.next.next; - odd = odd.next; - even.next = even.next.next; - even = even.next; - } - odd.next = evenHead; - return head; -} -``` - -## 二、进阶 - -## 三、链表与双指针 - - - -## 四、Floyd 环检测算法 - -弗洛伊德(Floyd )使用了两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的,只要一个比另一个快就行。但是如果移动步数增加,算法的复杂度可能增加)。如果两者在链表头以外(不包含开始情况)的某一点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了链表的结尾(如果存在结尾,肯定无环),那么说明没环。 - -### 环形链表(141) - -[141 Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/description/) - -给定一个链表,判断链表中是否有环。 - -为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos` 是 `-1`,则在该链表中没有环。 - - - -**示例 1:** - -``` -输入:head = [3,2,0,-4], pos = 1 -输出:true -解释:链表中有一个环,其尾部连接到第二个节点。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png) - -**示例 2:** - -``` -输入:head = [1,2], pos = 0 -输出:true -解释:链表中有一个环,其尾部连接到第一个节点。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) - -**示例 3:** - -``` -输入:head = [1], pos = -1 -输出:false -解释:链表中没有环。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test3.png) - - - -**进阶:** - -你能用 *O(1)*(即,常量)内存解决此问题吗? - -```java -//思路:Floyd 环检测算法 -public boolean hasCycle(ListNode head) { - if(head == null){ // 空链表是无环的 - return false; - } - if(head.next == null){ - return false; // 链表只有一个节点,也是无环的 - } - - ListNode slow = head; - ListNode fast = head; - while(slow !=null && fast!=null){ - slow = slow.next; - fast = fast.next; - if(fast !=null){ - fast = fast.next; - } - if(slow == fast){ //相遇,说明有环 - return true; - } - } - return false; -} -``` - -### 环形链表 II(142) - -[142 Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/description/) - -给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。 - -为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos` 是 `-1`,则在该链表中没有环。 - -**说明:**不允许修改给定的链表。 - - - -**示例 1:** - -``` -输入:head = [3,2,0,-4], pos = 1 -输出:tail connects to node index 1 -解释:链表中有一个环,其尾部连接到第二个节点。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png) - -**示例 2:** - -``` -输入:head = [1,2], pos = 0 -输出:tail connects to node index 0 -解释:链表中有一个环,其尾部连接到第一个节点。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) - -**示例 3:** - -``` -输入:head = [1], pos = -1 -输出:no cycle -解释:链表中没有环。 -``` - -![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test3.png) - - - -**进阶:** -你是否可以不用额外空间解决此题? - -```java -//思路:Floyd 环检测算法,尤其要注意后面 TODO: 部分 -public ListNode detectCycle(ListNode head) { - if(head == null || head.next == null){ - //空链表,或者只有一个节点的链表,返回 null - return null; - } - - ListNode slow = head; - ListNode fast = head; - - while(slow != null && fast != null){ - slow = slow.next; - fast = fast.next; - if(fast != null){ - fast = fast.next; - } - if(fast == slow){ - break; - } - } - - slow = head; - - //TODO:判断是否有进行后面操作的必要,后面会涉及到 fast 、fast.next - if(fast == null || fast.next == null){ - return null; - } - - while(slow != fast){ - slow = slow.next; - fast = fast.next; - } - return slow; -} -``` - diff --git "a/docs/NetWork/13get345円222円214円post346円257円224円350円276円203円.md" "b/docs/NetWork/13get345円222円214円post346円257円224円350円276円203円.md" index 450cd83..a4c0cd0 100644 --- "a/docs/NetWork/13get345円222円214円post346円257円224円350円276円203円.md" +++ "b/docs/NetWork/13get345円222円214円post346円257円224円350円276円203円.md" @@ -1,20 +1,10 @@ - -* [九、GET 和 POST 比较](#九get-和-post-比较) - * [作用](#作用) - * [参数](#参数) - * [安全](#安全) - * [幂等性](#幂等性) - * [可缓存](#可缓存) - * [XMLHttpRequest](#xmlhttprequest) - - -# 九、GET 和 POST 比较 +#九、GET 和 POST 比较 ## Http报文层面: ### 作用 -GET 用于获取资源,而 POST 用于传输实体主体。 +GET 用于获取资源,而 POST 用于传输**实体主体**。 ### 参数 @@ -36,19 +26,19 @@ name1=value1&name2=value2 ### 安全性 -安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 +**安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的**。 GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 安全的方法除了 GET 之外还有:HEAD、OPTIONS。 -不安全的方法除了 POST 之外还有 PUT、DELETE。 +不安全的方法除了 POST 之外还有 PUT、DELETE。 ### 幂等性 -幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 +**幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的**。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 -所有的安全方法也都是幂等的。 +**所有的安全方法也都是幂等的**。 在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 diff --git a/docs/Spring/06SpringMVC.md b/docs/Spring/06SpringMVC.md index 6bdf065..80dc882 100644 --- a/docs/Spring/06SpringMVC.md +++ b/docs/Spring/06SpringMVC.md @@ -1,12 +1,6 @@ - -* [SpringMVC](#SpringMVC) - * [MVC设计模式](#MVC设计模式) - * [SpringMVC框架](#SpringMVC框架) - - # SpringMVC -## MVC设计模式 +## MVC(Model-View-Controller) MVC 的原理图: @@ -14,39 +8,35 @@ MVC 的原理图: - Model(模型端) -Model 封装的是数据源和所有基于对这些数据的操作。 -在一个组件中,Model往往表示组件的状态和操作这些状态的方法,往往是一系列的公开方法。 -通过这些公开方法,便可以取得模型端的所有功能。 - -在这些公开方法中,有些是取值方法,让系统其他部分可以得到模型端的内部状态参数, -其他的改值方法则允许外部修改模型端的内部状态。 -模型端还必须有方法登记视图,以便在模型端的内部状态发生变化时,可以通知视图端。 -我们可以自己定义一个Subject接口来提供登记和通知视图所需的接口或者继承 Java.util.Observable类,让父类完成这件事。 +**Model 封装的是数据源和所有基于对这些数据的操作**。 + +在一个组件中,Model 往往表示组件的状态和操作这些状态的方法,往往是一系列的公开方法。 通过这些公开方法,便可以取得 Model 的所有功能。 + +在这些公开方法中,有些是**取值方法**,让系统其他部分可以得到模型端的内部状态参数, 其他的**改值方法**则允许外部修改模型端的内部状态。 模型端还必须有方法**登记视图**,以便在模型端的内部状态发生变化时,可以通知视图端。 - View(视图端) -View封装的是对数据源Model的一种显示。 -一个模型可以由多个视图,并且可以在需要的时候动态地登记上所需的视图。 -而一个视图理论上也可以同不同的模型关联起来。 +**View 封装的是对数据源 Model 的一种显示**。 +一个模型可以有多个视图,并且可以在需要的时候动态地登记上所需的视图。而一个视图理论上也可以和不同的模型关联起来。 - Controller(控制器端) -封装的是外界作用于模型的操作。通常,这些操作会转发到模型上, -并调用模型中相应的一个或者多个方法(这个方法就是前面在介绍模型的时候说的改值方法)。 -一般Controller在Model和View之间起到了沟通的作用,处理用户在View上的输入,并转发给Model来更改其状态值。 -这样 Model 和 View 两者之间可以做到松散耦合,甚至可以彼此不知道对方,而由Controller连接起这两个部分。也在前言里提到,MVC用到了策略模式,这是因为View用一个特定的Controller的实例来实现一个特定的响应策略,更换不同的Controller,可以改变View对用户输入的响应。 +**封装的是外界作用于 Model 的操作。**通常,这些操作会转发到模型上,并调用模型中相应的一个或者多个方法(这个方法就是前面在介绍模型的时候说的改值方法)。 +一般 Controller 在 Model 和 View 之间起到了沟通的作用,处理用户在 View 上的输入,并转发给 Model 来更改Model 状态值。 +这样 Model 和 View 两者之间可以做到**松散耦合**,甚至可以彼此不知道对方,而由Controller连接起这两个部分。也在前言里提到,MVC 用到了策略模式,这是**因为 View 用一个特定的 Controller 的实例来实现一个特定的响应策略**,更换不同的Controller,可以改变 View 对用户输入的响应。 + +## MVC 中涉及到的设计模式 -MVC(Model-View-Controller): -利用"观察者"让控制器和视图可以随最新的状态改变而更新。 -另一方面,视图和控制器则实现了"策略模式"。控制器是视图的行为。 +利用**"观察者"**让控制器 (Controller) 和视图 (View) 可以随最新的状态改变而改变。 +视图 (View) 和控制器 (Controller) 则实现了**"策略模式"**,控制器是视图的行为。 -- [观察者模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/02%E8%A1%8C%E4%B8%BA%E5%9E%8B.md#7-%E8%A7%82%E5%AF%9F%E8%80%85observer) +### [观察者模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_7-观察者(observer)) -- [策略模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/02%E8%A1%8C%E4%B8%BA%E5%9E%8B.md#9-%E7%AD%96%E7%95%A5strategy) +### [策略模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_9-策略(strategy)) -## SpringMVC框架 +## SpringMVC 框架 SpringMVC 框架是以请求为驱动,围绕 Servlet 设计, 将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。 From 7cba6cbcb7b9192f9c1d256c693001bcb1c4860c Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月25日 13:42:28 +0800 Subject: [PATCH 08/29] =?UTF-8?q?Update=20=E9=94=81=E6=9C=BA=E5=88=B6.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../351円224円201円346円234円272円345円210円266円.md" | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git "a/docs/DataBase/351円224円201円346円234円272円345円210円266円.md" "b/docs/DataBase/351円224円201円346円234円272円345円210円266円.md" index 107c82f..f4cbd8e 100644 --- "a/docs/DataBase/351円224円201円346円234円272円345円210円266円.md" +++ "b/docs/DataBase/351円224円201円346円234円272円345円210円266円.md" @@ -8,7 +8,7 @@ - 按照锁的级别划分:**共享锁**、**排它锁**。 -- 按照加锁方式划分:自动锁、显示锁。 +- 按照加锁方式划分:自动锁、显式锁。 - 按照操作划分:DML 锁、DDL 锁。 @@ -92,7 +92,7 @@ InnoDB 只有通过**索引**条件检索数据**才使用行级锁**,否则 5、操作员 B 完成了操作,也将版本号version++(version=2)试图向数据库提交数据(balance=80ドル), 6、 此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 , -不满足**提交版本必须大于记录当前版本才能执行更新**的乐观锁策略,操作员 B 的提交被驳回。 +不满足"提交版本必须大于记录当前版本才能执行更新"的乐观锁策略,因此操作员 B 的提交被驳回。 ``` ## MyISAM 和 InnoDB 适用场景 @@ -209,7 +209,7 @@ T1 读取某个**范围**的数据,T2 在这个**范围* | 可重复读
(REPEATABLE READ) | 避免 | 避免 | 避免 | 发生 | | 可串行化
(SERIALIZABLE) | 避免 | 避免 | 避免 | 避免 | -注意:**MySQL 的 InnoDB 在可重复读(REPEATABLE READ)隔离级别下可以避免幻读 ** +**注意**:*MySQL 的 InnoDB 在可重复读(REPEATABLE READ)隔离级别下可以避免幻读* @@ -276,7 +276,7 @@ undo log 中记录的是数据表**记录行的多个版本**,也就是事务 1、初始数据行 -F1〜F6 是某字段的名称名称,1〜6是其对应的数据。 +F1〜F6 是某些字段的名称,1〜6是其对应的数据。 假设这条数据是刚 INSERT 的,可以认为 ID 为 1,其他两个字段为 NULL。 @@ -300,9 +300,11 @@ F1〜F6 是某字段的名称名称,1〜6是其对应的数据。 #### read view +trx_id_0表示当前事务的id,read view中最早开始的事务用trx_id_1表示,最晚开始的事务用trx_id_2表示。 + 主要用来判断当前版本数据的可见性: -- 当行记录的事务 id 小于当前系统的活动事务最小 id,就是可见的。 +- 当行记录的事务 id 小于当前系统的活动事务最小 id,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。 ```c if (trx_id < view->up_limit_id) { // up_limit_id 活动事务最小 id @@ -318,7 +320,7 @@ F1〜F6 是某字段的名称名称,1〜6是其对应的数据。 } ``` -- 当行记录的事务 id 在活动范围之中时,判断是否在活动链表中,如果在就不可见,如果不在就是可见的。 +- 当行记录的事务 id 在活动范围之中时,**判断是否在活动链表中**,从trx_id_1到trx_id_2进行遍历,如果trx_id_0等于他们之中的某个事务id的话,那么不可见。从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该trx_id_0。 ```c for (i = 0; i < n_ids; i++) { @@ -428,7 +430,7 @@ delete from tb2 where id = 9 - [数据库两大神器【索引和锁】](https://segmentfault.com/a/1190000015738121#articleHeader12) - [MySql锁机制简单了解一下](https://blog.csdn.net/qq_34337272/article/details/80611486) -- [[MySQL高级](六) 锁机制](https://blog.csdn.net/why15732625998/article/details/80439315) +- [[MySQL高级] (六) 锁机制](https://blog.csdn.net/why15732625998/article/details/80439315) - [MySQL innoDB——redo log/undo log](https://www.jianshu.com/p/d829df873332) - [MySQL加锁分析与死锁解读,你真的弄懂Gap锁了吗?](https://www.jianshu.com/p/a287afb5d5ba) - [Mysql(Innodb)如何避免幻读](https://blog.csdn.net/ashic/article/details/53735537) From effcc926f37b2fd0feacd58c3ff1fe99c9cedd6d Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月25日 13:42:35 +0800 Subject: [PATCH 09/29] =?UTF-8?q?Update=20MySQL=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...47350円203円275円344円274円230円345円214円226円.md" | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git "a/docs/DataBase/MySQL346円200円247円350円203円275円344円274円230円345円214円226円.md" "b/docs/DataBase/MySQL346円200円247円350円203円275円344円274円230円345円214円226円.md" index 6f8820b..e28e411 100644 --- "a/docs/DataBase/MySQL346円200円247円350円203円275円344円274円230円345円214円226円.md" +++ "b/docs/DataBase/MySQL346円200円247円350円203円275円344円274円230円345円214円226円.md" @@ -79,6 +79,19 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explai - id:标明 sql 执行顺序(id 越大,越先执行) - select_type : 查询类型,有简单查询、联合查询、子查询等 - type:MySQL 找到需要的数据行的方式。从最好到最差的链接类型为 const、eq_ref、ref、fulltext、ref_or_null、index_merge、unique_subquery、index_subquery、range、 **index 和 ALL **。 + +> `const`: 针对主键或唯一索引的等值查询扫描, 最多只返回一行数据。 const 查询速度非常快, 因为它仅仅读取一次即可。 +> +> `eq_ref`: 此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果。并且查询的比较操作通常是 `=`, 查询效率较高。 +> +> `ref`: 此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 `最左前缀` 规则索引的查询。 +> +> `range`: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, ,>,>=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中。 +> +> `index`: 表示全索引扫描(full index scan), 和 ALL 类型类似, 只不过 ALL 类型是全表扫描, 而 index 类型则仅仅扫描所有的索引, 而不扫描数据。 +> +> `ALL`: 表示全表扫描, 这个类型的查询是性能最差的查询之一。通常来说, 我们的查询不应该出现 ALL 类型的查询。 + - key :使用的索引。如果为 NULL,则没有使用索引。 - key_len:使用索引的长度。在不损失精度的条件下,长度越短越好。 - ref:显示索引的哪一列被使用了,如果可能的话是一个常数 @@ -88,7 +101,7 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explai | Extra 值 | 说明 | | --------------- | ------------------------------------------------------------ | | Using filesort | 表示 MySQL 会对结果使用一个**外部索引排序**,不是从表里 按索引次序读到相关内容,可能在内存或磁盘上进行排序。 MySQL 中无法利用索引完成的排序操作称为"文件排序"。 | -| Using temporary | 表示 MySQL 在对查询结果排序时使用临时表。 常见于排序 order by 和分组查询 group by。 | +| Using temporary | 表示 MySQL 在对查询结果排序时使用**临时表**。 常见于排序 order by 和分组查询 group by。 | 注意:当 Extra 的值为 Using filesort 或者 Using temporary 查询是需要优化的。 @@ -275,7 +288,7 @@ order by film_id limit 50,5; # 从第 1 条记录开始读取 5 条记录 select * form payment where customer_id = 584 and staff_id = 2; ``` -### 索引优化 SQL 的方法 +### 索引优化的方式 #### 重复索引 @@ -500,4 +513,8 @@ Windows 中导入数据库: source C:\Users18351円\Desktop\MYSQL 性能优化\sakila-db\sakila-schema.sql -source C:\Users18351円\Desktop\MYSQL 性能优化\sakila-db\sakila-data.sql \ No newline at end of file +source C:\Users18351円\Desktop\MYSQL 性能优化\sakila-db\sakila-data.sql + +# 参考资料 + +- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) \ No newline at end of file From 6b0f8bec61a2bde23280805b28c066a69773d7e6 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月25日 16:33:49 +0800 Subject: [PATCH 10/29] Java-Notes --- ...02350円241円214円344円270円272円345円236円213円.md" | 151 +++++++++-------- docs/Spring/06SpringMVC.md | 155 +++++++----------- 2 files changed, 138 insertions(+), 168 deletions(-) diff --git "a/docs/OO/02350円241円214円344円270円272円345円236円213円.md" "b/docs/OO/02350円241円214円344円270円272円345円236円213円.md" index e2f9e96..4aac9ab 100644 --- "a/docs/OO/02350円241円214円344円270円272円345円236円213円.md" +++ "b/docs/OO/02350円241円214円344円270円272円345円236円213円.md" @@ -1545,129 +1545,138 @@ No gumball dispensed ### Implementation1 -设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。 +报价管理:向客户报价,对于销售部门的人来讲,这是一个非常重大、非常复杂的问题,对不同的客户要报不同的价格,比如: + +1. 对普通客户或者是新客户报的是全价 +2. 对老客户报的价格,根据客户年限,给予一定的折扣 +3. 对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣 + +
```java -public interface QuackBehavior { - void quack(); +/** + * 策略,定义计算报价算法的接口 + */ +public interface Strategy { + /** + * 计算报价 + * @param goodsPrice 商品原价 + */ + void calcPrice(double goodsPrice); } ``` ```java -public class Quack implements QuackBehavior { +public class NormalCustomerStrategy implements Strategy{ @Override - public void quack() { - System.out.println("quack!"); + public void calcPrice(double goodsPrice) { + System.out.println("对于新客户或者是普通客户,没有折扣:"+goodsPrice); } } ``` ```java -public class Squeak implements QuackBehavior{ +public class OldCustomerStrategy implements Strategy{ @Override - public void quack() { - System.out.println("squeak!"); + public void calcPrice(double goodsPrice) { + System.out.println("对于老客户,统一折扣5%:"+goodsPrice*(1-0.05)); } } ``` ```java -public class Duck { +public class LargeCustomerStrategy implements Strategy{ + @Override + public void calcPrice(double goodsPrice) { + System.out.println("对于大客户,统一折扣10%:"+goodsPrice*(1-0.1)); + } +} +``` - private QuackBehavior quackBehavior; +```java +/** + * Context 是使用到该算法族的类,其中的 doSomething()方法会调用 behavior(), + * setStrategy(Strategy) 方法可以动态地改变 strategy 对象, + * 也就是说能动态地改变 Context 所使用的算法。 + */ +public class Price { + private Strategy strategy; - public void performQuack() { - if (quackBehavior != null) { - quackBehavior.quack(); - } + public void price(double goodsPrice){ + strategy.calcPrice(goodsPrice); } - public void setQuackBehavior(QuackBehavior quackBehavior) { - this.quackBehavior = quackBehavior; + public void setStrategy(Strategy strategy) { + this.strategy = strategy; } } ``` ```java public class Client { - public static void main(String[] args) { - Duck duck = new Duck(); - duck.setQuackBehavior(new Squeak()); - duck.performQuack(); - duck.setQuackBehavior(new Quack()); - duck.performQuack(); + Price price = new Price(); + price.setStrategy(new NormalCustomerStrategy()); + price.price(1000.0); + price.setStrategy(new OldCustomerStrategy()); + price.price(1000.0); + price.setStrategy(new LargeCustomerStrategy()); + price.price(1000.0); } } ``` ```html -squeak! -quack! +对于新客户或者是普通客户,没有折扣:1000.0 +对于老客户,统一折扣5%:950.0 +对于大客户,统一折扣10%:900.0 ``` ### Implementation2 -报价管理:向客户报价,对于销售部门的人来讲,这是一个非常重大、非常复杂的问题,对不同的客户要报不同的价格,比如: - -1. 对普通客户或者是新客户报的是全价 - -2. 对老客户报的价格,根据客户年限,给予一定的折扣 - -3. 对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣 - -
+设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。 ```java /** - * 策略,定义计算报价算法的接口 + * 策略 */ public interface Strategy { - /** - * 计算报价 - * @param goodsPrice 商品原价 - * @return - */ - double calcPrice(double goodsPrice); + void behavior(); } ``` ```java -public class NormalCustomerStrategy implements Strategy{ - @Override - public double calcPrice(double goodsPrice) { - System.out.println("对于新客户或者是普通客户,没有折扣"); - return goodsPrice; - } -} -``` +public class QuackStrategy implements Strategy{ -```java -public class OldCustomerStrategy implements Strategy{ @Override - public double calcPrice(double goodsPrice) { - System.out.println("对于老客户,统一折扣5%"); - return goodsPrice*(1-0.05); + public void behavior() { + System.out.println("quack!"); } } ``` ```java -public class LargeCustomerStrategy implements Strategy{ +public class SqueakStrategy implements Strategy{ @Override - public double calcPrice(double goodsPrice) { - System.out.println("对于大客户,统一折扣10%"); - return goodsPrice*(1-0.1); + public void behavior() { + System.out.println("squeak!"); } } ``` ```java -public class Price { +/** + * Context 是使用到该算法族的类,其中的 doSomething()方法会调用 behavior(), + * setStrategy(Strategy) 方法可以动态地改变 strategy 对象, + * 也就是说能动态地改变 Context 所使用的算法。 + */ +public class Duck { private Strategy strategy; - public double price(double goodsPrice){ - return strategy.calcPrice(goodsPrice); + public void perform(){ + if(strategy!=null){ + strategy.behavior(); + } } public void setStrategy(Strategy strategy) { @@ -1679,18 +1688,20 @@ public class Price { ```java public class Client { public static void main(String[] args) { - //1:选择并创建需要使用的策略对象 - Strategy strategy = new BigCustomerStrategy (); - //2:创建上下文 - Price ctx = new Price(); - ctx.setStrategy(strategy); - - //3:计算报价 - double price = ctx.price(1000); - System.out.println("向客户报价:"+price); + Duck duck = new Duck(); + duck.setStrategy(new QuackStrategy()); + duck.perform(); + duck.setStrategy(new SqueakStrategy()); + duck.perform(); } } ``` + +```html +quack! +squeak! +``` + ### JDK - java.util.Comparator#compare() diff --git a/docs/Spring/06SpringMVC.md b/docs/Spring/06SpringMVC.md index 80dc882..5c833c3 100644 --- a/docs/Spring/06SpringMVC.md +++ b/docs/Spring/06SpringMVC.md @@ -35,17 +35,22 @@ MVC 的原理图: ### [策略模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_9-策略(strategy)) +## SpringMVC -## SpringMVC 框架 +SpringMVC 是一种基于 Java,实现了 Web MVC 设计模式,请求驱动类型的轻量级 Web 框架。优点如下: -SpringMVC 框架是以请求为驱动,围绕 Servlet 设计, -将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。 -其中核心类是 DispatcherServlet,它是一个 Servlet,顶层实现Servlet接口。 +- 基于组件技术。全部的应用对象,无论是控制器、视图,还是业务对象之类都是 Java 组件。并且和 Spring 提供的其他基础结构紧密集成; +- 不依赖于 Servlert API; +- 可以任意使用各种视图技术,而不仅仅局限于jspl; +- 支持各种请求资源的映射策略; +- 易扩展。 -### 1. 使用 +除了观察者模式和策略模式外,涉及到的设计模式有:[适配器模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_1-适配器(adapter))和[责任链模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_1-责任链(chain-of-responsibility))。 -需要在 web.xml 中配置 DispatcherServlet 。 -并且需要配置 Spring 监听器ContextLoaderListener +### 使用 + +需要在 web.xml 中配置 **DispatcherServlet** 。 +并且需要配置 Spring **监听器 ContextLoaderListener**。 - 配置监听器: ```html @@ -74,100 +79,60 @@ SpringMVC 框架是以请求为驱动,围绕 Servlet 设计, ``` -### 2. 原理 - -原理图如下: - - - -**流程说明**: - -1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet。 - -2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。 - -3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后, -开始由 HandlerAdapter 适配器处理。 - -4. HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求, -并处理相应的业务逻辑。 - -5. 处理器处理完业务后,会返回一个 ModelAndView 对象, -Model 是返回的数据对象,View 是个**逻辑上的 View**。 +### 原理 -6. ViewResolver 会根据逻辑 View 查找实际的 View。 +SpringMVC 框架是以请求为驱动,围绕 Servlet 设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。 +其中**核心类是 DispatcherServlet**,它是一个 Servlet,顶层实现 Servlet 接口。 -7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。 - -8. 把 View 返回给请求者(浏览器) - -### 3. 重要组件 - -> DispatcherServlet - -前端控制器,不需要工程师开发,由框架提供。 - -作用:**Spring MVC 的入口函数**。**接收请求,响应结果,相当于转发器,中央处理器**。 -有了 DispatcherServlet 减少了其它组件之间的耦合度。 -DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求, -DispatcherServlet的存在**降低了组件之间的耦合性**。 - -> HandlerMapping - -处理器映射器,不需要工程师开发,由框架提供。 +原理图如下: -作用:根据请求的url查找Handler。 -**HandlerMapping负责根据用户请求找到Handler即处理器(Controller)**, -SpringMVC提供了不同的映射器实现不同的映射方式, -例如:配置文件方式,实现接口方式,注解方式等。 + -> HandlerAdapter +### 重要组件 -处理器适配器 +#### 1、DispatcherServlet -作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler, -通过HandlerAdapter对处理器进行执行,这是适配器模式的应用, -通过扩展适配器可以对更多类型的处理器进行执行。 +- 说明:前端控制器,不需要工程师开发,由 SpringMVC 框架提供。 +- 作用:**Spring MVC 的入口。接收请求,响应结果,相当于转发器,中央处理器**。DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,**DispatcherServlet 降低了组件之间的耦合度**。 -[适配器模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/03%E7%BB%93%E6%9E%84%E5%9E%8B.md#1-%E9%80%82%E9%85%8D%E5%99%A8adapter) +#### 2、HandlerMapping -> Handler +- 说明:处理器映射器,不需要工程师开发,由 SpringMVC 框架提供。 +- 作用:**根据请求的 url 查找 Handler**。SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 -处理器,需要工程师开发 +#### 3、HandlerAdapter -注意:编写Handler时按照HandlerAdapter的要求去做, -这样适配器才可以去正确执行Handler, -Handler是继DispatcherServlet前端控制器的**后端控制器**, -在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 -由于Handler涉及到具体的用户业务请求, -所以一般情况需要工程师根据业务需求开发Handler。 +- 说明:处理器适配器。 +- 作用:**按照特定规则(HandlerAdapter要求的规则)执行 Handler**。通过 HandlerAdapter 对处理器进行执行,这是[适配器模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_1-适配器(adapter))的应用,通过扩展适配器可以对更多类型的处理器进行执行。 -> ViewResolver +#### 4、Handler -视图解析器,不需要工程师开发,由框架提供 +- 说明:处理器,需要工程师开发。 +- 注意:编写 Handler 时按照 HandlerAdapter 的要求的规则去做,这样适配器才可以去正确执行 Handler, + Handler 是**后端控制器**,在 DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。 + 由于 Handler 涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发 Handler。 -作用:进行视图解析,根据逻辑视图名解析成真正的视图(view)。 -ViewResolver负责将处理结果生成View视图, -ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址, -再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 -springmvc框架提供了很多的View视图类型, -包括:jstlView、freemarkerView、pdfView等。 -一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户, -需要由工程师根据业务需求开发具体的页面。 +#### 5、ViewResolver -> View +- 说明:视图解析器,不需要工程师开发,由 SpringMVC 框架提供。 +- 作用:**进行视图解析,根据逻辑视图名解析成真正的视图**。ViewResolver 负责将处理结果生成 View 视图, + ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对View 进行渲染将处理结果通过页面展示给用户。 + SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView等。 + 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要工程师根据业务需求开发具体的页面。 -视图View,需要工程师开发。 +#### 6、View -View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...) +- 说明:视图 View,需要工程师开发。 +- 作用:**View 是一个接口,实现类支持不同的 View类型(jsp、freemarker、pdf...)**。 -### 4. DispatcherServlet 源码解析 +## DispatcherServlet 源码解析 ```java package org.springframework.web.servlet; @SuppressWarnings("serial") -public class DispatcherServlet extends FrameworkServlet { +public class DispatcherServlet extends FrameworkServlet { + // DispatcherServlet,它是一个 Servlet,顶层实现 Servlet 接口。 public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; @@ -281,40 +246,34 @@ public class DispatcherServlet extends FrameworkServlet { } ``` -DispatcherServlet类中与属性相关的Bean: +DispatcherServlet 类中与属性相关的 Bean: | Bean | 说明 | | :---: | :--: | -| HandlerMapping | 用于Handlers映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller注解。 | -| HandlerAdapter | 帮助DispatcherServlet处理映射请求处理程序的适配器,而不用考虑实际调用的是哪个处理程序。| -| ViewResolver | 根据实际配置解析实际的View类型 | -| ThemeResolver | 解决Web应用程序可以使用的主题,例如提供个性化布局。| -| MultipartResolver | 解析多部分请求,以支持从HTML表单上传文件。 | -| FlashMapManager | 存储并检索可用于将一个请求属性传递到另一个请求的input和output的FlashMap,通常用于重定向。| +| HandlerMapping | 用于 Handler 映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller 注解。 | +| HandlerAdapter | 帮助 DispatcherServlet 处理映射请求处理程序的适配器,而不用考虑实际调用的是哪个处理程序。 | +| ViewResolver | 根据实际配置解析实际的 View 类型 | -在Web MVC框架中,每个DispatcherServlet都拥自己的 WebApplicationContext, -它继承了ApplicationContext。 -WebApplicationContext 包含了其上下文和Servlet实例之间共享的所有的基础框架的Bean。 +在 Web MVC 框架中,每个 DispatcherServlet 都拥自己的 WebApplicationContext,它继承了ApplicationContext。**WebApplicationContext 包含了其上下文和Servlet实例之间共享的所有的基础框架的Bean。** -> HandlerMapping +### HandlerMapping 接口的实现类 -HandlerMapping 接口的实现类: - SimpleUrlHandlerMapping 类通过**配置文件**把URL映射到Controller类。 - DefaultAnnotationHandlerMapping 类通过**注解**把URL映射到Controller类。 -> HandlerAdapter +### HandlerAdapter 接口的实现类 -HandlerAdapter 接口的实现类: - AnnotationMethodHandlerAdapter 类通过注解,把请求URL映射到Controller类的方法上。 -> HandlerExceptionResolver +### HandlerExceptionResolver 接口的实现类 -HandlerExceptionResolver 接口的实现类: - SimpleMappingExceptionResolver 类通过配置文件进行异常处理。 - AnnotationMethodHandlerExceptionResolver 类通过注解进行异常处理。 -> ViewResolver +### ViewResolver 接口实现类 + +- UrlBasedViewResolver 类通过配置文件,把一个视图名交给到一个View来处理。 -ViewResolver接口实现类: +# 参考资料 -- UrlBasedViewResolver 类通过配置文件,把一个视图名交给到一个View来处理。 \ No newline at end of file +- [SpringMVC框架理解](https://blog.csdn.net/litianxiang_kaola/article/details/79169148) \ No newline at end of file From b6a654b3dfc04502c3a181d457a6d1ee2eb10ce7 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月25日 16:44:25 +0800 Subject: [PATCH 11/29] Java-Notes --- docs/Spring/06SpringMVC.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/docs/Spring/06SpringMVC.md b/docs/Spring/06SpringMVC.md index 5c833c3..7fb5569 100644 --- a/docs/Spring/06SpringMVC.md +++ b/docs/Spring/06SpringMVC.md @@ -25,16 +25,6 @@ MVC 的原理图: 一般 Controller 在 Model 和 View 之间起到了沟通的作用,处理用户在 View 上的输入,并转发给 Model 来更改Model 状态值。 这样 Model 和 View 两者之间可以做到**松散耦合**,甚至可以彼此不知道对方,而由Controller连接起这两个部分。也在前言里提到,MVC 用到了策略模式,这是**因为 View 用一个特定的 Controller 的实例来实现一个特定的响应策略**,更换不同的Controller,可以改变 View 对用户输入的响应。 -## MVC 中涉及到的设计模式 - -利用**"观察者"**让控制器 (Controller) 和视图 (View) 可以随最新的状态改变而改变。 - -视图 (View) 和控制器 (Controller) 则实现了**"策略模式"**,控制器是视图的行为。 - -### [观察者模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_7-观察者(observer)) - -### [策略模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_9-策略(strategy)) - ## SpringMVC SpringMVC 是一种基于 Java,实现了 Web MVC 设计模式,请求驱动类型的轻量级 Web 框架。优点如下: @@ -45,7 +35,7 @@ SpringMVC 是一种基于 Java,实现了 Web MVC 设计模式,请求驱动 - 支持各种请求资源的映射策略; - 易扩展。 -除了观察者模式和策略模式外,涉及到的设计模式有:[适配器模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_1-适配器(adapter))和[责任链模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_1-责任链(chain-of-responsibility))。 +涉及到的设计模式有:[适配器模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_1-适配器(adapter))和[责任链模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_1-责任链(chain-of-responsibility))。 ### 使用 From cc164714155e7c27800373109468a4bb4b945f1e Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月26日 11:14:44 +0800 Subject: [PATCH 12/29] Update Redis.md --- docs/DataBase/Redis.md | 781 +++++++++++++++++++++-------------------- 1 file changed, 399 insertions(+), 382 deletions(-) diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index dfe7a1a..17c28f6 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -527,9 +527,15 @@ appendonly yes > **重写 AOF** -为了解决 AOF 体积过大的问题,用户可以向 Redis 发送 **BGREWRITEAOF 命令** ,这个命令会通过**移除 AOF 文件中的冗余命令来重写(rewrite)AOF 文件来减小 AOF 文件的体积**。(新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。) +AOF重写的原理是:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,最后代替之前记录这个键值对的多条命令。 -BGREWRITEAOF 命令和 BGSAVE 创建快照原理十分相似,AOF 文件重写也需要用到子进程。执行 BGREWRITEAOF 命令时,**Redis 服务器会维护一个 AOF 重写缓冲区**,该缓冲区会在子进程创建新的 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新的 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新的 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 +为了解决因为 AOF 体积过大重写过程被长时间阻塞的问题,用户可以向 Redis 发送 **BGREWRITEAOF 命令** ,这个命令会通过**移除 AOF 文件中的冗余命令来重写(rewrite)AOF 文件来减小 AOF 文件的体积**。(新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。) + +BGREWRITEAOF 命令和 BGSAVE 创建快照原理十分相似,AOF 文件重写也需要用到子进程。执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区和一个AOF缓冲区,子进程创建新的 AOF 文件期间,记录服务器执行的所有写命令会同时写入两个缓冲区中。这样保证了 1.AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。2.从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。 + +当子进程完成AOF重写的工作之后,服务器会将重写缓冲区中的所有内容追加到新的 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件原子地覆盖旧的 AOF 文件,以此来完成 AOF 文件重写操作。 + +在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。 @@ -552,6 +558,16 @@ Redis 4.0 对于持久化机制进行优化:Redis 4.0 开始支持 RDB 和 AOF ## 八、事务 +### 相关命令 + +MULTI命令:将客户端从非事务状态切换到事务状态,标志着事务的开始。 + +EXEC命令:客户端向服务端发送该命令后,服务器会遍历这个客户端的事务队列,并将所有命令的执行结果返回给客户端。 + +WATCH命令:它是一个乐观锁,可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行的时候,检查被监视的键是否至少有一个已经被修改过了,如果有,服务器将拒绝执行该事务,并向客户端返回代表事务执行失败的空回复。 + +### 简述 + **Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务功能**。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制。服务器在执行事务期间,不会改去执行其它客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。 事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为**pipeline**。 @@ -566,7 +582,11 @@ Redis 4.0 对于持久化机制进行优化:Redis 4.0 开始支持 RDB 和 AOF pipeline 可以一次性发送多条命令并在执行完后一次性将结果返回,可以减少客户端与服务器之间的**网络通信次数**从而提升性能,并且 **pineline 基于队列**,而队列的特点是先进先出,这样就保证数据的**顺序性**。 -> **Redis 并发竞争问题** +Redis 的事务和传统关系型数据库事务的最大区别在于,**Redis 不支持事务回滚机制(rollback)**,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。因为其作者认为,Redis 事务执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他任务没有必要开发Redis 的回滚功能。 + + + +## 九、Redis 并发竞争问题 并发竞争问题举例: @@ -589,478 +609,212 @@ Redis 4.0 对于持久化机制进行优化:Redis 4.0 开始支持 RDB 和 AOF -## 九、高并发和高可用的实现 +## 十、Redis 集群 -Redis 实现**高并发**主要依靠**主从架构**,进行主从架构部署后,使用**Sentinel(哨兵)**实现**高可用**。 +Redis 集群主要是针对海量数据、高并发高可用的场景。 -> 扩展:[高并发](https://www.jianshu.com/p/be66a52d2b9b) +Redis 集群是一个提供在**多个Redis 间节点间共享数据**的程序集。 -### Redis 主从架构实现高并发 +Redis 集群并不支持处理多个键的命令,因为这需要在不同的节点间移动数据,在高负载的情况下可能会导致不可预料的错误。 -单机的 Redis,能够承载的 QPS(每秒的响应请求数,即最大吞吐量)大概就在上万到几万不等。 +Redis 集群通过分区来提供**一定程度的可用性**,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。Redis 集群的优势: -对于缓存来说,一般都是用来支持**读高并发**。主从架构(Master/Slave 架构)中:Master 负责写,并且将数据**复制**到其它的 Slave 节点,而 Slave 节点负责读,所有的**读请求全部访问 Slave 节点**。 +- 自动将数据进行分片,每个 Master 上放一部分数据 +- 提供内置的高可用支持,部分 Master 不可用时,还可以继续工作 -Redis 主从架构可以很轻松实现**水平扩展**来支持高并发。 +在 Redis 集群架构下,每个 Redis 要放开两个端口号,假设是 6379,另一个就是加 10000 的端口号,即 16379。16379 端口号就是用来进行节点间通信的,也就是 cluster bus,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议:gossip 协议,用于节点间进行高效的数据交换,并且占用更少的网络带宽和处理时间。 - +### 节点间的内部通信机制 -#### Redis 主从复制核心机制 +集群元数据的维护有两种方式: -核心机制如下: +- 集中式 +- gossip 协议 -- Redis 采用**异步方式**复制数据到 Slave 节点。从 Redis2.8 开始,Slave 节点会周期性地确认自己每次复制的数据量; -- 一个 Master 节点可配置多个 Slave 节点,Slave 节点也可以连接其他的 Slave 节点; -- Slave 节点进行复制时,不会 block Master 节点的正常工作;也不会 block 查询操作,它会用旧的数据集来提供服务,但是当复制完成时,需要删除旧数据集,加载新数据集,此时会暂停对外服务; -- Slave 节点主要用来进行水平扩展,做读写分离,扩容的 Slave 节点可以提高读的吞吐量。 +#### 集中式 -注意: +集中式是将集群元数据(节点信息、故障等等)集中存储在某个节点上。一般底层基于 Zookeeper 对所有元数据进行存储维护。 -- 如果采用主从架构,那么建议**必须开启 Master 节点的持久化**(详细信息看第七章)。不建议用 Slave 节点n作为 Master 节点的数据热备,因为如果关闭 Master 节点的持久化,可能在 Master 宕机重启的时候数据是空的,然后可能一经过复制, Slave 节点的数据也丢了。 -- 准备 Master 的各种备份方案。万一本地的所有文件丢失,从备份中挑选一份 RDB 去恢复 Master,这样可确保启动时,是有数据的。即使采用了高可用机制,Slave 节点可以自动接管 Master 节点,但也可能哨兵还没检测到 Master Failure,Master 节点就自动重启了,还是可能导致上面所有的 Slave 节点数据被清空。 + -#### Redis 主从复制核心原理 +集中式的好处: -当启动一个 Slave 节点时,它会发送一个 `PSYNC` 命令给 Master 节点。 +元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到; -如果这是 Slave 节点初次连接到 Master 节点,那么会触发一次 `full resynchronization` **全量复制**。此时 Master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端新收到的所有写命令缓存在内存中。 +集中式的问题: -`RDB` 文件生成完毕后, Master 会将这个 `RDB` 发送给 Slave,Slave 会**先写入本地磁盘,再从本地磁盘加载到内存中**,接着 Master 会将内存中缓存的写命令发送到 Slave,Slave 也会同步这些数据。Slave 节点如果跟 Master节点有网络故障,断开了连接,会自动重连,连接之后 Master 仅会复制给 Slave 部分缺少的数据。 +所有的元数据的更新压力全部集中在某个节点,可能会导致元数据的存储有压力。 - +#### gossip 协议 -> **断点续传** +Redis 维护集群元数据采用另一个方式:[gossip 协议](https://zhuanlan.zhihu.com/p/41228196)。 -从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 +所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 -Master 节点在内存中维护一个 backlog,Master 和 Slave 都会保存`replica offset`和 `master run id`,其中`replica offset` 就保存在 backlog 中。如果 Master 和 Slave 网络连接断掉了,Slave 会让 Master 从上次 的`replica offset` 开始继续复制,如果没有找到对应的 `replica offset`,那么就会执行一次 `PSYNC` 命令。 + -注意:如果根据 host+ip 定位 Master 节点是不可靠的。如果 Master 节点重启或者数据出现了变化,那么 Slave 节点应该根据不同的 `master run id` 区分。 +gossip 的好处: -> **无磁盘化复制** +元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续到所有节点上去更新,降低了压力; -之前的版本中,一次完整的重同步需要在磁盘上创建一个 RDB ,然后从磁盘重新加载同一个 RDB 来服务 Slave。 +gossip 的问题: -Master 在内存中直接创建 RDB,然后直接发送给 Slave,不使用磁盘作中间存储。只需要在配置文件中开启 `repl-diskless-sync yes` 即可。 +元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 -``` -repl-diskless-sync yes -# Master 在内存中直接创建 RDB,然后发送给 Slave,不会存入本地磁盘。 +gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。 -repl-diskless-sync-delay 5 -# 等待 5s 后再开始复制,因为要等更多 Slave 重新连接过来。 -``` +- **meet** -> **过期键处理** + 某个节点发送 meet 消息给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 -Slave 不会过期键,只会等待 Master 过期键。如果 Master 过期了一个键,或者通过 LRU 淘汰了一个键,那么会模拟一条 del 命令发送给 Slave。 +- **ping** -#### Redis 主从复制完整流程 + 每个节点都会频繁给其它节点发送 ping,其中包含自己的**状态**还有自己维护的**集群元数据**,节点通过 ping 交换元数据。 -Slave 节点启动时,会在自己本地保存 Master 节点的信息,包括 Master 节点的 host 和 ip,但是复制流程没开始。 +- **pong** -Slave 节点内部有个定时任务,每秒检查是否有新的 Master 节点要连接和复制,如果发现,就跟 Master 节点建立 Socket 网络连接。 + ping 或 meet 的返回消息,包含自己的状态和其它信息,也用于信息广播和更新。 -Slave 节点发送 ping 命令给 Master 节点,如果 Master 设置了 requirepass,那么 Slave 节点必须发送 masterauth 的口令过去进行认证。 +- **fail** -Master 节点第一次执行**全量复制**,将所有数据发给 Slave 节点。 + 某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 -Master 节点持续将写命令,**异步复制**给 Slave 节点。 +> **ping 消息深入** - +ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 -> **全量复制** +每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 -- Master 启动全量复制,将所有数据发给 Slave 节点。 +每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含(总节点数 - 2) 个其它节点的信息。 -- Master 执行 BGSAVE,在本地生成一份 RDB 快照文件。 +### 分布式寻址算法 -- Master 节点将 RDB 快照文件发送给 Slave 节点,如果 RDB 复制时间超过 60秒(repl-timeout),那么 Slave 节点就会认为复制失败。 +分布式寻址算法有如下 3 种: - (可以适当调大这个参数,对于千兆网卡的机器,一般每秒传输 100 MB,传输 6 G 文件可能超过 60s) +- Hash 算法 +- 一致性 Hash 算法 +- Redis 集群的 Hash Slot 算法 -- Master 节点在生成 RDB 时,会将所有新的写命令缓存在内存中,在 Slave 节点保存了 RDB 之后,再将新的写命令复制给 Slave 节点。 +#### Hash 算法 -- 如果在复制期间,内存缓冲区持续消耗超过 64 MB,或者一次性超过 256 MB,那么停止复制,复制失败。 +根据键值,首先计算哈希值,然后对节点数取模,然后映射在不同的 Master 节点上。 -``` -client-output-buffer-limit slave 256MB 64MB 60 -``` +一旦某一个 Master 节点宕机,当请求过来时,会基于最新的剩余 Master 节点数去取模,尝试去获取数据,导致大部分的请求过来,全部无法拿到有效的缓存,大量的流量涌入数据库。 -- Slave 节点接收到 RDB 之后,清空旧数据,然后重新加载 RDB 到内存中,同时**基于旧的数据版本**对外提供服务。 -- 如果 Slave 节点开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。 +换句话说,**当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端数据库请求数据**。 -> **增量复制** + -- 如果全量复制过程中,Master 和 Slave 网络连接断掉,那么 Slave 重新连接 Master 时,会触发增量复制。 -- Master 直接从自己的 backlog 中获取部分丢失的数据,发送给 Slave 节点,默认 backlog 大小是 1 MB。 -- Master 就是根据 Slave 发送的 `PSYNC` 命令中的 `replica offset`来从 backlog 中获取数据的。 +#### 一致性 Hash 算法* -> **异步复制** +一致性 Hash 算法将整个哈希值空间组织成一个**虚拟的圆环**,假设某哈希函数 H 的值空间为0~2^32-1(即哈希值是一个32位无符号整形)。 -Master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 Slave 节点。 +整个空间按**顺时针方向**组织,圆环的正上方的点代表0,0 点右侧的第一个点代表1,以此类推,2、3 ... 2^32-1,也就是说 0 点左侧的第一个点代表 2^32-1, 0 和 2^32-1 在零点中方向重合,我们把这个由 2^32 个点组成的圆环称为**哈希环**。 -> **heartbeat** + -主从节点互相都会发送 heartbeat 信息。 +将各个服务器进行哈希,具体可以选择服务器的 IP 或主机名作为关键字进行哈希,这样每台机器就能**确定其在哈希环上的位置**。假设将 4 台服务器的 IP 地址哈希后在哈希环的位置如下: -Master 默认每隔 10 秒发送一次 heartbeat,Slave 节点每隔 1 秒发送一个 heartbeat。 + -### Redis 哨兵集群实现高可用 +接下来使用如下算法定位数据访问到相应服务器: -如果系统在 365 天内,有 99.99% 的时间,都是可以对外提供服务,那么就说系统是高可用的。 +将数据键使用相同的函数 Hash 计算出哈希值,并确定此数据在环上的位置,从此位置**沿环顺时针"行走",第一台遇到的服务器就是其应该定位到的服务器**。 -一个 Slave 挂掉了,是不会影响可用性的,还有其它的 Slave 在提供相同数据下的相同的对外的查询服务。但是,如果 Master 节点挂掉了,Slave 节点还有什么用呢,因为没有 Master 给它们复制数据,系统相当于不可用。 +例如有 Object A、Object B、Object C、Object D 四个数据对象,经过哈希计算后,在哈希环上的位置如下: -Redis 的高可用架构,叫做 failover 故障转移,也叫做**主备切换**。 + -Master 节点在故障时,自动检测,并且将某个 Slave 节点自动切换为 Master 节点的过程,叫做主备切换。 +根据一致性 Hash 算法: -#### 哨兵简介 +Object A 会被定位到 Node A 上; -Sentinel(哨兵)是 Redis 集群中非常重要的一个组件,主要有以下功能: +Object B 会被定位到 Node B 上; -- 集群监控:负责监控 Redis Master 和 Slave 进程是否正常工作。 -- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 -- 故障转移:如果 Master 节点挂掉了,会自动转移到 Slave 节点上。 -- 配置中心:如果故障转移发生了,通知 client 客户端新的 Master 地址。 +Object C 会被定位到 Node C 上; -哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作: +Object D 会被定位到 Node D 上。 -- 故障转移时,判断一个 Master 节点是否宕机了,需要大部分(majority)的哨兵都同意才行,涉及到了分布式选举的问题。 -- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。 +> **容错性和可扩展性** -#### 哨兵核心知识 +假设 Node C 宕机,可以看到此时对象 A、B、D 不会受到影响,只有对象 C 被重定位到 Node D。 -核心知识: + -- 哨兵至少需要 3 个实例,来保证自己的健壮性。 -- 哨兵结合 Redis 主从架构,**不保证数据零丢失**,只能保证 Redis 集群的高可用。 +通常情况下,一致性 Hash 算法中,如果一台服务器不可用,则**受影响的数据**仅仅是此服务器到其环空间中前一台服务器(即沿着**逆时针方向行走遇到的第一台服务器**)之间数据,其它数据不会受到影响。 -注意:哨兵集群必须部署 2 个以上节点。假设哨兵集群仅仅部署了 2 个哨兵实例,则 quorum = 1。 +下面考虑另外一种情况:如果在系统中增加一台服务器 Node X。 -``` -+----+ +----+ -| M1 |---------| R1 | -| S1 | | S2 | -+----+ +----+ -``` +此时对象 A、B、D 不受影响,只有对象 C 需要重定位到新的Node X 。 -如果 Master 宕机, S1 和 S2 中只要有 1 个哨兵认为 Master 宕机了,就可以进行切换,同时 S1 和 S2 会选举出一个哨兵来执行**故障转移**,此时需要"大多数哨兵"都是运行的。下标是集群与"大多数"的对应关系: + -| 哨兵数 | majority | -| :----: | :------: | -| 2 | 2 | -| 3 | 2 | -| 4 | 2 | -| 5 | 3 | +通常情况下,一致性 Hash 算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。 -*表中第一行 哨兵数 = 2 ,majority = 2,说明该 2 哨兵集群中至少有 2 个哨兵是可用的,才能保证集群正常工作。* +综上所述,一致性 Hash 算法对于节点的增减都只需**重定位哈希环中的一小部分数据**,具有**较好的容错性和可扩展性**。 -如果此时仅仅是 M1 进程宕机了,哨兵 S1 正常运行,那么可以进行故障转移。(哨兵数 = 2,majority = 2) +> **数据倾斜问题** -如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。 +一致性 Hash 算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。例如系统中只有 2 台服务器,如下所示: -经典的 3 节点哨兵集群如下(quorum = 2): + -``` - +----+ - | M1 | - | S1 | - +----+ - | -+----+ | +----+ -| R2 |----+----| R3 | -| S2 | | S3 | -+----+ +----+ -``` +此时必然造成大量数据集中到 Node A 上,而只有极少量会定位到 Node B 上。 -如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 Master 宕机了,然后选举出一个哨兵来执行故障转移,同时 3 个哨兵的 majority 是 2,允许执行故障转移。 +为了解决这种数据倾斜问题,一致性 Hash 算法引入了**虚拟节点机制**,即**对每一个服务节点计算多个哈希**,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器 IP 或主机名的后面增加编号来实现。 -#### 数据丢失问题 +例如针对上面的情况,可以为每台服务器计算 3 个虚拟节点: -主备切换的过程,可能会导致数据丢失问题: +- Node A 的 3 个虚拟节点:"Node A#1"、"Node A#2"、"Node A#3" +- Node B 的 3 个虚拟节点:"Node B#1"、"Node B#2"、"Node B#3" -- **异步复制导致的数据丢失** +进行哈希计算后,六个虚拟节点在哈希环中的位置如下: - 因为 Master 到 Slave 的复制是异步的,所以可能有部分数据还没复制到 Slave,Master 就宕机了,这部分数据就会丢失。 + -- **脑裂导致的数据丢失** +同时**数据定位算法不变**,只是多了一步虚拟节点到实际节点的映射过程,例如"Node A#1"、"Node A#2"、"Node A#3" 这 3 个虚拟节点的数据均定位到 Node A 上,解决了服务节点少时数据倾斜的问题。 - 脑裂指的是某个 Master 所在机器突然**脱离了正常的网络**,跟其他 Slave 机器不能连接,但是实际上 Master 还运行着。哨兵可能就会认为 Master 宕机了,然后开启选举,将其他 Slave 切换成了 Master,此时集群中就会有两个 Master ,也就是所谓的**脑裂**。 +在实际应用中,通常将虚拟节点数设置为 32 甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。 - 虽然某个 Slave 被切换成了 Master,但是 **client 可能还没来得及切换到新的 Master,仍继续向旧 Master 写数据**,当旧 Master 再次恢复的时候,会被作为一个 Slave 挂到新的 Master 上去,自己的数据会清空,重新从新的 Master 复制数据。而新的 Master 并没有后来 client 写入的数据,这部分数据就丢失了。 +### Redis 集群高可用与主备切换原理 -#### 数据丢失问题解决方案 +Redis 集群的高可用的原理,几乎跟哨兵是类似的: -进行如下配置: +- **判断节点是否宕机** -```xml -min-slaves-to-write 1 -min-slaves-max-lag 10 -# 要求至少有 1 个 Slave,数据复制和同步的延迟不能超过 10 秒。 -# 如果所有的 Slave 数据复制和同步的延迟都超过了 10 秒钟,那么 Master 就不会再接收任何请求了。 -``` + 主观宕机:如果一个节点认为另外一个节点宕机,那么就是 pfail,称为主观宕机。 -- **减少异步复制导致的数据丢失** + 客观宕机:如果多个节点都认为另外一个节点宕机了,那么就是 fail,称为客观宕机。 - 上述配置可确保:一旦 Slave 复制数据和 ACK 延时太长,就认为可能 Master 宕机后损失的数据太多,那么就拒绝写请求,这样可以把 Master 宕机时由于部分数据未同步到 Slave 导致的数据丢失降低的可控范围内。 + 在 `cluster-node-timeout` 内,某个节点一直没有返回 pong ,那么就被认为 pfail 。 -- **减少脑裂导致的数据丢失** + 如果一个节点认为某个节点 pfail ,那么会在 gossip ping 消息中,ping 给其他节点,如果**超过半数**的节点都认为 pfail 了,那么就会变成 fail。 - 出现脑裂情况,上面两个配置可确保:如果 Master 不能继续给指定数量的 Slave 发送数据,而且 Slave 超过 10 秒没有返回 ACK 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。 +- **Slave 节点过滤** -#### 选举算法 + 对宕机的 Master 节点,从其所有的 Slave 节点中,选择一个切换成 Master 节点。检查每个 Slave 节点与 Master 节点断开连接的时间,如果超过 -> sdown 和 odown 转换机制 + `cluster-node-timeout * cluster-slave-validity-factor`, -- sdown 是**主观(subjective)宕机**,如果一个哨兵觉得一个 Master 宕机,那么就是主观宕机。 + 那么该 Slave 就没有资格切换成 Master。 - sdown 达成的条件很简单,如果一个哨兵 ping 一个 Master,超过了 `is-master-down-after-milliseconds` 指定的毫秒数之后,就主观认为 Master 宕机。 +- **Slave 节点选举** -- odown 是**客观(objective)宕机**,如果 quorum 数量的哨兵都觉得一个 Master 宕机,那么就是客观宕机。 + 每个 Slave 节点,都根据自己对 Master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的 Slave 节点,选举时间越靠前,优先进行选举。 - 如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 Master 是 sdown 的,那么就认为是 odown 了。 + 所有的 Master 节点开始 Slave 选举投票,给要进行选举的 Slave 进行投票,如果大部分 Master 节点(N/2+1) 都投票给了某个 Slave 节点,那么选举通过,那个 Slave 节点可以切换成 Master。 -> **quorum 和 majority** + Slave 节点执行主备切换,Slave 节点切换为 Master 节点。 -每次一个哨兵要做主备切换,首先需要 **quorum 数量的哨兵认为 odown**,然后从中选出一个哨兵来做切换,这个哨兵还需要得到 **majority 哨兵是可运行的**,才能正式执行切换。 +## 十一、缓存雪崩、缓存穿透和缓存击穿 -- quorum < majority,majority 数量的哨兵是可运行的,就可执行切换。 -- quorum>= majority,必须 quorum 数量的哨兵是可运行的才能执行切换。 +### 缓存雪崩 -如果一个 Master 被认为 odown,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作。此时首先要选举一个 Slave 来,会考虑 Slave 的一些信息: +> **问题** -- **跟 Master 断开连接的时长** +缓存雪崩是指在我们设置缓存时采用了**相同的过期时间**,导致**缓存在某一时刻同时失效**,请求全部转发到数据库,数据库瞬时压力过重雪崩。 - 一个 Slave 如果跟 Master 断开连接的时间超过`(down-after-milliseconds * 10) + Master 宕机的时长` - - ,那么就被认为不适合选举为 Master。 - -- **Slave 优先级** - - slave priority 值越小,优先级就越高 - -- **`replica offset`** - - 如果 slave priority 相同,那么比较`replica offset` 。 - - Slave 复制的数据越多,`replica offset` 越靠后,优先级就越高。 - -- **`master run id`** - - 如果 slave priority 和 `replica offset` 都相同,那么选择一个`master run id`较小的 Slave。 - - - -## 十、Redis 集群 - -Redis 集群主要是针对海量数据、高并发高可用的场景。 - -Redis 集群是一个提供在**多个Redis 间节点间共享数据**的程序集。 - -Redis 集群并不支持处理多个键的命令,因为这需要在不同的节点间移动数据,在高负载的情况下可能会导致不可预料的错误。 - -Redis 集群通过分区来提供**一定程度的可用性**,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。Redis 集群的优势: - -- 自动将数据进行分片,每个 Master 上放一部分数据 -- 提供内置的高可用支持,部分 Master 不可用时,还可以继续工作 - -在 Redis 集群架构下,每个 Redis 要放开两个端口号,假设是 6379,另一个就是加 10000 的端口号,即 16379。16379 端口号就是用来进行节点间通信的,也就是 cluster bus,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议:gossip 协议,用于节点间进行高效的数据交换,并且占用更少的网络带宽和处理时间。 - -### 节点间的内部通信机制 - -集群元数据的维护有两种方式: - -- 集中式 -- gossip 协议 - -#### 集中式 - -集中式是将集群元数据(节点信息、故障等等)集中存储在某个节点上。一般底层基于 Zookeeper 对所有元数据进行存储维护。 - - - -集中式的好处: - -元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到; - -集中式的问题: - -所有的元数据的更新压力全部集中在某个节点,可能会导致元数据的存储有压力。 - -#### gossip 协议 - -Redis 维护集群元数据采用另一个方式:gossip 协议。 - -所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 - - - -gossip 的好处: - -元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续到所有节点上去更新,降低了压力; - -gossip 的问题: - -元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 - -gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。 - -- **meet** - - 某个节点发送 meet 消息给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 - -- **ping** - - 每个节点都会频繁给其它节点发送 ping,其中包含自己的**状态**还有自己维护的**集群元数据**,节点通过 ping 交换元数据。 - -- **pong** - - ping 或 meet 的返回消息,包含自己的状态和其它信息,也用于信息广播和更新。 - -- **fail** - - 某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 - -> **ping 消息深入** - -ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 - -每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 - -每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含(总节点数 - 2) 个其它节点的信息。 - -### 分布式寻址算法 - -分布式寻址算法有如下 3 种: - -- Hash 算法 -- 一致性 Hash 算法 -- Redis 集群的 Hash Slot 算法 - -#### Hash 算法 - -根据键值,首先计算哈希值,然后对节点数取模,然后映射在不同的 Master 节点上。 - -一旦某一个 Master 节点宕机,当请求过来时,会基于最新的剩余 Master 节点数去取模,尝试去获取数据,导致大部分的请求过来,全部无法拿到有效的缓存,大量的流量涌入数据库。 - -换句话说,**当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端数据库请求数据**。 - - - -#### 一致性 Hash 算法* - -一致性 Hash 算法将整个哈希值空间组织成一个**虚拟的圆环**,假设某哈希函数 H 的值空间为0~2^32-1(即哈希值是一个32位无符号整形)。 - -整个空间按**顺时针方向**组织,圆环的正上方的点代表0,0 点右侧的第一个点代表1,以此类推,2、3 ... 2^32-1,也就是说 0 点左侧的第一个点代表 2^32-1, 0 和 2^32-1 在零点中方向重合,我们把这个由 2^32 个点组成的圆环称为**哈希环**。 - - - -将各个服务器进行哈希,具体可以选择服务器的 IP 或主机名作为关键字进行哈希,这样每台机器就能**确定其在哈希环上的位置**。假设将 4 台服务器的 IP 地址哈希后在哈希环的位置如下: - - - -接下来使用如下算法定位数据访问到相应服务器: - -将数据键使用相同的函数 Hash 计算出哈希值,并确定此数据在环上的位置,从此位置**沿环顺时针"行走",第一台遇到的服务器就是其应该定位到的服务器**。 - -例如有 Object A、Object B、Object C、Object D 四个数据对象,经过哈希计算后,在哈希环上的位置如下: - - - -根据一致性 Hash 算法: - -Object A 会被定位到 Node A 上; - -Object B 会被定位到 Node B 上; - -Object C 会被定位到 Node C 上; - -Object D 会被定位到 Node D 上。 - -> **容错性和可扩展性** - -假设 Node C 宕机,可以看到此时对象 A、B、D 不会受到影响,只有对象 C 被重定位到 Node D。 - - - -通常情况下,一致性 Hash 算法中,如果一台服务器不可用,则**受影响的数据**仅仅是此服务器到其环空间中前一台服务器(即沿着**逆时针方向行走遇到的第一台服务器**)之间数据,其它数据不会受到影响。 - -下面考虑另外一种情况:如果在系统中增加一台服务器 Node X。 - -此时对象 A、B、D 不受影响,只有对象 C 需要重定位到新的Node X 。 - - - -通常情况下,一致性 Hash 算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。 - -综上所述,一致性 Hash 算法对于节点的增减都只需**重定位哈希环中的一小部分数据**,具有**较好的容错性和可扩展性**。 - -> **数据倾斜问题** - -一致性 Hash 算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。例如系统中只有 2 台服务器,如下所示: - - - -此时必然造成大量数据集中到 Node A 上,而只有极少量会定位到 Node B 上。 - -为了解决这种数据倾斜问题,一致性 Hash 算法引入了**虚拟节点机制**,即**对每一个服务节点计算多个哈希**,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器 IP 或主机名的后面增加编号来实现。 - -例如针对上面的情况,可以为每台服务器计算 3 个虚拟节点: - -- Node A 的 3 个虚拟节点:"Node A#1"、"Node A#2"、"Node A#3" -- Node B 的 3 个虚拟节点:"Node B#1"、"Node B#2"、"Node B#3" - -进行哈希计算后,六个虚拟节点在哈希环中的位置如下: - - - -同时**数据定位算法不变**,只是多了一步虚拟节点到实际节点的映射过程,例如"Node A#1"、"Node A#2"、"Node A#3" 这 3 个虚拟节点的数据均定位到 Node A 上,解决了服务节点少时数据倾斜的问题。 - -在实际应用中,通常将虚拟节点数设置为 32 甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。 - -### Redis 集群高可用与主备切换原理 - -Redis 集群的高可用的原理,几乎跟哨兵是类似的: - -- **判断节点是否宕机** - - 主观宕机:如果一个节点认为另外一个节点宕机,那么就是 pfail,称为主观宕机。 - - 客观宕机:如果多个节点都认为另外一个节点宕机了,那么就是 fail,称为客观宕机。 - - 在 `cluster-node-timeout` 内,某个节点一直没有返回 pong ,那么就被认为 pfail 。 - - 如果一个节点认为某个节点 pfail ,那么会在 gossip ping 消息中,ping 给其他节点,如果**超过半数**的节点都认为 pfail 了,那么就会变成 fail。 - -- **Slave 节点过滤** - - 对宕机的 Master 节点,从其所有的 Slave 节点中,选择一个切换成 Master 节点。检查每个 Slave 节点与 Master 节点断开连接的时间,如果超过 - - `cluster-node-timeout * cluster-slave-validity-factor`, - - 那么该 Slave 就没有资格切换成 Master。 - -- **Slave 节点选举** - - 每个 Slave 节点,都根据自己对 Master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的 Slave 节点,选举时间越靠前,优先进行选举。 - - 所有的 Master 节点开始 Slave 选举投票,给要进行选举的 Slave 进行投票,如果大部分 Master 节点(N/2+1) 都投票给了某个 Slave 节点,那么选举通过,那个 Slave 节点可以切换成 Master。 - - Slave 节点执行主备切换,Slave 节点切换为 Master 节点。 - - - -## 十一、缓存雪崩、缓存穿透和缓存击穿 - -### 缓存雪崩 - -> **问题** - -缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致**缓存在某一时刻同时失效**,请求全部转发到数据库,数据库瞬时压力过重雪崩。 - -> **解决** +> **解决** 缓存雪崩的事前事中事后的解决方案如下: @@ -1080,12 +834,12 @@ Redis 集群的高可用的原理,几乎跟哨兵是类似的: > **问题** -缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,数据库可能就挂掉了,有人利用不存在的键频繁攻击我们的应用,这就是漏洞。 +缓存穿透是指查询**一个一定不存在的数据**,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,数据库可能就挂掉了,有人利用不存在的键频繁攻击我们的应用,这就是漏洞。 > **解决** - 简单粗暴的方法:如果一个查询返回的数据为空(不管数据是否存在,还是系统故障),我们仍然把这个**空结果进行缓存**,但它的过期时间会很短,最长不超过 5 分钟。 -- **布隆过滤器**:布隆过滤器能够以极小的空间开销解决海量数据判重问题。一个一定不存在的数据会被该过滤器拦截掉,从而避免了对底层存储系统的查询压力。 +- **布隆过滤器**:布隆过滤器能够以极小的空间开销解决海量数据判重问题。**一个一定不存在的数据会被该过滤器拦截掉**,从而避免了对底层存储系统的查询压力。 > 扩展:[布隆过滤器](https://xiaozhuanlan.com/topic/2847301659) @@ -1097,11 +851,9 @@ Redis 集群的高可用的原理,几乎跟哨兵是类似的: > **解决** -- 将热点数据设置为永远不过期。不会出现热点键过期问题。 +- 将热点数据设置为**永远不过期**。不会出现热点键过期问题。 - 基于 Redis 或 Zookeeper 实现互斥锁。根据键获得的值为空时,先加锁,然后从数据库加载数据,加载完毕后再释放锁。其他线程发现获取锁失败,等待一段时间后重试。 - - ## 十二、Redis 和数据库双写一致性问题 一致性问题是分布式常见问题,还可以再分为**最终一致性**和**强一致性**。 @@ -1117,7 +869,7 @@ Cache Aside Pattern 最经典的缓存结合数据库读写的模式。 问题:**为什么是删除缓存,而不是更新缓存?** -举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 100 次,那么缓存更新 100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。 +举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 100 次,那么缓存更新 100 次;但是这个缓存在 1 分钟内只被**读取**了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。 其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,**不要每次都重新做复杂的计算,不管它会不会用到,而是让它在需要被使用时才重新计算**。 @@ -1143,7 +895,7 @@ Cache Aside Pattern 最经典的缓存结合数据库读写的模式。 数据更新,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后更新数据的程序完成了数据库的修改,此时,数据库和缓存中的数据又不一致了。 -只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现这种问题。 +只有在对一个数据在**并发**的进行读写的时候,才可能会出现这种问题。如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现这种问题。 > **解决方案** @@ -1266,9 +1018,274 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 +## 十六、高并发和高可用的实现 + +Redis 实现**高并发**主要依靠**主从架构**,进行主从架构部署后,使用**Sentinel(哨兵)**实现**高可用**。 + +> 扩展:[高并发](https://www.jianshu.com/p/be66a52d2b9b) + +### Redis 主从架构实现高并发 + +单机的 Redis,能够承载的 QPS(每秒的响应请求数,即最大吞吐量)大概就在上万到几万不等。 + +对于缓存来说,一般都是用来支持**读高并发**。主从架构(Master/Slave 架构)中:Master 负责写,并且将数据**复制**到其它的 Slave 节点,而 Slave 节点负责读,所有的**读请求全部访问 Slave 节点**。 + +Redis 主从架构可以很轻松实现**水平扩展**来支持高并发。 + + + +#### Redis 主从复制核心机制 + +核心机制如下: + +- Redis 采用**异步方式**复制数据到 Slave 节点。从 Redis2.8 开始,Slave 节点会周期性地确认自己每次复制的数据量; +- 一个 Master 节点可配置多个 Slave 节点,Slave 节点也可以连接其他的 Slave 节点; +- Slave 节点进行复制时,不会 block Master 节点的正常工作;也不会 block 查询操作,它会用旧的数据集来提供服务,但是当复制完成时,需要删除旧数据集,加载新数据集,此时会暂停对外服务; +- Slave 节点主要用来进行水平扩展,做读写分离,扩容的 Slave 节点可以提高读的吞吐量。 + +注意: + +- 如果采用主从架构,那么建议**必须开启 Master 节点的持久化**(详细信息看第七章)。不建议用 Slave 节点n作为 Master 节点的数据热备,因为如果关闭 Master 节点的持久化,可能在 Master 宕机重启的时候数据是空的,然后可能一经过复制, Slave 节点的数据也丢了。 +- 准备 Master 的各种备份方案。万一本地的所有文件丢失,从备份中挑选一份 RDB 去恢复 Master,这样可确保启动时,是有数据的。即使采用了高可用机制,Slave 节点可以自动接管 Master 节点,但也可能哨兵还没检测到 Master Failure,Master 节点就自动重启了,还是可能导致上面所有的 Slave 节点数据被清空。 + +#### Redis 主从复制核心原理 + +当启动一个 Slave 节点时,它会发送一个 `PSYNC` 命令给 Master 节点。 + +如果这是 Slave 节点初次连接到 Master 节点,那么会触发一次 `full resynchronization` **全量复制**。此时 Master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端新收到的所有写命令缓存在内存中。 + +`RDB` 文件生成完毕后, Master 会将这个 `RDB` 发送给 Slave,Slave 会**先写入本地磁盘,再从本地磁盘加载到内存中**,接着 Master 会将内存中缓存的写命令发送到 Slave,Slave 也会同步这些数据。Slave 节点如果跟 Master节点有网络故障,断开了连接,会自动重连,连接之后 Master 仅会复制给 Slave 部分缺少的数据。 + + + +> **断点续传** + +从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 + +Master 节点在内存中维护一个 backlog,Master 和 Slave 都会保存`replica offset`和 `master run id`,其中`replica offset` 就保存在 backlog 中。如果 Master 和 Slave 网络连接断掉了,Slave 会让 Master 从上次 的`replica offset` 开始继续复制,如果没有找到对应的 `replica offset`,那么就会执行一次 `PSYNC` 命令。 + +注意:如果根据 host+ip 定位 Master 节点是不可靠的。如果 Master 节点重启或者数据出现了变化,那么 Slave 节点应该根据不同的 `master run id` 区分。 + +> **无磁盘化复制** + +之前的版本中,一次完整的重同步需要在磁盘上创建一个 RDB ,然后从磁盘重新加载同一个 RDB 来服务 Slave。 + +Master 在内存中直接创建 RDB,然后直接发送给 Slave,不使用磁盘作中间存储。只需要在配置文件中开启 `repl-diskless-sync yes` 即可。 + +``` +repl-diskless-sync yes +# Master 在内存中直接创建 RDB,然后发送给 Slave,不会存入本地磁盘。 + +repl-diskless-sync-delay 5 +# 等待 5s 后再开始复制,因为要等更多 Slave 重新连接过来。 +``` + +> **过期键处理** + +Slave 不会过期键,只会等待 Master 过期键。如果 Master 过期了一个键,或者通过 LRU 淘汰了一个键,那么会模拟一条 del 命令发送给 Slave。 + +#### Redis 主从复制完整流程 + +Slave 节点启动时,会在自己本地保存 Master 节点的信息,包括 Master 节点的 host 和 ip,但是复制流程没开始。 + +Slave 节点内部有个定时任务,每秒检查是否有新的 Master 节点要连接和复制,如果发现,就跟 Master 节点建立 Socket 网络连接。 + +Slave 节点发送 ping 命令给 Master 节点,如果 Master 设置了 requirepass,那么 Slave 节点必须发送 masterauth 的口令过去进行认证。 + +Master 节点第一次执行**全量复制**,将所有数据发给 Slave 节点。 + +Master 节点持续将写命令,**异步复制**给 Slave 节点。 + + + +> **全量复制** + +- Master 启动全量复制,将所有数据发给 Slave 节点。 + +- Master 执行 BGSAVE,在本地生成一份 RDB 快照文件。 + +- Master 节点将 RDB 快照文件发送给 Slave 节点,如果 RDB 复制时间超过 60秒(repl-timeout),那么 Slave 节点就会认为复制失败。 + + (可以适当调大这个参数,对于千兆网卡的机器,一般每秒传输 100 MB,传输 6 G 文件可能超过 60s) + +- Master 节点在生成 RDB 时,会将所有新的写命令缓存在内存中,在 Slave 节点保存了 RDB 之后,再将新的写命令复制给 Slave 节点。 + +- 如果在复制期间,内存缓冲区持续消耗超过 64 MB,或者一次性超过 256 MB,那么停止复制,复制失败。 + +``` +client-output-buffer-limit slave 256MB 64MB 60 +``` + +- Slave 节点接收到 RDB 之后,清空旧数据,然后重新加载 RDB 到内存中,同时**基于旧的数据版本**对外提供服务。 +- 如果 Slave 节点开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。 + +> **增量复制** + +- 如果全量复制过程中,Master 和 Slave 网络连接断掉,那么 Slave 重新连接 Master 时,会触发增量复制。 +- Master 直接从自己的 backlog 中获取部分丢失的数据,发送给 Slave 节点,默认 backlog 大小是 1 MB。 +- Master 就是根据 Slave 发送的 `PSYNC` 命令中的 `replica offset`来从 backlog 中获取数据的。 + +> **异步复制** + +Master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 Slave 节点。 + +> **heartbeat** + +主从节点互相都会发送 heartbeat 信息。 + +Master 默认每隔 10 秒发送一次 heartbeat,Slave 节点每隔 1 秒发送一个 heartbeat。 + +### Redis 哨兵集群实现高可用 + +如果系统在 365 天内,有 **99.99%** 的时间,都是可以对外提供服务,那么就说系统是高可用的。 + +一个 Slave 挂掉了,是不会影响可用性的,还有其它的 Slave 在提供相同数据下的相同的对外的查询服务。但是,如果 Master 节点挂掉了,Slave 节点还有什么用呢,因为没有 Master 给它们复制数据,系统相当于不可用。 + +Redis 的高可用架构,叫做 failover 故障转移,也叫做**主备切换**。 + +Master 节点在故障时,自动检测,并且将某个 Slave 节点自动切换为 Master 节点的过程,叫做主备切换。这有些类似于现代国家中的"议会"机制。 + +#### 哨兵简介 + +Sentinel(哨兵)是 Redis 集群中非常重要的一个组件,其针对的是单个Redis应用,主要有以下功能: + +- 集群监控:负责监控 Redis Master 和 Slave 进程是否正常工作。 +- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 +- 故障转移:如果 Master 节点挂掉了,会自动转移到 Slave 节点上。 +- 配置中心:如果故障转移发生了,通知 client 客户端新的 Master 地址。 + +哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作: + +- 故障转移时,判断一个 Master 节点是否宕机了,需要大部分(majority)的哨兵都同意才行,涉及到了分布式选举的问题。 +- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。 + +#### 哨兵核心知识 + +核心知识: + +- 哨兵至少需要 3 个实例,来保证自己的健壮性。 +- 哨兵结合 Redis 主从架构,**不保证数据零丢失**,只能保证 Redis 集群的高可用。 + +注意:哨兵集群必须部署 2 个以上节点。假设哨兵集群仅仅部署了 2 个哨兵实例,则 quorum = 1。 + +``` ++----+ +----+ +| M1 |---------| R1 | +| S1 | | S2 | ++----+ +----+ +``` + +如果 Master 宕机, S1 和 S2 中只要有 1 个哨兵认为 Master 宕机了,就可以进行切换,同时 S1 和 S2 会选举出一个哨兵来执行**故障转移**,此时需要"大多数哨兵"都是运行的。下标是集群与"大多数"的对应关系: + +| 哨兵数 | majority | +| :----: | :------: | +| 2 | 2 | +| 3 | 2 | +| 4 | 2 | +| 5 | 3 | + +*表中第一行 哨兵数 = 2 ,majority = 2,说明该 2 哨兵集群中至少有 2 个哨兵是可用的,才能保证集群正常工作。* + +如果此时仅仅是 M1 进程宕机了,哨兵 S1 正常运行,那么可以进行故障转移。(哨兵数 = 2,majority = 2) + +如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。 + +经典的 3 节点哨兵集群如下(quorum = 2): + +``` + +----+ + | M1 | + | S1 | + +----+ + | ++----+ | +----+ +| R2 |----+----| R3 | +| S2 | | S3 | ++----+ +----+ +``` + +如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 Master 宕机了,然后选举出一个哨兵来执行故障转移,同时 3 个哨兵的 majority 是 2,允许执行故障转移。 + +#### 数据丢失问题 + +主备切换的过程,可能会导致数据丢失问题: + +- **异步复制导致的数据丢失** + + 因为 Master 到 Slave 的复制是异步的,所以可能有部分数据还没复制到 Slave,Master 就宕机了,这部分数据就会丢失。 + +- **脑裂导致的数据丢失** + + 脑裂指的是某个 Master 所在机器突然**脱离了正常的网络**,跟其他 Slave 机器不能连接,但是实际上 Master 还运行着。哨兵可能就会认为 Master 宕机了,然后开启选举,将其他 Slave 切换成了 Master,此时集群中就会有两个 Master ,也就是所谓的**脑裂**。 + + 虽然某个 Slave 被切换成了 Master,但是 **client 可能还没来得及切换到新的 Master,仍继续向旧 Master 写数据**,当旧 Master 再次恢复的时候,会被作为一个 Slave 挂到新的 Master 上去,自己的数据会清空,重新从新的 Master 复制数据。而新的 Master 并没有后来 client 写入的数据,这部分数据就丢失了。 + +#### 数据丢失问题解决方案 + +进行如下配置: + +```xml +min-slaves-to-write 1 +min-slaves-max-lag 10 +# 要求至少有 1 个 Slave,数据复制和同步的延迟不能超过 10 秒。 +# 如果所有的 Slave 数据复制和同步的延迟都超过了 10 秒钟,那么 Master 就不会再接收任何请求了。 +``` + +- **减少异步复制导致的数据丢失** + + 上述配置可确保:一旦 Slave 复制数据和 ACK 延时太长,就认为可能 Master 宕机后损失的数据太多,那么就拒绝写请求,这样可以把 Master 宕机时由于部分数据未同步到 Slave 导致的数据丢失降低的可控范围内。 + +- **减少脑裂导致的数据丢失** + + 出现脑裂情况,上面两个配置可确保:如果 Master 不能继续给指定数量的 Slave 发送数据,而且 Slave 超过 10 秒没有返回 ACK 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。 + + 脑裂有点类似于发生了["土木堡之变"](https://baike.baidu.com/item/%E5%9C%9F%E6%9C%A8%E4%B9%8B%E5%8F%98/837824?fromtitle=%E5%9C%9F%E6%9C%A8%E5%A0%A1%E4%B9%8B%E5%8F%98&fromid=1727167) + +#### 选举算法 + +> sdown 和 odown 转换机制 + +- sdown 是**主观(subjective)宕机**,如果一个哨兵觉得一个 Master 宕机,那么就是主观宕机。 + + sdown 达成的条件很简单,如果一个哨兵 ping 一个 Master,超过了 `is-master-down-after-milliseconds` 指定的毫秒数之后,就主观认为 Master 宕机。 + +- odown 是**客观(objective)宕机**,如果 quorum 数量的哨兵都觉得一个 Master 宕机,那么就是客观宕机。 + + 如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 Master 是 sdown 的,那么就认为是 odown 了。 + +> **quorum 和 majority** + +每次一个哨兵要做主备切换,首先需要 **quorum 数量的哨兵认为 odown**,然后从中选出一个哨兵来做切换,这个哨兵还需要得到 **majority 哨兵是可运行的**,才能正式执行切换。 + +- quorum < majority,majority 数量的哨兵是可运行的,就可执行切换。 +- quorum>= majority,必须 quorum 数量的哨兵是可运行的才能执行切换。 + +如果一个 Master 被认为 odown,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作。此时首先要选举一个 Slave 来,会考虑 Slave 的一些信息: + +- **跟 Master 断开连接的时长** + + 一个 Slave 如果跟 Master 断开连接的时间超过`(down-after-milliseconds * 10) + Master 宕机的时长` + + ,那么就被认为不适合选举为 Master。 + +- **Slave 优先级** + + slave priority 值越小,优先级就越高 + +- **`replica offset`** + + 如果 slave priority 相同,那么比较`replica offset` 。 + + Slave 复制的数据越多,`replica offset` 越靠后,优先级就越高。 + +- **`master run id`** + + 如果 slave priority 和 `replica offset` 都相同,那么选择一个`master run id`较小的 Slave。 + ## 参考资料 - [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html) +- [ 互联网 Java 工程师进阶知识完全扫盲](https://github.com/doocs/advanced-java) - [REDIS IN ACTION](https://redislabs.com/ebook/foreword/) - [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/) - [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) From 6fefae90360d23ca313ad811897a8f9d7c21d151 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月28日 09:39:45 +0800 Subject: [PATCH 13/29] =?UTF-8?q?=E3=80=90=E7=B3=BB=E7=BB=9F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E3=80=91=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + ...03345円274円217円344円272円213円345円212円241円.md" | 165 ++++++++++++++++-- docs/SystemDesign/03CAP.md | 42 ----- docs/SystemDesign/04BASE.md | 32 ---- ...210206円345円270円203円345円274円217円Session.md" | 6 +- ...6346円236円204円-347円233円256円345円275円225円.md" | 4 +- 6 files changed, 158 insertions(+), 92 deletions(-) delete mode 100644 docs/SystemDesign/03CAP.md delete mode 100644 docs/SystemDesign/04BASE.md rename "docs/SystemDesign/08351円233円206円347円276円244円344円270円213円347円232円204円 Session 347円256円241円347円220円206円.md" => "docs/SystemDesign/08345円210円206円345円270円203円345円274円217円Session.md" (94%) diff --git a/.gitignore b/.gitignore index 7be1202..0fd834d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ *.xml +.idea/vcs.xml diff --git "a/docs/SystemDesign/02345円210円206円345円270円203円345円274円217円344円272円213円345円212円241円.md" "b/docs/SystemDesign/02345円210円206円345円270円203円345円274円217円344円272円213円345円212円241円.md" index 92d2deb..3d3d432 100644 --- "a/docs/SystemDesign/02345円210円206円345円270円203円345円274円217円344円272円213円345円212円241円.md" +++ "b/docs/SystemDesign/02345円210円206円345円270円203円345円274円217円344円272円213円345円212円241円.md" @@ -1,28 +1,86 @@ * [二、分布式事务](#二分布式事务) - * [本地消息表](#本地消息表) + * [CAP](#CAP) + * [BASE](#BASE) * [2PC](#2pc) - + * [TCC](#TCC) + * [本地消息表](#本地消息表) + * [MQ事务消息](#MQ事务消息) + * [Sagas事务模型](#Sagas事务模型) + * # 二、分布式事务 -指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。 +指事务的操作位于**不同的节点**上,需要保证事务的 ACID 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 -例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 +这个时候单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫CAP定理,那么CAP定理指的是什么呢? -## 本地消息表 +## CAP -本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。 +分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 -1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 -2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 -3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 +
-
+### 一致性 + +一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 + +对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 + +### 可用性 + +可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 + +在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 + +### 分区容忍性 + +网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 + +在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。 + +### 权衡 + +在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在**可用性(P)和一致性(C)之间做权衡**。 + +可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时, + +- 为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性,zookeeper其实就是追求的强一致; +- 为了保证可用性(AP),放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。 + +
+ +## BASE + +在分布式系统中,我们往往追求的是**可用性**,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。 + +BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。 + +BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +
+ +### 基本可用 + +指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。 + +例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 + +### 软状态 + +指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。 + +### 最终一致性 + +最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。 + +ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。 + +在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 ## 2PC -两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。 +两阶段提交(Two-phase Commit,2PC),通过引入**协调者**(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。 ### 1. 运行过程 @@ -56,4 +114,87 @@ #### 2.4 太过保守 -任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 \ No newline at end of file +任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 + +## TCC + +TCC 即补偿事务,其实就是采用的**补偿机制**,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段: + +- Try 阶段主要是对业务系统做检测及资源预留; +- Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功; +- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。 + +举个例子,假入 Bob 要向 Smith 转账,思路大概是: +我们有一个本地方法,里面依次调用 + +1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。 +2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。 +3. 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。 + +**优点:** 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些 + +**缺点:** 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。 + +## 本地消息表 + +此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 + +本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。 + +1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 +2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 +3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 +4. 如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。 + +
+ +这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。 + +**优点:** 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。 + +**缺点:** 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。 + +## MQ事务消息 + +有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。 + +以阿里的 RocketMQ 中间件为例,其思路大致为: + +第一阶段Prepared消息,会拿到消息的地址。 +第二阶段执行本地事务。 + +第三阶段通过第一阶段拿到的地址去访问消息,消息接受者就能使用这个消息。 + +
+ +
+ +也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就**保证了消息发送与本地事务同时成功或同时失败**。 + +
+ +
+ +**优点:** 实现了最终一致性,不需要依赖本地数据库事务。 + +**缺点:** 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。 + +## Sagas 事务模型 + +Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。你可以在[这里](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf)看到 Sagas 相关论文。 + +我们这里说的是一种基于 Sagas 机制的工作流事务模型,这个模型的相关理论目前来说还是比较新的,以至于百度上几乎没有什么相关资料。 + +该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。 + +比如我们一次关于购买旅游套餐业务操作涉及到三个操作,他们分别是预定车辆,预定宾馆,预定机票,他们分别属于三个不同的远程接口。可能从我们程序的角度来说他们不属于一个事务,但是从业务角度来说是属于同一个事务的。当发生失败时,会依次进行取消的补偿操作。 + +
+ +
+ +# 参考资料 + +- [再有人问你分布式事务,把这篇扔给他]() +- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) + diff --git a/docs/SystemDesign/03CAP.md b/docs/SystemDesign/03CAP.md deleted file mode 100644 index 67ce192..0000000 --- a/docs/SystemDesign/03CAP.md +++ /dev/null @@ -1,42 +0,0 @@ - -* [三、CAP](#三cap) - * [一致性](#一致性) - * [可用性](#可用性) - * [分区容忍性](#分区容忍性) - * [权衡](#权衡) - - -# 三、CAP - -分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 - -
- -## 一致性 - -一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 - -对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 - -## 可用性 - -可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 - -在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 - -## 分区容忍性 - -网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 - -在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。 - -## 权衡 - -在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。 - -可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时, - -- 为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性; -- 为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。 - -
\ No newline at end of file diff --git a/docs/SystemDesign/04BASE.md b/docs/SystemDesign/04BASE.md deleted file mode 100644 index efe1252..0000000 --- a/docs/SystemDesign/04BASE.md +++ /dev/null @@ -1,32 +0,0 @@ - -* [四、BASE](#四base) - * [基本可用](#基本可用) - * [软状态](#软状态) - * [最终一致性](#最终一致性) - - -# 四、BASE - -BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。 - -BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 - -
- -## 基本可用 - -指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。 - -例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 - -## 软状态 - -指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。 - -## 最终一致性 - -最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。 - -ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。 - -在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 \ No newline at end of file diff --git "a/docs/SystemDesign/08351円233円206円347円276円244円344円270円213円347円232円204円 Session 347円256円241円347円220円206円.md" "b/docs/SystemDesign/08345円210円206円345円270円203円345円274円217円Session.md" similarity index 94% rename from "docs/SystemDesign/08351円233円206円347円276円244円344円270円213円347円232円204円 Session 347円256円241円347円220円206円.md" rename to "docs/SystemDesign/08345円210円206円345円270円203円345円274円217円Session.md" index e093384..992a491 100644 --- "a/docs/SystemDesign/08351円233円206円347円276円244円344円270円213円347円232円204円 Session 347円256円241円347円220円206円.md" +++ "b/docs/SystemDesign/08345円210円206円345円270円203円345円274円217円Session.md" @@ -9,7 +9,7 @@ 一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。 -## Sticky Session +## 粘性 Seesion(Sticky Session) 需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。 @@ -19,7 +19,7 @@
-## Session Replication +## Session 广播(Session Replication) 在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。 @@ -30,7 +30,7 @@
-## Session Server +## 缓存集中式管理(Session Server) 使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。 diff --git "a/docs/347円263円273円347円273円237円346円236円266円346円236円204円-347円233円256円345円275円225円.md" "b/docs/347円263円273円347円273円237円346円236円266円346円236円204円-347円233円256円345円275円225円.md" index cd5e6ac..47f740f 100644 --- "a/docs/347円263円273円347円273円237円346円236円266円346円236円204円-347円233円256円345円275円225円.md" +++ "b/docs/347円263円273円347円273円237円346円236円266円346円236円204円-347円233円256円345円275円225円.md" @@ -8,15 +8,13 @@ - [分布式锁](./SystemDesign/01分布式锁.md) - [分布式事务](./SystemDesign/02分布式事务.md) -- [CAP](./SystemDesign/03CAP.md) -- [BASE](./SystemDesign/04BASE.md) - [Paxos](./SystemDesign/05Paxos.md) - [Raft](./SystemDesign/06Raft.md) ### 集群 - [负载均衡](./SystemDesign/07负载均衡.md) -- [集群下的 Session 管理](./SystemDesign/08集群下的%%Session%%管理.md) +- [分布式 Session ](./SystemDesign/08分布式Session.md) ### 攻击技术 From e85b6d7f1b5c6ebbefd375a2ff3084a3f7af8f76 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月28日 09:40:02 +0800 Subject: [PATCH 14/29] Update Zookeeper.md --- docs/BigData/Zookeeper.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/BigData/Zookeeper.md b/docs/BigData/Zookeeper.md index 618b7bc..b05f72c 100644 --- a/docs/BigData/Zookeeper.md +++ b/docs/BigData/Zookeeper.md @@ -201,10 +201,15 @@ ZAB 协议两种基本的模式:崩溃恢复和消息广播。 # Zookeeper 的应用场景 -- 统一配置 -- 统一命名管理 -- 分布式锁 -- 集群管理 +## 分布式协调/通知 + +ZooKeeper中持有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发送变化,那么所有订阅者都能够收到相应的Watcher通知,并做出相应处理。 + +## 分布式锁 + +举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。 + +> 实现原理: ## 数据发布/订阅 @@ -214,15 +219,13 @@ ZAB 协议两种基本的模式:崩溃恢复和消息广播。 由于系统的配置信息通常具有数据量较小、数据内容在运行时会动态变化、集群中各机器共享等特性,这样特别适合使用ZooKeeper来进行统一配置管理。 -## 统一命名服务 - -ZooKeeper提供了一套分布式全局唯一ID的分配机制,所谓ID,就是一个能够唯一标识某个对象的标识符。 +## 元数据/配置信息管理 -## 分布式协调/通知 - -ZooKeeper中持有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发送变化,那么所有订阅者都能够收到相应的Watcher通知,并做出相应处理。 +zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么? +## HA高可用 +这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。 > 补充资料: From 801d06791bf49667e3f3097b80d04874fab838a7 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月28日 09:40:09 +0800 Subject: [PATCH 15/29] Update Redis.md --- docs/DataBase/Redis.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index 02c9f54..7d4eefe 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -339,7 +339,9 @@ List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 -可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 +- 单节点:可以使用 Redis 自带的 SETNX 或SET命令实现分布式锁。 + +- 多节点:[RedLock模型](https://www.jianshu.com/p/fba7dd6dcef5),具有安全性、避免死锁和容错性的特性。 ### 其它 From 37a5886ad42d8dd6f9c7beaa82cc6f8cd2addea4 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月28日 13:32:58 +0800 Subject: [PATCH 16/29] Java-Notes --- ...03347円273円223円346円236円204円345円236円213円.md" | 38 +- .../00Spring346円246円202円350円277円260円.md" | 93 ++- docs/Spring/01SpringIOC.md | 669 +++++++++++++-- docs/Spring/Spring AOP.md | 776 ++++++++++++++++++ ...37345円221円275円345円221円250円346円234円237円.md" | 545 ++++++++++++ ...76350円256円241円346円250円241円345円274円217円.md" | 29 + docs/Spring/SpringIOC.md | 463 +++++++++++ 7 files changed, 2495 insertions(+), 118 deletions(-) create mode 100644 docs/Spring/Spring AOP.md create mode 100644 "docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" create mode 100644 "docs/Spring/Spring 344円270円255円346円266円211円345円217円212円345円210円260円347円232円204円350円256円276円350円256円241円346円250円241円345円274円217円.md" create mode 100644 docs/Spring/SpringIOC.md diff --git "a/docs/OO/03347円273円223円346円236円204円345円236円213円.md" "b/docs/OO/03347円273円223円346円236円204円345円236円213円.md" index 58daedd..cbf2d0d 100644 --- "a/docs/OO/03347円273円223円346円236円204円345円236円213円.md" +++ "b/docs/OO/03347円273円223円346円236円204円345円236円213円.md" @@ -30,26 +30,20 @@ Adapter:适配器,把Adaptee适配成为Client需要的Target。
### Implementation1 -美国的电饭煲是在电压为110V下工作,而中国的电饭煲在电压220V下工作。 - -要求将在中国使用的电饭煲适配成能在美国使用,从而使得电饭煲能够跨国销售! +美国的电饭煲是在电压为 110V 下工作,而中国的电饭煲在电压 220V 下工作。要求将在美国使用的电饭煲适配成能在中国使用。 ```java /** - * Adaptee: - * 已经存在的接口,通常能满足客户端的功能要求, + * Adaptee:已经存在的接口,通常能满足客户端的功能要求。 * 但是接口与客户端要求的特定领域接口不一致,需要被适配。 */ public interface CHINA220 { //220 V电压接口 - public void connect(); + void connect(); } ``` ```java -/** - * Adaptee的具体实现类 - */ public class CHINA220Impl implements CHINA220{ @Override public void connect() { @@ -64,7 +58,7 @@ public class CHINA220Impl implements CHINA220{ */ public interface USA110 { //110 V电压接口 - public void connect(); + void connect(); } ``` @@ -92,10 +86,10 @@ public class PowerAdapter implements USA110{ /** * 该电器在110V下工作 */ -public class USAElectricCooker { +public class USACooker { private USA110 usa110; - public USAElectricCooker(USA110 usa110) { + public USACooker(USA110 usa110) { this.usa110 = usa110; } @@ -107,20 +101,24 @@ public class USAElectricCooker { ``` ```java -/** - * 想要110V,但是只有220V,就用220V "装" 110V - */ public class Client { public static void main(String[] args) { - CHINA220 china220=new CHINA220Impl(); - PowerAdapter usa110=new PowerAdapter(china220); - USAElectricCooker cooker=new USAElectricCooker(usa110); - cooker.cook(); + // 110 V 电压下直接 cook + USA110 usa110 = new USA110Impl(); + USACooker usaCooker = new USACooker(usa110); + usaCooker.cook(); + + // 220 V 电压下需要适配成 110V,才可 cook + CHINA220 china220 = new CHINA220Impl(); + PowerAdapter adapter = new PowerAdapter(china220); + USACooker usaCooker2 = new USACooker(adapter); + usaCooker2.cook(); } } ``` -输出结果: ```html +110V 接通电源,开始工作... +开始煮饭... 220V 接通电源,开始工作... 开始煮饭... ``` diff --git "a/docs/Spring/00Spring346円246円202円350円277円260円.md" "b/docs/Spring/00Spring346円246円202円350円277円260円.md" index d3c4201..8ec5548 100644 --- "a/docs/Spring/00Spring346円246円202円350円277円260円.md" +++ "b/docs/Spring/00Spring346円246円202円350円277円260円.md" @@ -1,55 +1,72 @@ - -* [Spring概述](#Spring概述) - * [Spring框架](#Spring框架) - * [Spring的核心](#Spring的核心) - * [Spring优点](#Spring优点) - * [Spring入门程序](#Spring入门程序) - -# Spring概述 +# 一、Spring 概述 -## Spring框架 -Spring是**分层**的JavaSE/EE full-stack(**一站式**) 轻量级开源框架 +## Spring 框架 +Spring 是**分层**的 JavaSE/EE **一站式** 轻量级开源框架。 -> 分层: +- **分层** -SUN提供的EE的三层结构:web层、业务层、数据访问层(持久层,集成层) + SUN 提供的 EE 的三层结构:Web 层、业务层、数据访问层。 -> 一站式: +- **一站式** -Spring框架有对三层的每层解决方案: + Spring 框架对三层结构的每层都有解决方案: -- web层:Spring MVC -- 持久层:JDBC Template -- 业务层:Spring的Bean管理 + * Web 层:[SpringMVC](https://duhouan.github.io/Java-Notes/#/./Spring/06SpringMVC) + * 业务层:SpringBean 管理 + * 业务层:JDBCTemplate +Spring 框架的几个模块: -## Spring的核心 -> IOC + +组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下: -控制反转(Inverse of Control):将对象的创建权,交由Spring完成。 +- **核心容器(Spring Core)**:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 `BeanFactory`,它是工厂模式的实现。`BeanFactory` 使用*控制反转* (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。 +- **Spring 上下文**:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。 +- **Spring AOP**:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。 +- **Spring DAO**:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。 +- **Spring ORM**:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。 +- **Spring Web 模块**:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。 +- **Spring MVC 框架**:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。 -> AOP +Spring 框架优点: -面向切面编程(Aspect Oriented Programming):**面向对象的功能延伸**。不是替换面向对象,是用来解决OO中一些问题. +- **方便解耦,简化开发** + Spring 就是一个大工厂,可以将所有对象创建和依赖关系维护,交给 Spring 管理 -## Spring优点 -- **方便解耦,简化开发**; Spring就是一个大工厂,可以将所有对象创建和依赖关系维护,交给Spring管理 +- **AOP 编程的支持** -- **AOP编程的支持**; Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能 + Spring 提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能 -- **声明式事务的支持**:只需要通过配置就可以完成对事务的管理,而无需手动编程 +- **声明式事务的支持** -- **方便程序的测试**:Spring对Junit4支持,可以通过注解方便的测试Spring程序 + 只需要通过配置就可以完成对事务的管理,而无需手动编程 -- **方便集成各种优秀框架**:Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架 -(如:Struts、Hibernate、MyBatis、Quartz等)的直接支持 +- **方便程序的测试** -- **降低JavaEE API的使用难度**:Spring 对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等), -都提供了封装,使这些API应用难度大大降低 + Spring 对 Junit4 支持,可以通过注解方便的测试 Spring 程序 + +- **方便集成各种优秀框架** + + Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz等)的直接支持 + +- **降低 JavaEE API 的使用难度** + + Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低 + +## Spring 核心 + +- **IOC** + + 控制反转(Inversion of Control,IoC):将对象的创建,交由 Spring 完成。 + +- **AOP** + + 面向切面变成(Aspect Oriented Programming,AOP):是面向对象思想的延伸,不是替换面向对象思想,是用来解决使用面向对象中出现的一些问题的。 + +## Spring 入门程序 +- 创建 Spring 的配置文件 -## Spring入门程序 -### 1.创建Spring的配置文件 在src下创建一个applicationContext.xml。 引入XML的约束: 找到xsd-config.html.引入beans约束: @@ -64,13 +81,14 @@ http://www.springframework.org/schema/beans http://www.springframework.org/schem ``` -### 2.在配置文件中配置类 +- 在配置文件中配置类 ```html ``` -### 3.测试 +- 测试 + ```java public interface UserService { void say(); @@ -129,8 +147,3 @@ public class SpringTest { } } ``` - -### IOC(控制反转)和DI(依赖注入)区别 -IOC(控制反转):对象的创建权,由Spring管理。 - -DI(依赖注入):在Spring创建对象的过程中,把**对象依赖的属性**注入到类中。 \ No newline at end of file diff --git a/docs/Spring/01SpringIOC.md b/docs/Spring/01SpringIOC.md index b489fcc..50afe18 100644 --- a/docs/Spring/01SpringIOC.md +++ b/docs/Spring/01SpringIOC.md @@ -1,14 +1,612 @@ - -* [SpringIOC](#SpringIOC) - * [IOC装配Bean](#IOC装配Bean) - * [IOC使用注解方式装配Bean](#IOC使用注解方式装配Bean) - * [SpringIOC原理](#SpringIOC原理) - * [SpringIOC源码分析](#SpringIOC源码分析) - +# 二、Spring IOC + +## IoC 与 DI + +### IoC 概念 + +控制反转(Iversion of Control,IoC)是一种**设计思想**。**将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理**。涉及到 2 个方面: + +- **控制** + + 控制对象的创建及销毁(生命周期)。 + +- **反转** + + 将对象的控制权交给 IoC 容器。 + +所有的类都会在 Spring 容器中注册,告诉 Spring 你是个什么东西,你需要什么东西,然后 Spring 会在系统运行到适当的时候,把你需要的东西主动给你 + +**所有类的创建、销毁都由 Spring 来控制,也就是说控制对象生命周期的不是引用它的对象,而是 Spring**。对于某个具体对象而言,以前是它控制其他对象,现在所有对象都被 Spring 控制。 + +### DI 概念 + +依赖注入(Dependency Injection,DI) 。依赖注入举例:设计行李箱。 + + + +相应的代码如下: + + + +size 固定值,改进后的代码: + + + +使用 DI 方式进行改进: + + + +改进后相应的代码如下: + + + +不难理解,依赖注入就是**将底层类作为参数传递给上层类,实现上层对下层的控制**,**依赖注入实现控制反转**。 + +### IoC 和 DI 关系 + +IoC 是在系统运行中,**动态的向某个对象提供它所需要的其他对象**,通过 DI 来实现。 + +- IoC 和 DI 的关系如下图: + + + +- 依赖倒置原则、IoC、DI 和 IoC 容器的关系: + + + +## IoC 容器 + +IoC 容器功能: + +- 管理 Bean 的生命周期 +- 控制 Bean 依赖注入 + +使用 IoC 容器的好处: + +- 避免在各处使用 new 来创建类,并且可以做到统一维护 + +- 创建实例的时候不需要了解其中的细节 + + 依赖注入方式获取实例: + + + + 使用 IoC 容器获取获取实例(创建实例的时候不需要了解其中的细节): + + + + + +## XML 配置文件解析 + +### BeanDefinition + +BeanDefinition 是一个接口。在 Spring 中存在 3 种实现: + +- RootBeanDefinition +- ChildBeanDefinition +- GenericBeanDefinition + +三种实现都继承了 AbstractBeanDefinition。**AbstractBeanDefinition 是对上述的共同的类信息进行抽象**。 + +```java +public abstract class AbstractBeanDefinition extends + BeanMetadataAttributeAccessor implements BeanDefinition, Cloneable { + //... +} +``` + +**BeanDefinition 是配置文件中 ``元素标签在容器中的内部表现形式**。 + +在配置文件中,可以定义父`` 和子``,父`` 用 RootBeanDefinition 表示,而子``用ChildBeanDefinition 表示,而没有父`` 的 `` 就使用 RootBeanDefinition 表示。 + +### BeanDefinitionRegistry + +Spring 通过 BeanDefinition 将配置文件中的``配置信息转换为容器的内部表示,并且将这些 BeanDefinition 注册 **BeanDefinitionRegistry** 中。 + +Spring 容器的 BeanDefinitionRegistry 就是配置信息的 内存数据库,主要是以 map 的形式保存数据,后续操作直接从 BeanDefinitionRegistry 中读取配置信息。 + +### Resource + +Spring 采用 Resource 来对各种资源进行统一抽象。 + +Resource 是一个接口,定义了资源的基本操作,包括是否存在、是否可读、是否已经打开等等。 + +Resource 继承了 InputStreamSource。 + +```java +public interface Resource extends InputStreamSource { + //... +} +``` + +InputStreamSource 封装任何可以返回 InputStream 的类。 + +```java +/* +* InputStreamSource 封装任何可以返回 InputStream 的类。 +*/ +public interface InputStreamSource { + /* + * 用于返回一个新的 InputStream 对象 + */ + InputStream getInputStream() throws IOException; +} +``` + +对于不同来源的资源文件都有相应的 Resource 实现: + +| 资源来源 | Resource 实现类 | +| :--------------: | :-----------------: | +| 文件 | FileSystemResource | +| classpath 资源 | ClassPathResource | +| URL 资源 | UrlResource | +| InputStream 资源 | InputStreamResource | +| Byte 数组 | ByteArrayResource | + +### 解析过程 + +分为 2 大步骤: + +**1、配置文件的封装** + +Resource 接口**抽象了所有 Spring 内部使用到的底层资源**:File、URL、ClassPath 等。 + +**2、加载 Bean** + +- 封装资源文件。XMLBeanDefinitionReader 对参数 Resource 使用 EncodeResource 类进行封装。 +- 获取输入流。从 Resource 中获取对应的 InputStream 并构造 InputSource。 +- 通过构造的 InputSource 实例和 Resource 实例继续调用函数 `doLoadBeanDefinition`。 + * 获取 XML 文件的实体解析器和验证模式(常见的验证模式有 DTD 和 XSD 两种) + * 加载 XML 文件,并得到对应的 Document + * 根据返回的 Document 解析并注册 BeanDefinition + +## Spring 的 IoC 容器 + + + +IoC 容器其实就是一个大工厂,它用来管理我们所有的对象以及依赖关系: + +- 根据 Bean 配置信息在容器内部创建 Bean 定义注册表 +- 根据注册表加载,实例化 Bean,**建立 Bean 与 Bean 之间的依赖关系** +- 将 Bean 实例放入 Spring IoC 容器中,等待应用程序调用 + +### Spring IoC 支持的功能 + +- 依赖注入 +- 依赖检查 +- 自动装配属性 +- 可指定初始化方法和销毁方法 + +### Spring 的 2 种 IoC 容器 + +- **BeanFactory** + - IoC 容器要实现的最基础的接口 + - 采用**延迟初始化策略**(容器初始化完成后并不会创建 Bean 对象,只有当收到初始化请求时才进行初始化) + - 由于是延迟初始化策略,因此启动速度较快,占用资源较少 + +- **ApplicationConext** + - 在 BeanFactory 基础上,增加了更为高级的特性:事件发布、国际化等。 + - 在容器启动时,完成所有 Bean 的创建 + - 启动时间较长,占用资源较多 + +### IoC 容器的初始化过程 + +Spring IoC 容器的初始化过程分为 3 个阶段:Resource 定位、BeanDefinition 的载入和向 IoC 容器注册 BeanDefinition。Spring 将这 3 个阶段分离,并使用不同的模块来完成,这样可以让用户更加灵活的对这 3 个阶段进行扩展。 + + + +1、**Resource 定位** + +Resource 定位指的是 BeanDefinition 的资源定位,它由 ResourceLoader 通过统一的 Resource 接口来完成,Resource 为各种形式的 BeanDefinition 的使用都提供了统一的接口。 + +2、**BeanDefinition 的载入** + +BeanDefinition 实际上就是 POJO 对象在 IoC 容器中的抽象,通过 BeanDefiniton,IoC 容器可以方便对 POJO 对象进行管理。 + +3、 **向 IoC 容器注册 BeanDefinition** + +向 IoC 容器注册 BeanDefinition 是通过调用 BeanDefinitionRegistry 接口来的实现来完成的,这个注册过程是把载入的 BeanDefinition 向 IoC 容器进行注册。实际上 IoC 容器内部维护这一个 HashMap,而这个注册过程其实就是将 BeanDefinition 添加至该 HashMap 中。 + +> 注意:BeanFactory 和 FactoryBean 的区别 + +BeanFactory 是 IoC 最基本的容器,负责生产和管理 Bean,为其他具体的 IoC 容器提供了最基本的规范。 + +FactoryBean 是一个 Bean,是一个接口,当 IoC 容器中的 Bean 实现了 FactoryBean 后, + +通过 getBean(String beanName) 获取到的 Bean 对象并不是 FactoryBean 的实现类对象,而是这个实现类中的 getObject() 方法返回的对象。 + +要想获取 FactoryBean 的实现类对象,就是在 beanName 前面加上 "&"。 + +## Spring Bean 加载流程 + +AbstractBeanFactory 中 `getBean`最终调用 `doGetBean`方法。 + +```java +protected T doGetBean(String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { + String beanName = this.transformedBeanName(name); + Object sharedInstance = this.getSingleton(beanName); + Object bean; + if (sharedInstance != null && args == null) { + if (this.logger.isTraceEnabled()) { + if (this.isSingletonCurrentlyInCreation(beanName)) { + this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); + } else { + this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); + } + } + + bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null); + } else { + if (this.isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + BeanFactory parentBeanFactory = this.getParentBeanFactory(); + if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) { + String nameToLookup = this.originalBeanName(name); + if (parentBeanFactory instanceof AbstractBeanFactory) { + return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly); + } + + if (args != null) { + return parentBeanFactory.getBean(nameToLookup, args); + } + + if (requiredType != null) { + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + + return parentBeanFactory.getBean(nameToLookup); + } + + if (!typeCheckOnly) { + this.markBeanAsCreated(beanName); + } + + try { + RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName); + this.checkMergedBeanDefinition(mbd, beanName, args); + String[] dependsOn = mbd.getDependsOn(); + String[] var11; + if (dependsOn != null) { + var11 = dependsOn; + int var12 = dependsOn.length; + + for(int var13 = 0; var13 < var12; ++var13) { + String dep = var11[var13]; + if (this.isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + + this.registerDependentBean(dep, beanName); + + try { + this.getBean(dep); + } catch (NoSuchBeanDefinitionException var24) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", var24); + } + } + } + + if (mbd.isSingleton()) { + sharedInstance = this.getSingleton(beanName, () -> { + try { + return this.createBean(beanName, mbd, args); + } catch (BeansException var5) { + this.destroySingleton(beanName); + throw var5; + } + }); + bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } else if (mbd.isPrototype()) { + var11 = null; + + Object prototypeInstance; + try { + this.beforePrototypeCreation(beanName); + prototypeInstance = this.createBean(beanName, mbd, args); + } finally { + this.afterPrototypeCreation(beanName); + } + + bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } else { + String scopeName = mbd.getScope(); + Scope scope = (Scope)this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + + try { + Object scopedInstance = scope.get(beanName, () -> { + this.beforePrototypeCreation(beanName); + + Object var4; + try { + var4 = this.createBean(beanName, mbd, args); + } finally { + this.afterPrototypeCreation(beanName); + } + + return var4; + }); + bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } catch (IllegalStateException var23) { + throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", var23); + } + } + } catch (BeansException var26) { + this.cleanupAfterBeanCreationFailure(beanName); + throw var26; + } + } + + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = this.getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } else { + return convertedBean; + } + } catch (TypeMismatchException var25) { + if (this.logger.isTraceEnabled()) { + this.logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", var25); + } + + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } else { + return bean; + } +} +``` + +doGetBean 大致流程如下: + +**1、获取参数 name 对应的真正 beanName** + +**2、尝试从缓存中加载单例** + +单例在 Spring 的同一个容器中只会被创建一次,后续再获取 Bean,就直接从**单例缓存**中获取。 + +这里只是尝试加载,首先尝试从**缓存**中加载,如果加载不成功则再次尝试从 **singletonFactories** 中加载。 + +在创建依赖的时候为了避免循环依赖,在 Spring 中创建 Bean 的原则:**不等 bean 创建完成就会将创建 Bean 的 ObjectFactory 提早曝光加入到缓存中,一旦下一个 Bean 创建时需要依赖上一个 Bean 则直接使用 ObjectFactory**。 + +**3、Bean 的实例化** + +如果从缓存中得到了 Bean 的原始状态,则需要对 Bean 进行实例化。这里有必要强调一下,缓存中记录的是原始的 Bean 状态,并一定是我们最终想要的 Bean。 + +**4、原型模式的依赖检查** + +只有在**单例情况下**才会尝试解决循环依赖。 + +如果存在 A 中有 B 的属性,B 中有 A 的属性,那么当依赖注入的时 候,就会产生当 A 还未创建完的时候因为对于 B 的创建再次返回创建 A ,造成循环依赖,即 isPrototypeCurrentlyInCreation(String beanName) 判断 true。 + +**5、检测 parentBeanFactory** + +如果缓存没有数据的话,直接转到 parentBeanFactory 去加载,条件是: + +```java +parentBeanFactory != null && !this.containsBeanDefinition(beanName) +``` + +containsBeanDefinition 方法是检测如果当前 XML 中没有配置对应的 beanName,只能到 parentBeanfactory 中尝试一下。 + +**6、将 GenericBeanDefinition 对象转换为 RootBeanDefiniion 对象** + +从 XML 配置文件中读取 Bean 信息会放到 GenericBeanDefinition 中, 但是所有的 Bean 后续处理都是针对 RootBeanDefinition 的。 + +这里做一下转换,转换的同时如果父类 Bean 不为空的话,则会进一步合并父类的属性。 + +**7、初始化依赖的 Bean** + +Spring 在初始化某一个 Bean 的时候首先初始化这个 Bean 的依赖。 + +**8、依据当前 bean 的作用域对 bean 进行实例化** + +Spring 中可以指定各种 scope,默认是 singleton 。 + +还有其他的配置比如 prototype、request 等,Spring 会根据不同的配置进行不同的初始化策略。 + +**9、类型转换** + +将返回的 Bean 转化为 requiredType 所指定的类型。 + +**10、返回 Bean** + +## Spring 解决循环依赖 + +循环依赖就是循环引用,就是两个或者多个 Bean 之间相互持有对方,比如 CircleA 引用 CircleB,CircleB 引用 CircleC,CircleC 引用 CircleA,则它们最终反映为一个环。 + +### 构造器循环依赖 + +表示通过构造器注入构成的循环依赖。此依赖是无法解决的,只能抛出 BeanConcurrentlyCreationException 异常表示循环依赖。 + +### setter 循环依赖 + +表示通过 setter 注入构成的循环依赖。 解决该依赖问题是通过 Spring 容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter 注入) 的 Bean 来完成的,而且只能解决单例作用域的 Bean 的循环依赖。 + +对于 `scope="prototype"`的 Bean,Spring 容器无法完成依赖注入,因为 "prototype" 作用域 Bean,Spring 容器不进行缓存,因此无法提前暴露一个创建中的 Bean。对于 "singleton" 作用域 Bean,可以通过setAllowCircleReferences(false) 来禁用循环引用。 + +对于单例对象来说,在 Spring 整个容器的生命周期内,有且只有一个对象,很容易想到这个对象应该存在于**缓存**中,Spring 在解决循环依赖问题的过程中就使用了"三级缓存": + +- **singletonObjects ** + + 用于保存 beanName 和创建 Bean 实例之间的关系,缓存的是 **Bean 实例**。 + +- **singletonFactories** + + 用于保存 beanName 和创建 Bean 的工厂之间的关系,缓存的是为解决循环依赖而准备的 **ObjectFactory**。 + +- **earlySingletonObjects** + + 用于保存 beanName 和创建 Bean 实例之间的关系,与 singletonObjects 的不同之处在于,当一个单例 Bean 被放到这里面后,当 Bean 还在创建过程中,就可以通过 getBean 方法获得,其目的是检查循环引用。这边缓存的也是实例,只是这边的是为解决循环依赖而提早暴露出来的实例,其实就是 ObjectFactory。 + +首先尝试从 singletonObjects 中获取实例,如果获取不到,再从 earlySingletonObjects 中获取,如果还获取不到,再尝试从 singletonFactories 中获取 beanName 对应的 ObjectFactory,然后调用 getObject 来创建 Bean,并放到 earlySingletonObjects 中去,并且从 singletonFactories 中 remove 掉这个 ObjectFactory。 + +> 注意:singleObjects 和 earlySingletonObjects + +- 两者都是以 beanName 为key,Bean 实例为 value 进行存储 + +- 两者得出区别在于 singletonObjects 存储的是实例化的完成的 Bean 实例,而 earlySingletonObjects 存储的是正在实例化中的 Bean,所以两个集合的内容是互斥的。 + + + +# Spring 框架中涉及到的设计模式 + +- [工厂模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_2-简单工厂(simple-factory)) + + BeanFactory 用来创建各种不同的 Bean。 + +- [单例模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_1-单例(singleton)) + + 在 Spring 配置文件中定义的 Bean 默认为单利模式。 + +- [模板方法模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_10-模板方法(template-method)) + + 用来解决代码重复的问题。比如 JdbcTempolate。 + +- [代理模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_7-代理(proxy)) + + AOP、事务都大量运用了代理模式。 + +- [原型模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_6-原型模式(prototype)) + + 特点在于通过"复制"一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的"原型",这个原型是可定制的。 + +- [责任链模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_1-责任链(chain-of-responsibility)) + + 在 SpringMVC 中,会经常使用一些拦截器(HandlerInterceptor),当存在多个拦截器的时候,所有的拦截器就构成了一条拦截器链。 + +- [观察者模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_7-观察者(observer)) + + Spring 中提供了一种监听机制,即 ApplicationListenber,可以实现 Spring 容器内的事件监听。 + + + + + +# //////////////////// + + + +## Spring IoC + +### Spring IoC 支持的功能 + +- + +### Spring IoC 的核心接口 + +Spring 中核心的接口或者类: + +- **BeanDefinition** + + **用来描述 Bean 的定义**。将 xml 文件或者注解中的 Bean 定义解析成 BeanDefintion。 + +- **BeanDefiniitonRegistry** + + **提供向 IoC 容器注册 BeanDefinition 对象的方法。**以` `键值对形式存储到 beanDefinitionMap 中,同时将 beanName 存入到 beanDefinionNames 中,方便后续 Bean 的实例化。 + +Spring IoC 的核心接口: + +- **BeanFactory** +- **ApplicationContext** + +### Spring IoC 容器的原理 + +#### BeanFactory + +BeanFactory 中: + +- 提供 IoC 控制机制 +- 包含 Bean 的各种定义,便于实例化 Bean +- 建立 Bean 之间的依赖关系 +- Bean 生命周期的控制 + +BeanFactory 类继承体系: + + + +BeanFactory 的生命周期: + + + +#### ApplicationContext + +ApplicationContext 类继承体系: + + + +ApplicationContext 的生命周期: + + + +初始化的过程都是比较长,我们可以**分类**来对其进行解析: + +- **Bean 自身的方法** + + 如调用 Bean 的构造函数实例化 Bean,调用 Setter 设置 Bean 的属性值以及通过 ``的 init-method 和 destroy-method 所指定的方法; + +- **Bean 级生命周期接口方法** + + 如 BeanNameAware、 BeanFactoryAware、 InitializingBean 和 DisposableBean,这些接口方法由 Bean 类直接实现; + +- **容器级生命周期接口方法** + + 在上图中带"★" 的步骤是由 InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为" **后处理器**" 。 **后处理器接口一般不由 Bean 本身实现,它们独立于 Bean,实现类以容器附加装置的形式注册到 Spring 容器中并通过接口反射为 Spring 容器预先识别**。当 Spring 容器创建任何 Bean 的时候,这些后处理器都会发生作用,所以这些后处理器的影响是全局性的。当然,用户可以通过合理地编写后处理器,让其仅对感兴趣 Bean 进行加工处理。 + +#### ApplicationContext 和 BeanFactory + +- ApplicationContext 会利用 Java 反射机制**自动识别出配置文件中定义的 BeanPostProcessor、 InstantiationAwareBeanPostProcesso 和 BeanFactoryPostProcessor 后置器**,自动将它们注册到应用上下文中。 + + BeanFactory 需要在代码中通过**手工调用`addBeanPostProcessor()`方法进行注册**。 + +- ApplicationContext 在**初始化**应用上下文的时候**就实例化所有单实例的 Bean**。 + + BeanFactory 在初始化容器的时候并未实例化 Bean,**直到**第一次访问某个 Bean 时**才**实例化目标 Bean。 + +#### Bean 的初始化过程 + + + +简要总结: + +- BeanDefinitionReader **读取 Resource 所指向的配置文件资源**,然后解析配置文件。配置文件中每一个`` 解析成一个 **BeanDefinition 对象**,并**保存**到 BeanDefinitionRegistry 中; + +- 容器扫描 BeanDefinitionRegistry 中的 BeanDefinition; + + 调用 InstantiationStrategy **进行 Bean 实例化的工作**; + + 使用 **BeanWrapper 完成 Bean 属性的设置**工作; + +- 单例 Bean 缓存池:Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的**缓存器**,它是一个用 HashMap 实现的缓存器,单实例的 Bean **以 beanName 为键保存在这个 HashMap** 中。 + + + +> 实践:通过注解方式实现 Bean 装载到 IOC 容器中 + +```java + +``` + + + + + + + + + +# ===================== + -# SpringIOC ## IOC装配Bean + ### Spring框架Bean实例化的方式 提供了三种方式实例化Bean: @@ -497,55 +1095,10 @@ IoC的一个重点是在系统运行中,**动态的向某个对象提供它所 注射到A当中,这样就完成了对各个对象之间关系的控制。 A需要依赖 Connection才能正常运行,而这个Connection是由Spring注入到A中的,依赖注入的名字就这么来的。 -
- -
- - - -那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection), -它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性, -**Spring就是通过[反射]()来实现注入的**。 - -## IOC容器的优势 - -- 避免在各处使用new来创建类,这个容器可以自动对代码进行初始化,并且可以做到统一维护。 -- 创建实例的时候不需要了解其中的细节,这个容器相当于一个工厂。 - -
- -
- - - -## BeanFactory与ApplicationContext的比较 - -- BeanFactory 是 Spring 框架的基础设施,面向Spring -- ApplicationContext 面向使用 Spring 框架的开发者 - -> 补充参考资料: - -## SpringIOC源码分析 - -### 初始化 -Spring IOC的初始化过程,整个脉络很庞大,初始化的过程主要就是**读取XML资源**,并**解析**, -最终**注册到Bean Factory中**。 - - - -### 注入依赖 -当完成初始化IOC容器后,如果Bean没有设置lazy-init(延迟加载)属性,那么Bean的实例就会在初始化IOC完成之后,及时地进行**初始化**。 -初始化时会**创建实例**,然后根据配置利用反射对实例进行进一步操作,具体流程如下所示: - - - -### getBean方法的代码逻辑 +# 参考资料 -- 转换beanName -- 从缓存中加载实例 -- 实例化Bean -- 检测partentBeanFactory -- 初始化依赖的Bean -- 创建Bean +- [Spring 框架小白的蜕变](https://www.imooc.com/learn/1108) -### [Spring核心源码学习](https://yikun.github.io/2015/05/29/Spring-IOC%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/) \ No newline at end of file +- [Spring IOC知识点一网打尽!](https://segmentfault.com/a/1190000014979704) +- [Spring核心思想,IoC与DI详解(如果还不明白,放弃java吧)](https://blog.csdn.net/Baple/article/details/53667767) +- [15个经典的Spring面试常见问题](https://mp.weixin.qq.com/s/uVoeXRLNEMK8c00u3tm9KA) \ No newline at end of file diff --git a/docs/Spring/Spring AOP.md b/docs/Spring/Spring AOP.md new file mode 100644 index 0000000..05a5326 --- /dev/null +++ b/docs/Spring/Spring AOP.md @@ -0,0 +1,776 @@ + +* [SpringAOP](#SpringAOP) + * [SpringAOP概述](#SpringAOP概述) + * [AOP的底层实现](#AOP的底层实现) + * [Spring中的AOP](#Spring中的AOP) + * [Spring的AspectJ的AOP](#Spring的AspectJ的AOP) + + +# SpringAOP + +## SpringAOP概述 +### 什么是AOP +AOP(面向切面编程,Aspect Oriented Programing)。 + +AOP采取**横向抽取**机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存) + +Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类**织入**增强代码 + +AspecJ是一个基于Java语言的AOP框架,Spring2.0开始,Spring AOP引入对AspectJ的支持, +AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。 + +AOP底层原理就是**代理机制**。 + +> 关注点分离:不同的问题交给不同部分去解决 + +### Spring的AOP代理 +- JDK动态代理:对实现了接口的类生成代理 +- CGLib代理机制:对类生成代理 + +### AOP的术语 + +| 术语 | 描述 | +| :--: | :--: | +| Joinpoint(连接点) | 所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是**方法**,因为Spring只支持**方法类型的连接点**。 | +| Pointcut(切入点) | 所谓切入点是指我们要**对哪些Joinpoint进行拦截**的定义。 | +| Advice(通知/增强) | 所谓通知是指拦截到Joinpoint之后所要做的事情就是**通知**。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能) | +| Introduction(引介) | 引介是一种**特殊的通知**在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field | +| Target(目标对象) | 代理的目标对象 | +| Weaving(织入) | 是指把增强应用到目标对象来创建新的代理对象的过程。有三种织入方式: Spring采用**动态代理织入**,而AspectJ采用**编译期织入**和**类装载期织入** | +| Proxy(代理)| 一个类被AOP织入增强后,就产生一个结果代理类 | +| Aspect(切面) | 是切入点和通知(/引介)的结合 | + +- 示例: + + + +## AOP的底层实现 +### JDK动态代理 +JDK动态代理:对**实现了接口的类**生成代理 + +```java +public interface IUserDao { + void add(); + void delete(); + void update(); + void search(); +} +``` +- UserDao实现了IUserDao接口 +```java +public class UserDao implements IUserDao{ + @Override + public void add() { + System.out.println("添加功能"); + } + + @Override + public void delete() { + System.out.println("删除功能"); + } + + @Override + public void update() { + System.out.println("更新功能"); + } + + @Override + public void search() { + System.out.println("查找功能"); + } +} +``` + +```java +public class JdkProxy implements InvocationHandler{ + private IUserDao iUserDao; + + public JdkProxy(IUserDao iUserDao){ + this.iUserDao=iUserDao; + } + + public IUserDao getPrxoy(){ + return (IUserDao)Proxy.newProxyInstance( + iUserDao.getClass().getClassLoader(), + iUserDao.getClass().getInterfaces(), + this + ); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Object obj = null; + //只对add方法进行增强 + if ("add".equals(methodName)) { + System.out.println("开启事务"); + obj = method.invoke(iUserDao, args); + System.out.println("结束事务"); + } else { + obj = method.invoke(iUserDao, args); + } + return obj; + } +} +``` +- 对实现了接口的类UserDao生成代理 +```java +public class JdkProxyDemo { + public static void main(String[] args) { + IUserDao userDao=new UserDao(); + JdkProxy jdkProxy=new JdkProxy(userDao); + IUserDao userDao2=jdkProxy.getPrxoy(); + userDao2.add(); + } +} +``` +- 输出结果 +```html +开启事务 +添加功能 +结束事务 +``` + +### Cglib动态代理 +以**继承的方式**动态生成目标类的代理。 + +CGLIB(Code Generation Library)是一个开源项目! +是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。 + +```java +public class ProductDao { + public void add() { + System.out.println("添加功能"); + } + + public void delete() { + System.out.println("删除功能"); + } + + public void update() { + System.out.println("更新功能"); + } + + public void search() { + System.out.println("查找功能"); + } +} +``` + +```java +public class CGLibProxy implements MethodInterceptor { + private ProductDao productDao; + + public CGLibProxy(ProductDao productDao) { + this.productDao = productDao; + } + + public ProductDao getProxy(){ + // 使用CGLIB生成代理: + // 1.创建核心类: + Enhancer enhancer = new Enhancer(); + // 2.为其设置父类: + enhancer.setSuperclass(productDao.getClass()); + // 3.设置回调: + enhancer.setCallback(this); + // 4.创建代理: + return (ProductDao) enhancer.create(); + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + String menthodName=method.getName(); + Object obj=null; + if("add".equals(menthodName)){ + System.out.println("开启事务"); + obj=methodProxy.invokeSuper(proxy,args); + System.out.println("结束事务"); + }else{ + obj=methodProxy.invokeSuper(proxy,args); + } + return obj; + } +} +``` +```java +public class CGLibProxyDemo { + public static void main(String[] args) { + ProductDao productDao=new ProductDao(); + CGLibProxy proxy=new CGLibProxy(productDao); + ProductDao productDao2=proxy.getProxy(); + productDao2.add(); + } +} +``` +- 输出结果 +```html +开启事务 +添加功能 +结束事务 +``` + +### 总结 + +Spring框架中: + +- **如果类实现了接口,就使用JDK的动态代理生成代理对象** +- **如果这个类没有实现任何接口,使用CGLIB生成代理对象** + + +### 相关阅读 + +- [代理设计模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/03%E7%BB%93%E6%9E%84%E5%9E%8B.md#7-%E4%BB%A3%E7%90%86proxy) + +Spring中代理模式的实现: + +- 真实实现类的逻辑包含在getBean方法中; +- getBean方法返回的实际上是Proxy的实例; +- Proxy实例是Spring 采用JDK Proxy或CGLIB动态生成的。 + +## Spring中的AOP + +### Spring中通知 +Spring中的通知Advice其实是指"增强代码"。 + +| 通知类型 | 全类名 | 说明 | +| :--: | :--: | :--: | +| 前置通知 | org.springframework.aop.MethodBeforeAdvice | 在目标方法执行前实施增强 | +| 后置通知 | org.springframework.aop.AfterReturningAdvice | 在目标方法执行后实施增强 | +| 环绕通知 | org.aopalliance.intercept.MethodInterceptor | 在目标方法执行前后实施增强 | +| 异常抛出通知 | org.springframework.aop.ThrowsAdvice | 在方法抛出异常后实施增强 | +| 引介通知 | org.springframework.aop.IntroductionInterceptor | 在目标类中添加一些新的方法和属性 | + + +### Spring中切面类型 +Advisor : Spring中传统切面。 +- Advisor:一个切点和一个通知组合。 +- Aspect:多个切点和多个通知组合。 + +Advisor : 代表一般切面,Advice本身就是一个切面,对目标类所有方法进行拦截(不带有切点的切面.针对所有方法进行拦截) +PointcutAdvisor : 代表具有切点的切面,可以指定拦截目标类哪些方法(带有切点的切面,针对某个方法进行拦截) +IntroductionAdvisor : 代表引介切面,针对引介通知而使用切面(不要求掌握) + +### Spring的AOP的开发 + +#### 1. 不带有切点的切面(针对所有方法的增强) +> 第一步:导入相应jar包 +```html + + aopalliance + aopalliance + 1.0 + +``` + +> 第二步:编写被代理对象 + +IUserDao接口 +```java +public interface IUserDao { + void add(); + void update(); + void delete(); + void search(); +} +``` +UserDao实现类 +```java +public class UserDao implements IUserDao{ + @Override + public void add() { + System.out.println("增加功能"); + } + + @Override + public void update() { + System.out.println("修改功能"); + } + + @Override + public void delete() { + System.out.println("删除功能"); + } + + @Override + public void search() { + System.out.println("查找功能"); + } +} +``` + +> 第三步:编写增强的代码 +```java +import org.springframework.aop.MethodBeforeAdvice; +import java.lang.reflect.Method; + +/** + * 前置增强 + */ +public class MyBeforeAdvice implements MethodBeforeAdvice{ + @Override + public void before(Method method, Object[] args, Object target) throws Throwable { + System.out.println("前置增强..."); + } +} +``` + +> 第四步:生成代理(配置生成代理) + +**生成代理Spring基于ProxyFactoryBean类。底层自动选择使用JDK的动态代理还是CGLIB的代理**。 + +属性: +- target : 代理的目标对象 +- proxyInterfaces : 代理要实现的接口 +```html +如果多个接口可以使用以下格式赋值 + + + .... + +``` +- proxyTargetClass : 是否对类代理而不是接口,设置为true时,使用CGLib代理 +- interceptorNames : 需要织入目标的Advice +- singleton : 返回代理是否为单实例,默认为单例 +- optimize : 当设置为true时,强制使用CGLib + +```html + + + + + + + + + + + + + + + + + + +``` +> 测试 +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("userDaoProxy") + // 注入是真实的对象,必须注入代理对象. + IUserDao iUserDao; + + @Test + public void test(){ + iUserDao.add(); + iUserDao.delete(); + iUserDao.update(); + iUserDao.search(); + } +} +``` +- 输出结果: +```html +前置增强... +增加功能 +前置增强... +删除功能 +前置增强... +修改功能 +前置增强... +查找功能 +``` + +#### 2. 带有切点的切面(针对目标对象的某些方法进行增强) +PointcutAdvisor 接口: +- DefaultPointcutAdvisor 最常用的切面类型,它可以通过任意Pointcut和Advice组合定义切面 +- RegexpMethodPointcutAdvisor 构造正则表达式切点切面 + +> 第一步:创建被代理对象 +```java +public class OrderDao { + public void add() { + System.out.println("增加功能"); + } + + public void update() { + System.out.println("修改功能"); + } + + public void delete() { + System.out.println("删除功能"); + } + + public void search() { + System.out.println("查找功能"); + } +} +``` + +> 第二步:编写增强的代码 + +```java +/** + * 环绕增强 + */ +public class MyAroundAdvice implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + System.out.println("环绕前增强..."); + Object obj= invocation.proceed(); + System.out.println("环绕后增强..."); + return obj; + } +} +``` + +> 第三步:生成代理(配置生成代理) + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +> 测试 +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("orderDaoProxy") + OrderDao orderDao; + + @Test + public void test(){ + orderDao.add(); + orderDao.delete(); + orderDao.update(); + orderDao.search(); + } +} +``` + +- 输出结果: +```html +环绕前增强... +增加功能 +环绕后增强... +删除功能 +修改功能 +环绕前增强... +查找功能 +环绕后增强... +``` + +#### 3. 自动代理 +前面的案例中,每个代理都是通过ProxyFactoryBean织入切面代理, +在实际开发中,非常多的**Bean每个都配置ProxyFactoryBean开发维护量巨大**。 + +自动创建代理(**基于后处理Bean。在Bean创建的过程中完成的增强。生成Bean就是代理**。) +- BeanNameAutoProxyCreator 根据Bean名称创建代理 +- DefaultAdvisorAutoProxyCreator 根据Advisor本身包含信息创建代理。 +其中AnnotationAwareAspectJAutoProxyCreator是基于Bean中的AspectJ 注解进行自动代理。 + +**BeanNameAutoProxyCreator(按名称生成代理)** + +```html + + + + + + + + + + + + + + + +``` + +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("userDao") + UserDao userDao; + //代理的是实例类,不是接口。 + + @Autowired + @Qualifier("orderDao") + OrderDao orderDao; + + @Test + public void test(){ + userDao.add(); + userDao.search(); + + orderDao.add(); + orderDao.search(); + } +} +``` + +**DefaultAdvisorAutoProxyCreator(根据切面中定义的信息生成代理)** + +```html + + + + + + + + + + + + + + + + + .*add.* + .*search.* + + + + + + + + + + + +``` + +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("userDao") + IUserDao userDao; + + @Autowired + @Qualifier("orderDao") + OrderDao orderDao; + + @Test + public void test(){ + userDao.add(); + userDao.search(); + + orderDao.add(); + orderDao.search(); + } +} +``` + +### 区分基于ProxyFattoryBean的代理与自动代理区别? + +- ProxyFactoryBean:先有被代理对象,将被代理对象传入到代理类中生成代理。 + +- 自动代理基于后处理Bean。**在Bean的生成过程中,就产生了代理对象**,把代理对象返回。 +生成的Bean已经是代理对象。 + +## Spring的AspectJ的AOP + +### 基于XML +> 第一步:编写被增强的类 + +UserDao + +> 第二步:定义切面 + +```java +public class MyAspectXML { + public void before(){ + System.out.println("前置通知..."); + } + + public void afterReturing(Object returnVal){ + System.out.println("后置通知...返回值:"+returnVal); + } + + public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ + System.out.println("环绕前增强...."); + Object result = proceedingJoinPoint.proceed(); + System.out.println("环绕后增强...."); + return result; + } + + public void afterThrowing(Throwable e){ + System.out.println("异常通知..."+e.getMessage()); + } + + public void after(){ + System.out.println("最终通知...."); + } +} +``` + +> 第三步:配置applicationContext.xml + +```html + + + + + + + + + + + + + + + + + + + + + + + +``` + +- 测试 +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("userDao") + IUserDao userDao; + + @Test + public void test(){ + userDao.add(); + userDao.search(); + } +} +``` + +### 基于注解 +AspectJ的通知类型: + +| 通知类型 | 解释 | 说明 | +| :--:| :--: | :--: | +| @Before | 前置通知,相当于BeforeAdvice | 就在方法之前执行。没有办法阻止目标方法执行的。 | +| @AfterReturning | 后置通知,相当于AfterReturningAdvice | 后置通知,获得方法返回值。 | +| @Around | 环绕通知,相当于MethodInterceptor | 在可以方法之前和之后来执行的,而且可以阻止目标方法的执行。 | +| @AfterThrowing | 抛出通知,相当于ThrowAdvice | | +| @After | 最终final通知,不管是否异常,该通知都会执行 | | +| @DeclareParents | 引介通知,相当于IntroductionInterceptor | | + + +**Advisor和Aspect的区别**? +- Advisor:Spring传统意义上的切面,支持一个切点和一个通知的组合。 +- Aspect:可以支持多个切点和多个通知的组合。 + +> 使用注解编写切面类 +```java +/** + * 切面类:就是切点与增强结合 + */ +@Aspect //申明是切面 +public class MyAspect{ + @Before("execution(* advice.UserDao.add(..))") + public void before(JoinPoint joinPoint){ + System.out.println("前置增强...."+joinPoint); + } + + @AfterReturning(value="execution(* advice.UserDao.update(..))",returning="returnVal") + public void afterReturin(Object returnVal){ + System.out.println("后置增强....方法的返回值:"+returnVal); + } + + @Around(value="MyAspect.myPointcut()") + public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ + System.out.println("环绕前增强...."); + Object obj = proceedingJoinPoint.proceed(); + System.out.println("环绕后增强...."); + return obj; + } + + @AfterThrowing(value="MyAspect.myPointcut()",throwing="e") + public void afterThrowing(Throwable e){ + System.out.println("不好了 出异常了!!!"+e.getMessage()); + } + + @After("MyAspect.myPointcut()") + public void after(){ + System.out.println("最终通知..."); + } + + //定义切点 + @Pointcut("execution(* advice.UserDao.search(..))") + private void myPointcut(){} +} +``` +> 配置applicationContext.xml +```html + + + + + + + + +``` + +- 测试: +```java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:applicationContext.xml") +public class SpringTest { + @Autowired + @Qualifier("userDao") + IUserDao userDao; + + @Test + public void test(){ + userDao.add(); + userDao.search(); + } +} +``` \ No newline at end of file diff --git "a/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" "b/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" new file mode 100644 index 0000000..024f5a2 --- /dev/null +++ "b/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" @@ -0,0 +1,545 @@ + +* [Spring中Bean的生命周期](#Spring中Bean的生命周期) + * [Bean的作用域](#Bean的作用域) + * [Bean的生命周期](#Bean的生命周期) + * [initialize和destroy](#initialize和destroy) + * [XxxAware接口](#XxxAware接口) + * [BeanPostProcessor](#BeanPostProcessor) + * [总结](#总结) + + +# Spring中Bean的生命周期 +Spring 中,组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。 +简单地讲,Bean 就是由 IOC 容器初始化、装配及管理的对象。 +Bean 的定义以及 Bean 相互间的依赖关系通过**配置元数据**来描述。 + +Spring中的Bean默认都是**单例**的, +这些单例Bean在多线程程序下如何保证线程安全呢? +例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求, +引入Spring框架之后,**每个Action都是单例的**, +那么对于Spring托管的单例Service Bean,如何保证其安全呢? +Spring使用ThreadLocal解决线程安全问题(为每一个线程都提供了一份变量,因此可以同时访问而互不影响)。 +Spring的单例是**基于BeanFactory**也就是Spring容器的,单例Bean在此容器内只有一个, +**Java的单例是基于 JVM,每个 JVM 内只有一个实例**。 + +## Bean的作用域 + +Spring Framework支持五种作用域: + +| 类别 | 说明 | +| :--:| :--: | +| singleton | 在SpringIOC容器中仅存在一个Bean实例,Bean以单例方式存在 | +| prototype | 每次从容器中调用Bean时,都返回一个新的实例 | +| request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 | +| session | 同一个Http Session共享一个Bean,不同Session使用不同Bean,仅适用于WebApplicationContext环境 | +| globalSession | 一般同于Portlet应用环境,该作用域仅适用于WebApplicationContext环境 | + +注意:五种作用域中, +request、session 和 global session +三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架), +只能用在基于 web 的 Spring ApplicationContext 环境。 + +### 1. singleton + +当一个 Bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个**共享的 Bean 实例**, +并且所有对 Bean 的请求,只要 id 与该 Bean 定义相匹配,则只会**返回 Bean 的同一实例**。 + +singleton 是单例类型(对应于单例模式),就是**在创建容器时就同时自动创建一个Bean对象**, +不管你是否使用,但我们可以指定Bean节点的 lazy-init="true" 来延迟初始化Bean, +这时候,只有在第一次获取Bean时才会初始化Bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。 +注意,singleton 作用域是Spring中的**缺省作用域**。 + +- 配置文件XML中将 Bean 定义成 singleton : +```html + +``` + +- @Scope 注解的方式: + +```java +@Service +@Scope("singleton") +public class ServiceImpl{ + +} +``` + +#### 2. prototype + +当一个Bean的作用域为 prototype,表示一个 Bean 定义对应多个对象实例。 +prototype 作用域的 Bean 会导致在每次对该 Bean 请求 +(将其注入到另一个 Bean 中,或者以程序的方式调用容器的 getBean() 方法)时都会创建一个新的 bean 实例。 +prototype 是原型类型,它在我们创建容器的时候并没有实例化, +而是当我们获取Bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。 + +根据经验,**对有状态的 Bean 应该使用 prototype 作用域,而对无状态的 Bean 则应该使用 singleton 作用域。** + +- 配置文件XML中将 Bean 定义成 prototype : + +```html + +``` +或者 + +```html + +``` + +- @Scope 注解的方式: + +```java +@Service +@Scope("prototype") +public class ServiceImpl{ + +} +``` + +### 3. request + +request只适用于**Web程序**,每一次 HTTP 请求都会产生一个新的 Bean , +同时该 Bean 仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。 + +在 XML 中将 bean 定义成 request ,可以这样配置: + +- 配置文件XML中将 Bean 定义成 prototype : + +```html + +``` + + +### 4. session + +session只适用于**Web程序**, +session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 Bean, +同时**该 Bean 仅在当前 HTTP session 内有效**。 +与request作用域一样,可以根据需要放心的更改所创建实例的内部状态, +而别的 HTTP session 中根据 userPreferences 创建的实例, +将不会看到这些特定于某个 HTTP session 的状态变化。 +当HTTP session最终被废弃的时候,在该HTTP session作用域内的bean也会被废弃掉。 + +```html + +``` + +### 5. globalSession + +globalSession 作用域**类似于标准的 HTTP session** 作用域, +不过仅仅在基于 portlet 的 Web 应用中才有意义。 +Portlet 规范定义了全局 Session 的概念, +它被所有构成某个 portlet web 应用的各种不同的 portlet所共享。 +在globalSession 作用域中定义的 bean 被限定于全局portlet Session的生命周期范围内。 + +```html + +``` + +## Bean的生命周期 + +Spring容器在创建、初始化和销毁Bean的过程中做了哪些事情: + +```java +Spring容器初始化 +===================================== +调用GiraffeService无参构造函数 +GiraffeService中利用set方法设置属性值 +调用setBeanName:: Bean Name defined in context=giraffeService +调用setBeanClassLoader,ClassLoader Name = sun.misc.Launcher$AppClassLoader +调用setBeanFactory,setBeanFactory:: giraffe bean singleton=true +调用setEnvironment +调用setResourceLoader:: Resource File Name=spring-beans.xml +调用setApplicationEventPublisher +调用setApplicationContext:: Bean Definition Names=[giraffeService, org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#0, com.giraffe.spring.service.GiraffeServicePostProcessor#0] +执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=giraffeService +调用PostConstruct注解标注的方法 +执行InitializingBean接口的afterPropertiesSet方法 +执行配置的init-method +执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=giraffeService +Spring容器初始化完毕 +===================================== +从容器中获取Bean +giraffe Name=张三 +===================================== +调用preDestroy注解标注的方法 +执行DisposableBean接口的destroy方法 +执行配置的destroy-method +Spring容器关闭 +``` + +### initialize和destroy + +有时我们需要在Bean属性值设置好之后和Bean销毁之前做一些事情, +比如检查Bean中某个属性是否被正常的设置好了。 +Spring框架提供了多种方法让我们可以在Spring Bean的生命周期中执行initialization和pre-destroy方法。 + +> InitializingBean 和 DisposableBean 接口 + +- InitializingBean 接口 : +```java +public interface InitializingBean { + void afterPropertiesSet() throws Exception; +} +``` + +- DisposableBean 接口 : +```java +public interface DisposableBean { + void destroy() throws Exception; +} +``` + +实现 InitializingBean 接口的afterPropertiesSet()方法可以在**Bean属性值设置好之后做一些操作**, +实现DisposableBean接口的destroy()方法可以在**销毁Bean之前做一些操作**。 + +```java +public class GiraffeService implements InitializingBean,DisposableBean { + @Override + public void afterPropertiesSet() throws Exception { + System.out.println("执行InitializingBean接口的afterPropertiesSet方法"); + } + @Override + public void destroy() throws Exception { + System.out.println("执行DisposableBean接口的destroy方法"); + } +} +``` +这种方法比较简单,但是不建议使用。 +因为这样会**将Bean的实现和Spring框架耦合在一起**。 + +> init-method 和 destroy-method 方法 + +配置文件中的配值init-method 和 destroy-method : + +```html + + +``` + +```java +public class GiraffeService { + //通过的destroy-method属性指定的销毁方法 + public void destroyMethod() throws Exception { + System.out.println("执行配置的destroy-method"); + } + //通过的init-method属性指定的初始化方法 + public void initMethod() throws Exception { + System.out.println("执行配置的init-method"); + } +} +``` + +需要注意的是自定义的init-method和post-method方法**可以抛异常但是不能有参数**。 + +这种方式比较推荐,因为可以**自己创建方法,无需将Bean的实现直接依赖于Spring的框架**。 + +> @PostConstruct 和 @PreDestroy注解 + +Spring 支持用 @PostConstruct和 @PreDestroy注解来指定 init 和 destroy 方法。 +这两个注解均在javax.annotation 包中。 +为了注解可以生效, +需要在配置文件中定义 +org.springframework.context.annotation.CommonAnnotationBeanPostProcessor。 + +```html + +``` + +```java +public class GiraffeService { + @PostConstruct + public void initPostConstruct(){ + System.out.println("执行PostConstruct注解标注的方法"); + } + @PreDestroy + public void preDestroy(){ + System.out.println("执行preDestroy注解标注的方法"); + } +} +``` + +### XxxAware接口 + +有些时候我们需要在 Bean 的初始化中**使用 Spring 框架自身的一些对象**来执行一些操作, +比如获取 ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。 +为了让 Bean 可以获取到框架自身的一些对象,Spring 提供了一组名为 XxxAware 的接口。 + +XxxAware接口均继承于org.springframework.beans.factory.Aware标记接口, +并提供一个将由 Bean 实现的set*方法, +Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。 + +常见的 XxxAware 接口: + +| 接口 | 说明 | +| :--: | :--: | +| ApplicationContextAware | 获得ApplicationContext对象,可以用来获取所有BeanDefinition的名字。 | +| BeanFactoryAware | 获得BeanFactory对象,可以用来检测Bean的作用域。 | +| BeanNameAware | 获得Bean在配置文件中定义的name。 | +| ResourceLoaderAware | 获得ResourceLoader对象,可以获得classpath中某个文件。 | +| ServletContextAware | 在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。 | +| ServletConfigAware | 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。 | + +```java +public class GiraffeService implements ApplicationContextAware, + ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware, + BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware{ + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + System.out.println("执行setBeanClassLoader,ClassLoader Name = " + classLoader.getClass().getName()); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + System.out.println("执行setBeanFactory,setBeanFactory:: giraffe bean singleton=" + beanFactory.isSingleton("giraffeService")); + } + + @Override + public void setBeanName(String s) { + System.out.println("执行setBeanName:: Bean Name defined in context=" + + s); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + System.out.println("执行setApplicationContext:: Bean Definition Names=" + + Arrays.toString(applicationContext.getBeanDefinitionNames())); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + System.out.println("执行setApplicationEventPublisher"); + } + + @Override + public void setEnvironment(Environment environment) { + System.out.println("执行setEnvironment"); + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + Resource resource = resourceLoader.getResource("classpath:spring-beans.xml"); + System.out.println("执行setResourceLoader:: Resource File Name=" + + resource.getFilename()); + } + + @Override + public void setImportMetadata(AnnotationMetadata annotationMetadata) { + System.out.println("执行setImportMetadata"); + } +} +``` + +### BeanPostProcessor +上面的XxxAware接口是**针对某个实现这些接口的Bean定制初始化的过程**, +Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程, +只需提供一个实现BeanPostProcessor接口的类即可。 + +- BeanPostProcessor 接口: +```java +public interface BeanPostProcessor{ + //postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行 + public abstract Object postProcessBeforeInitialization(Object obj, String s) + throws BeansException; + + //postProcessAfterInitialization方法在容器中的Bean初始化之后执行 + public abstract Object postProcessAfterInitialization(Object obj, String s) + throws BeansException; +} +``` + +```java +public class CustomerBeanPostProcessor implements BeanPostProcessor { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" + + beanName); + return bean; + } + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + System.out.println("执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=" + + beanName); + return bean; + } +} +``` + +将实现BeanPostProcessor接口的Bean像其他Bean一样定义在配置文件中: + +```html + +``` + +### 总结 +Spring Bean的生命周期 + +1. Bean容器找到配置文件中 Spring Bean 的定义。 + +2. Bean容器利用Java Reflection API创建一个Bean的实例。 + +3. 如果涉及到一些属性值,利用set方法设置一些属性值。 + +4. 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 + +5. 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 + +6. 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 + +7. 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 + +8. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, +执行postProcessBeforeInitialization()方法 + +9. 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 + +10. 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 + +11. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, +执行postProcessAfterInitialization()方法 + +12. 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法; +如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 + + + + + +很多时候我们并不会真的去实现上面说描述的那些接口, +那么下面我们就除去那些接口,针对 Bean 的单例和非单例来描述下 Bean 的生命周期: + +> **1. 单例管理的对象** + +scope="singleton",即默认情况下,会在启动容器时(即实例化容器时)时实例化。 +但我们可以指定 lazy-init="true"。这时候,只有在第一次获取 Bean 时才会初始化 Bean, +即第一次请求该 Bean时才初始化。配置如下: + +```html + +``` + +想对所有的默认单例Bean都应用延迟初始化, +可以在根节点beans 指定default-lazy-init="true",如下所示: + +```html + +``` + +默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。 +在创建对象的时候先调用**构造器**,然后调用 **init-method 属性值中所指定的方法**。 +对象在被销毁的时候,会调用 **destroy-method 属性值中所指定的方法**。 + +- LifeCycleBean : +```java +public class LifeCycleBean { + private String name; + + public LifeCycleBean(){ + System.out.println("LifeCycleBean()构造函数"); + } + public String getName() { + return name; + } + + public void setName(String name) { + System.out.println("setName()"); + this.name = name; + } + + public void init(){ + System.out.println("this is init of lifeBean"); + } + + public void destroy(){ + System.out.println("this is destory of lifeBean " + this); + } +} +``` + +- 配置文件 : +```html + +``` + +- 测试 : + +```java +public class LifeTest { + @Test + public void test() { + AbstractApplicationContext context = + new ClassPathXmlApplicationContext("lifeCycleBean.xml"); + LifeCycleBean life = (LifeCycleBean) context.getBean("lifeCycleBean"); + System.out.println("life:"+life); + context.close(); + } +} +``` + +- 输出结果: + +```html +LifeBean()构造函数 +this is init of lifeBean +life:com.southeast.bean.LifeCycleBean@573f2bb1 +this is destory of lifeBean com.southeast.bean.LifeCycleBean@573f2bb1 +``` + +> **2. 非单例管理的对象** + +当 scope= "prototype" 时,容器也会延迟初始化 Bean, +Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 Bean 时才初始化(如调用getBean方法时)。 + +在第一次请求每一个 prototype 的 Bean 时,Spring容器都会调用其构造器创建这个对象, +然后调用init-method属性值中所指定的方法。 +对象销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例, +这个类型的对象有很多个,**Spring容器一旦把这个对象交给你之后,就不再管理这个对象了**。 + +- 配置文件 : +```html + +``` + +- 测试: + +```java +public class LifeTest2 { + @Test + public void test() { + AbstractApplicationContext context = + new ClassPathXmlApplicationContext("lifeCycleBean.xml"); + LifeCycleBean life = (LifeCycleBean) context.getBean("lifeCycleBean"); + System.out.println("life:"+life); + + LifeCycleBean life2 = (LifeCycleBean) context.getBean("lifeCycleBeans"); + System.out.println("life2:"+life2); + context.close(); + } +} +``` + +- 输出结果: +```html +LifeBean()构造函数 +this is init of lifeBean +life:com.southeast.bean.LifeCycleBean@573f2bb1 +LifeBean()构造函数 +this is init of lifeBean +life2:com.southeast.bean.LifeCycleBean@5ae9a829 +this is destory of lifeBean LifeCycleBean@573f2bb1 +``` + +作用域为 prototype 的 Bean ,其destroy方法并没有被调用。 +如果 bean 的 scope 设为prototype时,当容器关闭时,destroy 方法不会被调用。 +对于 prototype 作用域的 bean,有一点非常重要, + +**Spring 容器可以管理 singleton 作用域下 Bean 的生命周期, +在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。 +而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给了客户端的代码管理, +Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的Bean的生命周期**。 \ No newline at end of file diff --git "a/docs/Spring/Spring 344円270円255円346円266円211円345円217円212円345円210円260円347円232円204円350円256円276円350円256円241円346円250円241円345円274円217円.md" "b/docs/Spring/Spring 344円270円255円346円266円211円345円217円212円345円210円260円347円232円204円350円256円276円350円256円241円346円250円241円345円274円217円.md" new file mode 100644 index 0000000..54b0777 --- /dev/null +++ "b/docs/Spring/Spring 344円270円255円346円266円211円345円217円212円345円210円260円347円232円204円350円256円276円350円256円241円346円250円241円345円274円217円.md" @@ -0,0 +1,29 @@ +# Spring 中涉及到的设计模式 + +## [工厂模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_2-简单工厂(simple-factory)) + +BeanFactory 用来创建各种不同的 Bean。 + +## [单例模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_1-单例(singleton)) + +在 Spring 配置文件中定义的 Bean 默认为单利模式。 + +## [模板方法模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_10-模板方法(template-method)) + +用来解决代码重复的问题。比如 JdbcTempolate。 + +## [代理模式](https://duhouan.github.io/Java-Notes/#/./OO/03结构型?id=_7-代理(proxy)) + +AOP、事务都大量运用了代理模式。 + +## [原型模式](https://duhouan.github.io/Java-Notes/#/./OO/01创建型?id=_6-原型模式(prototype)) + +特点在于通过"复制"一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的"原型",这个原型是可定制的。 + +## [责任链模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_1-责任链(chain-of-responsibility)) + +在 SpringMVC 中,会经常使用一些拦截器(HandlerInterceptor),当存在多个拦截器的时候,所有的拦截器就构成了一条拦截器链。 + +## [观察者模式](https://duhouan.github.io/Java-Notes/#/./OO/02行为型?id=_7-观察者(observer)) + +Spring 中提供了一种监听机制,即 ApplicationListenber,可以实现 Spring 容器内的事件监听。 \ No newline at end of file diff --git a/docs/Spring/SpringIOC.md b/docs/Spring/SpringIOC.md new file mode 100644 index 0000000..93eaaa4 --- /dev/null +++ b/docs/Spring/SpringIOC.md @@ -0,0 +1,463 @@ +# 二、Spring IOC + +## IoC 与 DI + +### IoC 概念 + +控制反转(Iversion of Control,IoC)是一种**设计思想**。**将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理**。涉及到 2 个方面: + +- **控制** + + 控制对象的创建及销毁(生命周期)。 + +- **反转** + + 将对象的控制权交给 IoC 容器。 + +所有的类都会在 Spring 容器中注册,告诉 Spring 你是个什么东西,你需要什么东西,然后 Spring 会在系统运行到适当的时候,把你需要的东西主动给你 + +**所有类的创建、销毁都由 Spring 来控制,也就是说控制对象生命周期的不是引用它的对象,而是 Spring**。对于某个具体对象而言,以前是它控制其他对象,现在所有对象都被 Spring 控制。 + +### DI 概念 + +依赖注入(Dependency Injection,DI) 。依赖注入举例:设计行李箱。 + + + +相应的代码如下: + + + +size 固定值,改进后的代码: + + + +使用 DI 方式进行改进: + + + +改进后相应的代码如下: + + + +不难理解,依赖注入就是**将底层类作为参数传递给上层类,实现上层对下层的控制**,**依赖注入实现控制反转**。 + +### IoC 和 DI 关系 + +IoC 是在系统运行中,**动态的向某个对象提供它所需要的其他对象**,通过 DI 来实现。 + +- IoC 和 DI 的关系如下图: + + + +- 依赖倒置原则、IoC、DI 和 IoC 容器的关系: + + + +## IoC 容器 + +IoC 容器功能: + +- 管理 Bean 的生命周期 +- 控制 Bean 依赖注入 + +使用 IoC 容器的好处: + +- 避免在各处使用 new 来创建类,并且可以做到统一维护 + +- 创建实例的时候不需要了解其中的细节 + + 依赖注入方式获取实例: + + + + 使用 IoC 容器获取获取实例(创建实例的时候不需要了解其中的细节): + + + + + +## XML 配置文件解析 + +### BeanDefinition + +BeanDefinition 是一个接口。在 Spring 中存在 3 种实现: + +- RootBeanDefinition +- ChildBeanDefinition +- GenericBeanDefinition + +三种实现都继承了 AbstractBeanDefinition。**AbstractBeanDefinition 是对上述的共同的类信息进行抽象**。 + +```java +public abstract class AbstractBeanDefinition extends + BeanMetadataAttributeAccessor implements BeanDefinition, Cloneable { + //... +} +``` + +**BeanDefinition 是配置文件中 ``元素标签在容器中的内部表现形式**。 + +在配置文件中,可以定义父`` 和子``,父`` 用 RootBeanDefinition 表示,而子``用ChildBeanDefinition 表示,而没有父`` 的 `` 就使用 RootBeanDefinition 表示。 + +### BeanDefinitionRegistry + +Spring 通过 BeanDefinition 将配置文件中的``配置信息转换为容器的内部表示,并且将这些 BeanDefinition 注册 **BeanDefinitionRegistry** 中。 + +Spring 容器的 BeanDefinitionRegistry 就是配置信息的 内存数据库,主要是以 map 的形式保存数据,后续操作直接从 BeanDefinitionRegistry 中读取配置信息。 + +### Resource + +Spring 采用 Resource 来对各种资源进行统一抽象。 + +Resource 是一个接口,定义了资源的基本操作,包括是否存在、是否可读、是否已经打开等等。 + +Resource 继承了 InputStreamSource。 + +```java +public interface Resource extends InputStreamSource { + //... +} +``` + +InputStreamSource 封装任何可以返回 InputStream 的类。 + +```java +/* +* InputStreamSource 封装任何可以返回 InputStream 的类。 +*/ +public interface InputStreamSource { + /* + * 用于返回一个新的 InputStream 对象 + */ + InputStream getInputStream() throws IOException; +} +``` + +对于不同来源的资源文件都有相应的 Resource 实现: + +| 资源来源 | Resource 实现类 | +| :--------------: | :-----------------: | +| 文件 | FileSystemResource | +| classpath 资源 | ClassPathResource | +| URL 资源 | UrlResource | +| InputStream 资源 | InputStreamResource | +| Byte 数组 | ByteArrayResource | + +### 解析过程 + +分为 2 大步骤: + +**1、配置文件的封装** + +Resource 接口**抽象了所有 Spring 内部使用到的底层资源**:File、URL、ClassPath 等。 + +**2、加载 Bean** + +- 封装资源文件。XMLBeanDefinitionReader 对参数 Resource 使用 EncodeResource 类进行封装。 +- 获取输入流。从 Resource 中获取对应的 InputStream 并构造 InputSource。 +- 通过构造的 InputSource 实例和 Resource 实例继续调用函数 `doLoadBeanDefinition`。 + * 获取 XML 文件的实体解析器和验证模式(常见的验证模式有 DTD 和 XSD 两种) + * 加载 XML 文件,并得到对应的 Document + * 根据返回的 Document 解析并注册 BeanDefinition + +## Spring 的 IoC 容器 + + + +IoC 容器其实就是一个大工厂,它用来管理我们所有的对象以及依赖关系: + +- 根据 Bean 配置信息在容器内部创建 Bean 定义注册表 +- 根据注册表加载,实例化 Bean,**建立 Bean 与 Bean 之间的依赖关系** +- 将 Bean 实例放入 Spring IoC 容器中,等待应用程序调用 + +### Spring IoC 支持的功能 + +- 依赖注入 +- 依赖检查 +- 自动装配属性 +- 可指定初始化方法和销毁方法 + +### Spring 的 2 种 IoC 容器 + +- **BeanFactory** + - IoC 容器要实现的最基础的接口 + - 采用**延迟初始化策略**(容器初始化完成后并不会创建 Bean 对象,只有当收到初始化请求时才进行初始化) + - 由于是延迟初始化策略,因此启动速度较快,占用资源较少 + +- **ApplicationConext** + - 在 BeanFactory 基础上,增加了更为高级的特性:事件发布、国际化等。 + - 在容器启动时,完成所有 Bean 的创建 + - 启动时间较长,占用资源较多 + +### IoC 容器的初始化过程 + +Spring IoC 容器的初始化过程分为 3 个阶段:Resource 定位、BeanDefinition 的载入和向 IoC 容器注册 BeanDefinition。Spring 将这 3 个阶段分离,并使用不同的模块来完成,这样可以让用户更加灵活的对这 3 个阶段进行扩展。 + + + +1、**Resource 定位** + +Resource 定位指的是 BeanDefinition 的资源定位,它由 ResourceLoader 通过统一的 Resource 接口来完成,Resource 为各种形式的 BeanDefinition 的使用都提供了统一的接口。 + +2、**BeanDefinition 的载入** + +BeanDefinition 实际上就是 POJO 对象在 IoC 容器中的抽象,通过 BeanDefiniton,IoC 容器可以方便对 POJO 对象进行管理。 + +3、 **向 IoC 容器注册 BeanDefinition** + +向 IoC 容器注册 BeanDefinition 是通过调用 BeanDefinitionRegistry 接口来的实现来完成的,这个注册过程是把载入的 BeanDefinition 向 IoC 容器进行注册。实际上 IoC 容器内部维护这一个 HashMap,而这个注册过程其实就是将 BeanDefinition 添加至该 HashMap 中。 + +> 注意:BeanFactory 和 FactoryBean 的区别 + +BeanFactory 是 IoC 最基本的容器,负责生产和管理 Bean,为其他具体的 IoC 容器提供了最基本的规范。 + +FactoryBean 是一个 Bean,是一个接口,当 IoC 容器中的 Bean 实现了 FactoryBean 后, + +通过 getBean(String beanName) 获取到的 Bean 对象并不是 FactoryBean 的实现类对象,而是这个实现类中的 getObject() 方法返回的对象。 + +要想获取 FactoryBean 的实现类对象,就是在 beanName 前面加上 "&"。 + +## Spring Bean 加载流程 + +AbstractBeanFactory 中 `getBean`最终调用 `doGetBean`方法。 + +```java +protected T doGetBean(String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { + String beanName = this.transformedBeanName(name); + Object sharedInstance = this.getSingleton(beanName); + Object bean; + if (sharedInstance != null && args == null) { + if (this.logger.isTraceEnabled()) { + if (this.isSingletonCurrentlyInCreation(beanName)) { + this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); + } else { + this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); + } + } + + bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null); + } else { + if (this.isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + BeanFactory parentBeanFactory = this.getParentBeanFactory(); + if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) { + String nameToLookup = this.originalBeanName(name); + if (parentBeanFactory instanceof AbstractBeanFactory) { + return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly); + } + + if (args != null) { + return parentBeanFactory.getBean(nameToLookup, args); + } + + if (requiredType != null) { + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + + return parentBeanFactory.getBean(nameToLookup); + } + + if (!typeCheckOnly) { + this.markBeanAsCreated(beanName); + } + + try { + RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName); + this.checkMergedBeanDefinition(mbd, beanName, args); + String[] dependsOn = mbd.getDependsOn(); + String[] var11; + if (dependsOn != null) { + var11 = dependsOn; + int var12 = dependsOn.length; + + for(int var13 = 0; var13 < var12; ++var13) { + String dep = var11[var13]; + if (this.isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + + this.registerDependentBean(dep, beanName); + + try { + this.getBean(dep); + } catch (NoSuchBeanDefinitionException var24) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", var24); + } + } + } + + if (mbd.isSingleton()) { + sharedInstance = this.getSingleton(beanName, () -> { + try { + return this.createBean(beanName, mbd, args); + } catch (BeansException var5) { + this.destroySingleton(beanName); + throw var5; + } + }); + bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } else if (mbd.isPrototype()) { + var11 = null; + + Object prototypeInstance; + try { + this.beforePrototypeCreation(beanName); + prototypeInstance = this.createBean(beanName, mbd, args); + } finally { + this.afterPrototypeCreation(beanName); + } + + bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } else { + String scopeName = mbd.getScope(); + Scope scope = (Scope)this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + + try { + Object scopedInstance = scope.get(beanName, () -> { + this.beforePrototypeCreation(beanName); + + Object var4; + try { + var4 = this.createBean(beanName, mbd, args); + } finally { + this.afterPrototypeCreation(beanName); + } + + return var4; + }); + bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } catch (IllegalStateException var23) { + throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", var23); + } + } + } catch (BeansException var26) { + this.cleanupAfterBeanCreationFailure(beanName); + throw var26; + } + } + + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = this.getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } else { + return convertedBean; + } + } catch (TypeMismatchException var25) { + if (this.logger.isTraceEnabled()) { + this.logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", var25); + } + + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } else { + return bean; + } +} +``` + +doGetBean 大致流程如下: + +**1、获取参数 name 对应的真正 beanName** + +**2、尝试从缓存中加载单例** + +单例在 Spring 的同一个容器中只会被创建一次,后续再获取 Bean,就直接从**单例缓存**中获取。 + +这里只是尝试加载,首先尝试从**缓存**中加载,如果加载不成功则再次尝试从 **singletonFactories** 中加载。 + +在创建依赖的时候为了避免循环依赖,在 Spring 中创建 Bean 的原则:**不等 bean 创建完成就会将创建 Bean 的 ObjectFactory 提早曝光加入到缓存中,一旦下一个 Bean 创建时需要依赖上一个 Bean 则直接使用 ObjectFactory**。 + +**3、Bean 的实例化** + +如果从缓存中得到了 Bean 的原始状态,则需要对 Bean 进行实例化。这里有必要强调一下,缓存中记录的是原始的 Bean 状态,并一定是我们最终想要的 Bean。 + +**4、原型模式的依赖检查** + +只有在**单例情况下**才会尝试解决循环依赖。 + +如果存在 A 中有 B 的属性,B 中有 A 的属性,那么当依赖注入的时 候,就会产生当 A 还未创建完的时候因为对于 B 的创建再次返回创建 A ,造成循环依赖,即 isPrototypeCurrentlyInCreation(String beanName) 判断 true。 + +**5、检测 parentBeanFactory** + +如果缓存没有数据的话,直接转到 parentBeanFactory 去加载,条件是: + +```java +parentBeanFactory != null && !this.containsBeanDefinition(beanName) +``` + +containsBeanDefinition 方法是检测如果当前 XML 中没有配置对应的 beanName,只能到 parentBeanfactory 中尝试一下。 + +**6、将 GenericBeanDefinition 对象转换为 RootBeanDefiniion 对象** + +从 XML 配置文件中读取 Bean 信息会放到 GenericBeanDefinition 中, 但是所有的 Bean 后续处理都是针对 RootBeanDefinition 的。 + +这里做一下转换,转换的同时如果父类 Bean 不为空的话,则会进一步合并父类的属性。 + +**7、初始化依赖的 Bean** + +Spring 在初始化某一个 Bean 的时候首先初始化这个 Bean 的依赖。 + +**8、依据当前 bean 的作用域对 bean 进行实例化** + +Spring 中可以指定各种 scope,默认是 singleton 。 + +还有其他的配置比如 prototype、request 等,Spring 会根据不同的配置进行不同的初始化策略。 + +**9、类型转换** + +将返回的 Bean 转化为 requiredType 所指定的类型。 + +**10、返回 Bean** + +## Spring 解决循环依赖 + +循环依赖就是循环引用,就是两个或者多个 Bean 之间相互持有对方,比如 CircleA 引用 CircleB,CircleB 引用 CircleC,CircleC 引用 CircleA,则它们最终反映为一个环。 + +### 构造器循环依赖 + +表示通过构造器注入构成的循环依赖。此依赖是无法解决的,只能抛出 BeanConcurrentlyCreationException 异常表示循环依赖。 + +### setter 循环依赖 + +表示通过 setter 注入构成的循环依赖。 解决该依赖问题是通过 Spring 容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter 注入) 的 Bean 来完成的,而且只能解决单例作用域的 Bean 的循环依赖。 + +对于 `scope="prototype"`的 Bean,Spring 容器无法完成依赖注入,因为 "prototype" 作用域 Bean,Spring 容器不进行缓存,因此无法提前暴露一个创建中的 Bean。对于 "singleton" 作用域 Bean,可以通过setAllowCircleReferences(false) 来禁用循环引用。 + +对于单例对象来说,在 Spring 整个容器的生命周期内,有且只有一个对象,很容易想到这个对象应该存在于**缓存**中,Spring 在解决循环依赖问题的过程中就使用了"三级缓存": + +- **singletonObjects ** + + 用于保存 beanName 和创建 Bean 实例之间的关系,缓存的是 **Bean 实例**。 + +- **singletonFactories** + + 用于保存 beanName 和创建 Bean 的工厂之间的关系,缓存的是为解决循环依赖而准备的 **ObjectFactory**。 + +- **earlySingletonObjects** + + 用于保存 beanName 和创建 Bean 实例之间的关系,与 singletonObjects 的不同之处在于,当一个单例 Bean 被放到这里面后,当 Bean 还在创建过程中,就可以通过 getBean 方法获得,其目的是检查循环引用。这边缓存的也是实例,只是这边的是为解决循环依赖而提早暴露出来的实例,其实就是 ObjectFactory。 + +首先尝试从 singletonObjects 中获取实例,如果获取不到,再从 earlySingletonObjects 中获取,如果还获取不到,再尝试从 singletonFactories 中获取 beanName 对应的 ObjectFactory,然后调用 getObject 来创建 Bean,并放到 earlySingletonObjects 中去,并且从 singletonFactories 中 remove 掉这个 ObjectFactory。 + +> 注意:singleObjects 和 earlySingletonObjects + +- 两者都是以 beanName 为key,Bean 实例为 value 进行存储 + +- 两者得出区别在于 singletonObjects 存储的是实例化的完成的 Bean 实例,而 earlySingletonObjects 存储的是正在实例化中的 Bean,所以两个集合的内容是互斥的。 + + +# 参考资料 + +- [Spring 框架小白的蜕变](https://www.imooc.com/learn/1108) + +- [Spring IOC知识点一网打尽!](https://segmentfault.com/a/1190000014979704) +- [Spring核心思想,IoC与DI详解(如果还不明白,放弃java吧)](https://blog.csdn.net/Baple/article/details/53667767) +- [15个经典的Spring面试常见问题](https://mp.weixin.qq.com/s/uVoeXRLNEMK8c00u3tm9KA) \ No newline at end of file From eaf2f436fafe225b7ae8851f17ce98832ab6c6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=8E=9A=E5=AE=89?= <33805265+duhouan@users.noreply.github.com> Date: 2019年6月28日 13:36:30 +0800 Subject: [PATCH 17/29] =?UTF-8?q?Update=20=E5=BC=80=E5=8F=91=E6=A1=86?= =?UTF-8?q?=E6=9E=B6-=E7=9B=AE=E5=BD=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" index 87d8c53..1bc1f6e 100644 --- "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" +++ "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" @@ -3,13 +3,11 @@ ### Spring - [Spring概述](./Spring/00Spring概述.md) -- [SpringIOC](./Spring/01SpringIOC.md) +- [SpringIOC](./Spring/SpringIOC.md) - [SpringAOP](./Spring/02SpringAOP.md) -- [手撕TinySpring](./Spring/03手撕TinySpring.md) - [Spring事务管理](./Spring/04Spring事务管理.md) - [Spring中Bean的生命周期](./Spring/05Spring中Bean的生命周期.md) ### SpringMVC - [SpringMVC](./Spring/06SpringMVC.md) -- [常见面试题总结](./Spring/07常见面试题总结.md) \ No newline at end of file From d423bb63f7cccb983f62faa7a148e3152c6efeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=8E=9A=E5=AE=89?= <33805265+duhouan@users.noreply.github.com> Date: 2019年6月28日 13:37:02 +0800 Subject: [PATCH 18/29] =?UTF-8?q?Update=20=E5=BC=80=E5=8F=91=E6=A1=86?= =?UTF-8?q?=E6=9E=B6-=E7=9B=AE=E5=BD=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46241円206円346円236円266円-347円233円256円345円275円225円.md" | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" index 1bc1f6e..3ad4caf 100644 --- "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" +++ "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" @@ -2,11 +2,11 @@ ### Spring -- [Spring概述](./Spring/00Spring概述.md) -- [SpringIOC](./Spring/SpringIOC.md) -- [SpringAOP](./Spring/02SpringAOP.md) -- [Spring事务管理](./Spring/04Spring事务管理.md) -- [Spring中Bean的生命周期](./Spring/05Spring中Bean的生命周期.md) +- [Spring 概述](./Spring/00Spring概述.md) +- [Spring IoC](./Spring/SpringIOC.md) +- [Spring AOP](./Spring/02SpringAOP.md) +- [Spring 事务管理](./Spring/04Spring事务管理.md) +- [Spring 中 Bean 的生命周期](./Spring/05Spring中Bean的生命周期.md) ### SpringMVC From 7d7729e1ba503340a384c040adb166757268b9b9 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年6月28日 20:35:24 +0800 Subject: [PATCH 19/29] Update Kafka.md --- docs/BigData/Kafka.md | 53 ++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/BigData/Kafka.md b/docs/BigData/Kafka.md index b25a3a5..85a9ba4 100644 --- a/docs/BigData/Kafka.md +++ b/docs/BigData/Kafka.md @@ -144,31 +144,7 @@ LEO(Log End Offset)是所有的副本都会有的一个 offset 标记,它 Producers 往 Brokers 中指定的 Topic 写消息,Consumers 从 Brokers 里面拉去指定 Topic 的消息,然后进行业务处理。 图中有两个 Topic,Topic-0 有两个 Partition,Topic-1 有一个 Partition,三副本备份。可以看到 Consumer-Group-1 中的 Consumer-2 没有分到 Partition 处理,这是有可能出现的。 - - - - - - - - - - - - - - - - - - - - - - -//=============================================== - -Kafka 工作流程: +#### Kafka 工作流程 生产者会根据业务逻辑产生消息,之后根据路由规则将消息发送到指定分区的 Leader 副本所在的 Broker 上。在Kafka 服务端接收到消息后,会将消息追加到 Log 中保存,之后 Follower 副本会与 Leader 副本进行同步,当 ISR集合中所有副本都完成了此消息的同步后,则 Leader 副本的 HW 会增加,并向生产者返回响应。 @@ -283,7 +259,32 @@ Kafka将数据以日志的形式保存在磁盘中。 - 每一个 log 文件中又分为多个 segment。 -## 七、Kafka 的分布式实现 +## 七、Kafka 的高可用 + +#### 高可用如何实现? + +Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 + +这样就算某个broker宕机,该broker上面的partition也不会丢失,因为其他broker上都有副本,这样Kafka就具有了高可用性。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。 + +#### 那么如何选举leader呢? + +最简单最直观的方案是,所有Follower都在Zookeeper上设置一个Watch,一旦Leader宕机,其对应的ephemeral znode会自动删除,此时所有Follower都尝试创建该节点,而创建成功者(Zookeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower。 + +**写数据**的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为) + +**消费**的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。 + +#### 如何处理所有Replica都不工作? + + 上文提到,在ISR中至少有一个follower时,Kafka可以确保已经commit的数据不丢失,但如果某个Partition的所有Replica都宕机了,就无法保证数据不丢失了。这种情况下有两种可行的方案: + +- 等待ISR中的任一个Replica"活"过来,并且选它作为Leader +- 选择第一个"活"过来的Replica(不一定是ISR中的)作为Leader + + 这就需要在可用性和一致性当中作出一个简单的折衷。如果一定要等待ISR中的Replica"活"过来,那不可用的时间就可能会相对较长。而且如果ISR中的所有Replica都无法"活"过来了,或者数据都丢失了,这个Partition将永远不可用。选择第一个"活"过来的Replica作为Leader,而这个Replica不是ISR中的Replica,那即使它并不保证已经包含了所有已commit的消息,它也会成为Leader而作为consumer的数据源(前文有说明,所有读写都由Leader完成)。Kafka0.8.*使用了第二种方式。根据Kafka的文档,在以后的版本中,Kafka支持用户通过配置选择这两种方式中的一种,从而根据不同的使用场景选择高可用性还是强一致性。 + +## 八、Kafka 的分布式实现 From 194d3f948c66382558fe094557c39532aea10475 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: 2019年6月28日 23:35:44 +0800 Subject: [PATCH 20/29] Java-Notes --- docs/Spring/02SpringAOP.md | 65 +- ...13345円212円241円347円256円241円347円220円206円.md" | 174 ++-- ...37345円221円275円345円221円250円346円234円237円.md" | 200 ++--- docs/Spring/Spring AOP.md | 776 ------------------ ...37345円221円275円345円221円250円346円234円237円.md" | 545 ------------ ...70350円247円201円346円263円250円350円247円243円.md" | 50 ++ .../SpringBoot 344円273円213円347円273円215円.md" | 22 + 7 files changed, 260 insertions(+), 1572 deletions(-) delete mode 100644 docs/Spring/Spring AOP.md delete mode 100644 "docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" create mode 100644 "docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" create mode 100644 "docs/Spring/SpringBoot 344円273円213円347円273円215円.md" diff --git a/docs/Spring/02SpringAOP.md b/docs/Spring/02SpringAOP.md index 05a5326..c1d87bd 100644 --- a/docs/Spring/02SpringAOP.md +++ b/docs/Spring/02SpringAOP.md @@ -6,10 +6,10 @@ * [Spring的AspectJ的AOP](#Spring的AspectJ的AOP) -# SpringAOP +# Spring AOP -## SpringAOP概述 -### 什么是AOP +## AOP 概述 +### 什么是 AOP AOP(面向切面编程,Aspect Oriented Programing)。 AOP采取**横向抽取**机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存) @@ -21,11 +21,7 @@ AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提 AOP底层原理就是**代理机制**。 -> 关注点分离:不同的问题交给不同部分去解决 - -### Spring的AOP代理 -- JDK动态代理:对实现了接口的类生成代理 -- CGLib代理机制:对类生成代理 +> 关注点分离:不同的问题交给不同部分去解决。 ### AOP的术语 @@ -36,16 +32,29 @@ AOP底层原理就是**代理机制**。 | Advice(通知/增强) | 所谓通知是指拦截到Joinpoint之后所要做的事情就是**通知**。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能) | | Introduction(引介) | 引介是一种**特殊的通知**在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field | | Target(目标对象) | 代理的目标对象 | -| Weaving(织入) | 是指把增强应用到目标对象来创建新的代理对象的过程。有三种织入方式: Spring采用**动态代理织入**,而AspectJ采用**编译期织入**和**类装载期织入** | +| Weaving(织入) | 是指把增强应用到目标对象来创建新的代理对象的过程。
有三种织入方式: Spring采用**动态代理织入**,而AspectJ采用**编译期织入**和**类装载期织入** | | Proxy(代理)| 一个类被AOP织入增强后,就产生一个结果代理类 | | Aspect(切面) | 是切入点和通知(/引介)的结合 | -- 示例: - - - -## AOP的底层实现 -### JDK动态代理 +- 示例:在 IUserDao() 中 + + ```java + public interface IUserDao { + void add(); + void delete(); + void update(); + void search(); + } + // IUserDao 被增强的对象,就是 Target(目标对象) + // add()、delete()、update() 和 search() 都是 JoinPoint(连接点) + // 这里要对 add() 和 update() JoinPoint 进行拦截,则 add() 和 update() 就是 Pointcut(切入点) + // Advice 指的是要增强的代码,也就是代码的增强 + // Weaving:指的是把增强(Advice)应用到目标对象(Target)创建新的代理对象得人过程 + // Aspect:是切入点和通知的结合,在 add 或 delete 方法上应用增强 + ``` + +## AOP 的底层实现 +### JDK 动态代理 JDK动态代理:对**实现了接口的类**生成代理 ```java @@ -131,7 +140,7 @@ public class JdkProxyDemo { 结束事务 ``` -### Cglib动态代理 +### Cglib 动态代理 以**继承的方式**动态生成目标类的代理。 CGLIB(Code Generation Library)是一个开源项目! @@ -215,21 +224,11 @@ Spring框架中: - **如果类实现了接口,就使用JDK的动态代理生成代理对象** - **如果这个类没有实现任何接口,使用CGLIB生成代理对象** +- **可以通过配置文件指定对接口使用 CGLIB 生成代理对象** +## Spring 中的 AOP -### 相关阅读 - -- [代理设计模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/03%E7%BB%93%E6%9E%84%E5%9E%8B.md#7-%E4%BB%A3%E7%90%86proxy) - -Spring中代理模式的实现: - -- 真实实现类的逻辑包含在getBean方法中; -- getBean方法返回的实际上是Proxy的实例; -- Proxy实例是Spring 采用JDK Proxy或CGLIB动态生成的。 - -## Spring中的AOP - -### Spring中通知 +### Spring 中通知 Spring中的通知Advice其实是指"增强代码"。 | 通知类型 | 全类名 | 说明 | @@ -241,7 +240,7 @@ Spring中的通知Advice其实是指"增强代码"。 | 引介通知 | org.springframework.aop.IntroductionInterceptor | 在目标类中添加一些新的方法和属性 | -### Spring中切面类型 +### Spring 中切面类型 Advisor : Spring中传统切面。 - Advisor:一个切点和一个通知组合。 - Aspect:多个切点和多个通知组合。 @@ -250,7 +249,7 @@ Advisor : 代表一般切面,Advice本身就是一个切面,对目标类所 PointcutAdvisor : 代表具有切点的切面,可以指定拦截目标类哪些方法(带有切点的切面,针对某个方法进行拦截) IntroductionAdvisor : 代表引介切面,针对引介通知而使用切面(不要求掌握) -### Spring的AOP的开发 +### Spring 的 AOP 的开发 #### 1. 不带有切点的切面(针对所有方法的增强) > 第一步:导入相应jar包 @@ -608,9 +607,9 @@ public class SpringTest { - 自动代理基于后处理Bean。**在Bean的生成过程中,就产生了代理对象**,把代理对象返回。 生成的Bean已经是代理对象。 -## Spring的AspectJ的AOP +## Spring 的 AspectJ 的 AOP -### 基于XML +### 基于 XML > 第一步:编写被增强的类 UserDao diff --git "a/docs/Spring/04Spring344円272円213円345円212円241円347円256円241円347円220円206円.md" "b/docs/Spring/04Spring344円272円213円345円212円241円347円256円241円347円220円206円.md" index 667b85b..9b0ff4c 100644 --- "a/docs/Spring/04Spring344円272円213円345円212円241円347円256円241円347円220円206円.md" +++ "b/docs/Spring/04Spring344円272円213円345円212円241円347円256円241円347円220円206円.md" @@ -1,120 +1,114 @@ - +# Spring 事务管理 - +## 事务属性 -# 简介 +事务属性可以理解成事务的一些基本配置,描述事务策略如何应用到方法上。 -Spring 的事务有两种类型: +事务属性包含了 5 个方面: -- 全局事务(分布式事务) -- 局部事务(单机事务) +- 传播行为 +- 隔离级别 +- 是否只读事务 +- 事务超时 +- 回滚规则 -我们平时常用的是单机事务,也就是操作一个数据库的事务。 +### 1、传播行为 -按照使用方式来分,又可以分为编程式事务模型(TransactionTemplate)和声明式事务模型(@Transactional注解),后者可以理解为AOP + 编程式事务模型。 - -# 一、编程式事务模型 - -## 1.1 统一事务模型 - -在统一事务模型中,最重要的一个类就是TransactionTemplate,其中最重要的方法是execute方法,其源码如下: - -```java -@Override - public T execute(TransactionCallback action) throws TransactionException { - if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { - return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action); - } - else { - //获取当前事务的状态 - TransactionStatus status = this.transactionManager.getTransaction(this); - T result; - try { - result = action.doInTransaction(status); - } - catch (RuntimeException ex) { - // 出现异常的回滚操作 - rollbackOnException(status, ex); - throw ex; - } - catch (Error err) { - // 出现Error的回滚操作 - rollbackOnException(status, err); - throw err; - } - catch (Throwable ex) { - // Transactional code threw unexpected exception -> rollback - rollbackOnException(status, ex); - throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception"); - } - this.transactionManager.commit(status); - return result; - } - } -``` - -其中transactionManager是PlatformTransactionManager接口,负责事务的创建、提交、回滚策略。这里TransactionTemplate相当于实现了[模板方法]()+[策略模式]()这两种设计模式。 - -针对不同的厂商,只需要提供不同的PlatformTransactionManager实现即可。我们在使用的时候,只需要通过Spring IOC,告诉Spring,要注入哪个TransactionManager,要使用哪种策略即可。 - -> Spring 如何保证所有数据库操作都是使用同一个数据库连接呢? -> -> [补充资料]() - -## 1.2 事务传播级别 - -事务传播行为:**用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的事务时如何传播**。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。 +当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。 + +事务传播行为:**用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的事务时如何传播**。 Spring中有七种事务传播类型: -| 类型 | 说明 | -| :--------------------------------------- | :----------------------------------------------------------- | -| PROPAGATION_REQUIRED(required) | 表示**当前方法必须运行在事务中**。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。 | -| PROPAGATION_SUPPORTS(support) | 表示支持当前事务,如果当前没有事务,就以无事务方式执行。 | -| PROPAGATION_MANDATORY(mandatory) | 表示使用当前的事务,如果当前没有事务,就抛出异常。 | -| PROPAGATION_REQUIRES_NEW(required_new) | 表示新建事务,如果当前存在事务,把当前事务挂起。 | +| 类型 | 说明 | +| :--------------------------------------: | :----------------------------------------------------------: | +| PROPAGATION_REQUIRED(required) | 表示**当前方法必须运行在事务中**。
如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。 | +| PROPAGATION_SUPPORTS(support) | 表示支持当前事务,如果当前没有事务,就以无事务方式执行。 | +| PROPAGATION_MANDATORY(mandatory) | 表示使用当前的事务,如果当前没有事务,就抛出异常。 | +| PROPAGATION_REQUIRES_NEW(required_new) | 表示新建事务,如果当前存在事务,把当前事务挂起。 | | PROPAGATION_NOT_SUPPORTED(not_support) | 表示以无事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | -| PROPAGATION_NEVER(never) | 表示以无事务方式执行,如果当前存在事务,则抛出异常。 | -| PROPAGATION_NESTED(nested) | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 | - +| PROPAGATION_NEVER(never) | 表示以无事务方式执行,如果当前存在事务,则抛出异常。 | +| PROPAGATION_NESTED(nested) | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 | +### 2、隔离级别 -## 1.3 事务隔离级别 +事务隔离级别是指多个事务之间的隔离程度。 -事务隔离级别是指多个事务之间的隔离程度,其中TransactionDefinition 接口中定义了五个表示隔离级别的常量: +TransactionDefinition 接口中定义了五个表示隔离级别的常量: -| 隔离级别 | 说明 | -| -------------------------------------- | ------------------------------------------------------------ | -| ISOLATION_DEFAULT(默认) | 这是默认值,表示使用底层数据库的默认隔离级别。 | +| 隔离级别 | 说明 | +| :------------------------------------: | :----------------------------------------------------------: | +| ISOLATION_DEFAULT(默认) | 这是默认值,表示使用底层数据库的默认隔离级别。 | | ISOLATION_READ_UNCOMMITTED(读未提交) | 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,该级别不能防止脏读和不可重复读,因此很少使用该隔离级别 | -| ISOLATION_READ_COMMITTED(读可提交) | 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 | +| ISOLATION_READ_COMMITTED(读可提交) | 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 | | ISOLATION_REPEATABLE_READ(可重复读) | 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读 | -| ISOLATION_SERIALIZABLE(可串行化) | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是,这将严重影响程序的性能,通常情况下也不会用到该级别 | +| ISOLATION_SERIALIZABLE(可串行化) | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是,这将严重影响程序的性能,通常情况下也不会用到该级别 | + +### 3、是否只读事务 + +如果事务只对后端的数据库进行操作,数据库可以利用事务的只读特性来进行一些特定的优化。 + +### 4、事务超时 + +为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。 + +事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自然回滚,而不是一直等待其结束。 + +### 5、回滚规则 + +这些规则定义了哪些异常会导致事务回滚哪些不会。 + +默认情况下,事务只有遇到运行期异常事时才回滚,而在遇到检查型异常时不会回滚,但是我们可以声明事务在遇到特殊性的检查型异常时像遇到运行期异常那样回滚。同样,我们也可以声明遇到特定的异常不回滚,及时这些异常时运行期异常。 -> 这里和数据库中的隔离级别的概念是相通的,可参考[Java-Notes]() +## Spring事务管理 -# 二、声明式事务模型 +Spring 的事务有两种类型: + +- 全局事务(分布式事务) +- 局部事务(单机事务) + +我们平时常用的是单机事务,也就是操作一个数据库的事务。 + +按照使用方式来分,又可以分为编程式事务(TransactionTemplate)和声明式事务(@Transactional注解): + +- 编程式事务 +- 声明式事务 + +### 编程式事务 + +编程式事务指的是通过编码方式实现事务,类似 JDBC 编程实现事务管理。 + +### 声明式事务 + +声明式事务实现方式: + +- XML 实现 +- @Transactional 注解实现 常使用的 @Transactional 注解方式来使用事务,比编程式方式方便得多。 -主要是利用了[AOP原理]()来实现了这个功能,对于@Transactional 修饰的方法,Spring会为其创建一个代理对象,这个代理对象中就会有事务开启、提交、回滚等逻辑。 +声明式事务主要是利用了[AOP 原理](https://duhouan.github.io/Java-Notes/#/./Spring/02SpringAOP)来实现了这个功能,对于 @Transactional 修饰的方法,Spring 会为其创建一个**代理对象**,这个代理对象中就会有事务开启、提交、回滚等逻辑。 -Spring中,事务对应的Advisor是BeanFactoryTransactionAttributeSourceAdvisor,它的adivce是TransactionInterceptor,pointcut是TransactionAttributeSourcePointcut,其作用是判断方法是否被@Transactional 注解修饰。 +### 编程式和声明式的对比 +| 类型 | 优点 | 缺点 | +| :--------: | :------------------: | :------------------------------: | +| 编程式事务 | 显式调用,不易出错 | 侵入式代码,编码量大 | +| 声明式事务 | 简洁,对代码侵入性小 | 隐藏了实现细节,出现问题不易定位 | +### Spring 事务管理 -# 三、编程式和声明式的对比 +Spring 对事务管理是通过**事务管理器**实现的,Spring 提供了许多内置事务管理器实现。 -| 类型 | 优点 | 缺点 | -| -------------- | -------------------- | ---------------------------------------------- | -| 编程式事务模型 | 显式调用,不易出错 | 侵入式代码,编码量大 | -| 声明式事务模型 | 简洁,对代码侵入性小 | 隐藏了实现细节,出现问题不易定位,容易导致误用 | +Spring 事务管理器的接口是 PlatformTransactionManager,我们在使用的时候,只需要通过 Spring IoC,告诉Spring,要注入哪个 TransactionManager,要使用哪种策略即可。 +具体的事务管理机制对 Spring 来说是透明的,而具体的事务管理机制是各个平台(如 JDBC 等)自己处理。 +Spring 事务管理机制的提个优点就是为不同事务的 API 提供一致的编程模型。 -# 四、补充资料 +## 补充资料 - [使用方式](https://juejin.im/post/5b010f27518825426539ba38) @@ -122,9 +116,11 @@ Spring中,事务对应的Advisor是BeanFactoryTransactionAttributeSourceAdviso # 参考资料 - +https://zhuanlan.zhihu.com/p/38772486 + +https://zhuanlan.zhihu.com/p/41864893 - +https://segmentfault.com/a/1190000013341344 - +- [全面分析 Spring 的编程式事务管理及声明式事务管理](https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html) diff --git "a/docs/Spring/05Spring344円270円255円Bean347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" "b/docs/Spring/05Spring344円270円255円Bean347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" index 024f5a2..8d42d52 100644 --- "a/docs/Spring/05Spring344円270円255円Bean347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" +++ "b/docs/Spring/05Spring344円270円255円Bean347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" @@ -1,49 +1,33 @@ - -* [Spring中Bean的生命周期](#Spring中Bean的生命周期) - * [Bean的作用域](#Bean的作用域) - * [Bean的生命周期](#Bean的生命周期) - * [initialize和destroy](#initialize和destroy) - * [XxxAware接口](#XxxAware接口) - * [BeanPostProcessor](#BeanPostProcessor) - * [总结](#总结) - - -# Spring中Bean的生命周期 -Spring 中,组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。 -简单地讲,Bean 就是由 IOC 容器初始化、装配及管理的对象。 -Bean 的定义以及 Bean 相互间的依赖关系通过**配置元数据**来描述。 - -Spring中的Bean默认都是**单例**的, -这些单例Bean在多线程程序下如何保证线程安全呢? -例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求, -引入Spring框架之后,**每个Action都是单例的**, -那么对于Spring托管的单例Service Bean,如何保证其安全呢? -Spring使用ThreadLocal解决线程安全问题(为每一个线程都提供了一份变量,因此可以同时访问而互不影响)。 +# Spring 中 Bean 的生命周期 + +Spring 中,组成应用程序的主体及由 Spring IoC 容器所管理的对象,被称之为 Bean。 +简单地讲,Bean 就是由 IoC 容器初始化、装配及管理的对象,Bean 的定义以及 Bean 相互间的依赖关系通过**配置元数据**来描述。 + +Spring 中的 Bean 默认都是**单例**的,这些单例 Bean 在多线程程序下如何保证线程安全呢? +例如对于 Web 应用来说,Web 容器对于每个用户请求都创建一个单独的 Sevlet 线程来处理请求, +引入 Spring 框架之后,**每个 Action 都是单例的**,那么对于 Spring 托管的单例 Service Bean,如何保证其安全呢? Spring 使用 ThreadLocal 解决线程安全问题(为每一个线程都提供了一份变量,因此可以同时访问而互不影响)。 Spring的单例是**基于BeanFactory**也就是Spring容器的,单例Bean在此容器内只有一个, **Java的单例是基于 JVM,每个 JVM 内只有一个实例**。 -## Bean的作用域 +## Bean 的作用域 -Spring Framework支持五种作用域: +Spring Framework 支持五种作用域: | 类别 | 说明 | | :--:| :--: | -| singleton | 在SpringIOC容器中仅存在一个Bean实例,Bean以单例方式存在 | -| prototype | 每次从容器中调用Bean时,都返回一个新的实例 | -| request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 | -| session | 同一个Http Session共享一个Bean,不同Session使用不同Bean,仅适用于WebApplicationContext环境 | -| globalSession | 一般同于Portlet应用环境,该作用域仅适用于WebApplicationContext环境 | +| singleton | 在 SpringIoC 容器中仅存在一个 Bean 实例,Bean 以单例方式存在 | +| prototype | 每次从容器中调用 Bean 时,都返回一个新的实例 | +| request | 每次HTTP请求都会创建一个新的 Bean,该作用域仅适用于 WebApplicationContext 环境 | +| session | 同一个 Http Session 共享一个 Bean,不同 Session 使用不同 Bean,仅适用于WebApplicationContext 环境 | +| globalSession | 一般同于 Portlet 应用环境,该作用域仅适用于 WebApplicationContext 环境 | -注意:五种作用域中, -request、session 和 global session -三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架), -只能用在基于 web 的 Spring ApplicationContext 环境。 +注意:五种作用域中,request、session 和 global session 三种作用域仅在基于 web 的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于 web 的 Spring ApplicationContext 环境。 ### 1. singleton 当一个 Bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个**共享的 Bean 实例**, 并且所有对 Bean 的请求,只要 id 与该 Bean 定义相匹配,则只会**返回 Bean 的同一实例**。 - + singleton 是单例类型(对应于单例模式),就是**在创建容器时就同时自动创建一个Bean对象**, 不管你是否使用,但我们可以指定Bean节点的 lazy-init="true" 来延迟初始化Bean, 这时候,只有在第一次获取Bean时才会初始化Bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。 @@ -64,7 +48,7 @@ public class ServiceImpl{ } ``` -#### 2. prototype +### 2. prototype 当一个Bean的作用域为 prototype,表示一个 Bean 定义对应多个对象实例。 prototype 作用域的 Bean 会导致在每次对该 Bean 请求 @@ -99,7 +83,7 @@ public class ServiceImpl{ request只适用于**Web程序**,每一次 HTTP 请求都会产生一个新的 Bean , 同时该 Bean 仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。 - + 在 XML 中将 bean 定义成 request ,可以这样配置: - 配置文件XML中将 Bean 定义成 prototype : @@ -135,39 +119,51 @@ Portlet 规范定义了全局 Session 的概念, ``` -## Bean的生命周期 +## Bean 的生命周期 -Spring容器在创建、初始化和销毁Bean的过程中做了哪些事情: +### Bean 的创建过程 -```java -Spring容器初始化 -===================================== -调用GiraffeService无参构造函数 -GiraffeService中利用set方法设置属性值 -调用setBeanName:: Bean Name defined in context=giraffeService -调用setBeanClassLoader,ClassLoader Name = sun.misc.Launcher$AppClassLoader -调用setBeanFactory,setBeanFactory:: giraffe bean singleton=true -调用setEnvironment -调用setResourceLoader:: Resource File Name=spring-beans.xml -调用setApplicationEventPublisher -调用setApplicationContext:: Bean Definition Names=[giraffeService, org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#0, com.giraffe.spring.service.GiraffeServicePostProcessor#0] -执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=giraffeService -调用PostConstruct注解标注的方法 -执行InitializingBean接口的afterPropertiesSet方法 -执行配置的init-method -执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=giraffeService -Spring容器初始化完毕 -===================================== -从容器中获取Bean -giraffe Name=张三 -===================================== -调用preDestroy注解标注的方法 -执行DisposableBean接口的destroy方法 -执行配置的destroy-method -Spring容器关闭 -``` + + +### Bean 的销毁过程 + +- 若实现了 DisposableBean 接口,则会调用 destroy 方法 + +- 若配置了 destroy-method 属性,则会调用其配置的销毁方法 + +### Bean 的详细生命周期 + +1、Spring 对 Bean 进行实例化。 + +2、Spring 将值和 Bean 的引用注入到 Bean 对应的属性中。 + +3、如果 Bean 实现了 BeanNameAware 接口,Spring 将 bean 的 id 传递给 setBeanName() 接口方法。 + +4、如果 Bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanFactory() 接口方法,将 BeanFactory 容器实例传入。 + +5、如果 Bean 实现了 ApplicationContextAware 接口,Spring 将调用 setApplicationContext() 接口方法,将应用上下文的引用传入。 + +6、如果 Bean 实现了BeanPostProcessor 接口,Spring 将调用 postProcessBeforeInitialization() 接口方法。 -### initialize和destroy +7、如果 Bean 实现了InitializingBean 接口,Spring 将调用他们的 afterPropertiesSet() 接口方法,类似地,如果Bean 实现了 init-method 声明了初始化方法,该方法也会被调用。 + +8、如果 Bean 实现了BeanPostProcessor 接口,Spring 将调用 postProcessAfterInitialization() 接口方法。 + +9、此时 Bean 已经准备就绪,可以被应用程序使用了,他们将一一直驻留在应用上下文中,一直到该应用上下文被销毁。 + +10、如果 Bean 实现了 DisposableBean 接口,Spring 将调用它的 destroy() 接口方法。同样,如果 Bean 使用 destroy-method 声明了销毁方法,方法也会被调用。 + + + + + + + + + +### 实践 + +#### initialize 和 destroy 有时我们需要在Bean属性值设置好之后和Bean销毁之前做一些事情, 比如检查Bean中某个属性是否被正常的设置好了。 @@ -260,7 +256,7 @@ public class GiraffeService { } ``` -### XxxAware接口 +#### XxxAware接口 有些时候我们需要在 Bean 的初始化中**使用 Spring 框架自身的一些对象**来执行一些操作, 比如获取 ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。 @@ -332,7 +328,8 @@ public class GiraffeService implements ApplicationContextAware, } ``` -### BeanPostProcessor +#### BeanPostProcessor + 上面的XxxAware接口是**针对某个实现这些接口的Bean定制初始化的过程**, Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程, 只需提供一个实现BeanPostProcessor接口的类即可。 @@ -375,65 +372,12 @@ public class CustomerBeanPostProcessor implements BeanPostProcessor { ``` -### 总结 -Spring Bean的生命周期 - -1. Bean容器找到配置文件中 Spring Bean 的定义。 +## 注意 -2. Bean容器利用Java Reflection API创建一个Bean的实例。 - -3. 如果涉及到一些属性值,利用set方法设置一些属性值。 - -4. 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 - -5. 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - -6. 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - -7. 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 - -8. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, -执行postProcessBeforeInitialization()方法 - -9. 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 - -10. 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 - -11. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, -执行postProcessAfterInitialization()方法 - -12. 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法; -如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 - - - - - -很多时候我们并不会真的去实现上面说描述的那些接口, -那么下面我们就除去那些接口,针对 Bean 的单例和非单例来描述下 Bean 的生命周期: - -> **1. 单例管理的对象** - -scope="singleton",即默认情况下,会在启动容器时(即实例化容器时)时实例化。 -但我们可以指定 lazy-init="true"。这时候,只有在第一次获取 Bean 时才会初始化 Bean, -即第一次请求该 Bean时才初始化。配置如下: - -```html - -``` - -想对所有的默认单例Bean都应用延迟初始化, -可以在根节点beans 指定default-lazy-init="true",如下所示: - -```html - -``` - -默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。 -在创建对象的时候先调用**构造器**,然后调用 **init-method 属性值中所指定的方法**。 -对象在被销毁的时候,会调用 **destroy-method 属性值中所指定的方法**。 +**Spring 容器可以管理 singleton 作用域下 Bean 的生命周期,在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给了客户端的代码管理,Spring 容器将不再跟踪其生命周期,并且不会管理那些被配置成 prototype 作用域的 Bean 的生命周期**。 - LifeCycleBean : + ```java public class LifeCycleBean { private String name; @@ -461,6 +405,7 @@ public class LifeCycleBean { ``` - 配置文件 : + ```html @@ -501,6 +446,7 @@ Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一 这个类型的对象有很多个,**Spring容器一旦把这个对象交给你之后,就不再管理这个对象了**。 - 配置文件 : + ```html @@ -525,6 +471,7 @@ public class LifeTest2 { ``` - 输出结果: + ```html LifeBean()构造函数 this is init of lifeBean @@ -537,9 +484,4 @@ this is destory of lifeBean LifeCycleBean@573f2bb1 作用域为 prototype 的 Bean ,其destroy方法并没有被调用。 如果 bean 的 scope 设为prototype时,当容器关闭时,destroy 方法不会被调用。 -对于 prototype 作用域的 bean,有一点非常重要, - -**Spring 容器可以管理 singleton 作用域下 Bean 的生命周期, -在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。 -而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给了客户端的代码管理, -Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的Bean的生命周期**。 \ No newline at end of file +对于 prototype 作用域的 bean,有一点非常重要, \ No newline at end of file diff --git a/docs/Spring/Spring AOP.md b/docs/Spring/Spring AOP.md deleted file mode 100644 index 05a5326..0000000 --- a/docs/Spring/Spring AOP.md +++ /dev/null @@ -1,776 +0,0 @@ - -* [SpringAOP](#SpringAOP) - * [SpringAOP概述](#SpringAOP概述) - * [AOP的底层实现](#AOP的底层实现) - * [Spring中的AOP](#Spring中的AOP) - * [Spring的AspectJ的AOP](#Spring的AspectJ的AOP) - - -# SpringAOP - -## SpringAOP概述 -### 什么是AOP -AOP(面向切面编程,Aspect Oriented Programing)。 - -AOP采取**横向抽取**机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存) - -Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类**织入**增强代码 - -AspecJ是一个基于Java语言的AOP框架,Spring2.0开始,Spring AOP引入对AspectJ的支持, -AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。 - -AOP底层原理就是**代理机制**。 - -> 关注点分离:不同的问题交给不同部分去解决 - -### Spring的AOP代理 -- JDK动态代理:对实现了接口的类生成代理 -- CGLib代理机制:对类生成代理 - -### AOP的术语 - -| 术语 | 描述 | -| :--: | :--: | -| Joinpoint(连接点) | 所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是**方法**,因为Spring只支持**方法类型的连接点**。 | -| Pointcut(切入点) | 所谓切入点是指我们要**对哪些Joinpoint进行拦截**的定义。 | -| Advice(通知/增强) | 所谓通知是指拦截到Joinpoint之后所要做的事情就是**通知**。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能) | -| Introduction(引介) | 引介是一种**特殊的通知**在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field | -| Target(目标对象) | 代理的目标对象 | -| Weaving(织入) | 是指把增强应用到目标对象来创建新的代理对象的过程。有三种织入方式: Spring采用**动态代理织入**,而AspectJ采用**编译期织入**和**类装载期织入** | -| Proxy(代理)| 一个类被AOP织入增强后,就产生一个结果代理类 | -| Aspect(切面) | 是切入点和通知(/引介)的结合 | - -- 示例: - - - -## AOP的底层实现 -### JDK动态代理 -JDK动态代理:对**实现了接口的类**生成代理 - -```java -public interface IUserDao { - void add(); - void delete(); - void update(); - void search(); -} -``` -- UserDao实现了IUserDao接口 -```java -public class UserDao implements IUserDao{ - @Override - public void add() { - System.out.println("添加功能"); - } - - @Override - public void delete() { - System.out.println("删除功能"); - } - - @Override - public void update() { - System.out.println("更新功能"); - } - - @Override - public void search() { - System.out.println("查找功能"); - } -} -``` - -```java -public class JdkProxy implements InvocationHandler{ - private IUserDao iUserDao; - - public JdkProxy(IUserDao iUserDao){ - this.iUserDao=iUserDao; - } - - public IUserDao getPrxoy(){ - return (IUserDao)Proxy.newProxyInstance( - iUserDao.getClass().getClassLoader(), - iUserDao.getClass().getInterfaces(), - this - ); - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Object obj = null; - //只对add方法进行增强 - if ("add".equals(methodName)) { - System.out.println("开启事务"); - obj = method.invoke(iUserDao, args); - System.out.println("结束事务"); - } else { - obj = method.invoke(iUserDao, args); - } - return obj; - } -} -``` -- 对实现了接口的类UserDao生成代理 -```java -public class JdkProxyDemo { - public static void main(String[] args) { - IUserDao userDao=new UserDao(); - JdkProxy jdkProxy=new JdkProxy(userDao); - IUserDao userDao2=jdkProxy.getPrxoy(); - userDao2.add(); - } -} -``` -- 输出结果 -```html -开启事务 -添加功能 -结束事务 -``` - -### Cglib动态代理 -以**继承的方式**动态生成目标类的代理。 - -CGLIB(Code Generation Library)是一个开源项目! -是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。 - -```java -public class ProductDao { - public void add() { - System.out.println("添加功能"); - } - - public void delete() { - System.out.println("删除功能"); - } - - public void update() { - System.out.println("更新功能"); - } - - public void search() { - System.out.println("查找功能"); - } -} -``` - -```java -public class CGLibProxy implements MethodInterceptor { - private ProductDao productDao; - - public CGLibProxy(ProductDao productDao) { - this.productDao = productDao; - } - - public ProductDao getProxy(){ - // 使用CGLIB生成代理: - // 1.创建核心类: - Enhancer enhancer = new Enhancer(); - // 2.为其设置父类: - enhancer.setSuperclass(productDao.getClass()); - // 3.设置回调: - enhancer.setCallback(this); - // 4.创建代理: - return (ProductDao) enhancer.create(); - } - - @Override - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - String menthodName=method.getName(); - Object obj=null; - if("add".equals(menthodName)){ - System.out.println("开启事务"); - obj=methodProxy.invokeSuper(proxy,args); - System.out.println("结束事务"); - }else{ - obj=methodProxy.invokeSuper(proxy,args); - } - return obj; - } -} -``` -```java -public class CGLibProxyDemo { - public static void main(String[] args) { - ProductDao productDao=new ProductDao(); - CGLibProxy proxy=new CGLibProxy(productDao); - ProductDao productDao2=proxy.getProxy(); - productDao2.add(); - } -} -``` -- 输出结果 -```html -开启事务 -添加功能 -结束事务 -``` - -### 总结 - -Spring框架中: - -- **如果类实现了接口,就使用JDK的动态代理生成代理对象** -- **如果这个类没有实现任何接口,使用CGLIB生成代理对象** - - -### 相关阅读 - -- [代理设计模式](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/03%E7%BB%93%E6%9E%84%E5%9E%8B.md#7-%E4%BB%A3%E7%90%86proxy) - -Spring中代理模式的实现: - -- 真实实现类的逻辑包含在getBean方法中; -- getBean方法返回的实际上是Proxy的实例; -- Proxy实例是Spring 采用JDK Proxy或CGLIB动态生成的。 - -## Spring中的AOP - -### Spring中通知 -Spring中的通知Advice其实是指"增强代码"。 - -| 通知类型 | 全类名 | 说明 | -| :--: | :--: | :--: | -| 前置通知 | org.springframework.aop.MethodBeforeAdvice | 在目标方法执行前实施增强 | -| 后置通知 | org.springframework.aop.AfterReturningAdvice | 在目标方法执行后实施增强 | -| 环绕通知 | org.aopalliance.intercept.MethodInterceptor | 在目标方法执行前后实施增强 | -| 异常抛出通知 | org.springframework.aop.ThrowsAdvice | 在方法抛出异常后实施增强 | -| 引介通知 | org.springframework.aop.IntroductionInterceptor | 在目标类中添加一些新的方法和属性 | - - -### Spring中切面类型 -Advisor : Spring中传统切面。 -- Advisor:一个切点和一个通知组合。 -- Aspect:多个切点和多个通知组合。 - -Advisor : 代表一般切面,Advice本身就是一个切面,对目标类所有方法进行拦截(不带有切点的切面.针对所有方法进行拦截) -PointcutAdvisor : 代表具有切点的切面,可以指定拦截目标类哪些方法(带有切点的切面,针对某个方法进行拦截) -IntroductionAdvisor : 代表引介切面,针对引介通知而使用切面(不要求掌握) - -### Spring的AOP的开发 - -#### 1. 不带有切点的切面(针对所有方法的增强) -> 第一步:导入相应jar包 -```html - - aopalliance - aopalliance - 1.0 - -``` - -> 第二步:编写被代理对象 - -IUserDao接口 -```java -public interface IUserDao { - void add(); - void update(); - void delete(); - void search(); -} -``` -UserDao实现类 -```java -public class UserDao implements IUserDao{ - @Override - public void add() { - System.out.println("增加功能"); - } - - @Override - public void update() { - System.out.println("修改功能"); - } - - @Override - public void delete() { - System.out.println("删除功能"); - } - - @Override - public void search() { - System.out.println("查找功能"); - } -} -``` - -> 第三步:编写增强的代码 -```java -import org.springframework.aop.MethodBeforeAdvice; -import java.lang.reflect.Method; - -/** - * 前置增强 - */ -public class MyBeforeAdvice implements MethodBeforeAdvice{ - @Override - public void before(Method method, Object[] args, Object target) throws Throwable { - System.out.println("前置增强..."); - } -} -``` - -> 第四步:生成代理(配置生成代理) - -**生成代理Spring基于ProxyFactoryBean类。底层自动选择使用JDK的动态代理还是CGLIB的代理**。 - -属性: -- target : 代理的目标对象 -- proxyInterfaces : 代理要实现的接口 -```html -如果多个接口可以使用以下格式赋值 - - - .... - -``` -- proxyTargetClass : 是否对类代理而不是接口,设置为true时,使用CGLib代理 -- interceptorNames : 需要织入目标的Advice -- singleton : 返回代理是否为单实例,默认为单例 -- optimize : 当设置为true时,强制使用CGLib - -```html - - - - - - - - - - - - - - - - - - -``` -> 测试 -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("userDaoProxy") - // 注入是真实的对象,必须注入代理对象. - IUserDao iUserDao; - - @Test - public void test(){ - iUserDao.add(); - iUserDao.delete(); - iUserDao.update(); - iUserDao.search(); - } -} -``` -- 输出结果: -```html -前置增强... -增加功能 -前置增强... -删除功能 -前置增强... -修改功能 -前置增强... -查找功能 -``` - -#### 2. 带有切点的切面(针对目标对象的某些方法进行增强) -PointcutAdvisor 接口: -- DefaultPointcutAdvisor 最常用的切面类型,它可以通过任意Pointcut和Advice组合定义切面 -- RegexpMethodPointcutAdvisor 构造正则表达式切点切面 - -> 第一步:创建被代理对象 -```java -public class OrderDao { - public void add() { - System.out.println("增加功能"); - } - - public void update() { - System.out.println("修改功能"); - } - - public void delete() { - System.out.println("删除功能"); - } - - public void search() { - System.out.println("查找功能"); - } -} -``` - -> 第二步:编写增强的代码 - -```java -/** - * 环绕增强 - */ -public class MyAroundAdvice implements MethodInterceptor { - @Override - public Object invoke(MethodInvocation invocation) throws Throwable { - System.out.println("环绕前增强..."); - Object obj= invocation.proceed(); - System.out.println("环绕后增强..."); - return obj; - } -} -``` - -> 第三步:生成代理(配置生成代理) - -```html - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -> 测试 -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("orderDaoProxy") - OrderDao orderDao; - - @Test - public void test(){ - orderDao.add(); - orderDao.delete(); - orderDao.update(); - orderDao.search(); - } -} -``` - -- 输出结果: -```html -环绕前增强... -增加功能 -环绕后增强... -删除功能 -修改功能 -环绕前增强... -查找功能 -环绕后增强... -``` - -#### 3. 自动代理 -前面的案例中,每个代理都是通过ProxyFactoryBean织入切面代理, -在实际开发中,非常多的**Bean每个都配置ProxyFactoryBean开发维护量巨大**。 - -自动创建代理(**基于后处理Bean。在Bean创建的过程中完成的增强。生成Bean就是代理**。) -- BeanNameAutoProxyCreator 根据Bean名称创建代理 -- DefaultAdvisorAutoProxyCreator 根据Advisor本身包含信息创建代理。 -其中AnnotationAwareAspectJAutoProxyCreator是基于Bean中的AspectJ 注解进行自动代理。 - -**BeanNameAutoProxyCreator(按名称生成代理)** - -```html - - - - - - - - - - - - - - - -``` - -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("userDao") - UserDao userDao; - //代理的是实例类,不是接口。 - - @Autowired - @Qualifier("orderDao") - OrderDao orderDao; - - @Test - public void test(){ - userDao.add(); - userDao.search(); - - orderDao.add(); - orderDao.search(); - } -} -``` - -**DefaultAdvisorAutoProxyCreator(根据切面中定义的信息生成代理)** - -```html - - - - - - - - - - - - - - - - - .*add.* - .*search.* - - - - - - - - - - - -``` - -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("userDao") - IUserDao userDao; - - @Autowired - @Qualifier("orderDao") - OrderDao orderDao; - - @Test - public void test(){ - userDao.add(); - userDao.search(); - - orderDao.add(); - orderDao.search(); - } -} -``` - -### 区分基于ProxyFattoryBean的代理与自动代理区别? - -- ProxyFactoryBean:先有被代理对象,将被代理对象传入到代理类中生成代理。 - -- 自动代理基于后处理Bean。**在Bean的生成过程中,就产生了代理对象**,把代理对象返回。 -生成的Bean已经是代理对象。 - -## Spring的AspectJ的AOP - -### 基于XML -> 第一步:编写被增强的类 - -UserDao - -> 第二步:定义切面 - -```java -public class MyAspectXML { - public void before(){ - System.out.println("前置通知..."); - } - - public void afterReturing(Object returnVal){ - System.out.println("后置通知...返回值:"+returnVal); - } - - public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ - System.out.println("环绕前增强...."); - Object result = proceedingJoinPoint.proceed(); - System.out.println("环绕后增强...."); - return result; - } - - public void afterThrowing(Throwable e){ - System.out.println("异常通知..."+e.getMessage()); - } - - public void after(){ - System.out.println("最终通知...."); - } -} -``` - -> 第三步:配置applicationContext.xml - -```html - - - - - - - - - - - - - - - - - - - - - - - -``` - -- 测试 -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("userDao") - IUserDao userDao; - - @Test - public void test(){ - userDao.add(); - userDao.search(); - } -} -``` - -### 基于注解 -AspectJ的通知类型: - -| 通知类型 | 解释 | 说明 | -| :--:| :--: | :--: | -| @Before | 前置通知,相当于BeforeAdvice | 就在方法之前执行。没有办法阻止目标方法执行的。 | -| @AfterReturning | 后置通知,相当于AfterReturningAdvice | 后置通知,获得方法返回值。 | -| @Around | 环绕通知,相当于MethodInterceptor | 在可以方法之前和之后来执行的,而且可以阻止目标方法的执行。 | -| @AfterThrowing | 抛出通知,相当于ThrowAdvice | | -| @After | 最终final通知,不管是否异常,该通知都会执行 | | -| @DeclareParents | 引介通知,相当于IntroductionInterceptor | | - - -**Advisor和Aspect的区别**? -- Advisor:Spring传统意义上的切面,支持一个切点和一个通知的组合。 -- Aspect:可以支持多个切点和多个通知的组合。 - -> 使用注解编写切面类 -```java -/** - * 切面类:就是切点与增强结合 - */ -@Aspect //申明是切面 -public class MyAspect{ - @Before("execution(* advice.UserDao.add(..))") - public void before(JoinPoint joinPoint){ - System.out.println("前置增强...."+joinPoint); - } - - @AfterReturning(value="execution(* advice.UserDao.update(..))",returning="returnVal") - public void afterReturin(Object returnVal){ - System.out.println("后置增强....方法的返回值:"+returnVal); - } - - @Around(value="MyAspect.myPointcut()") - public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ - System.out.println("环绕前增强...."); - Object obj = proceedingJoinPoint.proceed(); - System.out.println("环绕后增强...."); - return obj; - } - - @AfterThrowing(value="MyAspect.myPointcut()",throwing="e") - public void afterThrowing(Throwable e){ - System.out.println("不好了 出异常了!!!"+e.getMessage()); - } - - @After("MyAspect.myPointcut()") - public void after(){ - System.out.println("最终通知..."); - } - - //定义切点 - @Pointcut("execution(* advice.UserDao.search(..))") - private void myPointcut(){} -} -``` -> 配置applicationContext.xml -```html - - - - - - - - -``` - -- 测试: -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration("classpath:applicationContext.xml") -public class SpringTest { - @Autowired - @Qualifier("userDao") - IUserDao userDao; - - @Test - public void test(){ - userDao.add(); - userDao.search(); - } -} -``` \ No newline at end of file diff --git "a/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" "b/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" deleted file mode 100644 index 024f5a2..0000000 --- "a/docs/Spring/Spring 344円270円255円 Bean 347円232円204円347円224円237円345円221円275円345円221円250円346円234円237円.md" +++ /dev/null @@ -1,545 +0,0 @@ - -* [Spring中Bean的生命周期](#Spring中Bean的生命周期) - * [Bean的作用域](#Bean的作用域) - * [Bean的生命周期](#Bean的生命周期) - * [initialize和destroy](#initialize和destroy) - * [XxxAware接口](#XxxAware接口) - * [BeanPostProcessor](#BeanPostProcessor) - * [总结](#总结) - - -# Spring中Bean的生命周期 -Spring 中,组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。 -简单地讲,Bean 就是由 IOC 容器初始化、装配及管理的对象。 -Bean 的定义以及 Bean 相互间的依赖关系通过**配置元数据**来描述。 - -Spring中的Bean默认都是**单例**的, -这些单例Bean在多线程程序下如何保证线程安全呢? -例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求, -引入Spring框架之后,**每个Action都是单例的**, -那么对于Spring托管的单例Service Bean,如何保证其安全呢? -Spring使用ThreadLocal解决线程安全问题(为每一个线程都提供了一份变量,因此可以同时访问而互不影响)。 -Spring的单例是**基于BeanFactory**也就是Spring容器的,单例Bean在此容器内只有一个, -**Java的单例是基于 JVM,每个 JVM 内只有一个实例**。 - -## Bean的作用域 - -Spring Framework支持五种作用域: - -| 类别 | 说明 | -| :--:| :--: | -| singleton | 在SpringIOC容器中仅存在一个Bean实例,Bean以单例方式存在 | -| prototype | 每次从容器中调用Bean时,都返回一个新的实例 | -| request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 | -| session | 同一个Http Session共享一个Bean,不同Session使用不同Bean,仅适用于WebApplicationContext环境 | -| globalSession | 一般同于Portlet应用环境,该作用域仅适用于WebApplicationContext环境 | - -注意:五种作用域中, -request、session 和 global session -三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架), -只能用在基于 web 的 Spring ApplicationContext 环境。 - -### 1. singleton - -当一个 Bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个**共享的 Bean 实例**, -并且所有对 Bean 的请求,只要 id 与该 Bean 定义相匹配,则只会**返回 Bean 的同一实例**。 - -singleton 是单例类型(对应于单例模式),就是**在创建容器时就同时自动创建一个Bean对象**, -不管你是否使用,但我们可以指定Bean节点的 lazy-init="true" 来延迟初始化Bean, -这时候,只有在第一次获取Bean时才会初始化Bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。 -注意,singleton 作用域是Spring中的**缺省作用域**。 - -- 配置文件XML中将 Bean 定义成 singleton : -```html - -``` - -- @Scope 注解的方式: - -```java -@Service -@Scope("singleton") -public class ServiceImpl{ - -} -``` - -#### 2. prototype - -当一个Bean的作用域为 prototype,表示一个 Bean 定义对应多个对象实例。 -prototype 作用域的 Bean 会导致在每次对该 Bean 请求 -(将其注入到另一个 Bean 中,或者以程序的方式调用容器的 getBean() 方法)时都会创建一个新的 bean 实例。 -prototype 是原型类型,它在我们创建容器的时候并没有实例化, -而是当我们获取Bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。 - -根据经验,**对有状态的 Bean 应该使用 prototype 作用域,而对无状态的 Bean 则应该使用 singleton 作用域。** - -- 配置文件XML中将 Bean 定义成 prototype : - -```html - -``` -或者 - -```html - -``` - -- @Scope 注解的方式: - -```java -@Service -@Scope("prototype") -public class ServiceImpl{ - -} -``` - -### 3. request - -request只适用于**Web程序**,每一次 HTTP 请求都会产生一个新的 Bean , -同时该 Bean 仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。 - -在 XML 中将 bean 定义成 request ,可以这样配置: - -- 配置文件XML中将 Bean 定义成 prototype : - -```html - -``` - - -### 4. session - -session只适用于**Web程序**, -session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 Bean, -同时**该 Bean 仅在当前 HTTP session 内有效**。 -与request作用域一样,可以根据需要放心的更改所创建实例的内部状态, -而别的 HTTP session 中根据 userPreferences 创建的实例, -将不会看到这些特定于某个 HTTP session 的状态变化。 -当HTTP session最终被废弃的时候,在该HTTP session作用域内的bean也会被废弃掉。 - -```html - -``` - -### 5. globalSession - -globalSession 作用域**类似于标准的 HTTP session** 作用域, -不过仅仅在基于 portlet 的 Web 应用中才有意义。 -Portlet 规范定义了全局 Session 的概念, -它被所有构成某个 portlet web 应用的各种不同的 portlet所共享。 -在globalSession 作用域中定义的 bean 被限定于全局portlet Session的生命周期范围内。 - -```html - -``` - -## Bean的生命周期 - -Spring容器在创建、初始化和销毁Bean的过程中做了哪些事情: - -```java -Spring容器初始化 -===================================== -调用GiraffeService无参构造函数 -GiraffeService中利用set方法设置属性值 -调用setBeanName:: Bean Name defined in context=giraffeService -调用setBeanClassLoader,ClassLoader Name = sun.misc.Launcher$AppClassLoader -调用setBeanFactory,setBeanFactory:: giraffe bean singleton=true -调用setEnvironment -调用setResourceLoader:: Resource File Name=spring-beans.xml -调用setApplicationEventPublisher -调用setApplicationContext:: Bean Definition Names=[giraffeService, org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#0, com.giraffe.spring.service.GiraffeServicePostProcessor#0] -执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=giraffeService -调用PostConstruct注解标注的方法 -执行InitializingBean接口的afterPropertiesSet方法 -执行配置的init-method -执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=giraffeService -Spring容器初始化完毕 -===================================== -从容器中获取Bean -giraffe Name=张三 -===================================== -调用preDestroy注解标注的方法 -执行DisposableBean接口的destroy方法 -执行配置的destroy-method -Spring容器关闭 -``` - -### initialize和destroy - -有时我们需要在Bean属性值设置好之后和Bean销毁之前做一些事情, -比如检查Bean中某个属性是否被正常的设置好了。 -Spring框架提供了多种方法让我们可以在Spring Bean的生命周期中执行initialization和pre-destroy方法。 - -> InitializingBean 和 DisposableBean 接口 - -- InitializingBean 接口 : -```java -public interface InitializingBean { - void afterPropertiesSet() throws Exception; -} -``` - -- DisposableBean 接口 : -```java -public interface DisposableBean { - void destroy() throws Exception; -} -``` - -实现 InitializingBean 接口的afterPropertiesSet()方法可以在**Bean属性值设置好之后做一些操作**, -实现DisposableBean接口的destroy()方法可以在**销毁Bean之前做一些操作**。 - -```java -public class GiraffeService implements InitializingBean,DisposableBean { - @Override - public void afterPropertiesSet() throws Exception { - System.out.println("执行InitializingBean接口的afterPropertiesSet方法"); - } - @Override - public void destroy() throws Exception { - System.out.println("执行DisposableBean接口的destroy方法"); - } -} -``` -这种方法比较简单,但是不建议使用。 -因为这样会**将Bean的实现和Spring框架耦合在一起**。 - -> init-method 和 destroy-method 方法 - -配置文件中的配值init-method 和 destroy-method : - -```html - - -``` - -```java -public class GiraffeService { - //通过的destroy-method属性指定的销毁方法 - public void destroyMethod() throws Exception { - System.out.println("执行配置的destroy-method"); - } - //通过的init-method属性指定的初始化方法 - public void initMethod() throws Exception { - System.out.println("执行配置的init-method"); - } -} -``` - -需要注意的是自定义的init-method和post-method方法**可以抛异常但是不能有参数**。 - -这种方式比较推荐,因为可以**自己创建方法,无需将Bean的实现直接依赖于Spring的框架**。 - -> @PostConstruct 和 @PreDestroy注解 - -Spring 支持用 @PostConstruct和 @PreDestroy注解来指定 init 和 destroy 方法。 -这两个注解均在javax.annotation 包中。 -为了注解可以生效, -需要在配置文件中定义 -org.springframework.context.annotation.CommonAnnotationBeanPostProcessor。 - -```html - -``` - -```java -public class GiraffeService { - @PostConstruct - public void initPostConstruct(){ - System.out.println("执行PostConstruct注解标注的方法"); - } - @PreDestroy - public void preDestroy(){ - System.out.println("执行preDestroy注解标注的方法"); - } -} -``` - -### XxxAware接口 - -有些时候我们需要在 Bean 的初始化中**使用 Spring 框架自身的一些对象**来执行一些操作, -比如获取 ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。 -为了让 Bean 可以获取到框架自身的一些对象,Spring 提供了一组名为 XxxAware 的接口。 - -XxxAware接口均继承于org.springframework.beans.factory.Aware标记接口, -并提供一个将由 Bean 实现的set*方法, -Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。 - -常见的 XxxAware 接口: - -| 接口 | 说明 | -| :--: | :--: | -| ApplicationContextAware | 获得ApplicationContext对象,可以用来获取所有BeanDefinition的名字。 | -| BeanFactoryAware | 获得BeanFactory对象,可以用来检测Bean的作用域。 | -| BeanNameAware | 获得Bean在配置文件中定义的name。 | -| ResourceLoaderAware | 获得ResourceLoader对象,可以获得classpath中某个文件。 | -| ServletContextAware | 在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。 | -| ServletConfigAware | 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。 | - -```java -public class GiraffeService implements ApplicationContextAware, - ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware, - BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware{ - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - System.out.println("执行setBeanClassLoader,ClassLoader Name = " + classLoader.getClass().getName()); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - System.out.println("执行setBeanFactory,setBeanFactory:: giraffe bean singleton=" + beanFactory.isSingleton("giraffeService")); - } - - @Override - public void setBeanName(String s) { - System.out.println("执行setBeanName:: Bean Name defined in context=" - + s); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - System.out.println("执行setApplicationContext:: Bean Definition Names=" - + Arrays.toString(applicationContext.getBeanDefinitionNames())); - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - System.out.println("执行setApplicationEventPublisher"); - } - - @Override - public void setEnvironment(Environment environment) { - System.out.println("执行setEnvironment"); - } - - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - Resource resource = resourceLoader.getResource("classpath:spring-beans.xml"); - System.out.println("执行setResourceLoader:: Resource File Name=" - + resource.getFilename()); - } - - @Override - public void setImportMetadata(AnnotationMetadata annotationMetadata) { - System.out.println("执行setImportMetadata"); - } -} -``` - -### BeanPostProcessor -上面的XxxAware接口是**针对某个实现这些接口的Bean定制初始化的过程**, -Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程, -只需提供一个实现BeanPostProcessor接口的类即可。 - -- BeanPostProcessor 接口: -```java -public interface BeanPostProcessor{ - //postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行 - public abstract Object postProcessBeforeInitialization(Object obj, String s) - throws BeansException; - - //postProcessAfterInitialization方法在容器中的Bean初始化之后执行 - public abstract Object postProcessAfterInitialization(Object obj, String s) - throws BeansException; -} -``` - -```java -public class CustomerBeanPostProcessor implements BeanPostProcessor { - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) - throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" - + beanName); - return bean; - } - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) - throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=" - + beanName); - return bean; - } -} -``` - -将实现BeanPostProcessor接口的Bean像其他Bean一样定义在配置文件中: - -```html - -``` - -### 总结 -Spring Bean的生命周期 - -1. Bean容器找到配置文件中 Spring Bean 的定义。 - -2. Bean容器利用Java Reflection API创建一个Bean的实例。 - -3. 如果涉及到一些属性值,利用set方法设置一些属性值。 - -4. 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 - -5. 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - -6. 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - -7. 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 - -8. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, -执行postProcessBeforeInitialization()方法 - -9. 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 - -10. 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 - -11. 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象, -执行postProcessAfterInitialization()方法 - -12. 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法; -如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 - - - - - -很多时候我们并不会真的去实现上面说描述的那些接口, -那么下面我们就除去那些接口,针对 Bean 的单例和非单例来描述下 Bean 的生命周期: - -> **1. 单例管理的对象** - -scope="singleton",即默认情况下,会在启动容器时(即实例化容器时)时实例化。 -但我们可以指定 lazy-init="true"。这时候,只有在第一次获取 Bean 时才会初始化 Bean, -即第一次请求该 Bean时才初始化。配置如下: - -```html - -``` - -想对所有的默认单例Bean都应用延迟初始化, -可以在根节点beans 指定default-lazy-init="true",如下所示: - -```html - -``` - -默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。 -在创建对象的时候先调用**构造器**,然后调用 **init-method 属性值中所指定的方法**。 -对象在被销毁的时候,会调用 **destroy-method 属性值中所指定的方法**。 - -- LifeCycleBean : -```java -public class LifeCycleBean { - private String name; - - public LifeCycleBean(){ - System.out.println("LifeCycleBean()构造函数"); - } - public String getName() { - return name; - } - - public void setName(String name) { - System.out.println("setName()"); - this.name = name; - } - - public void init(){ - System.out.println("this is init of lifeBean"); - } - - public void destroy(){ - System.out.println("this is destory of lifeBean " + this); - } -} -``` - -- 配置文件 : -```html - -``` - -- 测试 : - -```java -public class LifeTest { - @Test - public void test() { - AbstractApplicationContext context = - new ClassPathXmlApplicationContext("lifeCycleBean.xml"); - LifeCycleBean life = (LifeCycleBean) context.getBean("lifeCycleBean"); - System.out.println("life:"+life); - context.close(); - } -} -``` - -- 输出结果: - -```html -LifeBean()构造函数 -this is init of lifeBean -life:com.southeast.bean.LifeCycleBean@573f2bb1 -this is destory of lifeBean com.southeast.bean.LifeCycleBean@573f2bb1 -``` - -> **2. 非单例管理的对象** - -当 scope= "prototype" 时,容器也会延迟初始化 Bean, -Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 Bean 时才初始化(如调用getBean方法时)。 - -在第一次请求每一个 prototype 的 Bean 时,Spring容器都会调用其构造器创建这个对象, -然后调用init-method属性值中所指定的方法。 -对象销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例, -这个类型的对象有很多个,**Spring容器一旦把这个对象交给你之后,就不再管理这个对象了**。 - -- 配置文件 : -```html - -``` - -- 测试: - -```java -public class LifeTest2 { - @Test - public void test() { - AbstractApplicationContext context = - new ClassPathXmlApplicationContext("lifeCycleBean.xml"); - LifeCycleBean life = (LifeCycleBean) context.getBean("lifeCycleBean"); - System.out.println("life:"+life); - - LifeCycleBean life2 = (LifeCycleBean) context.getBean("lifeCycleBeans"); - System.out.println("life2:"+life2); - context.close(); - } -} -``` - -- 输出结果: -```html -LifeBean()构造函数 -this is init of lifeBean -life:com.southeast.bean.LifeCycleBean@573f2bb1 -LifeBean()构造函数 -this is init of lifeBean -life2:com.southeast.bean.LifeCycleBean@5ae9a829 -this is destory of lifeBean LifeCycleBean@573f2bb1 -``` - -作用域为 prototype 的 Bean ,其destroy方法并没有被调用。 -如果 bean 的 scope 设为prototype时,当容器关闭时,destroy 方法不会被调用。 -对于 prototype 作用域的 bean,有一点非常重要, - -**Spring 容器可以管理 singleton 作用域下 Bean 的生命周期, -在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。 -而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给了客户端的代码管理, -Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的Bean的生命周期**。 \ No newline at end of file diff --git "a/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" "b/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" new file mode 100644 index 0000000..ab2a945 --- /dev/null +++ "b/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" @@ -0,0 +1,50 @@ +# Spring 中常见注解 + +## @Contoller + +SpringMVC 中,控制器 Controller 负责处理 DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个 Model,然后再把该 Model 返回给对应的 View 进行展示。 + +SpringMVC 提供了一个非常简便的定义 Controller 的方法,你无需继承特定的类或者接口,只需使用 @Controller 标记一个类是 Contoller。 + +## @RequestMapping + +使用 @RequestMapping 来映射 URL 到控制器,或者到 Controller 控制器的处理方法上。method 的值一旦指定,则处理方法只对指定的 HTTP method 类型请求处理。 + +可以为多个方法映射相同的 URI,不同的 HTTP method 类型,Spring MVC 根据请求的 method 类型是可以区分开这些方法的。 + +## @RequestParam 和 @PathVariable + +在 SpringMVC 中,两者的作用都是将 request 里的参数的值绑定到 Controller 里的方法参数中,区别在于 URL 的写法不同。 + +- 使用 @RequestParam 时,URL 是这样的: + +```html +http://host:port/path?参数名=参数值 +``` + +- 使用 @PathVariable 时,URL 是这样的: + +```html +http://host:port/path/参数值 +``` + +## @Autowired + +@Autowired 可以对成员变量、成员方法和构造函数进行标注,来完成自动装配工作。 + +## @Service、 @Contrller、 @Repository 和 @Component + + @Service、 @Contrller、 @Repository 其实这 3 个注解和 @Component 是等效的,用在实现类上: + +- @Service 用于标注业务层组件 +- @Controller 用于标注控制层组件 +- @Repository 用于编著数据访问组件 +- @Component 泛指组件,当组件不好归类时,可以使用这个注解进行标注 + +## @Value + +在 Spring 3.0 中,可以通过使用 @Value,对一些如 xxx.properties 文件中的文件,进行键值对的注入。 + +## @ResponseBody + +该注解用于将 Controller 中方法返回的对象,通过适当的 HttpMessageConverter 转换为指定的格式后,写入到 Response 对象的 body s数据区。 \ No newline at end of file diff --git "a/docs/Spring/SpringBoot 344円273円213円347円273円215円.md" "b/docs/Spring/SpringBoot 344円273円213円347円273円215円.md" new file mode 100644 index 0000000..e871f0f --- /dev/null +++ "b/docs/Spring/SpringBoot 344円273円213円347円273円215円.md" @@ -0,0 +1,22 @@ +# SpringBoot 介绍 + +**Spring Boot 简化 Spring 应用程序开发**。从本质上来说,Spring Boot 就是 Spring,基于 **"习惯优于配置"** 理念使得项目快速运行起来。 + +所谓 "习惯优先配置" 理念即项目中存在大量的配置,此外还内置了一个习惯性的配置,让程序开发人员无需手动进行配置。 + +## 自动配置 + +针对很多 Spring 应用程序常见的应用功能,Spring Boot 能够自动提供相关配置。当开发人员在 pom.xml 文件汇中添加 starter 依赖后, maven 或者 gradle 会自动下载很多 jar 包到 classpath 中。 + +当 Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,自动创建和织入需要 Spring Bean 到程序上下文中。@SpringBootApplication 开启 Spring 组件扫描和 Spring Boot 的自动配置功能。 + +## 起步依赖 + +告诉 SpringBoot 需要什么功能,它就能引入需要的库,利用了传递依赖解析,把常用库聚合在一起,组成了几个特定功能而定制的依赖,不需要担心版本不兼容的问题。 + +## 命令行界面 + +这是 SpringBoot 的可选性质,借此你只需要能完成完整的应用程序,无需传统项目构建。 + + + From d69765fa71a287e44270d6e5b095317e0f19041b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=8E=9A=E5=AE=89?= <33805265+duhouan@users.noreply.github.com> Date: 2019年6月28日 23:39:06 +0800 Subject: [PATCH 21/29] =?UTF-8?q?Update=20=E5=BC=80=E5=8F=91=E6=A1=86?= =?UTF-8?q?=E6=9E=B6-=E7=9B=AE=E5=BD=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" | 3 +++ 1 file changed, 3 insertions(+) diff --git "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" index 3ad4caf..5679326 100644 --- "a/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" +++ "b/docs/345円274円200円345円217円221円346円241円206円346円236円266円-347円233円256円345円275円225円.md" @@ -7,6 +7,9 @@ - [Spring AOP](./Spring/02SpringAOP.md) - [Spring 事务管理](./Spring/04Spring事务管理.md) - [Spring 中 Bean 的生命周期](./Spring/05Spring中Bean的生命周期.md) +- [Spring 中涉及到的设计模式](./Spring/Spring%20中涉及到的设计模式.md) +- [Spring 常见注解](./Spring/Spring%20常见注解.md) +- [SpringBoot 介绍](./Spring/SpringBoot%20介绍.md) ### SpringMVC From 7cfd28389d8183897c6445f701eb067b4ff21689 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: Wed, 3 Jul 2019 20:57:29 +0800 Subject: [PATCH 22/29] Update Redis.md --- docs/DataBase/Redis.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index 7d4eefe..7cb7a05 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -871,11 +871,13 @@ Cache Aside Pattern 最经典的缓存结合数据库读写的模式。 问题:**为什么是删除缓存,而不是更新缓存?** +原因:如果每次更新了数据库,都要更新缓存【这里指的是频繁更新的场景,这会耗费一定的性能】,倒不如直接删除掉。等再次读取时,缓存里没有,那再去数据库找,在数据库找到再写到缓存里边(体现**懒加载**)。 + 举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 100 次,那么缓存更新 100 次;但是这个缓存在 1 分钟内只被**读取**了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。 其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,**不要每次都重新做复杂的计算,不管它会不会用到,而是让它在需要被使用时才重新计算**。 -### 简单的数据不一致问题 +### 先更新数据库,再删除缓存 > **问题** @@ -885,27 +887,49 @@ Cache Aside Pattern 最经典的缓存结合数据库读写的模式。 -> **解决方案** +如果在高并发的场景下,出现数据库与缓存数据不一致的**概率特别低**,也不是没有: + +- 缓存**刚好**失效 +- 线程A查询数据库,得一个旧值 +- 线程B将新值写入数据库 +- 线程B删除缓存 +- 线程A将查到的旧值写入缓存 + +要达成上述情况,还是说一句**概率特别低**: -**先删除缓存,再修改数据库**。 +> 因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,**而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存**,所有的这些条件都具备的概率基本并不大。 -如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。读的时候缓存中没有数据,就会读数据库中旧数据,然后更新到缓存中。 +对于这种策略,其实是一种设计模式:`Cache Aside Pattern` -### 复杂的数据不一致问题 +> **删除缓存失败的解决思路** + +- 将需要删除的key发送到消息队列中 +- 自己消费消息,获得需要删除的key +- **不断重试删除操作,直到成功** + +### 先删除缓存,再更新数据库 > **问题** 数据更新,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后更新数据的程序完成了数据库的修改,此时,数据库和缓存中的数据又不一致了。 -只有在对一个数据在**并发**的进行读写的时候,才可能会出现这种问题。如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现这种问题。 +并发场景下分析: + +- 线程A删除了缓存 +- 线程B查询,发现缓存已不存在 +- 线程B去数据库查询得到旧值 +- 线程B将旧值写入缓存 +- 线程A将新值写入数据库 + +所以也会导致数据库和缓存不一致的问题。 > **解决方案** -读请求和写请求串行化,串到一个**内存队列**里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 +读请求和写请求串行化,串到一个**内存队列**里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的**吞吐量会大幅度的降低**,用比正常情况下多几倍的机器去支撑线上的一个请求。 ### 比较 -- 先更新数据库,再删除缓存:在高并发下表现优异,在原子性被破坏时表现不如意。 +- 先更新数据库,再删除缓存(`Cache Aside Pattern`设计模式):在高并发下表现优异,在原子性被破坏时表现不如意。 - 先删除缓存,再更新数据库:在高并发下表现不如意,在原子性被破坏时表现优异。 From 8e45b7d63b228d240d4250afbbb59154c588e461 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: Wed, 3 Jul 2019 20:57:34 +0800 Subject: [PATCH 23/29] =?UTF-8?q?Update=20Java-=E8=99=9A=E6=8B=9F=E6=9C=BA?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" "b/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" index 768d022..9598d72 100644 --- "a/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" +++ "b/docs/Java/Java-350円231円232円346円213円237円346円234円272円.md" @@ -221,7 +221,7 @@ Hotspot虚拟机的对象头包括两部分信息: 一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等等), -另一部分是类型指针,即对象指向它的**类元数据的指针**,虚拟机通过这个指针来**确定这个对象是那个类的实例**。 +另一部分是类型指针,即对象指向它的**类元数据的指针**,虚拟机通过这个指针来**确定这个对象是哪个类的实例**。 - **实例数据** @@ -241,7 +241,7 @@ Hotspot虚拟机的对象头包括两部分信息: - **使用句柄** -如果使用句柄的话,那么 **Java 堆**中将会划分出一块内存来作为句柄池,reference 中存储的就是**对象的句柄地址**,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 。 +如果使用[句柄](https://zh.wikipedia.org/wiki/%E5%8F%A5%E6%9F%84)的话,那么 **Java 堆**中将会划分出一块内存来作为句柄池,reference 中存储的就是**对象的句柄地址**,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 。 From 0ac0d4a791c438b0464a03b15b61334c8de8c823 Mon Sep 17 00:00:00 2001 From: DuHouAn <18351926682@163.com> Date: Sun, 7 Jul 2019 19:27:51 +0800 Subject: [PATCH 24/29] Java-Notes --- ...code-Database 351円242円230円350円247円243円.md" | 435 +-- docs/DataBase/Redis.md | 2 +- "docs/Java/Java 345円271円266円345円217円221円.md" | 2494 +++++++++++++++++ 3 files changed, 2743 insertions(+), 188 deletions(-) create mode 100644 "docs/Java/Java 345円271円266円345円217円221円.md" diff --git "a/docs/DataBase/09Leetcode-Database 351円242円230円350円247円243円.md" "b/docs/DataBase/09Leetcode-Database 351円242円230円350円247円243円.md" index 5b4a3d0..4597546 100644 --- "a/docs/DataBase/09Leetcode-Database 351円242円230円350円247円243円.md" +++ "b/docs/DataBase/09Leetcode-Database 351円242円230円350円247円243円.md" @@ -1,27 +1,8 @@ - -* [595. Big Countries](#595-big-countries) -* [627. Swap Salary](#627-swap-salary) -* [620. Not Boring Movies](#620-not-boring-movies) -* [596. Classes More Than 5 Students](#596-classes-more-than-5-students) -* [182. Duplicate Emails](#182-duplicate-emails) -* [196. Delete Duplicate Emails](#196-delete-duplicate-emails) -* [175. Combine Two Tables](#175-combine-two-tables) -* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers) -* [183. Customers Who Never Order](#183-customers-who-never-order) -* [184. Department Highest Salary](#184-department-highest-salary) -* [176. Second Highest Salary](#176-second-highest-salary) -* [177. Nth Highest Salary](#177-nth-highest-salary) -* [178. Rank Scores](#178-rank-scores) -* [180. Consecutive Numbers](#180-consecutive-numbers) -* [626. Exchange Seats](#626-exchange-seats) - - - -# 595. Big Countries - -https://leetcode.com/problems/big-countries/description/ - -## Description +# 1、大的国家(595) + +[595. 大的国家](https://leetcode-cn.com/problems/big-countries/) + +- 问题描述 ```html +-----------------+------------+------------+--------------+---------------+ @@ -46,7 +27,7 @@ https://leetcode.com/problems/big-countries/description/ +--------------+-------------+--------------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -62,24 +43,22 @@ VALUES ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' ); ``` -## Solution +- 解题 ```sql -SELECT name, - population, - area -FROM - World -WHERE - area> 3000000 - OR population> 25000000; +# 思路: +# 1、根据样例,我们知道。查询字段是 name population 和 area +# 2、查询条件是 area> 3000000 || population> 25000000 +SELECT name,population,area +FROM World +WHERE area> 3000000 || population> 25000000; ``` -# 627. Swap Salary +# 2、交换工资(627) -https://leetcode.com/problems/swap-salary/description/ +[627. 交换工资](https://leetcode-cn.com/problems/swap-salary/) -## Description +- 问题描述 ```html | id | name | sex | salary | @@ -101,7 +80,7 @@ https://leetcode.com/problems/swap-salary/description/ | 4 | D | m | 500 | ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -116,18 +95,22 @@ VALUES ( '4', 'D', 'f', '500' ); ``` -## Solution +- 解题 ```sql -UPDATE salary -SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) ); +# 思路: +# l、利用位运算:比如若 x^b^a = a,则判断 x=b +# 2、利用 ASCII 函数将字符转换为数值进行运算,然后再利用 CHAR 函数将数值转换为字符 +UPDATE +salary +SET sex = CHAR(ASCII(sex)^ASCII('m')^ASCII('f')); ``` -# 620. Not Boring Movies +# 3、有趣的电影(62) -https://leetcode.com/problems/not-boring-movies/description/ +[620. 有趣的电影](https://leetcode-cn.com/problems/not-boring-movies/) -## Description +- 问题描述 ```html @@ -153,7 +136,7 @@ https://leetcode.com/problems/not-boring-movies/description/ +---------+-----------+--------------+-----------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -169,25 +152,23 @@ VALUES ( 5, 'House card', 'Interesting', 9.1 ); ``` -## Solution +- 解题 ```sql -SELECT - * -FROM - cinema -WHERE - id % 2 = 1 - AND description != 'boring' -ORDER BY - rating DESC; +#思路: +#1、观察测试用例的查询结果,我们知道,其实查询的是所有的字段 +#2、id 为奇数,则查询条件为 id % 2 = 1 +SELECT id,movie,description,rating +FROM cinema +WHERE id%2=1 AND description != 'boring' +ORDER BY rating DESC; ``` -# 596. Classes More Than 5 Students +# 4、超过5名学生的课(596) -https://leetcode.com/problems/classes-more-than-5-students/description/ +[596. 超过5名学生的课](https://leetcode-cn.com/problems/classes-more-than-5-students/) -## Description +- 问题描述 ```html +---------+------------+ @@ -215,7 +196,7 @@ https://leetcode.com/problems/classes-more-than-5-students/description/ +---------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -235,24 +216,23 @@ VALUES ( 'I', 'Math' ); ``` -## Solution +- 解答 ```sql -SELECT - class -FROM - courses -GROUP BY - class -HAVING - count( DISTINCT student )>= 5; +# 思路: +# 1、很显然要按照 class 进行分组 +# 2、然后按照分组后的 class 来统计学生的人数 +SELECT class +FROM courses +GROUP BY class +HAVING COUNT( DISTINCT student)>= 5; ``` -# 182. Duplicate Emails +# 5、查找重复的电子邮箱(182) -https://leetcode.com/problems/duplicate-emails/description/ +[182. 查找重复的电子邮箱](https://leetcode-cn.com/problems/duplicate-emails/) -## Description +- 问题描述 邮件地址表: @@ -276,7 +256,7 @@ https://leetcode.com/problems/duplicate-emails/description/ +---------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -290,24 +270,23 @@ VALUES ( 3, 'a@b.com' ); ``` -## Solution +- 解题 ```sql -SELECT - Email -FROM - Person -GROUP BY - Email -HAVING - COUNT( * )>= 2; +# 思路:与 596 题类似 +# 1、按照 mail 进行分组 +# 2、统计出现次数>=2 就是重复的邮件 +SELECT Email +FROM Person +GROUP BY EMAIL +HAVING COUNT(id)>= 2; ``` -# 196. Delete Duplicate Emails +# *6、删除重复的电子邮箱(196) https://leetcode.com/problems/delete-duplicate-emails/description/ -## Description +- 问题描述 邮件地址表: @@ -332,15 +311,14 @@ https://leetcode.com/problems/delete-duplicate-emails/description/ +----+------------------+ ``` -## SQL Schema +- SQL Schema 与 182 相同。 -## Solution - -连接: +- 解题: ```sql +# 思路一:将一张表看成两张表来进行操作 DELETE p1 FROM Person p1, @@ -350,33 +328,37 @@ WHERE AND p1.Id> p2.Id ``` -子查询: - ```sql +# 思路二: +# 第一步:根据 email 进行分组,获取 email 对应的最小 id,一个 email 对应一个最小的 id +SELECT min( id ) AS id FROM Person GROUP BY email; + +# 第二步:删除不在该 id 集合中的数据 DELETE FROM Person WHERE id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m ); +# 应该注意的是上述解法额外嵌套了一个 SELECT 语句。 +# 如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。 ``` -应该注意的是上述解法额外嵌套了一个 SELECT 语句,如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。 - ```sql DELETE FROM Person WHERE - id NOT IN ( SELECT min( id ) AS id FROM Person GROUP BY email ); + id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m ); +# 发生 You can't specify target table 'Person' for update in FROM clause。 ``` 参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause) -# 175. Combine Two Tables +# 7、组合两个表(175) https://leetcode.com/problems/combine-two-tables/description/ -## Description +- 问题描述: Person 表: @@ -407,7 +389,7 @@ AddressId is the primary key column for this table. 查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。 -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -426,27 +408,35 @@ VALUES ( 1, 2, 'New York City', 'New York' ); ``` -## Solution - -使用左外连接。 +- 解题: ```sql -SELECT - FirstName, - LastName, - City, - State -FROM - Person P - LEFT JOIN Address A - ON P.PersonId = A.PersonId; +# 思路:左外连接 +SELECT p.FirstName,p.LastName,a.City,a.State +FROM Person p +LEFT JOIN Address a +ON p.PersonId=a.PersonId; ``` -# 181. Employees Earning More Than Their Managers +- 扩展: + + * 内连接:返回两张表的交集部分。 + + + + * 左连接: + + + + * 右连接: + + + +# *8、超过经理收入的员工(181) https://leetcode.com/problems/employees-earning-more-than-their-managers/description/ -## Description +- 问题描述: Employee 表: @@ -463,7 +453,7 @@ Employee 表: 查找薪资大于其经理薪资的员工信息。 -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -478,23 +468,45 @@ VALUES ( 4, 'Max', 90000, NULL ); ``` -## Solution +- 解题: ```sql -SELECT - E1.NAME AS Employee -FROM - Employee E1 - INNER JOIN Employee E2 - ON E1.ManagerId = E2.Id - AND E1.Salary> E2.Salary; +# 思路:Employee e1 INNER JOIN Employee e2 ON e1.managerid = e2.id 比如 + ++----+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | ++----+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | +| 2 | Henry | 80000 | 4 | +| 3 | Sam | 60000 | NULL | +| 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+ + +根据 e1.managerid = e2.id 条件进行内连接后,得到 + ++----+-------+--------+-----------+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | Name | Salary | ManagerId | ++----+-------+--------+-----------+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | Sam | 60000 | NULL | +| 2 | Henry | 80000 | 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+-------+--------+-----------+ ``` -# 183. Customers Who Never Order +```sql +SELECT + e1.name AS Employee # 注意:这里的 Employee 是给该字段起的别名,实际上查找的是姓名 +FROM + Employee e1 + INNER JOIN Employee e2 + ON e1.managerid = e2.id + AND e1.salary> e2.salary; +``` + +# 9、从不订购的客户(183) https://leetcode.com/problems/customers-who-never-order/description/ -## Description +- 问题描述: Curstomers 表: @@ -531,7 +543,7 @@ Orders 表: +-----------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -554,37 +566,34 @@ VALUES ( 2, 1 ); ``` -## Solution - -左外链接 +- 解答 ```sql +# 解法一:左外连接 SELECT - C.Name AS Customers + c.Name AS Customers FROM - Customers C - LEFT JOIN Orders O - ON C.Id = O.CustomerId + Customers c + LEFT JOIN Orders o + ON c.Id = o.CustomerId WHERE - O.CustomerId IS NULL; + o.CustomerId IS NULL; ``` -子查询 - ```sql -SELECT - Name AS Customers -FROM +# 解法二:子查询方式 +SELECT Name AS Customers +FROM Customers -WHERE - Id NOT IN ( SELECT CustomerId FROM Orders ); +WHERE + Id NOT IN (SELECT CustomerId FROM Orders); ``` -# 184. Department Highest Salary +# *10、部门工资最高的员工(184) https://leetcode.com/problems/department-highest-salary/description/ -## Description +- 问题描述: Employee 表: @@ -621,7 +630,7 @@ Department 表: +------------+----------+--------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE IF EXISTS Employee; @@ -640,32 +649,42 @@ VALUES ( 2, 'Sales' ); ``` -## Solution +- 解题: -创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。 +```sql +# 创建一个临时表,包含了部门员工的最大薪资。 +# 可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。 -之后使用连接找到一个部门中薪资等于临时表中最大薪资的员工。 +SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId; + +# 结果: ++--------------+--------+ +| DepartmentId | Salary | ++--------------+--------+ +| 1 | 90000 | +| 2 | 80000 | ++--------------+--------+ +``` + +使用连接找到一个部门中薪资等于临时表中最大薪资的员工。 ```sql -SELECT - D.NAME Department, - E.NAME Employee, - E.Salary -FROM - Employee E, - Department D, - ( SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId ) M -WHERE - E.DepartmentId = D.Id - AND E.DepartmentId = M.DepartmentId - AND E.Salary = M.Salary; +SELECT d.name as Department, e.name as Employee, m.Salary +FROM + Employee e, + Department d, + (SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId) m +WHERE + e.DepartmentId=d.Id + AND e.DepartmentId=m.DepartmentId + AND e.Salary=m.Salary; ``` -# 176. Second Highest Salary +# 11、第二高的薪水(176) https://leetcode.com/problems/second-highest-salary/description/ -## Description +- 问题描述: ```html +----+--------+ @@ -689,7 +708,7 @@ https://leetcode.com/problems/second-highest-salary/description/ 没有找到返回 null 而不是不返回数据。 -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -703,41 +722,54 @@ VALUES ( 3, 300 ); ``` -## Solution +- 解题: -为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。 +```sql +# 查询所有 salary 按照降序排列 +SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC; +``` + +```sql +# 要获取第二高的的薪水,就是获取第二个元素, +# 使用 limit start,count; start:开始查询的位置,count 是查询多少条语句 +SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1,1; +``` ```sql +# 为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。 SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1, 1 ) SecondHighestSalary; ``` -# 177. Nth Highest Salary +# 12、第N高的薪水(177) + +[177. 第N高的薪水](https://leetcode-cn.com/problems/nth-highest-salary/) -## Description +- 问题描述: 查找工资第 N 高的员工。 -## SQL Schema +- SQL Schema 同 176。 -## Solution +- 解题: ```sql +# 思路:其实与 176题目类似 CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN -SET N = N - 1; +SET N = N - 1; #注意 LIMIT 的 start 是从 0 开始的,第 N 实际上是第 (N-1) RETURN ( SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N, 1 ) ); END ``` -# 178. Rank Scores +# 13、分数排名(178) https://leetcode.com/problems/rank-scores/description/ -## Description +- 问题描述: 得分表: @@ -769,7 +801,7 @@ https://leetcode.com/problems/rank-scores/description/ +-------+------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -786,9 +818,11 @@ VALUES ( 6, 3.65 ); ``` -## Solution +- 解题: ```sql +#思路:关键在于如何统计 Rank +#这里使用同一张表进行内连接,注意连接的条件 S1.score <= S2.score 就是为了方便统计 Rank SELECT S1.score, COUNT( DISTINCT S2.score ) Rank @@ -802,11 +836,9 @@ ORDER BY S1.score DESC; ``` -# 180. Consecutive Numbers - -https://leetcode.com/problems/consecutive-numbers/description/ +# *14、连续出现的数字(180) -## Description +- 问题描述: 数字表: @@ -834,7 +866,7 @@ https://leetcode.com/problems/consecutive-numbers/description/ +-----------------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -852,26 +884,55 @@ VALUES ( 7, 2 ); ``` -## Solution +- 解题 + +- ```sql -SELECT - DISTINCT L1.num ConsecutiveNums +# 思路:要求是连续出现 3 次,可使用 3 张该表 +SELECT L1.Num ConsecutiveNums FROM - Logs L1, - Logs L2, - Logs L3 -WHERE L1.id = l2.id - 1 - AND L2.id = L3.id - 1 - AND L1.num = L2.num - AND l2.num = l3.num; + Logs L1, + Logs L2, + Logs L3 +WHERE + L1.Id = L2.Id-1 + AND L2.ID = L3.Id-1 + AND L1.Num = L2.Num + AND L2.Num = L3.Num; +# 判断条件 Id 是不相同的,但是 Num 是相同的,并且 Id 是连续变化的 +``` + +```sql +# 由于要求是至少出现 3 次,所以需要 DISTINCT 进行去重 +SELECT DISTINCT L1.Num ConsecutiveNums +FROM + Logs L1, + Logs L2, + Logs L3 +WHERE + L1.Id = L2.Id-1 + AND L2.ID = L3.Id-1 + AND L1.Num = L2.Num + AND L2.Num = L3.Num; +``` + +```sql +# 另外一种写法 +SELECT DISTINCT L1.Num ConsecutiveNums +FROM + Logs L1 + LEFT JOIN Logs L2 ON L1.Id = L2.Id-1 + LEFT JOIN Logs L3 ON L1.Id = L3.Id-2 +WHERE L1.Num = L2.Num + AND L2.Num = L3.Num; ``` -# 626. Exchange Seats +# 15、换座位(626)(了解) https://leetcode.com/problems/exchange-seats/description/ -## Description +- 问题描述: seat 表存储着座位对应的学生。 @@ -901,7 +962,7 @@ seat 表存储着座位对应的学生。 +---------+---------+ ``` -## SQL Schema +- SQL Schema ```sql DROP TABLE @@ -917,7 +978,7 @@ VALUES ( '5', 'Jeames' ); ``` -## Solution +- 解题: 使用多个 union。 diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index 7d4eefe..4d365d4 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -10,7 +10,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, 主要从"高性能"和"高并发"这两点来看待这个问题。 -性能:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,那么同步改变缓存中相应的数据即可! +性能:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。**操作缓存就是直接操作内存**,所以速度相当快。如果数据库中的对应数据改变的之后,那么同步改变缓存中相应的数据即可! 并发:高并发情况下,所有请求直接访问数据库,数据库会出现连接异常,直接操作缓存能够承受的请求是远远大于直接访问数据库的,因此,可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存而不用经过数据库。 diff --git "a/docs/Java/Java 345円271円266円345円217円221円.md" "b/docs/Java/Java 345円271円266円345円217円221円.md" new file mode 100644 index 0000000..0901273 --- /dev/null +++ "b/docs/Java/Java 345円271円266円345円217円221円.md" @@ -0,0 +1,2494 @@ +## 一、进程与线程 + +### 进程与线程的区别 + +> **进程与线程的由来** + +- 串行:初期的计算机只能串行执行任务,并且需要长时间等待用户输入 +- 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行 +- 进程:**进程独占内存空间**,保存各自运行状态,相互间**互不干扰**并且可以互相**切换**,为并发处理任务提供了可能 +- 线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行 + +> **进程和线程的区别** + +**进程是资源分配的最小单位,线程是 CPU 调度的最小单位** + +- 所有与进程相关的资源,都被记录在 PCB 中 + +![](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_1.png) + +- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源 +- 线程只由堆栈寄存器、程序计数器和 TCP 组成 + +![](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_2.png) + +> **总结** + +- 线程不能看做独立应用,而进程可看做独立应用 +- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径 +- 线程没有独立的地址空间,多进程程序比多线程程序健壮 +- 进程的切换比线程切换开销大 + +> **多进程与多线程** + +- 多进程的意义 + +CPU 在某个时间点上只能做一件事情,计算机是在进程 1 和 进程 2 间做着频繁切换,且切换速度很快,所以,我们感觉进程 1 和进程 2 在同时进行,其实并不是同时执行的。 + +**多进程的作用不是提高执行速度,而是提高 CPU 的使用率**。 + +- 多线程的意义 + + 多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈。所以他们仍然是在抢 CPU 的资源执行。一个时间点上只有能有一个线程执行。而且谁抢到,这个不一定,所以,造成了线程运行的随机性。 + +**多线程的作用不是提高执行速度,而是为了提高应用程序的使用率**。 + +### Java 进程与线程的关系 + +- Java 对操作系统提供的功能进行封装,包括进程和线程 +- 运行程序会产生一个进程,进程包含至少一个线程 +- 每个进程对应一个 JVM 实例多个线程共享 JVM 里的堆 +- Java 采用单线程编程模型,程序会自动创建主线程 +- 主线程可以创建子线程,原则上要后与子线程完成执行 + +## 二、使用线程 + +- 继承 Thread 类; + +- 实现 Runnable 接口; +- 实现 Callable 接口。 + +实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。 + +### 继承 Thread 类 + +需要实现 run() 方法,因为 Thread 类实现了 Runnable 接口。 + +当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。 + +```java +public class MyThread extends Thread{ + private void attack() { + System.out.println("Fight"); + System.out.println("Current Thread is : " + Thread.currentThread().getName()); + } + + @Override + public void run() { //重写 run() 方法 + attack(); + } +} +``` + +> **Java 中 start 和 run 方法的区别** + +- 调用 start 方法会创建一个新的子线程并启动 +- run 方法只是 Thread 只是 Thread 的一个普通方法 + +![](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_3.png) + +```java +public class MyThreadTest { + public static void main(String[] args) { + Thread t = new MyThread(); + + System.out.println("current main thread is : " + Thread.currentThread().getName()); + t.run(); //调用 run() 方法 + } +} +//输出结果: +//current main thread is : main +//Fight +//Current Thread is : main +``` + +```java +public class MyThreadTest2 { + public static void main(String[] args) { + Thread t = new MyThread(); + + System.out.println("current main thread is : " + Thread.currentThread().getName()); + t.start(); //调用 run() 方法 + } +} +//输出结果: +//current main thread is : main +//Fight +//Current Thread is : Thread-0 +``` + +### 实现 Runnable 接口 + +需要实现 run() 方法。 + +通过 Thread 调用 start() 方法来启动线程。 + +```java +public class MyRunnable implements Runnable { + public void run() { + // ... + } +} +``` + +```java +public static void main(String[] args) { + MyRunnable instance = new MyRunnable(); + Thread thread = new Thread(instance); + thread.start(); +} +``` + +> **实现接口 VS 继承 Thread** + +实现接口会更好一些,因为: + +- Thread 是实现了 Runnable 接口的类,使得 run 支持多线程 + +- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; +- 类可能只要求可执行就行,继承整个 Thread 类开销过大。 + +### 实现 Callable 接口 + +与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。 + +```java +public class MyCallable implements Callable { + public Integer call() { + return 123; + } +} +``` + +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + MyCallable mc = new MyCallable(); + FutureTask ft = new FutureTask(mc); + Thread thread = new Thread(ft); + thread.start(); + System.out.println(ft.get()); +} +``` + +> **实现处理线程的返回值** + +- **主线程等待法** + +```java +public class CycleWait implements Runnable{ + private String value; + + @Override + public void run() { + try { + Thread.currentThread().sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value = "we have data now"; + } +} +``` + +```java +public static void main(String[] args) { + CycleWait cycleWait = new CycleWait(); + Thread t = new Thread(cycleWait); + t.start(); + + System.out.println("value:"+cycleWait.value); + //输出结果: + //value:null + } +``` + +```java + public static void main(String[] args) throws InterruptedException { + CycleWait cycleWait = new CycleWait(); + Thread t = new Thread(cycleWait); + t.start(); + + while (cycleWait.value == null){ //主线程等待法 + Thread.sleep(1000); + } + System.out.println("value:"+cycleWait.value); + //输出结果: + //value:we have data now + } +``` + +- **使用 Thread 的 join() 阻塞当前线程以等待子线程处理完毕** + +```java +public static void main(String[] args) { + CycleWait cycleWait = new CycleWait(); + Thread t = new Thread(cycleWait); + t.start(); + t.join(); //阻塞当前线程以等待子线程执行完毕 + System.out.println("value:"+cycleWait.value); + //输出结果: + //value:we have data now + } +``` + +- **通过 Callable 接口实现:通过 FutureTask 或者线程池获取** + +方式一:通过 FutureTask 获取 + +```java +public class CycleWait2 implements Callable{ + private String value; + @Override + public String call() throws Exception { + try { + Thread.currentThread().sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value = "we have data now"; + return value; + } + + public static void main(String[] args) throws ExecutionException, InterruptedException { + Callable cycleWait2 = new CycleWait2(); + FutureTask ft = new FutureTask(cycleWait2); + Thread t = new Thread(ft); + t.start(); + + if(!ft.isDone()){ + System.out.println("task has not finished,please wait!"); + } + + System.out.println("value:"+ft.get()); + //输出结果: + //value:we have data now + } +} +``` + +方式二:通过线程池获取 + +```java +public class CycleWait3 implements Callable{ + private String value; + @Override + public String call() throws Exception { + try { + Thread.currentThread().sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value = "we have data now"; + return value; + } + + public static void main(String[] args){ + ExecutorService service = Executors.newCachedThreadPool(); + Future future = service.submit(new CycleWait3()); + + if(!future.isDone()){ + System.out.println("task has not finished,please wait!"); + } + + try { + System.out.println("value:"+future.get()); + //输出结果: + //value:we have data now + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + }finally { + service.shutdown(); //关闭线程池 + } + } +} +``` + + + +## 三、线程状态转换 + +
+ +
+ +> **新建(New)** + +创建后尚未启动。 + +> **可运行(Runnable)** + +可能正在运行,也可能正在等待 CPU 时间片。 + +包含了操作系统线程状态中的 Running 和 Ready。 + +调用 `start()` 方法后开始运行,线程这时候处于 Ready 状态。可运行状态的线程获得了 CPU 时间片后就处于 Running 状态。 + +> **阻塞(Blocked)** + +等待获取一个**排它锁**,如果其他线程释放了锁就会结束此状态。 + +> **无限期等待(Waiting)** + +等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。 + +| 进入方法 | 退出方法 | +| --- | --- | +| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | +| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | +| LockSupport.park() 方法 | LockSupport.unpark(Thread) | + +> **限期等待(Timed Waiting)** + +无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。 + +调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用"使一个线程睡眠"进行描述。 + +调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用"挂起一个线程"进行描述。 + +睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 + +阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 + +| 进入方法 | 退出方法 | +| --- | --- | +| Thread.sleep() 方法 | 时间结束 | +| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() | +| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | +| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) | +| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) | + +> **死亡(Terminated)** + +可以是线程结束任务之后自己结束,或者产生了异常而结束。 + + +## 四、基础线程机制 + +### Executor + +Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。 + +主要有三种 Executor: + +- CachedThreadPool:一个任务创建一个线程; +- FixedThreadPool:所有任务只能使用固定大小的线程; +- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyRunnable()); + } + executorService.shutdown(); //关闭线程池 +} +``` + +### Daemon + +守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。 + +当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。 + +main() 属于非守护线程。 + +使用 setDaemon() 方法将一个线程设置为守护线程。 + +```java +public static void main(String[] args) { + Thread thread = new Thread(new MyRunnable()); + thread.setDaemon(true); +} +``` + +### sleep() + +Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。 + +sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 + +```java +public void run() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } +} +``` + +### yield() + +对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法**只是对线程调度器的一个建议**,而且也只是建议具有相同优先级的其它线程可以运行。 + +```java +public class YieldDemo { + public static void main(String[] args) { + Runnable yieldTask = new Runnable() { + @Override + public void run() { + for(int i=1;i<=10;i++){ + System.out.println(Thread.currentThread().getName()+"\t"+i); + if(i==5){ + Thread.yield(); //只是对线程调度器的一个建议 + } + } + } + }; + + Thread t1 = new Thread(yieldTask,"Thread-A"); + Thread t2 = new Thread(yieldTask,"Thread-B"); + + t1.start(); + t2.start(); + } +} +``` + +```html +Thread-A 1 +Thread-A 2 +Thread-A 3 +Thread-A 4 +Thread-A 5 +Thread-B 1 +Thread-B 2 +Thread-B 3 +Thread-B 4 +Thread-A 6 +Thread-B 5 +Thread-A 7 +Thread-B 6 +Thread-A 8 +Thread-B 7 +Thread-A 9 +Thread-B 8 +Thread-B 9 +Thread-A 10 +Thread-B 10 +``` + +## 五、中断 + +一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。 + +### interrupt() + +通过调用一个线程的 interrupt() 来中断该线程: + +- 如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是**不能中断 I/O 阻塞和 synchronized 锁阻塞**。 +- 如果该线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。 + +对于以下代码。在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 + +```java +public class InterruptExample1 { + private static class MyThread1 extends Thread { + @Override + public void run() { + try { + Thread.sleep(2000); //thread1 进入限期等待状态 + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Thread run"); + } + } + + public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); //中断 thread1 + System.out.println("Main run"); + } +} +``` + +```java +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at InterruptExample.lambda$main0ドル(InterruptExample.java:5) + at InterruptExample$$Lambda1ドル/713338599.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) +``` + +```java +public class InterruptExample2 { + private static class MyThread1 extends Thread { + @Override + public void run() { //thread1 线程处于正常活动状态 + System.out.println("Thread run"); + } + } + + public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); //中断 thread1 + System.out.println("Main run"); + } +} +``` + +```html +Main run +Thread run +``` + +### interrupted() + +如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。 + +但是调用 interrupt() 方法会设置线程的中断标记,此时**调用 interrupted() 方法会返回 true**。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。 + +```java +public class InterruptExample { + public static void main(String[] args) throws InterruptedException { + Runnable interruptTask = new Runnable() { + @Override + public void run() { + int i = 0; + try { + //在正常运行任务时,经常检查本线程的中断标志位, + //如果被设置了中断标志就自行停止线程 + while (!Thread.currentThread().isInterrupted()) { + Thread.sleep(100); // 休眠100ms + i++; + System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") loop " + i); + } + } catch (InterruptedException e) { + //在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。) + System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") catch InterruptedException."); + } + } + }; + Thread t1 = new Thread(interruptTask, "t1"); + System.out.println(t1.getName() +" ("+t1.getState()+") is new."); + + t1.start(); // 启动"线程t1" + System.out.println(t1.getName() +" ("+t1.getState()+") is started."); + + // 主线程休眠300ms,然后主线程给t1发"中断"指令。 + Thread.sleep(300); + t1.interrupt(); + System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); + + // 主线程休眠300ms,然后查看t1的状态。 + Thread.sleep(300); + System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now."); + } +} +``` + +```java +t1 (NEW) is new. +t1 (RUNNABLE) is started. +t1 (RUNNABLE) loop 1 +t1 (RUNNABLE) loop 2 +t1 (TIMED_WAITING) is interrupted. +t1 (RUNNABLE) catch InterruptedException. +t1 (TERMINATED) is interrupted now. +``` + +### Executor 的中断操作 + +调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。 + +以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。 + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> { + try { + Thread.sleep(2000); + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + executorService.shutdownNow(); + System.out.println("Main run"); +} +``` + +```java +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at ExecutorInterruptExample.lambda$main0ドル(ExecutorInterruptExample.java:9) + at ExecutorInterruptExample$$Lambda1ドル/1160460865.run(Unknown Source) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) + at java.lang.Thread.run(Thread.java:745) +``` + +如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。 + +```java +Future future = executorService.submit(() -> { + // .. +}); +future.cancel(true); +``` + +## 六、线程之间的协作 + +当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。 + +### join() + +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 + +对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。 + +```java +public class JoinExample { + + private class A extends Thread { + @Override + public void run() { + System.out.println("A"); + } + } + + private class B extends Thread { + + private A a; + + B(A a) { + this.a = a; + } + + @Override + public void run() { + try { + a.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("B"); + } + } + + public void test() { + A a = new A(); + B b = new B(a); + b.start(); + a.start(); + } +} +``` + +```java +public static void main(String[] args) { + JoinExample example = new JoinExample(); + example.test(); +} +``` + +```java +A +B +``` + +### wait() notify() notifyAll() + +调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 + +它们都属于 Object 的一部分,而不属于 Thread。 + +只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。 + +使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。 + +```java +public class WaitNotifyExample { + + public synchronized void before() { + System.out.println("before"); + notifyAll(); + } + + public synchronized void after() { + try { + wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("after"); + } +} +``` + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + WaitNotifyExample example = new WaitNotifyExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); +} +``` + +```java +before +after +``` + +> **wait() 和 sleep() 的区别** + +基本差别: + +- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; +- sleep() 方法可以在任何地方使用,而 wait() 方法只能在 synchronized 块或者同步方法中使用; + +本质区别: + +- Thread.sleep 只会让出 CPU,不会导致锁行为的变化 +- Object.wait 不仅让出 CPU ,还会释放已经占有的同步资源锁 + +```java +public class WaitSleepDemo { + public static void main(String[] args) { + final Object lock = new Object(); + + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + System.out.println("thread A is waiting to get lock"); + synchronized (lock){ + try { + System.out.println("thread A get lock"); + Thread.sleep(20); + //Thread.sleep 只会让出 CPU,不会导致锁行为的变化 + System.out.println("thread A do wait method"); + + //==========位置 A ========== + //lock.wait(1000); + //Object.wait 不仅让出 CPU ,还会释放已经占有的同步资源锁 + + Thread.sleep(1000); + //Thread.sleep 只会让出 CPU,不会导致锁行为的变化 + //=========================== + + System.out.println("thread A is done"); + } catch (InterruptedException e){ + e.printStackTrace(); + } + } + } + }); + t1.start(); + + try{ + Thread.sleep(10); //主线程等待 10 ms + } catch (InterruptedException e){ + e.printStackTrace(); + } + new Thread(new Runnable() { + @Override + public void run() { + System.out.println("thread B is waiting to get lock"); + synchronized (lock){ + try { + System.out.println("thread B get lock"); + System.out.println("thread B is sleeping 10 ms"); + Thread.sleep(10); + //Thread.sleep 只会让出 CPU,不会导致锁行为的变化 + System.out.println("thread B is done"); + } catch (InterruptedException e){ + e.printStackTrace(); + } + } + } + }).start(); + } +} +``` + +位置 A 为 `lock.wait(1000);` 输出结果: + +```html +thread A is waiting to get lock +thread A get lock +thread A do wait method +thread B is waiting to get lock +thread B get lock +thread B is sleeping 10 ms +thread B is done +thread A is done +``` + +位置 A 为 `Thread.sleep(1000);` 输出结果: + +```html +thread A is waiting to get lock +thread A get lock +thread B is waiting to get lock +thread A do wait method +thread A is done +thread B get lock +thread B is sleeping 10 ms +thread B is done +``` + +> **notify() 和 notifyAll() 的区别** + +两个重要概念: + +- 锁池(EntryList) + +假设线程 A 已经拥有了**某个对象的锁**,而其他线程 B、C 想要调用这个对象的同步方法(或者同步代码块),由于 B 、C 线程在进入对象的同步方法(或者同步代码块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程 A 所占用,此时 B 、C 线程就会被阻塞,进入一个地方去**等待锁的释放**,这个地方便是该对象的锁池。 + +- 等待池(WaitSet) + +假设线程 A 调用了某个对象的 wait() 方法,线程 A 就会释放该对象的锁,同时线程 A 就进入该对象的等待池,**进入到等待池的线程不会竞争该对象的锁**。 + +区别: + +notifyAll() 会让所有处于**等待池的线程全部进入锁池**去竞争锁 + +```java +public class NotificationDemo { + private volatile boolean go = false; + + public static void main(String args[]) throws InterruptedException { + final NotificationDemo test = new NotificationDemo(); + + Runnable waitTask = new Runnable(){ + @Override + public void run(){ + try { + test.shouldGo(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " finished Execution"); + } + }; + + Runnable notifyTask = new Runnable(){ + @Override + public void run(){ + test.go(); + System.out.println(Thread.currentThread().getName() + " finished Execution"); + } + }; + + //t1、t2、t3 等待 + Thread t1 = new Thread(waitTask, "WT1"); + Thread t2 = new Thread(waitTask, "WT2"); + Thread t3 = new Thread(waitTask, "WT3"); + //t4 唤醒 + Thread t4 = new Thread(notifyTask,"NT1"); + t1.start(); + t2.start(); + t3.start(); + + Thread.sleep(200); + t4.start(); + } + + //wait and notify can only be called from synchronized method or block + private synchronized void shouldGo() throws InterruptedException { + while(go != true){ + System.out.println(Thread.currentThread() + + " is going to wait on this object"); + wait(); //this.wait 不仅让出 CPU ,还会释放已经占有的同步资源锁 + System.out.println(Thread.currentThread() + " is woken up"); + } + go = false; //resetting condition + } + + // both shouldGo() and go() are locked on current object referenced by "this" keyword + private synchronized void go() { + while (go == false){ + System.out.println(Thread.currentThread() + + " is going to notify all or one thread waiting on this object"); + + go = true; //making condition true for waiting thread + //===================位置 A ===================== + //notify(); //只会唤醒 WT1, WT2,WT3 中的一个线程 + notifyAll(); //所有的等待线程 WT1, WT2,WT3 都会被唤醒 + //============================================== + } + } +} +``` + +位置 A 为 `notify();` 输出结果: + +```html +Thread[WT1,5,main] is going to wait on this object +Thread[WT2,5,main] is going to wait on this object +Thread[WT3,5,main] is going to wait on this object +Thread[NT1,5,main] is going to notify all or one thread waiting on this object +Thread[WT1,5,main] is woken up +WT1 finished Execution +NT1 finished Execution +``` + +位置 B 为 `notifyAll();` 输出结果: + +```html +Thread[WT1,5,main] is going to wait on this object +Thread[WT3,5,main] is going to wait on this object +Thread[WT2,5,main] is going to wait on this object +Thread[NT1,5,main] is going to notify all or one thread waiting on this object +Thread[WT2,5,main] is woken up +NT1 finished Execution +WT2 finished Execution +Thread[WT3,5,main] is woken up +Thread[WT3,5,main] is going to wait on this object +Thread[WT1,5,main] is woken up +Thread[WT1,5,main] is going to wait on this object +``` + +### await() signal() signalAll() + +java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。 + +相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。 + +使用 Lock 来获取一个 Condition 对象。 + +```java +public class AwaitSignalExample { + + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void before() { + lock.lock(); + try { + System.out.println("before"); + condition.signalAll(); + } finally { + lock.unlock(); + } + } + + public void after() { + lock.lock(); + try { + condition.await(); + System.out.println("after"); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } +} +``` + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + AwaitSignalExample example = new AwaitSignalExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); +} +``` + +```java +before +after +``` + +## 七、Java 原子操作类 + +由于 synchronized 是采用的是**悲观锁策略**,并不是特别高效的一种解决方案。 实际上,在 J.U.C下 的 atomic 包提供了一系列的操作简单,性能高效,并能保证线程安全的类,去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型。 atomic 包下的这些类都是采用的是**乐观锁策略**去原子更新数据,在 Java 中则是**使用 CAS操作具体实现**。 + +### 1、原子更新基本类 + +使用原子的方式更新基本类型,atomic 包提供了以下 3 个类: + +| 类 | 说明 | +| :-----------: | :-------------------: | +| AtomicBoolean | 原子更新 boolean 类型 | +| AtomicInteger | 原子更新整型 | +| AtomicLong | 原子更新长整型 | + +以 AtomicInteger 为例总结常用的方法: + +| 方法 | 说明 | +| :---------------------: | :----------------------------------------------------------: | +| addAndGet(int delta) | 以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果 | +| incrementAndGet() | 以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果 | +| getAndSet(int newValue) | 将实例中的值更新为新值,并返回旧值 | +| getAndIncrement() | 以原子的方式将实例中的原值 +1,返回的是自增前的旧值 | + +> **AtomicInteger ** + + AtomicInteger 测试: + +```java +public class AtomicIntegerDemo { + // 请求总数 + public static int clientTotal = 5000; + + // 同时并发执行的线程数 + public static int threadTotal = 200; + + //java.util.concurrent.atomic.AtomicInteger; + public static AtomicInteger count = new AtomicInteger(0); + + public static void main(String[] args) throws InterruptedException { + ExecutorService executorService = Executors.newCachedThreadPool(); + //Semaphore和CountDownLatch模拟并发 + final Semaphore semaphore = new Semaphore(threadTotal); + final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); + for (int i = 0; i < clientTotal ; i++) { + executorService.execute(() -> { + try { + semaphore.acquire(); + add(); + semaphore.release(); + } catch (Exception e) { + e.printStackTrace(); + } + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + executorService.shutdown(); + System.out.println("count:{"+count.get()+"}"); + } + + public static void add() { + count.incrementAndGet(); + } +} +``` + +```html +count:{5000} +``` + + AtomicInteger 的 getAndIncrement() 方法源码: + +```java +private static final Unsafe unsafe = Unsafe.getUnsafe(); + +public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; +} +``` + +实际上是调用了 unsafe 实例的 getAndAddInt 方法。Unsafe 类在 sun.misc 包下,Unsafe 类提供了一些底层操作,atomic 包下的原子操作类的也主要是通过 Unsafe 类提供的 compareAndSwapObject、compareAndSwapInt 和 compareAndSwapLong 等一系列提供 CAS 操作的方法来进行实现。 + +> **AtomicLong** + +AtomicLong 的实现原理和 AtomicInteger 一致,只不过一个针对的是 long 变量,一个针对的是 int 变量。 + +> **AtomicBoolean** + +AtomicBoolean 的核心方法是 compareAndSet() 方法: + +```java +public final boolean compareAndSet(boolean expect, boolean update) { + int e = expect ? 1 : 0; + int u = update ? 1 : 0; + return unsafe.compareAndSwapInt(this, valueOffset, e, u); +} +``` + +可以看出,compareAndSet 方法的实际上也是先转换成 0、1 的整型变量,然后是通过针对 int 型变量的原子更新方法 compareAndSwapInt 来实现的。atomic 包中只提供了对 boolean ,int ,long 这三种基本类型的原子更新的方法,参考对 boolean 更新的方式,原子更新 char,double,float 也可以采用类似的思路进行实现。 + +### 2、原子更新数组 + +通过原子的方式更新数组里的某个元素,atomic 包提供了以下 3 个类: + +| 类 | 说明 | +| :------------------: | :--------------------------: | +| AtomicIntegerArray | 原子更新整型数组中的元素 | +| AtomicLongArray | 原子更新长整型数组中的元素 | +| AtomicReferenceArray | 原子更新引用类型数组中的元素 | + +这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法: + +| 方法 | 说明 | +| :------------------------------------------: | :-----------------------------------------------: | +| addAndGet(int i, int delta) | 以原子更新的方式将数组中索引为i的元素与输入值相加 | +| getAndIncrement(int i) | 以原子更新的方式将数组中索引为i的元素自增 +1 | +| compareAndSet(int i, int expect, int update) | 将数组中索引为 i 的位置的元素进行更新 | + +AtomicIntegerArray 与 AtomicInteger 的方法基本一致,只不过在 AtomicIntegerArray 的方法中会多一个指定数组索引位 i。 + +```java +public class AtomicIntegerArrayDemo { + // private static AtomicInteger atomicInteger = new AtomicInteger(1); + private static int[] value = new int[]{1, 2, 3}; + private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value); + + public static void main(String[] args) { + //对数组中索引为1的位置的元素加5 + int result = integerArray.getAndAdd(1, 5); + System.out.println(integerArray.get(1)); + System.out.println(result); + } +} +``` + +```html +7 +2 +``` + +getAndAdd 方法将位置为 1 的元素 +5,从结果可以看出索引为 1 的元素变成了 7 ,该方法返回的也是相加之前的数为 2。 + +### 3、原子更新引用类型 + +如果需要原子更新引用类型变量的话,为了保证线程安全,atomic 也提供了相关的类: + +| 类 | 说明 | +| :-------------------------: | :--------------------------: | +| AtomicReference | 原子更新引用类型 | +| AtomicReferenceFieldUpdater | 原子更新引用类型里的字段 | +| AtomicMarkableReference | 原子更新带有标记位的引用类型 | + +这几个类的使用方法也是基本一样的,以AtomicReference为例,来说明这些类的基本用法: + +```java +public class AtomicDemo { + + private static AtomicReference reference = new AtomicReference(); + + public static void main(String[] args) { + User user1 = new User("a",1); + reference.set(user1); + User user2 = new User("b",2); + User user = reference.getAndSet(user2); + System.out.println(user); + System.out.println(reference.get()); + } + + static class User { + private String userName; + private int age; + + public User(String userName, int age) { + this.userName = userName; + this.age = age; + } + + @Override + public String toString() { + return "User{" + + "userName='" + userName + '\'' + + ", age=" + age + + '}'; + } + } +} +``` + +```html +User{userName='a', age=1} +User{userName='b', age=2} +``` + +首先构建一个 user1 对象,然会把 user1 对象设置进 AtomicReference 中,然后调用 getAndSet 方法。从结果可以看出,该方法会原子更新引用的 user 对象,变为`User{userName='b', age=2}`,返回的是原来的 user 对象User`{userName='a', age=1}`。 + +### 4、原子更新字段类型 + +如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic 同样也提供了相应的原子操作类: + +| 类 | 说明 | +| :----------------------: | :----------------------------------------------------------: | +| AtomicIntegeFieldUpdater | 原子更新整型字段类 | +| AtomicLongFieldUpdater | 原子更新长整型字段类 | +| AtomicStampedReference | 原子更新带版本号引用类型。
为什么在更新的时候会带有版本号,是为了解决 CAS 的 ABA 问题 | + +要想使用原子更新字段需要两步操作: + +- 原子更新字段类都是**抽象类**,只能通过静态方法 newUpdater 来创建一个更新器,并且需要设置想要更新的类和属性 +- 更新类的属性必须使用 public volatile 进行修饰 + +```java +public class AtomicIntegerFieldUpdaterDemo { + private static AtomicIntegerFieldUpdater updater = + AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); + + public static void main(String[] args) { + User user = new User("a", 1); + System.out.println(user); + int oldValue = updater.getAndAdd(user, 5); + System.out.println(user); + } + + static class User { + private String userName; + public volatile int age; + + public User(String userName, int age) { + this.userName = userName; + this.age = age; + } + + @Override + public String toString() { + return "User{" + + "userName='" + userName + '\'' + + ", age=" + age + + '}'; + } + } +} +``` + +```html +User{userName='a', age=1} +User{userName='a', age=6} +``` + +创建 AtomicIntegerFieldUpdater 是通过它提供的静态方法进行创建, getAndAdd 方法会将指定的字段加上输入的值,并且返回相加之前的值。 user 对象中 age 字段原值为 1,+5 之后,可以看出 user 对象中的 age 字段的值已经变成了 6。 + +## 八、J.U.C - AQS + +java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。 + +### CountDownLatch + +用来控制一个线程等待多个线程。 + +维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。 + + + + + +> **场景1:程序执行需要等待某个条件完成后,才能进行后面的操作** + +比如父任务等待所有子任务都完成的时候, 再继续往下进行。 + +```java +public class CountDownLatchExample { + //线程数量 + private static int threadCount=10; + + public static void main(String[] args) throws InterruptedException { + ExecutorService executorService= Executors.newCachedThreadPool(); + + final CountDownLatch countDownLatch=new CountDownLatch(threadCount); + + + for (int i = 1; i <= threadCount; i++) { + final int threadNum=i; + executorService.execute(()->{ + try { + test(threadNum); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(); + //上面的所有线程都执行完了,再执行主线程 + System.out.println("Finished!"); + executorService.shutdown(); + } + + private static void test(int threadNum) throws InterruptedException { + Thread.sleep(100); + System.out.println("run: Thread "+threadNum); + Thread.sleep(100); + } +} +``` + +```html +run: Thread 8 +run: Thread 7 +run: Thread 6 +run: Thread 5 +run: Thread 4 +run: Thread 3 +run: Thread 2 +run: Thread 1 +run: Thread 10 +run: Thread 9 +Finished! +``` + + + +> **场景2:指定执行时间的情况,超过这个任务就不继续等待了,完成多少算多少。** + +```java +public class CountDownLatchExample2 { + //线程数量 + private static int threadCount=10; + + public static void main(String[] args) throws InterruptedException { + ExecutorService executorService= Executors.newCachedThreadPool(); + + final CountDownLatch countDownLatch=new CountDownLatch(threadCount); + + + for (int i = 1; i <= threadCount; i++) { + final int threadNum=i; + executorService.execute(()->{ + try { + test(threadNum); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(10, TimeUnit.MILLISECONDS); + //上面线程如果在10 毫秒内未完成,则有可能会执行主线程 + System.out.println("Finished!"); + executorService.shutdown(); + } + + private static void test(int threadNum) throws InterruptedException { + Thread.sleep(10); + System.out.println("run: Thread "+threadNum); + } +} +``` + +```html +run: Thread 10 +run: Thread 4 +run: Thread 3 +run: Thread 2 +Finished! +run: Thread 9 +run: Thread 5 +run: Thread 8 +run: Thread 6 +run: Thread 7 +run: Thread 1 +``` + +### CyclicBarrier + +用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被拦截的线程才会继续执行。 + +和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。 + +CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。 + +CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。 + +```java +public CyclicBarrier(int parties, Runnable barrierAction) { + if (parties <= 0) throw new IllegalArgumentException(); + this.parties = parties; + this.count = parties; + this.barrierCommand = barrierAction; +} + +public CyclicBarrier(int parties) { + this(parties, null); +} +``` + + + +```java +public class CyclicBarrierExample { + //指定必须有6个运动员到达才行 + private static CyclicBarrier barrier = new CyclicBarrier(6, () -> { + System.out.println("所有运动员入场,裁判员一声令下!!!!!"); + }); + public static void main(String[] args) { + System.out.println("运动员准备进场,全场欢呼............"); + + ExecutorService service = Executors.newFixedThreadPool(6); + for (int i = 0; i < 6; i++) { + service.execute(() -> { + try { + System.out.println(Thread.currentThread().getName() + " 运动员,进场"); + barrier.await(); + System.out.println(Thread.currentThread().getName() + " 运动员出发"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + }); + } + + service.shutdown(); + } +} +``` + +```html +运动员准备进场,全场欢呼............ +pool-1-thread-1 运动员,进场 +pool-1-thread-2 运动员,进场 +pool-1-thread-3 运动员,进场 +pool-1-thread-4 运动员,进场 +pool-1-thread-5 运动员,进场 +pool-1-thread-6 运动员,进场 +所有运动员入场,裁判员一声令下!!!!! +pool-1-thread-6 运动员出发 +pool-1-thread-1 运动员出发 +pool-1-thread-4 运动员出发 +pool-1-thread-3 运动员出发 +pool-1-thread-2 运动员出发 +pool-1-thread-5 运动员出发 +``` + +> **场景:多线程计算数据,最后合并计算结果。** + +```java +public class CyclicBarrierExample2 { + private static int threadCount = 10; + + public static void main(String[] args) throws InterruptedException { + CyclicBarrier cyclicBarrier=new CyclicBarrier(5); + + ExecutorService executorService= Executors.newCachedThreadPool(); + + + for (int i = 0; i < threadCount; i++) { + final int threadNum=i; + executorService.execute(()->{ + try { + System.out.println("before..."+threadNum); + cyclicBarrier.await(); + System.out.println("after..."+threadNum); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + executorService.shutdown(); + } +} +``` + +```html +before...1 +before...4 +before...3 +before...2 +before...6 +before...5 +before...7 +before...8 +after...6 +before...9 +after...1 +after...2 +before...10 +after...3 +after...7 +after...8 +after...4 +after...9 +after...5 +after...10 +``` + +> **CountDownLatch 与 CyclicBarrier 比较** + +- CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完后再执行。CountDownLatch 强调一个线程等多个线程完成某件事情; + + CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 + +- 调用 CountDownLatch 的 countDown 方法后,当前线程并不会阻塞,会继续往下执行; + + 调用 CyclicBarrier 的 await 方法,会阻塞当前线程,直到 CyclicBarrier 指定的线程全部都到达了指定点时,才能继续往下执行。 + +- CountDownLatch 方法比较少,操作比较简单; + + CyclicBarrier 提供的方法更多,比如能够通过 getNumberWaiting(),isBroken() 等方法获取当前多个线程的状态,并且 **CyclicBarrier 的构造方法可以传入 barrierAction**,指定当所有线程都到达时执行的业务功能。 + +- CountDownLatch 是不能复用的; + + CyclicLatch 是可以复用的。 + + + +### Semaphore + +Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。 + +其中`acquire()`方法,用来获取资源,`release()`方法用来释放资源。Semaphore维护了当前访问的个数,通过提供**同步机制**来控制同时访问的个数。 + +> **场景:特殊资源的并发访问控制** + +比如数据库的连接数最大只有 20,而上层的并发数远远大于 20,这时候如果不作限制, 可能会由于无法获取连接而导致并发异常,这时候可以使用 Semaphore 来进行控制。当信号量设置为1的时候,就和单线程很相似了。 + +```java +//每次获取一个许可 +public class SemaphoreExample { + private static int clientCount = 3; + private static int totalRequestCount = 10; + + public static void main(String[] args) { + ExecutorService executorService= Executors.newCachedThreadPool(); + + //Semaphore允许的最大许可数为 clientCount , + //也就是允许的最大并发执行的线程个数为 clientCount + final Semaphore semaphore=new Semaphore(clientCount); + + for(int i=1;i<=totalrequestcount;i++){ + final int threadNum=i; + executorService.execute(() -> { + try{ + semaphore.acquire();//每次获取一个许可 + test(threadNum); + }catch (InterruptedException e) { + e.printStackTrace(); + }finally { + semaphore.release(); //释放一个许可 + } + }); + } + executorService.shutdown(); + } + + private static void test(int threadNum) throws InterruptedException { + System.out.println("run: "+threadNum); + Thread.sleep(1000); // 线程睡眠 1 s + } +} +``` + +```html +run: 1 +run: 2 +run: 3 +//---- 1 s ----- +run: 4 +run: 5 +run: 6 +//---- 1 s ----- +run: 7 +run: 8 +run: 9 +//---- 1 s ----- +run: 10 +``` + + + +```java +//每次获取多个许可 +public class SemaphoreExample2 { + private static int clientCount = 3; + private static int totalRequestCount = 10; + + public static void main(String[] args) { + ExecutorService executorService= Executors.newCachedThreadPool(); + + final Semaphore semaphore=new Semaphore(clientCount); + + for(int i=1;i<=totalrequestcount;i++){ + final int threadNum=i; + executorService.execute(() -> { + try{ + semaphore.acquire(3);//每次获取3个许可 + //并发数是 3 ,一次性获取 3 个许可,同 1s 内无其他许可释放,相当于单线程 + test(threadNum); + }catch (InterruptedException e) { + e.printStackTrace(); + }finally { + semaphore.release(3); //释放 3 个许可 + } + }); + } + executorService.shutdown(); + } + + private static void test(int threadNum) throws InterruptedException { + System.out.println("run: "+threadNum); + Thread.sleep(1000); // 线程睡眠 1 s + } +} +``` + +```html +run: 1 +//---- 1 s ----- +run: 2 +//---- 1 s ----- +run: 3 +//---- 1 s ----- +run: 4 +//---- 1 s ----- +run: 5 +//---- 1 s ----- +run: 6 +//---- 1 s ----- +run: 7 +//---- 1 s ----- +run: 8 +//---- 1 s ----- +run: 10 +//---- 1 s ----- +run: 9 +``` + + + +```java +public class SemaphoreExample3 { + private static int clientCount = 3; + private static int totalRequestCount = 10; + + public static void main(String[] args) { + ExecutorService executorService= Executors.newCachedThreadPool(); + + final Semaphore semaphore=new Semaphore(clientCount); + + for(int i=1;i<=totalrequestcount;i++){ + final int threadNum=i; + executorService.execute(() -> { + try{ + if(semaphore.tryAcquire()){ + //尝试获取一个许可 + test(threadNum); + semaphore.release(); + } + }catch (InterruptedException e) { + e.printStackTrace(); + } + }); + } + executorService.shutdown(); + } + + private static void test(int threadNum) throws InterruptedException { + System.out.println("run: "+threadNum); + Thread.sleep(1000); // 线程睡眠 1 s + } +} +``` + +```html +run: 1 +run: 2 +run: 3 +``` + +## 九、J.U.C - 其它组件 + +### FutureTask + +在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。 + +```java +public class FutureTask implements RunnableFuture +``` + +```java +public interface RunnableFuture extends Runnable, Future +``` + +FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。 + +```java +public class FutureTaskExample { + + public static void main(String[] args) throws ExecutionException, InterruptedException { + FutureTask futureTask = new FutureTask(new Callable() { + @Override + public Integer call() throws Exception { + int result = 0; + for (int i = 0; i < 100; i++) { + Thread.sleep(10); + result += i; + } + return result; + } + }); + + Thread computeThread = new Thread(futureTask); + computeThread.start(); + + Thread otherThread = new Thread(() -> { + System.out.println("other task is running..."); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + otherThread.start(); + System.out.println(futureTask.get()); + } +} +``` + +```java +other task is running... +4950 +``` + +### BlockingQueue + +java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: + +- **FIFO 队列** :LinkedBlockingQueue、ArrayBlockingQueue(固定长度) +- **优先级队列** :PriorityBlockingQueue + +提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。 + +**使用 BlockingQueue 实现生产者消费者问题** + +```java +public class ProducerConsumer { + + private static BlockingQueue queue = new ArrayBlockingQueue(5); + + private static class Producer extends Thread { + @Override + public void run() { + try { + queue.put("product"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.print("produce.."); + } + } + + private static class Consumer extends Thread { + + @Override + public void run() { + try { + String product = queue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.print("consume.."); + } + } +} +``` + +```java +public static void main(String[] args) { + for (int i = 0; i < 2; i++) { + Producer producer = new Producer(); + producer.start(); + } + for (int i = 0; i < 5; i++) { + Consumer consumer = new Consumer(); + consumer.start(); + } + for (int i = 0; i < 3; i++) { + Producer producer = new Producer(); + producer.start(); + } +} +``` + +```html +produce..produce..consume..consume..produce..consume..produce..consume..produce..consume.. +``` + +### ForkJoin + +主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。 + +```java +public class ForkJoinExample extends RecursiveTask { + + private final int threshold = 5; + private int first; + private int last; + + public ForkJoinExample(int first, int last) { + this.first = first; + this.last = last; + } + + @Override + protected Integer compute() { + int result = 0; + if (last - first <= threshold) { + // 任务足够小则直接计算 + for (int i = first; i <= last; i++) { + result += i; + } + } else { + // 拆分成小任务 + int middle = first + (last - first) / 2; + ForkJoinExample leftTask = new ForkJoinExample(first, middle); + ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last); + leftTask.fork(); + rightTask.fork(); + result = leftTask.join() + rightTask.join(); + } + return result; + } +} +``` + +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + ForkJoinExample example = new ForkJoinExample(1, 10000); + ForkJoinPool forkJoinPool = new ForkJoinPool(); + Future result = forkJoinPool.submit(example); + System.out.println(result.get()); +} +``` + +ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。 + +```java +public class ForkJoinPool extends AbstractExecutorService +``` + +ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。 + + + +## 十、生产者与消费者模式 + +生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。 + +所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是**生产者线程用于生产数据**,另一种是**消费者线程用于消费数据**,为了解耦生产者和消费者的关系,通常会采用**共享数据区域**。 + +共享数据区域就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能: + +- 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内 +- 如果共享数据区为空的话,阻塞消费者继续消费数据 + +![](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_4.png) + +### wait() / notify() 潜在的一些问题 + +> **notify() 早期通知** + +线程 A 还没开始 wait 的时候,线程 B 已经 notify 了。线程 B 通知是没有任何响应的,当线程 B 退出同步代码块后,线程 A 再开始 wait,便会一直阻塞等待,直到被别的线程打断。 + +```java +public class EarlyNotify { + public static void main(String[] args) { + final Object lock = new Object(); + + Thread notifyThread = new Thread(new NotifyThread(lock),"notifyThread"); + Thread waitThread = new Thread(new WaitThread(lock),"waitThread"); + notifyThread.start(); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + waitThread.start(); + } + + private static class WaitThread implements Runnable{ + private Object lock; + + public WaitThread(Object lock){ + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + try { + System.out.println(Thread.currentThread().getName() + " 进去代码块"); + System.out.println(Thread.currentThread().getName() + " 开始 wait"); + lock.wait(); + System.out.println(Thread.currentThread().getName() + " 结束 wait"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + private static class NotifyThread implements Runnable{ + private Object lock; + + public NotifyThread(Object lock){ + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + System.out.println(Thread.currentThread().getName() + " 进去代码块"); + System.out.println(Thread.currentThread().getName() + " 开始 notify"); + lock.notify(); + System.out.println(Thread.currentThread().getName() + " 结束开始 notify"); + } + } + } +} +``` + +```html +notifyThread 进去代码块 +notifyThread 开始 notify +notifyThread 结束开始 notify +waitThread 进去代码块 +waitThread 开始 wait +``` + +示例中开启了两个线程,一个是 WaitThread,另一个是 NotifyThread。NotifyThread 先启动,先调用 notify() 方法。然后 WaitThread 线程才启动,调用 wait() 方法,但是由于已经通知过了,wait() 方法就无法再获取到相应的通知,因此 WaitThread 会一直在 wait() 方法处阻塞,这种现象就是**通知过早**的现象。 + +这种现象的解决方法:添加一个状态标志,让 WaitThread 调用 wait() 方法前先判断状态是否已经改变,如果通知早已发出的话,WaitThread 就不再调用 wait() 方法。对上面的代码进行更正: + +```java +public class EarlyNotify2 { + private static boolean isWait = true; //isWait 判断线程是否需要等待 + + public static void main(String[] args) { + final Object lock = new Object(); + + Thread notifyThread = new Thread(new NotifyThread(lock),"notifyThread"); + Thread waitThread = new Thread(new WaitThread(lock),"waitThread"); + notifyThread.start(); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + waitThread.start(); + } + + private static class WaitThread implements Runnable{ + private Object lock; + + public WaitThread(Object lock){ + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + try { + System.out.println(Thread.currentThread().getName() + " 进去代码块"); + while (isWait){ + System.out.println(Thread.currentThread().getName() + " 开始 wait"); + lock.wait(); + System.out.println(Thread.currentThread().getName() + " 结束 wait"); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + private static class NotifyThread implements Runnable{ + private Object lock; + + public NotifyThread(Object lock){ + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + System.out.println(Thread.currentThread().getName() + " 进去代码块"); + System.out.println(Thread.currentThread().getName() + " 开始 notify"); + lock.notify(); + isWait = false; //已经唤醒了 + System.out.println(Thread.currentThread().getName() + " 结束开始 notify"); + } + } + } +} +``` + +```html +notifyThread 进去代码块 +notifyThread 开始 notify +notifyThread 结束开始 notify +waitThread 进去代码块 +``` + +增加了一个 isWait 状态变量,NotifyThread 调用 notify() 方法后会对状态变量进行更新,在 WaitThread 中调用wait() 方法之前会先对状态变量进行判断,在该示例中,调用 notify() 后将状态变量 isWait 改变为 false ,因此,在 WaitThread 中 while 对 isWait 判断后就不会执行 wait 方法,从而**避免了Notify过早通知造成遗漏的情况。** + +小结:在使用线程的等待 / 通知机制时,一般都要配合一个 boolean 变量值(或者其他能够判断真假的条件),在 notify 之前改变该 boolean 变量的值,让 wait 返回后能够退出 while 循环(一般都要在 wait 方法外围加一层 while 循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在 wait() 方法处。这样便保证了程序的正确性。 + +> **等待 wait 的条件发生变化** + +如果线程在等待时接受到了通知,但是之后**等待的条件**发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。 + +```java +public class ConditionChange { + public static void main(String[] args) { + final List lock = new ArrayList(); + Thread consumer1 = new Thread(new Consumer(lock),"consume1"); + Thread consumer2 = new Thread(new Consumer(lock),"consume1"); + Thread producer = new Thread(new Producer(lock),"producer"); + consumer1.start(); + consumer2.start(); + producer.start(); + } + + static class Consumer implements Runnable{ + private List lock; + + public Consumer(List lock) { + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + try { + //这里使用if的话,就会存在 wait 条件变化造成程序错误的问题 + if(lock.isEmpty()) { + System.out.println(Thread.currentThread().getName() + " list 为空"); + System.out.println(Thread.currentThread().getName() + " 调用 wait 方法"); + lock.wait(); + System.out.println(Thread.currentThread().getName() + " wait 方法结束"); + } + String element = lock.remove(0); + System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + static class Producer implements Runnable{ + private List lock; + + public Producer(List lock) { + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + System.out.println(Thread.currentThread().getName() + " 开始添加元素"); + lock.add(Thread.currentThread().getName()); + lock.notifyAll(); + } + } + } +} +``` + +```html +consume1 list 为空 +consume1 调用 wait 方法 +consume2 list 为空 +consume2 调用 wait 方法 +producer 开始添加元素 +consume2 wait 方法结束 +Exception in thread "consume1" consume2 取出第一个元素为:producer +java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 +consume1 wait 方法结束 +``` + +异常原因分析:例子中一共开启了 3 个线程:consumer1,consumer2 以及 producer。 + +首先 consumer1调用了 wait 方法后,线程处于了 WAITTING 状态,并且将对象锁释放出来。consumer2 能够获取对象锁,从而进入到同步代块中,当执行到 wait 方法时,同样的也会释放对象锁。productor 能够获取到对象锁,进入到同步代码块中,向 list 中插入数据后,通过 notifyAll 方法通知处于 WAITING 状态的 consumer1 和consumer2 。consumer2 得到对象锁后,从 wait 方法出退出,退出同步块,释放掉对象锁,然后删除了一个元素,list 为空。这个时候 consumer1 获取到对象锁后,从 wait 方法退出,继续往下执行,这个时候consumer1 再执行 + +```java +lock.remove(0) +``` + +就会出错,因为 list 由于 consumer2 删除一个元素之后已经为空了。 + +解决方案:通过上面的分析,可以看出 consumer1 报异常是因为线程从 wait 方法退出之后没有再次对 wait 条件进行判断,因此,此时的 wait 条件已经发生了变化。解决办法就是,在 wait 退出之后再对条件进行判断即可。 + +```java +public class ConditionChange2 { + public static void main(String[] args) { + final List lock = new ArrayList(); + Thread consumer1 = new Thread(new Consumer(lock),"consume1"); + Thread consumer2 = new Thread(new Consumer(lock),"consume2"); + Thread producer = new Thread(new Producer(lock),"producer"); + consumer1.start(); + consumer2.start(); + producer.start(); + } + + static class Consumer implements Runnable{ + private List lock; + + public Consumer(List lock) { + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + try { + //这里使用if的话,就会存在 wait 条件变化造成程序错误的问题 + while(lock.isEmpty()) { + System.out.println(Thread.currentThread().getName() + " list 为空"); + System.out.println(Thread.currentThread().getName() + " 调用 wait 方法"); + lock.wait(); + System.out.println(Thread.currentThread().getName() + " wait 方法结束"); + } + String element = lock.remove(0); + System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + static class Producer implements Runnable{ + private List lock; + + public Producer(List lock) { + this.lock = lock; + } + + @Override + public void run() { + synchronized (lock) { + System.out.println(Thread.currentThread().getName() + " 开始添加元素"); + lock.add(Thread.currentThread().getName()); + lock.notifyAll(); + } + } + } +} +``` + +```html +consume1 list 为空 +consume1 调用 wait 方法 +consume2 list 为空 +consume2 调用 wait 方法 +producer 开始添加元素 +consume2 wait 方法结束 +consume2 取出第一个元素为:producer +consume1 wait 方法结束 +consume1 list 为空 +consume1 调用 wait 方法 +``` + +上面的代码与之前的代码仅仅只是将 wait() 外围的 if 语句改为 while 循环即可,这样当 list 为空时,线程便会继续等待,而不会继续去执行删除 list 中元素的代码。 + +小结:在使用线程的等待/通知机制时,一般都要在 while 循环中调用 wait()方法,配合使用一个 boolean 变量(或其他能判断真假的条件,如本文中的 list.isEmpty())。在满足 while 循环的条件时,进入 while 循环,执行 wait()方法,不满足 while 循环的条件时,跳出循环,执行后面的代码。 + +> **"假死"状态** + +如果是多消费者和多生产者情况,如果使用 notify() 方法可能会出现"假死"的情况,即唤醒的是同类线程。 + +原因分析:假设当前多个生产者线程会调用 wait 方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify 通知处于 WAITTING 状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。 + +解决办法:将 notify() 方法替换成 notifyAll() 方法,如果使用的是 lock 的话,就将 signal() 方法替换成 signalAll()方法。 + +> **Object提供的消息通知机制总结** + +- 永远在 while 循环中对条件进行判断而不是 if 语句中进行 wait 条件的判断 +- 使用 notifyAll() 而不是使用 notify() + +基本的使用范式如下: + +```java +// The standard idiom for calling the wait method in Java +synchronized (sharedObject) { + while (condition) { + sharedObject.wait(); + // (Releases lock, and reacquires on wakeup) + } + // do action based upon condition e.g. take or put into queue +} +``` + +### 1、wait() / notifyAll() 实现生产者-消费者 + +```java +public class ProducerConsumer { + public static void main(String[] args) { + final LinkedList linkedList = new LinkedList(); + final int capacity = 5; + Thread producer = new Thread(new Producer(linkedList,capacity),"producer"); + Thread consumer = new Thread(new Consumer(linkedList),"consumer"); + + producer.start(); + consumer.start(); + } + + static class Producer implements Runnable { + private List list; + private int capacity; + + public Producer(List list, int capacity) { + this.list = list; + this.capacity = capacity; + } + + @Override + public void run() { + while (true) { + synchronized (list) { + try { + while (list.size() == capacity) { + System.out.println("生产者 " + Thread.currentThread().getName() + " list 已达到最大容量,进行 wait"); + list.wait(); + System.out.println("生产者 " + Thread.currentThread().getName() + " 退出 wait"); + } + Random random = new Random(); + int i = random.nextInt(); + System.out.println("生产者 " + Thread.currentThread().getName() + " 生产数据" + i); + list.add(i); + list.notifyAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + } + } + } + + static class Consumer implements Runnable { + private List list; + + public Consumer(List list) { + this.list = list; + } + + @Override + public void run() { + while (true) { + synchronized (list) { + try { + while (list.isEmpty()) { + System.out.println("消费者 " + Thread.currentThread().getName() + " list 为空,进行 wait"); + list.wait(); + System.out.println("消费者 " + Thread.currentThread().getName() + " 退出wait"); + } + Integer element = list.remove(0); + System.out.println("消费者 " + Thread.currentThread().getName() + " 消费数据:" + element); + list.notifyAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + } +} +``` + +输出结果: + +```html +生产者 producer 生产数据-1652445373 +生产者 producer 生产数据1234295578 +生产者 producer 生产数据-1885445180 +生产者 producer 生产数据864400496 +生产者 producer 生产数据621858426 +生产者 producer list 已达到最大容量,进行 wait +消费者 consumer 消费数据:-1652445373 +消费者 consumer 消费数据:1234295578 +消费者 consumer 消费数据:-1885445180 +消费者 consumer 消费数据:864400496 +消费者 consumer 消费数据:621858426 +消费者 consumer list 为空,进行 wait +生产者 producer 退出 wait +``` + +### 2、await() / signalAll() 实现生产者-消费者 + +```java +public class ProducerConsumer { + private static ReentrantLock lock = new ReentrantLock(); + private static Condition full = lock.newCondition(); + private static Condition empty = lock.newCondition(); + + public static void main(String[] args) { + final LinkedList linkedList = new LinkedList(); + final int capacity = 5; + Thread producer = new Thread(new Producer(linkedList,capacity,lock),"producer"); + Thread consumer = new Thread(new Consumer(linkedList,lock),"consumer"); + + producer.start(); + consumer.start(); + } + + static class Producer implements Runnable { + private List list; + private int capacity; + private Lock lock; + + public Producer(List list, int capacity,Lock lock) { + this.list = list; + this.capacity = capacity; + this.lock = lock; + } + + @Override + public void run() { + while (true) { + lock.lock(); + try { + while (list.size() == capacity) { + System.out.println("生产者 " + Thread.currentThread().getName() + " list 已达到最大容量,进行 wait"); + full.await(); + System.out.println("生产者 " + Thread.currentThread().getName() + " 退出 wait"); + } + Random random = new Random(); + int i = random.nextInt(); + System.out.println("生产者 " + Thread.currentThread().getName() + " 生产数据" + i); + list.add(i); + empty.signalAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + lock.unlock(); + } + + } + } + } + + static class Consumer implements Runnable { + private List list; + private Lock lock; + + public Consumer(List list,Lock lock) { + this.list = list; + this.lock = lock; + } + + @Override + public void run() { + while (true) { + lock.lock(); + try { + while (list.isEmpty()) { + System.out.println("消费者 " + Thread.currentThread().getName() + " list 为空,进行 wait"); + empty.await(); + System.out.println("消费者 " + Thread.currentThread().getName() + " 退出wait"); + } + Integer element = list.remove(0); + System.out.println("消费者 " + Thread.currentThread().getName() + " 消费数据:" + element); + full.signalAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + lock.unlock(); + } + } + } + } +} +``` + +```html +生产者 producer 生产数据-1748993481 +生产者 producer 生产数据-131075825 +生产者 producer 生产数据-683676621 +生产者 producer 生产数据1543722525 +生产者 producer 生产数据804266076 +生产者 producer list 已达到最大容量,进行 wait +消费者 consumer 消费数据:-1748993481 +消费者 consumer 消费数据:-131075825 +消费者 consumer 消费数据:-683676621 +消费者 consumer 消费数据:1543722525 +消费者 consumer 消费数据:804266076 +消费者 consumer list 为空,进行 wait +生产者 producer 退出 wait +``` + +### 3、BlockingQueue 实现生产者-消费者 + +由于 BlockingQueue 内部实现就附加了两个阻塞操作。即 + +- 当队列已满时,阻塞向队列中插入数据的线程,直至队列中未满 +- 当队列为空时,阻塞从队列中获取数据的线程,直至队列非空时为止 + +可以利用 BlockingQueue 实现生产者-消费者,**阻塞队列完全可以充当共享数据区域**,就可以很好的完成生产者和消费者线程之间的协作。 + +```java +public class ProducerConsumer { + private static LinkedBlockingQueue queue = new LinkedBlockingQueue(); + + public static void main(String[] args) { + Thread producer = new Thread(new Producer(queue),"producer"); + Thread consumer = new Thread(new Consumer(queue),"consumer"); + + producer.start(); + consumer.start(); + } + + static class Producer implements Runnable { + private BlockingQueue queue; + + public Producer(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public void run() { + while (true) { + Random random = new Random(); + int i = random.nextInt(); + System.out.println("生产者" + Thread.currentThread().getName() + "生产数据" + i); + try { + queue.put(i); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + static class Consumer implements Runnable { + private BlockingQueue queue; + + public Consumer(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public void run() { + while (true) { + try { + Integer element = (Integer) queue.take(); + System.out.println("消费者" + Thread.currentThread().getName() + "正在消费数据" + element); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } +} +``` + +```html +生产者producer生产数据-222876564 +消费者consumer正在消费数据-906876105 +生产者producer生产数据-9385856 +消费者consumer正在消费数据1302744938 +生产者producer生产数据-177925219 +生产者producer生产数据-881052378 +生产者producer生产数据-841780757 +生产者producer生产数据-1256703008 +消费者consumer正在消费数据1900668223 +消费者consumer正在消费数据2070540191 +消费者consumer正在消费数据1093187 +消费者consumer正在消费数据6614703 +消费者consumer正在消费数据-1171326759 +``` + +使用 BlockingQueue 来实现生产者-消费者很简洁,这正是利用了 BlockingQueue 插入和获取数据附加阻塞操作的特性。 + +## 参考资料 + +- [Java 并发知识总结](https://github.com/CL0610/Java-concurrency) +- [CS-Notes Java并发](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Java%20%E5%B9%B6%E5%8F%91.md) +- 《 Java 并发编程的艺术》 +- [剑指Java面试-Offer直通车](https://coding.imooc.com/class/303.html) From bfb1546554dd935f00d1ec1cf4f655af2681fe80 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: Tue, 9 Jul 2019 17:10:35 +0800 Subject: [PATCH 25/29] Update 11HTTPs.md --- docs/NetWork/11HTTPs.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/NetWork/11HTTPs.md b/docs/NetWork/11HTTPs.md index 51ca883..ea03eb4 100644 --- a/docs/NetWork/11HTTPs.md +++ b/docs/NetWork/11HTTPs.md @@ -72,6 +72,14 @@ HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内 HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 +## 连接建立过程 + +HTTPs的连接建立过程比较复杂,大致情况如下图所示: + +
+ +
+ ## HTTPs 的缺点 - 因为需要进行加密解密等过程,因此速度会更慢; From 464df35a48e5b03e2a0cab50ebad75fd618c0300 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年7月11日 15:50:08 +0800 Subject: [PATCH 26/29] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...03347円275円221円347円273円234円345円261円202円.md" | 8 +++--- .../09HTTP351円246円226円351円203円250円.md" | 0 ...11345円244円247円347円211円271円346円200円247円.md" | 27 ++++++++++++++----- ...1347円273円234円-347円233円256円345円275円225円.md" | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) rename "docs/NetWork/09HTTP 351円246円226円351円203円250円.md" => "docs/NetWork/09HTTP351円246円226円351円203円250円.md" (100%) diff --git "a/docs/NetWork/03347円275円221円347円273円234円345円261円202円.md" "b/docs/NetWork/03347円275円221円347円273円234円345円261円202円.md" index 8293df4..a5a9166 100644 --- "a/docs/NetWork/03347円275円221円347円273円234円345円261円202円.md" +++ "b/docs/NetWork/03347円275円221円347円273円234円345円261円202円.md" @@ -112,7 +112,7 @@ ARP 实现由 IP 地址得到 MAC 地址。 ## 网际控制报文协议 ICMP -ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 +ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。由于IP协议并不是一个可靠的协议,它不保证数据被送达,那么,自然地,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。它封装在 IP 数据报中,但是不属于高层协议。
@@ -124,17 +124,17 @@ ICMP 报文分为差错报告报文和询问报文。 Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 -Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 +**Ping 的原理:**通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 ### 2. Traceroute -Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 +Traceroute 是 ICMP 的另一个应用,用来**跟踪一个分组从源点到终点的路径**。 Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。 - 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; - 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 -- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。这样,**Traceroute 就拿到了所有的路由器IP,从而避开了IP头只能记录有限路由IP的问题。**但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 - 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 ## 虚拟专用网 VPN diff --git "a/docs/NetWork/09HTTP 351円246円226円351円203円250円.md" "b/docs/NetWork/09HTTP351円246円226円351円203円250円.md" similarity index 100% rename from "docs/NetWork/09HTTP 351円246円226円351円203円250円.md" rename to "docs/NetWork/09HTTP351円246円226円351円203円250円.md" diff --git "a/docs/OO/04351円235円242円345円220円221円345円257円271円350円261円241円344円270円211円345円244円247円347円211円271円346円200円247円.md" "b/docs/OO/04351円235円242円345円220円221円345円257円271円350円261円241円344円270円211円345円244円247円347円211円271円346円200円247円.md" index d379467..05f839f 100644 --- "a/docs/OO/04351円235円242円345円220円221円345円257円271円350円261円241円344円270円211円345円244円247円347円211円271円346円200円247円.md" +++ "b/docs/OO/04351円235円242円345円220円221円345円257円271円350円261円241円344円270円211円345円244円247円347円211円271円346円200円247円.md" @@ -26,9 +26,9 @@ ### 3. 成员变量和局部变量的区别 | 区别 | 成员变量 | 局部变量 | -| :--: | :--: | :--: | +| :--: | :--: | :--: | | 在类中位置不同 | 类中方法外 | 方法内或者方法声明上 | -| 在内存中位置不同 | 堆内存 | 栈内存 | +| 在内存中位置不同 | 堆内存 | 栈内存 | | 声明周期不同 | 随着**对象**存在而存在,随着对象消失而消失 | 随着**方法**调用而存在,随着方法调用完毕而消失 | | 初始化值不同 | 有默认的初始值 | 没有默认的初始值,必须先定义,赋值,才能使用 | @@ -189,7 +189,7 @@ public class SmartPhoneDemo2 { | 区别 | 静态变量 | 成员变量 | | :--: | :--: | :--: | -| 所属不同 | 属于类,所以也称为**类变量** | 属于对象,所以也称为**实例变量**(对象变量) | +| 所属不同 | 属于类,所以也称为**类变量** | 属于对象,所以也称为**实例变量**(对象变量) | | 内存中位置不同 | 存储于**方法区**的静态区 | 存储于堆内存 | | 内存中出现时间不同 | 随着**类**的加载而加载,随着类的消失而消失 | 随着**对象**的创建而存在,随着对象的消失而消失 | | 调用不同 | 可以通过类名调用,也可以通过对象调用 | 只能通过的对象来调用 | @@ -267,7 +267,8 @@ Animal animal = new Cat(); ### 5. 继承中成员变量的关系 - 子类中成员变量和父类中成员变量名称不同。 - 子类中成员变量和父类中成员变量名称相同。 - + + 在子类方法中的查找顺序: 在子类方法的局部范围找,有就使用 @@ -275,7 +276,7 @@ Animal animal = new Cat(); 在子类的成员范围找,有就使用 在**父类的成员范围**找,有就使用 - + 如果还找不到,就报错 ### 6. 继承中构造方法的关系 @@ -290,7 +291,7 @@ Animal animal = new Cat(); 3. 如果父类中没有无参构造方法,该怎么办呢? 方式一:子类通过super去显示调用父类其他的带参的构造方法 - + 方式二:子类通过this去调用本类的其他构造方法 (子类一定要有一个去访问父类的构造方法,否则父类数据就没有初始化) @@ -746,4 +747,16 @@ public class PolymorphismDemo2 { 100 show Zi function Fu -``` \ No newline at end of file +``` + +### 6. Java中多态实现的原理* + +多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于**方法表**,通过继承和接口的多态实现有所不同。 + +- 继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的**偏移量**,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。 因为方法表的偏移量总是**固定的**,所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个**定值**。 + +- 接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的**位置就可能不一样了**。所以不能通过偏移量的方法,而是通过**搜索完整的方法表**。 + +# 参考资料 + +- [Java技术——多态的实现原理]() \ No newline at end of file diff --git "a/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" "b/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" index 4aa3ab3..24c2c4e 100644 --- "a/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" +++ "b/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" @@ -14,7 +14,7 @@ - [基础概念](./NetWork/06基础概念.md) - [HTTP 方法](./NetWork/07HTTP%%方法.md) - [HTTP 状态码](./NetWork/08HTTP%%状态码.md) -- [HTTP 首部](./NetWork/09HTTP%%首部.md) +- [HTTP 首部](./NetWork/09HTTP首部.md) - [具体应用](./NetWork/10具体应用.md) - [HTTPs](./NetWork/11HTTPs.md) - [HTTP20](./NetWork/12HTTP20.md) From e40eb28e959f9c40da3be92bc37170c412d94cae Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年7月12日 08:54:45 +0800 Subject: [PATCH 27/29] =?UTF-8?q?Update=20Java=20=E5=A4=9A=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E4=B8=8E=E5=B9=B6=E5=8F=91-=E5=8E=9F=E7=90=86.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1266円345円217円221円-345円216円237円347円220円206円.md" | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git "a/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" "b/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" index 3b999f7..d9c18df 100644 --- "a/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" +++ "b/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" @@ -360,6 +360,20 @@ public class SyncBlockAndMethod { synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是 **ACC_SYNCHRONIZED 标识**,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 +### synchronized的可重入性 + +如果一个获取锁的线程调用其它的synchronized修饰的方法,会发生什么? + +从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。 + +我们回来看synchronized,synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是**永远可以拿到锁的**。 + +在 Java 内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,**同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入**。原因是 Java 中线程获得对象锁的操作是以**线程为单位**的,而不是以调用为单位的。 + +### synchronized可重入锁的实现 + +每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。 + ### JDK1.6 之后的锁优化 JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等**技术**来减少锁操作的开销。 @@ -3677,3 +3691,4 @@ public static void main(String[] args) { - [Java 并发知识总结](https://github.com/CL0610/Java-concurrency) - [CS-Notes Java并发](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Java%20%E5%B9%B6%E5%8F%91.md) - [《 Java 并发编程的艺术》]() +- [Java多线程:synchronized的可重入性](https://www.cnblogs.com/cielosun/p/6684775.html) From 01803905ff40880c8a14285912ef8ca81587eb69 Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年7月31日 09:49:27 +0800 Subject: [PATCH 28/29] =?UTF-8?q?=E7=AC=94=E8=AE=B0=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BigData/Zookeeper.md | 13 +++++++++++++ docs/DataBase/Redis.md | 2 ++ ...271266円345円217円221円-345円216円237円347円220円206円.md" | 6 ++++++ docs/Spring/02SpringAOP.md | 2 +- ...270円270円350円247円201円346円263円250円350円247円243円.md" | 13 ++++++++++++- ...275221円347円273円234円-347円233円256円345円275円225円.md" | 1 + 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/BigData/Zookeeper.md b/docs/BigData/Zookeeper.md index b05f72c..a7266a3 100644 --- a/docs/BigData/Zookeeper.md +++ b/docs/BigData/Zookeeper.md @@ -199,6 +199,19 @@ ZAB 协议两种基本的模式:崩溃恢复和消息广播。 当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进人**消息广播模式**了。当一台同样遵守 ZAB 协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,**Zookeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理**。Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器。 +# ZooKeeper的选举机制 + +当leader崩溃或者leader失去大多数的follower,这时zk进入恢复模式,恢复模式需要重新选举出一个新leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。 + +1. Zookeeper选主流程(basic paxos) + (1)选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐Server; + (2)选举线程首先向所有Server发起一次询问(包括自己); + (3)选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中; + (4)收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server; + (5)线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。 通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1. 每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。 +2. Zookeeper选主流程(fast paxos) + fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。 + # Zookeeper 的应用场景 ## 分布式协调/通知 diff --git a/docs/DataBase/Redis.md b/docs/DataBase/Redis.md index 057ce36..0a0c7ba 100644 --- a/docs/DataBase/Redis.md +++ b/docs/DataBase/Redis.md @@ -1,5 +1,7 @@ ## 一、概述 +[Redis脑图](http://naotu.baidu.com/file/b2cc07d1dabb080eac294567c31bbe04?token=6dcf94de0168d614) + Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 diff --git "a/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" "b/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" index d9c18df..cb71d12 100644 --- "a/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" +++ "b/docs/Java/Java 345円244円232円347円272円277円347円250円213円344円270円216円345円271円266円345円217円221円-345円216円237円347円220円206円.md" @@ -323,6 +323,12 @@ public class Singleton { 考虑下面的实现,也就是只使用了一个 if 语句。 在 `uniqueInstance == null` 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,那么就会进行两次实例化。 +### 锁的内存语义 + +- 锁的释放-获取遵循Happens-before +- 当线程释放锁的时候,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中 +- 当线程获取锁的时候,JMM会把该线程对应的工作内存中的共享变量置为无效 + ### synchronized 原理 ```java diff --git a/docs/Spring/02SpringAOP.md b/docs/Spring/02SpringAOP.md index c1d87bd..d58e67a 100644 --- a/docs/Spring/02SpringAOP.md +++ b/docs/Spring/02SpringAOP.md @@ -600,7 +600,7 @@ public class SpringTest { } ``` -### 区分基于ProxyFattoryBean的代理与自动代理区别? +### 区分基于ProxyFactoryBean的代理与自动代理区别? - ProxyFactoryBean:先有被代理对象,将被代理对象传入到代理类中生成代理。 diff --git "a/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" "b/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" index ab2a945..d84ae93 100644 --- "a/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" +++ "b/docs/Spring/Spring 345円270円270円350円247円201円346円263円250円350円247円243円.md" @@ -47,4 +47,15 @@ http://host:port/path/参数值 ## @ResponseBody -该注解用于将 Controller 中方法返回的对象,通过适当的 HttpMessageConverter 转换为指定的格式后,写入到 Response 对象的 body s数据区。 \ No newline at end of file +该注解用于将 Controller 中方法返回的对象,通过适当的 HttpMessageConverter 转换为指定的格式后,写入到 Response 对象的 body s数据区。 + +## @Autowired和@Resource的区别 + +1. @Autowired与@Resource都可以用来装配bean,也都可以写在字段上,或写在setter方法上。 +2. @Autowired默认按**类型**装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false。 +3. @Resource(这个注解属于J2EE的),默认按照**名称**进行装配,名称可以通过name属性进行指定, 如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。 当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。这个注解是属于J2EE的,减少了与spring的耦合。 + +# 参考资料 + +- https://blog.csdn.net/u011067360/article/details/38873755 + diff --git "a/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" "b/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" index 24c2c4e..9649764 100644 --- "a/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" +++ "b/docs/347円275円221円347円273円234円-347円233円256円345円275円225円.md" @@ -19,6 +19,7 @@ - [HTTPs](./NetWork/11HTTPs.md) - [HTTP20](./NetWork/12HTTP20.md) - [get和post比较](./NetWork/13get和post比较.md) +- [HTTP脑图](http://naotu.baidu.com/file/3f21f7d32eec65e66408e6c60849c5cc?token=723248a938dcd301) ### Socket From 925fc29ac3eb0eab36e22c824ff7ea9a2fae716b Mon Sep 17 00:00:00 2001 From: IvanLu1024 <501275190@qq.com> Date: 2019年7月31日 11:12:05 +0800 Subject: [PATCH 29/29] =?UTF-8?q?Update=2010=E7=B4=A2=E5=BC=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/DataBase/10347円264円242円345円274円225円.md" | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git "a/docs/DataBase/10347円264円242円345円274円225円.md" "b/docs/DataBase/10347円264円242円345円274円225円.md" index 986f490..d07acad 100644 --- "a/docs/DataBase/10347円264円242円345円274円225円.md" +++ "b/docs/DataBase/10347円264円242円345円274円225円.md" @@ -128,6 +128,28 @@ B+ 树是 B 树的变体,其定义基本与 B 树相同,除了: **InnoDB 存储引擎有一个特殊的功能叫"自适应哈希索引"**,当某个索引值被使用的非常频繁时,会在 B+ 树索引之上再创建一个哈希索引,这样就让 B+ 树索引具有哈希索引的一些优点,比如快速的哈希查找。 +## 聚集(密集)索引和非聚集索引 + +### 聚集索引 + +定义:数据行的**物理顺序与列值(一般是主键的那一列)的逻辑顺序相同**,一个表中只能拥有一个聚集索引。 + +> 索引文件中的每个搜索码对应一个索引值。 + +打个比方,一个表就像是我们以前用的新华字典,聚集索引就像是**拼音目录**,而每个字存放的页码就是我们的数据物理地址,我们如果要查询一个"哇"字,我们只需要查询"哇"字对应在新华字典拼音目录对应的页码,就可以查询到对应的"哇"字所在的位置,而拼音目录对应的A-Z的字顺序,和新华字典实际存储的字的顺序A-Z也是一样的。 + +由于物理排列方式与聚集索引的顺序相同,所以一张表只能建立一个聚集索引。 + +### 非聚集索引 + +定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。 + +其实按照定义,除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。如果非要把非聚集索引类比成现实生活中的东西,那么非聚集索引就像新华字典的偏旁字典,其结构顺序与实际存放顺序不一定一致。 + +> 索引文件只为索引码的某些值建立索引项。 + + + ## 索引的物理存储 索引是在**存储引擎层实现**的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 @@ -136,7 +158,7 @@ MySQL主要的存储引擎是 MyISAM 和 InnoDB。 ### MyISAM 索引存储机制 -MyISAM 引擎使用 B+ 树作索引结构,**叶子节点的 data 域存放的是数据记录的地址**。 +MyISAM 引擎使用 B+ 树作索引结构,**叶子节点的 data 域存放的是数据记录的地址**,所有索引均是非聚集索引。 @@ -155,7 +177,7 @@ MyISAM 的索引方式也叫做**非聚集索引(稀疏索引)**(索引和 ### InnoDB 索引存储机制 -InnoDB 也使用 B+ 树作为索引结构。 +InnoDB 也使用 B+ 树作为索引结构。有且仅有一个聚集索引,和多个非聚集索引。 InnoDB 的数据文件本身就是索引文件。MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在 InnoDB 中,表数据文件本身就是按 B+ 树组织的一个索引结构,这棵树的**叶子节点 data 域保存了完整的数据记录**。这个索引的 key 是数据表的主键,因此 **InnoDB 表数据文件本身就是主索引**。 @@ -165,7 +187,11 @@ InnoDB 的数据文件本身就是索引文件。MyISAM 索引文件和数据文 上图是 InnoDB 主索引(同时也是数据文件)的示意图。可以看到叶子节点包含了完整的数据记录。 -这种索引叫做**聚集索引(密集索引)**(索引和数据保存在同一文件中)。因为 InnoDB 的数据文件本身要按主键聚集,所以 InnoDB 要求表必须有主键( MyISAM 可以没有),如果没有显式指定,则 MySQL 系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整形。 +这种索引叫做**聚集索引(密集索引)**(索引和数据保存在同一文件中): + +- 若一个主键被定义,该主键作为聚集索引; +- 若没有主键定义,该表的第一个唯一非空索引作为聚集索引; +- 若均不满足,则会生成一个隐藏的主键( MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段是递增的,长度为 6 个字节)。 与 MyISAM 索引的不同是 **InnoDB 的辅助索引 data 域存储相应记录主键的值**而不是地址。例如,定义在 Col3 上的一个辅助索引: @@ -356,5 +382,5 @@ customer_id_selectivity: 0.0373 - [干货:mysql索引的数据结构](https://www.jianshu.com/p/1775b4ff123a) - [MySQL优化系列(三)--索引的使用、原理和设计优化](https://blog.csdn.net/Jack__Frost/article/details/72571540) - [数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79#comment) - -- [Mysql索引整理总结](https://blog.csdn.net/u010648555/article/details/81102957) \ No newline at end of file +- [Mysql索引整理总结](https://blog.csdn.net/u010648555/article/details/81102957) +- https://www.imooc.com/article/22915 \ No newline at end of file

AltStyle によって変換されたページ (->オリジナル) /