diff --git a/79884.log b/79884.log index d09b8c77..3b791505 100644 --- a/79884.log +++ b/79884.log @@ -9,30 +9,30 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode): "Blocked2" #13 prio=5 os_prio=31 tid=0x00007ffd7b08b000 nid=0x5503 waiting for monitor entry [0x00007000083d1000] java.lang.Thread.State: BLOCKED (on object monitor) - at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59) - - waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked) + at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59) + - waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked) at java.lang.Thread.run(Thread.java:748) "Blocked1" #12 prio=5 os_prio=31 tid=0x00007ffd7b08a000 nid=0x5303 waiting on condition [0x00007000082ce000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) - at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59) - - locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked) + at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59) + - locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked) at java.lang.Thread.run(Thread.java:748) "Waiting" #11 prio=5 os_prio=31 tid=0x00007ffd7b089800 nid=0x5103 in Object.wait() [0x00007000081cb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - - waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting) + - waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting) at java.lang.Object.wait(Object.java:502) - at com.crossoverjie.concurrent.ThreadState$Waiting.run(ThreadState.java:42) - - locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting) + at com.crossoverjie.thread.ThreadState$Waiting.run(ThreadState.java:42) + - locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting) at java.lang.Thread.run(Thread.java:748) "TimeWaiting" #10 prio=5 os_prio=31 tid=0x00007ffd7b82c800 nid=0x4f03 waiting on condition [0x00007000080c8000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) - at com.crossoverjie.concurrent.ThreadState$TimeWaiting.run(ThreadState.java:27) + at com.crossoverjie.thread.ThreadState$TimeWaiting.run(ThreadState.java:27) at java.lang.Thread.run(Thread.java:748) "Monitor Ctrl-Break" #9 daemon prio=5 os_prio=31 tid=0x00007ffd7a97e000 nid=0x4d03 runnable [0x0000700007fc5000] diff --git a/MD/ConcurrentHashMap.md b/MD/ConcurrentHashMap.md index 773ac14b..0a9d7546 100644 --- a/MD/ConcurrentHashMap.md +++ b/MD/ConcurrentHashMap.md @@ -9,7 +9,7 @@ ## JDK1.7 实现 ### 数据结构 -![](https://ws2.sinaimg.cn/large/006tNc79ly1fn2f5pgxinj30dw0730t7.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。 @@ -44,7 +44,7 @@ 首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。 -而 ConcurrentHashMap 不一样,它是先将数据插入之后再检查是否需要扩容,之后再做插入。 +而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。 ### size 方法 @@ -58,13 +58,14 @@ ## JDK1.8 实现 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthpv4odbsj30lp0drmxr.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) 1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthq78e5gqj30nr09mmz9.jpg) +![](https://s2.loli.net/2024/05/21/MVr92SEeJI34fas.png) + 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 @@ -74,7 +75,8 @@ 重点来看看 put 函数: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthrz8jlo8j30oc0rbte3.jpg) +![](https://s2.loli.net/2024/05/21/EpBRMOQnD8bx2wH.png) + - 根据 key 计算出 hashcode 。 - 判断是否需要进行初始化。 @@ -85,12 +87,13 @@ ### get 方法 -![](https://ws1.sinaimg.cn/large/006tNc79gy1fthsnp2f35j30o409hwg7.jpg) +![](https://s2.loli.net/2024/05/21/CFvAuGp8BMUko6I.png) + - 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 - 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 +- 都不满足那就按照链表的方式遍历获取值。 ## 总结 -1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 +1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 \ No newline at end of file diff --git a/MD/GarbageCollection.md b/MD/GarbageCollection.md index a233f8ae..b6253e3f 100644 --- a/MD/GarbageCollection.md +++ b/MD/GarbageCollection.md @@ -33,7 +33,7 @@ ### 标记-清除算法 标记清除算法分为两个步骤,标记和清除。 -首先将需要回收的对象标记起来,然后统一清除。但是存在两个主要的问题: +首先将**不需要回收的对象**标记起来,然后再清除其余可回收对象。但是存在两个主要的问题: - 标记和清除的效率都不高。 - 清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。 @@ -60,7 +60,7 @@ 复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。 -所以老年代中使用的时候`分配整理算法`,它的原理和`分配清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。 +所以老年代中使用的时候`标记整理算法`,它的原理和`标记清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。 ![](https://ws3.sinaimg.cn/large/006tNc79gy1fmzbq55pfdj30fe08s3yx.jpg) diff --git a/MD/HashMap.md b/MD/HashMap.md index e6cbd7c7..d975c728 100644 --- a/MD/HashMap.md +++ b/MD/HashMap.md @@ -4,7 +4,7 @@ > 以下基于 JDK1.7 分析。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn84b0ftj4j30eb0560sv.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2be77958.jpg) 如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数: @@ -61,12 +61,12 @@ map.forEach((key,value)->{ 并发场景发生扩容,调用 `resize()` 方法里的 `rehash()` 时,容易出现环形链表。这样当获取一个不存在的 `key` 时,计算出的 `index` 正好是环形链表的下标时就会出现死循环。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn85u0a0d9j30n20ii0tp.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2c4ede54.jpg) > 所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。 在 `JDK1.8` 中对 `HashMap` 进行了优化: -当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为**红黑树**。 +当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8)并且 `table` 的长度不小于64(否则扩容一次)时,链表将会转换为**红黑树**。 假设 `hash` 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 `O(n)` 。 diff --git a/MD/Java-lock.md b/MD/Java-lock.md index b9132ac9..ff764786 100644 --- a/MD/Java-lock.md +++ b/MD/Java-lock.md @@ -3,7 +3,7 @@ ## 同一进程 ### [重入锁](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ReentrantLock.md) -使用 `ReentrantLock` 获取锁的时候会会判断当前线程是否为获取锁的线程,如果是则将同步的状态 +1 ,释放锁的时候则将状态 -1。只有将同步状态的次数置为 0 的时候才会最终释放锁。 +使用 `ReentrantLock` 获取锁的时候会判断当前线程是否为获取锁的线程,如果是则将同步的状态 +1 ,释放锁的时候则将状态 -1。只有将同步状态的次数置为 0 的时候才会最终释放锁。 ### 读写锁 使用 `ReentrantReadWriteLock` ,同时维护一对锁:读锁和写锁。当写线程访问时则其他所有锁都将阻塞,读线程访问时则不会。通过读写锁的分离可以很大程度的提高并发量和吞吐量。 @@ -30,7 +30,7 @@ ### 基于 Redis -使用 `setNX(key) setEX(timeout)` 命令,只有在该 `key` 不存在的时候创建和这个 `key`,就相当于获取了锁。由于有超时时间,所以过了规定时间会自动删除,这样也可以避免死锁。 +使用 `setNX(key) setEX(timeout)` 命令,只有在该 `key` 不存在的时候创建这个 `key`,就相当于获取了锁。由于有超时时间,所以过了规定时间会自动删除,这样也可以避免死锁。 可以参考: diff --git a/MD/LinkedList.md b/MD/LinkedList.md index 1e0a4f67..3002d8a3 100644 --- a/MD/LinkedList.md +++ b/MD/LinkedList.md @@ -1,6 +1,6 @@ # LinkedList 底层分析 -![](https://ws4.sinaimg.cn/large/006tKfTcly1fqzb66c00gj30p7056q38.jpg) +![](https://i.loli.net/2019/07/04/5d1cdc7b0c7d526575.jpg) 如图所示 `LinkedList` 底层是基于双向链表实现的,也是实现了 `List` 接口,所以也拥有 List 的一些特点(JDK1.7/8 之后取消了循环,修改为双向链表)。 @@ -54,7 +54,7 @@ } ``` -由此可以看出是使用二分查找来看 `index` 离 size 中间距离来判断是从头结点正序查还是从尾节点倒序查。 +上述代码,利用了双向链表的特性,如果`index`离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。 - `node()`会以`O(n/2)`的性能去获取一个结点 - 如果索引值大于链表大小的一半,那么将从尾结点开始遍历 diff --git a/MD/MemoryAllocation.md b/MD/MemoryAllocation.md index 3b50803d..bcff3259 100644 --- a/MD/MemoryAllocation.md +++ b/MD/MemoryAllocation.md @@ -15,24 +15,80 @@ 每一个栈帧由`局部变量区`、`操作数栈`等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。 > - 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `StackOverflowError`。 -> - 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 `StackOverFlowError`。 +> - 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 `StackOverflowError`。 > - 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出 `OutOfMemoryError`。 - + **这块内存区域也是线程私有的。** ## Java 堆 `Java` 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。 +可利用参数 `-Xms -Xmx` 进行堆内存控制。 + 这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用`分代回收算法`,所有堆内存也分为 `新生代`、`老年代`,可以方便垃圾的准确回收。 **这块内存属于线程共享区域。** -## 方法区 +## 方法区(JDK1.7) 方法区主要用于存放已经被虚拟机加载的类信息,如`常量,静态变量`。 这块区域也被称为`永久代`。 -### 运行时常量池 +可利用参数 `-XX:PermSize -XX:MaxPermSize` 控制初始化方法区和最大方法区大小。 + + + +## 元数据区(JDK1.8) + +在 `JDK1.8` 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(`Metaspace`)。 + +默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 `java.lang.OutOfMemoryError: PermGen`。 + +但也不能无限扩展,因此可以使用 `-XX:MaxMetaspaceSize`来控制最大内存。 + + + + + +## 运行时常量池 + +运行时常量池是方法区的一部分,其中存放了一些符号引用。当 `new` 一个对象时,会检查这个区域是否有这个符号的引用。 + + + +## 直接内存 + + + +直接内存又称为 `Direct Memory(堆外内存)`,它并不是由 `JVM` 虚拟机所管理的一块内存区域。 + +有使用过 `Netty` 的朋友应该对这块并内存不陌生,在 `Netty` 中所有的 IO(nio) 操作都会通过 `Native` 函数直接分配堆外内存。 + +它是通过在堆内存中的 `DirectByteBuffer` 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为`零拷贝`。 + +既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 `JVM` 管理,所以常规 `GC` 操作并不能回收堆外内存。它是借助于老年代产生的 `fullGC` 顺便进行回收。同时也可以显式调用 `System.gc()` 方法进行回收(前提是没有使用 `-XX:+DisableExplicitGC` 参数来禁止该方法)。 + +**值得注意的是**:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。 + + +## 常用参数 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fxjcmnkuqyj30p009vjsn.jpg) + +通过上图可以直观的查看各个区域的参数设置。 + +常见的如下: + +- `-Xms64m` 最小堆内存 `64m`. +- `-Xmx128m` 最大堆内存 `128m`. +- `-XX:NewSize=30m` 新生代初始化大小为`30m`. +- `-XX:MaxNewSize=40m` 新生代最大大小为`40m`. +- `-Xss=256k` 线程栈大小。 +- `-XX:+PrintHeapAtGC` 当发生 GC 时打印内存布局。 +- `-XX:+HeapDumpOnOutOfMemoryError` 发送内存溢出时 dump 内存。 + + +新生代和老年代的默认比例为 `1:2`,也就是说新生代占用 `1/3`的堆内存,而老年代占用 `2/3` 的堆内存。 -运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。 +可以通过参数 `-XX:NewRatio=2` 来设置老年代/新生代的比例。 \ No newline at end of file diff --git a/MD/MySQL-Index.md b/MD/MySQL-Index.md index fad03424..579ba42e 100644 --- a/MD/MySQL-Index.md +++ b/MD/MySQL-Index.md @@ -21,6 +21,6 @@ 观察树的结构,发现查询需要经历几次 IO 是由树的高度来决定的,而树的高度又由磁盘块,数据项的大小决定的。 -磁盘块越大,数据项越小那么数的高度就越低。这也就是为什么索引字段要尽可能小的原因。 +磁盘块越大,数据项越小那么树的高度就越低。这也就是为什么索引字段要尽可能小的原因。 > 索引使用的一些[原则](https://github.com/crossoverJie/Java-Interview/blob/master/MD/SQL-optimization.md)。 diff --git a/MD/OOM-analysis.md b/MD/OOM-analysis.md index 0ecd4070..27463515 100644 --- a/MD/OOM-analysis.md +++ b/MD/OOM-analysis.md @@ -7,7 +7,7 @@ 只要将`-Xms(最小堆)`,`-Xmx(最大堆)` 设置为一样禁止自动扩展堆内存。 -当使用一个 `while(true)` 循环来不断创建对象就会发生 `OutOfMemory`,还可以使用 `-XX:+HeapDumpOutofMemoryErorr` 当发生 OOM 时会自动 dump 堆栈到文件中。 +当使用一个 `while(true)` 循环来不断创建对象就会发生 `OutOfMemory`,还可以使用 `-XX:+HeapDumpOnOutOfMemoryError` 当发生 OOM 时会自动 dump 堆栈到文件中。 伪代码: @@ -43,6 +43,12 @@ Process finished with exit code 1 `java.lang.OutOfMemoryError: Java heap space`表示堆内存溢出。 + +更多内存溢出相关实战请看这里:[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + + + + ## MetaSpace (元数据) 内存溢出 > `JDK8` 中将永久代移除,使用 `MetaSpace` 来保存类加载之后的类信息,字符串常量池也被移动到 Java 堆。 diff --git a/MD/Spike.md b/MD/Spike.md index b35114e5..8d85d682 100644 --- a/MD/Spike.md +++ b/MD/Spike.md @@ -1,5 +1,7 @@ # 设计一个秒杀系统 +**具体实现参考 [秒杀架构实践](https://crossoverjie.top/2018/05/07/ssm/SSM18-seconds-kill/)** + 主要做到以下两点: - 尽量将请求过滤在上游。 @@ -7,7 +9,7 @@ 常用的系统分层结构: -![](https://ws4.sinaimg.cn/large/006tNc79ly1fmjw06nz2zj306f0fejrh.jpg) +

针对于浏览器端,可以使用 JS 进行请求过滤,比如五秒钟之类只能点一次抢购按钮,五秒钟只能允许请求一次后端服务。(APP 同理) @@ -27,4 +29,4 @@ - 如果流量巨大,导致各个层的压力都很大可以适当的加机器横向扩容。如果加不了机器那就只有放弃流量直接返回失败。快速失败非常重要,至少可以保证系统的可用性。 - 业务分批执行:对于下单、付款等操作可以异步执行提高吞吐率。 -- 主要目的就是尽量少的请求直接访问到 `DB`。 \ No newline at end of file +- 主要目的就是尽量少的请求直接访问到 `DB`。 diff --git a/MD/SpringAOP.md b/MD/SpringAOP.md index 808cfa8f..cde06c7e 100644 --- a/MD/SpringAOP.md +++ b/MD/SpringAOP.md @@ -41,7 +41,7 @@ public class ProxyImplement implement InterfaceA{ } ``` 使用如下: -``` +```java public class Main(){ public static void main(String[] args){ InterfaceA interface = new ProxyImplement() ; @@ -59,7 +59,7 @@ public class Main(){ 其中有两个非常核心的类: - `java.lang.reflect.Proxy`类。 -- `java.lang.reflect.InvocationHandle`接口。 +- `java.lang.reflect.InvocationHandler`接口。 `Proxy` 类是用于创建代理对象,而 `InvocationHandler` 接口主要你是来处理执行逻辑。 @@ -102,7 +102,7 @@ public class CustomizeHandle implements InvocationHandler { } ``` -其中构造方法传入被代理类的类类型。其实传代理类的实例或者是类类型并没有强制的规定,传类类型的是因为被代理对象应当有代理创建而不应该由调用方创建。 +其中构造方法传入被代理类的类类型。其实传代理类的实例或者是类类型并没有强制的规定,传类类型的是因为被代理对象应当由代理创建而不应该由调用方创建。 使用方式如下: ```java diff --git a/MD/Synchronize.md b/MD/Synchronize.md index a6dbd85c..cf00e90c 100644 --- a/MD/Synchronize.md +++ b/MD/Synchronize.md @@ -103,7 +103,7 @@ public class com.crossoverjie.synchronize.Synchronize { #### 释放锁 当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 `Mark Word` 设置为无锁或者是轻量锁状态。 -偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 `-XX:-userBiasedLocking=false` 来关闭偏向锁,并默认进入轻量锁。 +偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 `-XX:-UseBiasedLocking` 来关闭偏向锁,并默认进入轻量锁。 ### 其他优化 diff --git a/MD/Thread-common-problem.md b/MD/Thread-common-problem.md index 16693075..905767c7 100644 --- a/MD/Thread-common-problem.md +++ b/MD/Thread-common-problem.md @@ -4,14 +4,14 @@ 多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 CPU 通过给每个线程分配一定的时间片,由于时间非常短通常是几十毫秒,所以 CPU 可以不停的切换线程执行任务从而达到了多线程的效果。 -但是由于在线程切换的时候需要保存本次执行的信息([详见](https://github.com/crossoverJie/Java-Interview/blob/master/MD/MemoryAllocation.md#%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8)),在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就成为上下文切换。 +但是由于在线程切换的时候需要保存本次执行的信息([详见](https://github.com/crossoverJie/Java-Interview/blob/master/MD/MemoryAllocation.md#%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8)),在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。 > 上下文切换是非常耗效率的。 通常有以下解决方案: - 采用无锁编程,比如将数据按照 `Hash(id)` 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。 -- 采用 CAS(compare and swap) 算法,如 `Atomic` 包就是采用 CAS 算法([详见](https://github.com/crossoverJie/Java-Interview/blob/master/Threadcore.md#%E5%8E%9F%E5%AD%90%E6%80%A7))。 -- 合理的创建线程,避免创建了一些线程但其中大部分都是出于 `waiting` 状态,因为每当从 `waiting` 状态切换到 `running` 状态都是一次上下文切换。 +- 采用 CAS(compare and swap) 算法,如 `Atomic` 包就是采用 CAS 算法([详见](https://github.com/crossoverJie/JCSprout/blob/master/MD/Threadcore.md#%E5%8E%9F%E5%AD%90%E6%80%A7))。 +- 合理的创建线程,避免创建了一些线程但其中大部分都是处于 `waiting` 状态,因为每当从 `waiting` 状态切换到 `running` 状态都是一次上下文切换。 ## 死锁 diff --git a/MD/ThreadPoolExecutor.md b/MD/ThreadPoolExecutor.md index b28f0973..5e912626 100644 --- a/MD/ThreadPoolExecutor.md +++ b/MD/ThreadPoolExecutor.md @@ -1,49 +1,420 @@ -# 线程池原理分析 -首先要明确为什么要使用线程池,使用线程池会带来什么好处? + +## 前言 + +平时接触过多线程开发的童鞋应该都或多或少了解过线程池,之前发布的《阿里巴巴 Java 手册》里也有一条: + +![](https://s2.loli.net/2024/05/21/H7oVe3Xqz8c2pWJ.png) + +可见线程池的重要性。 + +简单来说使用线程池有以下几个目的: - 线程是稀缺资源,不能频繁的创建。 +- 解耦作用;线程的创建于执行完全分开,方便维护。 - 应当将其放入一个池子中,可以给其他任务进行复用。 -- 解耦作用,线程的创建于执行完全分开,方便维护。 + +## 线程池原理 + +谈到线程池就会想到池化技术,其中最核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用,有点吃大锅饭的意思。 + +那在 Java 中又是如何实现的呢? + +在 JDK 1.5 之后推出了相关的 api,常见的创建线程池方式有以下几种: + +- `Executors.newCachedThreadPool()`:无限线程池。 +- `Executors.newFixedThreadPool(nThreads)`:创建固定大小的线程池。 +- `Executors.newSingleThreadExecutor()`:创建单个线程的线程池。 -## 创建一个线程池 +其实看这三种方式创建的源码就会发现: -以一个使用较多的 +```java + public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); + } +``` + +实际上还是利用 `ThreadPoolExecutor` 类实现的。 + +所以我们重点来看下 `ThreadPoolExecutor` 是怎么玩的。 + +首先是创建线程的 api: ```java ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) ``` -为例: +这几个核心参数的作用: -- 其中的 `corePoolSize` 为线程池的基本大小。 +- `corePoolSize` 为线程池的基本大小。 - `maximumPoolSize` 为线程池最大线程大小。 - `keepAliveTime` 和 `unit` 则是线程空闲后的存活时间。 - `workQueue` 用于存放任务的阻塞队列。 - `handler` 当队列和最大线程池都满了之后的饱和策略。 -## 处理流程 -当提交一个任务到线程池时它的执行流程是怎样的呢? +了解了这几个参数再来看看实际的运用。 + +通常我们都是使用: + +```java +threadPool.execute(new Job()); +``` + +这样的方式来提交一个任务到线程池中,所以核心的逻辑就是 `execute()` 函数了。 + +在具体分析之前先了解下线程池中所定义的状态,这些状态都和线程的执行密切相关: + +![](https://s2.loli.net/2024/05/21/Kf7kDlFUQy816eV.png) + + +- `RUNNING` 自然是运行状态,指可以接受任务执行队列里的任务 +- `SHUTDOWN` 指调用了 `shutdown()` 方法,不再接受新任务了,但是队列里的任务得执行完毕。 +- `STOP` 指调用了 `shutdownNow()` 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。 +- `TIDYING` 所有任务都执行完毕,在调用 `shutdown()/shutdownNow()` 中都会尝试更新为这个状态。 +- `TERMINATED` 终止状态,当执行 `terminated()` 后会更新为这个状态。 + +用图表示为: + +![](https://s2.loli.net/2024/05/21/U2tQ3RWN5CnaquJ.png) + + +然后看看 `execute()` 方法是如何处理的: + +![](https://s2.loli.net/2024/05/21/Fa6ogDun8wkbAes.png) + + +1. 获取当前线程池的状态。 +2. 当前线程数量小于 coreSize 时创建一个新的线程运行。 +3. 如果当前线程处于运行状态,并且写入阻塞队列成功。 +4. 双重检查,再次获取线程池状态;如果线程池状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 +5. 如果当前线程池为空就新创建一个线程并执行。 +6. 如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略。 + +这里借助《聊聊并发》的一张图来描述这个流程: + +![](https://s2.loli.net/2024/05/21/hNXE42uOroLlDRY.png) + + + +### 如何配置线程 + +流程聊完了再来看看上文提到了几个核心参数应该如何配置呢? + +有一点是肯定的,线程池肯定是不是越大越好。 + +通常我们是需要根据这批任务执行的性质来确定的。 + +- IO 密集型任务:由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 CPU 个数 * 2 +- CPU 密集型任务(大量复杂的运算)应当分配较少的线程,比如 CPU 个数相当的大小。 + + +当然这些都是经验值,最好的方式还是根据实际情况测试得出最佳配置。 + +### 优雅的关闭线程池 + +有运行任务自然也有关闭任务,从上文提到的 5 个状态就能看出如何来关闭线程池。 + +其实无非就是两个方法 `shutdown()/shutdownNow()`。 + +但他们有着重要的区别: + +- `shutdown()` 执行后停止接受新任务,会把队列的任务执行完毕。 +- `shutdownNow()` 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。 + +> 两个方法都会中断线程,用户可自行判断是否需要响应中断。 + +`shutdownNow()` 要更简单粗暴,可以根据实际场景选择不同的方法。 + +我通常是按照以下方式关闭线程池的: + +```java + long start = System.currentTimeMillis(); + for (int i = 0; i <= 5; i++) { + pool.execute(new Job()); + } + + pool.shutdown(); + + while (!pool.awaitTermination(1, TimeUnit.SECONDS)) { + LOGGER.info("线程还在执行。。。"); + } + long end = System.currentTimeMillis(); + LOGGER.info("一共处理了【{}】", (end - start)); +``` + +`pool.awaitTermination(1, TimeUnit.SECONDS)` 会每隔一秒钟检查一次是否执行完毕(状态为 `TERMINATED`),当从 while 循环退出时就表明线程池已经完全终止了。 + + +## SpringBoot 使用线程池 + +2018 年了,SpringBoot 盛行;来看看在 SpringBoot 中应当怎么配置和使用线程池。 + +既然用了 SpringBoot ,那自然得发挥 Spring 的特性,所以需要 Spring 来帮我们管理线程池: + +```java +@Configuration +public class TreadPoolConfig { + + + /** + * 消费队列线程 + * @return + */ + @Bean(value = "consumerQueueThreadPool") + public ExecutorService buildConsumerQueueThreadPool(){ + ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() + .setNameFormat("consumer-queue-thread-%d").build(); + + ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue(5),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy()); + + return pool ; + } + + + +} +``` + +使用时: + +```java + @Resource(name = "consumerQueueThreadPool") + private ExecutorService consumerQueueThreadPool; + + + @Override + public void execute() { + + //消费队列 + for (int i = 0; i < 5; i++) { + consumerQueueThreadPool.execute(new ConsumerQueueThread()); + } + + } +``` + +其实也挺简单,就是创建了一个线程池的 bean,在使用时直接从 Spring 中取出即可。 + + +## 监控线程池 + +谈到了 SpringBoot,也可利用它 actuator 组件来做线程池的监控。 + +线程怎么说都是稀缺资源,对线程池的监控可以知道自己任务执行的状况、效率等。 + +关于 actuator 就不再细说了,感兴趣的可以看看[这篇](http://t.cn/ReimM0o),有详细整理过如何暴露监控端点。 + +其实 ThreadPool 本身已经提供了不少 api 可以获取线程状态: + +![](https://s2.loli.net/2024/05/21/8YJ9ULEWFfBqR2k.png) + + +很多方法看名字就知道其含义,只需要将这些信息暴露到 SpringBoot 的监控端点中,我们就可以在可视化页面查看当前的线程池状态了。 + + +甚至我们可以继承线程池扩展其中的几个函数来自定义监控逻辑: + +![](https://s2.loli.net/2024/05/21/l1YjPUmvFqeHW3n.png) + +![](https://s2.loli.net/2024/05/21/jKGwm679LinTW3y.png) + + +看这些名称和定义都知道,这是让子类来实现的。 + +可以在线程执行前、后、终止状态执行自定义逻辑。 + +## 线程池隔离 + +> 线程池看似很美好,但也会带来一些问题。 + +如果我们很多业务都依赖于同一个线程池,当其中一个业务因为各种不可控的原因消耗了所有的线程,导致线程池全部占满。 + +这样其他的业务也就不能正常运转了,这对系统的打击是巨大的。 + +比如我们 Tomcat 接受请求的线程池,假设其中一些响应特别慢,线程资源得不到回收释放;线程池慢慢被占满,最坏的情况就是整个应用都不能提供服务。 + +所以我们需要将线程池**进行隔离**。 + +通常的做法是按照业务进行划分: + +> 比如下单的任务用一个线程池,获取数据的任务用另一个线程池。这样即使其中一个出现问题把线程池耗尽,那也不会影响其他的任务运行。 + +### hystrix 隔离 + +这样的需求 [Hystrix](https://github.com/Netflix/Hystrix) 已经帮我们实现了。 + +> Hystrix 是一款开源的容错插件,具有依赖隔离、系统容错降级等功能。 + +下面来看看 `Hystrix` 简单的应用: + +首先需要定义两个线程池,分别用于执行订单、处理用户。 + +```java +/** + * Function:订单服务 + * + * @author crossoverJie + * Date: 2018/7/28 16:43 + * @since JDK 1.8 + */ +public class CommandOrder extends HystrixCommand { + + private final static Logger LOGGER = LoggerFactory.getLogger(CommandOrder.class); + + private String orderName; + + public CommandOrder(String orderName) { + + + super(Setter.withGroupKey( + //服务分组 + HystrixCommandGroupKey.Factory.asKey("OrderGroup")) + //线程分组 + .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("OrderPool")) + + //线程池配置 + .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() + .withCoreSize(10) + .withKeepAliveTimeMinutes(5) + .withMaxQueueSize(10) + .withQueueSizeRejectionThreshold(10000)) + + .andCommandPropertiesDefaults( + HystrixCommandProperties.Setter() + .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)) + ) + ; + this.orderName = orderName; + } + + + @Override + public String run() throws Exception { + + LOGGER.info("orderName=[{}]", orderName); + + TimeUnit.MILLISECONDS.sleep(100); + return "OrderName=" + orderName; + } + + +} + + +/** + * Function:用户服务 + * + * @author crossoverJie + * Date: 2018/7/28 16:43 + * @since JDK 1.8 + */ +public class CommandUser extends HystrixCommand { + + private final static Logger LOGGER = LoggerFactory.getLogger(CommandUser.class); + + private String userName; + + public CommandUser(String userName) { + + + super(Setter.withGroupKey( + //服务分组 + HystrixCommandGroupKey.Factory.asKey("UserGroup")) + //线程分组 + .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("UserPool")) + + //线程池配置 + .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() + .withCoreSize(10) + .withKeepAliveTimeMinutes(5) + .withMaxQueueSize(10) + .withQueueSizeRejectionThreshold(10000)) + + //线程池隔离 + .andCommandPropertiesDefaults( + HystrixCommandProperties.Setter() + .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)) + ) + ; + this.userName = userName; + } + + + @Override + public String run() throws Exception { + + LOGGER.info("userName=[{}]", userName); + + TimeUnit.MILLISECONDS.sleep(100); + return "userName=" + userName; + } + + +} +``` + +----- + +`api` 特别简洁易懂,具体详情请查看官方文档。 + +然后模拟运行: + +```java + public static void main(String[] args) throws Exception { + CommandOrder commandPhone = new CommandOrder("手机"); + CommandOrder command = new CommandOrder("电视"); + + + //阻塞方式执行 + String execute = commandPhone.execute(); + LOGGER.info("execute=[{}]", execute); + + //异步非阻塞方式 + Future queue = command.queue(); + String value = queue.get(200, TimeUnit.MILLISECONDS); + LOGGER.info("value=[{}]", value); + + + CommandUser commandUser = new CommandUser("张三"); + String name = commandUser.execute(); + LOGGER.info("name=[{}]", name); + } +``` + +---- + +运行结果: + +![](https://s2.loli.net/2024/05/21/kJL2ZYFv4o6nP7y.png) + + +可以看到两个任务分成了两个线程池运行,他们之间互不干扰。 + +获取任务任务结果支持同步阻塞和异步非阻塞方式,可自行选择。 + + +它的实现原理其实容易猜到: + +> 利用一个 Map 来存放不同业务对应的线程池。 + -![](https://ws1.sinaimg.cn/large/006tNbRwgy1fnbzmai8yrj30dw08574s.jpg) +通过刚才的构造函数也能证明: -首先第一步会判断核心线程数有没有达到上限,如果没有则创建线程(会获取全局锁),满了则会将任务丢进阻塞队列。 +![](https://s2.loli.net/2024/05/21/uW1eDmV3CGipI2F.png) -如果队列也满了则需要判断最大线程数是否达到上限,如果没有则创建线程(获取全局锁),如果最大线程数也满了则会根据饱和策略处理。 -常用的饱和策略有: -- 直接丢弃任务。 -- 调用者线程处理。 -- 丢弃队列中的最近任务,执行当前任务。 +还要注意的一点是: -所以当线程池完成预热之后都是将任务放入队列,接着由工作线程一个个从队列里取出执行。 +> 自定义的 Command 并不是一个单例,每次执行需要 new 一个实例,不然会报 ` This instance can only be executed once. Please instantiate a new instance.` 异常。 -## 合理配置线程池 +## 总结 -线程池并不是配置越大越好,而是要根据任务的熟悉来进行划分: -如果是 `CPU` 密集型任务应当分配较少的线程,比如 `CPU` 个数相当的大小。 +池化技术确实在平时应用广泛,熟练掌握能提高不少效率。 -如果是 IO 密集型任务,由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 `CPU 个数 * 2` 。 +文末的 hystrix 源码: -当是一个混合型任务,可以将其拆分为 `CPU` 密集型任务以及 `IO` 密集型任务,这样来分别配置。 +[https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix](https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix) \ No newline at end of file diff --git a/MD/Threadcore.md b/MD/Threadcore.md index 99b94dd7..1a4f8d56 100644 --- a/MD/Threadcore.md +++ b/MD/Threadcore.md @@ -48,7 +48,7 @@ public final boolean compareAndSet(long expect, long update) { 现代计算机中,由于 `CPU` 直接从主内存中读取数据的效率不高,所以都会对应的 `CPU` 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。 -![](https://ws2.sinaimg.cn/large/006tKfTcly1fmouu3fpokj31ae0osjt1.jpg) +![](https://i.loli.net/2019/05/08/5cd1c6a9c6546.jpg) 如上图所示。 @@ -121,7 +121,9 @@ Java 中可以使用 `volatile` 来保证顺序性,`synchronized 和 lock` 也 new Thread(new Runnable() { @Override public void run() { - doSomeThing(); + while (flag) { + doSomeThing(); + } } }); } diff --git a/MD/additional-skills/how-to-use-git-efficiently.md b/MD/additional-skills/how-to-use-git-efficiently.md new file mode 100755 index 00000000..38b0734a --- /dev/null +++ b/MD/additional-skills/how-to-use-git-efficiently.md @@ -0,0 +1,120 @@ + +# 【译】如何高效的使用 Git + +[原文链接](https://medium.freecodecamp.org/how-to-use-git-efficiently-54320a236369) + +![](https://ws1.sinaimg.cn/large/0069RVTdly1fuz415uvavj318g0tmh0f.jpg) + +> 代码昨天还是运行好好的今天就不行了。 + +> 代码被删了。 + +> 突然出现了一个奇怪的 bug,但是没人知道怎么回事。 + + +如果你出现过上面的任何一种情况,那本篇文章就是为你准备的。 + +除了知道 `git add`, `git commit` , `git push` 之外,Git 中还需要其他重要的技术需要掌握。长远来看对我们是有帮助的。这里我将向你展示 Git 的最佳实践。 + + +# Git 工作流 + +当有多个开发者同时涉及到一个项目时那么就非常有必要正确使用 Git 工作流。 + +这里我将介绍一种工作流,它在一个多人大型项目中将非常有用。 + +![](https://ws1.sinaimg.cn/large/0069RVTdly1fuz4imimuuj313111zq6q.jpg) + + +# 前言 + +突然有一天,你成为了一个项目的技术 Leader 并计划做出下一个 Facebook。在这个项目中你有三个开发人员。 + +1. Alice:一个开发小白。 +2. Bob:拥有一年工作经验,了解基本开发。 +3. John:三年开发经验,熟练开发技能。 +4. 你:该项目的技术负责人。 + +# Git 开发流程 + +## Master 分支 + +1. Master 分支应该始终和生产环境保持一致。 +2. 由于 master 和生产代码是一致的,所以没有人包括技术负责人能在 master 上直接开发。 +3. 真正的开发代码应当写在其他分支上。 + +## Release(发布) 分支 + +1. 当项目开始时,第一件事情就是创建发布分支。发布分支是基于 master 分支创建而来。 +2. 所有与本项目相关的代码都在发布分支中,这个分支也是一个以 `release/` 开头的普通分支。 +3. 比如这次的发布分支名为 `release/fb`。 +4. 可能有多个项目都基于同一份代码运行,因此对于每一个项目来说都需要创建一个独立的发布分支。假设现在还有一个项目正在并行运行,那就得为这个项目创建一个单独的发布分支比如 `release/messenger`。 +5. 需要单独的发布分支的原因是:多个并行项目是基于同一份代码运行的,但是项目之间不能有冲突。 + +## Feature(功能分支) branch + +1. 对于应用中的每一个功能都应该创建一个独立的功能分支,这会确保这些功能能被单独构建。 +2. 功能分支也和其他分支一样,只是以 `feature/` 开头。 +3. 现在作为技术 Leader,你要求 Alice 去做 Facebook 的登录页面。因此他创建了一个新的功能分支。把他命名为 `feature/login`。Alice 将会在这个分支上编写所有的登录代码。 +4. 这个功能分支通常是基于 Release(发布) 分支 创建而来。 +5. Bob 的任务为创建添加好友页面,因此他创建了一个名为 `feature/friendrequest` 的功能分支。 +6. John 则被安排构建消息流,因此创建了一个 `feature/newsfeed` 的功能分支。 +7. 所有的开发人员都在自己的分支上进行开发,目前为止都很正常。 +8. 现在当 Alice 完成了他的登录开发,他需要将他的功能分支 `feature/login` 发送给 Release(发布) 分支。这个过程是通过发起一个 `pull request` 完成的。 + + +## Pull request + +首先 `pull request` 不能和 `git pull` 搞混了。 + +开发人员不能直接向 Release(发布) 分支推送代码,技术 Leader 需要在功能分支合并到 Release(发布) 分支之前做好代码审查。这也是通过 `pull request` 完成的。 + +Alice 能够按照如下 GitHub 方式提交 `pull request`。 + +![](https://ws1.sinaimg.cn/large/0069RVTdgy1fv03386jcoj30ig05swet.jpg) + +在分支名字的旁边有一个 “New pull request” 按钮,点击之后将会显示如下界面: + +![](https://ws4.sinaimg.cn/large/0069RVTdgy1fv03etb1afj30no078gmn.jpg) + +- 比较分支是 Alice 的功能分支 `feature/login`。 +- base 分支则应该是发布分支 `release/fb`。 + +点击之后 Alice 需要为这个 `pull request` 输入名称和描述,最后再点击 “Create Pull Request” 按钮。 + +同时 Alice 需要为这个 `pull request` 指定一个 reviewer。作为技术 Leader 的你被选为本次 `pull request` 的 reviewer。 + +你完成代码审查之后就需要把这个功能分支合并到 Release(发布) 分支。 + +现在你已经把 `feature/login` 分支合并到 `release/fb`,并且 Alice 非常高兴他的代码被合并了。 + +## 代码冲突 😠 + +1. Bob 完成了他的编码工作,同时向 `release/fb` 分支发起了一个 `pull request`。 +2. 因为发布分支已经合并了登录的代码,这时代码冲突发生了。解决冲突和合并代码是 reviewer 的责任。在这样的情况下,作为技术 Leader 就需要解决冲突和合并代码了。 +3. 现在 John 也已经完成了他的开发,同时也想把代码合并到发布分支。但 John 非常擅长于解决代码冲突。他将 `release/fb` 上最新的代码合并到他自己的功能分支 `feature/newsfeed` (通过 git pull 或 git merge 命令)。同时他解决了所有存在的冲突,现在 `feature/newsfeed` 已经有了所有发布分支 `release/fb` 的代码。 +4. 最后 John 创建了一个 `pull request`,由于 John 已经解决了所有问题,所以本次 `pull request` 不会再有冲突了。 + +因此通常有两种方式来解决代码冲突: + +- `pull request` 的 reviewer 需要解决所有的代码冲突。 +- 开发人员需要确保将发布分支的最新代码合并到功能分支,并且解决所有的冲突。 + + +# 还是 Master 分支 + + +一旦项目完成,发布分支的代码需要合并回 master 分支,同时需要发布到生产环境。 + +因此生产环境中的代码总是和 master 分支保持一致。同时对于今后的任何项目来说都是要确保 master 代码是最新的。 + + + + + +> 我们现在团队就是按照这样的方式进行开发,确实可以尽可能的减少代码管理上的问题。 + + + + +**你的点赞与转发是最大的支持。** \ No newline at end of file diff --git a/MD/architecture-design/million-sms-push.md b/MD/architecture-design/million-sms-push.md new file mode 100755 index 00000000..a3d366b7 --- /dev/null +++ b/MD/architecture-design/million-sms-push.md @@ -0,0 +1,360 @@ +# 设计一个百万级的消息推送系统 + +![business-communication-computer-261706.jpg](https://i.loli.net/2018/09/23/5ba7ae180e8eb.jpg) + +# 前言 + +首先迟到的祝大家中秋快乐。 + +最近一周多没有更新了。其实我一直想憋一个大招,分享一些大家感兴趣的干货。 + +鉴于最近我个人的工作内容,于是利用这三天小长假憋了一个出来(其实是玩了两天🤣)。 + + +--- + +先简单说下本次的主题,由于我最近做的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 + +最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 + +所以本次分享的内容不但可以满足物联网领域同时还支持以下场景: + +- 基于 `WEB` 的聊天系统(点对点、群聊)。 +- `WEB` 应用中需求服务端推送的场景。 +- 基于 SDK 的消息推送平台。 + +# 技术选型 + +要满足大量的连接数、同时支持双全工通信,并且性能也得有保障。 + +在 Java 技术栈中进行选型首先自然是排除掉了传统 `IO`。 + +那就只有选 NIO 了,在这个层面其实选择也不多,考虑到社区、资料维护等方面最终选择了 Netty。 + +最终的架构图如下: + +![](https://ws1.sinaimg.cn/mw690/72fbb941gy1fvjz1teappj20rg0humy1.jpg) + + +现在看着蒙没关系,下文一一介绍。 + +# 协议解析 + +既然是一个消息系统,那自然得和客户端定义好双方的协议格式。 + +常见和简单的是 HTTP 协议,但我们的需求中有一项需要是双全工的交互方式,同时 HTTP 更多的是服务于浏览器。我们需要的是一个更加精简的协议,减少许多不必要的数据传输。 + +因此我觉得最好是在满足业务需求的情况下定制自己的私有协议,在我这个场景下其实有标准的物联网协议。 + +如果是其他场景可以借鉴现在流行的 `RPC` 框架定制私有协议,使得双方通信更加高效。 + +不过根据这段时间的经验来看,不管是哪种方式都得在协议中预留安全相关的位置。 + +协议相关的内容就不过讨论了,更多介绍具体的应用。 + +# 简单实现 + +首先考虑如何实现功能,再来思考百万连接的情况。 + +## 注册鉴权 + +在做真正的消息上、下行之前首先要考虑的就是鉴权问题。 + +就像你使用微信一样,第一步怎么也得是登录吧,不能无论是谁都可以直接连接到平台。 + +所以第一步得是注册才行。 + +如上面架构图中的 `注册/鉴权` 模块。通常来说都需要客户端通过 `HTTP` 请求传递一个唯一标识,后台鉴权通过之后会响应一个 `token`,并将这个 `token` 和客户端的关系维护到 `Redis` 或者是 DB 中。 + +客户端将这个 token 也保存到本地,今后的每一次请求都得带上这个 token。一旦这个 token 过期,客户端需要再次请求获取 token。 + +鉴权通过之后客户端会直接通过`TCP 长连接`到图中的 `push-server` 模块。 + +这个模块就是真正处理消息的上、下行。 + +## 保存通道关系 + +在连接接入之后,真正处理业务之前需要将当前的客户端和 Channel 的关系维护起来。 + +假设客户端的唯一标识是手机号码,那就需要把手机号码和当前的 Channel 维护到一个 Map 中。 + +这点和之前 [SpringBoot 整合长连接心跳机制](http://t.cn/EPcNHFZ) 类似。 + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkj6oe4rej30k104c0tg.jpg) + +同时为了可以通过 Channel 获取到客户端唯一标识(手机号码),还需要在 Channel 中设置对应的属性: + +```java +public static void putClientId(Channel channel, String clientId) { + channel.attr(CLIENT_ID).set(clientId); +} +``` + +获取时手机号码时: + +```java +public static String getClientId(Channel channel) { + return (String)getAttribute(channel, CLIENT_ID); +} +``` + +这样当我们客户端下线的时便可以记录相关日志: + +```java +String telNo = NettyAttrUtil.getClientId(ctx.channel()); +NettySocketHolder.remove(telNo); +log.info("客户端下线,TelNo=" + telNo); +``` + +> 这里有一点需要注意:存放客户端与 Channel 关系的 Map 最好是预设好大小(避免经常扩容),因为它将是使用最为频繁同时也是占用内存最大的一个对象。 + +## 消息上行 + +接下来则是真正的业务数据上传,通常来说第一步是需要判断上传消息输入什么业务类型。 + +在聊天场景中,有可能上传的是文本、图片、视频等内容。 + +所以我们得进行区分,来做不同的处理;这就和客户端协商的协议有关了。 + +- 可以利用消息头中的某个字段进行区分。 +- 更简单的就是一个 `JSON` 消息,拿出一个字段用于区分不同消息。 + +不管是哪种只有可以区分出来即可。 + +### 消息解析与业务解耦 + +消息可以解析之后便是处理业务,比如可以是写入数据库、调用其他接口等。 + +我们都知道在 Netty 中处理消息一般是在 `channelRead()` 方法中。 + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkkawymbkj30o6027mxf.jpg) + +在这里可以解析消息,区分类型。 + +但如果我们的业务逻辑也写在里面,那这里的内容将是巨多无比。 + +甚至我们分为好几个开发来处理不同的业务,这样将会出现许多冲突、难以维护等问题。 + +所以非常有必要将消息解析与业务处理完全分离开来。 + + +> 这时面向接口编程就发挥作用了。 + +这里的核心代码和 [「造个轮子」——cicada(轻量级 WEB 框架)](https://crossoverjie.top/2018/09/03/wheel/cicada1/#%E9%85%8D%E7%BD%AE%E4%B8%9A%E5%8A%A1-Action) 是一致的。 + +都是先定义一个接口用于处理业务逻辑,然后在解析消息之后通过反射创建具体的对象执行其中的`处理函数`即可。 + +这样不同的业务、不同的开发人员只需要实现这个接口同时实现自己的业务逻辑即可。 + +伪代码如下: + +![](https://ws1.sinaimg.cn/large/006tNbRwgy1fvkkhd8961j30n602kglr.jpg) + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkkhwsgkqj30nh0m0gpt.jpg) + +想要了解 cicada 的具体实现请点击这里: + +[https://github.com/TogetherOS/cicada](https://github.com/TogetherOS/cicada) + + +上行还有一点需要注意;由于是基于长连接,所以客户端需要定期发送心跳包用于维护本次连接。同时服务端也会有相应的检查,N 个时间间隔没有收到消息之后将会主动断开连接节省资源。 + +这点使用一个 `IdleStateHandler` 就可实现,更多内容可以查看 [Netty(一) SpringBoot 整合长连接心跳机制](https://crossoverjie.top/2018/05/24/netty/Netty(1)TCP-Heartbeat/#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%BF%83%E8%B7%B3)。 + + + +## 消息下行 + +有了上行自然也有下行。比如在聊天的场景中,有两个客户端连上了 `push-server`,他们直接需要点对点通信。 + +这时的流程是: + +- A 将消息发送给服务器。 +- 服务器收到消息之后,得知消息是要发送给 B,需要在内存中找到 B 的 Channel。 +- 通过 B 的 Channel 将 A 的消息转发下去。 + +这就是一个下行的流程。 + +甚至管理员需要给所有在线用户发送系统通知也是类似: + +遍历保存通道关系的 Map,挨个发送消息即可。这也是之前需要存放到 Map 中的主要原因。 + +伪代码如下: + +![](https://ws3.sinaimg.cn/large/006tNbRwgy1fvkkpefci7j30w408h768.jpg) + +具体可以参考: + +[https://github.com/crossoverJie/netty-action/](https://github.com/crossoverJie/netty-action/) + + +# 分布式方案 + +单机版的实现了,现在着重讲讲如何实现百万连接。 + +百万连接其实只是一个形容词,更多的是想表达如何来实现一个分布式的方案,可以灵活的水平拓展从而能支持更多的连接。 + +再做这个事前首先得搞清楚我们单机版的能支持多少连接。影响这个的因素就比较多了。 + +- 服务器自身配置。内存、CPU、网卡、Linux 支持的最大文件打开数等。 +- 应用自身配置,因为 Netty 本身需要依赖于堆外内存,但是 JVM 本身也是需要占用一部分内存的,比如存放通道关系的大 `Map`。这点需要结合自身情况进行调整。 + +结合以上的情况可以测试出单个节点能支持的最大连接数。 + +单机无论怎么优化都是有上限的,这也是分布式主要解决的问题。 + +## 架构介绍 + +在将具体实现之前首先得讲讲上文贴出的整体架构图。 + +![](https://ws1.sinaimg.cn/mw690/72fbb941gy1fvjz1teappj20rg0humy1.jpg) + +先从左边开始。 + +上文提到的 `注册鉴权` 模块也是集群部署的,通过前置的 Nginx 进行负载。之前也提过了它主要的目的是来做鉴权并返回一个 token 给客户端。 + +但是 `push-server` 集群之后它又多了一个作用。那就是得返回一台可供当前客户端使用的 `push-server`。 + +右侧的 `平台` 一般指管理平台,它可以查看当前的实时在线数、给指定客户端推送消息等。 + +推送消息则需要经过一个推送路由(`push-server`)找到真正的推送节点。 + +其余的中间件如:Redis、Zookeeper、Kafka、MySQL 都是为了这些功能所准备的,具体看下面的实现。 + +## 注册发现 + +首先第一个问题则是 `注册发现`,`push-server` 变为多台之后如何给客户端选择一台可用的节点是第一个需要解决的。 + +这块的内容其实已经在 [分布式(一) 搞定服务注册与发现](https://crossoverjie.top/2018/08/27/distributed/distributed-discovery-zk/) 中详细讲过了。 + +所有的 `push-server` 在启动时候需要将自身的信息注册到 Zookeeper 中。 + +`注册鉴权` 模块会订阅 Zookeeper 中的节点,从而可以获取最新的服务列表。结构如下: + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fundatqf6uj30el06f0su.jpg) + +以下是一些伪代码: + +应用启动注册 Zookeeper。 + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkriuz7yrj30m304lq3r.jpg) + +![](https://ws4.sinaimg.cn/large/006tNbRwgy1fvkrj927rsj30od08ejst.jpg) + +对于`注册鉴权`模块来说只需要订阅这个 Zookeeper 节点: + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkrlfdgrkj30tb08j0uf.jpg) + +### 路由策略 + +既然能获取到所有的服务列表,那如何选择一台刚好合适的 `push-server` 给客户端使用呢? + +这个过程重点要考虑以下几点: + +- 尽量保证各个节点的连接均匀。 +- 增删节点是否要做 Rebalance。 + +首先保证均衡有以下几种算法: + +- 轮询。挨个将各个节点分配给客户端。但会出现新增节点分配不均匀的情况。 +- Hash 取模的方式。类似于 HashMap,但也会出现轮询的问题。当然也可以像 HashMap 那样做一次 Rebalance,让所有的客户端重新连接。不过这样会导致所有的连接出现中断重连,代价有点大。 +- 由于 Hash 取模方式的问题带来了[`一致性 Hash`算法](https://crossoverjie.top/%2F2018%2F01%2F08%2FConsistent-Hash%2F),但依然会有一部分的客户端需要 Rebalance。 +- 权重。可以手动调整各个节点的负载情况,甚至可以做成自动的,基于监控当某些节点负载较高就自动调低权重,负载较低的可以提高权重。 + +还有一个问题是: + +> 当我们在重启部分应用进行升级时,在该节点上的客户端怎么处理? + +由于我们有心跳机制,当心跳不通之后就可以认为该节点出现问题了。那就得重新请求`注册鉴权`模块获取一个可用的节点。在弱网情况下同样适用。 + +如果这时客户端正在发送消息,则需要将消息保存到本地等待获取到新的节点之后再次发送。 + +## 有状态连接 + +在这样的场景中不像是 HTTP 那样是无状态的,我们得明确的知道各个客户端和连接的关系。 + +在上文的单机版中我们将这个关系保存到本地的缓存中,但在分布式环境中显然行不通了。 + +比如在平台向客户端推送消息的时候,它得首先知道这个客户端的通道保存在哪台节点上。 + +借助我们以前的经验,这样的问题自然得引入一个第三方中间件用来存放这个关系。 + +也就是架构图中的存放`路由关系的 Redis`,在客户端接入 `push-server` 时需要将当前客户端唯一标识和服务节点的 `ip+port` 存进 `Redis`。 + +同时在客户端下线时候得在 Redis 中删掉这个连接关系。 + + +> 这样在理想情况下各个节点内存中的 map 关系加起来应该正好等于 Redis 中的数据。 + +伪代码如下: + +![](https://ws1.sinaimg.cn/large/006tNbRwgy1fvkt2ytdxoj30r109u40n.jpg) + +这里存放路由关系的时候会有并发问题,最好是换为一个 `lua` 脚本。 + +## 推送路由 + +设想这样一个场景:管理员需要给最近注册的客户端推送一个系统消息会怎么做? + +> 结合架构图 + +假设这批客户端有 10W 个,首先我们需要将这批号码通过`平台`下的 `Nginx` 下发到一个推送路由中。 + +为了提高效率甚至可以将这批号码再次分散到每个 `push-route` 中。 + +拿到具体号码之后再根据号码的数量启动多线程的方式去之前的路由 Redis 中获取客户端所对应的 `push-server`。 + +再通过 HTTP 的方式调用 `push-server` 进行真正的消息下发(Netty 也很好的支持 HTTP 协议)。 + +推送成功之后需要将结果更新到数据库中,不在线的客户端可以根据业务再次推送等。 + +## 消息流转 + +也许有些场景对于客户端上行的消息非常看重,需要做持久化,并且消息量非常大。 + +在 `push-sever` 做业务显然不合适,这时完全可以选择 Kafka 来解耦。 + +将所有上行的数据直接往 Kafka 里丢后就不管了。 + +再由消费程序将数据取出写入数据库中即可。 + +其实这块内容也很值得讨论,可以先看这篇了解下:[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + +后续谈到 Kafka 再做详细介绍。 + +# 分布式问题 + +分布式解决了性能问题但却带来了其他麻烦。 + +## 应用监控 + +比如如何知道线上几十个 `push-server` 节点的健康状况? + +这时就得监控系统发挥作用了,我们需要知道各个节点当前的内存使用情况、GC。 + +以及操作系统本身的内存使用,毕竟 Netty 大量使用了堆外内存。 + +同时需要监控各个节点当前的在线数,以及 Redis 中的在线数。理论上这两个数应该是相等的。 + +这样也可以知道系统的使用情况,可以灵活的维护这些节点数量。 + +## 日志处理 + +日志记录也变得异常重要了,比如哪天反馈有个客户端一直连不上,你得知道问题出在哪里。 + + +最好是给每次请求都加上一个 traceID 记录日志,这样就可以通过这个日志在各个节点中查看到底是卡在了哪里。 + +以及 ELK 这些工具都得用起来才行。 + +# 总结 + +本次是结合我日常经验得出的,有些坑可能在工作中并没有踩到,所有还会有一些遗漏的地方。 + +就目前来看想做一个稳定的推送系统其实是比较麻烦的,其中涉及到的点非常多,只有真正做过之后才会知道。 + +看完之后觉得有帮助的还请不吝转发分享。 + +**欢迎关注公众号一起交流:** + +![](https://ws4.sinaimg.cn/large/006tNbRwgy1fvkwiw9pwaj30760760t7.jpg) \ No newline at end of file diff --git a/MD/collection/HashSet.md b/MD/collection/HashSet.md index a18212d9..3fd3565e 100644 --- a/MD/collection/HashSet.md +++ b/MD/collection/HashSet.md @@ -39,7 +39,7 @@ ``` 比较关键的就是这个 `add()` 方法。 -可以看出它是将存放的对象当做了 `HashMap` 的健,`value` 都是相同的 `PRESENT` 。由于 `HashMap` 的 `key` 是不能重复的,所以每当有重复的值写入到 `HashSet` 时,`value` 会被覆盖,但 `key` 不会收到影响,这样就保证了 `HashSet` 中只能存放不重复的元素。 +可以看出它是将存放的对象当做了 `HashMap` 的健,`value` 都是相同的 `PRESENT` 。由于 `HashMap` 的 `key` 是不能重复的,所以每当有重复的值写入到 `HashSet` 时,`value` 会被覆盖,但 `key` 不会受到影响,这样就保证了 `HashSet` 中只能存放不重复的元素。 ## 总结 diff --git a/MD/collection/LinkedHashMap.md b/MD/collection/LinkedHashMap.md index bbc49c37..7ccde30e 100644 --- a/MD/collection/LinkedHashMap.md +++ b/MD/collection/LinkedHashMap.md @@ -31,7 +31,7 @@ 调试可以看到 `map` 的组成: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fo6l9xp91lj319m0s4tgi.jpg) +![](https://i.loli.net/2019/05/08/5cd1ba2adf7c0.jpg) 打开源码可以看到: @@ -66,7 +66,7 @@ 上边的 demo 总结成一张图如下: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1fodggwc523j30za0n4wgj.jpg) +![](https://i.loli.net/2019/05/08/5cd1ba2d418b6.jpg) 第一个类似于 `HashMap` 的结构,利用 `Entry` 中的 `next` 指针进行关联。 @@ -86,7 +86,7 @@ } ``` -这个构造方法可以显示的传入 `accessOrder `。 +这个构造方法可以显式的传入 `accessOrder `。 ## 构造方法 @@ -137,7 +137,7 @@ 看 `LinkedHashMap` 的 `put()` 方法之前先看看 `HashMap` 的 `put` 方法: -``` +```java public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); @@ -273,6 +273,6 @@ LinkedHashMap 的 `get()` 方法也重写了: 总的来说 `LinkedHashMap` 其实就是对 `HashMap` 进行了拓展,使用了双向链表来保证了顺序性。 -因为是继承与 `HashMap` 的,所以一些 `HashMap` 存在的问题 `LinkedHashMap` 也会存在,比如不支持并发等。 +因为是继承于 `HashMap` 的,所以一些 `HashMap` 存在的问题 `LinkedHashMap` 也会存在,比如不支持并发等。 diff --git a/MD/concurrent/thread-communication.md b/MD/concurrent/thread-communication.md index 75707021..fd3e7090 100644 --- a/MD/concurrent/thread-communication.md +++ b/MD/concurrent/thread-communication.md @@ -131,7 +131,7 @@ B 线程调用了 notify() 方法,这样 A 线程收到通知之后就可以 有一些需要注意: -- wait() 、nofify() 、nofityAll() 调用的前提都是获得了对象的锁(也可称为对象监视器)。 +- wait() 、notify()、notifyAll() 调用的前提都是获得了对象的锁(也可称为对象监视器)。 - 调用 wait() 方法后线程会释放锁,进入 `WAITING` 状态,该线程也会被移动到**等待队列**中。 - 调用 notify() 方法会将**等待队列**中的线程移动到**同步队列**中,线程状态也会更新为 `BLOCKED` - 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。 @@ -585,4 +585,4 @@ Java 虽说是基于内存通信的,但也可以使用管道通信。 需要注意的是,输入流和输出流需要首先建立连接。这样线程 B 就可以收到线程 A 发出的消息了。 -实际开发中可以灵活根据需求选择最适合的线程通信方式。 \ No newline at end of file +实际开发中可以灵活根据需求选择最适合的线程通信方式。 diff --git a/MD/distributed/Distributed-Limit.md b/MD/distributed/Distributed-Limit.md new file mode 100644 index 00000000..dd33f793 --- /dev/null +++ b/MD/distributed/Distributed-Limit.md @@ -0,0 +1,483 @@ +![](https://ws3.sinaimg.cn/large/006tKfTcly1fqrle104hwj31i6104aig.jpg) + +## 前言 + +本文接着上文[应用限流](http://crossoverjie.top/2017/08/11/sbc4/)进行讨论。 + +之前谈到的限流方案只能针对于单个 JVM 有效,也就是单机应用。而对于现在普遍的分布式应用也得有一个分布式限流的方案。 + +基于此尝试写了这个组件: + +[https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) + + +## DEMO + +以下采用的是 + +[https://github.com/crossoverJie/springboot-cloud](https://github.com/crossoverJie/springboot-cloud) + +来做演示。 + +在 Order 应用提供的接口中采取了限流。首先是配置了限流工具的 Bean: + +```java +@Configuration +public class RedisLimitConfig { + + + @Value("${redis.limit}") + private int limit; + + + @Autowired + private JedisConnectionFactory jedisConnectionFactory; + + @Bean + public RedisLimit build() { + RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection(); + JedisCluster jedisCluster = (JedisCluster) clusterConnection.getNativeConnection(); + RedisLimit redisLimit = new RedisLimit.Builder<>(jedisCluster) + .limit(limit) + .build(); + + return redisLimit; + } +} +``` + +接着在 Controller 使用组件: + +```java + @Autowired + private RedisLimit redisLimit ; + + @Override + @CheckReqNo + public BaseResponse getOrderNo(@RequestBody OrderNoReqVO orderNoReq) { + BaseResponse res = new BaseResponse(); + + //限流 + boolean limit = redisLimit.limit(); + if (!limit){ + res.setCode(StatusEnum.REQUEST_LIMIT.getCode()); + res.setMessage(StatusEnum.REQUEST_LIMIT.getMessage()); + return res ; + } + + res.setReqNo(orderNoReq.getReqNo()); + if (null == orderNoReq.getAppId()){ + throw new SBCException(StatusEnum.FAIL); + } + OrderNoResVO orderNoRes = new OrderNoResVO() ; + orderNoRes.setOrderId(DateUtil.getLongTime()); + res.setCode(StatusEnum.SUCCESS.getCode()); + res.setMessage(StatusEnum.SUCCESS.getMessage()); + res.setDataBody(orderNoRes); + return res ; + } + +``` + +为了方便使用,也提供了注解: + +```java + @Override + @ControllerLimit + public BaseResponse getOrderNoLimit(@RequestBody OrderNoReqVO orderNoReq) { + BaseResponse res = new BaseResponse(); + // 业务逻辑 + return res ; + } +``` +该注解拦截了 http 请求,会再请求达到阈值时直接返回。 + +普通方法也可使用: + +```java +@CommonLimit +public void doSomething(){} +``` + +会在调用达到阈值时抛出异常。 + +为了模拟并发,在 [User](https://github.com/crossoverJie/springboot-cloud/blob/master/sbc-user/user/src/main/java/com/crossoverJie/sbcuser/controller/UserController.java#L72-L91) 应用中开启了 10 个线程调用 Order(**限流次数为5**) 接口(也可使用专业的并发测试工具 JMeter 等)。 + + + +```java + @Override + public BaseResponse getUserByFeign(@RequestBody UserReqVO userReq) { + //调用远程服务 + OrderNoReqVO vo = new OrderNoReqVO(); + vo.setAppId(1L); + vo.setReqNo(userReq.getReqNo()); + + for (int i = 0; i < 10; i++) { + executorService.execute(new Worker(vo, orderServiceClient)); + } + + UserRes userRes = new UserRes(); + userRes.setUserId(123); + userRes.setUserName("张三"); + + userRes.setReqNo(userReq.getReqNo()); + userRes.setCode(StatusEnum.SUCCESS.getCode()); + userRes.setMessage("成功"); + + return userRes; + } + + + private static class Worker implements Runnable { + + private OrderNoReqVO vo; + private OrderServiceClient orderServiceClient; + + public Worker(OrderNoReqVO vo, OrderServiceClient orderServiceClient) { + this.vo = vo; + this.orderServiceClient = orderServiceClient; + } + + @Override + public void run() { + + BaseResponse orderNo = orderServiceClient.getOrderNoCommonLimit(vo); + logger.info("远程返回:" + JSON.toJSONString(orderNo)); + + } + } +``` + +> 为了验证分布式效果启动了两个 Order 应用。 + +![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrnxt2l8lj313x09rwfm.jpg) + +效果如下: +![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrlvvj8cbj31kw0f1wws.jpg) + + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fqrlznycdnj31kw0gbh0n.jpg) + +![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrm0jpbjjj31kw04wgq9.jpg) + + +## 实现原理 +实现原理其实很简单。既然要达到分布式全局限流的效果,那自然需要一个第三方组件来记录请求的次数。 + +其中 Redis 就非常适合这样的场景。 + +- 每次请求时将当前时间(精确到秒)作为 Key 写入到 Redis 中,超时时间设置为 2 秒,Redis 将该 Key 的值进行自增。 +- 当达到阈值时返回错误。 +- 写入 Redis 的操作用 Lua 脚本来完成,利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性。 + +Lua 脚本如下: + +```lua +--lua 下标从 1 开始 +-- 限流 key +local key = KEYS[1] +-- 限流大小 +local limit = tonumber(ARGV[1]) + +-- 获取当前流量大小 +local curentLimit = tonumber(redis.call('get', key) or "0") + +if curentLimit + 1 > limit then + -- 达到限流大小 返回 + return 0; +else + -- 没有达到阈值 value + 1 + redis.call("INCRBY", key, 1) + redis.call("EXPIRE", key, 2) + return curentLimit + 1 +end +``` + +Java 中的调用逻辑: + +```java + public boolean limit() { + String key = String.valueOf(System.currentTimeMillis() / 1000); + Object result = null; + if (jedis instanceof Jedis) { + result = ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + } else if (jedis instanceof JedisCluster) { + result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + } else { + //throw new RuntimeException("instance is error") ; + return false; + } + + if (FAIL_CODE != (Long) result) { + return true; + } else { + return false; + } + } +``` + +所以只需要在需要限流的地方调用该方法对返回值进行判断即可达到限流的目的。 + +当然这只是利用 Redis 做了一个粗暴的计数器,如果想实现类似于上文中的令牌桶算法可以基于 Lua 自行实现。 + + +### Builder 构建器 + +在设计这个组件时想尽量的提供给使用者清晰、可读性、不易出错的 API。 + +> 比如第一步,如何构建一个限流对象。 + +最常用的方式自然就是构造函数,如果有多个域则可以采用重叠构造器的方式: + +```java +public A(){} +public A(int a){} +public A(int a,int b){} +``` + +缺点也是显而易见的:如果参数过多会导致难以阅读,甚至如果参数类型一致的情况下客户端颠倒了顺序,但不会引起警告从而出现难以预测的结果。 + +第二种方案可以采用 JavaBean 模式,利用 `setter` 方法进行构建: + +```java +A a = new A(); +a.setA(a); +a.setB(b); +``` + +这种方式清晰易读,但却容易让对象处于不一致的状态,使对象处于线程不安全的状态。 + +所以这里采用了第三种创建对象的方式,构建器: + +```java +public class RedisLimit { + + private JedisCommands jedis; + private int limit = 200; + + private static final int FAIL_CODE = 0; + + /** + * lua script + */ + private String script; + + private RedisLimit(Builder builder) { + this.limit = builder.limit ; + this.jedis = builder.jedis ; + buildScript(); + } + + + /** + * limit traffic + * @return if true + */ + public boolean limit() { + String key = String.valueOf(System.currentTimeMillis() / 1000); + Object result = null; + if (jedis instanceof Jedis) { + result = ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + } else if (jedis instanceof JedisCluster) { + result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + } else { + //throw new RuntimeException("instance is error") ; + return false; + } + + if (FAIL_CODE != (Long) result) { + return true; + } else { + return false; + } + } + + + /** + * read lua script + */ + private void buildScript() { + script = ScriptUtil.getScript("limit.lua"); + } + + + /** + * the builder + * @param + */ + public static class Builder{ + private T jedis = null ; + + private int limit = 200; + + + public Builder(T jedis){ + this.jedis = jedis ; + } + + public Builder limit(int limit){ + this.limit = limit ; + return this; + } + + public RedisLimit build(){ + return new RedisLimit(this) ; + } + + } +} +``` + +这样客户端在使用时: + +```java +RedisLimit redisLimit = new RedisLimit.Builder<>(jedisCluster) + .limit(limit) + .build(); +``` + +更加的简单直接,并且避免了将创建过程分成了多个子步骤。 + +这在有多个构造参数,但又不是必选字段时很有作用。 + +因此顺便将分布式锁的构建器方式也一并更新了: + +[https://github.com/crossoverJie/distributed-redis-tool#features](https://github.com/crossoverJie/distributed-redis-tool#features) + +> 更多内容可以参考 Effective Java + +### API + +从上文可以看出,使用过程就是调用 `limit` 方法。 + +```java + //限流 + boolean limit = redisLimit.limit(); + if (!limit){ + //具体限流逻辑 + } +``` + +为了减少侵入性,也为了简化客户端提供了两种注解方式。 + +#### @ControllerLimit + +该注解可以作用于 `@RequestMapping` 修饰的接口中,并会在限流后提供限流响应。 + +实现如下: + +```java +@Component +public class WebIntercept extends WebMvcConfigurerAdapter { + + private static Logger logger = LoggerFactory.getLogger(WebIntercept.class); + + + @Autowired + private RedisLimit redisLimit; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new CustomInterceptor()) + .addPathPatterns("/**"); + } + + + private class CustomInterceptor extends HandlerInterceptorAdapter { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + + + if (redisLimit == null) { + throw new NullPointerException("redisLimit is null"); + } + + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod) handler; + + ControllerLimit annotation = method.getMethodAnnotation(ControllerLimit.class); + if (annotation == null) { + //skip + return true; + } + + boolean limit = redisLimit.limit(); + if (!limit) { + logger.warn("request has bean limit"); + response.sendError(500, "request limit"); + return false; + } + + } + + return true; + + } + } +} +``` + +其实就是实现了 SpringMVC 中的拦截器,并在拦截过程中判断是否有使用注解,从而调用限流逻辑。 + +**前提是应用需要扫描到该类,让 Spring 进行管理。** + +```java +@ComponentScan(value = "com.crossoverjie.distributed.intercept") +``` + +#### @CommonLimit + +当然也可以在普通方法中使用。实现原理则是 Spring AOP (SpringMVC 的拦截器本质也是 AOP)。 + +```java +@Aspect +@Component +@EnableAspectJAutoProxy(proxyTargetClass = true) +public class CommonAspect { + + private static Logger logger = LoggerFactory.getLogger(CommonAspect.class); + + @Autowired + private RedisLimit redisLimit ; + + @Pointcut("@annotation(com.crossoverjie.distributed.annotation.CommonLimit)") + private void check(){} + + @Before("check()") + public void before(JoinPoint joinPoint) throws Exception { + + if (redisLimit == null) { + throw new NullPointerException("redisLimit is null"); + } + + boolean limit = redisLimit.limit(); + if (!limit) { + logger.warn("request has bean limit"); + throw new RuntimeException("request has bean limit") ; + } + + } +} +``` + +很简单,也是在拦截过程中调用限流。 + +当然使用时也得扫描到该包: + +```java +@ComponentScan(value = "com.crossoverjie.distributed.intercept") +``` + +### 总结 + +**限流**在一个高并发大流量的系统中是保护应用的一个利器,成熟的方案也很多,希望对刚了解这一块的朋友提供一些思路。 + +以上所有的源码: + +- [https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) +- [https://github.com/crossoverJie/springboot-cloud](https://github.com/crossoverJie/springboot-cloud) + +感兴趣的朋友可以点个 Star 或是提交 PR。 + diff --git a/MD/distributed/distributed-lock-redis.md b/MD/distributed/distributed-lock-redis.md new file mode 100644 index 00000000..8c7d8abb --- /dev/null +++ b/MD/distributed/distributed-lock-redis.md @@ -0,0 +1,287 @@ +![](https://ws3.sinaimg.cn/large/006tKfTcgy1fpvathnbf6j31kw11xwl3.jpg) + +## 前言 +分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三。 + +首先谈到分布式锁自然也就联想到分布式应用。 + +在我们将应用拆分为分布式应用之前的单机系统中,对一些并发场景读取公共资源时如扣库存,卖车票之类的需求可以简单的使用[同步](http://crossoverjie.top/2018/01/14/Synchronize/)或者是[加锁](http://crossoverjie.top/2018/01/25/ReentrantLock/)就可以实现。 + +但是应用分布式了之后系统由以前的单进程多线程的程序变为了多进程多线程,这时使用以上的解决方案明显就不够了。 + + +因此业界常用的解决方案通常是借助于一个第三方组件并利用它自身的排他性来达到多进程的互斥。如: + +- 基于 DB 的唯一索引。 +- 基于 ZK 的临时有序节点。 +- 基于 Redis 的 `NX EX` 参数。 + +这里主要基于 Redis 进行讨论。 + + + +## 实现 + +既然是选用了 Redis,那么它就得具有排他性才行。同时它最好也有锁的一些基本特性: + +- 高性能(加、解锁时高性能) +- 可以使用阻塞锁与非阻塞锁。 +- 不能出现死锁。 +- 可用性(不能出现节点 down 掉后加锁失败)。 + +这里利用 `Redis set key` 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。 + +所以利用以上两个特性可以保证在同一时刻只会有一个进程获得锁,并且不会出现死锁(最坏的情况就是超时自动删除 key)。 + + +### 加锁 + +实现代码如下: + +```java + + private static final String SET_IF_NOT_EXIST = "NX"; + private static final String SET_WITH_EXPIRE_TIME = "PX"; + + public boolean tryLock(String key, String request) { + String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); + + if (LOCK_MSG.equals(result)){ + return true ; + }else { + return false ; + } + } +``` + +注意这里使用的 jedis 的 + +```java +String set(String key, String value, String nxxx, String expx, long time); +``` + +api。 + +该命令可以保证 NX EX 的原子性。 + +一定不要把两个命令(NX EX)分开执行,如果在 NX 之后程序出现问题就有可能产生死锁。 + +#### 阻塞锁 +同时也可以实现一个阻塞锁: + +```java + //一直阻塞 + public void lock(String key, String request) throws InterruptedException { + + for (;;){ + String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); + if (LOCK_MSG.equals(result)){ + break ; + } + + //防止一直消耗 CPU + Thread.sleep(DEFAULT_SLEEP_TIME) ; + } + + } + + //自定义阻塞时间 + public boolean lock(String key, String request,int blockTime) throws InterruptedException { + + while (blockTime >= 0){ + + String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); + if (LOCK_MSG.equals(result)){ + return true ; + } + blockTime -= DEFAULT_SLEEP_TIME ; + + Thread.sleep(DEFAULT_SLEEP_TIME) ; + } + return false ; + } + +``` + +### 解锁 + +解锁也很简单,其实就是把这个 key 删掉就万事大吉了,比如使用 `del key` 命令。 + +但现实往往没有那么 easy。 + +如果进程 A 获取了锁设置了超时时间,但是由于执行周期较长导致到了超时时间之后锁就自动释放了。这时进程 B 获取了该锁执行很快就释放锁。这样就会出现进程 B 将进程 A 的锁释放了。 + +所以最好的方式是在每次解锁时都需要判断锁**是否是自己**的。 + +这时就需要结合加锁机制一起实现了。 + +加锁时需要传递一个参数,将该参数作为这个 key 的 value,这样每次解锁时判断 value 是否相等即可。 + +所以解锁代码就不能是简单的 `del`了。 + +```java + public boolean unlock(String key,String request){ + //lua script + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + + Object result = null ; + if (jedis instanceof Jedis){ + result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request)); + }else if (jedis instanceof JedisCluster){ + result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request)); + }else { + //throw new RuntimeException("instance is error") ; + return false ; + } + + if (UNLOCK_MSG.equals(result)){ + return true ; + }else { + return false ; + } + } +``` + +这里使用了一个 `lua` 脚本来判断 value 是否相等,相等才执行 del 命令。 + +使用 `lua` 也可以保证这里两个操作的原子性。 + +因此上文提到的四个基本特性也能满足了: + +- 使用 Redis 可以保证性能。 +- 阻塞锁与非阻塞锁见上文。 +- 利用超时机制解决了死锁。 +- Redis 支持集群部署提高了可用性。 + +## 使用 + +我自己有撸了一个完整的实现,并且已经用于了生产,有兴趣的朋友可以开箱使用: + +maven 依赖: + +```xml + + top.crossoverjie.opensource + distributed-redis-lock + 1.0.0 + +``` + +配置 bean : + +```java +@Configuration +public class RedisLockConfig { + + @Bean + public RedisLock build(){ + RedisLock redisLock = new RedisLock() ; + HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ; + JedisCluster jedisCluster = new JedisCluster(hostAndPort) ; + // Jedis 或 JedisCluster 都可以 + redisLock.setJedisCluster(jedisCluster) ; + return redisLock ; + } + +} + +``` + +使用: + +```java + @Autowired + private RedisLock redisLock ; + + public void use() { + String key = "key"; + String request = UUID.randomUUID().toString(); + try { + boolean locktest = redisLock.tryLock(key, request); + if (!locktest) { + System.out.println("locked error"); + return; + } + + + //do something + + } finally { + redisLock.unlock(key,request) ; + } + + } + +``` + +使用很简单。这里主要是想利用 Spring 来帮我们管理 RedisLock 这个单例的 bean,所以在释放锁的时候需要手动(因为整个上下文只有一个 RedisLock 实例)的传入 key 以及 request(api 看起来不是特别优雅)。 + +也可以在每次使用锁的时候 new 一个 RedisLock 传入 key 以及 request,这样倒是在解锁时很方便。但是需要自行管理 RedisLock 的实例。各有优劣吧。 + +项目源码在: + +[https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) + +欢迎讨论。 + +## 单测 + +在做这个项目的时候让我不得不想提一下**单测**。 + +因为这个应用是强依赖于第三方组件的(Redis),但是在单测中我们需要排除掉这种依赖。比如其他伙伴 fork 了该项目想在本地跑一遍单测,结果运行不起来: + +1. 有可能是 Redis 的 ip、端口和单测里的不一致。 +2. Redis 自身可能也有问题。 +3. 也有可能是该同学的环境中并没有 Redis。 + +所以最好是要把这些外部不稳定的因素排除掉,单测只测我们写好的代码。 + +于是就可以引入单测利器 `Mock` 了。 + +它的想法很简答,就是要把你所依赖的外部资源统统屏蔽掉。如:数据库、外部接口、外部文件等等。 + +使用方式也挺简单,可以参考该项目的单测: + +```java + @Test + public void tryLock() throws Exception { + String key = "test"; + String request = UUID.randomUUID().toString(); + Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), + Mockito.anyString(), Mockito.anyLong())).thenReturn("OK"); + + boolean locktest = redisLock.tryLock(key, request); + System.out.println("locktest=" + locktest); + + Assert.assertTrue(locktest); + + //check + Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), + Mockito.anyString(), Mockito.anyLong()); + } +``` + +这里只是简单演示下,可以的话下次仔细分析分析。 + +它的原理其实也挺简单,debug 的话可以很直接的看出来: + +![](https://ws2.sinaimg.cn/large/006tKfTcgy1fpxho866hbj311u0ej42f.jpg) + +这里我们所依赖的 JedisCluster 其实是一个 `cglib 代理对象`。所以也不难想到它是如何工作的。 + +比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。 + +Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。 + +这样我们就可以随心所欲的测试了,**完全把外部依赖所屏蔽了**。 + +## 总结 + +至此一个基于 Redis 的分布式锁完成,但是依然有些问题。 + +- 如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。 +- 就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。 + +感兴趣的朋友还可以参考 [Redisson](https://github.com/redisson/redisson) 的实现。 + + diff --git a/MD/jvm/OOM-Disruptor.md b/MD/jvm/OOM-Disruptor.md new file mode 100644 index 00000000..8b88697e --- /dev/null +++ b/MD/jvm/OOM-Disruptor.md @@ -0,0 +1,125 @@ +![](https://ws2.sinaimg.cn/large/0069RVTdgy1fupvtq0tf1j31kw11x1ab.jpg) + +# 前言 + +`OutOfMemoryError` 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。 + +本文以最近碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的同学带来思路和帮助。 + +主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。 + + + +# 表象 + +最近我们生产上的一个应用不断的爆出内存溢出,并且随着业务量的增长出现的频次越来越高。 + +该程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来然后批量的做持久化操作。 + +而现象则是随着 Kafka 的消息越多,出现的异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。 + +> 重启大法虽好,可是依然不能根本解决问题。 + +# 排查 + +于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。 + +![](https://ws1.sinaimg.cn/large/0069RVTdgy1fupwodz2tlj30rd0b1tcj.jpg) + +结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 + +结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。 + +![](https://ws4.sinaimg.cn/large/0069RVTdly1fupx2amu1lj30t90b17oe.jpg) + +甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 + +这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。 + +# 定位 + +由于生产上的内存 dump 文件非常大,达到了几十G。也是由于我们的内存设置太大有关。 + +所以导致想使用 MAT 分析需要花费大量时间。 + +因此我们便想是否可以在本地复现,这样就要好定位的多。 + +为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。 + + +然后在消费 Kafka 那里 Mock 为一个 while 循环一直不断的生成数据。 + +同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。 + +结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。 + +![](https://ws2.sinaimg.cn/large/0069RVTdly1fupxfovjhgj30vl0kywps.jpg) + + +没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。 + +查看生产的日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**。 + +为了尽可能的模拟生产情况便在服务器上跑着一个生产者程序,一直源源不断的向 Kafka 中发送数据。 + +果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 + +![](https://ws4.sinaimg.cn/large/0069RVTdly1fupxcg3yh7j31kw0xi122.jpg) + +同时后台也开始打印内存溢出了,这样便复现出问题。 + +# 解决 + +从目前的表现来看就是内存中有许多对象一直存在强引用关系导致得不到回收。 + +于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。 + +![](https://ws2.sinaimg.cn/large/0069RVTdly1fupxqxqjdcj318c0q4kb3.jpg) + +结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 + +看到这个包自然就想到了 `Disruptor` 环形队列。 + +再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。 + +这里也就能说明为什么第一次模拟数据没复现问题了。 + +模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量是 700 倍的差距。 + +而 Disruptor 作为一个环形队列,再对象没有被覆盖之前是一直存在的。 + +我也做了一个实验,证明确实如此。 + +![](https://ws4.sinaimg.cn/large/0069RVTdly1fupy48es6lj30jd0b9dhu.jpg) + +我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。 + +所以在生产上假设我们的队列大小是 1024,那么随着系统的运行最终肯定会导致 1024 个位置上装满了对象,而且每个位置是 700 个! + +于是查看了生产上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`。 + +这个数量级就非常吓人了。 + +为了验证是否是这个问题,我在本地将该值换为 2 ,一个最小值试试。 + +同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: + +![](https://ws4.sinaimg.cn/large/0069RVTdly1fupyds04cij31kw0xial3.jpg) + +跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。 + +这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024*1024 是绝对不能再使用了。 + +# 总结 + +虽然到了最后也就改了一行代码(还没改,直接修改配置),但这排查过程我觉得是有意义的。 + +也会让大部分觉得 JVM 这样的黑盒难以下手的同学有一个直观的感受。 + +`同时也得感叹 Disruptor 东西虽好,也不能乱用哦!` + +相关演示代码查看: + +[https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) + +**你的点赞与转发是最大的支持。** \ No newline at end of file diff --git a/MD/kafka/kafka-product.md b/MD/kafka/kafka-product.md new file mode 100755 index 00000000..b3eecb4e --- /dev/null +++ b/MD/kafka/kafka-product.md @@ -0,0 +1,323 @@ + +# 从源码分析如何优雅的使用 Kafka 生产者 + + +![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw2g4pw7ooj31kw11xwjh.jpg) + +# 前言 + +在上文 [设计一个百万级的消息推送系统](https://crossoverjie.top/2018/09/25/netty/million-sms-push/) 中提到消息流转采用的是 `Kafka` 作为中间件。 + +其中有朋友咨询在大量消息的情况下 `Kakfa` 是如何保证消息的高效及一致性呢? + +正好以这个问题结合 `Kakfa` 的源码讨论下如何正确、高效的发送消息。 + +> 内容较多,对源码感兴趣的朋友请系好安全带😏(源码基于 `v0.10.0.0` 版本分析)。同时最好是有一定的 Kafka 使用经验,知晓基本的用法。 + + +# 简单的消息发送 + +在分析之前先看一个简单的消息发送是怎么样的。 + +> 以下代码基于 SpringBoot 构建。 + +首先创建一个 `org.apache.kafka.clients.producer.Producer` 的 bean。 + +![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw2hc2t8oij30n507g0u6.jpg) + +主要关注 `bootstrap.servers`,它是必填参数。指的是 Kafka 集群中的 broker 地址,例如 `127.0.0.1:9094`。 + +> 其余几个参数暂时不做讨论,后文会有详细介绍。 + +接着注入这个 bean 即可调用它的发送函数发送消息。 + +![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw2he841x7j30ou054751.jpg) + +这里我给某一个 Topic 发送了 10W 条数据,运行程序消息正常发送。 + +但这仅仅只是做到了消息发送,对消息是否成功送达完全没管,等于是纯`异步`的方式。 + +## 同步 + +那么我想知道消息到底发送成功没有该怎么办呢? + +其实 `Producer` 的 `API` 已经帮我们考虑到了,发送之后只需要调用它的 `get()` 方法即可同步获取发送结果。 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3fsyrkpbj3103065mya.jpg) + +发送结果: + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3ftq0w5lj312g053770.jpg) + +这样的发送效率其实是比较低下的,因为每次都需要同步等待消息发送的结果。 + +## 异步 + +为此我们应当采取异步的方式发送,其实 `send()` 方法默认则是异步的,只要不手动调用 `get()` 方法。 + +但这样就没法获知发送结果。 + +所以查看 `send()` 的 API 可以发现还有一个参数。 + +```java +Future send(ProducerRecord producer, Callback callback); +``` + +`Callback` 是一个回调接口,在消息发送完成之后可以回调我们自定义的实现。 + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3g4hce6aj30zv0b0dhp.jpg) + +执行之后的结果: + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3g54ne3oj31do06t0wl.jpg) + +同样的也能获取结果,同时发现回调的线程并不是上文同步时的`主线程`,这样也能证明是异步回调的。 + +同时回调的时候会传递两个参数: + +- `RecordMetadata` 和上文一致的消息发送成功后的元数据。 +- `Exception` 消息发送过程中的异常信息。 + +但是这两个参数并不会同时都有数据,只有发送失败才会有异常信息,同时发送元数据为空。 + +所以正确的写法应当是: + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3g9fst9kj30zy07jab0.jpg) + +> 至于为什么会只有参数一个有值,在下文的源码分析中会一一解释。 + + +# 源码分析 + +现在只掌握了基本的消息发送,想要深刻的理解发送中的一些参数配置还是得源码说了算。 + +首先还是来谈谈消息发送时的整个流程是怎么样的,`Kafka` 并不是简单的把消息通过网络发送到了 `broker` 中,在 Java 内部还是经过了许多优化和设计。 + +## 发送流程 + +为了直观的了解发送的流程,简单的画了几个在发送过程中关键的步骤。 + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3j5x05izj30a40btmxt.jpg) + +从上至下依次是: + +- 初始化以及真正发送消息的 `kafka-producer-network-thread` IO 线程。 +- 将消息序列化。 +- 得到需要发送的分区。 +- 写入内部的一个缓存区中。 +- 初始化的 IO 线程不断的消费这个缓存来发送消息。 + +## 步骤解析 + +接下来详解每个步骤。 + +### 初始化 + + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jc9hvwbj30rc0273yn.jpg) + +调用该构造方法进行初始化时,不止是简单的将基本参数写入 `KafkaProducer`。比较麻烦的是初始化 `Sender` 线程进行缓冲区消费。 + +初始化 IO 线程处: + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3jh4xtt2j31fo02pgms.jpg) + +可以看到 Sender 线程有需要成员变量,比如: + +``` +acks,retries,requestTimeout +``` + +等,这些参数会在后文分析。 + +### 序列化消息 + +在调用 `send()` 函数后其实第一步就是序列化,毕竟我们的消息需要通过网络才能发送到 Kafka。 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3job8ejaj31fw05owg2.jpg) + +其中的 `valueSerializer.serialize(record.topic(), record.value());` 是一个接口,我们需要在初始化时候指定序列化实现类。 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3jq5h0nyj30p607oq4e.jpg) + +我们也可以自己实现序列化,只需要实现 `org.apache.kafka.common.serialization.Serializer` 接口即可。 + +### 路由分区 + +接下来就是路由分区,通常我们使用的 `Topic` 为了实现扩展性以及高性能都会创建多个分区。 + +如果是一个分区好说,所有消息都往里面写入即可。 + +但多个分区就不可避免需要知道写入哪个分区。 + +通常有三种方式。 + +#### 指定分区 + +可以在构建 `ProducerRecord` 为每条消息指定分区。 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jxiet6mj30pj06smyb.jpg) + +这样在路由时会判断是否有指定,有就直接使用该分区。 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jybsavdj30zj077abj.jpg) + +这种一般在特殊场景下会使用。 + +#### 自定义路由策略 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3k0giiy6j30zm079ta7.jpg) + +如果没有指定分区,则会调用 `partitioner.partition` 接口执行自定义分区策略。 + +而我们也只需要自定义一个类实现 `org.apache.kafka.clients.producer.Partitioner` 接口,同时在创建 `KafkaProducer` 实例时配置 `partitioner.class` 参数。 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3k5uqf68j30rm04pt94.jpg) + +通常需要自定义分区一般是在想尽量的保证消息的顺序性。 + +或者是写入某些特有的分区,由特别的消费者来进行处理等。 + +#### 默认策略 + +最后一种则是默认的路由策略,如果我们啥都没做就会执行该策略。 + +该策略也会使得消息分配的比较均匀。 + +来看看它的实现: + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3kajn4iyj30r20g2772.jpg) + +简单的来说分为以下几步: + +- 获取 Topic 分区数。 +- 将内部维护的一个线程安全计数器 +1。 +- 与分区数取模得到分区编号。 + +其实这就是很典型的轮询算法,所以只要分区数不频繁变动这种方式也会比较均匀。 + +### 写入内部缓存 + +在 `send()` 方法拿到分区后会调用一个 `append()` 函数: + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3khecuqej313704uwg9.jpg) + +该函数中会调用一个 `getOrCreateDeque()` 写入到一个内部缓存中 `batches`。 + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3kih9wf1j30j005daaq.jpg) + + +### 消费缓存 + +在最开始初始化的 IO 线程其实是一个守护线程,它会一直消费这些数据。 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3kntf8xlj30sn0ju42o.jpg) + +通过图中的几个函数会获取到之前写入的数据。这块内容可以不必深究,但其中有个 `completeBatch` 方法却非常关键。 + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3kqrk5rnj312e0jbjve.jpg) + +调用该方法时候肯定已经是消息发送完毕了,所以会调用 `batch.done()` 来完成之前我们在 `send()` 方法中定义的回调接口。 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3kuprn02j30zo09qgnr.jpg) + + > 从这里也可以看出为什么之前说发送完成后元数据和异常信息只会出现一个。 + +# Producer 参数解析 + +发送流程讲完了再来看看 `Producer` 中比较重要的几个参数。 + +## acks + +`acks` 是一个影响消息吞吐量的一个关键参数。 + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3l52birsj30u607o0ta.jpg) + +主要有 `[all、-1, 0, 1]` 这几个选项,默认为 1。 + +由于 `Kafka` 不是采取的主备模式,而是采用类似于 Zookeeper 的主备模式。 + +> 前提是 `Topic` 配置副本数量 `replica > 1`。 + +当 `acks = all/-1` 时: + +意味着会确保所有的 follower 副本都完成数据的写入才会返回。 + +这样可以保证消息不会丢失! + +> 但同时性能和吞吐量却是最低的。 + + +当 `acks = 0` 时: + +producer 不会等待副本的任何响应,这样最容易丢失消息但同时性能却是最好的! + +当 `acks = 1` 时: + +这是一种折中的方案,它会等待副本 Leader 响应,但不会等到 follower 的响应。 + +一旦 Leader 挂掉消息就会丢失。但性能和消息安全性都得到了一定的保证。 + +## batch.size + +这个参数看名称就知道是内部缓存区的大小限制,对他适当的调大可以提高吞吐量。 + +但也不能极端,调太大会浪费内存。小了也发挥不了作用,也是一个典型的时间和空间的权衡。 + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3l2ydx4tj311l0e9ae3.jpg) + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3l3mh0pqj312409940u.jpg) + +上图是几个使用的体现。 + + +## retries + +`retries` 该参数主要是来做重试使用,当发生一些网络抖动都会造成重试。 + +这个参数也就是限制重试次数。 + +但也有一些其他问题。 + +- 因为是重发所以消息顺序可能不会一致,这也是上文提到就算是一个分区消息也不会是完全顺序的情况。 +- 还是由于网络问题,本来消息已经成功写入了但是没有成功响应给 producer,进行重试时就可能会出现`消息重复`。这种只能是消费者进行幂等处理。 + +# 高效的发送方式 + +如果消息量真的非常大,同时又需要尽快的将消息发送到 `Kafka`。一个 `producer` 始终会收到缓存大小等影响。 + +那是否可以创建多个 `producer` 来进行发送呢? + +- 配置一个最大 producer 个数。 +- 发送消息时首先获取一个 `producer`,获取的同时判断是否达到最大上限,没有就新建一个同时保存到内部的 `List` 中,保存时做好同步处理防止并发问题。 +- 获取发送者时可以按照默认的分区策略使用轮询的方式获取(保证使用均匀)。 + +这样在大量、频繁的消息发送场景中可以提高发送效率减轻单个 `producer` 的压力。 + +# 关闭 Producer + +最后则是 `Producer` 的关闭,Producer 在使用过程中消耗了不少资源(线程、内存、网络等)因此需要显式的关闭从而回收这些资源。 + + +![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3mw4a00rj311x0kp434.jpg) + +默认的 `close()` 方法和带有超时时间的方法都是在一定的时间后强制关闭。 + +但在过期之前都会处理完剩余的任务。 + +所以使用哪一个得视情况而定。 + + +# 总结 + +本文内容较多,从实例和源码的角度分析了 Kafka 生产者。 + +希望看完的朋友能有收获,同时也欢迎留言讨论。 + +不出意外下期会讨论 Kafka 消费者。 + +> 如果对你有帮助还请分享让更多的人看到。 + +**欢迎关注公众号一起交流:** + + \ No newline at end of file diff --git a/MD/soft-skills/Interview-experience.md b/MD/soft-skills/Interview-experience.md new file mode 100644 index 00000000..6c0bd3ba --- /dev/null +++ b/MD/soft-skills/Interview-experience.md @@ -0,0 +1,359 @@ +![](https://ws2.sinaimg.cn/large/006tNc79ly1fshrh2oexpj31kw0wkgsx.jpg) + +## 前言 + +最近有些朋友在面试阿里,加上 [Java-Interview](https://github.com/crossoverJie/Java-Interview) 项目的原因也有小伙伴和我讨论,近期也在负责部门的招聘,这让我想起年初那段长达三个月的奇葩面试经历🤣。 + +本来没想拿出来说的,毕竟最后也没成。 + +但由于那几个月的经历让我了解到了大厂的工作方式、对候选同学的考察重点以及面试官的套路等都有了全新的认识。 + +当然最重要的是这段时间的查漏补缺也让自己精进不少。 + + +先交代下背景吧: + + +从去年 12 月到今年三月底,我前前后后面了阿里三个部门。 + +其中两个部门通过了技术面试,还有一个跪在了三面。 + +光看结果还不错,但整个流程堪称曲折。 + +下面我会尽量描述流程以及大致的面试题目大纲,希望对想要跳槽、正在面试的同学带来点灵感,帮助可能谈不上,但启发还是能有。 + +以下内容较长,请再次备好瓜子板凳。 + + + + +## A 部门 + +首先是第一次机会,去年 12 月份有位大佬加我,后来才知道是一个部门的技术 Leader 在网上看到我的博客,问我想不想来阿里试试。 + +这时距离上次面阿里也过去一年多了,也想看看现在几斤几两,于是便同意了。 + +在推荐一周之后收到了杭州打来的电话,说来也巧,那时候我正在机场候机,距离登记还有大概一个小时,心想时间肯定够了。 + +那是我时隔一年多第一次面试,还是在机场这样嘈杂的环境里。多多少少还是有些紧张。 + +### 一面 + +以下是我印象比较深刻的内容: + +**面试官:** + +谈谈你做过项目中印象较深或自认为做的比较好的地方? + +**博主:** + +我觉得我在 XX 做的不错,用了 XX 需求实现 XX 功能,性能提高了 N 倍。 + +**面试官:** + +你说使用到了 AOP ,能谈谈它的实现原理嘛? + +**博主:** + +它是依靠动态代理实现的,动态代理又分为 JDK 自身的以及 CGLIB 。。。。 + +**面试官:** + +嗯,能说说他们的不同及优缺点嘛? + +**博主:** + +JDK 是基于接口实现,而 CGLIB 继承代理类。。。 + +就是这样会一直问下去,如果聊的差不多了就开始问一些零散的问题: + +- JMM 内存模型,如何划分的?分别存储什么内容?线程安全与否? +- 类加载机制,谈到双亲委派模型后会问到哪些违反了双亲委派模型?为什么?为什么要双亲委派?好处是什么? +- 平时怎么使用多线程?有哪些好处?线程池的几个核心参数的意义? +- 线程间通信的方式? +- HashMap 的原理?当谈到线程不安全时自然引申出 ConcurrentHashMap ,它的实现原理? +- 分库分表如何设计?垂直拆分、水平拆分? +- 业务 ID 的生成规则,有哪些方式? +- SQL 调优?平时使用数据库有哪些注意点? +- 当一个应用启动缓慢如何优化? + +大概是以上这些,当聊到倒数第二个时我已经登机了。最后不得不提前挂断,结束之前告诉我之后会换一个同事和我沟通,听到这样的回复一面应该是过了, +后面也确实证实了这点。 + +### 二面 + +大概过了一周,二面如期而至。 + +我听声音很熟,就尝试问下是不是之前一面的面试官,结果真是。 + +由于二面的面试官临时有事所以他来替一下。于是我赶紧问他能否把之前答的不好的再说说?的到了肯定的答复后开始了我的表演。 + +有了第一次的经验这一次自然也轻车熟路,原本感觉一切尽在掌握却被告知需要笔试突然被激醒。 + +笔试是一个在线平台,需要在网页中写代码,会有一个明确的题目: + +> 从一个日志文件中根据关键字读取日志,记录出现的次数,最后按照次数排序打印。 + +在这过程中切记要和面试官多多交流,因为笔试有时间限制,别到最后发现题目理解错了,这就和高考作文写完发现方向错了一样要命。 + +而且在沟通过程中体现出你解题的思路,即使最终结果不对,但说不定思考的过程很符合面试官的胃口哦。这也和今年的高考改卷一样;过程正确得高分,只有结果得低分。 + +### 三面 + +又过了差不多一周的时间接到了三面的电话,一般到了三面会是技术 Leader 之类的角色。 + +这个过程中不会过多强调技术细节,更多的考察软件能,比如团队协作、学习能力等。 + +但我记得也问了以下一些技术问题: + +- 谈谈你所理解的 HTTP 协议? +- 对 TCP 的理解?三次握手?滑动窗口? +- 基本算法,Base64 等。 +- Java 内存模型,Happen Before 的理解。 + +一周之后我接到了 HR 助理的电话约了和 HRBP 以及产品技术负责人的视频面试。 + +但是我却没有面下去,具体原因得往下看。 + + +## B 部门 + +在 A 部门三面完成后,我等了差不多一星期,这期间我却收到了一封邮件。 + +大概内容是他在 GitHub 上看到的我,他们的技术总监对我很感兴趣(我都不敢相信我的眼镜),问我想不想来阿里试试。 + +我对比了 A B 部门的区别发现 B 部门在做的事情上确实更加有诱惑力,之后我表达了有一个面试正在流程中的顾虑;对方表示可以私下和我快速的进行三面,如果一切没问题再交由我自行选择。至少对双方都是一个双赢嘛。 + +我想也不亏,并且对方很有诚意,就答应试试;于是便有了下面的面试: + + +### 一面 + +**面试官:** + +对 Java 锁的理解? + +**博主:** + +我谈到了 synchronize,Lock 接口的应用。 + +**面试官:** + +他们两者的区别以及优缺点呢? + +**博主:** + +`synchronize` 在 JDK1.6 之前称为重量锁,是通过进出对象监视器来实现同步的;1.6 之后做了 XX 优化。。。 + +而 `ReentrantLock` 是利用了一个巧妙数据结构实现的,并且加锁解锁是显式的。。。 + +之后又引申到[分布式锁](https://crossoverjie.top/%2F2018%2F03%2F29%2Fdistributed-lock%2Fdistributed-lock-redis%2F),光这块就聊了差不多半个小时。 + +之后又聊到了我的[开源项目](https://github.com/crossoverJie): +- 是如何想做这个项目的? +- 已经有一些关注了后续是如何规划的? +- 你今后的学习计划是什么? +- 平时看哪些书? + +之后技术聊的不是很多,但对于个人发展却聊了不少。 + + +> 关于锁相关的内容可以参考这里:[ReentrantLock 实现原理](https://crossoverjie.top/%2F2018%2F01%2F25%2FReentrantLock%2F) [synchronize 关键字原理](https://crossoverjie.top/%2F2018%2F01%2F14%2FSynchronize%2F) + + +### 二面 + +隔了差不多一天的时间,二面很快就来了。 + +内容不是很多: + +- [线程间通信的多种方式](https://crossoverjie.top/%2F2018%2F03%2F16%2Fjava-senior%2Fthread-communication%2F)? +- 限流算法?单机限流?分布式限流? +- 提到了 Guava Cache ,了解它的[实现原理](https://crossoverjie.top/2018/06/13/guava/guava-cache/)嘛? +- 如何定位一个线上问题? +- CPU 高负载?OOM 排查等? + +聊完之后表示第二天应该会有三面。 + +### 三面 + +三面的面试官应该是之前邮件中提到的那位总监大佬,以前应该也是一线的技术大牛;聊的问题不是很多: + +- 谈谈对 Netty 的理解? +- Netty 的线程模型? +- [写一个 LRU 缓存](https://crossoverjie.top/2018/04/07/algorithm/LRU-cache/)。 + + +### 笔试 + +本以为技术面试完了,结果后面告知所有的面试流程都得有笔试了,于是又参与了一次笔试: + +> [交替打印奇偶数](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/TwoThread.java) + +这个相对比较简单,基于锁、等待唤醒机制都是可以的。最后也告知笔试通过。 + +之后在推荐我的那位大佬的帮助下戏剧般的通过了整个技术轮(真的很感谢他的认可),并且得知这个消息是在我刚好和 A 部门约好视频面试时间之后。 + +也就意味着我必须**拒掉一个部门!** + +没看错,是我要拒掉一个。这对我来说确实太难了,我压根没想过还有两个机会摆在我面前。 + +最后凭着个人的爱好以及 B 部门的热情我很不好意思的拒掉了 A 部门。。。 + + + + +### HR 面 + +在面这之前我从来没有面过这样大厂的 HR 流程,于是疯狂搜索,希望能弥补点经验。 + +也许这就是乐极生悲吧,我确实猜中了 HR 问的大部分问题,但遗憾的是最终依然没能通过。 + +后来我在想如果我没有拒掉 A ,会不会结局不一样了? + +但现实就是如此,没有那么多假设,并且每个人也得为自己的选择负责! + +大概的问题是: +- 为什么想来阿里? +- 个人做的最成功最有挑战的事情是什么? +- 工作中最难忘的经历? +- 对加入我们团队有何期待? + +## C 部门 + +HR 这关被 Pass 之后没多久我居然又收到了第三个部门的邀约。 + +说实话当时我是拒绝的,之前经历了将近两个月的时间却没能如愿我内心是崩溃的。 + +我向联系我的大佬表达了我的想法,他倒觉得我最后被 pass 的原因是个小问题,再尝试的话会有很大的几率通过。 + +我把这事给朋友说了之后也支持我再试试,反正也没啥损失嘛,而且面试的状态还在。 + +所以我又被打了鸡血,才有了下面的面试经过: + +### 一面 + + +**面试官:** + +服务化框架的选型和差异? + +**博主:** + +一起探讨了 SpringCloud、Dubbo、Thrift 的差异,优缺点等。 + +**面试官:** + +[一致性 Hash 算法的原理](https://crossoverjie.top/2018/01/08/Consistent-Hash/)? + +**博主:** + +将数据 Hash 之后落到一个 `0 ~ 2^32-1` 构成的一个环上。。。。 + +**面试官:** + +谈谈你理解的 Zookeeper? + +**博主:** + +作为一个分布式协调器。。。 + +**面试官:** + +如何处理 MQ 重复消费? + +**博主:** + +业务幂等处理。。。。 + +**面试官:** + +客户端负载算法? + +**博主:** + +轮询、随机、一致性 Hash、故障转移、LRU 等。。 + +**面试官:** + +long 类型的赋值是否是原子的? + +**博主:** + +不是。。。 + +**面试官:** + +[volatile 关键字的原理及作用?happen Before?](https://crossoverjie.top/2018/03/09/volatile/) + +**博主:** + +可见性、一致性。。 + + +### 二面 + +一面之后大概一周的时间接到了二面的电话: + +原以为会像之前一样直接进入笔试,这次上来先简单聊了下: + +- 谈谈对微服务的理解,好处以及弊端? +- 分布式缓存的设计?热点缓存? + +之后才正式进入笔试流程: + +> 这次主要考察设计能力,其实就是对设计模式的理解?能否应对后续的扩展性。 + +笔试完了之后也和面试官交流,原以为会是算法之类的测试,后来得知他能看到前几轮的笔试情况,特地挑的没有做过的方向。 + +所以大家也不用刻意去押题,总有你想不到的,平时多积累才是硬道理。 + +### 三面 + +又过了两周左右,得到 HR 通知;希望能过去杭州参加现场面试。并且阿里包了来回的机票酒店等。 + +可见阿里对人才渴望还是舍得下成本的。 + +既然都这样了,就当成一次旅游所以去了一趟杭州。 + +现场面的时候有别于其他面试,是由两个面试官同时参与: + +> 给一个场景,谈谈你的架构方式。 + +这就对平时的积累要求较高了。 + +还有一个印象较深的是: + +> 在网页上点击一个按钮到服务器的整个流程,尽量完整。 + +其实之前看过,好像是 Google 的一个面试题。 + +完了之后让我回去等通知,没有见到 HR 我就知道凉了,果不其然。 + +## 总结 + +看到这里的朋友应该都是老铁了,我也把上文提到的大多数面试题整理在了 GitHub: + +![](https://ws1.sinaimg.cn/large/006tNc79gy1fsi40z9dulj30sl0p00yg.jpg) + +厂库地址: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) + +最后总结下这将近四个月的面试心得: + +- 一定要积极的推销自己,像在 A 部门的三面时,由于基础答得不是很好;所以最后我表达了自己的态度,对工作、技术的积极性。让面试官看到你的潜力值得一个 HC 名额。 +- 面试过程中遇到自己的不会的可以主动提出,切不可不懂装懂,这一问就露馅。可以将面试官引导到自己擅长的领域。比如当时我正好研究了锁,所以和面试官一聊就是半小时这就是加分项。 +- 平时要主动积累知识。写博客和参与开源项目就是很好的方式。 +- 博客可以记录自己踩过的坑,加深印象,而且在写的过程中可以查漏补缺,最后把整个知识体系巩固的比较牢固,良好的内容还可以得到意想不到的收获,比如我第一次面试的机会。 +- GitHub 是开发者的一张名片,积极参与开源项目可以和全球大佬头脑风暴,并且在面试过程中绝对是一个加分利器。 +- 面试官一般最后都会问你有什么要问我的?千万不要问一些公司福利待遇之类的问题。可以问下本次面试的表现?还有哪些需要完善的?从而知道自己答得如何也能补全自己。 + +还有一点:不要在某次面试失利后否定自己,有时真的不是自己能力不行。这个也讲缘分。 + +**塞翁失马焉知非福** + +我就是个例子,虽然最后没能去成阿里,现在在公司也是一个部门的技术负责人,在我们城市还有个窝,温馨的家,和女朋友一起为想要的生活努力奋斗。 + + +> 欢迎关注作者公众号于我交流🤗。 \ No newline at end of file diff --git a/MD/soft-skills/how-to-be-developer.md b/MD/soft-skills/how-to-be-developer.md new file mode 100644 index 00000000..4bf400f8 --- /dev/null +++ b/MD/soft-skills/how-to-be-developer.md @@ -0,0 +1,345 @@ +![](https://ws4.sinaimg.cn/large/0069RVTdgy1fu1lwclu7hj31kw11vqf0.jpg) + +## 前言 + +已经记不清有多少读者问过: + +> 博主,你是怎么学习的?像我这样的情况有啥好的建议嘛? + + +也不知道啥时候我居然成人生导师了。当然我不排斥这些问题,和大家交流都是学习的过程。 + +因此也许诺会准备一篇关于学习方面的文章;所以本文其实准备了很久,篇幅较长,大家耐心看完希望能有收获。 + +> 以下内容仅代表我从业以来所积累的相关经验,我会从硬技能、软实力这些方面尽量阐述我所认为的 `“不那么差的程序员”` 应当做到哪些技能。 + + + +## 技能树 + +作为一名码代码的技术工人,怎么说干的还是技术活。 + +既然是技术活那专业实力就得过硬,下面我会按照相关类别谈谈我们应该掌握哪些。 + +### 计算机基础 + +一名和电脑打交道的工种,计算机是我们赖以生存的工具。所以一些基础技能是我们应该和必须掌握的。 + +> 比如网络相关的知识。 + +其中就包含了 TCP 协议,它和 UDP 的差异。需要理解 TCP 三次握手的含义,[拆、粘包](http://t.cn/RDYBny8)等问题。 + +当然上层最常见的 HTTP 也需要了解,甚至是熟悉。 + +这块推荐[《图解 HTTP》](https://book.douban.com/subject/25863515/)一书。 + +> 接着是操作系统相关知识。 + +由于工作后你写的大部分代码都是运行在 Linux 服务器上,所以对于这个看它脸色行事主你也得熟悉才行。 + +比如进程、线程、内存等概念;服务器常见的命令使用,这个没啥窍门就是得平时多敲敲多总结。 + +我也是之前兼职了半年运维才算是对这一块比较熟悉。 + +Linux 这个自然是推荐业界非常出名的[《鸟哥的 Linux 私房菜》](https://book.douban.com/subject/4889838/)。 + + +当作为一个初学者学习这些东西时肯定会觉得枯燥乏味,大学一般在讲专业课之前都会有这些基础学科。我相信大部分同学应该都没怎么仔细听讲,因为确实这些东西就算是学会了记熟了也没有太多直接的激励。 + +但当你工作几年之后会发现,只要你还在做计算机相关的工作,这些都是绕不开的,当哪天这些知识不经意的帮助到你时你会庆幸当初正确的选择。 + + +### 数据结构与算法 + +接下来会谈到另一门枯燥的课程:数据结构。 + +这块当初在大学时也是最不受待见的一门课程,也是我唯一挂过的科目。 + +记得当时每次上课老师就让大家用 C 语言练习书上的习题,看着一个个拆开都认识的字母组合在一起就六亲不认我果断选择了放弃。 + +这也造成现在的我每隔一段时间就要看二叉树、红黑树、栈、队列等知识,加深印象。 + +算法这个东西我确实没有啥发言权,之前坚持刷了部分 [LeetCode](https://github.com/crossoverJie/leetcode) 的题目也大多停留在初中级。 + +但像基本的查找、排序算法我觉得还是要会的,不一定要手写出来但要理解其思路。 + +所以**强烈建议**还在大学同学们积极参与一些 ACM 比赛,绝对是今后的加分利器。 + +这一块内容可能会在应届生校招时发挥较大作用,在工作中如果你的本职工作是 `Java Web` 开发的话,这一块涉猎的几率还是比较低。 + +不过一旦你接触到了模型设计、中间件、高效存储、查询等内容这些也是绕不过的坎。 + +这块内容和上面的计算机基础差不多,对于我们 Java 开发来说我觉得平时除了多刷刷 LeetCode 加深印象之外,在日常开发中每选择一个容器存放数据时想想为什么选它?有没有更好的存储方式?写入、查询效率如何? + +同样的坚持下去,今后肯定收货颇丰。 + +同时推荐[《算法(第4版)》](https://book.douban.com/subject/19952400/) + + +### Java 基础 + +这里大部分的读者都是 Java 相关,所以这个强相关的技能非常重要。 + +Java 基础则是走向 Java 高级的必经之路。 + +这里抛开基本语法不谈,重点讨论实际工作中高频次的东西。 + +- 基本容器,如:HashMap、ArrayList、HashSet、LinkedList 等,不但要会用还得了解其中的原理。这样才能在不同的场景选择最优的设计。 +- IO、NIO 也是需要掌握。日常开发中大部分是在和磁盘、网络(写日志、数据库、Redis)打交道,这些都是 IO 的过程。 +- 常见的设计模式如:代理、工厂、回调、构建者模式,这对开发灵活、扩展性强的应用有很大帮助。 +- Java 多线程是非常重要的特性,日常开发很多。能理解线程模型、多线程优缺点、以及如何避免。 +- 良好的单测习惯,很多人觉得写单测浪费时间没有意义。但正是有了单测可以提前暴露出许多问题,减少测试返工几率,提高代码质量。 +- 良好的编程规范,这个可以参考《阿里巴巴 Java 开发手册》以及在它基础上优化的[《唯品会 Java 手册》](https://vipshop.github.io/vjtools/#/standard/) + + +> [《Java核心技术·卷 I》](https://book.douban.com/subject/26880667/)值得推荐。 + + +### 多线程应用 + +有了扎实的基础之后来谈谈多线程、并发相关的内容。 + +想让自己的 title 里加上“高级”两字肯定得经过并发的洗礼。 + +> 这里谈论的并发主要是指单应用里的场景,多应用的可以看后文的分布式内容。 + +多线程的出现主要是为了提高 CPU 的利用率、任务的执行效率。但并不是用了多线程就一定能达到这样的效果,因为它同时也带来了一些问题: + +- 上下文切换 +- 共享资源 +- 可见性、原子性、有序性等。 + +一旦使用了多线程那肯定会比单线程的程序要变得复杂和不可控,甚至使用不当还会比单线程慢。所以要考虑清楚是否真的需要多线程。 + + +会用了之后也要考虑为啥多线程会出现那样的问题,这时就需要理解内存模型、可见性之类的知识点。 + +同样的解决方式又有哪些?各自的优缺点也需要掌握。 + +谈到多线程就不得不提并发包下面的内容 `java.util.concurrent`。 + +最常用及需要掌握的有: + +- 原子类:用于并发场景的原子操作。 +- 队列。常用于解耦,需要了解其实现原理。 +- 并发工具,如 [ConcurrentHashMap](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)、[CountDownLatch](https://crossoverjie.top/%2F2018%2F03%2F16%2Fjava-senior%2Fthread-communication%2F#CountDownLatch-%E5%B9%B6%E5%8F%91%E5%B7%A5%E5%85%B7) 之类的工具使用以及原理。 +- [线程池使用](https://crossoverjie.top/2018/07/29/java-senior/ThreadPool/),以及相关原理。 +- 锁相关内容:[synchronized](https://crossoverjie.top/2018/01/14/Synchronize/)、[ReentrantLock](https://crossoverjie.top/2018/01/25/ReentrantLock/) 的使用及原理。 + + +这一块的内容可以然我们知道写 JDK 大牛处理并发的思路,对我们自己编写高质量的多线程程序也有很多帮助。 + +推荐[《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/)很好的并发入门书籍。 + +### JVM 虚拟机 + +想要深入 Java ,JVM 是不可或缺的。对于大部分工作 1~3 年的开发者来说直接接触这一些内容是比较少的。 + +到了 3~5 年这个阶段就必须得了解了,以下内容我觉得是必须要掌握的: + +- JVM 内存划分,[知道哪块内存存放哪些内容](https://crossoverjie.top/%2F2018%2F01%2F18%2FnewObject%2F);线程安全与否;内存不够怎么处理等。 +- 不同情况的[内存溢出、栈溢出](https://github.com/crossoverJie/Java-Interview/blob/master/MD/OOM-analysis.md#oom-%E5%88%86%E6%9E%90),以及定位解决方案。 +- [分代的垃圾回收策略。](https://github.com/crossoverJie/Java-Interview/blob/master/MD/GarbageCollection.md) +- [线上问题定位及相关解决方案](https://crossoverjie.top/2018/07/08/java-senior/JVM-Troubleshoot/)。 +- 一个类的加载、创建对象、垃圾回收、类卸载的整个过程。 + +掌握这些内容真的对实际分析问题起到巨大帮助。 + +> 对此强力推荐[《深入理解Java虚拟机](https://book.douban.com/subject/24722612/)》,这本书反反复复看过好几遍,每个阶段阅读都有不同的收获。 + +### 数据库 + +做 WEB 应用开发的同学肯定要和数据库打不少交道,而且通常来说一个系统最先出现瓶颈往往都是数据库,说数据库是压到系统的最后一根稻草一点也不为过。 + +所以对数据库的掌握也是非常有必要。拿互联网用的较多的 MySQL 数据库为例,一些必须掌握的知识点: + + +- 索引的数据结构及原理、哪些字段应当创建索引。 +- 针对于一个慢 SQL 的优化思路。 +- 数据库水平垂直拆分的方案,需要了解业界常用的 MyCAT、sharding-sphere 等中间件。 + +常规使用可以参考《阿里巴巴 Java 开发手册》中的数据库章节,想要深入了解 MySQL 那肯定得推荐经典的[《高性能 MySQL》](https://book.douban.com/subject/23008813/)一书了。 + +### 分布式技术 + +随着互联网的发展,传统的单体应用越来越不适合现有场景。 + +因此分布式技术出现了,这块涵盖的内容太多了,经验有限只能列举我日常使用到的一些内容: + +- 首先是一些基础理论如:CAP 定理,知道分布式系统会带来的一些问题以及各个应用权衡的方式。 +- 了解近些年大热的微服务相关定义、来源以及对比,有条件的可以阅读 `martin fowler` 的原文 [Microservices](https://martinfowler.com/articles/microservices.html),或者也可以搜索相关的国内翻译。 +- 对 Dubbo、SpringCloud 等分布式框架的使用,最好是要了解原理。 +- 接着要对分布式带来的问题提出解决方案。如[分布式锁](https://crossoverjie.top/2018/03/29/distributed-lock/distributed-lock-redis/)、[分布式限流](https://crossoverjie.top/2018/04/28/sbc/sbc7-Distributed-Limit/)、分布式事务、[分布式缓存](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Cache-design.md)、分布式 ID、消息中间件等。 +- 也要了解一些分布式中的负载算法:权重、Hash、一致性 Hash、故障转移、[LRU](https://crossoverjie.top/2018/04/07/algorithm/LRU-cache/) 等。 +- 最好能做一个实践如:[秒杀架构实践 + ](https://crossoverjie.top/%2F2018%2F05%2F07%2Fssm%2FSSM18-seconds-kill%2F) + +之前有开源一个分布式相关解决组件: + +[https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) + +同时推荐一本入门科普[《大型网站技术架构》](https://book.douban.com/subject/25723064/),出版时间有点早,从中可以学习一些思路。 + + +### 懂点架构 + +相信大家都有一个架构师的梦想。 + +架构师给人的感觉就是画画图纸,搭好架子,下面的人员来添砖加瓦最终产出。 + +但其实需要的内功也要非常深厚,就上面列举的样样需要掌握,底层到操作系统、算法;上层到应用、框架都需要非常精通。(PPT 架构师除外) + +我自身参与架构经验也不多,所以只能提供有限的建议。 + +首先分布式肯定得掌握,毕竟现在大部分的架构都是基于分布式的。 + +这其中就得根据 CAP 理论结合项目情况来选择一致性还是可用性,同时如何做好适合现有团队的技术选型。 + +这里推荐下开涛老师的[《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/),列举了很多架构实例,不过网上褒贬不一,但对于刚入门架构的能科普不少知识。 + +## 如何学习 + +谈完了技能树,现在来聊聊如何学习,这也是被问的最多的一个话题。 + +而关于学习讨论的最多的也是看视频还是看书? + +### 视频 + +不得不承认视频是获取知识最便捷的来源,毕竟包含了图、文、声。 + +大学几年时间其实我也没好好上专业课,我记得真正入门 Java 还是一个暑假花了两个月的时间天天在家里看 ”马士兵“ 老师的视频教程,当时的资源也很老了,记得好像是 07 年出的视频(用的还是 Google )。 + +那段时间早起晚睡,每天学到东西之后马上实践,心里也很有成就感。后来开学之后一度成为同学们眼中的”学霸“人物。 + +> 现在打开我 12 年的电脑,硬盘里还躺着好几十 G 的教学视频。 + +### 看书 + +工作后时间真的很宝贵,完全没有了学生生涯的想学就学的自由。所以现在我主要知识来源还是书籍。 + +这些是我最近看的书: + +![IMG_2387.JPG](https://i.loli.net/2018/08/12/5b6fd28576e0b.jpg) + + +看书又会涉及到电子书和纸质书的区别,我个人比较喜欢纸质书。毕竟我可以方便的记笔记以及可以随时切换章节。最主要的还是从小养成的闻书香的习惯。 + + +### 知识付费 + +近几年知识付费越来越流行,许多大佬也加入了这个行列,人们也逐渐在习惯为知识去付费。 + +说实话写一好篇文章出一份视频都非常不容易,能有正向的激励,作者才能持续输出更好的内容。 + +这块我觉得国内做的比较好我也为之付费的有极客时间、大佬的知识星球等。 + +这三点没有绝对的好坏之分,其实可以看出我刚入门的时候看视频,工作之后看书及知识付费内容。 + +视频的好处是可以跟着里面老师的思路一步一步往下走,比较有音视频代入感强,就像学校老师讲课一样。 + +但由于内容较长使读者没法知晓其中的重点,甚至都不敢快进生怕错过了哪个重要知识,现在由于 IT 越来越火,网上的视频也很多导致质量参差不齐也不成体系。 + +而看书可以选择性的浏览自己感兴趣的章节,费解的内容也方便反复阅读 + +所以建议刚入门的同学可以看看视频跟着学,参与工作一段时间后可以尝试多看看书。 + +当然这不是绝对的,找到适合自己的学习方式就好。但不管是视频还是看书都要多做多实践。 + +## 打造个人品牌 + +个人品牌看似很程序员这个职业不怎么沾边,但在现今的互联网时代对于每个人来说都很重要。 + +以往我们在写简历或是评估他人简历的时候往往不会想到去网络搜索他的个人信息,但在这个信息爆炸的时代你在网上留下的一点印记都能被发现。 + +### 博客 + +因此我们需要维护好自己的名片,比如先搭建自己的个人博客。 + +博客的好处我也谈过几次了,前期关注人少没关系,重要的是坚持,当你写到 50、100篇文章后你会发现自己在这过程中一定是的到了提高。 + + +### GitHub + +第二点就和技术人比较相关了:参与维护好自己的 GitHub。 + +由于 GitHub 的特殊属性,维护好后可以更好的打造个人品牌。 + +`Talk is cheap. Show me the code` 可不是随便说说的。 + +想要维护好可以从几个方面着手: + +- 参与他人的项目,不管是代码库还是知识库都可以,先融入进社区。 +- 发起自己的开源项目,不管是平时开发过程中的小痛点,还是精心整理的知识点都可以。 + +但这过程中有几点还是要注意: + +- 我们需要遵守 GitHub 的社交礼仪。能用英文尽量就用英文,特别是在国外厂库中。 +- 尽量少 push 一些与代码工作无关的内容,我认为这并不能提高自己的品牌。 +- `别去刷 star`。这也是近期才流行起来,不知道为什么总有一些人会钻这种空子,刷起来的热度对自己并没有任何提高。 + +这里有一篇国外大佬写的 `How to build your personal brand as a new developer` : + +[https://medium.freecodecamp.org/building-your-personal-brand-as-a-new-web-developer-f6d4150fd217](https://medium.freecodecamp.org/building-your-personal-brand-as-a-new-web-developer-f6d4150fd217) + + +## English 挺重要 + +再来谈谈英语的重要性,我记得刚上大学时老师以及一些培训机构都会说: + +> 别怕自己英语差就学不了编程,真正常用的就那些词语。 + +这句话虽没错,但英语在对 IT 这行来说还是有着极大的加分能力。 + +拿常见的 JDK 里的源码注释也是纯英文的,如果英语还不错的话,一些 Spring 的东西完全可以自学,直接去 Spring 官网就可以查看,甚至后面出的 SpringCloud,官方资料就是最好的教程。 + +再有就是平时查资料时,有条件的可以尝试用 `Google + 英文` 搜索,你会发现新的世界。 + +不然也不会有面向 `Google/Stack Overflow` 编程。 + +对于英语好的同学自然不怕,那不怎么好的咋办呢? + +比如我,但我在坚持以下几点: + +- 所有的手机、电脑系统统统换成英语语言,养成习惯(不过也有尴尬的连菜单都找不到的情况)。 +- 订阅一些英语周刊,比如 ”湾区日报“。 +- 定期去类似于 [https://medium.com/](https://medium.com/) 这样具有影响力的国外社区阅读文章。 + +虽然现在我也谈不上多好,但目前我也在努力,希望大家也一起坚持。 + + +推荐一本近期在看的书《程序员的英语》。 + +## 保持竞争力 + +技术这个行业发展迅速、变化太快,每年也都有无数相关行业毕业生加入竞争,稍不留神就会被赶上甚至超越。 + +所以我们无时无刻都得保持竞争力。 + +多的谈不上,我只能谈下目前我在做的事情: + +- **打好基础**。不是学了之后就忘了,需要不停的去看,巩固,基础是万变不离其宗的。 +- 多看源码,了解原理,不要停留在调参侠的境界。 +- 关注行业发展、新技术、新动态至少不能落伍了。 +- 争取每周产出一篇技术相关文章。 +- 积极参与开源项目。 + + +## 思维导图 + +![](https://ws2.sinaimg.cn/large/0069RVTdgy1fu71j8bb1tj31kw1w1qlc.jpg) + +结合上文产出了一个思维导图更直观些。 + +## 总结 + +本文结合了自身的一些经验列举了一些方法,不一定对每位都有效需要自行判断。 + +也反反复复写了差不多一周的时间,希望对在这条路上和正在路上的朋友们起到一些作用。 + +大部分都只是谈了个思路,其实每一项单聊都能写很多。每个点都有推荐一本书籍,有更好建议欢迎留言讨论。 + +上文大部分的知识点都有维护在 GitHub 上,感兴趣的朋友可以自行查阅: + +![](https://ws1.sinaimg.cn/large/0069RVTdgy1fuc1ejsp0fj31kw1hx4qp.jpg) + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) \ No newline at end of file diff --git a/MD/spring/spring-bean-lifecycle.md b/MD/spring/spring-bean-lifecycle.md index b9693567..60f2796a 100644 --- a/MD/spring/spring-bean-lifecycle.md +++ b/MD/spring/spring-bean-lifecycle.md @@ -7,7 +7,7 @@ Spring Bean 的生命周期在整个 Spring 中占有很重要的位置,掌握 首先看下生命周期图: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fpjsamy6uoj30nt0cqq4i.jpg) +![](https://i.loli.net/2018/09/20/5ba2e83a54fd9.jpeg) 再谈生命周期之前有一点需要先明确: diff --git a/MD/third-party-component/cicada.md b/MD/third-party-component/cicada.md new file mode 100644 index 00000000..39e207bf --- /dev/null +++ b/MD/third-party-component/cicada.md @@ -0,0 +1,265 @@ +
+ + +
+ +[![Build Status](https://travis-ci.org/crossoverJie/cicada.svg?branch=master)](https://travis-ci.org/crossoverJie/cicada) +[![](https://maven-badges.herokuapp.com/maven-central/top.crossoverjie.opensource/cicada-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/top.crossoverjie.opensource/cicada-core/) +[![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5HPYvQk) + +[qq0groupsvg]: https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg +[qq0group]: https://jq.qq.com/?_wv=1027&k=5HPYvQk + + +📘[特性](#features) |🌁[快速启动](#quick-start) | 🏖[性能测试](#performance-test) | 🌈[更新记录](#changelog) | 💡 [联系作者](#contact-author)|🇦🇺[English](https://github.com/TogetherOS/cicada) + +

+ + +## 简介 + +基于 Netty4 实现的快速、轻量级 WEB 框架;没有过多的依赖,核心 jar 包仅 `30KB`。 + +如果你感兴趣,请点 [Star](https://github.com/crossoverJie/cicada/stargazers)。 + +## 特性 + +- [x] 代码简洁,没有过多依赖。 +- [x] 一行代码即可启动 HTTP 服务。 +- [x] 自定义拦截器。 +- [x] 灵活的传参方式。 +- [x] `json` 响应格式。 +- [x] 自定义配置。 +- [x] 多种响应方式。 +- [ ] `Cookie` 支持。 +- [ ] 文件上传。 + + +## 快速启动 + +创建一个 maven 项目,引入核心依赖。 + +```java + + top.crossoverjie.opensource + cicada-core + 1.0.3 + +``` + +启动类: + +```java +public class MainStart { + + public static void main(String[] args) throws InterruptedException { + CicadaServer.start(MainStart.class,"/cicada-example") ; + } +} +``` + +### 配置业务 Action + +创建业务 Action 实现 `top.crossoverjie.cicada.server.action.WorkAction` 接口。 + +```java +@CicadaAction(value = "demoAction") +public class DemoAction implements WorkAction { + + + private static final Logger LOGGER = LoggerBuilder.getLogger(DemoAction.class) ; + + private static AtomicLong index = new AtomicLong() ; + + @Override + public void execute(CicadaContext context,Param paramMap) throws Exception { + String name = paramMap.getString("name"); + Integer id = paramMap.getInteger("id"); + LOGGER.info("name=[{}],id=[{}]" , name,id); + + DemoResVO demoResVO = new DemoResVO() ; + demoResVO.setIndex(index.incrementAndGet()); + WorkRes res = new WorkRes(); + res.setCode(StatusEnum.SUCCESS.getCode()); + res.setMessage(StatusEnum.SUCCESS.getMessage()); + res.setDataBody(demoResVO) ; + context.json(res); + } + +} +``` + +启动应用访问 [http://127.0.0.1:7317/cicada-example/demoAction?name=12345&id=10](http://127.0.0.1:7317/cicada-example/demoAction?name=12345&id=10) + +```json +{ + "code": "9000", + "dataBody": { + "index": 1 + }, + "message": "成功" +} +``` + +## Cicada 上下文 + +通过 `context.json(),context.text()` 方法可以选择不同的响应方式。 + +```java +@CicadaAction("textAction") +public class TextAction implements WorkAction { + @Override + public void execute(CicadaContext context, Param param) throws Exception { + String url = context.request().getUrl(); + String method = context.request().getMethod(); + context.text("hello world url=" + url + " method=" + method); + } +} +``` + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fvxvvo8yioj313i0tudij.jpg) + +同时也可以根据 `context.request()` 获得请求上下文中的其他信息。 + +![](https://ws2.sinaimg.cn/large/006tNbRwly1fvxvxmpsjcj30yy0yo77h.jpg) + +## 自定义配置 + +`cicada` 默认会读取 classpath 下的 `application.properties` 配置文件。 + +同时也可以自定义配置文件。 + +只需要继承 `top.crossoverjie.cicada.server.configuration.AbstractCicadaConfiguration` + +并传入配置文件名称即可。比如: + + +```java +public class RedisConfiguration extends AbstractCicadaConfiguration { + + + public RedisConfiguration() { + super.setPropertiesName("redis.properties"); + } + +} + +public class KafkaConfiguration extends AbstractCicadaConfiguration { + + public KafkaConfiguration() { + super.setPropertiesName("kafka.properties"); + } + + +} +``` + +![](https://ws3.sinaimg.cn/large/0069RVTdgy1fv5mw7p5nvj31by0fo76t.jpg) + +### 获取配置 + +按照如下方式即可获取自定义配置: + +```java +KafkaConfiguration configuration = (KafkaConfiguration) getConfiguration(KafkaConfiguration.class); +RedisConfiguration redisConfiguration = (RedisConfiguration) ConfigurationHolder.getConfiguration(RedisConfiguration.class); +ApplicationConfiguration applicationConfiguration = (ApplicationConfiguration) ConfigurationHolder.getConfiguration(ApplicationConfiguration.class); + +String brokerList = configuration.get("kafka.broker.list"); +String redisHost = redisConfiguration.get("redis.host"); +String port = applicationConfiguration.get("cicada.port"); + +LOGGER.info("Configuration brokerList=[{}],redisHost=[{}] port=[{}]",brokerList,redisHost,port); +``` + +### 外置配置文件 + +当然在特殊环境中(`dev/test/pro`)也可以读取外置配置文件。只需要加上启动参数,保证参数名称和文件名一致即可。 + +```shell +-Dapplication.properties=/xx/application.properties +-Dkafka.properties=/xx/kakfa.properties +-Dredis.properties=/xx/redis.properties +``` + +## 自定义拦截器 + +实现 `top.crossoverjie.cicada.example.intercept.CicadaInterceptor` 接口。 + +```java +@Interceptor(value = "executeTimeInterceptor") +public class ExecuteTimeInterceptor implements CicadaInterceptor { + + private static final Logger LOGGER = LoggerBuilder.getLogger(ExecuteTimeInterceptor.class); + + private Long start; + + private Long end; + + @Override + public void before(Param param) { + start = System.currentTimeMillis(); + } + + @Override + public void after(Param param) { + end = System.currentTimeMillis(); + + LOGGER.info("cast [{}] times", end - start); + } +} +``` + +### 拦截适配器 + +同样也可以只实现其中一个方法,只需要继承 `top.crossoverjie.cicada.server.intercept.AbstractCicadaInterceptorAdapter` 抽象类。 + +```java +@Interceptor(value = "loggerInterceptor") +public class LoggerInterceptorAbstract extends AbstractCicadaInterceptorAdapter { + + private static final Logger LOGGER = LoggerBuilder.getLogger(LoggerInterceptorAbstract.class) ; + + @Override + public void before(Param param) { + LOGGER.info("logger param=[{}]",param.toString()); + } + +} +``` + +## 性能测试 + +![](https://ws4.sinaimg.cn/large/006tNbRwly1fv4luap7w0j31kw0iwdnu.jpg) + +> 测试条件:100 threads and 100 connections ;1G RAM/4 CPU。 + +**每秒将近 10W 请求。** + +## 更新记录 + +### v1.0.3 + +- 修复 [#9](https://github.com/TogetherOS/cicada/issues/9) +- 修复 [#8](https://github.com/TogetherOS/cicada/issues/8),多种响应方式。 +- 重构了核心代码,新增上下文环境。 +- 优雅停机。 + +### v1.0.2 + +- 修复 [#6](https://github.com/TogetherOS/cicada/issues/6) +- 自定义配置文件。 +- 灵活使用配置。 +- 重构代码。 + +## 联系作者 + + +> crossoverJie#gmail.com + + + +## 特别感谢 + +- [Netty](https://github.com/netty/netty) +- [blade](https://github.com/lets-blade/blade) \ No newline at end of file diff --git a/MD/third-party-component/guava-cache.md b/MD/third-party-component/guava-cache.md new file mode 100644 index 00000000..dfca253d --- /dev/null +++ b/MD/third-party-component/guava-cache.md @@ -0,0 +1,518 @@ +![1.jpeg](https://i.loli.net/2018/06/12/5b1fea79e07cb.jpeg) + +## 前言 + +Google 出的 [Guava](https://github.com/google/guava) 是 Java 核心增强的库,应用非常广泛。 + +我平时用的也挺频繁,这次就借助日常使用的 Cache 组件来看看 Google 大牛们是如何设计的。 + +## 缓存 + +> 本次主要讨论缓存。 + +缓存在日常开发中举足轻重,如果你的应用对某类数据有着较高的读取频次,并且改动较小时那就非常适合利用缓存来提高性能。 + +缓存之所以可以提高性能是因为它的读取效率很高,就像是 CPU 的 `L1、L2、L3` 缓存一样,级别越高相应的读取速度也会越快。 + +但也不是什么好处都占,读取速度快了但是它的内存更小资源更宝贵,所以我们应当缓存真正需要的数据。 + +> 其实也就是典型的空间换时间。 + +下面谈谈 Java 中所用到的缓存。 + + + +### JVM 缓存 + +首先是 JVM 缓存,也可以认为是堆缓存。 + +其实就是创建一些全局变量,如 `Map、List` 之类的容器用于存放数据。 + +这样的优势是使用简单但是也有以下问题: + +- 只能显式的写入,清除数据。 +- 不能按照一定的规则淘汰数据,如 `LRU,LFU,FIFO` 等。 +- 清除数据时的回调通知。 +- 其他一些定制功能等。 + +### Ehcache、Guava Cache + +所以出现了一些专门用作 JVM 缓存的开源工具出现了,如本文提到的 Guava Cache。 + +它具有上文 JVM 缓存不具有的功能,如自动清除数据、多种清除算法、清除回调等。 + +但也正因为有了这些功能,这样的缓存必然会多出许多东西需要额外维护,自然也就增加了系统的消耗。 + +### 分布式缓存 + +刚才提到的两种缓存其实都是堆内缓存,只能在单个节点中使用,这样在分布式场景下就招架不住了。 + +于是也有了一些缓存中间件,如 Redis、Memcached,在分布式环境下可以共享内存。 + +具体不在本次的讨论范围。 + +## Guava Cache 示例 + +之所以想到 Guava 的 Cache,也是最近在做一个需求,大体如下: + +> 从 Kafka 实时读取出应用系统的日志信息,该日志信息包含了应用的健康状况。 +> 如果在时间窗口 N 内发生了 X 次异常信息,相应的我就需要作出反馈(报警、记录日志等)。 + +对此 Guava 的 Cache 就非常适合,我利用了它的 N 个时间内不写入数据时缓存就清空的特点,在每次读取数据时判断异常信息是否大于 X 即可。 + +伪代码如下: + +```java + + @Value("${alert.in.time:2}") + private int time ; + + @Bean + public LoadingCache buildCache(){ + return CacheBuilder.newBuilder() + .expireAfterWrite(time, TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public AtomicLong load(Long key) throws Exception { + return new AtomicLong(0); + } + }); + } + + + /** + * 判断是否需要报警 + */ + public void checkAlert() { + try { + if (counter.get(KEY).incrementAndGet() >= limit) { + LOGGER.info("***********报警***********"); + + //将缓存清空 + counter.get(KEY).getAndSet(0L); + } + } catch (ExecutionException e) { + LOGGER.error("Exception", e); + } + } +``` + +首先是构建了 LoadingCache 对象,在 N 分钟内不写入数据时就回收缓存(当通过 Key 获取不到缓存时,默认返回 0)。 + +然后在每次消费时候调用 `checkAlert()` 方法进行校验,这样就可以达到上文的需求。 + +我们来设想下 Guava 它是如何实现过期自动清除数据,并且是可以按照 LRU 这样的方式清除的。 + +大胆假设下: + +> 内部通过一个队列来维护缓存的顺序,每次访问过的数据移动到队列头部,并且额外开启一个线程来判断数据是否过期,过期就删掉。有点类似于我之前写过的 [动手实现一个 LRU cache](https://crossoverjie.top/%2F2018%2F04%2F07%2Falgorithm%2FLRU-cache%2F) + + +胡适说过:大胆假设小心论证 + +下面来看看 Guava 到底是怎么实现。 + +### 原理分析 + +看原理最好不过是跟代码一步步走了: + +示例代码在这里: + +[https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java) + +![8.png](https://i.loli.net/2018/06/13/5b2008f4c1003.png) + + +为了能看出 Guava 是怎么删除过期数据的在获取缓存之前休眠了 5 秒钟,达到了超时条件。 + +![2.png](https://i.loli.net/2018/06/13/5b1ffe4eebae0.png) + +最终会发现在 `com.google.common.cache.LocalCache` 类的 2187 行比较关键。 + +再跟进去之前第 2182 行会发现先要判断 count 是否大于 0,这个 count 保存的是当前缓存的数量,并用 volatile 修饰保证了可见性。 + +> 更多关于 volatile 的相关信息可以查看 [你应该知道的 volatile 关键字](https://crossoverjie.top/%2F2018%2F03%2F09%2Fvolatile%2F) + + +接着往下跟到: + +![3.png](https://i.loli.net/2018/06/13/5b1fffc88c3e6.png) + +2761 行,根据方法名称可以看出是判断当前的 Entry 是否过期,该 entry 就是通过 key 查询到的。 + + +![](https://ws2.sinaimg.cn/large/006tNc79gy1ft9l0mx77rj30zk0a1tat.jpg) + +这里就很明显的看出是根据根据构建时指定的过期方式来判断当前 key 是否过期了。 + +![5.png](https://i.loli.net/2018/06/13/5b20017f32ff0.png) + +如果过期就往下走,尝试进行过期删除(需要加锁,后面会具体讨论)。 + +![6.png](https://i.loli.net/2018/06/13/5b2001eeb40d5.png) + +到了这里也很清晰了: + +- 获取当前缓存的总数量 +- 自减一(前面获取了锁,所以线程安全) +- 删除并将更新的总数赋值到 count。 + +其实大体上就是这个流程,Guava 并没有按照之前猜想的另起一个线程来维护过期数据。 + +应该是以下原因: + +- 新起线程需要资源消耗。 +- 维护过期数据还要获取额外的锁,增加了消耗。 + +而在查询时候顺带做了这些事情,但是如果该缓存迟迟没有访问也会存在数据不能被回收的情况,不过这对于一个高吞吐的应用来说也不是问题。 + +## 总结 + +最后再来总结下 Guava 的 Cache。 + +其实在上文跟代码时会发现通过一个 key 定位数据时有以下代码: + +![7.png](https://i.loli.net/2018/06/13/5b20040d257cb.png) + +如果有看过 [ConcurrentHashMap 的原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ConcurrentHashMap.md) 应该会想到这其实非常类似。 + +其实 Guava Cache 为了满足并发场景的使用,核心的数据结构就是按照 ConcurrentHashMap 来的,这里也是一个 key 定位到一个具体位置的过程。 + +> 先找到 Segment,再找具体的位置,等于是做了两次 Hash 定位。 + +上文有一个假设是对的,它内部会维护两个队列 `accessQueue,writeQueue` 用于记录缓存顺序,这样才可以按照顺序淘汰数据(类似于利用 LinkedHashMap 来做 LRU 缓存)。 + +同时从上文的构建方式来看,它也是[构建者模式](https://crossoverjie.top/2018/04/28/sbc/sbc7-Distributed-Limit/)来创建对象的。 + +因为作为一个给开发者使用的工具,需要有很多的自定义属性,利用构建则模式再合适不过了。 + +Guava 其实还有很多东西没谈到,比如它利用 GC 来回收内存,移除数据时的回调通知等。之后再接着讨论。 + +扫码关注微信公众号,第一时间获取消息。 + + + +## 进一步分析 + +## 前言 + +在上文「[Guava 源码分析(Cache 原理)](https://crossoverjie.top/2018/06/13/guava/guava-cache/)」中分析了 `Guava Cache` 的相关原理。 + +文末提到了**回收机制、移除时间通知**等内容,许多朋友也挺感兴趣,这次就这两个内容再来分析分析。 + + +> 在开始之前先补习下 Java 自带的两个特性,Guava 中都有具体的应用。 + +## Java 中的引用 + +首先是 Java 中的**引用**。 + +在之前分享过 JVM 是根据[可达性分析算法](https://github.com/crossoverJie/Java-Interview/blob/master/MD/GarbageCollection.md#%E5%8F%AF%E8%BE%BE%E6%80%A7%E5%88%86%E6%9E%90%E7%AE%97%E6%B3%95)找出需要回收的对象,判断对象的存活状态都和`引用`有关。 + +在 JDK1.2 之前这点设计的非常简单:一个对象的状态只有**引用**和**没被引用**两种区别。 + + + +这样的划分对垃圾回收不是很友好,因为总有一些对象的状态处于这两之间。 + +因此 1.2 之后新增了四种状态用于更细粒度的划分引用关系: + +- 强引用(Strong Reference):这种对象最为常见,比如 **`A a = new A();`**这就是典型的强引用;这样的强引用关系是不能被垃圾回收的。 +- 软引用(Soft Reference):这样的引用表明一些有用但不是必要的对象,在将发生垃圾回收之前是需要将这样的对象再次回收。 +- 弱引用(Weak Reference):这是一种比软引用还弱的引用关系,也是存放非必须的对象。当垃圾回收时,无论当前内存是否足够,这样的对象都会被回收。 +- 虚引用(Phantom Reference):这是一种最弱的引用关系,甚至没法通过引用来获取对象,它唯一的作用就是在被回收时可以获得通知。 + +## 事件回调 + +事件回调其实是一种常见的设计模式,比如之前讲过的 [Netty](https://crossoverjie.top/categories/Netty/) 就使用了这样的设计。 + +这里采用一个 demo,试下如下功能: + +- Caller 向 Notifier 提问。 +- 提问方式是异步,接着做其他事情。 +- Notifier 收到问题执行计算然后回调 Caller 告知结果。 + +在 Java 中利用接口来实现回调,所以需要定义一个接口: + +```java +public interface CallBackListener { + + /** + * 回调通知函数 + * @param msg + */ + void callBackNotify(String msg) ; +} +``` + +Caller 中调用 Notifier 执行提问,调用时将接口传递过去: + +```java +public class Caller { + + private final static Logger LOGGER = LoggerFactory.getLogger(Caller.class); + + private CallBackListener callBackListener ; + + private Notifier notifier ; + + private String question ; + + /** + * 使用 + */ + public void call(){ + + LOGGER.info("开始提问"); + + //新建线程,达到异步效果 + new Thread(new Runnable() { + @Override + public void run() { + try { + notifier.execute(Caller.this,question); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); + + LOGGER.info("提问完毕,我去干其他事了"); + } + + //隐藏 getter/setter + +} +``` + +Notifier 收到提问,执行计算(耗时操作),最后做出响应(回调接口,告诉 Caller 结果)。 + + +```java +public class Notifier { + + private final static Logger LOGGER = LoggerFactory.getLogger(Notifier.class); + + public void execute(Caller caller, String msg) throws InterruptedException { + LOGGER.info("收到消息=【{}】", msg); + + LOGGER.info("等待响应中。。。。。"); + TimeUnit.SECONDS.sleep(2); + + + caller.getCallBackListener().callBackNotify("我在北京!"); + + } + +} +``` + + +模拟执行: + +```java + public static void main(String[] args) { + Notifier notifier = new Notifier() ; + + Caller caller = new Caller() ; + caller.setNotifier(notifier) ; + caller.setQuestion("你在哪儿!"); + caller.setCallBackListener(new CallBackListener() { + @Override + public void callBackNotify(String msg) { + LOGGER.info("回复=【{}】" ,msg); + } + }); + + caller.call(); + } +``` + +最后执行结果: + +```log +2018-07-15 19:52:11.105 [main] INFO c.crossoverjie.guava.callback.Caller - 开始提问 +2018-07-15 19:52:11.118 [main] INFO c.crossoverjie.guava.callback.Caller - 提问完毕,我去干其他事了 +2018-07-15 19:52:11.117 [Thread-0] INFO c.c.guava.callback.Notifier - 收到消息=【你在哪儿!】 +2018-07-15 19:52:11.121 [Thread-0] INFO c.c.guava.callback.Notifier - 等待响应中。。。。。 +2018-07-15 19:52:13.124 [Thread-0] INFO com.crossoverjie.guava.callback.Main - 回复=【我在北京!】 +``` + +这样一个模拟的异步事件回调就完成了。 + +## Guava 的用法 + +Guava 就是利用了上文的两个特性来实现了**引用回收**及**移除通知**。 + +### 引用 + +可以在初始化缓存时利用: + +- CacheBuilder.weakKeys() +- CacheBuilder.weakValues() +- CacheBuilder.softValues() + +来自定义键和值的引用关系。 + +![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftatngp76aj30n20h6gpn.jpg) + +在上文的分析中可以看出 Cache 中的 `ReferenceEntry` 是类似于 HashMap 的 Entry 存放数据的。 + +来看看 ReferenceEntry 的定义: + +```java + interface ReferenceEntry { + /** + * Returns the value reference from this entry. + */ + ValueReference getValueReference(); + + /** + * Sets the value reference for this entry. + */ + void setValueReference(ValueReference valueReference); + + /** + * Returns the next entry in the chain. + */ + @Nullable + ReferenceEntry getNext(); + + /** + * Returns the entry's hash. + */ + int getHash(); + + /** + * Returns the key for this entry. + */ + @Nullable + K getKey(); + + /* + * Used by entries that use access order. Access entries are maintained in a doubly-linked list. + * New entries are added at the tail of the list at write time; stale entries are expired from + * the head of the list. + */ + + /** + * Returns the time that this entry was last accessed, in ns. + */ + long getAccessTime(); + + /** + * Sets the entry access time in ns. + */ + void setAccessTime(long time); +} +``` + +包含了很多常用的操作,如值引用、键引用、访问时间等。 + +根据 `ValueReference getValueReference();` 的实现: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftatsg5jfvj30vg059wg9.jpg) + +具有强引用和弱引用的不同实现。 + +key 也是相同的道理: + +![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftattls2uzj30w005eq4t.jpg) + +当使用这样的构造方式时,弱引用的 key 和 value 都会被垃圾回收。 + +当然我们也可以显式的回收: + +``` + /** + * Discards any cached value for key {@code key}. + * 单个回收 + */ + void invalidate(Object key); + + /** + * Discards any cached values for keys {@code keys}. + * + * @since 11.0 + */ + void invalidateAll(Iterable keys); + + /** + * Discards all entries in the cache. + */ + void invalidateAll(); +``` + +### 回调 + +改造了之前的例子: + +```java +loadingCache = CacheBuilder.newBuilder() + .expireAfterWrite(2, TimeUnit.SECONDS) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + LOGGER.info("删除原因={},删除 key={},删除 value={}",notification.getCause(),notification.getKey(),notification.getValue()); + } + }) + .build(new CacheLoader() { + @Override + public AtomicLong load(Integer key) throws Exception { + return new AtomicLong(0); + } + }); +``` + +执行结果: + +```log +2018-07-15 20:41:07.433 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1 +2018-07-15 20:41:07.442 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的所有内容={1000=0} +2018-07-15 20:41:07.443 [main] INFO c.crossoverjie.guava.CacheLoaderTest - job running times=10 +2018-07-15 20:41:10.461 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 删除原因=EXPIRED,删除 key=1000,删除 value=1 +2018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1 +2018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 缓存的所有内容={1000=0} +``` + +可以看出当缓存被删除的时候会回调我们自定义的函数,并告知删除原因。 + +那么 Guava 是如何实现的呢? + +![](https://ws3.sinaimg.cn/large/006tKfTcgy1ftau23uj5aj30mp08odh8.jpg) + +根据 LocalCache 中的 `getLiveValue()` 中判断缓存过期时,跟着这里的调用关系就会一直跟到: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau4ed7dcj30rm0a5acd.jpg) + +`removeValueFromChain()` 中的: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau5ywcojj30rs0750u9.jpg) + +`enqueueNotification()` 方法会将回收的缓存(包含了 key,value)以及回收原因包装成之前定义的事件接口加入到一个**本地队列**中。 + +![](https://ws4.sinaimg.cn/large/006tKfTcgy1ftau7hpijrj30sl06wtaf.jpg) + +这样一看也没有回调我们初始化时候的事件啊。 + +不过用过队列的同学应该能猜出,既然这里写入队列,那就肯定就有消费。 + +我们回到获取缓存的地方: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau9rwgacj30ti0hswio.jpg) + +在 finally 中执行了 `postReadCleanup()` 方法;其实在这里面就是对刚才的队列进行了消费: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftaubaco48j30lw0513zi.jpg) + +一直跟进来就会发现这里消费了队列,将之前包装好的移除消息调用了我们自定义的事件,这样就完成了一次事件回调。 + +## 总结 + +以上所有源码: + +[https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/callback/Main.java](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/callback/Main.java) + +通过分析 Guava 的源码可以让我们学习到顶级的设计及实现方式,甚至自己也能尝试编写。 + +Guava 里还有很多强大的增强实现,值得我们再好好研究。 diff --git a/MD/third-party-component/seconds-kill.md b/MD/third-party-component/seconds-kill.md new file mode 100644 index 00000000..a3139ad2 --- /dev/null +++ b/MD/third-party-component/seconds-kill.md @@ -0,0 +1,694 @@ +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr1z9k79lrj31kw11zwt8.jpg) + +## 前言 + +之前在 [JCSprout](https://github.com/crossoverJie/JCSprout/blob/master/MD/Spike.md) 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。 + +> 本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果,文章较长请准备好瓜子板凳(liushuizhang😂)。 + +本文所有涉及的代码: + +- [https://github.com/crossoverJie/SSM](https://github.com/crossoverJie/SSM) +- [https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) + +最终架构图: + +![系统架构设计.png](https://i.loli.net/2018/05/08/5af079ea8618b.png) + + + +先简单根据这个图谈下请求的流转,因为后面不管怎么改进这个都是没有变的。 + +- 前端请求进入 `web` 层,对应的代码就是 `controller`。 +- 之后将真正的库存校验、下单等请求发往 `Service` 层(其中 RPC 调用依然采用的 `dubbo`,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 [基于dubbo的分布式架构](https://crossoverjie.top/%2F2017%2F04%2F07%2FSSM11%2F))。 +- `Service` 层再对数据进行落地,下单完成。 + + +## 无限制 + +其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步: + +- 校验库存 +- 扣库存 +- 创建订单 +- 支付 + +基于上文的架构所以我们有了以下实现: + +先看看实际项目的结构: + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr38jkau5kj30jk07a754.jpg) + +还是和以前一样: + +- 提供出一个 `API` 用于 `Service` 层实现,以及 `web` 层消费。 +- web 层简单来说就是一个 `SpringMVC`。 +- `Service` 层则是真正的数据落地。 +- `SSM-SECONDS-KILL-ORDER-CONSUMER` 则是后文会提到的 `Kafka` 消费。 + + +数据库也是只有简单的两张表模拟下单: + +```sql +CREATE TABLE `stock` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', + `count` int(11) NOT NULL COMMENT '库存', + `sale` int(11) NOT NULL COMMENT '已售', + `version` int(11) NOT NULL COMMENT '乐观锁,版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + + +CREATE TABLE `stock_order` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `sid` int(11) NOT NULL COMMENT '库存ID', + `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8; +``` + +web 层 `controller` 实现: + + +```java + + @Autowired + private StockService stockService; + + @Autowired + private OrderService orderService; + + @RequestMapping("/createWrongOrder/{sid}") + @ResponseBody + public String createWrongOrder(@PathVariable int sid) { + logger.info("sid=[{}]", sid); + int id = 0; + try { + id = orderService.createWrongOrder(sid); + } catch (Exception e) { + logger.error("Exception",e); + } + return String.valueOf(id); + } +``` + +其中 web 作为一个消费者调用看 `OrderService` 提供出来的 dubbo 服务。 + +Service 层,`OrderService` 实现: + +首先是对 API 的实现(会在 API 提供出接口): + +```java +@Service +public class OrderServiceImpl implements OrderService { + + @Resource(name = "DBOrderService") + private com.crossoverJie.seconds.kill.service.OrderService orderService ; + + @Override + public int createWrongOrder(int sid) throws Exception { + return orderService.createWrongOrder(sid); + } +} +``` + +这里只是简单调用了 `DBOrderService` 中的实现,DBOrderService 才是真正的数据落地,也就是写数据库了。 + +DBOrderService 实现: + +```java +Transactional(rollbackFor = Exception.class) +@Service(value = "DBOrderService") +public class OrderServiceImpl implements OrderService { + @Resource(name = "DBStockService") + private com.crossoverJie.seconds.kill.service.StockService stockService; + + @Autowired + private StockOrderMapper orderMapper; + + @Override + public int createWrongOrder(int sid) throws Exception{ + + //校验库存 + Stock stock = checkStock(sid); + + //扣库存 + saleStock(stock); + + //创建订单 + int id = createOrder(stock); + + return id; + } + + private Stock checkStock(int sid) { + Stock stock = stockService.getStockById(sid); + if (stock.getSale().equals(stock.getCount())) { + throw new RuntimeException("库存不足"); + } + return stock; + } + + private int saleStock(Stock stock) { + stock.setSale(stock.getSale() + 1); + return stockService.updateStockById(stock); + } + + private int createOrder(Stock stock) { + StockOrder order = new StockOrder(); + order.setSid(stock.getId()); + order.setName(stock.getName()); + int id = orderMapper.insertSelective(order); + return id; + } + +} +``` + +> 预先初始化了 10 条库存。 + + +手动调用下 `createWrongOrder/1` 接口发现: + +库存表: +![](https://ws3.sinaimg.cn/large/006tKfTcly1fr38x4wqhcj30g404ajrg.jpg) + +订单表: +![](https://ws1.sinaimg.cn/large/006tKfTcly1fr38xpcdn7j30f0040glq.jpg) + +一切看起来都没有问题,数据也正常。 + +但是当用 `JMeter` 并发测试时: + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr391hontsj31ge0b8dgt.jpg) + +测试配置是:300个线程并发,测试两轮来看看数据库中的结果: + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr393xxc0rj31ge0463z6.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3939yo1bj30c4062t8s.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr393pxvf3j30j60d60v4.jpg) + +请求都响应成功,库存确实也扣完了,但是订单却生成了 **124** 条记录。 + +这显然是典型的超卖现象。 + +> 其实现在再去手动调用接口会返回库存不足,但为时晚矣。 + + +## 乐观锁更新 + +怎么来避免上述的现象呢? + +最简单的做法自然是乐观锁了,这里不过多讨论这个,不熟悉的朋友可以看下[这篇](http://crossoverjie.top/%2F2017%2F07%2F09%2FSSM15%2F)。 + +来看看具体实现: + +> 其实其他的都没怎么改,主要是 Service 层。 + +```java + @Override + public int createOptimisticOrder(int sid) throws Exception { + + //校验库存 + Stock stock = checkStock(sid); + + //乐观锁更新库存 + saleStockOptimistic(stock); + + //创建订单 + int id = createOrder(stock); + + return id; + } + + private void saleStockOptimistic(Stock stock) { + int count = stockService.updateStockByOptimistic(stock); + if (count == 0){ + throw new RuntimeException("并发更新库存失败") ; + } + } +``` + +对应的 XML: + +```xml + + update stock + + sale = sale + 1, + version = version + 1, + + + WHERE id = #{id,jdbcType=INTEGER} + AND version = #{version,jdbcType=INTEGER} + + +``` + +同样的测试条件,我们再进行上面的测试 `/createOptimisticOrder/1`: + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr39fxn691j31g603adgg.jpg) + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39dlobs1j30ca042wej.jpg) + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39dwfmrzj30f60gqgn7.jpg) + +这次发现无论是库存订单都是 OK 的。 + +查看日志发现: + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39hxcbsgj31kw0jhu0y.jpg) + +很多并发请求会响应错误,这就达到了效果。 + +### 提高吞吐量 + +为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 Service 都进行了横向扩展。 + +- web 利用 Nginx 进行负载。 +- Service 也是多台应用。 + +![](https://ws3.sinaimg.cn/large/006tKfTcly1fr39lm8iyjj31kw0ad784.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr39lvxnunj31kw0adaeh.jpg) + +再用 JMeter 测试时可以直观的看到效果。 + +> 由于我是在阿里云的一台小水管服务器进行测试的,加上配置不高、应用都在同一台,所以并没有完全体现出性能上的优势( `Nginx` 做负载转发时候也会增加额外的网络消耗)。 + +### shell 脚本实现简单的 CI + +由于应用多台部署之后,手动发版测试的痛苦相信经历过的都有体会。 + +这次并没有精力去搭建完整的 CI CD,只是写了一个简单的脚本实现了自动化部署,希望对这方面没有经验的同学带来一点启发: + +#### 构建 web + +```shell +#!/bin/bash + +# 构建 web 消费者 + +#read appname + +appname="consumer" +echo "input="$appname + +PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}') + +# 遍历杀掉 pid +for var in ${PID[@]}; +do + echo "loop pid= $var" + kill -9 $var +done + +echo "kill $appname success" + +cd .. + +git pull + +cd SSM-SECONDS-KILL + +mvn -Dmaven.test.skip=true clean package + +echo "build war success" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/webapps +echo "cp tomcat-dubbo-consumer-8083/webapps ok!" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/webapps +echo "cp tomcat-dubbo-consumer-7083-slave/webapps ok!" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.sh +echo "tomcat-dubbo-consumer-8083/bin/startup.sh success" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.sh +echo "tomcat-dubbo-consumer-7083-slave/bin/startup.sh success" + +echo "start $appname success" +``` + +#### 构建 Service + +```shell +# 构建服务提供者 + +#read appname + +appname="provider" + +echo "input="$appname + + +PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}') + +#if [ $? -eq 0 ]; then +# echo "process id:$PID" +#else +# echo "process $appname not exit" +# exit +#fi + +# 遍历杀掉 pid +for var in ${PID[@]}; +do + echo "loop pid= $var" + kill -9 $var +done + +echo "kill $appname success" + + +cd .. + +git pull + +cd SSM-SECONDS-KILL + +mvn -Dmaven.test.skip=true clean package + +echo "build war success" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/webapps + +echo "cp tomcat-dubbo-provider-8080/webapps ok!" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/webapps + +echo "cp tomcat-dubbo-provider-7080-slave/webapps ok!" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.sh +echo "tomcat-dubbo-provider-8080/bin/startup.sh success" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.sh +echo "tomcat-dubbo-provider-8080/bin/startup.sh success" + +echo "start $appname success" +``` + +之后每当我有更新,只需要执行这两个脚本就可以帮我自动构建。 + +都是最基础的 Linux 命令,相信大家都看得明白。 + + +## 乐观锁更新 + 分布式限流 + +上文的结果看似没有问题,其实还差得远呢。 + +这里只是模拟了 300 个并发没有问题,但是当请求达到了 3000 ,3W,300W 呢? + +虽说可以横向扩展可以支撑更多的请求。 + +但是能不能利用最少的资源解决问题呢? + +其实仔细分析下会发现: + +> 假设我的商品一共只有 10 个库存,那么无论你多少人来买其实最终也最多只有 10 人可以下单成功。 + +所以其中会有 `99%` 的请求都是无效的。 + +大家都知道:大多数应用数据库都是压倒骆驼的最后一根稻草。 + +通过 `Druid` 的监控来看看之前请求数据库的情况: + +因为 Service 是两个应用。 +![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3a1zpp5lj31kw0h277s.jpg) + +![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3a2c0vvdj31kw0g4n0m.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3a3xwslqj319g10cthl.jpg) + +数据库也有 20 多个连接。 + +怎么样来优化呢? +其实很容易想到的就是[分布式限流](http://crossoverjie.top/2018/04/28/sbc/sbc7-Distributed-Limit/)。 + + +我们将并发控制在一个可控的范围之内,然后快速失败这样就能最大程度的保护系统。 + +### distributed-redis-tool ⬆️v1.0.3 + +为此还对 [https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) 进行了小小的升级。 + +因为加上该组件之后所有的请求都会经过 Redis,所以对 Redis 资源的使用也是要非常小心。 + +#### API 更新 + +修改之后的 API 如下: + +```java +@Configuration +public class RedisLimitConfig { + + private Logger logger = LoggerFactory.getLogger(RedisLimitConfig.class); + + @Value("${redis.limit}") + private int limit; + + + @Autowired + private JedisConnectionFactory jedisConnectionFactory; + + @Bean + public RedisLimit build() { + RedisLimit redisLimit = new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE) + .limit(limit) + .build(); + + return redisLimit; + } +} +``` + +这里构建器改用了 `JedisConnectionFactory`,所以得配合 Spring 来一起使用。 + +并在初始化时显示传入 Redis 是以集群方式部署还是单机(强烈建议集群,限流之后对 Redis 还是有一定的压力)。 + +##### 限流实现 + +既然 API 更新了,实现自然也要修改: + +```java + /** + * limit traffic + * @return if true + */ + public boolean limit() { + + //get connection + Object connection = getConnection(); + + Object result = limitRequest(connection); + + if (FAIL_CODE != (Long) result) { + return true; + } else { + return false; + } + } + + private Object limitRequest(Object connection) { + Object result = null; + String key = String.valueOf(System.currentTimeMillis() / 1000); + if (connection instanceof Jedis){ + result = ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + ((Jedis) connection).close(); + }else { + result = ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + try { + ((JedisCluster) connection).close(); + } catch (IOException e) { + logger.error("IOException",e); + } + } + return result; + } + + private Object getConnection() { + Object connection ; + if (type == RedisToolsConstant.SINGLE){ + RedisConnection redisConnection = jedisConnectionFactory.getConnection(); + connection = redisConnection.getNativeConnection(); + }else { + RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection(); + connection = clusterConnection.getNativeConnection() ; + } + return connection; + } +``` + +如果是原生的 Spring 应用得采用 `@SpringControllerLimit(errorCode = 200)` 注解。 + +实际使用如下: + +web 端: + +```java + /** + * 乐观锁更新库存 限流 + * @param sid + * @return + */ + @SpringControllerLimit(errorCode = 200) + @RequestMapping("/createOptimisticLimitOrder/{sid}") + @ResponseBody + public String createOptimisticLimitOrder(@PathVariable int sid) { + logger.info("sid=[{}]", sid); + int id = 0; + try { + id = orderService.createOptimisticOrder(sid); + } catch (Exception e) { + logger.error("Exception",e); + } + return String.valueOf(id); + } +``` + +Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 + +再压测看下效果 `/createOptimisticLimitOrderByRedis/1`: + +![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3amu17zuj30e603ewej.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3an1x3pqj30oy0fwq4p.jpg) + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr3aml0c8rj31ek0ssn3g.jpg) + +![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3ank9otcj31kw0d4die.jpg) + +![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3anxbb0hj31kw0cjtbb.jpg) + +首先是看结果没有问题,再看数据库连接以及并发请求数都有**明显的下降**。 + + +## 乐观锁更新 + 分布式限流 + Redis 缓存 + +其实仔细观察 Druid 监控数据发现这个 SQL 被多次查询: + +![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3aq7shudj31kw0bomzp.jpg) + +其实这是实时查询库存的 SQL,主要是为了在每次下单之前判断是否还有库存。 + +**这也是个优化点**。 + +这种数据我们完全可以放在内存中,效率比在数据库要高很多。 + +由于我们的应用是分布式的,所以堆内缓存显然不合适,Redis 就非常适合。 + +这次主要改造的是 Service 层: + +- 每次查询库存时走 Redis。 +- 扣库存时更新 Redis。 +- 需要提前将库存信息写入 Redis(手动或者程序自动都可以)。 + +主要代码如下: + +```java + @Override + public int createOptimisticOrderUseRedis(int sid) throws Exception { + //检验库存,从 Redis 获取 + Stock stock = checkStockByRedis(sid); + + //乐观锁更新库存 以及更新 Redis + saleStockOptimisticByRedis(stock); + + //创建订单 + int id = createOrder(stock); + return id ; + } + + + private Stock checkStockByRedis(int sid) throws Exception { + Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid)); + Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid)); + if (count.equals(sale)){ + throw new RuntimeException("库存不足 Redis currentCount=" + sale); + } + Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid)); + Stock stock = new Stock() ; + stock.setId(sid); + stock.setCount(count); + stock.setSale(sale); + stock.setVersion(version); + + return stock; + } + + + /** + * 乐观锁更新数据库 还要更新 Redis + * @param stock + */ + private void saleStockOptimisticByRedis(Stock stock) { + int count = stockService.updateStockByOptimistic(stock); + if (count == 0){ + throw new RuntimeException("并发更新库存失败") ; + } + //自增 + redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ; + redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ; + } +``` + +压测看看实际效果 `/createOptimisticLimitOrderByRedis/1`: + +![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3b419f2aj30by04g0ss.jpg) + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fr3b48vebkj30gk0cy0u3.jpg) + +![](https://ws2.sinaimg.cn/large/006tKfTcgy1fr3b55kyv6j31kw0dijtx.jpg) + +![](https://ws3.sinaimg.cn/large/006tKfTcgy1fr3b5n1n21j31kw0c2acg.jpg) + +最后发现数据没问题,数据库的请求与并发也都下来了。 + + + +## 乐观锁更新 + 分布式限流 + Redis 缓存 + Kafka 异步 + +最后的优化还是想如何来再次提高吞吐量以及性能的。 + +我们上文所有例子其实都是同步请求,完全可以利用同步转异步来提高性能啊。 + +这里我们将写订单以及更新库存的操作进行异步化,利用 `Kafka` 来进行解耦和队列的作用。 + +每当一个请求通过了限流到达了 Service 层通过了库存校验之后就将订单信息发给 Kafka ,这样一个请求就可以直接返回了。 + +消费程序再对数据进行入库落地。 + +因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。 + +这里代码较多就不贴了,消费程序其实就是把之前的 Service 层的逻辑重写了一遍,不过采用的是 SpringBoot。 + +感兴趣的朋友可以看下。 + +[https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER](https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER) + + + + +## 总结 + +其实经过上面的一顿优化总结起来无非就是以下几点: + +- 尽量将请求拦截在上游。 +- 还可以根据 UID 进行限流。 +- 最大程度的减少请求落到 DB。 +- 多利用缓存。 +- 同步操作异步化。 +- fail fast,尽早失败,保护应用。 + +码字不易,这应该是我写过字数最多的了,想想当年高中 800 字的作文都憋不出来😂,可想而知是有多难得了。 + +**以上内容欢迎讨论**。 + +### 号外 +最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。 + +> 地址: [https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) \ No newline at end of file diff --git a/README.md b/README.md index d50960c7..9e8df602 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,133 @@ -[![Build Status](https://travis-ci.org/crossoverJie/Java-Interview.svg?branch=master)](https://travis-ci.org/crossoverJie/Java-Interview) + +
+ + +
+ +[![Build Status](https://travis-ci.org/crossoverJie/JCSprout.svg?branch=master)](https://travis-ci.org/crossoverJie/JCSprout) [![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5HPYvQk) [qq0groupsvg]: https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg [qq0group]: https://jq.qq.com/?_wv=1027&k=5HPYvQk -Java 知识点,继续完善中。 +

+ + +> `Java Core Sprout`:处于萌芽阶段的 Java 核心知识库。 + +**访问这里获取更好的阅读体验**:[https://crossoverjie.top/JCSprout/](https://crossoverjie.top/JCSprout/) + +
+ +
-> 多数是一些 Java 基础知识、底层原理、算法详解。也有上层应用设计,其中不乏一些大厂面试真题。 +202405171520366.png +
+最近开通了知识星球,感谢大家对 `JCSprout` 的支持,为大家提供 100 份 10 元优惠券,也就是 69-10=59 元,具体福利大家可以扫码参考再决定是否加入。 -如果对你有帮助请点下 `Star`,有疑问欢迎提 [Issues](https://github.com/crossoverJie/Java-Interview/issues),有好的想法请提 [PR](https://github.com/crossoverJie/Java-Interview/pulls)。 +> PS: 后续会继续维护该项目,同时加入现在热门的 Golang/kubernetes/OpenTelemetry 等知识点,感兴趣的可以加入星球当面催更(当然内容也会更新到这个项目里)。 -[常用集合](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E5%B8%B8%E7%94%A8%E9%9B%86%E5%90%88) | [Java 多线程](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#java-%E5%A4%9A%E7%BA%BF%E7%A8%8B) | [JVM](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#jvm) | [分布式相关](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3) |[常用框架\第三方组件](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E5%B8%B8%E7%94%A8%E6%A1%86%E6%9E%B6%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6)|[架构设计](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1)|[DB 相关](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#db-%E7%9B%B8%E5%85%B3)|[数据结构与算法](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95)|[Netty 相关](https://github.com/crossoverJie/Java-Interview#netty-%E7%9B%B8%E5%85%B3)|[附加技能](https://github.com/crossoverJie/Java-Interview/blob/master/README.md#%E9%99%84%E5%8A%A0%E6%8A%80%E8%83%BD)|[联系作者](https://github.com/crossoverJie/Java-Interview#%E8%81%94%E7%B3%BB%E4%BD%9C%E8%80%85) ----- | --- | --- | ---| ---| ---| ---| ---| ---|---|--- +| 📊 |⚔️ | 🖥 | 🚏 | 🏖 | 🌁| 📮 | 🔍 | 🚀 | 🌈 |💡 +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|:------:| +| [集合](#常用集合) | [多线程](#java-多线程)|[JVM](#jvm) | [分布式](#分布式相关) |[框架](#常用框架第三方组件)|[架构设计](#架构设计)| [数据库](#db-相关) |[算法](#数据结构与算法)|[Netty](#netty-相关)| [附加技能](#附加技能)|[联系作者](#联系作者) | ### 常用集合 -- [ArrayList/Vector](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ArrayList.md) -- [LinkedList](https://github.com/crossoverJie/Java-Interview/blob/master/MD/LinkedList.md) -- [HashMap](https://github.com/crossoverJie/Java-Interview/blob/master/MD/HashMap.md) -- [HashSet](https://github.com/crossoverJie/Java-Interview/blob/master/MD/collection/HashSet.md) -- [LinkedHashMap](https://github.com/crossoverJie/Java-Interview/blob/master/MD/collection/LinkedHashMap.md) +- [ArrayList/Vector](https://github.com/crossoverJie/JCSprout/blob/master/MD/ArrayList.md) +- [LinkedList](https://github.com/crossoverJie/JCSprout/blob/master/MD/LinkedList.md) +- [HashMap](https://github.com/crossoverJie/JCSprout/blob/master/MD/HashMap.md) +- [HashSet](https://github.com/crossoverJie/JCSprout/blob/master/MD/collection/HashSet.md) +- [LinkedHashMap](https://github.com/crossoverJie/JCSprout/blob/master/MD/collection/LinkedHashMap.md) ### Java 多线程 -- [多线程中的常见问题](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Thread-common-problem.md) -- [synchronized 关键字原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Synchronize.md) -- [多线程的三大核心](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md) -- [对锁的一些认知](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Java-lock.md) -- [ReentrantLock 实现原理 ](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ReentrantLock.md) -- [ConcurrentHashMap 的实现原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ConcurrentHashMap.md) -- [线程池原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ThreadPoolExecutor.md) -- [深入理解线程通信](https://github.com/crossoverJie/Java-Interview/blob/master/MD/concurrent/thread-communication.md) -- [交替打印奇偶数](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/TwoThread.java) +- [多线程中的常见问题](https://github.com/crossoverJie/JCSprout/blob/master/MD/Thread-common-problem.md) +- [synchronized 关键字原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/Synchronize.md) +- [多线程的三大核心](https://github.com/crossoverJie/JCSprout/blob/master/MD/Threadcore.md) +- [对锁的一些认知](https://github.com/crossoverJie/JCSprout/blob/master/MD/Java-lock.md) +- [ReentrantLock 实现原理 ](https://github.com/crossoverJie/JCSprout/blob/master/MD/ReentrantLock.md) +- [ConcurrentHashMap 的实现原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/ConcurrentHashMap.md) +- [如何优雅的使用和理解线程池](https://github.com/crossoverJie/JCSprout/blob/master/MD/ThreadPoolExecutor.md) +- [深入理解线程通信](https://github.com/crossoverJie/JCSprout/blob/master/MD/concurrent/thread-communication.md) +- [一个线程罢工的诡异事件](docs/thread/thread-gone.md) +- [线程池中你不容错过的一些细节](docs/thread/thread-gone2.md) +- [『并发包入坑指北』之阻塞队列](docs/thread/ArrayBlockingQueue.md) ### JVM -- [Java 运行时内存划分](https://github.com/crossoverJie/Java-Interview/blob/master/MD/MemoryAllocation.md) -- [类加载机制](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ClassLoad.md) -- [OOM 分析](https://github.com/crossoverJie/Java-Interview/blob/master/MD/OOM-analysis.md) -- [垃圾回收](https://github.com/crossoverJie/Java-Interview/blob/master/MD/GarbageCollection.md) -- [对象的创建与内存分配](https://github.com/crossoverJie/Java-Interview/blob/master/MD/newObject.md) -- [你应该知道的 volatile 关键字](https://github.com/crossoverJie/Java-Interview/blob/master/MD/concurrent/volatile.md) +- [Java 运行时内存划分](https://github.com/crossoverJie/JCSprout/blob/master/MD/MemoryAllocation.md) +- [类加载机制](https://github.com/crossoverJie/JCSprout/blob/master/MD/ClassLoad.md) +- [OOM 分析](https://github.com/crossoverJie/JCSprout/blob/master/MD/OOM-analysis.md) +- [垃圾回收](https://github.com/crossoverJie/JCSprout/blob/master/MD/GarbageCollection.md) +- [对象的创建与内存分配](https://github.com/crossoverJie/JCSprout/blob/master/MD/newObject.md) +- [你应该知道的 volatile 关键字](https://github.com/crossoverJie/JCSprout/blob/master/MD/concurrent/volatile.md) +- [一次内存溢出排查优化实战](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) +- [一次 HashSet 所引起的并发问题](docs/jvm/JVM-concurrent-HashSet-problem.md) +- [一次生产 CPU 100% 排查优化实践](docs/jvm/cpu-percent-100.md) ### 分布式相关 - [分布式限流](http://crossoverjie.top/2018/04/28/sbc/sbc7-Distributed-Limit/) - [基于 Redis 的分布式锁](http://crossoverjie.top/2018/03/29/distributed-lock/distributed-lock-redis/) -- [分布式缓存设计](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Cache-design.md) -- [分布式 ID 生成器](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ID-generator.md) +- [分布式缓存设计](https://github.com/crossoverJie/JCSprout/blob/master/MD/Cache-design.md) +- [分布式 ID 生成器](https://github.com/crossoverJie/JCSprout/blob/master/MD/ID-generator.md) ### 常用框架\第三方组件 -- [Spring Bean 生命周期](https://github.com/crossoverJie/Java-Interview/blob/master/MD/spring/spring-bean-lifecycle.md) -- [Spring AOP 的实现原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/SpringAOP.md) +- [Spring Bean 生命周期](https://github.com/crossoverJie/JCSprout/blob/master/MD/spring/spring-bean-lifecycle.md) +- [Spring AOP 的实现原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/SpringAOP.md) - [Guava 源码分析(Cache 原理)](https://crossoverjie.top/2018/06/13/guava/guava-cache/) -- SpringBoot 启动过程 -- Tomcat 类加载机制 +- [轻量级 HTTP 框架](https://github.com/crossoverJie/cicada) +- [Kafka produce 源码分析](https://github.com/crossoverJie/JCSprout/blob/master/MD/kafka/kafka-product.md) +- [Kafka 消费实践](https://github.com/crossoverJie/JCSprout/blob/master/docs/frame/kafka-consumer.md) ### 架构设计 -- [秒杀系统设计](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Spike.md) +- [秒杀系统设计](https://github.com/crossoverJie/JCSprout/blob/master/MD/Spike.md) - [秒杀架构实践](http://crossoverjie.top/2018/05/07/ssm/SSM18-seconds-kill/) +- [设计一个百万级的消息推送系统](https://github.com/crossoverJie/JCSprout/blob/master/MD/architecture-design/million-sms-push.md) ### DB 相关 -- [MySQL 索引原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/MySQL-Index.md) -- [SQL 优化](https://github.com/crossoverJie/Java-Interview/blob/master/MD/SQL-optimization.md) -- [数据库水平垂直拆分](https://github.com/crossoverJie/Java-Interview/blob/master/MD/DB-split.md) +- [MySQL 索引原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/MySQL-Index.md) +- [SQL 优化](https://github.com/crossoverJie/JCSprout/blob/master/MD/SQL-optimization.md) +- [数据库水平垂直拆分](https://github.com/crossoverJie/JCSprout/blob/master/MD/DB-split.md) +- [一次分表踩坑实践的探讨](docs/db/sharding-db.md) ### 数据结构与算法 -- [红包算法](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/red/RedPacket.java) -- [二叉树中序遍历](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/BinaryNode.java#L76-L101) -- [是否为快乐数字](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/HappyNum.java#L38-L55) -- [链表是否有环](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/LinkLoop.java#L32-L59) -- [从一个数组中返回两个值相加等于目标值的下标](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/TwoSum.java#L38-L59) -- [一致性 Hash 算法](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Consistent-Hash.md) -- [限流算法](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Limiting.md) -- [三种方式反向打印单向链表](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/ReverseNode.java) -- [合并两个排好序的链表](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/MergeTwoSortedLists.java) -- [两个栈实现队列](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/algorithm/TwoStackQueue.java) +- [红包算法](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/red/RedPacket.java) +- [二叉树层序遍历](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/BinaryNode.java#L76-L101) +- [是否为快乐数字](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/HappyNum.java#L38-L55) +- [链表是否有环](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/LinkLoop.java#L32-L59) +- [从一个数组中返回两个值相加等于目标值的下标](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/TwoSum.java#L38-L59) +- [一致性 Hash 算法原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/Consistent-Hash.md) +- [一致性 Hash 算法实践](https://github.com/crossoverJie/JCSprout/blob/master/docs/algorithm/consistent-hash-implement.md) +- [限流算法](https://github.com/crossoverJie/JCSprout/blob/master/MD/Limiting.md) +- [三种方式反向打印单向链表](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/ReverseNode.java) +- [合并两个排好序的链表](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/MergeTwoSortedLists.java) +- [两个栈实现队列](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/TwoStackQueue.java) - [动手实现一个 LRU cache](http://crossoverjie.top/2018/04/07/algorithm/LRU-cache/) - [链表排序](./src/main/java/com/crossoverjie/algorithm/LinkedListMergeSort.java) - [数组右移 k 次](./src/main/java/com/crossoverjie/algorithm/ArrayKShift.java) +- [交替打印奇偶数](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/actual/TwoThread.java) +- [亿级数据中判断数据是否不存在](https://github.com/crossoverJie/JCSprout/blob/master/docs/algorithm/guava-bloom-filter.md) ### Netty 相关 - [SpringBoot 整合长连接心跳机制](https://crossoverjie.top/2018/05/24/netty/Netty(1)TCP-Heartbeat/) - [从线程模型的角度看 Netty 为什么是高性能的?](https://crossoverjie.top/2018/07/04/netty/Netty(2)Thread-model/) +- [为自己搭建一个分布式 IM(即时通讯) 系统](https://github.com/crossoverJie/cim) ### 附加技能 -- [TCP/IP 协议](https://github.com/crossoverJie/Java-Interview/blob/master/MD/TCP-IP.md) +- [TCP/IP 协议](https://github.com/crossoverJie/JCSprout/blob/master/MD/TCP-IP.md) - [一个学渣的阿里之路](https://crossoverjie.top/2018/06/21/personal/Interview-experience/) +- [如何成为一位「不那么差」的程序员](https://crossoverjie.top/2018/08/12/personal/how-to-be-developer/) +- [如何高效的使用 Git](https://github.com/crossoverJie/JCSprout/blob/master/MD/additional-skills/how-to-use-git-efficiently.md) ### 联系作者 > crossoverJie#gmail.com -![](https://ws2.sinaimg.cn/large/006tKfTcly1fsa01u7ro1j30gs0howfq.jpg) \ No newline at end of file +![index.jpg](https://i.loli.net/2021/10/12/ckQW9LYXSxFogJZ.jpg) diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..6cf49e92 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,72 @@ +# Introduction + +
+ + +[![Build Status](https://travis-ci.org/crossoverJie/JCSprout.svg?branch=master)](https://travis-ci.org/crossoverJie/JCSprout) +[![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5HPYvQk) + +[qq0groupsvg]: https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg +[qq0group]: https://jq.qq.com/?_wv=1027&k=5HPYvQk + + +
+ + +`Java Core Sprout`:处于萌芽阶段的 `Java` 核心知识库。 + +
+ +
+ + +---------- + + +# CONTACT + +
+ +
+ +202405171520366.png +
+ +最近开通了知识星球,感谢大家对 `JCSprout` 的支持,为大家提供 100 份 10 元优惠券,也就是 69-10=59 元,具体福利大家可以扫码参考再决定是否加入。 + +> PS: 后续会继续维护该项目,同时加入现在热门的 Golang/kubernetes/OpenTelemetry 等知识点,感兴趣的可以加入星球当面催更(当然内容也会更新到这个项目里)。 + +[个人博客](https://crossoverjie.top) + +[Twitter](https://twitter.com/crossoverJie) + +[微博](http://weibo.com/crossoverJie "微博") + +[GitHub](https://github.com/crossoverJie "github") + +[crossoverJie@gmail.com](mailto:crossoverjie@gmail.com) + + +--- + + + + +--- + + +--- + + + +--- + + +**欢迎我的关注公众号一起交流:** + +![](https://crossoverjie.top/uploads/weixinfooter1.jpg) + +
+ + + diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 00000000..0c71d9e8 --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,9 @@ + + + + + +> `Java Core Sprout`:处于萌芽阶段的 Java 核心知识库。 + +[GitHub](https://github.com/crossoverJie/JCSprout) +[Get Started](#introduction) \ No newline at end of file diff --git a/docs/_media/icon-above-font.png b/docs/_media/icon-above-font.png new file mode 100644 index 00000000..cb9b2535 Binary files /dev/null and b/docs/_media/icon-above-font.png differ diff --git a/docs/_media/icon-left-font-monochrome-black.png b/docs/_media/icon-left-font-monochrome-black.png new file mode 100644 index 00000000..a754b12d Binary files /dev/null and b/docs/_media/icon-left-font-monochrome-black.png differ diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 00000000..50f96c93 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,88 @@ +- 集合 + + - [ArrayList/Vector](collections/ArrayList.md) + - [LinkedList](collections/LinkedList.md) + - [HashMap](collections/HashMap.md) + - [HashSet](collections/HashSet.md) + - [LinkedHashMap](collections/LinkedHashMap.md) + +- Java 多线程 + + - [多线程中的常见问题](thread/Thread-common-problem.md) + - [synchronized 关键字原理](thread/Synchronize.md) + - [多线程的三大核心](thread/Threadcore.md) + - [对锁的一些认知](thread/Java-lock.md) + - [ReentrantLock 实现原理 ](thread/ReentrantLock.md) + - [ConcurrentHashMap 的实现原理](thread/ConcurrentHashMap.md) + - [如何优雅的使用和理解线程池](thread/ThreadPoolExecutor.md) + - [深入理解线程通信](thread/thread-communication.md) + - [一个线程罢工的诡异事件](thread/thread-gone.md) + - [线程池中你不容错过的一些细节](thread/thread-gone2.md) + - [『并发包入坑指北』之阻塞队列](thread/ArrayBlockingQueue.md) + +- JVM + + - [Java 运行时内存划分](jvm/MemoryAllocation.md) + - [类加载机制](jvm/ClassLoad.md) + - [OOM 分析](jvm/OOM-analysis.md) + - [垃圾回收](jvm/GarbageCollection.md) + - [对象的创建与内存分配](jvm/newObject.md) + - [你应该知道的 volatile 关键字](jvm/volatile.md) + - [一次内存溢出排查优化实战](jvm/OOM-Disruptor.md) + - [一次 HashSet 所引起的并发问题](jvm/JVM-concurrent-HashSet-problem.md) + - [一次生产 CPU 100% 排查优化实践](jvm/cpu-percent-100.md) + +- 分布式 + + - [分布式限流](distributed/Distributed-Limit.md) + - [基于 Redis 的分布式锁](distributed/distributed-lock-redis.md) + - [分布式缓存设计](distributed/Cache-design.md) + - [分布式 ID 生成器](distributed/ID-generator.md) + +- 常用框架 + + - [Spring Bean 生命周期](frame/spring-bean-lifecycle.md) + - [Spring AOP 的实现原理](frame/SpringAOP.md) + - [Guava 源码分析(Cache 原理)](frame/guava-cache.md) + - [Kafka produce 源码分析](frame/kafka-product.md) + - [Kafka 消费实践](frame/kafka-consumer.md) + + +- 架构设计 + + - [秒杀系统设计](architecture-design/Spike.md) + - [秒杀架构实践](architecture-design/seconds-kill.md) + - [设计一个百万级的消息推送系统](architecture-design/million-sms-push.md) + +- 数据库 + + - [MySQL 索引原理](db/MySQL-Index.md) + - [SQL 优化](db/SQL-optimization.md) + - [数据库水平垂直拆分](db/DB-split.md) + - [一次分表踩坑实践的探讨](db/sharding-db.md) + +- 数据结构与算法 + + - [常见算法](algorithm/common-algorithm.md) + - [一致性 Hash 算法原理](algorithm/Consistent-Hash.md) + - [一致性 Hash 算法实践](algorithm/consistent-hash-implement.md) + - [限流算法](algorithm/Limiting.md) + - [动手实现一个 LRU cache](algorithm/LRU-cache.md) + - [亿级数据中判断数据是否不存在](algorithm/guava-bloom-filter.md) + + +- Netty 相关 + + - [SpringBoot 整合长连接心跳机制](netty/Netty(1)TCP-Heartbeat.md) + - [从线程模型的角度看 Netty 为什么是高性能的?](netty/Netty(2)Thread-model.md) + - [自己实现一个轻量级 HTTP 框架](netty/cicada.md) + - [为自己搭建一个分布式 IM(即时通讯) 系统](netty/cim.md) + +- 附加技能 + + - [TCP/IP 协议](soft-skills/TCP-IP.md) + - [一个学渣的阿里之路](soft-skills/Interview-experience.md) + - [如何成为一位「不那么差」的程序员](soft-skills/how-to-be-developer.md) + - [如何高效的使用 Git](soft-skills/how-to-use-git-efficiently.md) + +- [联系作者](contactme.md) diff --git a/docs/algorithm/Consistent-Hash.md b/docs/algorithm/Consistent-Hash.md new file mode 100644 index 00000000..83c433f3 --- /dev/null +++ b/docs/algorithm/Consistent-Hash.md @@ -0,0 +1,62 @@ +# 一致 Hash 算法 + +当我们在做数据库分库分表或者是分布式缓存时,不可避免的都会遇到一个问题: + +如何将数据均匀的分散到各个节点中,并且尽量的在加减节点时能使受影响的数据最少。 + +## Hash 取模 +随机放置就不说了,会带来很多问题。通常最容易想到的方案就是 `hash 取模`了。 + +可以将传入的 Key 按照 `index = hash(key) % N` 这样来计算出需要存放的节点。其中 hash 函数是一个将字符串转换为正整数的哈希映射方法,N 就是节点的数量。 + +这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性都较差。 + +比如增加或删除了一个节点时,所有的 Key 都需要重新计算,显然这样成本较高,为此需要一个算法满足分布均匀同时也要有良好的容错性和拓展性。 + +## 一致 Hash 算法 + +一致 Hash 算法是将所有的哈希值构成了一个环,其范围在 `0 ~ 2^32-1`。如下图: + +![](https://i.loli.net/2019/06/26/5d13931ace0d988790.jpg) + +之后将各个节点散列到这个环上,可以用节点的 IP、hostname 这样的唯一性字段作为 Key 进行 `hash(key)`,散列之后如下: + +![](https://i.loli.net/2019/06/26/5d13931b42d3941564.jpg) + +之后需要将数据定位到对应的节点上,使用同样的 `hash 函数` 将 Key 也映射到这个环上。 + +![](https://i.loli.net/2019/06/26/5d13931b811c782755.jpg) + +这样按照顺时针方向就可以把 k1 定位到 `N1节点`,k2 定位到 `N3节点`,k3 定位到 `N2节点`。 + +### 容错性 +这时假设 N1 宕机了: + +![](https://i.loli.net/2019/06/26/5d13931ba4a0869451.jpg) + +依然根据顺时针方向,k2 和 k3 保持不变,只有 k1 被重新映射到了 N3。这样就很好的保证了容错性,当一个节点宕机时只会影响到少少部分的数据。 + +### 拓展性 + +当新增一个节点时: + +![](https://i.loli.net/2019/06/26/5d13931bc818391034.jpg) + +在 N2 和 N3 之间新增了一个节点 N4 ,这时会发现受印象的数据只有 k3,其余数据也是保持不变,所以这样也很好的保证了拓展性。 + +## 虚拟节点 +到目前为止该算法依然也有点问题: + +当节点较少时会出现数据分布不均匀的情况: + +![](https://i.loli.net/2019/06/26/5d13931c0392a99489.jpg) + +这样会导致大部分数据都在 N1 节点,只有少量的数据在 N2 节点。 + +为了解决这个问题,一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点: + +![](https://i.loli.net/2019/06/26/5d13931c3e2f146589.jpg) + +计算时可以在 IP 后加上编号来生成哈希值。 + +这样只需要在原有的基础上多一步由虚拟节点映射到实际节点的步骤即可让少量节点也能满足均匀性。 diff --git a/docs/algorithm/LRU-cache.md b/docs/algorithm/LRU-cache.md new file mode 100644 index 00000000..ca512d10 --- /dev/null +++ b/docs/algorithm/LRU-cache.md @@ -0,0 +1,851 @@ +![](https://i.loli.net/2019/06/26/5d13931b1ef2443865.jpg) + +## 前言 +LRU 是 `Least Recently Used` 的简写,字面意思则是`最近最少使用`。 + +通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被撑满。 + +如常用的 Redis 就有以下几种策略: + +| 策略 | 描述 | +| :--: | :--: | +| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | +| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | +|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | +| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | +| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | +| no-envicition | 禁止驱逐数据 | + +> 摘抄自:[https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md#%E5%8D%81%E4%B8%89%E6%95%B0%E6%8D%AE%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md#%E5%8D%81%E4%B8%89%E6%95%B0%E6%8D%AE%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5) + + + + + +## 实现一 + +之前也有接触过一道面试题,大概需求是: + +- 实现一个 LRU 缓存,当缓存数据达到 N 之后需要淘汰掉最近最少使用的数据。 +- N 小时之内没有被访问的数据也需要淘汰掉。 + +以下是我的实现: + +```java +public class LRUAbstractMap extends java.util.AbstractMap { + + private final static Logger LOGGER = LoggerFactory.getLogger(LRUAbstractMap.class); + + /** + * 检查是否超期线程 + */ + private ExecutorService checkTimePool ; + + /** + * map 最大size + */ + private final static int MAX_SIZE = 1024 ; + + private final static ArrayBlockingQueue QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ; + + /** + * 默认大小 + */ + private final static int DEFAULT_ARRAY_SIZE =1024 ; + + + /** + * 数组长度 + */ + private int arraySize ; + + /** + * 数组 + */ + private Object[] arrays ; + + + /** + * 判断是否停止 flag + */ + private volatile boolean flag = true ; + + + /** + * 超时时间 + */ + private final static Long EXPIRE_TIME = 60 * 60 * 1000L ; + + /** + * 整个 Map 的大小 + */ + private volatile AtomicInteger size ; + + + public LRUAbstractMap() { + + + arraySize = DEFAULT_ARRAY_SIZE; + arrays = new Object[arraySize] ; + + //开启一个线程检查最先放入队列的值是否超期 + executeCheckTime(); + } + + /** + * 开启一个线程检查最先放入队列的值是否超期 设置为守护线程 + */ + private void executeCheckTime() { + ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() + .setNameFormat("check-thread-%d") + .setDaemon(true) + .build(); + checkTimePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy()); + checkTimePool.execute(new CheckTimeThread()) ; + + } + + @Override + public Set entrySet() { + return super.keySet(); + } + + @Override + public Object put(Object key, Object value) { + int hash = hash(key); + int index = hash % arraySize ; + Node currentNode = (Node) arrays[index] ; + + if (currentNode == null){ + arrays[index] = new Node(null,null, key, value); + + //写入队列 + QUEUE.offer((Node) arrays[index]) ; + + sizeUp(); + }else { + Node cNode = currentNode ; + Node nNode = cNode ; + + //存在就覆盖 + if (nNode.key == key){ + cNode.val = value ; + } + + while (nNode.next != null){ + //key 存在 就覆盖 简单判断 + if (nNode.key == key){ + nNode.val = value ; + break ; + }else { + //不存在就新增链表 + sizeUp(); + Node node = new Node(nNode,null,key,value) ; + + //写入队列 + QUEUE.offer(currentNode) ; + + cNode.next = node ; + } + + nNode = nNode.next ; + } + + } + + return null ; + } + + + @Override + public Object get(Object key) { + + int hash = hash(key) ; + int index = hash % arraySize ; + Node currentNode = (Node) arrays[index] ; + + if (currentNode == null){ + return null ; + } + if (currentNode.next == null){ + + //更新时间 + currentNode.setUpdateTime(System.currentTimeMillis()); + + //没有冲突 + return currentNode ; + + } + + Node nNode = currentNode ; + while (nNode.next != null){ + + if (nNode.key == key){ + + //更新时间 + currentNode.setUpdateTime(System.currentTimeMillis()); + + return nNode ; + } + + nNode = nNode.next ; + } + + return super.get(key); + } + + + @Override + public Object remove(Object key) { + + int hash = hash(key) ; + int index = hash % arraySize ; + Node currentNode = (Node) arrays[index] ; + + if (currentNode == null){ + return null ; + } + + if (currentNode.key == key){ + sizeDown(); + arrays[index] = null ; + + //移除队列 + QUEUE.poll(); + return currentNode ; + } + + Node nNode = currentNode ; + while (nNode.next != null){ + + if (nNode.key == key){ + sizeDown(); + //在链表中找到了 把上一个节点的 next 指向当前节点的下一个节点 + nNode.pre.next = nNode.next ; + nNode = null ; + + //移除队列 + QUEUE.poll(); + + return nNode; + } + + nNode = nNode.next ; + } + + return super.remove(key); + } + + /** + * 增加size + */ + private void sizeUp(){ + + //在put值时候认为里边已经有数据了 + flag = true ; + + if (size == null){ + size = new AtomicInteger() ; + } + int size = this.size.incrementAndGet(); + if (size >= MAX_SIZE) { + //找到队列头的数据 + Node node = QUEUE.poll() ; + if (node == null){ + throw new RuntimeException("data error") ; + } + + //移除该 key + Object key = node.key ; + remove(key) ; + lruCallback() ; + } + + } + + /** + * 数量减小 + */ + private void sizeDown(){ + + if (QUEUE.size() == 0){ + flag = false ; + } + + this.size.decrementAndGet() ; + } + + @Override + public int size() { + return size.get() ; + } + + /** + * 链表 + */ + private class Node{ + private Node next ; + private Node pre ; + private Object key ; + private Object val ; + private Long updateTime ; + + public Node(Node pre,Node next, Object key, Object val) { + this.pre = pre ; + this.next = next; + this.key = key; + this.val = val; + this.updateTime = System.currentTimeMillis() ; + } + + public void setUpdateTime(Long updateTime) { + this.updateTime = updateTime; + } + + public Long getUpdateTime() { + return updateTime; + } + + @Override + public String toString() { + return "Node{" + + "key=" + key + + ", val=" + val + + '}'; + } + } + + + /** + * copy HashMap 的 hash 实现 + * @param key + * @return + */ + public int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } + + private void lruCallback(){ + LOGGER.debug("lruCallback"); + } + + + private class CheckTimeThread implements Runnable{ + + @Override + public void run() { + while (flag){ + try { + Node node = QUEUE.poll(); + if (node == null){ + continue ; + } + Long updateTime = node.getUpdateTime() ; + + if ((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){ + remove(node.key) ; + } + } catch (Exception e) { + LOGGER.error("InterruptedException"); + } + } + } + } + +} +``` + +感兴趣的朋友可以直接从: + +[https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUAbstractMap.java](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUAbstractMap.java) + +下载代码本地运行。 + +代码看着比较多,其实实现的思路还是比较简单: + +- 采用了与 HashMap 一样的保存数据方式,只是自己手动实现了一个简易版。 +- 内部采用了一个队列来保存每次写入的数据。 +- 写入的时候判断缓存是否大于了阈值 N,如果满足则根据队列的 FIFO 特性将队列头的数据删除。因为队列头的数据肯定是最先放进去的。 +- 再开启了一个守护线程用于判断最先放进去的数据是否超期(因为就算超期也是最先放进去的数据最有可能满足超期条件。) +- 设置为守护线程可以更好的表明其目的(最坏的情况下,如果是一个用户线程最终有可能导致程序不能正常退出,因为该线程一直在运行,守护线程则不会有这个情况。) + +以上代码大体功能满足了,但是有一个致命问题。 + +就是最近**最少使用**没有满足,删除的数据都是最先放入的数据。 + +> 不过其中的 `put get` 流程算是一个简易的 HashMap 实现,可以对 HashMap 加深一些理解。 + + +## 实现二 + +因此如何来实现一个完整的 LRU 缓存呢,这次不考虑过期时间的问题。 + +其实从上一个实现也能想到一些思路: +- 要记录最近最少使用,那至少需要一个有序的集合来保证写入的顺序。 +- 在使用了数据之后能够更新它的顺序。 + +基于以上两点很容易想到一个常用的数据结构:**链表**。 + +1. 每次写入数据时将数据放入链表头结点。 +2. 使用数据时候将数据**移动到头结点**。 +3. 缓存数量超过阈值时移除链表尾部数据。 + +因此有了以下实现: + +```java +public class LRUMap { + private final Map cacheMap = new HashMap<>(); + + /** + * 最大缓存大小 + */ + private int cacheSize; + + /** + * 节点大小 + */ + private int nodeCount; + + + /** + * 头结点 + */ + private Node header; + + /** + * 尾结点 + */ + private Node tailer; + + public LRUMap(int cacheSize) { + this.cacheSize = cacheSize; + //头结点的下一个结点为空 + header = new Node<>(); + header.next = null; + + //尾结点的上一个结点为空 + tailer = new Node<>(); + tailer.tail = null; + + //双向链表 头结点的上结点指向尾结点 + header.tail = tailer; + + //尾结点的下结点指向头结点 + tailer.next = header; + + + } + + public void put(K key, V value) { + cacheMap.put(key, value); + + //双向链表中添加结点 + addNode(key, value); + } + + public V get(K key){ + + Node node = getNode(key); + + //移动到头结点 + moveToHead(node) ; + + return cacheMap.get(key); + } + + private void moveToHead(Node node){ + + //如果是最后的一个节点 + if (node.tail == null){ + node.next.tail = null ; + tailer = node.next ; + nodeCount -- ; + } + + //如果是本来就是头节点 不作处理 + if (node.next == null){ + return ; + } + + //如果处于中间节点 + if (node.tail != null && node.next != null){ + //它的上一节点指向它的下一节点 也就删除当前节点 + node.tail.next = node.next ; + nodeCount -- ; + } + + //最后在头部增加当前节点 + //注意这里需要重新 new 一个对象,不然原本的node 还有着下面的引用,会造成内存溢出。 + node = new Node<>(node.getKey(),node.getValue()) ; + addHead(node) ; + + } + + /** + * 链表查询 效率较低 + * @param key + * @return + */ + private Node getNode(K key){ + Node node = tailer ; + while (node != null){ + + if (node.getKey().equals(key)){ + return node ; + } + + node = node.next ; + } + + return null ; + } + + + /** + * 写入头结点 + * @param key + * @param value + */ + private void addNode(K key, V value) { + + Node node = new Node<>(key, value); + + //容量满了删除最后一个 + if (cacheSize == nodeCount) { + //删除尾结点 + delTail(); + } + + //写入头结点 + addHead(node); + + } + + + + /** + * 添加头结点 + * + * @param node + */ + private void addHead(Node node) { + + //写入头结点 + header.next = node; + node.tail = header; + header = node; + nodeCount++; + + //如果写入的数据大于2个 就将初始化的头尾结点删除 + if (nodeCount == 2) { + tailer.next.next.tail = null; + tailer = tailer.next.next; + } + + } + + private void delTail() { + //把尾结点从缓存中删除 + cacheMap.remove(tailer.getKey()); + + //删除尾结点 + tailer.next.tail = null; + tailer = tailer.next; + + nodeCount--; + + } + + private class Node { + private K key; + private V value; + Node tail; + Node next; + + public Node(K key, V value) { + this.key = key; + this.value = value; + } + + public Node() { + } + + public K getKey() { + return key; + } + + public void setKey(K key) { + this.key = key; + } + + public V getValue() { + return value; + } + + public void setValue(V value) { + this.value = value; + } + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() ; + Node node = tailer ; + while (node != null){ + sb.append(node.getKey()).append(":") + .append(node.getValue()) + .append("-->") ; + + node = node.next ; + } + + + return sb.toString(); + } +} +``` + +源码: +[https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUMap.java](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUMap.java) + +实际效果,写入时: + +```java + @Test + public void put() throws Exception { + LRUMap lruMap = new LRUMap(3) ; + lruMap.put("1",1) ; + lruMap.put("2",2) ; + lruMap.put("3",3) ; + + System.out.println(lruMap.toString()); + + lruMap.put("4",4) ; + System.out.println(lruMap.toString()); + + lruMap.put("5",5) ; + System.out.println(lruMap.toString()); + } + +//输出: +1:1-->2:2-->3:3--> +2:2-->3:3-->4:4--> +3:3-->4:4-->5:5--> +``` + +使用时: + +```java + @Test + public void get() throws Exception { + LRUMap lruMap = new LRUMap(3) ; + lruMap.put("1",1) ; + lruMap.put("2",2) ; + lruMap.put("3",3) ; + + System.out.println(lruMap.toString()); + System.out.println("=============="); + + Integer integer = lruMap.get("1"); + System.out.println(integer); + System.out.println("=============="); + System.out.println(lruMap.toString()); + } + +//输出 +1:1-->2:2-->3:3--> +============== +1 +============== +2:2-->3:3-->1:1--> +``` + +实现思路和上文提到的一致,说下重点: + +- 数据是直接利用 HashMap 来存放的。 +- 内部使用了一个双向链表来存放数据,所以有一个头结点 header,以及尾结点 tailer。 +- 每次写入头结点,删除尾结点时都是依赖于 header tailer,如果看着比较懵建议自己实现一个链表熟悉下,或结合下文的对象关系图一起理解。 +- 使用数据移动到链表头时,第一步是需要在双向链表中找到该节点。这里就体现出链表的问题了。查找效率很低,最差需要 `O(N)`。之后依赖于当前节点进行移动。 +- 在写入头结点时有判断链表大小等于 2 时需要删除初始化的头尾结点。这是因为初始化时候生成了两个双向节点,没有数据只是为了形成一个数据结构。当真实数据进来之后需要删除以方便后续的操作(这点可以继续优化)。 +- 以上的所有操作都是线程不安全的,需要使用者自行控制。 + +下面是对象关系图: + +### 初始化时 + +![](https://i.loli.net/2019/06/26/5d13931b9416744111.jpg) + +### 写入数据时 + +```java +LRUMap lruMap = new LRUMap(3) ; +lruMap.put("1",1) ; +``` + +![](https://i.loli.net/2019/06/26/5d13931c136d238581.jpg) + + +```java +lruMap.put("2",2) ; +``` +![](https://i.loli.net/2019/06/26/5d1393217488285452.jpg) + + +```java +lruMap.put("3",3) ; +``` +![](https://i.loli.net/2019/06/26/5d139321e34f996391.jpg) + +```java +lruMap.put("4",4) ; +``` +![](https://i.loli.net/2019/06/26/5d139322609e214433.jpg) + + +### 获取数据时 + +数据和上文一样: + +```java +Integer integer = lruMap.get("2"); +``` + +![](https://i.loli.net/2019/06/26/5d139322ea89567527.jpg) + +通过以上几张图应该是很好理解数据是如何存放的了。 + +## 实现三 + +其实如果对 Java 的集合比较熟悉的话,会发现上文的结构和 LinkedHashMap 非常类似。 + +对此不太熟悉的朋友可以先了解下 [LinkedHashMap 底层分析](http://crossoverjie.top/2018/02/06/LinkedHashMap/) 。 + +所以我们完全可以借助于它来实现: + +```java +public class LRULinkedMap { + + + /** + * 最大缓存大小 + */ + private int cacheSize; + + private LinkedHashMap cacheMap ; + + + public LRULinkedMap(int cacheSize) { + this.cacheSize = cacheSize; + + cacheMap = new LinkedHashMap(16,0.75F,true){ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (cacheSize + 1 == cacheMap.size()){ + return true ; + }else { + return false ; + } + } + }; + } + + public void put(K key,V value){ + cacheMap.put(key,value) ; + } + + public V get(K key){ + return cacheMap.get(key) ; + } + + + public Collection> getAll() { + return new ArrayList>(cacheMap.entrySet()); + } +} +``` + +源码: +[https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRULinkedMap.java](https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRULinkedMap.java) + +这次就比较简洁了,也就几行代码(具体的逻辑 LinkedHashMap 已经帮我们实现好了) + +实际效果: + +```java + @Test + public void put() throws Exception { + LRULinkedMap map = new LRULinkedMap(3) ; + map.put("1",1); + map.put("2",2); + map.put("3",3); + + for (Map.Entry e : map.getAll()){ + System.out.print(e.getKey() + " : " + e.getValue() + "\t"); + } + + System.out.println(""); + map.put("4",4); + for (Map.Entry e : map.getAll()){ + System.out.print(e.getKey() + " : " + e.getValue() + "\t"); + } + } + +//输出 +1 : 1 2 : 2 3 : 3 +2 : 2 3 : 3 4 : 4 +``` + +使用时: + +```java + @Test + public void get() throws Exception { + LRULinkedMap map = new LRULinkedMap(4) ; + map.put("1",1); + map.put("2",2); + map.put("3",3); + map.put("4",4); + + for (Map.Entry e : map.getAll()){ + System.out.print(e.getKey() + " : " + e.getValue() + "\t"); + } + + System.out.println(""); + map.get("1") ; + for (Map.Entry e : map.getAll()){ + System.out.print(e.getKey() + " : " + e.getValue() + "\t"); + } + } + +} + +//输出 +1 : 1 2 : 2 3 : 3 4 : 4 +2 : 2 3 : 3 4 : 4 1 : 1 +``` + +LinkedHashMap 内部也有维护一个双向队列,在初始化时也会给定一个缓存大小的阈值。初始化时自定义是否需要删除最近不常使用的数据,如果是则会按照实现二中的方式管理数据。 + +其实主要代码就是重写了 LinkedHashMap 的 removeEldestEntry 方法: + +```java + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } +``` + +它默认是返回 false,也就是不会管有没有超过阈值。 + +所以我们自定义大于了阈值时返回 true,这样 LinkedHashMap 就会帮我们删除最近最少使用的数据。 + +## 总结 + +以上就是对 LRU 缓存的实现,了解了这些至少在平时使用时可以知其所以然。 + +当然业界使用较多的还有 [guava](https://github.com/google/guava) 的实现,并且它还支持多种过期策略。 + + + diff --git a/docs/algorithm/Limiting.md b/docs/algorithm/Limiting.md new file mode 100644 index 00000000..2e37a28b --- /dev/null +++ b/docs/algorithm/Limiting.md @@ -0,0 +1,58 @@ +# 限流算法 + +限流是解决高并发大流量的一种方案,至少是可以保证应用的可用性。 + +通常有以下两种限流方案: + +- 漏桶算法 +- 令牌桶算法 + +## 漏桶算法 + +![漏桶算法,来自网络.png](https://i.loli.net/2017/08/11/598c905caa8cb.png) + +漏桶算法非常简单,就是将流量放入桶中并按照一定的速率流出。如果流量过大时候并不会提高流出效率,而溢出的流量也只能是抛弃掉了。 + +这种算法很简单,但也非常粗暴,无法应对突发的大流量。 +这时可以考虑令牌桶算法。 + +## 令牌桶算法 +![令牌桶算法-来自网络.gif](https://i.loli.net/2017/08/11/598c91f2a33af.gif) + +令牌桶算法是按照恒定的速率向桶中放入令牌,每当请求经过时则消耗一个或多个令牌。当桶中的令牌为 0 时,请求则会被阻塞。 + +> note: +令牌桶算法支持先消费后付款,比如一个请求可以获取多个甚至全部的令牌,但是需要后面的请求付费。也就是说后面的请求需要等到桶中的令牌补齐之后才能继续获取。 + +实例: +```java + @Override + public BaseResponse getUserByFeignBatch(@RequestBody UserReqVO userReqVO) { + //调用远程服务 + OrderNoReqVO vo = new OrderNoReqVO() ; + vo.setReqNo(userReqVO.getReqNo()); + + RateLimiter limiter = RateLimiter.create(2.0) ; + //批量调用 + for (int i = 0 ;i< 10 ; i++){ + double acquire = limiter.acquire(); + logger.debug("获取令牌成功!,消耗=" + acquire); + BaseResponse orderNo = orderServiceClient.getOrderNo(vo); + logger.debug("远程返回:"+JSON.toJSONString(orderNo)); + } + + UserRes userRes = new UserRes() ; + userRes.setUserId(123); + userRes.setUserName("张三"); + + userRes.setReqNo(userReqVO.getReqNo()); + userRes.setCode(StatusEnum.SUCCESS.getCode()); + userRes.setMessage("成功"); + + return userRes ; + } +``` + + +1. [单 JVM 限流](http://crossoverjie.top/2017/08/11/sbc4/) +2. [分布式限流](distributed/Distributed-Limit.md) diff --git a/docs/algorithm/common-algorithm.md b/docs/algorithm/common-algorithm.md new file mode 100644 index 00000000..9c919e4a --- /dev/null +++ b/docs/algorithm/common-algorithm.md @@ -0,0 +1,1448 @@ + +# 红包算法 + +# 红包算法 + + +```java +public class RedPacket { + + /** + * 生成红包最小值 1分 + */ + private static final int MIN_MONEY = 1; + + /** + * 生成红包最大值 200人民币 + */ + private static final int MAX_MONEY = 200 * 100; + + /** + * 小于最小值 + */ + private static final int LESS = -1; + /** + * 大于最大值 + */ + private static final int MORE = -2; + + /** + * 正常值 + */ + private static final int OK = 1; + + /** + * 最大的红包是平均值的 TIMES 倍,防止某一次分配红包较大 + */ + private static final double TIMES = 2.1F; + + private int recursiveCount = 0; + + public List splitRedPacket(int money, int count) { + List moneys = new LinkedList<>(); + + //金额检查,如果最大红包 * 个数 < 总金额;则需要调大最小红包 MAX_MONEY + if (MAX_MONEY * count <= money) { + System.err.println("请调大最小红包金额 MAX_MONEY=[" + MAX_MONEY + "]"); + return moneys ; + } + + + //计算出最大红包 + int max = (int) ((money / count) * TIMES); + max = max > MAX_MONEY ? MAX_MONEY : max; + + for (int i = 0; i < count; i++) { + //随机获取红包 + int redPacket = randomRedPacket(money, MIN_MONEY, max, count - i); + moneys.add(redPacket); + //总金额每次减少 + money -= redPacket; + } + + return moneys; + } + + private int randomRedPacket(int totalMoney, int minMoney, int maxMoney, int count) { + //只有一个红包直接返回 + if (count == 1) { + return totalMoney; + } + + if (minMoney == maxMoney) { + return minMoney; + } + + //如果最大金额大于了剩余金额 则用剩余金额 因为这个 money 每分配一次都会减小 + maxMoney = maxMoney > totalMoney ? totalMoney : maxMoney; + + //在 minMoney到maxMoney 生成一个随机红包 + int redPacket = (int) (Math.random() * (maxMoney - minMoney) + minMoney); + + int lastMoney = totalMoney - redPacket; + + int status = checkMoney(lastMoney, count - 1); + + //正常金额 + if (OK == status) { + return redPacket; + } + + //如果生成的金额不合法 则递归重新生成 + if (LESS == status) { + recursiveCount++; + System.out.println("recursiveCount==" + recursiveCount); + return randomRedPacket(totalMoney, minMoney, redPacket, count); + } + + if (MORE == status) { + recursiveCount++; + System.out.println("recursiveCount===" + recursiveCount); + return randomRedPacket(totalMoney, redPacket, maxMoney, count); + } + + return redPacket; + } + + /** + * 校验剩余的金额的平均值是否在 最小值和最大值这个范围内 + * + * @param lastMoney + * @param count + * @return + */ + private int checkMoney(int lastMoney, int count) { + double avg = lastMoney / count; + if (avg < MIN_MONEY) { + return LESS; + } + + if (avg > MAX_MONEY) { + return MORE; + } + + return OK; + } + + + public static void main(String[] args) { + RedPacket redPacket = new RedPacket(); + List redPackets = redPacket.splitRedPacket(20000, 100); + System.out.println(redPackets); + + int sum = 0; + for (Integer red : redPackets) { + sum += red; + } + System.out.println(sum); + } + +} +``` + + + +# 二叉树层序遍历 + +```java +public class BinaryNode { + private Object data ; + private BinaryNode left ; + private BinaryNode right ; + + public BinaryNode() { + } + + public BinaryNode(Object data, BinaryNode left, BinaryNode right) { + this.data = data; + this.left = left; + this.right = right; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public BinaryNode getLeft() { + return left; + } + + public void setLeft(BinaryNode left) { + this.left = left; + } + + public BinaryNode getRight() { + return right; + } + + public void setRight(BinaryNode right) { + this.right = right; + } + + + public BinaryNode createNode(){ + BinaryNode node = new BinaryNode("1",null,null) ; + BinaryNode left2 = new BinaryNode("2",null,null) ; + BinaryNode left3 = new BinaryNode("3",null,null) ; + BinaryNode left4 = new BinaryNode("4",null,null) ; + BinaryNode left5 = new BinaryNode("5",null,null) ; + BinaryNode left6 = new BinaryNode("6",null,null) ; + node.setLeft(left2) ; + left2.setLeft(left4); + left2.setRight(left6); + node.setRight(left3); + left3.setRight(left5) ; + return node ; + } + + @Override + public String toString() { + return "BinaryNode{" + + "data=" + data + + ", left=" + left + + ", right=" + right + + '}'; + } + + + /** + * 二叉树的层序遍历 借助于队列来实现 借助队列的先进先出的特性 + * + * 首先将根节点入队列 然后遍历队列。 + * 首先将根节点打印出来,接着判断左节点是否为空 不为空则加入队列 + * @param node + */ + public void levelIterator(BinaryNode node){ + LinkedList queue = new LinkedList<>() ; + + //先将根节点入队 + queue.offer(node) ; + BinaryNode current ; + while (!queue.isEmpty()){ + current = queue.poll(); + + System.out.print(current.data+"--->"); + + if (current.getLeft() != null){ + queue.offer(current.getLeft()) ; + } + if (current.getRight() != null){ + queue.offer(current.getRight()) ; + } + } + } +} + +public class BinaryNodeTest { + + @Test + public void test1(){ + BinaryNode node = new BinaryNode() ; + //创建二叉树 + node = node.createNode() ; + System.out.println(node); + + //层序遍历二叉树 + node.levelIterator(node) ; + + } + +} +``` + + + +# 是否为快乐数字 + +```java +/** + * Function: 判断一个数字是否为快乐数字 19 就是快乐数字 11就不是快乐数字 + * 19 + * 1*1+9*9=82 + * 8*8+2*2=68 + * 6*6+8*8=100 + * 1*1+0*0+0*0=1 + * + * 11 + * 1*1+1*1=2 + * 2*2=4 + * 4*4=16 + * 1*1+6*6=37 + * 3*3+7*7=58 + * 5*5+8*8=89 + * 8*8+9*9=145 + * 1*1+4*4+5*5=42 + * 4*4+2*2=20 + * 2*2+0*0=2 + * + * 这里结果 1*1+1*1=2 和 2*2+0*0=2 重复,所以不是快乐数字 + * @author crossoverJie + * Date: 04/01/2018 14:12 + * @since JDK 1.8 + */ +public class HappyNum { + + /** + * 判断一个数字是否为快乐数字 + * @param number + * @return + */ + public boolean isHappy(int number) { + Set set = new HashSet<>(30); + while (number != 1) { + int sum = 0; + while (number > 0) { + //计算当前值的每位数的平方 相加的和 在放入set中,如果存在相同的就认为不是 happy数字 + sum += (number % 10) * (number % 10); + number = number / 10; + } + if (set.contains(sum)) { + return false; + } else { + set.add(sum); + } + number = sum; + } + return true; + } +} + +public class HappyNumTest { + @Test + public void isHappy() throws Exception { + HappyNum happyNum = new HappyNum() ; + boolean happy = happyNum.isHappy(19); + Assert.assertEquals(happy,true); + } + + @Test + public void isHappy2() throws Exception { + HappyNum happyNum = new HappyNum() ; + boolean happy = happyNum.isHappy(11); + Assert.assertEquals(happy,false); + } + + @Test + public void isHappy3() throws Exception { + HappyNum happyNum = new HappyNum() ; + boolean happy = happyNum.isHappy(100); + System.out.println(happy); + } + +} +``` + +# 链表是否有环 + +```java +/** + * Function:是否是环链表,采用快慢指针,一个走的快些一个走的慢些 如果最终相遇了就说明是环 + * 就相当于在一个环形跑道里跑步,速度不一样的最终一定会相遇。 + * + * @author crossoverJie + * Date: 04/01/2018 11:33 + * @since JDK 1.8 + */ +public class LinkLoop { + + public static class Node{ + private Object data ; + public Node next ; + + public Node(Object data, Node next) { + this.data = data; + this.next = next; + } + + public Node(Object data) { + this.data = data ; + } + } + + /** + * 判断链表是否有环 + * @param node + * @return + */ + public boolean isLoop(Node node){ + Node slow = node ; + Node fast = node.next ; + + while (slow.next != null){ + Object dataSlow = slow.data; + Object dataFast = fast.data; + + //说明有环 + if (dataFast == dataSlow){ + return true ; + } + + //一共只有两个节点,但却不是环形链表的情况,判断NPE + if (fast.next == null){ + return false ; + } + //slow走慢点 fast走快点 + slow = slow.next ; + fast = fast.next.next ; + + //如果走的快的发现为空 说明不存在环 + if (fast == null){ + return false ; + } + } + return false ; + } +} +public class LinkLoopTest { + + /** + * 无环 + * @throws Exception + */ + @Test + public void isLoop() throws Exception { + LinkLoop.Node node3 = new LinkLoop.Node("3"); + LinkLoop.Node node2 = new LinkLoop.Node("2") ; + LinkLoop.Node node1 = new LinkLoop.Node("1") ; + + node1.next = node2 ; + node2.next = node3 ; + + LinkLoop linkLoop = new LinkLoop() ; + boolean loop = linkLoop.isLoop(node1); + Assert.assertEquals(loop,false); + } + + /** + * 有环 + * @throws Exception + */ + @Test + public void isLoop2() throws Exception { + LinkLoop.Node node3 = new LinkLoop.Node("3"); + LinkLoop.Node node2 = new LinkLoop.Node("2") ; + LinkLoop.Node node1 = new LinkLoop.Node("1") ; + + node1.next = node2 ; + node2.next = node3 ; + node3.next = node1 ; + + LinkLoop linkLoop = new LinkLoop() ; + boolean loop = linkLoop.isLoop(node1); + Assert.assertEquals(loop,true); + } + + /** + * 无环 + * @throws Exception + */ + @Test + public void isLoop3() throws Exception { + LinkLoop.Node node2 = new LinkLoop.Node("2") ; + LinkLoop.Node node1 = new LinkLoop.Node("1") ; + + node1.next = node2 ; + + + LinkLoop linkLoop = new LinkLoop() ; + boolean loop = linkLoop.isLoop(node1); + Assert.assertEquals(loop,false); + } + +} +``` + +# 从一个数组中返回两个值相加等于目标值的下标 + +```java +/** + * Function:{1,3,5,7} target=8 返回{2,3} + * + * @author crossoverJie + * Date: 04/01/2018 09:53 + * @since JDK 1.8 + */ +public class TwoSum { + + /** + * 时间复杂度为 O(N^2) + * @param nums + * @param target + * @return + */ + public int[] getTwo1(int[] nums,int target){ + int[] result = new int[2] ; + + for (int i= 0 ;i=0 ;j--){ + int b = nums[j] ; + + if ((a+b) == target){ + result = new int[]{i,j} ; + } + } + } + return result ; + } + + + /** + * 时间复杂度 O(N) + * 利用Map Key存放目标值和当前值的差值,value 就是当前的下标 + * 每次遍历是 查看当前遍历的值是否等于差值,如果是等于,说明两次相加就等于目标值。 + * 然后取出 map 中 value ,和本次遍历的下标,就是两个下标值相加等于目标值了。 + * + * @param nums + * @param target + * @return + */ + public int[] getTwo2(int[] nums,int target){ + int[] result = new int[2] ; + Map map = new HashMap<>(2) ; + for (int i=0 ;i stack = new Stack<>() ; + while (node != null){ + + System.out.print(node.value + "===>"); + + stack.push(node) ; + node = node.next ; + } + + System.out.println(""); + + System.out.println("====翻转之后===="); + while (!stack.isEmpty()){ + System.out.print(stack.pop().value + "===>"); + } + + } + + + /** + * 利用头插法插入链表 + * @param head + */ + public void reverseNode(Node head) { + if (head == null) { + return ; + } + + //最终翻转之后的 Node + Node node ; + + Node pre = head; + Node cur = head.next; + Node next ; + while(cur != null){ + next = cur.next; + + //链表的头插法 + cur.next = pre; + pre = cur; + + cur = next; + } + head.next = null; + node = pre; + + + //遍历新链表 + while (node != null){ + System.out.println(node.value); + node = node.next ; + } + + } + + + /** + * 递归 + * @param node + */ + public void recNode(Node node){ + + if (node == null){ + return ; + } + + if (node.next != null){ + recNode(node.next) ; + } + System.out.print(node.value+"===>"); + } + + + public static class Node{ + public T value; + public Node next ; + + + public Node(T value, Node next ) { + this.next = next; + this.value = value; + } + } +} +//单测 +public class ReverseNodeTest { + + @Test + public void reverseNode1() throws Exception { + ReverseNode.Node node4 = new Node<>("4",null) ; + Node node3 = new Node<>("3",node4); + Node node2 = new Node<>("2",node3); + Node node1 = new Node("1",node2) ; + + ReverseNode reverseNode = new ReverseNode() ; + reverseNode.reverseNode1(node1); + } + + @Test + public void reverseNode12() throws Exception { + + Node node1 = new Node("1",null) ; + + ReverseNode reverseNode = new ReverseNode() ; + reverseNode.reverseNode1(node1); + } + + @Test + public void reverseNode13() throws Exception { + + Node node1 = null ; + + ReverseNode reverseNode = new ReverseNode() ; + reverseNode.reverseNode1(node1); + } + + + /** + * 头插法 + * @throws Exception + */ + @Test + public void reverseHead21() throws Exception { + Node node4 = new Node<>("4",null) ; + Node node3 = new Node<>("3",node4); + Node node2 = new Node<>("2",node3); + Node node1 = new Node("1",node2) ; + + ReverseNode reverseNode = new ReverseNode() ; + reverseNode.reverseNode(node1); + + } + + + @Test + public void recNodeTest31(){ + Node node4 = new Node<>("4",null) ; + Node node3 = new Node<>("3",node4); + Node node2 = new Node<>("2",node3); + Node node1 = new Node("1",node2) ; + + ReverseNode reverseNode = new ReverseNode() ; + reverseNode.recNode(node1); + } + +} +``` + +# 合并两个排好序的链表 + +```java +/** + * Function: 合并两个排好序的链表 + * + * 每次比较两个链表的头结点,将较小结点放到新的链表,最后将新链表指向剩余的链表 + * + * @author crossoverJie + * Date: 07/12/2017 13:58 + * @since JDK 1.8 + */ +public class MergeTwoSortedLists { + + + /** + * 1. 声明一个头结点 + * 2. 将头结点的引用赋值给一个临时结点,也可以叫做下一结点。 + * 3. 进行循环比较,每次都将指向值较小的那个结点(较小值的引用赋值给 lastNode )。 + * 4. 再去掉较小值链表的头结点,指针后移。 + * 5. lastNode 指针也向后移,由于 lastNode 是 head 的引用,这样可以保证最终 head 的值是往后更新的。 + * 6. 当其中一个链表的指针移到最后时跳出循环。 + * 7. 由于这两个链表已经是排好序的,所以剩下的链表必定是最大的值,只需要将指针指向它即可。 + * 8. 由于 head 链表的第一个结点是初始化的0,所以只需要返回 0 的下一个结点即是合并了的链表。 + * @param l1 + * @param l2 + * @return + */ + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode head = new ListNode(0) ; + ListNode lastNode = head ; + + while (l1 != null && l2 != null){ + if (l1.currentVal < l2.currentVal){ + lastNode.next = l1 ; + l1 = l1.next ; + } else { + lastNode.next = l2 ; + l2 = l2.next ; + } + lastNode =lastNode.next ; + } + + if (l1 == null){ + lastNode.next = l2 ; + } + if (l2 == null){ + lastNode.next = l1 ; + } + + return head.next ; + } + + + public static class ListNode { + /** + * 当前值 + */ + int currentVal; + + /** + * 下一个节点 + */ + ListNode next; + + ListNode(int val) { + currentVal = val; + } + + @Override + public String toString() { + return "ListNode{" + + "currentVal=" + currentVal + + ", next=" + next + + '}'; + } + } + +} + +//单测 +public class MergeTwoSortedListsTest { + MergeTwoSortedLists mergeTwoSortedLists ; + @Before + public void setUp() throws Exception { + mergeTwoSortedLists = new MergeTwoSortedLists(); + } + + @Test + public void mergeTwoLists() throws Exception { + ListNode l1 = new ListNode(1) ; + ListNode l1_2 = new ListNode(4); + l1.next = l1_2 ; + ListNode l1_3 = new ListNode(5) ; + l1_2.next = l1_3 ; + + ListNode l2 = new ListNode(1) ; + ListNode l2_2 = new ListNode(3) ; + l2.next = l2_2 ; + ListNode l2_3 = new ListNode(6) ; + l2_2.next = l2_3 ; + ListNode l2_4 = new ListNode(9) ; + l2_3.next = l2_4 ; + ListNode listNode = mergeTwoSortedLists.mergeTwoLists(l1, l2); + + + ListNode node1 = new ListNode(1) ; + ListNode node2 = new ListNode(1); + node1.next = node2; + ListNode node3 = new ListNode(3) ; + node2.next= node3 ; + ListNode node4 = new ListNode(4) ; + node3.next = node4 ; + ListNode node5 = new ListNode(5) ; + node4.next = node5 ; + ListNode node6 = new ListNode(6) ; + node5.next = node6 ; + ListNode node7 = new ListNode(9) ; + node6.next = node7 ; + Assert.assertEquals(node1.toString(),listNode.toString()); + + + } + + @Test + public void mergeTwoLists2() throws Exception { + + ListNode l2 = new ListNode(1) ; + ListNode l2_2 = new ListNode(3) ; + l2.next = l2_2 ; + ListNode l2_3 = new ListNode(6) ; + l2_2.next = l2_3 ; + ListNode l2_4 = new ListNode(9) ; + l2_3.next = l2_4 ; + ListNode listNode = mergeTwoSortedLists.mergeTwoLists(null, l2); + + System.out.println(listNode.toString()); + + + } + + @Test + public void mergeTwoLists3() throws Exception { + + ListNode l2 = new ListNode(1) ; + ListNode l2_2 = new ListNode(3) ; + l2.next = l2_2 ; + ListNode l2_3 = new ListNode(6) ; + l2_2.next = l2_3 ; + ListNode l2_4 = new ListNode(9) ; + l2_3.next = l2_4 ; + ListNode listNode = mergeTwoSortedLists.mergeTwoLists(l2, null); + + + ListNode node1 = new ListNode(1) ; + ListNode node2 = new ListNode(3); + node1.next = node2; + ListNode node3 = new ListNode(6) ; + node2.next= node3 ; + ListNode node4 = new ListNode(9) ; + node3.next = node4 ; + + Assert.assertEquals(node1.toString(),listNode.toString()); + + } + +} +``` + +# 两个栈实现队列 +```java +/** + * Function: 两个栈实现队列 + * + * 利用两个栈来实现,第一个栈存放写队列的数据。 + * 第二个栈存放移除队列的数据,移除之前先判断第二个栈里是否有数据。 + * 如果没有就要将第一个栈里的数据依次弹出压入第二个栈,这样写入之后的顺序再弹出其实就是一个先进先出的结构了。 + * + * 这样出队列只需要移除第二个栈的头元素即可。 + * + * @author crossoverJie + * Date: 09/02/2018 23:51 + * @since JDK 1.8 + */ +public class TwoStackQueue { + + /** + * 写入的栈 + */ + private Stack input = new Stack() ; + + /** + * 移除队列所出的栈 + */ + private Stack out = new Stack() ; + + + /** + * 写入队列 + * @param t + */ + public void appendTail(T t){ + input.push(t) ; + } + + /** + * 删除队列头结点 并返回删除数据 + * @return + */ + public T deleteHead(){ + + //是空的 需要将 input 出栈写入 out + if (out.isEmpty()){ + while (!input.isEmpty()){ + out.push(input.pop()) ; + } + } + + //不为空时直接移除出栈就表示移除了头结点 + return out.pop() ; + } + + + public int getSize(){ + return input.size() + out.size() ; + } + +} +//单测 +public class TwoStackQueueTest { + private final static Logger LOGGER = LoggerFactory.getLogger(TwoStackQueueTest.class); + @Test + public void queue(){ + TwoStackQueue twoStackQueue = new TwoStackQueue() ; + twoStackQueue.appendTail("1") ; + twoStackQueue.appendTail("2") ; + twoStackQueue.appendTail("3") ; + twoStackQueue.appendTail("4") ; + twoStackQueue.appendTail("5") ; + + + int size = twoStackQueue.getSize(); + + for (int i = 0; i< size ; i++){ + LOGGER.info(twoStackQueue.deleteHead()); + } + + LOGGER.info("========第二次添加========="); + + twoStackQueue.appendTail("6") ; + + size = twoStackQueue.getSize(); + + for (int i = 0; i< size ; i++){ + LOGGER.info(twoStackQueue.deleteHead()); + } + } + +} +``` +# 链表排序 +```java +/** + * 链表排序, 建议使用归并排序, + * 问题描述,给定一个Int的链表,要求在时间最优的情况下完成链表元素由大到小的排序, + * e.g: 1->5->4->3->2 + * 排序后结果 5->4->3->2->1 + * + * @author 6563699600@qq.com + * @date 6/7/2018 11:42 PM + * @since 1.0 + */ +public class LinkedListMergeSort { + + /** + * 定义链表数据结构,包含当前元素,以及当前元素的后续元素指针 + */ + final static class Node { + int e; + Node next; + + public Node() { + } + + public Node(int e, Node next) { + this.e = e; + this.next = next; + } + } + + public Node mergeSort(Node first, int length) { + + if (length == 1) { + return first; + } else { + Node middle = new Node(); + Node tmp = first; + + /** + * 后期会对这里进行优化,通过一次遍历算出长度和中间元素 + */ + for (int i = 0; i < length; i++) { + if (i == length / 2) { + break; + } + middle = tmp; + tmp = tmp.next; + } + + /** + * 这里是链表归并时要注意的细节 + * 在链表进行归并排序过程中,会涉及到将一个链表打散为两个独立的链表,所以需要在中间元素的位置将其后续指针指为null; + */ + Node right = middle.next; + middle.next = null; + + Node leftStart = mergeSort(first, length / 2); + Node rightStart; + if (length % 2 == 0) { + rightStart = mergeSort(right, length / 2); + } else { + rightStart = mergeSort(right, length / 2 + 1); + } + return mergeList(leftStart, rightStart); + } + } + + /** + * 合并链表,具体的实现细节可参考MergeTwoSortedLists + * + * @param left + * @param right + * @return + */ + public Node mergeList(Node left, Node right) { + + Node head = new Node(); + Node result = head; + + /** + * 思想就是两个链表同时遍历,将更的元素插入结果中,同时更更大的元素所属的链表的指针向下移动 + */ + while (!(null == left && null == right)) { + Node tmp; + if (left == null) { + result.next = right; + break; + } else if (right == null) { + result.next = left; + break; + } else if (left.e >= right.e) { + tmp = left; + result.next = left; + result = tmp; + left = left.next; + } else { + tmp = right; + result.next = right; + result = tmp; + right = right.next; + } + } + + return head.next; + } + + public static void main(String[] args) { + + Node head = new Node(); + + head.next = new Node(7, + new Node(2, + new Node(5, + new Node(4, + new Node(3, + new Node(6, + new Node(11, null) + ) + ) + ) + ) + ) + ); + + int length = 0; + + for (Node e = head.next; null != e; e = e.next) { + length++; + } + + + LinkedListMergeSort sort = new LinkedListMergeSort(); + head.next = sort.mergeSort(head.next, length); + + + for (Node n = head.next; n != null; n = n.next) { + System.out.println(n.e); + } + + } +} +``` +# 数组右移 k 次 +```java +/** + * 数组右移K次, 原数组 [1, 2, 3, 4, 5, 6, 7] 右移3次后结果为 [5,6,7,1,2,3,4] + * + * 基本思路:不开辟新的数组空间的情况下考虑在原属组上进行操作 + * 1 将数组倒置,这样后k个元素就跑到了数组的前面,然后反转一下即可 + * 2 同理后 len-k个元素只需要翻转就完成数组的k次移动 + * + * @author 656369960@qq.com + * @date 12/7/2018 1:38 PM + * @since 1.0 + */ +public class ArrayKShift { + + public void arrayKShift(int[] array, int k) { + + /** + * constrictions + */ + + if (array == null || 0 == array.length) { + return ; + } + + k = k % array.length; + + if (0 > k) { + return; + } + + + /** + * reverse array , e.g: [1, 2, 3 ,4] to [4,3,2,1] + */ + + for (int i = 0; i < array.length / 2; i++) { + int tmp = array[i]; + array[i] = array[array.length - 1 - i]; + array[array.length - 1 - i] = tmp; + } + + /** + * first k element reverse + */ + for (int i = 0; i < k / 2; i++) { + int tmp = array[i]; + array[i] = array[k - 1 - i]; + array[k - 1 - i] = tmp; + } + + /** + * last length - k element reverse + */ + + for (int i = k; i < k + (array.length - k ) / 2; i ++) { + int tmp = array[i]; + array[i] = array[array.length - 1 - i + k]; + array[array.length - 1 - i + k] = tmp; + } + } + + public static void main(String[] args) { + int[] array = {1, 2, 3 ,4, 5, 6, 7}; + ArrayKShift shift = new ArrayKShift(); + shift.arrayKShift(array, 6); + + Arrays.stream(array).forEach(o -> { + System.out.println(o); + }); + + } +} +``` + +# 交替打印奇偶数 + +## lock 版 + +```java +/** + * Function: 两个线程交替执行打印 1~100 + * + * lock 版 + * + * @author crossoverJie + * Date: 11/02/2018 10:04 + * @since JDK 1.8 + */ +public class TwoThread { + + private int start = 1; + + /** + * 对 flag 的写入虽然加锁保证了线程安全,但读取的时候由于 不是 volatile 所以可能会读取到旧值 + * + */ + private volatile boolean flag = false; + + /** + * 重入锁 + */ + private final static Lock LOCK = new ReentrantLock(); + + public static void main(String[] args) { + TwoThread twoThread = new TwoThread(); + + Thread t1 = new Thread(new OuNum(twoThread)); + t1.setName("t1"); + + + Thread t2 = new Thread(new JiNum(twoThread)); + t2.setName("t2"); + + t1.start(); + t2.start(); + } + + /** + * 偶数线程 + */ + public static class OuNum implements Runnable { + + private TwoThread number; + + public OuNum(TwoThread number) { + this.number = number; + } + + @Override + public void run() { + while (number.start <= 1000) { + + if (number.flag) { + try { + LOCK.lock(); + System.out.println(Thread.currentThread().getName() + "+-+" + number.start); + number.start++; + number.flag = false; + + + } finally { + LOCK.unlock(); + } + } + } + } + } + + /** + * 奇数线程 + */ + public static class JiNum implements Runnable { + + private TwoThread number; + + public JiNum(TwoThread number) { + this.number = number; + } + + @Override + public void run() { + while (number.start <= 1000) { + + if (!number.flag) { + try { + LOCK.lock(); + System.out.println(Thread.currentThread().getName() + "+-+" + number.start); + number.start++; + number.flag = true; + + + } finally { + LOCK.unlock(); + } + } + } + } + } +} +``` + +## 等待通知版 +```java +/** + * Function:两个线程交替执行打印 1~100 + * 等待通知机制版 + * + * @author crossoverJie + * Date: 07/03/2018 13:19 + * @since JDK 1.8 + */ +public class TwoThreadWaitNotify { + + private int start = 1; + + private boolean flag = false; + + public static void main(String[] args) { + TwoThreadWaitNotify twoThread = new TwoThreadWaitNotify(); + + Thread t1 = new Thread(new OuNum(twoThread)); + t1.setName("t1"); + + + Thread t2 = new Thread(new JiNum(twoThread)); + t2.setName("t2"); + + t1.start(); + t2.start(); + } + + /** + * 偶数线程 + */ + public static class OuNum implements Runnable { + private TwoThreadWaitNotify number; + + public OuNum(TwoThreadWaitNotify number) { + this.number = number; + } + + @Override + public void run() { + + while (number.start <= 100) { + synchronized (TwoThreadWaitNotify.class) { + System.out.println("偶数线程抢到锁了"); + if (number.flag) { + System.out.println(Thread.currentThread().getName() + "+-+偶数" + number.start); + number.start++; + + number.flag = false; + TwoThreadWaitNotify.class.notify(); + + }else { + try { + TwoThreadWaitNotify.class.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + } + } + } + + + /** + * 奇数线程 + */ + public static class JiNum implements Runnable { + private TwoThreadWaitNotify number; + + public JiNum(TwoThreadWaitNotify number) { + this.number = number; + } + + @Override + public void run() { + while (number.start <= 100) { + synchronized (TwoThreadWaitNotify.class) { + System.out.println("奇数线程抢到锁了"); + if (!number.flag) { + System.out.println(Thread.currentThread().getName() + "+-+奇数" + number.start); + number.start++; + + number.flag = true; + + TwoThreadWaitNotify.class.notify(); + }else { + try { + TwoThreadWaitNotify.class.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + } + } +} +``` + +## 非阻塞版 +```java +/** + * Function: 两个线程交替执行打印 1~100 + *

+ * non blocking 版: + * 两个线程轮询volatile变量(flag) + * 线程一"看到"flag值为1时执行代码并将flag设置为0, + * 线程二"看到"flag值为0时执行代码并将flag设置未1, + * 2个线程不断轮询直到满足条件退出 + * + * @author twoyao + * Date: 05/07/2018 + * @since JDK 1.8 + */ + +public class TwoThreadNonBlocking implements Runnable { + + /** + * 当flag为1时只有奇数线程可以执行,并将其置为0 + * 当flag为0时只有偶数线程可以执行,并将其置为1 + */ + private volatile static int flag = 1; + + private int start; + private int end; + private String name; + + private TwoThreadNonBlocking(int start, int end, String name) { + this.name = name; + this.start = start; + this.end = end; + } + + @Override + public void run() { + while (start <= end) { + int f = flag; + if ((start & 0x01) == f) { + System.out.println(name + "+-+" + start); + start += 2; + // 因为只可能同时存在一个线程修改该值,所以不会存在竞争 + flag ^= 0x1; + } + } + } + + + public static void main(String[] args) { + new Thread(new TwoThreadNonBlocking(1, 100, "t1")).start(); + new Thread(new TwoThreadNonBlocking(2, 100, "t2")).start(); + } +} +``` \ No newline at end of file diff --git a/docs/algorithm/consistent-hash-implement.md b/docs/algorithm/consistent-hash-implement.md new file mode 100644 index 00000000..fb669b4e --- /dev/null +++ b/docs/algorithm/consistent-hash-implement.md @@ -0,0 +1,307 @@ + +![](https://i.loli.net/2019/05/08/5cd1be999402c.jpg) + +# 前言 + +记得一年前分享过一篇[《一致性 Hash 算法分析》](https://crossoverjie.top/2018/01/08/Consistent-Hash/),当时只是分析了这个算法的实现原理、解决了什么问题等。 + +但没有实际实现一个这样的算法,毕竟要加深印象还得自己撸一遍,于是本次就当前的一个路由需求来着手实现一次。 + +# 背景 + +看过[《为自己搭建一个分布式 IM(即时通讯) 系统》](https://crossoverjie.top/2019/01/02/netty/cim01-started/)的朋友应该对其中的登录逻辑有所印象。 + + +> 先给新来的朋友简单介绍下 [cim](https://github.com/crossoverJie/cim) 是干啥的: + +![](https://i.loli.net/2019/05/08/5cd1be99f3bb2.jpg) + +其中有一个场景是在客户端登录成功后需要从可用的服务端列表中选择一台服务节点返回给客户端使用。 + +而这个选择的过程就是一个负载策略的过程;第一版本做的比较简单,默认只支持轮询的方式。 + +虽然够用,但不够优雅😏。 + +**因此我的规划是内置多种路由策略供使用者根据自己的场景选择,同时提供简单的 API 供用户自定义自己的路由策略。** + + +先来看看一致性 Hash 算法的一些特点: + +- 构造一个 `0 ~ 2^32-1` 大小的环。 +- 服务节点经过 hash 之后将自身存放到环中的下标中。 +- 客户端根据自身的某些数据 hash 之后也定位到这个环中。 +- 通过顺时针找到离他最近的一个节点,也就是这次路由的服务节点。 +- 考虑到服务节点的个数以及 hash 算法的问题导致环中的数据分布不均匀时引入了虚拟节点。 + +![](https://i.loli.net/2019/05/08/5cd1be9b0e4e3.jpg) + +# 自定义有序 Map + +根据这些客观条件我们很容易想到通过自定义一个**有序**数组来模拟这个环。 + +这样我们的流程如下: + +1. 初始化一个长度为 N 的数组。 +2. 将服务节点通过 hash 算法得到的正整数,同时将节点自身的数据(hashcode、ip、端口等)存放在这里。 +3. 完成节点存放后将整个数组进行排序(排序算法有多种)。 +4. 客户端获取路由节点时,将自身进行 hash 也得到一个正整数; +5. 遍历这个数组直到找到一个数据大于等于当前客户端的 hash 值,就将当前节点作为该客户端所路由的节点。 +6. 如果没有发现比客户端大的数据就返回第一个节点(满足环的特性)。 + +先不考虑排序所消耗的时间,单看这个路由的时间复杂度: +- 最好是第一次就找到,时间复杂度为`O(1)`。 +- 最差为遍历完数组后才找到,时间复杂度为`O(N)`。 + +理论讲完了来看看具体实践。 + +我自定义了一个类:`SortArrayMap` + +他的使用方法及结果如下: + +![](https://i.loli.net/2019/05/08/5cd1be9b8278e.jpg) + +![](https://i.loli.net/2019/05/08/5cd1be9bb786e.jpg) + +可见最终会按照 `key` 的大小进行排序,同时传入 `hashcode = 101` 时会按照顺时针找到 `hashcode = 1000` 这个节点进行返回。 + +---- +下面来看看具体的实现。 + +成员变量和构造函数如下: + +![](https://i.loli.net/2019/05/08/5cd1be9c182fe.jpg) + +其中最核心的就是一个 `Node` 数组,用它来存放服务节点的 `hashcode` 以及 `value` 值。 + +其中的内部类 `Node` 结构如下: + +![](https://i.loli.net/2019/05/08/5cd1be9c6be0b.jpg) + +---- + +写入数据的方法如下: + +![](https://i.loli.net/2019/05/08/5cd1bea38b4ab.jpg) + +相信看过 `ArrayList` 的源码应该有印象,这里的写入逻辑和它很像。 + +- 写入之前判断是否需要扩容,如果需要则复制原来大小的 1.5 倍数组来存放数据。 +- 之后就写入数组,同时数组大小 +1。 + +但是存放时是按照写入顺序存放的,遍历时自然不会有序;因此提供了一个 `Sort` 方法,可以把其中的数据按照 `key` 其实也就是 `hashcode` 进行排序。 + +![](https://i.loli.net/2019/05/08/5cd1bea416c01.jpg) + +排序也比较简单,使用了 `Arrays` 这个数组工具进行排序,它其实是使用了一个 `TimSort` 的排序算法,效率还是比较高的。 + +最后则需要按照一致性 Hash 的标准顺时针查找对应的节点: + +![](https://i.loli.net/2019/05/08/5cd1bea459788.jpg) + +代码还是比较简单清晰的;遍历数组如果找到比当前 key 大的就返回,没有查到就取第一个。 + +这样就基本实现了一致性 Hash 的要求。 + +> ps:这里并不包含具体的 hash 方法以及虚拟节点等功能(具体实现请看下文),这个可以由使用者来定,SortArrayMap 可作为一个底层的数据结构,提供有序 Map 的能力,使用场景也不局限于一致性 Hash 算法中。 + +# TreeMap 实现 + +`SortArrayMap` 虽说是实现了一致性 hash 的功能,但效率还不够高,主要体现在 `sort` 排序处。 + +下图是目前主流排序算法的时间复杂度: + +![](https://i.loli.net/2019/05/08/5cd1bea49b947.jpg) + +最好的也就是 `O(N)` 了。 + +这里完全可以换一个思路,不用对数据进行排序;而是在写入的时候就排好顺序,只是这样会降低写入的效率。 + +比如二叉查找树,这样的数据结构 `jdk` 里有现成的实现;比如 `TreeMap` 就是使用红黑树来实现的,默认情况下它会对 key 进行自然排序。 + +--- + +来看看使用 `TreeMap` 如何来达到同样的效果。 +![](https://i.loli.net/2019/05/08/5cd1bea4e6550.jpg) +运行结果: + +``` +127.0.0.1000 +``` + +效果和上文使用 `SortArrayMap` 是一致的。 + +只使用了 TreeMap 的一些 API: + +- 写入数据候,`TreeMap` 可以保证 key 的自然排序。 +- `tailMap` 可以获取比当前 key 大的部分数据。 +- 当这个方法有数据返回时取第一个就是顺时针中的第一个节点了。 +- 如果没有返回那就直接取整个 `Map` 的第一个节点,同样也实现了环形结构。 + +> ps:这里同样也没有 hash 方法以及虚拟节点(具体实现请看下文),因为 TreeMap 和 SortArrayMap 一样都是作为基础数据结构来使用的。 + +## 性能对比 + +为了方便大家选择哪一个数据结构,我用 `TreeMap` 和 `SortArrayMap` 分别写入了一百万条数据来对比。 + +先是 `SortArrayMap`: + +![](https://i.loli.net/2019/05/08/5cd1bea9f1177.jpg) + +**耗时 2237 毫秒。** + +TreeMap: + +![](https://i.loli.net/2019/05/08/5cd1beaa90503.jpg) + +**耗时 1316毫秒。** + +结果是快了将近一倍,所以还是推荐使用 `TreeMap` 来进行实现,毕竟它不需要额外的排序损耗。 + +# cim 中的实际应用 + +下面来看看在 `cim` 这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。 + +## 模板方法 + +在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展自己的一致性 hash 算法因此我定义了一个抽象类;其中定义了一些模板方法,这样大家只需要在子类中进行不同的实现即可完成自己的算法。 + +AbstractConsistentHash,这个抽象类的主要方法如下: + +![](https://i.loli.net/2019/05/08/5cd1beab41c7a.jpg) + +- `add` 方法自然是写入数据的。 +- `sort` 方法用于排序,但子类也不一定需要重写,比如 `TreeMap` 这样自带排序的容器就不用。 +- `getFirstNodeValue` 获取节点。 +- `process` 则是面向客户端的,最终只需要调用这个方法即可返回一个节点。 + + +下面我们来看看利用 `SortArrayMap` 以及 `AbstractConsistentHash` 是如何实现的。 + +![](https://i.loli.net/2019/05/08/5cd1beab9a84f.jpg) + +就是实现了几个抽象方法,逻辑和上文是一样的,只是抽取到了不同的方法中。 + +只是在 add 方法中新增了几个虚拟节点,相信大家也看得明白。 + +> 把虚拟节点的控制放到子类而没有放到抽象类中也是为了灵活性考虑,可能不同的实现对虚拟节点的数量要求也不一样,所以不如自定义的好。 + +但是 `hash` 方法确是放到了抽象类中,子类不用重写;因为这是一个基本功能,只需要有一个公共算法可以保证他散列地足够均匀即可。 + +因此在 `AbstractConsistentHash` 中定义了 hash 方法。 + +![](https://i.loli.net/2019/05/08/5cd1beac476c2.jpg) + +> 这里的算法摘抄自 xxl_job,网上也有其他不同的实现,比如 `FNV1_32_HASH` 等;实现不同但是目的都一样。 + +--- + +这样对于使用者来说就非常简单了: + +![](https://i.loli.net/2019/05/08/5cd1beacc8e2c.jpg) + +他只需要构建一个服务列表,然后把当前的客户端信息传入 `process` 方法中即可获得一个一致性 hash 算法的返回。 + + + +--- + +同样的对于想通过 `TreeMap` 来实现也是一样的套路: + +![](https://i.loli.net/2019/05/08/5cd1bead5feca.jpg) + +他这里不需要重写 sort 方法,因为自身写入时已经排好序了。 + +而在使用时对于客户端来说只需求修改一个实现类,其他的啥都不用改就可以了。 + +![](https://i.loli.net/2019/05/08/5cd1beb27d748.jpg) + +运行的效果也是一样的。 + +这样大家想自定义自己的算法时只需要继承 `AbstractConsistentHash` 重写相关方法即可,**客户端代码无须改动。** + +## 路由算法扩展性 + +但其实对于 `cim` 来说真正的扩展性是对路由算法来说的,比如它需要支持轮询、hash、一致性hash、随机、LRU等。 + +只是一致性 hash 也有多种实现,他们的关系就如下图: + +![](https://i.loli.net/2019/05/08/5cd1beb2d6428.jpg) + +应用还需要满足对这一类路由策略的灵活支持,比如我也想自定义一个随机的策略。 + +因此定义了一个接口:`RouteHandle` + +```java +public interface RouteHandle { + + /** + * 再一批服务器里进行路由 + * @param values + * @param key + * @return + */ + String routeServer(List values,String key) ; +} +``` + +其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息即可。 + +而对于一致性 hash 算法来说也是只需要实现这个接口,同时在这个接口中选择使用 `SortArrayMapConsistentHash` 还是 `TreeMapConsistentHash` 即可。 + +![](https://i.loli.net/2019/05/08/5cd1beb35b595.jpg) + +这里还有一个 `setHash` 的方法,入参是 AbstractConsistentHash;这就是用于客户端指定需要使用具体的那种数据结构。 + +--- + +而对于之前就存在的轮询策略来说也是同样的实现 `RouteHandle` 接口。 + +![](https://i.loli.net/2019/05/08/5cd1beb3dbd86.jpg) + +这里我只是把之前的代码搬过来了而已。 + + +接下来看看客户端到底是如何使用以及如何选择使用哪种算法。 + +> 为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。 + +![](https://i.loli.net/2019/05/08/5cd1beb476ca8.jpg) + +1. 如果想使用原有的轮询策略,就配置实现了 `RouteHandle` 接口的轮询策略的全限定名。 +2. 如果想使用一致性 hash 的策略,也只需要配置实现了 `RouteHandle` 接口的一致性 hash 算法的全限定名。 +3. 当然目前的一致性 hash 也有多种实现,所以一旦配置为一致性 hash 后就需要再加一个配置用于决定使用 `SortArrayMapConsistentHash` 还是 `TreeMapConsistentHash` 或是自定义的其他方案。 +4. 同样的也是需要配置继承了 `AbstractConsistentHash` 的全限定名。 + + +不管这里的策略如何改变,在使用处依然保持不变。 + +只需要注入 `RouteHandle`,调用它的 `routeServer` 方法。 + +```java +@Autowired +private RouteHandle routeHandle ; +String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId())); + +``` + +既然使用了注入,那其实这个策略切换的过程就在创建 `RouteHandle bean` 的时候完成的。 + +![](https://i.loli.net/2019/05/08/5cd1beb4d7cd2.jpg) + +也比较简单,需要读取之前的配置文件来动态生成具体的实现类,主要是利用反射完成的。 + +这样处理之后就比较灵活了,比如想新建一个随机的路由策略也是同样的套路;到时候只需要修改配置即可。 + +> 感兴趣的朋友也可提交 PR 来新增更多的路由策略。 + +# 总结 + +希望看到这里的朋友能对这个算法有所理解,同时对一些设计模式在实际的使用也能有所帮助。 + +相信在金三银四的面试过程中还是能让面试官眼前一亮的,毕竟根据我这段时间的面试过程来看听过这个名词的都在少数😂(可能也是和候选人都在 1~3 年这个层级有关)。 + +以上所有源码: + +[https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) + +如果本文对你有所帮助还请不吝转发。 diff --git a/docs/algorithm/guava-bloom-filter.md b/docs/algorithm/guava-bloom-filter.md new file mode 100755 index 00000000..bb705451 --- /dev/null +++ b/docs/algorithm/guava-bloom-filter.md @@ -0,0 +1,401 @@ + +![](https://i.loli.net/2019/06/26/5d1393217483718447.jpg) + +# 前言 + +最近有朋友问我这么一个面试题目: + +> 现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。 + +需求其实很清晰,只是要判断一个数据是否存在即可。 + +但这里有一个比较重要的前提:**非常庞大的数据**。 + + +# 常规实现 + +先不考虑这个条件,我们脑海中出现的第一种方案是什么? + +我想大多数想到的都是用 `HashMap` 来存放数据,因为它的写入查询的效率都比较高。 + +写入和判断元素是否存在都有对应的 `API`,所以实现起来也比较简单。 + +为此我写了一个单测,利用 `HashSet` 来存数据(底层也是 `HashMap` );同时为了后面的对比将堆内存写死: + +```java +-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError +``` + +为了方便调试加入了 `GC` 日志的打印,以及内存溢出后 `Dump` 内存。 + +```java + @Test + public void hashMapTest(){ + long star = System.currentTimeMillis(); + + Set hashset = new HashSet<>(100) ; + for (int i = 0; i < 100; i++) { + hashset.add(i) ; + } + Assert.assertTrue(hashset.contains(1)); + Assert.assertTrue(hashset.contains(2)); + Assert.assertTrue(hashset.contains(3)); + + long end = System.currentTimeMillis(); + System.out.println("执行时间:" + (end - star)); + } +``` + +当我只写入 100 条数据时自然是没有问题的。 + +还是在这个基础上,写入 1000W 数据试试: + +![](https://i.loli.net/2019/06/26/5d139321d4ee464729.jpg) + +执行后马上就内存溢出。 + +![](https://i.loli.net/2019/06/26/5d139322c054a77994.jpg) + +可见在内存有限的情况下我们不能使用这种方式。 + +实际情况也是如此;既然要判断一个数据是否存在于集合中,考虑的算法的效率以及准确性肯定是要把数据全部 `load` 到内存中的。 + + +# Bloom Filter + +基于上面分析的条件,要实现这个需求最需要解决的是`如何将庞大的数据 load 到内存中。` + +而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。 + +伟大的科学家们已经帮我们想到了这样的需求。 + +`Burton Howard Bloom` 在 1970 年提出了一个叫做 `Bloom Filter`(中文翻译:布隆过滤)的算法。 + +它主要就是用于解决判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。 + +所以在这个场景下在合适不过了。 + +## Bloom Filter 原理 + +下面来分析下它的实现原理。 + +> 官方的说法是:它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。 + +听起来比较绕,但是通过一个图就比较容易理解了。 + +![](https://i.loli.net/2019/06/26/5d1393234976c40998.jpg) + +如图所示: + +- 首先需要初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。 +- 当写入一个 `A1=1000` 的数据时,需要进行 H 次 `hash` 函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的 `HashCode` 与 L 取模后定位到 0、2 处,将该处的值设为 1。 +- `A2=2000` 也是同理计算后将 `4、7` 位置设为 1。 +- 当有一个 `B1=1000` 需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为 `B1=1000` 存在于集合中。 +- 当有一个 `B2=3000` 时,也是同理。第一次 Hash 定位到 `index=4` 时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到 `index=5` 的值为 0,所以认为 `B2=3000` 不存在于集合中。 + +整个的写入、查询的流程就是这样,汇总起来就是: + +> 对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。 +> 一旦其中的有一位为 **0** 则认为数据**肯定不存在于集合**,否则数据**可能存在于集合中**。 + +所以布隆过滤有以下几个特点: + +1. 只要返回数据不存在,则肯定不存在。 +2. 返回数据存在,但只能是大概率存在。 +3. 同时不能清除其中的数据。 + +第一点应该都能理解,重点解释下 2、3 点。 + +为什么返回存在的数据却是可能存在呢,这其实也和 `HashMap` 类似。 + +在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 `A、B` 两个数据最后定位到的位置是一模一样的。 + +这时拿 B 进行查询时那自然就是误报了。 + +删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。 + +基于以上的 `Hash` 冲突的前提,所以 `Bloom Filter` 有一定的误报率,这个误报率和 `Hash` 算法的次数 H,以及数组长度 L 都是有关的。 + + +# 自己实现一个布隆过滤 + +算法其实很简单不难理解,于是利用 `Java` 实现了一个简单的雏形。 + +```java +public class BloomFilters { + + /** + * 数组长度 + */ + private int arraySize; + + /** + * 数组 + */ + private int[] array; + + public BloomFilters(int arraySize) { + this.arraySize = arraySize; + array = new int[arraySize]; + } + + /** + * 写入数据 + * @param key + */ + public void add(String key) { + int first = hashcode_1(key); + int second = hashcode_2(key); + int third = hashcode_3(key); + + array[first % arraySize] = 1; + array[second % arraySize] = 1; + array[third % arraySize] = 1; + + } + + /** + * 判断数据是否存在 + * @param key + * @return + */ + public boolean check(String key) { + int first = hashcode_1(key); + int second = hashcode_2(key); + int third = hashcode_3(key); + + int firstIndex = array[first % arraySize]; + if (firstIndex == 0) { + return false; + } + + int secondIndex = array[second % arraySize]; + if (secondIndex == 0) { + return false; + } + + int thirdIndex = array[third % arraySize]; + if (thirdIndex == 0) { + return false; + } + + return true; + + } + + + /** + * hash 算法1 + * @param key + * @return + */ + private int hashcode_1(String key) { + int hash = 0; + int i; + for (i = 0; i < key.length(); ++i) { + hash = 33 * hash + key.charAt(i); + } + return Math.abs(hash); + } + + /** + * hash 算法2 + * @param data + * @return + */ + private int hashcode_2(String data) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (int i = 0; i < data.length(); i++) { + hash = (hash ^ data.charAt(i)) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + return Math.abs(hash); + } + + /** + * hash 算法3 + * @param key + * @return + */ + private int hashcode_3(String key) { + int hash, i; + for (hash = 0, i = 0; i < key.length(); ++i) { + hash += key.charAt(i); + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return Math.abs(hash); + } +} +``` + +1. 首先初始化了一个 int 数组。 +2. 写入数据的时候进行三次 `hash` 运算,同时把对应的位置置为 1。 +3. 查询时同样的三次 `hash` 运算,取到对应的值,一旦值为 0 ,则认为数据不存在。 + +实现逻辑其实就和上文描述的一样。 + +下面来测试一下,同样的参数: + +```java +-Xms64m -Xmx64m -XX:+PrintHeapAtGC +``` + +```java + @Test + public void bloomFilterTest(){ + long star = System.currentTimeMillis(); + BloomFilters bloomFilters = new BloomFilters(10000000) ; + for (int i = 0; i < 10000000; i++) { + bloomFilters.add(i + "") ; + } + Assert.assertTrue(bloomFilters.check(1+"")); + Assert.assertTrue(bloomFilters.check(2+"")); + Assert.assertTrue(bloomFilters.check(3+"")); + Assert.assertTrue(bloomFilters.check(999999+"")); + Assert.assertFalse(bloomFilters.check(400230340+"")); + long end = System.currentTimeMillis(); + System.out.println("执行时间:" + (end - star)); + } +``` + +执行结果如下: + +![](https://i.loli.net/2019/06/26/5d139324062c317953.jpg) + +只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。 + +--- + +![](https://i.loli.net/2019/06/26/5d139324c314174414.jpg) + +当让我把数组长度缩小到了 100W 时就出现了一个误报,`400230340` 这个数明明没在集合里,却返回了存在。 + +这也体现了 `Bloom Filter` 的误报率。 + +我们提高数组长度以及 `hash` 计算次数可以降低误报率,但相应的 `CPU、内存`的消耗就会提高;这就需要根据业务需要自行权衡。 + + +# Guava 实现 + +![](https://i.loli.net/2019/06/26/5d13932a2cbfa10136.jpg) + +刚才的方式虽然实现了功能,也满足了大量数据。但其实观察 `GC` 日志非常频繁,同时老年代也使用了 90%,接近崩溃的边缘。 + +总的来说就是内存利用率做的不好。 + +其实 Google Guava 库中也实现了该算法,下面来看看业界权威的实现。 + +```java +-Xms64m -Xmx64m -XX:+PrintHeapAtGC +``` + +--- + +```java + @Test + public void guavaTest() { + long star = System.currentTimeMillis(); + BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 10000000, + 0.01); + + for (int i = 0; i < 10000000; i++) { + filter.put(i); + } + + Assert.assertTrue(filter.mightContain(1)); + Assert.assertTrue(filter.mightContain(2)); + Assert.assertTrue(filter.mightContain(3)); + Assert.assertFalse(filter.mightContain(10000000)); + long end = System.currentTimeMillis(); + System.out.println("执行时间:" + (end - star)); + } +``` + + +也是同样写入了 1000W 的数据,执行没有问题。 + +![](https://i.loli.net/2019/06/26/5d13932aa240376389.jpg) + +观察 GC 日志会发现没有一次 `fullGC`,同时老年代的使用率很低。和刚才的一对比这里明显的要好上很多,也可以写入更多的数据。 + +## 源码分析 + +那就来看看 `Guava` 它是如何实现的。 + +构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。 +我这里的测试 demo 分别是 1000W 以及 0.01。 + +![](https://i.loli.net/2019/06/26/5d13932b7b19733775.jpg) + +`Guava` 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 `numBits` 以及需要计算几次 Hash 函数 `numHashFunctions` 。 + +这个算法计算规则可以参考维基百科。 + +### put 写入函数 + +真正存放数据的 `put` 函数如下: + +![](https://i.loli.net/2019/06/26/5d13932bf409b70520.jpg) + +- 根据 `murmur3_128` 方法的到一个 128 位长度的 `byte[]`。 +- 分别取高低 8 位的到两个 `hash` 值。 +- 再根据初始化时的到的执行 `hash` 的次数进行 `hash` 运算。 + + +```java +bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); +``` + +其实也是 `hash取模`拿到 `index` 后去赋值 1. + +重点是 `bits.set()` 方法。 + +![](https://i.loli.net/2019/06/26/5d13932c8cb9133569.jpg) + +其实 set 方法是 `BitArray` 中的一个函数,`BitArray` 就是真正存放数据的底层数据结构。 + +利用了一个 `long[] data` 来存放数据。 + +所以 `set()` 时候也是对这个 `data` 做处理。 + +![](https://i.loli.net/2019/06/26/5d13932d2faa229373.jpg) + +- 在 `set` 之前先通过 `get()` 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。 +- 接下来就是通过位运算进行`位或赋值`。 +- `get()` 方法的计算逻辑和 set 类似,只要判断为 0 就直接返回存在该值。 + +### mightContain 是否存在函数 + +![](https://i.loli.net/2019/06/26/5d13932db4fcf97015.jpg) + +前面几步的逻辑都是类似的,只是调用了刚才的 `get()` 方法判断元素是否存在而已。 + + +# 总结 + +布隆过滤的应用还是蛮多的,比如数据库、爬虫、防缓存击穿等。 + +特别是需要精确知道某个数据不存在时做点什么事情就非常适合布隆过滤。 + +这段时间的研究发现算法也挺有意思的,后续应该会继续分享一些类似的内容。 + +如果对你有帮助那就分享一下吧。 + +本问的示例代码参考这里: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/blob/master/src/test/java/com/crossoverjie/algorithm/BloomFiltersTest.java) + + + +**你的点赞与分享是对我最大的支持** diff --git a/docs/architecture-design/Spike.md b/docs/architecture-design/Spike.md new file mode 100644 index 00000000..35142c69 --- /dev/null +++ b/docs/architecture-design/Spike.md @@ -0,0 +1,32 @@ +# 设计一个秒杀系统 + +**具体实现参考 [秒杀架构实践](architecture-design/seconds-kill.md)** + +主要做到以下两点: + +- 尽量将请求过滤在上游。 +- 尽可能的利用缓存(大多数场景下都是**查多于写**)。 + +常用的系统分层结构: + +


+ +针对于浏览器端,可以使用 JS 进行请求过滤,比如五秒钟之类只能点一次抢购按钮,五秒钟只能允许请求一次后端服务。(APP 同理) + +这样其实就可以过滤掉大部分普通用户。 + +但是防不住直接抓包循环调用。这种情况可以最简单的处理:在`Web层`通过限制一个 UID 五秒之类的请求服务层的次数(可利用 Redis 实现)。 + +但如果是真的有 10W 个不同的 UID 来请求,比如黑客抓肉鸡的方式。 + +这种情况可以在`服务层` 针对于写请求使用请求队列,再通过限流算法([限流算法](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Limiting.md))每秒钟放一部分请求到队列。 + +对于读请求则尽量使用缓存,可以提前将数据准备好,不管是 `Redis` 还是其他缓存中间件效率都是非常高的。 + +> ps : 刷新缓存情况,比如库存扣除成功这种情况不用马上刷新缓存,如果库存扣到了 0 再刷新缓存。因为大多数用户都只关心是否有货,并不关心现在还剩余多少。 + +## 总结 + +- 如果流量巨大,导致各个层的压力都很大可以适当的加机器横向扩容。如果加不了机器那就只有放弃流量直接返回失败。快速失败非常重要,至少可以保证系统的可用性。 +- 业务分批执行:对于下单、付款等操作可以异步执行提高吞吐率。 +- 主要目的就是尽量少的请求直接访问到 `DB`。 diff --git a/docs/architecture-design/million-sms-push.md b/docs/architecture-design/million-sms-push.md new file mode 100755 index 00000000..73a5421b --- /dev/null +++ b/docs/architecture-design/million-sms-push.md @@ -0,0 +1,360 @@ +# 设计一个百万级的消息推送系统 + +![business-communication-computer-261706.jpg](https://i.loli.net/2018/09/23/5ba7ae180e8eb.jpg) + +# 前言 + +首先迟到的祝大家中秋快乐。 + +最近一周多没有更新了。其实我一直想憋一个大招,分享一些大家感兴趣的干货。 + +鉴于最近我个人的工作内容,于是利用这三天小长假憋了一个出来(其实是玩了两天🤣)。 + + +--- + +先简单说下本次的主题,由于我最近做的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 + +最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 + +所以本次分享的内容不但可以满足物联网领域同时还支持以下场景: + +- 基于 `WEB` 的聊天系统(点对点、群聊)。 +- `WEB` 应用中需求服务端推送的场景。 +- 基于 SDK 的消息推送平台。 + +# 技术选型 + +要满足大量的连接数、同时支持双全工通信,并且性能也得有保障。 + +在 Java 技术栈中进行选型首先自然是排除掉了传统 `IO`。 + +那就只有选 NIO 了,在这个层面其实选择也不多,考虑到社区、资料维护等方面最终选择了 Netty。 + +最终的架构图如下: + +![](https://i.loli.net/2019/06/26/5d1393a70683166304.jpg) + + +现在看着蒙没关系,下文一一介绍。 + +# 协议解析 + +既然是一个消息系统,那自然得和客户端定义好双方的协议格式。 + +常见和简单的是 HTTP 协议,但我们的需求中有一项需要是双全工的交互方式,同时 HTTP 更多的是服务于浏览器。我们需要的是一个更加精简的协议,减少许多不必要的数据传输。 + +因此我觉得最好是在满足业务需求的情况下定制自己的私有协议,在我这个场景下其实有标准的物联网协议。 + +如果是其他场景可以借鉴现在流行的 `RPC` 框架定制私有协议,使得双方通信更加高效。 + +不过根据这段时间的经验来看,不管是哪种方式都得在协议中预留安全相关的位置。 + +协议相关的内容就不过讨论了,更多介绍具体的应用。 + +# 简单实现 + +首先考虑如何实现功能,再来思考百万连接的情况。 + +## 注册鉴权 + +在做真正的消息上、下行之前首先要考虑的就是鉴权问题。 + +就像你使用微信一样,第一步怎么也得是登录吧,不能无论是谁都可以直接连接到平台。 + +所以第一步得是注册才行。 + +如上面架构图中的 `注册/鉴权` 模块。通常来说都需要客户端通过 `HTTP` 请求传递一个唯一标识,后台鉴权通过之后会响应一个 `token`,并将这个 `token` 和客户端的关系维护到 `Redis` 或者是 DB 中。 + +客户端将这个 token 也保存到本地,今后的每一次请求都得带上这个 token。一旦这个 token 过期,客户端需要再次请求获取 token。 + +鉴权通过之后客户端会直接通过`TCP 长连接`到图中的 `push-server` 模块。 + +这个模块就是真正处理消息的上、下行。 + +## 保存通道关系 + +在连接接入之后,真正处理业务之前需要将当前的客户端和 Channel 的关系维护起来。 + +假设客户端的唯一标识是手机号码,那就需要把手机号码和当前的 Channel 维护到一个 Map 中。 + +这点和之前 [SpringBoot 整合长连接心跳机制](http://t.cn/EPcNHFZ) 类似。 + +![](https://i.loli.net/2019/06/26/5d1393a5e41f832920.jpg) + +同时为了可以通过 Channel 获取到客户端唯一标识(手机号码),还需要在 Channel 中设置对应的属性: + +```java +public static void putClientId(Channel channel, String clientId) { + channel.attr(CLIENT_ID).set(clientId); +} +``` + +获取时手机号码时: + +```java +public static String getClientId(Channel channel) { + return (String)getAttribute(channel, CLIENT_ID); +} +``` + +这样当我们客户端下线的时便可以记录相关日志: + +```java +String telNo = NettyAttrUtil.getClientId(ctx.channel()); +NettySocketHolder.remove(telNo); +log.info("客户端下线,TelNo=" + telNo); +``` + +> 这里有一点需要注意:存放客户端与 Channel 关系的 Map 最好是预设好大小(避免经常扩容),因为它将是使用最为频繁同时也是占用内存最大的一个对象。 + +## 消息上行 + +接下来则是真正的业务数据上传,通常来说第一步是需要判断上传消息输入什么业务类型。 + +在聊天场景中,有可能上传的是文本、图片、视频等内容。 + +所以我们得进行区分,来做不同的处理;这就和客户端协商的协议有关了。 + +- 可以利用消息头中的某个字段进行区分。 +- 更简单的就是一个 `JSON` 消息,拿出一个字段用于区分不同消息。 + +不管是哪种只有可以区分出来即可。 + +### 消息解析与业务解耦 + +消息可以解析之后便是处理业务,比如可以是写入数据库、调用其他接口等。 + +我们都知道在 Netty 中处理消息一般是在 `channelRead()` 方法中。 + +![](https://i.loli.net/2019/06/26/5d1393a6126d530691.jpg) + +在这里可以解析消息,区分类型。 + +但如果我们的业务逻辑也写在里面,那这里的内容将是巨多无比。 + +甚至我们分为好几个开发来处理不同的业务,这样将会出现许多冲突、难以维护等问题。 + +所以非常有必要将消息解析与业务处理完全分离开来。 + + +> 这时面向接口编程就发挥作用了。 + +这里的核心代码和 [「造个轮子」——cicada(轻量级 WEB 框架)](https://crossoverjie.top/2018/09/03/wheel/cicada1/#%E9%85%8D%E7%BD%AE%E4%B8%9A%E5%8A%A1-Action) 是一致的。 + +都是先定义一个接口用于处理业务逻辑,然后在解析消息之后通过反射创建具体的对象执行其中的`处理函数`即可。 + +这样不同的业务、不同的开发人员只需要实现这个接口同时实现自己的业务逻辑即可。 + +伪代码如下: + +![](https://i.loli.net/2019/06/26/5d1393a638fa183367.jpg) + +![](https://i.loli.net/2019/06/26/5d1393a68a53a59900.jpg) + +想要了解 cicada 的具体实现请点击这里: + +[https://github.com/TogetherOS/cicada](https://github.com/TogetherOS/cicada) + + +上行还有一点需要注意;由于是基于长连接,所以客户端需要定期发送心跳包用于维护本次连接。同时服务端也会有相应的检查,N 个时间间隔没有收到消息之后将会主动断开连接节省资源。 + +这点使用一个 `IdleStateHandler` 就可实现,更多内容可以查看 [Netty(一) SpringBoot 整合长连接心跳机制](https://crossoverjie.top/2018/05/24/netty/Netty(1)TCP-Heartbeat/#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%BF%83%E8%B7%B3)。 + + + +## 消息下行 + +有了上行自然也有下行。比如在聊天的场景中,有两个客户端连上了 `push-server`,他们直接需要点对点通信。 + +这时的流程是: + +- A 将消息发送给服务器。 +- 服务器收到消息之后,得知消息是要发送给 B,需要在内存中找到 B 的 Channel。 +- 通过 B 的 Channel 将 A 的消息转发下去。 + +这就是一个下行的流程。 + +甚至管理员需要给所有在线用户发送系统通知也是类似: + +遍历保存通道关系的 Map,挨个发送消息即可。这也是之前需要存放到 Map 中的主要原因。 + +伪代码如下: + +![](https://i.loli.net/2019/06/26/5d1393a6da88584453.jpg) + +具体可以参考: + +[https://github.com/crossoverJie/netty-action/](https://github.com/crossoverJie/netty-action/) + + +# 分布式方案 + +单机版的实现了,现在着重讲讲如何实现百万连接。 + +百万连接其实只是一个形容词,更多的是想表达如何来实现一个分布式的方案,可以灵活的水平拓展从而能支持更多的连接。 + +再做这个事前首先得搞清楚我们单机版的能支持多少连接。影响这个的因素就比较多了。 + +- 服务器自身配置。内存、CPU、网卡、Linux 支持的最大文件打开数等。 +- 应用自身配置,因为 Netty 本身需要依赖于堆外内存,但是 JVM 本身也是需要占用一部分内存的,比如存放通道关系的大 `Map`。这点需要结合自身情况进行调整。 + +结合以上的情况可以测试出单个节点能支持的最大连接数。 + +单机无论怎么优化都是有上限的,这也是分布式主要解决的问题。 + +## 架构介绍 + +在将具体实现之前首先得讲讲上文贴出的整体架构图。 + +![](https://i.loli.net/2019/06/26/5d1393a70683166304.jpg) + +先从左边开始。 + +上文提到的 `注册鉴权` 模块也是集群部署的,通过前置的 Nginx 进行负载。之前也提过了它主要的目的是来做鉴权并返回一个 token 给客户端。 + +但是 `push-server` 集群之后它又多了一个作用。那就是得返回一台可供当前客户端使用的 `push-server`。 + +右侧的 `平台` 一般指管理平台,它可以查看当前的实时在线数、给指定客户端推送消息等。 + +推送消息则需要经过一个推送路由(`push-server`)找到真正的推送节点。 + +其余的中间件如:Redis、Zookeeper、Kafka、MySQL 都是为了这些功能所准备的,具体看下面的实现。 + +## 注册发现 + +首先第一个问题则是 `注册发现`,`push-server` 变为多台之后如何给客户端选择一台可用的节点是第一个需要解决的。 + +这块的内容其实已经在 [分布式(一) 搞定服务注册与发现](https://crossoverjie.top/2018/08/27/distributed/distributed-discovery-zk/) 中详细讲过了。 + +所有的 `push-server` 在启动时候需要将自身的信息注册到 Zookeeper 中。 + +`注册鉴权` 模块会订阅 Zookeeper 中的节点,从而可以获取最新的服务列表。结构如下: + +![](https://i.loli.net/2019/06/26/5d1393a7327b184532.jpg) + +以下是一些伪代码: + +应用启动注册 Zookeeper。 + +![](https://i.loli.net/2019/06/26/5d1393a7624a976369.jpg) + +![](https://i.loli.net/2019/06/26/5d1393c2d2a1b31176.jpg) + +对于`注册鉴权`模块来说只需要订阅这个 Zookeeper 节点: + +![](https://i.loli.net/2019/06/26/5d1393ad257fe34873.jpg) + +### 路由策略 + +既然能获取到所有的服务列表,那如何选择一台刚好合适的 `push-server` 给客户端使用呢? + +这个过程重点要考虑以下几点: + +- 尽量保证各个节点的连接均匀。 +- 增删节点是否要做 Rebalance。 + +首先保证均衡有以下几种算法: + +- 轮询。挨个将各个节点分配给客户端。但会出现新增节点分配不均匀的情况。 +- Hash 取模的方式。类似于 HashMap,但也会出现轮询的问题。当然也可以像 HashMap 那样做一次 Rebalance,让所有的客户端重新连接。不过这样会导致所有的连接出现中断重连,代价有点大。 +- 由于 Hash 取模方式的问题带来了[`一致性 Hash`算法](https://crossoverjie.top/%2F2018%2F01%2F08%2FConsistent-Hash%2F),但依然会有一部分的客户端需要 Rebalance。 +- 权重。可以手动调整各个节点的负载情况,甚至可以做成自动的,基于监控当某些节点负载较高就自动调低权重,负载较低的可以提高权重。 + +还有一个问题是: + +> 当我们在重启部分应用进行升级时,在该节点上的客户端怎么处理? + +由于我们有心跳机制,当心跳不通之后就可以认为该节点出现问题了。那就得重新请求`注册鉴权`模块获取一个可用的节点。在弱网情况下同样适用。 + +如果这时客户端正在发送消息,则需要将消息保存到本地等待获取到新的节点之后再次发送。 + +## 有状态连接 + +在这样的场景中不像是 HTTP 那样是无状态的,我们得明确的知道各个客户端和连接的关系。 + +在上文的单机版中我们将这个关系保存到本地的缓存中,但在分布式环境中显然行不通了。 + +比如在平台向客户端推送消息的时候,它得首先知道这个客户端的通道保存在哪台节点上。 + +借助我们以前的经验,这样的问题自然得引入一个第三方中间件用来存放这个关系。 + +也就是架构图中的存放`路由关系的 Redis`,在客户端接入 `push-server` 时需要将当前客户端唯一标识和服务节点的 `ip+port` 存进 `Redis`。 + +同时在客户端下线时候得在 Redis 中删掉这个连接关系。 + + +> 这样在理想情况下各个节点内存中的 map 关系加起来应该正好等于 Redis 中的数据。 + +伪代码如下: + +![](https://i.loli.net/2019/06/26/5d1393ad5e2e263573.jpg) + +这里存放路由关系的时候会有并发问题,最好是换为一个 `lua` 脚本。 + +## 推送路由 + +设想这样一个场景:管理员需要给最近注册的客户端推送一个系统消息会怎么做? + +> 结合架构图 + +假设这批客户端有 10W 个,首先我们需要将这批号码通过`平台`下的 `Nginx` 下发到一个推送路由中。 + +为了提高效率甚至可以将这批号码再次分散到每个 `push-route` 中。 + +拿到具体号码之后再根据号码的数量启动多线程的方式去之前的路由 Redis 中获取客户端所对应的 `push-server`。 + +再通过 HTTP 的方式调用 `push-server` 进行真正的消息下发(Netty 也很好的支持 HTTP 协议)。 + +推送成功之后需要将结果更新到数据库中,不在线的客户端可以根据业务再次推送等。 + +## 消息流转 + +也许有些场景对于客户端上行的消息非常看重,需要做持久化,并且消息量非常大。 + +在 `push-sever` 做业务显然不合适,这时完全可以选择 Kafka 来解耦。 + +将所有上行的数据直接往 Kafka 里丢后就不管了。 + +再由消费程序将数据取出写入数据库中即可。 + +其实这块内容也很值得讨论,可以先看这篇了解下:[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + +后续谈到 Kafka 再做详细介绍。 + +# 分布式问题 + +分布式解决了性能问题但却带来了其他麻烦。 + +## 应用监控 + +比如如何知道线上几十个 `push-server` 节点的健康状况? + +这时就得监控系统发挥作用了,我们需要知道各个节点当前的内存使用情况、GC。 + +以及操作系统本身的内存使用,毕竟 Netty 大量使用了堆外内存。 + +同时需要监控各个节点当前的在线数,以及 Redis 中的在线数。理论上这两个数应该是相等的。 + +这样也可以知道系统的使用情况,可以灵活的维护这些节点数量。 + +## 日志处理 + +日志记录也变得异常重要了,比如哪天反馈有个客户端一直连不上,你得知道问题出在哪里。 + + +最好是给每次请求都加上一个 traceID 记录日志,这样就可以通过这个日志在各个节点中查看到底是卡在了哪里。 + +以及 ELK 这些工具都得用起来才行。 + +# 总结 + +本次是结合我日常经验得出的,有些坑可能在工作中并没有踩到,所有还会有一些遗漏的地方。 + +就目前来看想做一个稳定的推送系统其实是比较麻烦的,其中涉及到的点非常多,只有真正做过之后才会知道。 + +看完之后觉得有帮助的还请不吝转发分享。 + +**欢迎关注公众号一起交流:** + +![](https://i.loli.net/2019/06/26/5d1393ad8d38d78633.jpg) diff --git a/docs/architecture-design/seconds-kill.md b/docs/architecture-design/seconds-kill.md new file mode 100644 index 00000000..aa8061d7 --- /dev/null +++ b/docs/architecture-design/seconds-kill.md @@ -0,0 +1,694 @@ +![](https://i.loli.net/2019/05/08/5cd1d713e19ed.jpg) + +## 前言 + +之前在 [JCSprout](architecture-design/Spike.md) 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。 + +> 本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果,文章较长请准备好瓜子板凳(liushuizhang😂)。 + +本文所有涉及的代码: + +- [https://github.com/crossoverJie/SSM](https://github.com/crossoverJie/SSM) +- [https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) + +最终架构图: + +![系统架构设计.png](https://i.loli.net/2018/05/08/5af079ea8618b.png) + + + +先简单根据这个图谈下请求的流转,因为后面不管怎么改进这个都是没有变的。 + +- 前端请求进入 `web` 层,对应的代码就是 `controller`。 +- 之后将真正的库存校验、下单等请求发往 `Service` 层(其中 RPC 调用依然采用的 `dubbo`,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 [基于dubbo的分布式架构](https://crossoverjie.top/%2F2017%2F04%2F07%2FSSM11%2F))。 +- `Service` 层再对数据进行落地,下单完成。 + + +## 无限制 + +其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步: + +- 校验库存 +- 扣库存 +- 创建订单 +- 支付 + +基于上文的架构所以我们有了以下实现: + +先看看实际项目的结构: + +![](https://i.loli.net/2019/05/08/5cd1d71693bb0.jpg) + +还是和以前一样: + +- 提供出一个 `API` 用于 `Service` 层实现,以及 `web` 层消费。 +- web 层简单来说就是一个 `SpringMVC`。 +- `Service` 层则是真正的数据落地。 +- `SSM-SECONDS-KILL-ORDER-CONSUMER` 则是后文会提到的 `Kafka` 消费。 + + +数据库也是只有简单的两张表模拟下单: + +```sql +CREATE TABLE `stock` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', + `count` int(11) NOT NULL COMMENT '库存', + `sale` int(11) NOT NULL COMMENT '已售', + `version` int(11) NOT NULL COMMENT '乐观锁,版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + + +CREATE TABLE `stock_order` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `sid` int(11) NOT NULL COMMENT '库存ID', + `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8; +``` + +web 层 `controller` 实现: + + +```java + + @Autowired + private StockService stockService; + + @Autowired + private OrderService orderService; + + @RequestMapping("/createWrongOrder/{sid}") + @ResponseBody + public String createWrongOrder(@PathVariable int sid) { + logger.info("sid=[{}]", sid); + int id = 0; + try { + id = orderService.createWrongOrder(sid); + } catch (Exception e) { + logger.error("Exception",e); + } + return String.valueOf(id); + } +``` + +其中 web 作为一个消费者调用看 `OrderService` 提供出来的 dubbo 服务。 + +Service 层,`OrderService` 实现: + +首先是对 API 的实现(会在 API 提供出接口): + +```java +@Service +public class OrderServiceImpl implements OrderService { + + @Resource(name = "DBOrderService") + private com.crossoverJie.seconds.kill.service.OrderService orderService ; + + @Override + public int createWrongOrder(int sid) throws Exception { + return orderService.createWrongOrder(sid); + } +} +``` + +这里只是简单调用了 `DBOrderService` 中的实现,DBOrderService 才是真正的数据落地,也就是写数据库了。 + +DBOrderService 实现: + +```java +Transactional(rollbackFor = Exception.class) +@Service(value = "DBOrderService") +public class OrderServiceImpl implements OrderService { + @Resource(name = "DBStockService") + private com.crossoverJie.seconds.kill.service.StockService stockService; + + @Autowired + private StockOrderMapper orderMapper; + + @Override + public int createWrongOrder(int sid) throws Exception{ + + //校验库存 + Stock stock = checkStock(sid); + + //扣库存 + saleStock(stock); + + //创建订单 + int id = createOrder(stock); + + return id; + } + + private Stock checkStock(int sid) { + Stock stock = stockService.getStockById(sid); + if (stock.getSale().equals(stock.getCount())) { + throw new RuntimeException("库存不足"); + } + return stock; + } + + private int saleStock(Stock stock) { + stock.setSale(stock.getSale() + 1); + return stockService.updateStockById(stock); + } + + private int createOrder(Stock stock) { + StockOrder order = new StockOrder(); + order.setSid(stock.getId()); + order.setName(stock.getName()); + int id = orderMapper.insertSelective(order); + return id; + } + +} +``` + +> 预先初始化了 10 条库存。 + + +手动调用下 `createWrongOrder/1` 接口发现: + +库存表: +![](https://i.loli.net/2019/05/08/5cd1d7189c72f.jpg) + +订单表: +![](https://i.loli.net/2019/05/08/5cd1d721e9fd4.jpg) + +一切看起来都没有问题,数据也正常。 + +但是当用 `JMeter` 并发测试时: + +![](https://i.loli.net/2019/05/08/5cd1d7243c657.jpg) + +测试配置是:300个线程并发,测试两轮来看看数据库中的结果: + +![](https://i.loli.net/2019/05/08/5cd1d726cee79.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d72816d67.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d72b9f26a.jpg) + +请求都响应成功,库存确实也扣完了,但是订单却生成了 **124** 条记录。 + +这显然是典型的超卖现象。 + +> 其实现在再去手动调用接口会返回库存不足,但为时晚矣。 + + +## 乐观锁更新 + +怎么来避免上述的现象呢? + +最简单的做法自然是乐观锁了,这里不过多讨论这个,不熟悉的朋友可以看下[这篇](http://crossoverjie.top/%2F2017%2F07%2F09%2FSSM15%2F)。 + +来看看具体实现: + +> 其实其他的都没怎么改,主要是 Service 层。 + +```java + @Override + public int createOptimisticOrder(int sid) throws Exception { + + //校验库存 + Stock stock = checkStock(sid); + + //乐观锁更新库存 + saleStockOptimistic(stock); + + //创建订单 + int id = createOrder(stock); + + return id; + } + + private void saleStockOptimistic(Stock stock) { + int count = stockService.updateStockByOptimistic(stock); + if (count == 0){ + throw new RuntimeException("并发更新库存失败") ; + } + } +``` + +对应的 XML: + +```xml + + update stock + + sale = sale + 1, + version = version + 1, + + + WHERE id = #{id,jdbcType=INTEGER} + AND version = #{version,jdbcType=INTEGER} + + +``` + +同样的测试条件,我们再进行上面的测试 `/createOptimisticOrder/1`: + +![](https://i.loli.net/2019/05/08/5cd1d72dab853.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d730800b1.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d73324dd2.jpg) + +这次发现无论是库存订单都是 OK 的。 + +查看日志发现: + +![](https://i.loli.net//2019//05//08//5cd1daafb70bc.jpg) + +很多并发请求会响应错误,这就达到了效果。 + +### 提高吞吐量 + +为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 Service 都进行了横向扩展。 + +- web 利用 Nginx 进行负载。 +- Service 也是多台应用。 + +![](https://i.loli.net/2019/05/08/5cd1d752909b9.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d758c7714.jpg) + +再用 JMeter 测试时可以直观的看到效果。 + +> 由于我是在阿里云的一台小水管服务器进行测试的,加上配置不高、应用都在同一台,所以并没有完全体现出性能上的优势( `Nginx` 做负载转发时候也会增加额外的网络消耗)。 + +### shell 脚本实现简单的 CI + +由于应用多台部署之后,手动发版测试的痛苦相信经历过的都有体会。 + +这次并没有精力去搭建完整的 CI CD,只是写了一个简单的脚本实现了自动化部署,希望对这方面没有经验的同学带来一点启发: + +#### 构建 web + +```shell +#!/bin/bash + +# 构建 web 消费者 + +#read appname + +appname="consumer" +echo "input="$appname + +PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}') + +# 遍历杀掉 pid +for var in ${PID[@]}; +do + echo "loop pid= $var" + kill -9 $var +done + +echo "kill $appname success" + +cd .. + +git pull + +cd SSM-SECONDS-KILL + +mvn -Dmaven.test.skip=true clean package + +echo "build war success" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/webapps +echo "cp tomcat-dubbo-consumer-8083/webapps ok!" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/webapps +echo "cp tomcat-dubbo-consumer-7083-slave/webapps ok!" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.sh +echo "tomcat-dubbo-consumer-8083/bin/startup.sh success" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.sh +echo "tomcat-dubbo-consumer-7083-slave/bin/startup.sh success" + +echo "start $appname success" +``` + +#### 构建 Service + +```shell +# 构建服务提供者 + +#read appname + +appname="provider" + +echo "input="$appname + + +PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}') + +#if [ $? -eq 0 ]; then +# echo "process id:$PID" +#else +# echo "process $appname not exit" +# exit +#fi + +# 遍历杀掉 pid +for var in ${PID[@]}; +do + echo "loop pid= $var" + kill -9 $var +done + +echo "kill $appname success" + + +cd .. + +git pull + +cd SSM-SECONDS-KILL + +mvn -Dmaven.test.skip=true clean package + +echo "build war success" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/webapps + +echo "cp tomcat-dubbo-provider-8080/webapps ok!" + +cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/webapps + +echo "cp tomcat-dubbo-provider-7080-slave/webapps ok!" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.sh +echo "tomcat-dubbo-provider-8080/bin/startup.sh success" + +sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.sh +echo "tomcat-dubbo-provider-8080/bin/startup.sh success" + +echo "start $appname success" +``` + +之后每当我有更新,只需要执行这两个脚本就可以帮我自动构建。 + +都是最基础的 Linux 命令,相信大家都看得明白。 + + +## 乐观锁更新 + 分布式限流 + +上文的结果看似没有问题,其实还差得远呢。 + +这里只是模拟了 300 个并发没有问题,但是当请求达到了 3000 ,3W,300W 呢? + +虽说可以横向扩展可以支撑更多的请求。 + +但是能不能利用最少的资源解决问题呢? + +其实仔细分析下会发现: + +> 假设我的商品一共只有 10 个库存,那么无论你多少人来买其实最终也最多只有 10 人可以下单成功。 + +所以其中会有 `99%` 的请求都是无效的。 + +大家都知道:大多数应用数据库都是压倒骆驼的最后一根稻草。 + +通过 `Druid` 的监控来看看之前请求数据库的情况: + +因为 Service 是两个应用。 +![](https://i.loli.net/2019/05/08/5cd1d764221b5.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d7676e1d2.jpg) + +![](https://i.loli.net//2019//05//08//5cd1daeb0c306.jpg) + +数据库也有 20 多个连接。 + +怎么样来优化呢? +其实很容易想到的就是[分布式限流](http://crossoverjie.top/2018/04/28/sbc/sbc7-Distributed-Limit/)。 + + +我们将并发控制在一个可控的范围之内,然后快速失败这样就能最大程度的保护系统。 + +### distributed-redis-tool ⬆️v1.0.3 + +为此还对 [https://github.com/crossoverJie/distributed-redis-tool](https://github.com/crossoverJie/distributed-redis-tool) 进行了小小的升级。 + +因为加上该组件之后所有的请求都会经过 Redis,所以对 Redis 资源的使用也是要非常小心。 + +#### API 更新 + +修改之后的 API 如下: + +```java +@Configuration +public class RedisLimitConfig { + + private Logger logger = LoggerFactory.getLogger(RedisLimitConfig.class); + + @Value("${redis.limit}") + private int limit; + + + @Autowired + private JedisConnectionFactory jedisConnectionFactory; + + @Bean + public RedisLimit build() { + RedisLimit redisLimit = new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE) + .limit(limit) + .build(); + + return redisLimit; + } +} +``` + +这里构建器改用了 `JedisConnectionFactory`,所以得配合 Spring 来一起使用。 + +并在初始化时显示传入 Redis 是以集群方式部署还是单机(强烈建议集群,限流之后对 Redis 还是有一定的压力)。 + +##### 限流实现 + +既然 API 更新了,实现自然也要修改: + +```java + /** + * limit traffic + * @return if true + */ + public boolean limit() { + + //get connection + Object connection = getConnection(); + + Object result = limitRequest(connection); + + if (FAIL_CODE != (Long) result) { + return true; + } else { + return false; + } + } + + private Object limitRequest(Object connection) { + Object result = null; + String key = String.valueOf(System.currentTimeMillis() / 1000); + if (connection instanceof Jedis){ + result = ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + ((Jedis) connection).close(); + }else { + result = ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + try { + ((JedisCluster) connection).close(); + } catch (IOException e) { + logger.error("IOException",e); + } + } + return result; + } + + private Object getConnection() { + Object connection ; + if (type == RedisToolsConstant.SINGLE){ + RedisConnection redisConnection = jedisConnectionFactory.getConnection(); + connection = redisConnection.getNativeConnection(); + }else { + RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection(); + connection = clusterConnection.getNativeConnection() ; + } + return connection; + } +``` + +如果是原生的 Spring 应用得采用 `@SpringControllerLimit(errorCode = 200)` 注解。 + +实际使用如下: + +web 端: + +```java + /** + * 乐观锁更新库存 限流 + * @param sid + * @return + */ + @SpringControllerLimit(errorCode = 200) + @RequestMapping("/createOptimisticLimitOrder/{sid}") + @ResponseBody + public String createOptimisticLimitOrder(@PathVariable int sid) { + logger.info("sid=[{}]", sid); + int id = 0; + try { + id = orderService.createOptimisticOrder(sid); + } catch (Exception e) { + logger.error("Exception",e); + } + return String.valueOf(id); + } +``` + +Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 + +再压测看下效果 `/createOptimisticLimitOrderByRedis/1`: + +![](https://i.loli.net/2019/05/08/5cd1d776c39b7.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d77ba16d2.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d780d5aa2.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d784644d5.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d787b3e49.jpg) + +首先是看结果没有问题,再看数据库连接以及并发请求数都有**明显的下降**。 + + +## 乐观锁更新 + 分布式限流 + Redis 缓存 + +其实仔细观察 Druid 监控数据发现这个 SQL 被多次查询: + +![](https://i.loli.net/2019/05/08/5cd1d78b3896a.jpg) + +其实这是实时查询库存的 SQL,主要是为了在每次下单之前判断是否还有库存。 + +**这也是个优化点**。 + +这种数据我们完全可以放在内存中,效率比在数据库要高很多。 + +由于我们的应用是分布式的,所以堆内缓存显然不合适,Redis 就非常适合。 + +这次主要改造的是 Service 层: + +- 每次查询库存时走 Redis。 +- 扣库存时更新 Redis。 +- 需要提前将库存信息写入 Redis(手动或者程序自动都可以)。 + +主要代码如下: + +```java + @Override + public int createOptimisticOrderUseRedis(int sid) throws Exception { + //检验库存,从 Redis 获取 + Stock stock = checkStockByRedis(sid); + + //乐观锁更新库存 以及更新 Redis + saleStockOptimisticByRedis(stock); + + //创建订单 + int id = createOrder(stock); + return id ; + } + + + private Stock checkStockByRedis(int sid) throws Exception { + Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid)); + Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid)); + if (count.equals(sale)){ + throw new RuntimeException("库存不足 Redis currentCount=" + sale); + } + Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid)); + Stock stock = new Stock() ; + stock.setId(sid); + stock.setCount(count); + stock.setSale(sale); + stock.setVersion(version); + + return stock; + } + + + /** + * 乐观锁更新数据库 还要更新 Redis + * @param stock + */ + private void saleStockOptimisticByRedis(Stock stock) { + int count = stockService.updateStockByOptimistic(stock); + if (count == 0){ + throw new RuntimeException("并发更新库存失败") ; + } + //自增 + redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ; + redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ; + } +``` + +压测看看实际效果 `/createOptimisticLimitOrderByRedis/1`: + +![](https://i.loli.net/2019/05/08/5cd1d78d659b6.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d790607a1.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d79307676.jpg) + +![](https://i.loli.net/2019/05/08/5cd1d7973de43.jpg) + +最后发现数据没问题,数据库的请求与并发也都下来了。 + + + +## 乐观锁更新 + 分布式限流 + Redis 缓存 + Kafka 异步 + +最后的优化还是想如何来再次提高吞吐量以及性能的。 + +我们上文所有例子其实都是同步请求,完全可以利用同步转异步来提高性能啊。 + +这里我们将写订单以及更新库存的操作进行异步化,利用 `Kafka` 来进行解耦和队列的作用。 + +每当一个请求通过了限流到达了 Service 层通过了库存校验之后就将订单信息发给 Kafka ,这样一个请求就可以直接返回了。 + +消费程序再对数据进行入库落地。 + +因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。 + +这里代码较多就不贴了,消费程序其实就是把之前的 Service 层的逻辑重写了一遍,不过采用的是 SpringBoot。 + +感兴趣的朋友可以看下。 + +[https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER](https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER) + + + + +## 总结 + +其实经过上面的一顿优化总结起来无非就是以下几点: + +- 尽量将请求拦截在上游。 +- 还可以根据 UID 进行限流。 +- 最大程度的减少请求落到 DB。 +- 多利用缓存。 +- 同步操作异步化。 +- fail fast,尽早失败,保护应用。 + +码字不易,这应该是我写过字数最多的了,想想当年高中 800 字的作文都憋不出来😂,可想而知是有多难得了。 + +**以上内容欢迎讨论**。 + +### 号外 +最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。 + +> 地址: [https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) diff --git a/docs/collections/ArrayList.md b/docs/collections/ArrayList.md new file mode 100644 index 00000000..11bb2ae1 --- /dev/null +++ b/docs/collections/ArrayList.md @@ -0,0 +1,151 @@ +# ArrayList/Vector 的底层分析 + +## ArrayList + +`ArrayList` 实现于 `List`、`RandomAccess` 接口。可以插入空数据,也支持随机访问。 + +`ArrayList `相当于动态数据,其中最重要的两个属性分别是: +`elementData` 数组,以及 `size` 大小。 +在调用 `add()` 方法的时候: +```java + public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; + } +``` + +- 首先进行扩容校验。 +- 将插入的值放到尾部,并将 size + 1 。 + +如果是调用 `add(index,e)` 在指定位置添加的话: +```java + public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + //复制,向后移动 + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; + } +``` + + +- 也是首先扩容校验。 +- 接着对数据进行复制,目的是把 index 位置空出来放本次插入的数据,并将后面的数据向后移动一个位置。 + +其实扩容最终调用的代码: +```java + private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); + } +``` + +也是一个数组复制的过程。 + +由此可见 `ArrayList` 的主要消耗是数组扩容以及在指定位置添加数据,在日常使用时最好是指定大小,尽量减少扩容。更要减少在指定位置插入数据的操作。 + +### 序列化 + +由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 `transient` 修饰,可以防止被自动序列化。 + +```java +transient Object[] elementData; +``` + +因此 ArrayList 自定义了序列化与反序列化: + +```java + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException{ + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // Write out size as capacity for behavioural compatibility with clone() + s.writeInt(size); + + // Write out all elements in the proper order. + //只序列化了被使用的数据 + for (int i=0; i