diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9396aad0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: java + +install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true +script: mvn -DskipTests=true clean install + + +branches: + only: + - master \ No newline at end of file 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/ArrayList.md b/MD/ArrayList.md index 8601cd1f..11bb2ae1 100644 --- a/MD/ArrayList.md +++ b/MD/ArrayList.md @@ -117,7 +117,7 @@ transient Object[] elementData; ## Vector -`Voctor` 也是实现于 `List` 接口,底层数据结构和 `ArrayList` 类似,也是一个动态数组存放数据。不过是在 `add()` 方法的时候使用 `synchronize` 进行同步写数据,但是开销较大,所以 `Vector` 是一个同步容器并不是一个并发容器。 +`Vector` 也是实现于 `List` 接口,底层数据结构和 `ArrayList` 类似,也是一个动态数组存放数据。不过是在 `add()` 方法的时候使用 `synchronized` 进行同步写数据,但是开销较大,所以 `Vector` 是一个同步容器并不是一个并发容器。 以下是 `add()` 方法: ```java diff --git a/MD/Cache-design.md b/MD/Cache-design.md index 1dea5190..fc26b7a6 100644 --- a/MD/Cache-design.md +++ b/MD/Cache-design.md @@ -20,9 +20,9 @@ 写缓存时也要注意,通常来说分为以下几步: -- 开启事物。 +- 开启事务。 - 写入 DB 。 -- 提交事物。 +- 提交事务。 - 写入缓存。 这里可能会存在数据库写入成功但是缓存写入失败的情况,但是也不建议将写入缓存加入到事务中。 diff --git a/MD/ConcurrentHashMap.md b/MD/ConcurrentHashMap.md index c5cc24f0..0a9d7546 100644 --- a/MD/ConcurrentHashMap.md +++ b/MD/ConcurrentHashMap.md @@ -1,24 +1,29 @@ +**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。** + # ConcurrentHashMap 实现原理 由于 `HashMap` 是一个线程不安全的容器,主要体现在容量大于`总量*负载因子`发生扩容时会出现环形链表从而导致死循环。 因此需要支持线程安全的并发容器 `ConcurrentHashMap` 。 -## 数据结构 -![](https://ws2.sinaimg.cn/large/006tNc79ly1fn2f5pgxinj30dw0730t7.jpg) +## JDK1.7 实现 + +### 数据结构 +![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。 `ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。 -## get 方法 +### get 方法 `ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。 只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值([volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#%E5%8F%AF%E8%A7%81%E6%80%A7))。 -## put 方法 +### put 方法 内部 `HashEntry` 类 : + ```java static final class HashEntry { final int hash; @@ -39,12 +44,56 @@ 首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。 -而 ConcurrentHashMap 不一样,它是先将数据插入之后再检查是否需要扩容,之后再做插入。 +而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。 -## size 方法 +### size 方法 每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap` 的 `size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。 但如果每次调用 `size` 方法将其余的修改操作加锁效率也很低。所以做法是先尝试两次将 `count` 累加,如果容器的 `count` 发生了变化再加锁来统计 `size`。 至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。 + + + +## JDK1.8 实现 + +![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) + +1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 + +其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 + +![](https://s2.loli.net/2024/05/21/MVr92SEeJI34fas.png) + + +也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 + +其中的 `val next` 都用了 volatile 修饰,保证了可见性。 + +### put 方法 + +重点来看看 put 函数: + +![](https://s2.loli.net/2024/05/21/EpBRMOQnD8bx2wH.png) + + +- 根据 key 计算出 hashcode 。 +- 判断是否需要进行初始化。 +- `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 +- 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 +- 如果都不满足,则利用 synchronized 锁写入数据。 +- 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 + +### get 方法 + +![](https://s2.loli.net/2024/05/21/CFvAuGp8BMUko6I.png) + + +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 都不满足那就按照链表的方式遍历获取值。 + +## 总结 + +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 7146e6bf..d975c728 100644 --- a/MD/HashMap.md +++ b/MD/HashMap.md @@ -1,8 +1,10 @@ +**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。** + # HashMap 底层分析 > 以下基于 JDK1.7 分析。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn84b0ftj4j30eb0560sv.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2be77958.jpg) 如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数: @@ -16,7 +18,7 @@ 由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为 `2^n` 。这样用 `2^n - 1` 做位运算与取模效果一致,并且效率还要高出许多。 -由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 `table[index]`处形成环形链表,采用头插法将数据插入到链表中。 +由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 `table[index]`处形成链表,采用头插法将数据插入到链表中。 ## get 方法 @@ -59,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 ff9080ee..bcff3259 100644 --- a/MD/MemoryAllocation.md +++ b/MD/MemoryAllocation.md @@ -10,26 +10,85 @@ ## 虚拟机栈 -虚拟机栈是有一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。 +虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。 每一个栈帧由`局部变量区`、`操作数栈`等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。 -> 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `stackoverflow` 异常。 +> - 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `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/ReentrantLock.md b/MD/ReentrantLock.md index 22ec2b05..802b0bbc 100644 --- a/MD/ReentrantLock.md +++ b/MD/ReentrantLock.md @@ -1,6 +1,6 @@ # ReentrantLock 实现原理 -使用 `synchronize` 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。 +使用 `synchronized` 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。 而 `ReentrantLock` 就是一个普通的类,它是基于 `AQS(AbstractQueuedSynchronizer)`来实现的。 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 ff734d3d..cf00e90c 100644 --- a/MD/Synchronize.md +++ b/MD/Synchronize.md @@ -1,10 +1,10 @@ -# Synchronize 关键字原理 +# synchronized 关键字原理 -众所周知 `Synchronize` 关键字是解决并发问题常用解决方案,有以下三种使用方式: +众所周知 `synchronized` 关键字是解决并发问题常用解决方案,有以下三种使用方式: - 同步普通方法,锁的是当前对象。 - 同步静态方法,锁的是当前 `Class` 对象。 -- 同步块,锁的是 `{}` 中的对象。 +- 同步块,锁的是 `()` 中的对象。 实现原理: @@ -71,7 +71,7 @@ public class com.crossoverjie.synchronize.Synchronize { ## 锁优化 -`synchronize` 很多都称之为重量锁,`JDK1.6` 中对 `synchronize` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁`和`轻量锁`。 +`synchronized` 很多都称之为重量锁,`JDK1.6` 中对 `synchronized` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁`和`轻量锁`。 ### 轻量锁 @@ -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 a8a07347..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/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 c2c964d7..1a4f8d56 100644 --- a/MD/Threadcore.md +++ b/MD/Threadcore.md @@ -1,7 +1,7 @@ # Java 多线程三大核心 ## 原子性 -`Java` 的原子性就和数据库事物的原子性差不多,一个操作中要么全部执行成功或者失败。 +`Java` 的原子性就和数据库事务的原子性差不多,一个操作中要么全部执行成功或者失败。 `JMM` 只是保证了基本的原子性,但类似于 `i++` 之类的操作,看似是原子操作,其实里面涉及到: @@ -9,7 +9,7 @@ - 自增。 - 再赋值给 i。 -这三步操作,所以想要实现 `i++` 这样的原子操作就需要用到 `synchronize` 或者是 `lock` 进行加锁处理。 +这三步操作,所以想要实现 `i++` 这样的原子操作就需要用到 `synchronized` 或者是 `lock` 进行加锁处理。 如果是基础类的自增操作可以使用 `AtomicInteger` 这样的原子类来实现(其本质是利用了 `CPU` 级别的 的 `CAS` 指令来完成的)。 @@ -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) 如上图所示。 @@ -56,7 +56,7 @@ public final boolean compareAndSet(long expect, long update) { 使用 `volatile` 关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。 -`synchronize`和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 `volatile` 相比开销较大。 +`synchronized`和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 `volatile` 相比开销较大。 ## 顺序性 以下这段代码: @@ -71,7 +71,7 @@ int c = a + b ; //3 重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。 -Java 中可以使用 `volatile` 来保证顺序性,`synchronize 和 lock` 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。 +Java 中可以使用 `volatile` 来保证顺序性,`synchronized 和 lock` 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。 除了通过 `volatile` 关键字显式的保证顺序之外, `JVM` 还通过 `happen-before` 原则来隐式的保证顺序性。 @@ -84,21 +84,24 @@ Java 中可以使用 `volatile` 来保证顺序性,`synchronize 和 lock` 也 可以用 `volatile` 实现一个双重检查锁的单例模式: ```java -public class Singleton{ - private static volatile Singleton singleton ; - private Singleton(){} - public static Singleton getInstance(){ - if(singleton == null){ - synchronize(Singleton.class){ - if(singleton == null){ - singleton = new Singleton(); - } - } - } - return singleton ; - } - -} + public class Singleton { + private static volatile Singleton singleton; + + private Singleton() { + } + + public static Singleton getInstance() { + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } + } + return singleton; + } + + } ``` 这里的 `volatile` 关键字主要是为了防止指令重排。 @@ -108,23 +111,26 @@ public class Singleton{ - 初始化对象。(2) - 将 `singleton` 对象指向分配的内存地址。(3) -加上 `volatile` 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。 +加上 `volatile` 是为了让以上的三步操作顺序执行,反之有可能第三步在第二步之前被执行就有可能导致某个线程拿到的单例对象还没有初始化,以致于使用报错。 #### 控制停止线程的标记 ```java -private volatile boolean flag ; -private void run(){ - new Thread(new Runnable(){ - if(flag){ - doSomeThing(); - } - }); -} - -private void stop(){ - flag = false ; -} + private volatile boolean flag ; + private void run(){ + new Thread(new Runnable() { + @Override + public void run() { + while (flag) { + doSomeThing(); + } + } + }); + } + + private void stop(){ + flag = false ; + } ``` 这里如果没有用 volatile 来修饰 flag ,就有可能其中一个线程调用了 `stop()`方法修改了 flag 的值并不会立即刷新到主内存中,导致这个循环并不会立即停止。 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 6290f976..7ccde30e 100644 --- a/MD/collection/LinkedHashMap.md +++ b/MD/collection/LinkedHashMap.md @@ -11,7 +11,7 @@ - 根据写入顺序排序。 - 根据访问顺序排序。 -其中根据访问顺序排序时,每次 `get` 都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表。 +其中根据访问顺序排序时,每次 `get` 都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。 ## 数据结构 @@ -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/concurrent/volatile.md b/MD/concurrent/volatile.md index 0ec9200c..d836e7ec 100644 --- a/MD/concurrent/volatile.md +++ b/MD/concurrent/volatile.md @@ -37,7 +37,6 @@ public class Volatile implements Runnable{ @Override public void run() { while (flag){ - System.out.println(Thread.currentThread().getName() + "正在运行。。。"); } System.out.println(Thread.currentThread().getName() +"执行完毕"); } @@ -49,15 +48,30 @@ public class Volatile implements Runnable{ System.out.println("main 线程正在运行") ; - TimeUnit.MILLISECONDS.sleep(100) ; + Scanner sc = new Scanner(System.in); + while(sc.hasNext()){ + String value = sc.next(); + if(value.equals("1")){ - aVolatile.stopThread(); + new Thread(new Runnable() { + @Override + public void run() { + aVolatile.stopThread(); + } + }).start(); + + break ; + } + } + + System.out.println("主线程退出了!"); } private void stopThread(){ flag = false ; } + } ``` @@ -113,7 +127,7 @@ public class VolatileInc implements Runnable{ - 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。 -- 也可以使用 `synchronize` 或者是锁的方式来保证原子性。 +- 也可以使用 `synchronized` 或者是锁的方式来保证原子性。 - 还可以用 `Atomic` 包中 `AtomicInteger` 来替换 `int`,它利用了 `CAS` 算法来保证了原子性。 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/newObject.md b/MD/newObject.md index 50c80d35..6c091b4e 100644 --- a/MD/newObject.md +++ b/MD/newObject.md @@ -46,7 +46,7 @@ ### Eden 区分配 简单的来说对象都是在堆内存中分配的,往细一点看则是优先在 `Eden` 区分配。 -这里就涉及到堆内存的划分了,为了方便垃圾回收,JVM 将对内存分为新生代和老年代。 +这里就涉及到堆内存的划分了,为了方便垃圾回收,JVM 将堆内存分为新生代和老年代。 而新生代中又会划分为 `Eden` 区,`from Survivor、to Survivor` 区。 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 c89ff7bb..9e8df602 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,133 @@ -Java 知识点,继续完善中。 -> 多数是一些 Java 基础知识、底层原理、算法详解。也有上层应用设计,其中不乏一些大厂面试真题。 +
+ +
-如果对你有帮助请点下 `Star`,有疑问欢迎提 [Issues](https://github.com/crossoverJie/Java-Interview/issues),有好的想法请提 [PR](https://github.com/crossoverJie/Java-Interview/pulls)。 +[![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 -[常用集合](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 Core Sprout`:处于萌芽阶段的 Java 核心知识库。 + +**访问这里获取更好的阅读体验**:[https://crossoverjie.top/JCSprout/](https://crossoverjie.top/JCSprout/) + +
+ + + +最近开通了知识星球,感谢大家对 `JCSprout` 的支持,为大家提供 100 份 10 元优惠券,也就是 69-10=59 元,具体福利大家可以扫码参考再决定是否加入。 + +> PS: 后续会继续维护该项目,同时加入现在热门的 Golang/kubernetes/OpenTelemetry 等知识点,感兴趣的可以加入星球当面催更(当然内容也会更新到这个项目里)。 + + +| 📊 |⚔️ | 🖥 | 🚏 | 🏖 | 🌁| 📮 | 🔍 | 🚀 | 🌈 |💡 +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|:------:| +| [集合](#常用集合) | [多线程](#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) -- [synchronize 关键字原理](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 + +
+ + + +最近开通了知识星球,感谢大家对 `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

        > entryIterator = map.entrySet().iterator(); + while (entryIterator.hasNext()) { + Map.Entry next = entryIterator.next(); + System.out.println("key=" + next.getKey() + " value=" + next.getValue()); + } +``` + +```java +Iterator iterator = map.keySet().iterator(); + while (iterator.hasNext()){ + String key = iterator.next(); + System.out.println("key=" + key + " value=" + map.get(key)); + + } +``` + +```java +map.forEach((key,value)->{ + System.out.println("key=" + key + " value=" + value); +}); +``` + +**强烈建议**使用第一种 EntrySet 进行遍历。 + +第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低, 第三种需要 `JDK1.8` 以上,通过外层遍历 table,内层遍历链表或红黑树。 + + +## notice + +在并发环境下使用 `HashMap` 容易出现死循环。 + +并发场景发生扩容,调用 `resize()` 方法里的 `rehash()` 时,容易出现环形链表。这样当获取一个不存在的 `key` 时,计算出的 `index` 正好是环形链表的下标时就会出现死循环。 + +![](https://i.loli.net/2019/06/26/5d1391f90375678382.jpg) + +> 所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。 + +在 `JDK1.8` 中对 `HashMap` 进行了优化: +当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8)并且 `table` 的长度不小于64(否则扩容一次)时,链表将会转换为**红黑树**。 + +假设 `hash` 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 `O(n)` 。 + +如果是红黑树,时间复杂度就是 `O(logn)` 。 + +大大提高了查询效率。 + +多线程场景下推荐使用 [ConcurrentHashMap](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ConcurrentHashMap.md)。 diff --git a/docs/collections/HashSet.md b/docs/collections/HashSet.md new file mode 100644 index 00000000..3fd3565e --- /dev/null +++ b/docs/collections/HashSet.md @@ -0,0 +1,49 @@ +# HashSet + +`HashSet` 是一个不允许存储重复元素的集合,它的实现比较简单,只要理解了 `HashMap`,`HashSet` 就水到渠成了。 + +## 成员变量 +首先了解下 `HashSet` 的成员变量: + +```java + private transient HashMap map; + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); +``` + +发现主要就两个变量: + +- `map` :用于存放最终数据的。 +- `PRESENT` :是所有写入 map 的 `value` 值。 + +## 构造函数 + +```java + public HashSet() { + map = new HashMap(); + } + + public HashSet(int initialCapacity, float loadFactor) { + map = new HashMap(initialCapacity, loadFactor); + } +``` +构造函数很简单,利用了 `HashMap` 初始化了 `map` 。 + +## add + +```java + public boolean add(E e) { + return map.put(e, PRESENT)==null; + } +``` + +比较关键的就是这个 `add()` 方法。 +可以看出它是将存放的对象当做了 `HashMap` 的健,`value` 都是相同的 `PRESENT` 。由于 `HashMap` 的 `key` 是不能重复的,所以每当有重复的值写入到 `HashSet` 时,`value` 会被覆盖,但 `key` 不会受到影响,这样就保证了 `HashSet` 中只能存放不重复的元素。 + +## 总结 + +`HashSet` 的原理比较简单,几乎全部借助于 `HashMap` 来实现的。 + +所以 `HashMap` 会出现的问题 `HashSet` 依然不能避免。 + diff --git a/docs/collections/LinkedHashMap.md b/docs/collections/LinkedHashMap.md new file mode 100644 index 00000000..bbc49c37 --- /dev/null +++ b/docs/collections/LinkedHashMap.md @@ -0,0 +1,278 @@ +# LinkedHashMap 底层分析 + +众所周知 [HashMap](https://github.com/crossoverJie/Java-Interview/blob/master/MD/HashMap.md) 是一个无序的 `Map`,因为每次根据 `key` 的 `hashcode` 映射到 `Entry` 数组上,所以遍历出来的顺序并不是写入的顺序。 + +因此 JDK 推出一个基于 `HashMap` 但具有顺序的 `LinkedHashMap` 来解决有排序需求的场景。 + +它的底层是继承于 `HashMap` 实现的,由一个双向链表所构成。 + +`LinkedHashMap` 的排序方式有两种: + +- 根据写入顺序排序。 +- 根据访问顺序排序。 + +其中根据访问顺序排序时,每次 `get` 都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。 + +## 数据结构 + +```java + @Test + public void test(){ + Map map = new LinkedHashMap(); + map.put("1",1) ; + map.put("2",2) ; + map.put("3",3) ; + map.put("4",4) ; + map.put("5",5) ; + System.out.println(map.toString()); + + } +``` + +调试可以看到 `map` 的组成: + +![](https://ws2.sinaimg.cn/large/006tKfTcly1fo6l9xp91lj319m0s4tgi.jpg) + + +打开源码可以看到: + +```java + /** + * The head of the doubly linked list. + */ + private transient Entry header; + + /** + * The iteration ordering method for this linked hash map: true + * for access-order, false for insertion-order. + * + * @serial + */ + private final boolean accessOrder; + + private static class Entry extends HashMap.Entry { + // These fields comprise the doubly linked list used for iteration. + Entry before, after; + + Entry(int hash, K key, V value, HashMap.Entry next) { + super(hash, key, value, next); + } + } +``` + +其中 `Entry` 继承于 `HashMap` 的 `Entry`,并新增了上下节点的指针,也就形成了双向链表。 + +还有一个 `header` 的成员变量,是这个双向链表的头结点。 + +上边的 demo 总结成一张图如下: + +![](https://ws1.sinaimg.cn/large/006tKfTcgy1fodggwc523j30za0n4wgj.jpg) + +第一个类似于 `HashMap` 的结构,利用 `Entry` 中的 `next` 指针进行关联。 + +下边则是 `LinkedHashMap` 如何达到有序的关键。 + +就是利用了头节点和其余的各个节点之间通过 `Entry` 中的 `after` 和 `before` 指针进行关联。 + + +其中还有一个 `accessOrder` 成员变量,默认是 `false`,默认按照插入顺序排序,为 `true` 时按照访问顺序排序,也可以调用: + +``` + public LinkedHashMap(int initialCapacity, + float loadFactor, + boolean accessOrder) { + super(initialCapacity, loadFactor); + this.accessOrder = accessOrder; + } +``` + +这个构造方法可以显示的传入 `accessOrder `。 + + +## 构造方法 + +`LinkedHashMap` 的构造方法: + +```java + public LinkedHashMap() { + super(); + accessOrder = false; + } +``` + +其实就是调用的 `HashMap` 的构造方法: + +`HashMap` 实现: + +```java + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity> MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + + this.loadFactor = loadFactor; + threshold = initialCapacity; + //HashMap 只是定义了改方法,具体实现交给了 LinkedHashMap + init(); + } +``` + +可以看到里面有一个空的 `init()`,具体是由 `LinkedHashMap` 来实现的: + +```java + @Override + void init() { + header = new Entry(-1, null, null, null); + header.before = header.after = header; + } +``` +其实也就是对 `header` 进行了初始化。 + +## put() 方法 + +看 `LinkedHashMap` 的 `put()` 方法之前先看看 `HashMap` 的 `put` 方法: + +``` + public V put(K key, V value) { + if (table == EMPTY_TABLE) { + inflateTable(threshold); + } + if (key == null) + return putForNullKey(value); + int hash = hash(key); + int i = indexFor(hash, table.length); + for (Entry e = table[i]; e != null; e = e.next) { + Object k; + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + V oldValue = e.value; + e.value = value; + //空实现,交给 LinkedHashMap 自己实现 + e.recordAccess(this); + return oldValue; + } + } + + modCount++; + // LinkedHashMap 对其重写 + addEntry(hash, key, value, i); + return null; + } + + // LinkedHashMap 对其重写 + void addEntry(int hash, K key, V value, int bucketIndex) { + if ((size>= threshold) && (null != table[bucketIndex])) { + resize(2 * table.length); + hash = (null != key) ? hash(key) : 0; + bucketIndex = indexFor(hash, table.length); + } + + createEntry(hash, key, value, bucketIndex); + } + + // LinkedHashMap 对其重写 + void createEntry(int hash, K key, V value, int bucketIndex) { + Entry e = table[bucketIndex]; + table[bucketIndex] = new Entry(hash, key, value, e); + size++; + } +``` + +主体的实现都是借助于 `HashMap` 来完成的,只是对其中的 `recordAccess(), addEntry(), createEntry()` 进行了重写。 + +`LinkedHashMap` 的实现: + +```java + //就是判断是否是根据访问顺序排序,如果是则需要将当前这个 Entry 移动到链表的末尾 + void recordAccess(HashMap m) { + LinkedHashMap lm = (LinkedHashMap)m; + if (lm.accessOrder) { + lm.modCount++; + remove(); + addBefore(lm.header); + } + } + + + //调用了 HashMap 的实现,并判断是否需要删除最少使用的 Entry(默认不删除) + void addEntry(int hash, K key, V value, int bucketIndex) { + super.addEntry(hash, key, value, bucketIndex); + + // Remove eldest entry if instructed + Entry eldest = header.after; + if (removeEldestEntry(eldest)) { + removeEntryForKey(eldest.key); + } + } + + void createEntry(int hash, K key, V value, int bucketIndex) { + HashMap.Entry old = table[bucketIndex]; + Entry e = new Entry(hash, key, value, old); + //就多了这一步,将新增的 Entry 加入到 header 双向链表中 + table[bucketIndex] = e; + e.addBefore(header); + size++; + } + + //写入到双向链表中 + private void addBefore(Entry existingEntry) { + after = existingEntry; + before = existingEntry.before; + before.after = this; + after.before = this; + } + +``` + +## get 方法 + +LinkedHashMap 的 `get()` 方法也重写了: + +```java + public V get(Object key) { + Entry e = (Entry)getEntry(key); + if (e == null) + return null; + + //多了一个判断是否是按照访问顺序排序,是则将当前的 Entry 移动到链表头部。 + e.recordAccess(this); + return e.value; + } + + void recordAccess(HashMap m) { + LinkedHashMap lm = (LinkedHashMap)m; + if (lm.accessOrder) { + lm.modCount++; + + //删除 + remove(); + //添加到头部 + addBefore(lm.header); + } + } + + +``` + +`clear()` 清空就要比较简单了: + +```java + //只需要把指针都指向自己即可,原本那些 Entry 没有引用之后就会被 JVM 自动回收。 + public void clear() { + super.clear(); + header.before = header.after = header; + } +``` + + +## 总结 + +总的来说 `LinkedHashMap` 其实就是对 `HashMap` 进行了拓展,使用了双向链表来保证了顺序性。 + +因为是继承与 `HashMap` 的,所以一些 `HashMap` 存在的问题 `LinkedHashMap` 也会存在,比如不支持并发等。 + + diff --git a/docs/collections/LinkedList.md b/docs/collections/LinkedList.md new file mode 100644 index 00000000..041f3221 --- /dev/null +++ b/docs/collections/LinkedList.md @@ -0,0 +1,67 @@ +# LinkedList 底层分析 + +![](https://i.loli.net/2019/06/26/5d1391f88b05665203.jpg) + +如图所示 `LinkedList` 底层是基于双向链表实现的,也是实现了 `List` 接口,所以也拥有 List 的一些特点(JDK1.7/8 之后取消了循环,修改为双向链表)。 + +## 新增方法 + +```java + public boolean add(E e) { + linkLast(e); + return true; + } + /** + * Links e as last element. + */ + void linkLast(E e) { + final Node l = last; + final Node newNode = new Node(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; + } +``` + +可见每次插入都是移动指针,和 ArrayList 的拷贝数组来说效率要高上不少。 + +## 查询方法 + +```java + public E get(int index) { + checkElementIndex(index); + return node(index).item; + } + + Node node(int index) { + // assert isElementIndex(index); + + if (index < (size>> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + Node x = last; + for (int i = size - 1; i> index; i--) + x = x.prev; + return x; + } + } +``` + +上述代码,利用了双向链表的特性,如果`index`离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。 + +- `node()`会以`O(n/2)`的性能去获取一个结点 + - 如果索引值大于链表大小的一半,那么将从尾结点开始遍历 + +这样的效率是非常低的,特别是当 index 越接近 size 的中间值时。 + +总结: + +- LinkedList 插入,删除都是移动指针效率很高。 +- 查找需要进行遍历查询,效率较低。 diff --git a/docs/contactme.md b/docs/contactme.md new file mode 100644 index 00000000..6908d045 --- /dev/null +++ b/docs/contactme.md @@ -0,0 +1,33 @@ +# SHOW TIME + +> 请科学上网 + +--- + +[フレーム] + + +--- +[フレーム] + +--- + +[フレーム] + +--- + + + + +---------- +# CONTACT +> - [微博](http://weibo.com/crossoverJie "微博") +> - [GitHub](https://github.com/crossoverJie "github") +> - [crossoverJie@gmail.com](mailto:crossoverjie@gmail.com) + +[![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-787381170-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5HPYvQk) + +**欢迎我的关注公众号一起交流:** + +![](https://crossoverjie.top/uploads/weixinfooter1.jpg) + diff --git a/docs/db/DB-split.md b/docs/db/DB-split.md new file mode 100644 index 00000000..51fccdde --- /dev/null +++ b/docs/db/DB-split.md @@ -0,0 +1,35 @@ +# 数据库水平垂直拆分 + +当数据库量非常大的时候,DB 已经成为系统瓶颈时就可以考虑进行水平垂直拆分了。 + +## 水平拆分 + +一般水平拆分是根据表中的某一字段(通常是主键 ID )取模处理,将一张表的数据拆分到多个表中。这样每张表的表结构是相同的但是数据不同。 + +不但可以通过 ID 取模分表还可以通过时间分表,比如每月生成一张表。 +按照范围分表也是可行的:一张表只存储 `0~1000W`的数据,超过只就进行分表,这样分表的优点是扩展灵活,但是存在热点数据。 + +按照取模分表拆分之后我们的查询、修改、删除也都是取模。比如新增一条数据的时候往往需要一张临时表来生成 ID,然后根据生成的 ID 取模计算出需要写入的是哪张表(也可以使用[分布式 ID 生成器](distributed/ID-generator.md)来生成 ID)。 + +分表之后不能避免的就是查询要比以前复杂,通常不建议 `join` ,一般的做法是做两次查询。 + +## 垂直拆分 + +当一张表的字段过多时则可以考虑垂直拆分。 +通常是将一张表的字段才分为主表以及扩展表,使用频次较高的字段在一张表,其余的在一张表。 + +这里的多表查询也不建议使用 `join` ,依然建议使用两次查询。 + +## 拆分之后带来的问题 + +拆分之后由一张表变为了多张表,一个库变为了多个库。最突出的一个问题就是事务如何保证。 + +### 两段提交 + +### 最终一致性 + +如果业务对强一致性要求不是那么高那么最终一致性则是一种比较好的方案。 + +通常的做法就是补偿,比如 一个业务是 A 调用 B,两个执行成功才算最终成功,当 A 成功之后,B 执行失败如何来通知 A 呢。 + +比较常见的做法是 失败时 B 通过 MQ 将消息告诉 A,A 再来进行回滚。这种的前提是 A 的回滚操作得是幂等的,不然 B 重复发消息就会出现问题。 \ No newline at end of file diff --git a/docs/db/MySQL-Index.md b/docs/db/MySQL-Index.md new file mode 100644 index 00000000..c41f5bff --- /dev/null +++ b/docs/db/MySQL-Index.md @@ -0,0 +1,26 @@ +# MySQL 索引原理 + +现在互联网应用中对数据库的使用多数都是读较多,比例可以达到 `10:1`。并且数据库在做查询时 `IO` 消耗较大,所以如果能把一次查询的 `IO` 次数控制在常量级那对数据库的性能提升将是非常明显的,因此基于 `B+ Tree` 的索引结构出现了。 + + +## B+ Tree 的数据结构 + +![](https://i.loli.net/2019/06/26/5d139411b683d65706.jpg) + +如图所示是 `B+ Tree` 的数据结构。是由一个一个的磁盘块组成的树形结构,每个磁盘块由数据项和指针组成。 + +> 所有的数据都是存放在叶子节点,非叶子节点不存放数据。 + +## 查找过程 + +以磁盘块1为例,指针 P1 表示小于17的磁盘块,P2 表示在 `17~35` 之间的磁盘块,P3 则表示大于35的磁盘块。 + +比如要查找数据项99,首先将磁盘块1 load 到内存中,发生 1 次 `IO`。接着通过二分查找发现 99 大于 35,所以找到了 P3 指针。通过P3 指针发生第二次 IO 将磁盘块4加载到内存。再通过二分查找发现大于87,通过 P3 指针发生了第三次 IO 将磁盘块11 加载到内存。最后再通过一次二分查找找到了数据项99。 + +由此可见,如果一个几百万的数据查询只需要进行三次 IO 即可找到数据,那么整个效率将是非常高的。 + +观察树的结构,发现查询需要经历几次 IO 是由树的高度来决定的,而树的高度又由磁盘块,数据项的大小决定的。 + +磁盘块越大,数据项越小那么树的高度就越低。这也就是为什么索引字段要尽可能小的原因。 + +> 索引使用的一些[原则](db/SQL-optimization.md)。 diff --git a/docs/db/SQL-optimization.md b/docs/db/SQL-optimization.md new file mode 100644 index 00000000..73e6b456 --- /dev/null +++ b/docs/db/SQL-optimization.md @@ -0,0 +1,86 @@ +# SQL 优化 + +## 负向查询不能使用索引 + +```sql +select name from user where id not in (1,3,4); +``` +应该修改为: + +``` +select name from user where id in (2,5,6); +``` + +## 前导模糊查询不能使用索引 +如: + +```sql +select name from user where name like '%zhangsan' +``` + +非前导则可以: +```sql +select name from user where name like 'zhangsan%' +``` +建议可以考虑使用 `Lucene` 等全文索引工具来代替频繁的模糊查询。 + +## 数据区分不明显的不建议创建索引 + +如 user 表中的性别字段,可以明显区分的才建议创建索引,如身份证等字段。 + +## 字段的默认值不要为 null +这样会带来和预期不一致的查询结果。 + +## 在字段上进行计算不能命中索引 + +```sql +select name from user where FROM_UNIXTIME(create_time) < CURDATE(); +``` + +应该修改为: + +```sql +select name from user where create_time < FROM_UNIXTIME(CURDATE()); +``` + +## 最左前缀问题 + +如果给 user 表中的 username pwd 字段创建了复合索引那么使用以下SQL 都是可以命中索引: + +```sql +select username from user where username='zhangsan' and pwd ='axsedf1sd' + +select username from user where pwd ='axsedf1sd' and username='zhangsan' + +select username from user where username='zhangsan' +``` + +但是使用 + +```sql +select username from user where pwd ='axsedf1sd' +``` +是不能命中索引的。 + +## 如果明确知道只有一条记录返回 + +```sql +select name from user where username='zhangsan' limit 1 +``` +可以提高效率,可以让数据库停止游标移动。 + +## 不要让数据库帮我们做强制类型转换 + +```sql +select name from user where telno=18722222222 +``` +这样虽然可以查出数据,但是会导致全表扫描。 + +需要修改为 +```sql +select name from user where telno='18722222222' +``` + +## 如果需要进行 join 的字段两表的字段类型要相同 + +不然也不会命中索引。 \ No newline at end of file diff --git a/docs/db/sharding-db.md b/docs/db/sharding-db.md new file mode 100644 index 00000000..82a8068a --- /dev/null +++ b/docs/db/sharding-db.md @@ -0,0 +1,193 @@ +![](https://i.loli.net/2019/06/26/5d1394119463b63622.jpg) + +# 前言 + +之前不少人问我"能否分享一些分库分表相关的实践",其实不是我不分享,而是真的经验不多🤣;和大部分人一样都是停留在理论阶段。 + + +不过这次多少有些可以说道了。 + +先谈谈背景,我们生产数据库随着业务发展量也逐渐起来;好几张单表已经突破**亿级**数据,并且保持每天 200+W 的数据量增加。 + +而我们有些业务需要进行关联查询、或者是报表统计;在这样的背景下大表的问题更加突出(比如一个查询功能需要跑好几分钟)。 + + + +> 可能很多人会说:为啥单表都过亿了才想方案解决?其实不是不想,而是由于历史原因加上错误预估了数据增长才导致这个局面。总之原因比较复杂,也不是本次讨论的重点。 + + +# 临时方案 + +由于需求紧、人手缺的情况下,整个处理的过程分为几个阶段。 + +第一阶段应该是去年底,当时运维反应 `MySQL` 所在的主机内存占用很高,整体负载也居高不下,导致整个 MySQL 的吞吐量明显降低(写入、查询数据都明显减慢)。 + +为此我们找出了数据量最大的几张表,发现大部分数据量在7/8000W 左右,少数的已经突破一亿。 + +通过业务层面进行分析发现,这些数据多数都是用户产生的一些**日志型数据**,而且这些数据在业务上并不是强相关的,甚至两三个月前的数据其实已经不需要实时查询了。 + + +因为接近年底,尽可能的不想去动应用,考虑是否可以在运维层面缓解压力;主要的目的就是把单表的数据量降低。 + + +原本是想把两个月之前的数据直接迁移出来放到备份表中,但在准备实施的过程中发现一个大坑。 + +> 表中没有一个可以排序的索引,导致我们无法快速的筛选出一部分数据!这真是一个深坑,为后面的一些优化埋了个地雷;即便是加索引也需要花几个小时(具体多久没敢在生产测试)。 + + +如果我们强行按照时间进行筛选,可能查询出 4000W 的数据就得花上好几个小时;这显然是行不通的。 + + +于是我们便想到了一个大胆的想法:这部分数据是否可以直接不要了? + +这可能是最有效及最快的方式了,和产品沟通后得知这部分数据真的只是日志型的数据,即便是报表出不来今后补上也是可以的。 + +于是我们就简单粗暴的做了以下事情: + +- 修改原有表的表名,比如加上(`_190416bak`)。 +- 再新建一张和原有表名称相同的表。 + + +这样新的数据就写到了新表,同时业务上也是使用的这个数据量较小的新表。 + +虽说过程不太优雅,但至少是解决了问题同时也给我们做技术改造预留了时间。 + +# 分表方案 + +之前的方案虽说可以缓解压力,但不能根本解决问题。 + +有些业务必须得查询之前的数据,导致之前那招行不通了,所以正好我们就借助这个机会把表分了。 + + +我相信大部分人虽说没有做过实际做过分表,但也见过猪跑;网上一搜各种方案层出不穷。 + +我认为最重要的一点是要结合实际业务找出需要 sharding 的字段,同时还有上线阶段的数据迁移也非常重要。 + +## 时间 + +可能大家都会说用 hash 的方式分配得最均匀,但我认为这还是需要使用历史数据的场景才用哈希分表。 + + +而对于不需要历史数据的场景,比如业务上只查询近三个月的数据。 + +这类需求完成可以采取时间分表,按照月份进行划分,这样改动简单,同时对历史数据也比较好迁移。 + +于是我们首先将这类需求的表筛选出来,按照月份进行拆分,只是在查询的时候拼接好表名即可;也比较好理解。 + +## 哈希 + +刚才也提到了:需要根据业务需求进行分表策略。 + +而一旦所有的数据都有可能查询时,按照时间分表也就行不通了。(也能做,只是如果不是按照时间进行查询时需要遍历所有的表) + +因此我们计划采用 `hash` 的方式分表,这算是业界比较主流的方式就不再赘述。 + +采用哈希时需要将 `sharding` 字段选好,由于我们的业务比较单纯;是一个物联网应用,所有的数据都包含有物联网设备的唯一标识(IMEI),并且这个字段天然的就保持了唯一性;大多数的业务也都是根据这个字段来的,所以它非常适合来做这个 `sharding` 字段。 + +在做分表之前也调研过 `MyCAT` 及 `sharding-jdbc`(现已升级为 `shardingsphere`),最终考虑到对开发的友好性及不增加运维复杂度还是决定在 jdbc 层 sharding 的方式。 + +但由于历史原因我们并不太好集成 `sharding-jdbc`,但基于 `sharding` 的特点自己实现了一个分表策略。 + +这个简单也好理解: + +```java +int index = hash(sharding字段) % 分表数量 ; + +select xx from 'busy_'+index where sharding字段 = xxx; +``` + +其实就是算出了表名,然后路由过去查询即可。 + + +只是我们实现的非常简单:修改了所有的底层查询方法,每个方法都里都做了这样的一个判断。 + +并没有像 `sharding-jdbc` 一样,代理了数据库的查询方法;其中还要做 `SQL解析-->SQL路由-->执行SQL-->合并结果` 这一系列的流程。 + +如果自己再做一遍无异于重新造了一个轮子,并且并不专业,只是在现有的技术条件下选择了一个快速实现达成效果的方法。 + +不过这个过程中我们节省了将 sharding 字段哈希的过程,因为每一个 IMEI 号其实都是一个唯一的整型,直接用它做 mod 运算即可。 + + + +还有一个是需要一个统一的组件生成规则,分表后不能再依赖于单表的字段自增了;方法还是挺多的: + +- 比如时间戳+随机数可满足大部分业务。 +- UUID,生成简单,但没法做排序。 +- 雪花算法统一生成主键ID。 + +大家可以根据自己的实际情况做选择。 + +# 业务调整 + +因为我们并没有使用第三方的 sharding-jdbc 组件,所有没有办法做到对代码的低侵入性;每个涉及到分表的业务代码都需要做底层方法的改造(也就是路由到正确的表)。 + +考虑到后续业务的发展,我们决定将拆分的表分为 64 张;加上后续引入大数据平台足以应对几年的数据增长。 + +> 这里还有个小细节需要注意:分表的数量需要为 2∧N 次方,因为在取模的这种分表方式下,即便是今后再需要分表影响的数据也会尽量的小。 + + +再修改时只能将表名称进行全局搜索,然后加以修改,同时根据修改的方法倒推到表现的业务并记录下来,方便后续回归测试。 + +--- + +当然无法避免查询时利用非 sharding 字段导致的全表扫描,这是所有分片后都会遇到的问题。 + +因此我们在修改分表方法的底层查询时同时也会查看是否有走分片字段,如果不是,那是否可以调整业务。 + +比如对于一个上亿的数据是否还有必要存在按照分页查询、日期查询?这样的业务是否真的具有意义? + +我们尽可能的引导产品按照这样的方式来设计产品或者做出调整。 + +但对于报表这类的需求确实也没办法,比如统计表中某种类型的数据;这种我们也可以利用多线程的方式去并行查询然后汇总统计来提高查询效率。 + + +有时也有一些另类场景: + +> 比如一个千万表中有某一特殊类型的数据只占了很小一部分,比如说几千上万条。 + + +这时页面上需要对它进行分页查询是比较正常的(比如某种投诉消息,客户需要一条一条的单独处理),但如果我们按照 IMEI 号或者是主键进行分片后再分页查询那就比较蛋疼了。 + +所以这类型的数据建议单独新建一张表来维护,不要和其他数据混合在一起,这样不管是做分页还是 like 都比较简单和独立。 + +## 验证 + +代码改完,开发也单测完成后怎么来验证分表的业务是否正常也比较麻烦。 + +一个是测试麻烦,再一个是万一哪里改漏了还是查询的原表,但这样在测试环境并不会有异常,一旦上线产生了生产数据到新的 64 张表后想要再修复就比较麻烦了。 + +所以我们取了个巧,直接将原表的表名修改,比如加一个后缀;这样在测试过程中观察前后台有无报错就比较容易提前发现这个问题。 + +# 上线流程 + +测试验收通过后只是分表这个需求的80%,剩下如何上线也是比较头疼。 + +一旦应用上线后所有的查询、写入、删除都会先走路由然后到达新表;而老数据在原表里是不会发生改变的。 + +## 数据迁移 + +所以我们上线前的第一步自然是需要将原有的数据进行迁移,迁移的目的是要分片到新的 64 张表中,这样才会对原有的业务无影响。 + + +因此我们需要额外准备一个程序,它需要将老表里的数据按照分片规则复制到新表中; + +在我们这个场景下,生产数据有些已经上亿了,这个迁移过程我们在测试环境模拟发现耗时是非常久的。而且我们老表中对于 `create_time` 这样用于筛选数据的字段没有索引(以前的技术债),所以查询起来就更加慢了。 + +最后没办法,我们只能和产品协商告知用户对于之前产生的数据短期可能会查询不到,这个时间最坏可能会持续几天(我们只能在凌晨迁移,白天会影响到数据库负载)。 + + +# 总结 + +这便是我们这次的分表实践,虽说不少过程都不优雅,但受限于条件也只能折中处理。 + +但我们后续的计划是,修改我们底层的数据连接(目前是自己封装的一个 jar 包,导致集成 sharding-jdbc 比较麻烦)最终逐渐迁移到 `sharding-jdbc` . + +最后得出了几个结论: + +- 一个好的产品规划非常有必要,可以在合理的时间对数据处理(不管是分表还是切入归档)。 +- 每张表都需要一个可以用于排序查询的字段(自增ID、创建时间),整个过程由于没有这个字段导致耽搁了很长时间。 +- 分表字段需要谨慎,要全盘的考虑业务情况,尽量避免出现查询扫表的情况。 + +最后欢迎留言讨论。 + +**你的点赞与分享是对我最大的支持** diff --git a/docs/distributed/Cache-design.md b/docs/distributed/Cache-design.md new file mode 100644 index 00000000..fc26b7a6 --- /dev/null +++ b/docs/distributed/Cache-design.md @@ -0,0 +1,45 @@ +# 分布式缓存设计 + +目前常见的缓存方案都是分层缓存,通常可以分为以下几层: + +- `NG` 本地缓存,命中的话直接返回。 +- `NG` 没有命中时则需要查询分布式缓存,如 `Redis` 。 +- 如果分布式缓存没有命中则需要回源到 `Tomcat` 在本地堆进行查询,命中之后异步写回 `Redis` 。 +- 以上都没有命中那就只有从 `DB` 或者是数据源进行查询,并写回到 Redis 中。 + + +## 缓存更新的原子性 + +在写回 Redis 的时候如果是 `Tomcat` 集群,多个进程同时写那很有可能出现脏数据,这时就会出现更新原子性的问题。 + +可以有以下解决方案: +- 可以将多个 Tomcat 中的数据写入到 MQ 队列中,由消费者进行单线程更新缓存。 +- 利用[分布式锁](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Java-lock.md#%E5%9F%BA%E4%BA%8E%E6%95%B0%E6%8D%AE%E5%BA%93),只有获取到锁进程才能写数据。 + +## 如何写缓存 + +写缓存时也要注意,通常来说分为以下几步: + +- 开启事务。 +- 写入 DB 。 +- 提交事务。 +- 写入缓存。 + +这里可能会存在数据库写入成功但是缓存写入失败的情况,但是也不建议将写入缓存加入到事务中。 +因为写缓存的时候可能会因为网络原因耗时较长,这样会阻塞数据库事务。 +如果对一致性要求不高并且数据量也不大的情况下,可以单独起一个服务来做 DB 和缓存之间的数据同步操作。 + +更新缓存时也建议做增量更新。 + +## 负载策略 + +缓存负载策略一般有以下两种: +- 轮询机制。 +- 一致哈希算法。 + +轮询的优点是负载到各个服务器的请求是均匀的,但是如果进行扩容则缓存命中率会下降。 + +一致哈希的优点是相同的请求会负载到同一台服务器上,命中率不会随着扩容而降低,但是当大流量过来时有可能把服务器拖垮。 + +所以建议两种方案都采用: +首先采用一致哈希算法,当流量达到一定的阈值的时候则切换为轮询,这样既能保证缓存命中率,也能提高系统的可用性。 \ No newline at end of file diff --git a/docs/distributed/Distributed-Limit.md b/docs/distributed/Distributed-Limit.md new file mode 100644 index 00000000..c69a06fb --- /dev/null +++ b/docs/distributed/Distributed-Limit.md @@ -0,0 +1,483 @@ +![](https://i.loli.net/2019/06/26/5d1394364203855229.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://i.loli.net/2019/06/26/5d139436e765a73996.jpg) + +效果如下: +![](https://i.loli.net/2019/06/26/5d13943f1d81465048.jpg) + + +![](https://i.loli.net/2019/06/26/5d139440e0b0e36306.jpg) + +![](https://i.loli.net/2019/06/26/5d139441c7bb338785.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/docs/distributed/ID-generator.md b/docs/distributed/ID-generator.md new file mode 100644 index 00000000..04a71d93 --- /dev/null +++ b/docs/distributed/ID-generator.md @@ -0,0 +1,39 @@ +# 分布式 ID 生成器 + +一个唯一 ID 在一个分布式系统中是非常重要的一个业务属性,其中包括一些如订单 ID,消息 ID ,会话 ID,他们都有一些共有的特性: + +- 全局唯一。 +- 趋势递增。 + +全局唯一很好理解,目的就是唯一标识某个次请求,某个业务。 + +通常有以下几种方案: + +## 基于数据库 +可以利用 `MySQL` 中的自增属性 `auto_increment` 来生成全局唯一 ID,也能保证趋势递增。 +但这种方式太依赖 DB,如果数据库挂了那就非常容易出问题。 + +### 水平扩展改进 +但也有改进空间,可以将数据库水平拆分,如果拆为了两个库 A 库和 B 库。 +A 库的递增方式可以是 `0 ,2 ,4 ,6`。B 库则是 `1 ,3 ,5 ,7`。这样的方式可以提高系统可用性,并且 ID 也是趋势递增的。 + +但也有如下一下问题: + +- 想要扩容增加性能变的困难,之前已经定义好了 A B 库递增的步数,新加的数据库不好加入进来,水平扩展困难。 +- 也是强依赖与数据库,并且如果其中一台挂掉了那就不是绝对递增了。 + +## 本地 UUID 生成 +还可以采用 `UUID` 的方式生成唯一 ID,由于是在本地生成没有了网络之类的消耗,所有效率非常高。 + +但也有以下几个问题: +- 生成的 ID 是无序性的,不能做到趋势递增。 +- 由于是字符串并且不是递增,所以不太适合用作主键。 + +## 采用本地时间 +这种做法非常简单,可以利用本地的毫秒数加上一些业务 ID 来生成唯一ID,这样可以做到趋势递增,并且是在本地生成效率也很高。 + +但有一个致命的缺点:当并发量足够高的时候**唯一性**就不能保证了。 + +## Twitter 雪花算法 + +可以基于 `Twitter` 的 `Snowflake` 算法来实现。它主要是一种划分命名空间的算法,将生成的 ID 按照机器、时间等来进行标志。 \ No newline at end of file diff --git a/docs/distributed/distributed-lock-redis.md b/docs/distributed/distributed-lock-redis.md new file mode 100644 index 00000000..b57d052c --- /dev/null +++ b/docs/distributed/distributed-lock-redis.md @@ -0,0 +1,287 @@ +![](https://i.loli.net/2019/06/26/5d139438c1cec87655.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://i.loli.net/2019/06/26/5d139439c08cc20580.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/docs/frame/SpringAOP.md b/docs/frame/SpringAOP.md new file mode 100644 index 00000000..3ebb7f58 --- /dev/null +++ b/docs/frame/SpringAOP.md @@ -0,0 +1,224 @@ +# Spring AOP 实现原理 + +## 静态代理 + +众所周知 Spring 的 `AOP` 是基于动态代理实现的,谈到动态代理就不得不提下静态代理。实现如下: + +假设有一接口 `InterfaceA`: + +```java +public interface InterfaceA{ + void exec(); +} +``` + +其中有实现类 `RealImplement`: +```java +public class RealImplement implement InterfaceA{ + public void exec(){ + System.out.println("real impl") ; + } +} +``` + +这时也有一个代理类 `ProxyImplement` 也实现了 `InterfaceA`: +```java +public class ProxyImplement implement InterfaceA{ + private InterfaceA interface ; + + public ProxyImplement(){ + interface = new RealImplement() ; + } + + public void exec(){ + System.out.println("dosomethings before); + //实际调用 + interface.exec(); + + System.out.println("dosomethings after); + } + +} +``` +使用如下: +``` +public class Main(){ + public static void main(String[] args){ + InterfaceA interface = new ProxyImplement() ; + interface.exec(); + } +} +``` +可以看出这样的代理方式调用者其实都不知道被代理对象的存在。 + +## JDK 动态代理 +从静态代理中可以看出: 静态代理只能代理一个具体的类,如果要代理一个接口的多个实现的话需要定义不同的代理类。 + +需要解决这个问题就可以用到 JDK 的动态代理。 + +其中有两个非常核心的类: + +- `java.lang.reflect.Proxy`类。 +- `java.lang.reflect.InvocationHandle`接口。 + +`Proxy` 类是用于创建代理对象,而 `InvocationHandler` 接口主要你是来处理执行逻辑。 + +如下: +```java +public class CustomizeHandle implements InvocationHandler { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomizeHandle.class); + + private Object target; + + public CustomizeHandle(Class clazz) { + try { + this.target = clazz.newInstance(); + } catch (InstantiationException e) { + LOGGER.error("InstantiationException", e); + } catch (IllegalAccessException e) { + LOGGER.error("IllegalAccessException",e); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + before(); + Object result = method.invoke(target, args); + after(); + + LOGGER.info("proxy class={}", proxy.getClass()); + return result; + } + + + private void before() { + LOGGER.info("handle before"); + } + + private void after() { + LOGGER.info("handle after"); + } +} +``` + +其中构造方法传入被代理类的类类型。其实传代理类的实例或者是类类型并没有强制的规定,传类类型的是因为被代理对象应当有代理创建而不应该由调用方创建。 + +使用方式如下: +```java + @Test + public void test(){ + CustomizeHandle handle = new CustomizeHandle(ISubjectImpl.class) ; + ISubject subject = (ISubject) Proxy.newProxyInstance(JDKProxyTest.class.getClassLoader(), new Class[]{ISubject.class}, handle); + subject.execute() ; + } +``` + +首先传入被代理类的类类型构建代理处理器。接着使用 `Proxy` 的`newProxyInstance` 方法动态创建代理类。第一个参数为类加载器,第二个参数为代理类需要实现的接口列表,最后一个则是处理器。 + +其实代理类是由 + +![](https://i.loli.net/2019/06/26/5d13945f24cb855978.jpg) + +这个方法动态创建出来的。将 proxyClassFile 输出到文件并进行反编译的话就可以的到代理类。 +```java + @Test + public void clazzTest(){ + byte[] proxyClassFile = ProxyGenerator.generateProxyClass( + "$Proxy1", new Class[]{ISubject.class}, 1); + try { + FileOutputStream out = new FileOutputStream("/Users/chenjie/Documents/$Proxy1.class") ; + out.write(proxyClassFile); + out.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } +``` + +反编译后结果如下: +```java +import com.crossoverjie.proxy.jdk.ISubject; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; + +public class $Proxy1 extends Proxy implements ISubject { + private static Method m1; + private static Method m2; + private static Method m3; + private static Method m0; + + public $Proxy1(InvocationHandler var1) throws { + super(var1); + } + + public final boolean equals(Object var1) throws { + try { + return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue(); + } catch (RuntimeException | Error var3) { + throw var3; + } catch (Throwable var4) { + throw new UndeclaredThrowableException(var4); + } + } + + public final String toString() throws { + try { + return (String)super.h.invoke(this, m2, (Object[])null); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + public final void execute() throws { + try { + super.h.invoke(this, m3, (Object[])null); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + public final int hashCode() throws { + try { + return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue(); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + static { + try { + m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")}); + m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); + m3 = Class.forName("com.crossoverjie.proxy.jdk.ISubject").getMethod("execute", new Class[0]); + m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); + } catch (NoSuchMethodException var2) { + throw new NoSuchMethodError(var2.getMessage()); + } catch (ClassNotFoundException var3) { + throw new NoClassDefFoundError(var3.getMessage()); + } + } +} +``` + +可以看到代理类继承了 `Proxy` 类,并实现了 `ISubject` 接口,由此也可以看到 JDK 动态代理为什么需要实现接口,已经继承了 `Proxy`是不能再继承其余类了。 + +其中实现了 `ISubject` 的 `execute()` 方法,并通过 `InvocationHandler` 中的 `invoke()` 方法来进行调用的。 + + +## CGLIB 动态代理 + +cglib 是对一个小而快的字节码处理框架 `ASM` 的封装。 +他的特点是继承于被代理类,这就要求被代理类不能被 `final` 修饰。 + + diff --git a/docs/frame/guava-cache.md b/docs/frame/guava-cache.md new file mode 100644 index 00000000..8c35b72d --- /dev/null +++ b/docs/frame/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://i.loli.net/2019/06/26/5d13945fe1cae45017.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://i.loli.net/2019/06/26/5d139460a52cf85772.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://i.loli.net/2019/06/26/5d139461408fd93335.jpg) + +具有强引用和弱引用的不同实现。 + +key 也是相同的道理: + +![](https://i.loli.net/2019/06/26/5d139461d363838006.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://i.loli.net/2019/06/26/5d13946796ed610501.jpg) + +根据 LocalCache 中的 `getLiveValue()` 中判断缓存过期时,跟着这里的调用关系就会一直跟到: + +![](https://i.loli.net/2019/06/26/5d139468716d365202.jpg) + +`removeValueFromChain()` 中的: + +![](https://i.loli.net/2019/06/26/5d1394692e8d362414.jpg) + +`enqueueNotification()` 方法会将回收的缓存(包含了 key,value)以及回收原因包装成之前定义的事件接口加入到一个**本地队列**中。 + +![](https://i.loli.net/2019/06/26/5d139469c776a45831.jpg) + +这样一看也没有回调我们初始化时候的事件啊。 + +不过用过队列的同学应该能猜出,既然这里写入队列,那就肯定就有消费。 + +我们回到获取缓存的地方: + +![](https://i.loli.net/2019/06/26/5d13946c8960257603.jpg) + +在 finally 中执行了 `postReadCleanup()` 方法;其实在这里面就是对刚才的队列进行了消费: + +![](https://i.loli.net/2019/06/26/5d139471de1d710535.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/docs/frame/kafka-consumer.md b/docs/frame/kafka-consumer.md new file mode 100755 index 00000000..97d8269d --- /dev/null +++ b/docs/frame/kafka-consumer.md @@ -0,0 +1,204 @@ + + +![](https://i.loli.net/2019/06/26/5d1394729707f14762.jpg) + + +# 前言 + +之前写过一篇[《从源码分析如何优雅的使用 Kafka 生产者》](https://crossoverjie.top/2018/10/11/kafka/kafka-product/) ,有生产者自然也就有消费者。 + +> 建议对 Kakfa 还比较陌生的朋友可以先看看。 + +就我的使用经验来说,大部分情况都是处于数据下游的消费者角色。也用 `Kafka` 消费过日均过亿的消息(不得不佩服 Kakfa 的设计),本文将借助我使用 Kakfa 消费数据的经验来聊聊如何高效的消费数据。 + + + +# 单线程消费 + +以之前生产者中的代码为例,事先准备好了一个 `Topic:data-push`,3个分区。 + +先往里边发送 100 条消息,没有自定义路由策略,所以消息会均匀的发往三个分区。 + +先来谈谈最简单的单线程消费,如下图所示: + +![](https://i.loli.net/2019/06/26/5d1394735c50464148.jpg) + +由于数据散列在三个不同分区,所以单个线程需要遍历三个分区将数据拉取下来。 + +单线程消费的示例代码: + +![](https://i.loli.net/2019/06/26/5d139473f18c354807.jpg) + +这段代码大家在官网也可以找到:将数据取出放到一个内存缓冲中最后写入数据库的过程。 + +> 先不讨论其中的 offset 的提交方式。 + +![](https://i.loli.net/2019/06/26/5d139474d871a64091.jpg) + +![](https://i.loli.net/2019/06/26/5d13947ae42a961451.jpg) + +通过消费日志可以看出: + +取出的 100 条数据确实是分别遍历了三个分区。 + +单线程消费虽然简单,但存在以下几个问题: + +- 效率低下。如果分区数几十上百个,单线程无法高效的取出数据。 +- 可用性很低。一旦消费线程阻塞,甚至是进程挂掉,那么整个消费程序都将出现问题。 + +# 多线程消费 + +既然单线程有诸多问题,那是否可以用多线程来提高效率呢? + +在多线程之前不得不将消费模式分为两种进行探讨:消费组、独立消费者。 + +这两种消费模式对应的处理方式有着很大的不同,所以很有必要单独来讲。 + +## 独立消费者模式 + +先从`独立消费者模式`谈起,这种模式相对于消费组来说用的相对小众一些。 + +看一个简单示例即可知道它的用法: + +![](https://i.loli.net/2019/06/26/5d13947b7cc9386064.jpg) + +> 值得注意的是:独立消费者可以不设置 group.id 属性。 + +也是发送100条消息,消费结果如下: + +![](https://i.loli.net/2019/06/26/5d13947c15efb51481.jpg) + +通过 API 可以看出:我们可以手动指定需要消费哪些分区。 + +比如 `data-push` Topic 有三个分区,我可以手动只消费其中的 1 2 分区,第三个可以视情况来消费。 + +同时它也支持多线程的方式,每个线程消费指定分区进行消费。 + +![](https://i.loli.net/2019/06/26/5d13947cb8d4395802.jpg) + +![](https://i.loli.net/2019/06/26/5d13947de7b6033298.jpg) + +为了直观,只发送了 10 条数据。 + +![](https://i.loli.net/2019/06/26/5d139483f2cd338378.jpg) + +根据消费结果可以看出: + +c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 + +甚至我们可以将消费者多进程部署,这样的消费方式如下: + +![](https://i.loli.net/2019/06/26/5d139484a4f5012233.jpg) + +假设 `Topic:data-push` 的分区数为 4 个,那我们就可以按照图中的方式创建两个进程。 + +每个进程内有两个线程,每个线程再去消费对应的分区。 + +这样当我们性能不够新增 Topic 的分区数时,消费者这边只需要这样水平扩展即可,非常的灵活。 + + +这种自定义分区消费的方式在某些场景下还是适用的,比如生产者每次都将某一类的数据只发往一个分区。这样我们就可以只针对这一个分区消费。 + +但这种方式有一个问题:可用性不高,当其中一个进程挂掉之后;该进程负责的分区数据没法转移给其他进程处理。 + +## 消费组模式 + +消费组模式应当是使用最多的一种消费方式。 + +我们可以创建 N 个消费者实例(`new KafkaConsumer()`),当这些实例都用同一个 `group.id` 来创建时,他们就属于同一个消费组。 + +在同一个消费组中的消费实例可以收到消息,但一个分区的消息只会发往一个消费实例。 + + +还是借助官方的示例图来更好的理解它。 + +![](https://i.loli.net/2019/06/26/5d13948b443d263987.jpg) + +某个 Topic 有四个分区 `p0 p1 p2 p3`,同时创建了两个消费组 `groupA,groupB`。 + +- A 消费组中有两个消费实例 `C1、C2`。 +- B 消费组中有四个消费实例 `C3、C4、C5、C6`。 + +这样消息是如何划分到每个消费实例的呢? + +通过图中可以得知: + +- A 组中的 C1 消费了 P0 和 P3 分区;C2 消费 P1、P2 分区。 +- B 组有四个实例,所以每个实例消费一个分区;也就是消费实例和分区是一一对应的。 + +需要注意的是: + +> 这里的消费实例简单的可以理解为 `new KafkaConsumer`,**它和进程没有关系**。 + +--- + +比如说某个 Topic 有三个分区,但是我启动了两个进程来消费它。 + +其中每个进程有两个消费实例,那其实就相当于有四个实例了。 + +这时可能就会问 4 个实例怎么消费 3 个分区呢? + + +# 消费组自平衡 + +这个 Kafka 已经帮我做好了,它会来做消费组里的 `Rebalance`。 + +比如上面的情况,3 个分区却有 4 个消费实例;最终肯定只有三个实例能取到消息。但至于是哪三个呢,这点 Kakfa 会自动帮我们分配好。 + +看个例子,还在之前的 `data-push` 这个 Topic,其中有三个分区。 + +当其中一个进程(其中有三个线程,每个线程对应一个消费实例)时,消费结果如下: + +![](https://i.loli.net/2019/06/26/5d13948bc771365298.jpg) + +里边的 20 条数据都被这个进程的三个实例消费掉。 + +这时我新启动了一个进程,程序和上面那个一模一样;这样就相当于有两个进程,同时就是 6 个实例。 + +我再发送 10 条消息会发现: + +进程1 只取到了分区 1 里的两条数据(之前是所有数据都是进程1里的线程获取的)。 + +![](https://i.loli.net/2019/06/26/5d13948d56c0b79122.jpg) + +--- + +同时进程2则消费了剩下的 8 条消息,分别是分区 0、2 的数据(总的还是只有三个实例取到了数据,只是分别在不同的进程里)。 + +![](https://ws1.sinaimg.cn/large/006tNbRwly1fxdwm0ari3j30zy06a4ei.jpg) + +--- + +当我关掉进程2,再发送10条数据时会发现所有数据又被进程1里的三个线程消费了。 + +![](https://i.loli.net/2019/06/26/5d1395ff039d368487.jpg) + +通过这些测试相信大家已经可以看到消费组的优势了。 + +> 我们可以在一个消费组中创建多个消费实例来达到高可用、高容错的特性,不会出现单线程以及独立消费者挂掉之后数据不能消费的情况。同时基于多线程的方式也极大的提高了消费效率。 + + +而当新增消费实例或者是消费实例挂掉时 `Kakfa` 会为我们重新分配消费实例与分区的关系就被称为消费组 `Rebalance`。 + +发生这个的前提条件一般有以下几个: + +- 消费组中新增消费实例。 +- 消费组中消费实例 down 掉。 +- 订阅的 Topic 分区数发生变化。 +- 如果是正则订阅 Topic 时,匹配的 Topic 数发生变化也会导致 `Rebalance`。 + + +所以推荐使用这样的方式消费数据,同时扩展性也非常好。当性能不足新增分区时只需要启动新的消费实例加入到消费组中即可。 + + +# 总结 + +本次只分享了几个不同消费数据的方式,并没有着重研究消费参数、源码;这些内容感兴趣的话可以在下次分享。 + +文中提到的部分源码可以在这里查阅: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) + + + +**欢迎关注公众号一起交流:** diff --git a/docs/frame/kafka-product.md b/docs/frame/kafka-product.md new file mode 100755 index 00000000..d50364a8 --- /dev/null +++ b/docs/frame/kafka-product.md @@ -0,0 +1,324 @@ + +# 从源码分析如何优雅的使用 Kafka 生产者 + + +![](https://i.loli.net/2019/06/26/5d13945f3952999092.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://i.loli.net/2019/06/26/5d13945fedfe948763.jpg) + +主要关注 `bootstrap.servers`,它是必填参数。指的是 Kafka 集群中的 broker 地址,例如 `127.0.0.1:9094`。 + +> 其余几个参数暂时不做讨论,后文会有详细介绍。 + +接着注入这个 bean 即可调用它的发送函数发送消息。 + +![](https://i.loli.net/2019/06/26/5d1394607779261290.jpg) + +这里我给某一个 Topic 发送了 10W 条数据,运行程序消息正常发送。 + +但这仅仅只是做到了消息发送,对消息是否成功送达完全没管,等于是纯`异步`的方式。 + +## 同步 + +那么我想知道消息到底发送成功没有该怎么办呢? + +其实 `Producer` 的 `API` 已经帮我们考虑到了,发送之后只需要调用它的 `get()` 方法即可同步获取发送结果。 + +![](https://i.loli.net/2019/06/26/5d139460ec48465745.jpg) + +发送结果: + +![](https://i.loli.net/2019/06/26/5d1394624bd1b33069.jpg) + +这样的发送效率其实是比较低下的,因为每次都需要同步等待消息发送的结果。 + +## 异步 + +为此我们应当采取异步的方式发送,其实 `send()` 方法默认则是异步的,只要不手动调用 `get()` 方法。 + +但这样就没法获知发送结果。 + +所以查看 `send()` 的 API 可以发现还有一个参数。 + +```java +Future send(ProducerRecord producer, Callback callback); +``` + +`Callback` 是一个回调接口,在消息发送完成之后可以回调我们自定义的实现。 + +![](https://i.loli.net/2019/06/26/5d139467efab428313.jpg) + +执行之后的结果: + +![](https://i.loli.net/2019/06/26/5d139468a896053832.jpg) + +同样的也能获取结果,同时发现回调的线程并不是上文同步时的`主线程`,这样也能证明是异步回调的。 + +同时回调的时候会传递两个参数: + +- `RecordMetadata` 和上文一致的消息发送成功后的元数据。 +- `Exception` 消息发送过程中的异常信息。 + +但是这两个参数并不会同时都有数据,只有发送失败才会有异常信息,同时发送元数据为空。 + +所以正确的写法应当是: + +![](https://i.loli.net/2019/06/26/5d13946a0678117891.jpg) + +> 至于为什么会只有参数一个有值,在下文的源码分析中会一一解释。 + + +# 源码分析 + +现在只掌握了基本的消息发送,想要深刻的理解发送中的一些参数配置还是得源码说了算。 + +首先还是来谈谈消息发送时的整个流程是怎么样的,`Kafka` 并不是简单的把消息通过网络发送到了 `broker` 中,在 Java 内部还是经过了许多优化和设计。 + +## 发送流程 + +为了直观的了解发送的流程,简单的画了几个在发送过程中关键的步骤。 + +![](https://i.loli.net/2019/06/26/5d13946b61a1015175.jpg) + +从上至下依次是: + +- 初始化以及真正发送消息的 `kafka-producer-network-thread` IO 线程。 +- 将消息序列化。 +- 得到需要发送的分区。 +- 写入内部的一个缓存区中。 +- 初始化的 IO 线程不断的消费这个缓存来发送消息。 + +## 步骤解析 + +接下来详解每个步骤。 + +### 初始化 + + +![](https://i.loli.net/2019/06/26/5d13946bef0f188816.jpg) + +调用该构造方法进行初始化时,不止是简单的将基本参数写入 `KafkaProducer`。比较麻烦的是初始化 `Sender` 线程进行缓冲区消费。 + +初始化 IO 线程处: + +![kafka-product.md---006tNbRwly1fw3jh4xtt2j31fo02pgms.jpg](https://i.loli.net/2019/06/26/5d1395b88cb5d97051.jpg) + + +可以看到 Sender 线程有需要成员变量,比如: + +``` +acks,retries,requestTimeout +``` + +等,这些参数会在后文分析。 + +### 序列化消息 + +在调用 `send()` 函数后其实第一步就是序列化,毕竟我们的消息需要通过网络才能发送到 Kafka。 + +![](https://i.loli.net/2019/06/26/5d139473088b949912.jpg) + +其中的 `valueSerializer.serialize(record.topic(), record.value());` 是一个接口,我们需要在初始化时候指定序列化实现类。 + +![](https://i.loli.net/2019/06/26/5d139473ac2a494720.jpg) + +我们也可以自己实现序列化,只需要实现 `org.apache.kafka.common.serialization.Serializer` 接口即可。 + +### 路由分区 + +接下来就是路由分区,通常我们使用的 `Topic` 为了实现扩展性以及高性能都会创建多个分区。 + +如果是一个分区好说,所有消息都往里面写入即可。 + +但多个分区就不可避免需要知道写入哪个分区。 + +通常有三种方式。 + +#### 指定分区 + +可以在构建 `ProducerRecord` 为每条消息指定分区。 + +![](https://i.loli.net/2019/06/26/5d139474258e055711.jpg) + +这样在路由时会判断是否有指定,有就直接使用该分区。 + +![](https://i.loli.net/2019/06/26/5d13947490e3c51457.jpg) + +这种一般在特殊场景下会使用。 + +#### 自定义路由策略 + +![](https://i.loli.net/2019/06/26/5d13947582be674626.jpg) + +如果没有指定分区,则会调用 `partitioner.partition` 接口执行自定义分区策略。 + +而我们也只需要自定义一个类实现 `org.apache.kafka.clients.producer.Partitioner` 接口,同时在创建 `KafkaProducer` 实例时配置 `partitioner.class` 参数。 + +![](https://i.loli.net/2019/06/26/5d13947a8e98b39707.jpg) + +通常需要自定义分区一般是在想尽量的保证消息的顺序性。 + +或者是写入某些特有的分区,由特别的消费者来进行处理等。 + +#### 默认策略 + +最后一种则是默认的路由策略,如果我们啥都没做就会执行该策略。 + +该策略也会使得消息分配的比较均匀。 + +来看看它的实现: + +![](https://i.loli.net/2019/06/26/5d13947b2fbb155310.jpg) + +简单的来说分为以下几步: + +- 获取 Topic 分区数。 +- 将内部维护的一个线程安全计数器 +1。 +- 与分区数取模得到分区编号。 + +其实这就是很典型的轮询算法,所以只要分区数不频繁变动这种方式也会比较均匀。 + +### 写入内部缓存 + +在 `send()` 方法拿到分区后会调用一个 `append()` 函数: + +![](https://i.loli.net/2019/06/26/5d13947c189cf45913.jpg) + +该函数中会调用一个 `getOrCreateDeque()` 写入到一个内部缓存中 `batches`。 + +![](https://i.loli.net/2019/06/26/5d13947c8cf8b64631.jpg) + + +### 消费缓存 + +在最开始初始化的 IO 线程其实是一个守护线程,它会一直消费这些数据。 + +![](https://i.loli.net/2019/06/26/5d13947e0f60822234.jpg) + +通过图中的几个函数会获取到之前写入的数据。这块内容可以不必深究,但其中有个 `completeBatch` 方法却非常关键。 + +![](https://i.loli.net/2019/06/26/5d139483ba47613836.jpg) + +调用该方法时候肯定已经是消息发送完毕了,所以会调用 `batch.done()` 来完成之前我们在 `send()` 方法中定义的回调接口。 + +![](https://i.loli.net/2019/06/26/5d13948a61cbc31617.jpg) + +> 从这里也可以看出为什么之前说发送完成后元数据和异常信息只会出现一个。 + +# Producer 参数解析 + +发送流程讲完了再来看看 `Producer` 中比较重要的几个参数。 + +## acks + +`acks` 是一个影响消息吞吐量的一个关键参数。 + +![](https://i.loli.net/2019/06/26/5d13948ae180b18955.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://i.loli.net/2019/06/26/5d13948bbdf5832883.jpg) + +![](https://i.loli.net/2019/06/26/5d13948d0d86f22526.jpg) + +上图是几个使用的体现。 + + +## retries + +`retries` 该参数主要是来做重试使用,当发生一些网络抖动都会造成重试。 + +这个参数也就是限制重试次数。 + +但也有一些其他问题。 + +- 因为是重发所以消息顺序可能不会一致,这也是上文提到就算是一个分区消息也不会是完全顺序的情况。 +- 还是由于网络问题,本来消息已经成功写入了但是没有成功响应给 producer,进行重试时就可能会出现`消息重复`。这种只能是消费者进行幂等处理。 + +# 高效的发送方式 + +如果消息量真的非常大,同时又需要尽快的将消息发送到 `Kafka`。一个 `producer` 始终会收到缓存大小等影响。 + +那是否可以创建多个 `producer` 来进行发送呢? + +- 配置一个最大 producer 个数。 +- 发送消息时首先获取一个 `producer`,获取的同时判断是否达到最大上限,没有就新建一个同时保存到内部的 `List` 中,保存时做好同步处理防止并发问题。 +- 获取发送者时可以按照默认的分区策略使用轮询的方式获取(保证使用均匀)。 + +这样在大量、频繁的消息发送场景中可以提高发送效率减轻单个 `producer` 的压力。 + +# 关闭 Producer + +最后则是 `Producer` 的关闭,Producer 在使用过程中消耗了不少资源(线程、内存、网络等)因此需要显式的关闭从而回收这些资源。 + + +![](https://i.loli.net/2019/06/26/5d13948e08f3a58866.jpg) + +默认的 `close()` 方法和带有超时时间的方法都是在一定的时间后强制关闭。 + +但在过期之前都会处理完剩余的任务。 + +所以使用哪一个得视情况而定。 + + +# 总结 + +本文内容较多,从实例和源码的角度分析了 Kafka 生产者。 + +希望看完的朋友能有收获,同时也欢迎留言讨论。 + +不出意外下期会讨论 Kafka 消费者。 + +> 如果对你有帮助还请分享让更多的人看到。 + +**欢迎关注公众号一起交流:** + + diff --git a/docs/frame/spring-bean-lifecycle.md b/docs/frame/spring-bean-lifecycle.md new file mode 100644 index 00000000..60f2796a --- /dev/null +++ b/docs/frame/spring-bean-lifecycle.md @@ -0,0 +1,181 @@ +## Spring Bean 生命周期 + + +### 前言 + +Spring Bean 的生命周期在整个 Spring 中占有很重要的位置,掌握这些可以加深对 Spring 的理解。 + +首先看下生命周期图: + +![](https://i.loli.net/2018/09/20/5ba2e83a54fd9.jpeg) + +再谈生命周期之前有一点需要先明确: + +> Spring 只帮我们管理单例模式 Bean 的**完整**生命周期,对于 prototype 的 bean ,Spring 在创建好交给使用者之后则不会再管理后续的生命周期。 + + +### 注解方式 + +在 bean 初始化时会经历几个阶段,首先可以使用注解 `@PostConstruct`, `@PreDestroy` 来在 bean 的创建和销毁阶段进行调用: + +```java +@Component +public class AnnotationBean { + private final static Logger LOGGER = LoggerFactory.getLogger(AnnotationBean.class); + + @PostConstruct + public void start(){ + LOGGER.info("AnnotationBean start"); + } + + @PreDestroy + public void destroy(){ + LOGGER.info("AnnotationBean destroy"); + } +} +``` + +### InitializingBean, DisposableBean 接口 + +还可以实现 `InitializingBean,DisposableBean` 这两个接口,也是在初始化以及销毁阶段调用: + +```java +@Service +public class SpringLifeCycleService implements InitializingBean,DisposableBean{ + private final static Logger LOGGER = LoggerFactory.getLogger(SpringLifeCycleService.class); + @Override + public void afterPropertiesSet() throws Exception { + LOGGER.info("SpringLifeCycleService start"); + } + + @Override + public void destroy() throws Exception { + LOGGER.info("SpringLifeCycleService destroy"); + } +} +``` + +### 自定义初始化和销毁方法 + +也可以自定义方法用于在初始化、销毁阶段调用: + +```java +@Configuration +public class LifeCycleConfig { + + + @Bean(initMethod = "start", destroyMethod = "destroy") + public SpringLifeCycle create(){ + SpringLifeCycle springLifeCycle = new SpringLifeCycle() ; + + return springLifeCycle ; + } +} + +public class SpringLifeCycle{ + + private final static Logger LOGGER = LoggerFactory.getLogger(SpringLifeCycle.class); + public void start(){ + LOGGER.info("SpringLifeCycle start"); + } + + + public void destroy(){ + LOGGER.info("SpringLifeCycle destroy"); + } +} +``` + +以上是在 SpringBoot 中可以这样配置,如果是原始的基于 XML 也是可以使用: + +```xml + + +``` + +来达到同样的效果。 + +### 实现 *Aware 接口 + +`*Aware` 接口可以用于在初始化 bean 时获得 Spring 中的一些对象,如获取 `Spring 上下文`等。 + +```java +@Component +public class SpringLifeCycleAware implements ApplicationContextAware { + private final static Logger LOGGER = LoggerFactory.getLogger(SpringLifeCycleAware.class); + + private ApplicationContext applicationContext ; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext ; + LOGGER.info("SpringLifeCycleAware start"); + } +} +``` + +这样在 `springLifeCycleAware` 这个 bean 初始化会就会调用 `setApplicationContext` 方法,并可以获得 `applicationContext` 对象。 + +### BeanPostProcessor 增强处理器 + +实现 BeanPostProcessor 接口,Spring 中所有 bean 在做初始化时都会调用该接口中的两个方法,可以用于对一些特殊的 bean 进行处理: + +```java +@Component +public class SpringLifeCycleProcessor implements BeanPostProcessor { + private final static Logger LOGGER = LoggerFactory.getLogger(SpringLifeCycleProcessor.class); + + /** + * 预初始化 初始化之前调用 + * @param bean + * @param beanName + * @return + * @throws BeansException + */ + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if ("annotationBean".equals(beanName)){ + LOGGER.info("SpringLifeCycleProcessor start beanName={}",beanName); + } + return bean; + } + + /** + * 后初始化 bean 初始化完成调用 + * @param bean + * @param beanName + * @return + * @throws BeansException + */ + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if ("annotationBean".equals(beanName)){ + LOGGER.info("SpringLifeCycleProcessor end beanName={}",beanName); + } + return bean; + } +} +``` + +执行之后观察结果: + +``` +018-03-21 00:40:24.856 [restartedMain] INFO c.c.s.p.SpringLifeCycleProcessor - SpringLifeCycleProcessor start beanName=annotationBean +2018年03月21日 00:40:24.860 [restartedMain] INFO c.c.spring.annotation.AnnotationBean - AnnotationBean start +2018年03月21日 00:40:24.861 [restartedMain] INFO c.c.s.p.SpringLifeCycleProcessor - SpringLifeCycleProcessor end beanName=annotationBean +2018年03月21日 00:40:24.864 [restartedMain] INFO c.c.s.aware.SpringLifeCycleAware - SpringLifeCycleAware start +2018年03月21日 00:40:24.867 [restartedMain] INFO c.c.s.service.SpringLifeCycleService - SpringLifeCycleService start +2018年03月21日 00:40:24.887 [restartedMain] INFO c.c.spring.SpringLifeCycle - SpringLifeCycle start +2018年03月21日 00:40:25.062 [restartedMain] INFO o.s.b.d.a.OptionalLiveReloadServer - LiveReload server is running on port 35729 +2018年03月21日 00:40:25.122 [restartedMain] INFO o.s.j.e.a.AnnotationMBeanExporter - Registering beans for JMX exposure on startup +2018年03月21日 00:40:25.140 [restartedMain] INFO com.crossoverjie.Application - Started Application in 2.309 seconds (JVM running for 3.681) +2018年03月21日 00:40:25.143 [restartedMain] INFO com.crossoverjie.Application - start ok! +2018年03月21日 00:40:25.153 [Thread-8] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3913adad: startup date [Wed Mar 21 00:40:23 CST 2018]; root of context hierarchy +2018年03月21日 00:40:25.155 [Thread-8] INFO o.s.j.e.a.AnnotationMBeanExporter - Unregistering JMX-exposed beans on shutdown +2018年03月21日 00:40:25.156 [Thread-8] INFO c.c.spring.SpringLifeCycle - SpringLifeCycle destroy +2018年03月21日 00:40:25.156 [Thread-8] INFO c.c.s.service.SpringLifeCycleService - SpringLifeCycleService destroy +2018年03月21日 00:40:25.156 [Thread-8] INFO c.c.spring.annotation.AnnotationBean - AnnotationBean destroy +``` + +直到 Spring 上下文销毁时则会调用自定义的销毁方法以及实现了 `DisposableBean` 的 `destroy()` 方法。 + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..d235b3fc --- /dev/null +++ b/docs/index.html @@ -0,0 +1,43 @@ + + + + + JCSprout + + + + + +

        JCSprout

        + + + + + + + + +

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

        + diff --git a/docs/jvm/ClassLoad.md b/docs/jvm/ClassLoad.md new file mode 100644 index 00000000..3578ad71 --- /dev/null +++ b/docs/jvm/ClassLoad.md @@ -0,0 +1,17 @@ +# 类加载机制 + +## 双亲委派模型 + +模型如下图: + +![](https://i.loli.net/2019/07/19/5d31384d5ecdf51413.jpg) + +双亲委派模型中除了启动类加载器之外其余都需要有自己的父类加载器 + +当一个类收到了类加载请求时: 自己不会首先加载,而是委派给父加载器进行加载,每个层次的加载器都是这样。 + +所以最终每个加载请求都会经过启动类加载器。只有当父类加载返回不能加载时子加载器才会进行加载。 + +双亲委派的好处 : 由于每个类加载都会经过最顶层的启动类加载器,比如 `java.lang.Object`这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。) + +如果没有双亲委派模型,由各个类加载器自行加载的话。当用户自己编写了一个 `java.lang.Object`类,那样系统中就会出现多个 `Object`,这样 Java 程序中最基本的行为都无法保证,程序会变的非常混乱。 diff --git a/docs/jvm/GarbageCollection.md b/docs/jvm/GarbageCollection.md new file mode 100644 index 00000000..eeb3a43a --- /dev/null +++ b/docs/jvm/GarbageCollection.md @@ -0,0 +1,72 @@ +# 垃圾回收 + +> 垃圾回收主要思考三件事情: + +- 哪种内存需要回收? +- 什么时候回收? +- 怎么回收? + +## 对象是否存活 + +### 引用计数法 + +这是一种非常简单易理解的回收算法。每当有一个地方引用一个对象的时候则在引用计数器上 +1,当失效的时候就 -1,无论什么时候计数器为 0 的时候则认为该对象死亡可以回收了。 + +这种算法虽然简单高效,但是却无法解决**循环引用**的问题,因此 Java 虚拟机并没有采用这种算法。 + +### 可达性分析算法 +主流的语言其实都是采用可达性分析算法: + +可达性算法是通过一个称为 `GC Roots` 的对象向下搜索,整个搜索路径就称为引用链,当一个对象到 `GC Roots` 没有任何引用链 `JVM` 就认为该对象是可以被回收的。 + +![](https://i.loli.net/2019/07/19/5d313829b468683360.jpg) + +如图:Object1、2、3、4 都是存活的对象,而 Object5、6、7都是可回收对象。 + +可以用作 `GC-Roots` 的对象有: + +- 方法区中静态变量所引用的对象。 +- 虚拟机栈中所引用的对象。 + +## 垃圾回收算法 + +### 标记-清除算法 + +标记清除算法分为两个步骤,标记和清除。 +首先将**不需要回收的对象**标记起来,然后再清除其余可回收对象。但是存在两个主要的问题: +- 标记和清除的效率都不高。 +- 清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。 + +标记清除过程如下: + +![](https://i.loli.net/2019/07/19/5d31382a842c844446.jpg) + +### 复制算法 + +复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。 + +这样简单高效,而且还不存在标记清除算法中的内存碎片问题,但就是有点浪费内存。 + +> 在新生代会使用该算法。 + +新生代中分为一个 `Eden` 区和两个 `Survivor` 区。通常两个区域的比例是 `8:1:1` ,使用时会用到 `Eden` 区和其中一个 `Survivor` 区。当发生回收时则会将还存活的对象从 `Eden` ,`Survivor` 区拷贝到另一个 `Survivor` 区,当该区域内存也不足时则会使用分配担保利用老年代来存放内存。 + +复制算法过程: + +![](https://i.loli.net/2019/07/19/5d31382aea89b37377.jpg) + + +### 标记整理算法 + +复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。 + +所以老年代中使用的时候`标记整理算法`,它的原理和`标记清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。 + +![](https://i.loli.net/2019/07/19/5d31382b3ca8f11151.jpg) + +### 分代回收算法 +现代多数的商用 `JVM` 的垃圾收集器都是采用的分代回收算法,和之前所提到的算法并没有新的内容。 + +只是将 Java 堆分为了新生代和老年代。由于新生代中存活对象较少,所以采用**复制算法**,简单高效。 + +而老年代中对象较多,并且没有可以担保的内存区域,所以一般采用**标记清除或者是标记整理算法**。 diff --git a/docs/jvm/JVM-concurrent-HashSet-problem.md b/docs/jvm/JVM-concurrent-HashSet-problem.md new file mode 100755 index 00000000..f834b47a --- /dev/null +++ b/docs/jvm/JVM-concurrent-HashSet-problem.md @@ -0,0 +1,251 @@ + + +![](https://i.loli.net/2019/07/19/5d3138498107383830.jpg) + +# 背景 + +上午刚到公司,准备开始一天的摸鱼之旅时突然收到了一封监控中心的邮件。 + +心中暗道不好,因为监控系统从来不会告诉我应用完美无 `bug`,其实系统挺猥琐。 + + + +打开邮件一看,果然告知我有一个应用的线程池队列达到阈值触发了报警。 + +由于这个应用出问题非常影响用户体验;于是立马让运维保留现场 `dump` 线程和内存同时重启应用,还好重启之后恢复正常。于是开始着手排查问题。 + + + +# 分析 + +首先了解下这个应用大概是做什么的。 + +简单来说就是从 `MQ` 中取出数据然后丢到后面的业务线程池中做具体的业务处理。 + +而报警的队列正好就是这个线程池的队列。 + + + +跟踪代码发现构建线程池的方式如下: + +```java +ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue());; + put(poolName,executor); +``` + +采用的是默认的 `LinkedBlockingQueue` 并没有指定大小(这也是个坑),于是这个队列的默认大小为 `Integer.MAX_VALUE`。 + +由于应用已经重启,只能从仅存的线程快照和内存快照进行分析。 + + + +## 内存分析 + +先利用 `MAT` 分析了内存,的到了如下报告。 + + + +![](https://i.loli.net/2019/07/19/5d3138538800334258.jpg) + + + +其中有两个比较大的对象,一个就是之前线程池存放任务的 `LinkedBlockingQueue`,还有一个则是 `HashSet`。 + + + +当然其中队列占用了大量的内存,所以优先查看,`HashSet` 一会儿再看。 + + + +> 由于队列的大小给的够大,所以结合目前的情况来看应当是线程池里的任务处理较慢,导致队列的任务越堆越多,至少这是目前可以得出的结论。 + + + +## 线程分析 + +再来看看线程的分析,这里利用 [fastthread.io](http://fastthread.io/index.jsp) 这个网站进行线程分析。 + +因为从表现来看线程池里的任务迟迟没有执行完毕,所以主要看看它们在干嘛。 + + + +正好他们都处于 **RUNNABLE** 状态,同时堆栈如下: + + + +![](https://i.loli.net/2019/07/19/5d3138551b39c76261.jpg) + +发现正好就是在处理上文提到的 `HashSet`,看这个堆栈是在查询 `key` 是否存在。通过查看 312 行的业务代码确实也是如此。 + + + +> 这里的线程名字也是个坑,让我找了好久。 + + + +# 定位 + +分析了内存和线程的堆栈之后其实已经大概猜出一些问题了。 + + + +这里其实有一个前提忘记讲到: + +这个告警是`凌晨三点`发出的邮件,但并没有电话提醒之类的,所以大家都不知道。 + +到了早上上班时才发现并立即 `dump` 了上面的证据。 + + + +所有有一个很重要的事实:**这几个业务线程在查询 `HashSet` 的时候运行了 6 7 个小时都没有返回**。 + + + +通过之前的监控曲线图也可以看出: + +![](https://i.loli.net/2019/07/19/5d313854e9fa082346.jpg) + +操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。 + + + +同时发现这个应用生产上运行的是 `JDK1.7` ,所以我初步认为应该是在查询 key 的时候进入了 `HashMap` 的环形链表导致 `CPU` 高负载同时也进入了死循环。 + + + +为了验证这个问题再次 review 了代码。 + + + +整理之后的伪代码如下: + +```java +//线程池 +private ExecutorService executor; + +private Set set = new hashSet(); + +private void execute(){ + + while(true){ + //从 MQ 中获取数据 + String key = subMQ(); + executor.excute(new Worker(key)) ; + } +} + +public class Worker extends Thread{ + private String key ; + + public Worker(String key){ + this.key = key; + } + + @Override + private void run(){ + if(!set.contains(key)){ + + //数据库查询 + if(queryDB(key)){ + set.add(key); + return; + } + } + + //达到某种条件时清空 set + if(flag){ + set = null ; + } + } +} +``` + +大致的流程如下: + +- 源源不断的从 MQ 中获取数据。 +- 将数据丢到业务线程池中。 +- 判断数据是否已经写入了 `Set`。 +- 没有则查询数据库。 +- 之后写入到 `Set` 中。 + + + +这里有一个很明显的问题,**那就是作为共享资源的 Set 并没有做任何的同步处理**。 + +这里会有多个线程并发的操作,由于 `HashSet` 其实本质上就是 `HashMap`,所以它肯定是线程不安全的,所以会出现两个问题: + + + +- Set 中的数据在并发写入时被覆盖导致数据不准确。 +- **会在扩容的时候形成环形链表**。 + +第一个问题相对于第二个还能接受。 + + + +通过上文的内存分析我们已经知道这个 set 中的数据已经不少了。同时由于初始化时并没有指定大小,仅仅只是默认值,所以在大量的并发写入时候会导致频繁的扩容,而在 1.7 的条件下又可能会形成**环形链表**。 + + + +不巧的是代码中也有查询操作(`contains()`),观察上文的堆栈情况: + +![](https://i.loli.net/2019/07/19/5d3138551b39c76261.jpg) + +发现是运行在 `HashMap` 的 465 行,来看看 1.7 中那里具体在做什么: + +![](https://i.loli.net/2019/07/19/5d313855acb5545919.jpg) + +已经很明显了。这里在遍历链表,同时由于形成了环形链表导致这个 `e.next` 永远不为空,所以这个循环也不会退出了。 + + + +到这里其实已经找到问题了,但还有一个疑问是为什么线程池里的任务队列会越堆越多。我第一直觉是任务执行太慢导致的。 + + + +仔细查看了代码发现只有一个地方可能会慢:也就是有一个**数据库的查询**。 + + + +把这个 SQL 拿到生产环境执行发现确实不快,查看索引发现都有命中。 + + + +但我一看表中的数据发现已经快有 **7000W** 的数据了。同时经过运维得知 `MySQL` 那台服务器的 `IO` 压力也比较大。 + +所以这个原因也比较明显了: + +> 由于每消费一条数据都要去查询一次数据库,MySQL 本身压力就比较大,加上数据量也很高所以导致这个 IO 响应较慢,导致整个任务处理的就比较慢了。 + +但还有一个原因也不能忽视;由于所有的业务线程在某个时间点都进入了死循环,根本没有执行完任务的机会,而后面的数据还在源源不断的进入,所以这个队列只会越堆越多! + + + +这其实是一个老应用了,可能会有人问为什么之前没出现问题。 + +这是因为之前数据量都比较少,即使是并发写入也没有出现并发扩容形成环形链表的情况。这段时间业务量的暴增正好把这个隐藏的雷给揪出来了。所以还是得信墨菲他老人家的话。 + + + +# 总结 + + + +至此整个排查结束,而我们后续的调整措施大概如下: + +- `HashSet` 不是线程安全的,换为 `ConcurrentHashMap`同时把 `value` 写死一样可以达到 `set` 的效果。 +- 根据我们后面的监控,初始化 `ConcurrentHashMap` 的大小尽量大一些,避免频繁的扩容。 +- `MySQL` 中很多数据都已经不用了,进行冷热处理。尽量降低单表数据量。同时后期考虑分表。 +- 查数据那里调整为查缓存,提高查询效率。 +- 线程池的名称一定得取的有意义,不然是自己给自己增加难度。 +- 根据监控将线程池的队列大小调整为一个具体值,并且要有拒绝策略。 +- 升级到 `JDK1.8`。 +- 再一个是报警邮件酌情考虑为电话通知😂。 + + + +`HashMap` 的死循环问题在网上层出不穷,没想到还真被我遇到了。现在要满足这个条件还是挺少见的,比如 1.8 以下的 `JDK` 这一条可能大多数人就碰不到,正好又证实了一次墨菲定律。 + +**你的点赞与分享是对我最大的支持** diff --git a/docs/jvm/MemoryAllocation.md b/docs/jvm/MemoryAllocation.md new file mode 100644 index 00000000..ce615e40 --- /dev/null +++ b/docs/jvm/MemoryAllocation.md @@ -0,0 +1,93 @@ +# Java 运行时的内存划分 + +![](https://i.loli.net/2019/07/19/5d31384c568c531115.jpg) + +## 程序计数器 + +记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。 + +当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。由此也可以看出程序计数器是每个线程**私有**的。 + + +## 虚拟机栈 +虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。 + +每一个栈帧由`局部变量区`、`操作数栈`等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。 + +> - 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `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://i.loli.net/2019/07/19/5d31384cbc79744624.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` 的堆内存。 + +可以通过参数 `-XX:NewRatio=2` 来设置老年代/新生代的比例。 diff --git a/docs/jvm/OOM-Disruptor.md b/docs/jvm/OOM-Disruptor.md new file mode 100644 index 00000000..c9f32f7b --- /dev/null +++ b/docs/jvm/OOM-Disruptor.md @@ -0,0 +1,125 @@ +![](https://i.loli.net/2019/07/19/5d3138372d6e887188.jpg) + +# 前言 + +`OutOfMemoryError` 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。 + +本文以最近碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的同学带来思路和帮助。 + +主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。 + + + +# 表象 + +最近我们生产上的一个应用不断的爆出内存溢出,并且随着业务量的增长出现的频次越来越高。 + +该程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来然后批量的做持久化操作。 + +而现象则是随着 Kafka 的消息越多,出现的异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。 + +> 重启大法虽好,可是依然不能根本解决问题。 + +# 排查 + +于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。 + +![](https://i.loli.net/2019/07/19/5d313837e4bed39389.jpg) + +结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 + +结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。 + +![](https://i.loli.net/2019/07/19/5d31383dd7f7267709.jpg) + +甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 + +这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。 + +# 定位 + +由于生产上的内存 dump 文件非常大,达到了几十G。也是由于我们的内存设置太大有关。 + +所以导致想使用 MAT 分析需要花费大量时间。 + +因此我们便想是否可以在本地复现,这样就要好定位的多。 + +为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。 + + +然后在消费 Kafka 那里 Mock 为一个 while 循环一直不断的生成数据。 + +同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。 + +结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。 + +![](https://i.loli.net/2019/07/19/5d31383e755bb33860.jpg) + + +没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。 + +查看生产的日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**。 + +为了尽可能的模拟生产情况便在服务器上跑着一个生产者程序,一直源源不断的向 Kafka 中发送数据。 + +果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 + +![](https://i.loli.net/2019/07/19/5d3138428c8a826344.jpg) + +同时后台也开始打印内存溢出了,这样便复现出问题。 + +# 解决 + +从目前的表现来看就是内存中有许多对象一直存在强引用关系导致得不到回收。 + +于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。 + +![](https://i.loli.net/2019/07/19/5d3138486fea240331.jpg) + +结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 + +看到这个包自然就想到了 `Disruptor` 环形队列。 + +再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。 + +这里也就能说明为什么第一次模拟数据没复现问题了。 + +模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量是 700 倍的差距。 + +而 Disruptor 作为一个环形队列,再对象没有被覆盖之前是一直存在的。 + +我也做了一个实验,证明确实如此。 + +![](https://i.loli.net/2019/07/19/5d3138493076e20268.jpg) + +我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。 + +所以在生产上假设我们的队列大小是 1024,那么随着系统的运行最终肯定会导致 1024 个位置上装满了对象,而且每个位置是 700 个! + +于是查看了生产上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`。 + +这个数量级就非常吓人了。 + +为了验证是否是这个问题,我在本地将该值换为 2 ,一个最小值试试。 + +同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: + +![](https://i.loli.net/2019/07/19/5d31384bc3a3888930.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) + +**你的点赞与转发是最大的支持。** diff --git a/docs/jvm/OOM-analysis.md b/docs/jvm/OOM-analysis.md new file mode 100644 index 00000000..dee31454 --- /dev/null +++ b/docs/jvm/OOM-analysis.md @@ -0,0 +1,96 @@ +# OOM 分析 + +## Java 堆内存溢出 + +在 Java 堆中只要不断的创建对象,并且 `GC-Roots` 到对象之间存在引用链,这样 `JVM` 就不会回收对象。 + +只要将`-Xms(最小堆)`,`-Xmx(最大堆)` 设置为一样禁止自动扩展堆内存。 + + +当使用一个 `while(true)` 循环来不断创建对象就会发生 `OutOfMemory`,还可以使用 `-XX:+HeapDumpOutofMemoryErorr` 当发生 OOM 时会自动 dump 堆栈到文件中。 + +伪代码: + +```java + public static void main(String[] args) { + List list = new ArrayList(10) ; + while (true){ + list.add("1") ; + } + } +``` + +当出现 OOM 时可以通过工具来分析 `GC-Roots` [引用链](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) ,查看对象和 `GC-Roots` 是如何进行关联的,是否存在对象的生命周期过长,或者是这些对象确实改存在的,那就要考虑将堆内存调大了。 + +``` +Exception in thread "main" java.lang.OutOfMemoryError: Java heap space + at java.util.Arrays.copyOf(Arrays.java:3210) + at java.util.Arrays.copyOf(Arrays.java:3181) + at java.util.ArrayList.grow(ArrayList.java:261) + at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) + at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) + at java.util.ArrayList.add(ArrayList.java:458) + at com.crossoverjie.oom.HeapOOM.main(HeapOOM.java:18) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) + at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147) + +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 堆。 + +`PermSize` 和 `MaxPermSize` 已经不能使用了,在 JDK8 中配置这两个参数将会发出警告。 + + +JDK 8 中将类信息移到到了本地堆内存(Native Heap)中,将原有的永久代移动到了本地堆中成为 `MetaSpace` ,如果不指定该区域的大小,JVM 将会动态的调整。 + +可以使用 `-XX:MaxMetaspaceSize=10M` 来限制最大元数据。这样当不停的创建类时将会占满该区域并出现 `OOM`。 + +```java + public static void main(String[] args) { + while (true){ + Enhancer enhancer = new Enhancer() ; + enhancer.setSuperclass(HeapOOM.class); + enhancer.setUseCache(false) ; + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + return methodProxy.invoke(o,objects) ; + } + }); + enhancer.create() ; + + } + } +``` +使用 `cglib` 不停的创建新类,最终会抛出: +``` +Caused by: java.lang.reflect.InvocationTargetException + at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) + at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459) + at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336) + ... 11 more +Caused by: java.lang.OutOfMemoryError: Metaspace + at java.lang.ClassLoader.defineClass1(Native Method) + at java.lang.ClassLoader.defineClass(ClassLoader.java:763) + ... 16 more +``` + +注意:这里的 OOM 伴随的是 `java.lang.OutOfMemoryError: Metaspace` 也就是元数据溢出。 + diff --git a/docs/jvm/cpu-percent-100.md b/docs/jvm/cpu-percent-100.md new file mode 100755 index 00000000..4c327b5f --- /dev/null +++ b/docs/jvm/cpu-percent-100.md @@ -0,0 +1,152 @@ + + +![](https://i.loli.net/2019/07/19/5d31382a2d77079070.jpg) + +# 前言 + +到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。 + +还真是想什么来什么,前些天还故意把某些服务器的负载提高([没错,老板让我写个 BUG!](https://crossoverjie.top/2018/12/12/java-senior/java-memary-allocation/)),不过还好是不同的环境互相没有影响。 + + + + +# 定位问题 + +拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 `ps` 命令拿到了应用的 `PID`。 + +接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。 + +![](https://i.loli.net/2019/07/19/5d31382b1d3df15468.jpg) + +果然某些线程的 CPU 使用率非常高。 + + +为了方便定位问题我立马使用 `jstack pid> pid.log` 将线程栈 `dump` 到日志文件中。 + +我在上面 100% 的线程中随机选了一个 `pid=194283` 转换为 16 进制(2f6eb)后在线程快照中查询: + +> 因为线程快照中线程 ID 都是16进制存放。 + +![](https://i.loli.net/2019/07/19/5d31382bb08a129414.jpg) + +发现这是 `Disruptor` 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 [OOM]():[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + +没想到又来一出。 + +为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。 + +[http://fastthread.io/](http://fastthread.io/) + +![](https://i.loli.net/2019/07/19/5d31382fbe13e22162.jpg) + +其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。 + +也就是说都是 `Disruptor` 队列的堆栈,同时都在执行 `java.lang.Thread.yield` 函数。 + +众所周知 `yield` 函数会让当前线程让出 `CPU` 资源,再让其他线程来竞争。 + +根据刚才的线程快照发现处于 `RUNNABLE` 状态并且都在执行 `yield` 函数的线程大概有 30几个。 + +因此初步判断为大量线程执行 `yield` 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 `Disruptor` 有关。 + +# 解决问题 + +而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 `Disruptor` 队列来解耦。 + +假设现在有 7 个业务类型,那就等于是创建 `2*7=14` 个 `Disruptor` 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。 + +同时发现配置的消费等待策略为 `YieldingWaitStrategy` 这种等待策略确实会执行 yield 来让出 CPU。 + +代码如下: + +![](https://i.loli.net/2019/07/19/5d31383063b5729406.jpg) + +> 初步看来和这个等待策略有很大的关系。 + +## 本地模拟 + +为了验证,我在本地创建了 15 个 `Disruptor` 队列同时结合监控观察 CPU 的使用情况。 + +![](https://i.loli.net/2019/07/19/5d313830e683e59146.jpg) +![](https://i.loli.net/2019/07/19/5d3138364092a60230.jpg) + +创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。 + +消费程序仅仅只是打印一下。 + +![](https://i.loli.net/2019/07/19/5d313836ac8a448151.jpg) + +跑了一段时间发现 CPU 使用率确实很高。 + +--- + +![](https://i.loli.net/2019/07/19/5d31383d664cb51737.jpg) + +同时 `dump` 线程发现和生产的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`。 + +通过查询 `Disruptor` 官方文档发现: + +![](https://i.loli.net/2019/07/19/5d31383e10f0327921.jpg) + +> YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。 +> 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +--- + +![](https://i.loli.net/2019/07/19/5d31383fd2dc594576.jpg) + +同时查阅到其他的等待策略 `BlockingWaitStrategy` (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。 + +于是在和之前同样的条件下将等待策略换为 `BlockingWaitStrategy`。 + +![](https://i.loli.net/2019/07/19/5d31384097d6190496.jpg) + +--- + +![](https://i.loli.net/2019/07/19/5d3138411411e73544.jpg) +![](https://i.loli.net/2019/07/19/5d313841d679b99195.jpg) + +和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。 + + +## 优化解决 + +看样子将等待策略换为 `BlockingWaitStrategy` 可以减缓 CPU 的使用, + +但留意到官方对 `YieldingWaitStrategy` 的描述里谈道: +当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 `YieldingWaitStrategy`)。 + +![](https://i.loli.net/2019/07/19/5d313842427b798742.jpg) + +![](https://i.loli.net/2019/07/19/5d3138669071113680.jpg) + +跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。 + +# 总结 + +所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 `Disruptor` 队列。 + +由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。 + +所以我们的调整方式如下: + +- 为了快速缓解这个问题,先将等待策略换为 `BlockingWaitStrategy`,可以有效降低 CPU 的使用率(业务上也还能接受)。 +- 第二步就需要将应用拆分(上文模拟的一个 `Disruptor` 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。 + +当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。 + +创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。 + +所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。 + + +本文的演示代码已上传至 GitHub: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) + +**你的点赞与分享是对我最大的支持** + +![](https://i.loli.net/2019/07/19/5d313848b169269048.jpg) diff --git a/docs/jvm/newObject.md b/docs/jvm/newObject.md new file mode 100644 index 00000000..4542db99 --- /dev/null +++ b/docs/jvm/newObject.md @@ -0,0 +1,77 @@ +# 对象的创建与内存分配 + + +## 创建对象 + +当 `JVM` 收到一个 `new` 指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被[加载](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ClassLoad.md)过了,如果没有的话则要进行一次类加载。 + +接着就是分配内存了,通常有两种方式: + +- 指针碰撞 +- 空闲列表 + +使用指针碰撞的前提是堆内存是**完全工整**的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可。 + +当堆中已经使用的内存和未使用的内存**互相交错**时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式。虚拟机会维护一个空闲的列表,用于记录哪些内存是可以进行分配的,分配时直接从可用内存中直接分配即可。 + +堆中的内存是否工整是有**垃圾收集器**来决定的,如果带有压缩功能的垃圾收集器就是采用指针碰撞的方式来进行内存分配的。 + +分配内存时也会出现并发问题: + +这样可以在创建对象的时候使用 `CAS` 这样的乐观锁来保证。 + +也可以将内存分配安排在每个线程独有的空间进行,每个线程首先在堆内存中分配一小块内存,称为本地分配缓存(`TLAB : Thread Local Allocation Buffer`)。 + +分配内存时,只需要在自己的分配缓存中分配即可,由于这个内存区域是线程私有的,所以不会出现并发问题。 + +可以使用 `-XX:+/-UseTLAB` 参数来设定 `JVM` 是否开启 `TLAB` 。 + +内存分配之后需要对该对象进行设置,如对象头。对象头的一些应用可以查看 [Synchronize 关键字原理](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Synchronize.md)。 + +### 对象访问 + +一个对象被创建之后自然是为了使用,在 `Java` 中是通过栈来引用堆内存中的对象来进行操作的。 + +对于我们常用的 `HotSpot` 虚拟机来说,这样引用关系是通过直接指针来关联的。 + +如图: + +![](https://i.loli.net/2019/07/19/5d31384ddc06744280.jpg) + +这样的好处就是:在 Java 里进行频繁的对象访问可以提升访问速度(相对于使用句柄池来说)。 + +## 内存分配 + + +### Eden 区分配 +简单的来说对象都是在堆内存中分配的,往细一点看则是优先在 `Eden` 区分配。 + +这里就涉及到堆内存的划分了,为了方便垃圾回收,JVM 将堆内存分为新生代和老年代。 + +而新生代中又会划分为 `Eden` 区,`from Survivor、to Survivor` 区。 + +其中 `Eden` 和 `Survivor` 区的比例默认是 `8:1:1`,当然也支持参数调整 `-XX:SurvivorRatio=8`。 + +当在 `Eden` 区分配内存不足时,则会发生 `minorGC` ,由于 `Java` 对象多数是**朝生夕灭**的特性,所以 `minorGC` 通常会比较频繁,效率也比较高。 + +当发生 `minorGC` 时,JVM 会根据[复制算法](https://github.com/crossoverJie/Java-Interview/blob/master/MD/GarbageCollection.md#%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95)将存活的对象拷贝到另一个未使用的 `Survivor` 区,如果 `Survivor` 区内存不足时,则会使用分配担保策略将对象移动到老年代中。 + +谈到 `minorGC` 时,就不得不提到 `fullGC(majorGC)` ,这是指发生在老年代的 `GC` ,不论是效率还是速度都比 `minorGC` 慢的多,回收时还会发生 `stop the world` 使程序发生停顿,所以应当尽量避免发生 `fullGC` 。 + +### 老年代分配 + +也有一些情况会导致对象直接在老年代分配,比如当分配一个大对象时(大的数组,很长的字符串),由于 `Eden` 区没有足够大的连续空间来分配时,会导致提前触发一次 `GC`,所以尽量别频繁的创建大对象。 + +因此 `JVM` 会根据一个阈值来判断大于该阈值对象直接分配到老年代,这样可以避免在新生代频繁的发生 `GC`。 + + +对于一些在新生代的老对象 `JVM` 也会根据某种机制移动到老年代中。 + +JVM 是根据记录对象年龄的方式来判断该对象是否应该移动到老年代,根据新生代的复制算法,当一个对象被移动到 `Survivor` 区之后 JVM 就给该对象的年龄记为1,每当熬过一次 `minorGC` 后对象的年龄就 +1 ,直到达到阈值(默认为15)就移动到老年代中。 + +> 可以使用 `-XX:MaxTenuringThreshold=15` 来配置这个阈值。 + + +## 总结 + +虽说这些内容略显枯燥,但当应用发生不正常的 `GC` 时,可以方便更快的定位问题。 diff --git a/docs/jvm/volatile.md b/docs/jvm/volatile.md new file mode 100644 index 00000000..3110e2c6 --- /dev/null +++ b/docs/jvm/volatile.md @@ -0,0 +1,218 @@ +# 你应该知道的 volatile 关键字 + +## 前言 + +不管是在面试还是实际开发中 `volatile` 都是一个应该掌握的技能。 + +首先来看看为什么会出现这个关键字。 + +## 内存可见性 +由于 `Java` 内存模型(`JMM`)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。 + +线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。 + +> 这里所提到的主内存可以简单认为是**堆内存**,而工作内存则可以认为是**栈内存**。 + +如下图所示: + +![](https://i.loli.net/2019/07/19/5d31384d22ac511765.jpg) + +所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。 + +显然这肯定是会出问题的,因此 `volatile` 的作用出现了: + +> 当一个变量被 `volatile` 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。 + +*`volatile` 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中*。 + +### 内存可见性的应用 + +当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 `volatile` 来修饰: + +```java +public class Volatile implements Runnable{ + + private static volatile boolean flag = true ; + + @Override + public void run() { + while (flag){ + } + System.out.println(Thread.currentThread().getName() +"执行完毕"); + } + + public static void main(String[] args) throws InterruptedException { + Volatile aVolatile = new Volatile(); + new Thread(aVolatile,"thread A").start(); + + + System.out.println("main 线程正在运行") ; + + Scanner sc = new Scanner(System.in); + while(sc.hasNext()){ + String value = sc.next(); + if(value.equals("1")){ + + new Thread(new Runnable() { + @Override + public void run() { + aVolatile.stopThread(); + } + }).start(); + + break ; + } + } + + System.out.println("主线程退出了!"); + + } + + private void stopThread(){ + flag = false ; + } + +} +``` + +主线程在修改了标志位使得线程 A 立即停止,如果没有用 `volatile` 修饰,就有可能出现延迟。 + +但这里有个误区,这样的使用方式容易给人的感觉是: + +> 对 `volatile` 修饰的变量进行并发操作是线程安全的。 + +这里要重点强调,`volatile` 并**不能**保证线程安全性! + +如下程序: + +```java +public class VolatileInc implements Runnable{ + + private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性 + + //private static AtomicInteger count = new AtomicInteger() ; + + @Override + public void run() { + for (int i=0;i<10000 ;i++){ + count ++ ; + //count.incrementAndGet() ; + } + } + + public static void main(String[] args) throws InterruptedException { + VolatileInc volatileInc = new VolatileInc() ; + Thread t1 = new Thread(volatileInc,"t1") ; + Thread t2 = new Thread(volatileInc,"t2") ; + t1.start(); + //t1.join(); + + t2.start(); + //t2.join(); + for (int i=0;i<10000 ;i++){ + count ++ ; + //count.incrementAndGet(); + } + + + System.out.println("最终Count="+count); + } +} +``` + +当我们三个线程(t1,t2,main)同时对一个 `int` 进行累加时会发现最终的值都会小于 30000。 + +> 这是因为虽然 `volatile` 保证了内存可见性,每个线程拿到的值都是最新值,但 `count ++` 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。 +> + +- 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。 + +- 也可以使用 `synchronized` 或者是锁的方式来保证原子性。 + +- 还可以用 `Atomic` 包中 `AtomicInteger` 来替换 `int`,它利用了 `CAS` 算法来保证了原子性。 + + +## 指令重排 + +内存可见性只是 `volatile` 的其中一个语义,它还可以防止 `JVM` 进行指令重排优化。 + +举一个伪代码: + +```java +int a=10 ;//1 +int b=20 ;//2 +int c= a+b ;//3 +``` + +一段特别简单的代码,理想情况下它的执行顺序是:`1>2>3`。但有可能经过 JVM 优化之后的执行顺序变为了 `2>1>3`。 + +可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。 + +可能这里还看不出有什么问题,那看下一段伪代码: + +```java +private static Map value ; +private static volatile boolean flag = fasle ; + +//以下方法发生在线程 A 中 初始化 Map +public void initMap(){ + //耗时操作 + value = getMapValue() ;//1 + flag = true ;//2 +} + + +//发生在线程 B中 等到 Map 初始化成功进行其他操作 +public void doSomeThing(){ + while(!flag){ + sleep() ; + } + //dosomething + doSomeThing(value); +} + +``` + +这里就能看出问题了,当 `flag` 没有被 `volatile` 修饰时,`JVM` 对 1 和 2 进行重排,导致 `value` 都还没有被初始化就有可能被线程 B 使用了。 + +所以加上 `volatile` 之后可以防止这样的重排优化,保证业务的正确性。 +### 指令重排的的应用 + +一个经典的使用场景就是双重懒加载的单例模式了: + +```java +public class Singleton { + + private static volatile Singleton singleton; + + private Singleton() { + } + + public static Singleton getInstance() { + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + //防止指令重排 + singleton = new Singleton(); + } + } + } + return singleton; + } +} +``` + +这里的 `volatile` 关键字主要是为了防止指令重排。 + +如果不用 ,`singleton = new Singleton();`,这段代码其实是分为三步: +- 分配内存空间。(1) +- 初始化对象。(2) +- 将 `singleton` 对象指向分配的内存地址。(3) + +加上 `volatile` 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。 + +## 总结 + +`volatile` 在 `Java` 并发中用的很多,比如像 `Atomic` 包中的 `value`、以及 `AbstractQueuedLongSynchronizer` 中的 `state` 都是被定义为 `volatile` 来用于保证内存可见性。 + +将这块理解透彻对我们编写并发程序时可以提供很大帮助。 diff --git a/docs/netty/Netty(1)TCP-Heartbeat.md b/docs/netty/Netty(1)TCP-Heartbeat.md new file mode 100644 index 00000000..e562ecb1 --- /dev/null +++ b/docs/netty/Netty(1)TCP-Heartbeat.md @@ -0,0 +1,695 @@ + +![photo-1522204657746-fccce0824cfd.jpeg](https://i.loli.net/2018/05/25/5b0774828db53.jpeg) + +# 前言 + +Netty 是一个高性能的 NIO 网络框架,本文基于 SpringBoot 以常见的心跳机制来认识 Netty。 + +最终能达到的效果: + +- 客户端每隔 N 秒检测是否需要发送心跳。 +- 服务端也每隔 N 秒检测是否需要发送心跳。 +- 服务端可以主动 push 消息到客户端。 +- 基于 SpringBoot 监控,可以查看实时连接以及各种应用信息。 + +效果如下: + +![show](https://crossoverjie.top/uploads/netty-Heartbeat.gif) + + + +# IdleStateHandler + +Netty 可以使用 IdleStateHandler 来实现连接管理,当连接空闲时间太长(没有发送、接收消息)时则会触发一个事件,我们便可在该事件中实现心跳机制。 + +## 客户端心跳 + +当客户端空闲了 N 秒没有给服务端发送消息时会自动发送一个心跳来维持连接。 + +核心代码代码如下: + +```java +public class EchoClientHandle extends SimpleChannelInboundHandler { + + private final static Logger LOGGER = LoggerFactory.getLogger(EchoClientHandle.class); + + + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + + if (evt instanceof IdleStateEvent){ + IdleStateEvent idleStateEvent = (IdleStateEvent) evt ; + + if (idleStateEvent.state() == IdleState.WRITER_IDLE){ + LOGGER.info("已经 10 秒没有发送信息!"); + //向服务端发送消息 + CustomProtocol heartBeat = SpringBeanFactory.getBean("heartBeat", CustomProtocol.class); + ctx.writeAndFlush(heartBeat).addListener(ChannelFutureListener.CLOSE_ON_FAILURE) ; + } + + + } + + super.userEventTriggered(ctx, evt); + } + + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf in) throws Exception { + + //从服务端收到消息时被调用 + LOGGER.info("客户端收到消息={}",in.toString(CharsetUtil.UTF_8)) ; + + } +} +``` + +实现非常简单,只需要在事件回调中发送一个消息即可。 + +由于整合了 SpringBoot ,所以发送的心跳信息是一个单例的 Bean。 + +```java +@Configuration +public class HeartBeatConfig { + + @Value("${channel.id}") + private long id ; + + + @Bean(value = "heartBeat") + public CustomProtocol heartBeat(){ + return new CustomProtocol(id,"ping") ; + } +} +``` + +这里涉及到了自定义协议的内容,请继续查看下文。 + +当然少不了启动引导: + +```java +@Component +public class HeartbeatClient { + + private final static Logger LOGGER = LoggerFactory.getLogger(HeartbeatClient.class); + + private EventLoopGroup group = new NioEventLoopGroup(); + + + @Value("${netty.server.port}") + private int nettyPort; + + @Value("${netty.server.host}") + private String host; + + private SocketChannel channel; + + @PostConstruct + public void start() throws InterruptedException { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .handler(new CustomerHandleInitializer()) + ; + + ChannelFuture future = bootstrap.connect(host, nettyPort).sync(); + if (future.isSuccess()) { + LOGGER.info("启动 Netty 成功"); + } + channel = (SocketChannel) future.channel(); + } + +} + +public class CustomerHandleInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline() + //10 秒没发送消息 将IdleStateHandler 添加到 ChannelPipeline 中 + .addLast(new IdleStateHandler(0, 10, 0)) + .addLast(new HeartbeatEncode()) + .addLast(new EchoClientHandle()) + ; + } +} +``` + +所以当应用启动每隔 10 秒会检测是否发送过消息,不然就会发送心跳信息。 + +![](https://i.loli.net/2019/07/19/5d313938a059249899.jpg) + +## 服务端心跳 + +服务器端的心跳其实也是类似,也需要在 ChannelPipeline 中添加一个 IdleStateHandler 。 + +```java +public class HeartBeatSimpleHandle extends SimpleChannelInboundHandler { + + private final static Logger LOGGER = LoggerFactory.getLogger(HeartBeatSimpleHandle.class); + + private static final ByteBuf HEART_BEAT = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(new CustomProtocol(123456L,"pong").toString(),CharsetUtil.UTF_8)); + + + /** + * 取消绑定 + * @param ctx + * @throws Exception + */ + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + + NettySocketHolder.remove((NioSocketChannel) ctx.channel()); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + + if (evt instanceof IdleStateEvent){ + IdleStateEvent idleStateEvent = (IdleStateEvent) evt ; + + if (idleStateEvent.state() == IdleState.READER_IDLE){ + LOGGER.info("已经5秒没有收到信息!"); + //向客户端发送消息 + ctx.writeAndFlush(HEART_BEAT).addListener(ChannelFutureListener.CLOSE_ON_FAILURE) ; + } + + + } + + super.userEventTriggered(ctx, evt); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CustomProtocol customProtocol) throws Exception { + LOGGER.info("收到customProtocol={}", customProtocol); + + //保存客户端与 Channel 之间的关系 + NettySocketHolder.put(customProtocol.getId(),(NioSocketChannel)ctx.channel()) ; + } +} +``` + +**这里有点需要注意**: + +当有多个客户端连上来时,服务端需要区分开,不然响应消息就会发生混乱。 + +所以每当有个连接上来的时候,我们都将当前的 Channel 与连上的客户端 ID 进行关联(**因此每个连上的客户端 ID 都必须唯一**)。 + +这里采用了一个 Map 来保存这个关系,并且在断开连接时自动取消这个关联。 + +```java +public class NettySocketHolder { + private static final Map MAP = new ConcurrentHashMap(16); + + public static void put(Long id, NioSocketChannel socketChannel) { + MAP.put(id, socketChannel); + } + + public static NioSocketChannel get(Long id) { + return MAP.get(id); + } + + public static Map getMAP() { + return MAP; + } + + public static void remove(NioSocketChannel nioSocketChannel) { + MAP.entrySet().stream().filter(entry -> entry.getValue() == nioSocketChannel).forEach(entry -> MAP.remove(entry.getKey())); + } +} +``` + +启动引导程序: + +```java +Component +public class HeartBeatServer { + + private final static Logger LOGGER = LoggerFactory.getLogger(HeartBeatServer.class); + + private EventLoopGroup boss = new NioEventLoopGroup(); + private EventLoopGroup work = new NioEventLoopGroup(); + + + @Value("${netty.server.port}") + private int nettyPort; + + + /** + * 启动 Netty + * + * @return + * @throws InterruptedException + */ + @PostConstruct + public void start() throws InterruptedException { + + ServerBootstrap bootstrap = new ServerBootstrap() + .group(boss, work) + .channel(NioServerSocketChannel.class) + .localAddress(new InetSocketAddress(nettyPort)) + //保持长连接 + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childHandler(new HeartbeatInitializer()); + + ChannelFuture future = bootstrap.bind().sync(); + if (future.isSuccess()) { + LOGGER.info("启动 Netty 成功"); + } + } + + + /** + * 销毁 + */ + @PreDestroy + public void destroy() { + boss.shutdownGracefully().syncUninterruptibly(); + work.shutdownGracefully().syncUninterruptibly(); + LOGGER.info("关闭 Netty 成功"); + } +} + + +public class HeartbeatInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline() + //五秒没有收到消息 将IdleStateHandler 添加到 ChannelPipeline 中 + .addLast(new IdleStateHandler(5, 0, 0)) + .addLast(new HeartbeatDecoder()) + .addLast(new HeartBeatSimpleHandle()); + } +} +``` + +也是同样将IdleStateHandler 添加到 ChannelPipeline 中,也会有一个定时任务,每5秒校验一次是否有收到消息,否则就主动发送一次请求。 + +![](https://i.loli.net/2019/07/19/5d31393e0f8c660705.jpg) + +因为测试是有两个客户端连上所以有两个日志。 + +## 自定义协议 + +上文其实都看到了:服务端与客户端采用的是自定义的 POJO 进行通讯的。 + +所以需要在客户端进行编码,服务端进行解码,也都只需要各自实现一个编解码器即可。 + +CustomProtocol: + +```java +public class CustomProtocol implements Serializable{ + + private static final long serialVersionUID = 4671171056588401542L; + private long id ; + private String content ; + //省略 getter/setter +} +``` + +客户端的编码器: + +```java +public class HeartbeatEncode extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, CustomProtocol msg, ByteBuf out) throws Exception { + + out.writeLong(msg.getId()) ; + out.writeBytes(msg.getContent().getBytes()) ; + + } +} +``` + +也就是说消息的前八个字节为 header,剩余的全是 content。 + +服务端的解码器: + +```java +public class HeartbeatDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + + long id = in.readLong() ; + byte[] bytes = new byte[in.readableBytes()] ; + in.readBytes(bytes) ; + String content = new String(bytes) ; + + CustomProtocol customProtocol = new CustomProtocol() ; + customProtocol.setId(id); + customProtocol.setContent(content) ; + out.add(customProtocol) ; + + } +} +``` + +只需要按照刚才的规则进行解码即可。 + + +## 实现原理 + +其实联想到 IdleStateHandler 的功能,自然也能想到它实现的原理: + +> 应该会存在一个定时任务的线程去处理这些消息。 + +来看看它的源码: + +首先是构造函数: + +``` + public IdleStateHandler( + int readerIdleTimeSeconds, + int writerIdleTimeSeconds, + int allIdleTimeSeconds) { + + this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds, + TimeUnit.SECONDS); + } +``` + +其实就是初始化了几个数据: +- readerIdleTimeSeconds:一段时间内没有数据读取 +- writerIdleTimeSeconds:一段时间内没有数据发送 +- allIdleTimeSeconds:以上两种满足其中一个即可 + +因为 IdleStateHandler 也是一种 ChannelHandler,所以会在 `channelActive` 中初始化任务: + +```java + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // This method will be invoked only if this handler was added + // before channelActive() event is fired. If a user adds this handler + // after the channelActive() event, initialize() will be called by beforeAdd(). + initialize(ctx); + super.channelActive(ctx); + } + + private void initialize(ChannelHandlerContext ctx) { + // Avoid the case where destroy() is called before scheduling timeouts. + // See: https://github.com/netty/netty/issues/143 + switch (state) { + case 1: + case 2: + return; + } + + state = 1; + initOutputChanged(ctx); + + lastReadTime = lastWriteTime = ticksInNanos(); + if (readerIdleTimeNanos> 0) { + readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx), + readerIdleTimeNanos, TimeUnit.NANOSECONDS); + } + if (writerIdleTimeNanos> 0) { + writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx), + writerIdleTimeNanos, TimeUnit.NANOSECONDS); + } + if (allIdleTimeNanos> 0) { + allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx), + allIdleTimeNanos, TimeUnit.NANOSECONDS); + } + } +``` + +也就是会按照我们给定的时间初始化出定时任务。 + +接着在任务真正执行时进行判断: + +```java + private final class ReaderIdleTimeoutTask extends AbstractIdleTask { + + ReaderIdleTimeoutTask(ChannelHandlerContext ctx) { + super(ctx); + } + + @Override + protected void run(ChannelHandlerContext ctx) { + long nextDelay = readerIdleTimeNanos; + if (!reading) { + nextDelay -= ticksInNanos() - lastReadTime; + } + + if (nextDelay <= 0) { + // Reader is idle - set a new timeout and notify the callback. + readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS); + + boolean first = firstReaderIdleEvent; + firstReaderIdleEvent = false; + + try { + IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first); + channelIdle(ctx, event); + } catch (Throwable t) { + ctx.fireExceptionCaught(t); + } + } else { + // Read occurred before the timeout - set a new timeout with shorter delay. + readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS); + } + } + } +``` + +如果满足条件则会生成一个 IdleStateEvent 事件。 + + +# SpringBoot 监控 + +由于整合了 SpringBoot 之后不但可以利用 Spring 帮我们管理对象,也可以利用它来做应用监控。 + +## actuator 监控 + +当我们为引入了: + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +就开启了 SpringBoot 的 actuator 监控功能,他可以暴露出很多监控端点供我们使用。 + +如一些应用中的一些统计数据: +![](https://i.loli.net/2019/07/19/5d31393fa017351196.jpg) + +存在的 Beans: +![](https://i.loli.net/2019/07/19/5d31394d8fb6d16523.jpg) + +更多信息请查看:[https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html) + +但是如果我想监控现在我的服务端有多少客户端连上来了,分别的 ID 是多少? + +其实就是实时查看我内部定义的那个关联关系的 Map。 + +这就需要暴露自定义端点了。 + +## 自定义端点 + +暴露的方式也很简单: + +继承 AbstractEndpoint 并复写其中的 invoke 函数: + +```java +public class CustomEndpoint extends AbstractEndpoint
          > { + + + /** + * 监控端点的 访问地址 + * @param id + */ + public CustomEndpoint(String id) { + //false 表示不是敏感端点 + super(id, false); + } + + @Override + public Map invoke() { + return NettySocketHolder.getMAP(); + } +} +``` + +其实就是返回了 Map 中的数据。 + +再配置一个该类型的 Bean 即可: + +```java +@Configuration +public class EndPointConfig { + + + @Value("${monitor.channel.map.key}") + private String channelMap; + + @Bean + public CustomEndpoint buildEndPoint(){ + CustomEndpoint customEndpoint = new CustomEndpoint(channelMap) ; + return customEndpoint ; + } +} +``` + +这样我们就可以通过配置文件中的 `monitor.channel.map.key` 来访问了: + +一个客户端连接时: +![](https://i.loli.net/2019/07/19/5d31394e55fe384723.jpg) + +两个客户端连接时: +![](https://i.loli.net/2019/07/19/5d31395522dda45659.jpg) + + +## 整合 SBA + +这样其实监控功能已经可以满足了,但能不能展示的更美观、并且多个应用也可以方便查看呢? + +有这样的开源工具帮我们做到了: + +[https://github.com/codecentric/spring-boot-admin](https://github.com/codecentric/spring-boot-admin) + +简单来说我们可以利用该工具将 actuator 暴露出来的接口可视化并聚合的展示在页面中: + +![](https://i.loli.net/2019/07/19/5d313956b3aca18512.jpg) + +接入也很简单,首先需要引入依赖: + +```xml + + de.codecentric + spring-boot-admin-starter-client + + +``` + +并在配置文件中加入: + +```properties +# 关闭健康检查权限 +management.security.enabled=false +# SpringAdmin 地址 +spring.boot.admin.url=http://127.0.0.1:8888 +``` + +在启动应用之前先讲 SpringBootAdmin 部署好: + +这个应用就是一个纯粹的 SpringBoot ,只需要在主函数上加入 `@EnableAdminServer` 注解。 + +```java +@SpringBootApplication +@Configuration +@EnableAutoConfiguration +@EnableAdminServer +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } +} +``` + +引入: + +```xml + + de.codecentric + spring-boot-admin-starter-server + 1.5.7 + + + de.codecentric + spring-boot-admin-server-ui + 1.5.6 + +``` + +之后直接启动就行了。 + +这样我们在 SpringBootAdmin 的页面中就可以查看很多应用信息了。 + +![](https://i.loli.net/2019/07/19/5d31395886b8e59039.jpg) + +更多内容请参考官方指南: + +[http://codecentric.github.io/spring-boot-admin/1.5.6/](http://codecentric.github.io/spring-boot-admin/1.5.6/) + +### 自定义监控数据 + +其实我们完全可以借助 actuator 以及这个可视化页面帮我们监控一些简单的度量信息。 + +比如我在客户端和服务端中写了两个 Rest 接口用于向对方发送消息。 + +只是想要记录分别发送了多少次: + +客户端: + +```java +@Controller +@RequestMapping("/") +public class IndexController { + + /** + * 统计 service + */ + @Autowired + private CounterService counterService; + + @Autowired + private HeartbeatClient heartbeatClient ; + + /** + * 向服务端发消息 + * @param sendMsgReqVO + * @return + */ + @ApiOperation("客户端发送消息") + @RequestMapping("sendMsg") + @ResponseBody + public BaseResponse sendMsg(@RequestBody SendMsgReqVO sendMsgReqVO){ + BaseResponse res = new BaseResponse(); + heartbeatClient.sendMsg(new CustomProtocol(sendMsgReqVO.getId(),sendMsgReqVO.getMsg())) ; + + // 利用 actuator 来自增 + counterService.increment(Constants.COUNTER_CLIENT_PUSH_COUNT); + + SendMsgResVO sendMsgResVO = new SendMsgResVO() ; + sendMsgResVO.setMsg("OK") ; + res.setCode(StatusEnum.SUCCESS.getCode()) ; + res.setMessage(StatusEnum.SUCCESS.getMessage()) ; + res.setDataBody(sendMsgResVO) ; + return res ; + } +} +``` + +只要我们引入了 actuator 的包,那就可以直接注入 counterService ,利用它来帮我们记录数据。 + +当我们调用该接口时: + +![](https://i.loli.net/2019/07/19/5d313960a3a9826851.jpg) + +![](https://i.loli.net/2019/07/19/5d3139634d9e553115.jpg) + +在监控页面中可以查询刚才的调用情况: + +![](https://i.loli.net/2019/07/19/5d313964b932568620.jpg) + +服务端主动 push 消息也是类似,只是需要在发送时候根据客户端的 ID 查询到具体的 Channel 发送: + +![](https://i.loli.net/2019/07/19/5d31396c18f0c66961.jpg) + +![](https://i.loli.net/2019/07/19/5d313aacddff648185.jpg) + +![](https://i.loli.net/2019/07/19/5d313aada4d3082044.jpg) + + +# 总结 + +以上就是一个简单 Netty 心跳示例,并演示了 SpringBoot 的监控,之后会继续更新 Netty 相关内容,欢迎关注及指正。 + +本文所有代码: + +[https://github.com/crossoverJie/netty-action](https://github.com/crossoverJie/netty-action) + diff --git a/docs/netty/Netty(2)Thread-model.md b/docs/netty/Netty(2)Thread-model.md new file mode 100644 index 00000000..a60ab5a6 --- /dev/null +++ b/docs/netty/Netty(2)Thread-model.md @@ -0,0 +1,182 @@ +![](https://i.loli.net/2019/07/19/5d313935e4ef253589.jpg) + +## 前言 + +在之前的 [SpringBoot 整合长连接心跳机制](netty/Netty(1)TCP-Heartbeat.md) 一文中认识了 Netty。 + +但其实只是能用,为什么要用 Netty?它有哪些优势?这些其实都不清楚。 + +本文就来从历史源头说道说道。 + +## 传统 IO + +在 Netty 以及 NIO 出现之前,我们写 IO 应用其实用的都是用 `java.io.*` 下所提供的包。 + + +比如下面的伪代码: + +```java +ServeSocket serverSocket = new ServeSocket(8080); +Socket socket = serverSocket.accept() ; +BufferReader in = .... ; + +String request ; + +while((request = in.readLine()) != null){ + new Thread(new Task()).start() +} +``` + + + +大概是这样,其实主要想表达的是:**这样一个线程只能处理一个连接**。 + +如果是 100 个客户端连接那就得开 100 个线程,1000 那就得 1000 个线程。 + +要知道线程资源非常宝贵,每次的创建都会带来消耗,而且每个线程还得为它分配对应的栈内存。 + +即便是我们给 JVM 足够的内存,大量线程所带来的上下文切换也是受不了的。 + +> 并且传统 IO 是阻塞模式,每一次的响应必须的是发起 IO 请求,处理请求完成再同时返回,直接的结果就是性能差,吞吐量低。 + +## Reactor 模型 + +因此业界常用的高性能 IO 模型是 `Reactor`。 + +它是一种异步、非阻塞的事件驱动模型。 + +通常也表现为以下三种方式: + +### 单线程 + +![](https://i.loli.net/2019/07/19/5d3139369e57d74023.jpg) + +从图中可以看出: + +它是由一个线程来接收客户端的连接,并将该请求分发到对应的事件处理 handler 中,整个过程完全是异步非阻塞的;并且完全不存在共享资源的问题。所以理论上来说吞吐量也还不错。 + +> 但由于是一个线程,对多核 CPU 利用率不高,一旦有大量的客户端连接上来性能必然下降,甚至会有大量请求无法响应。 +> 最坏的情况是一旦这个线程哪里没有处理好进入了死循环那整个服务都将不可用! + +### 多线程 + +![](https://i.loli.net/2019/07/19/5d313937667e941981.jpg) + +因此产生了多线程模型。 + +其实最大的改进就是将原有的事件处理改为了多线程。 + +可以基于 Java 自身的线程池实现,这样在大量请求的处理上性能提示是巨大的。 + +虽然如此,但理论上来说依然有一个地方是单点的;那就是处理客户端连接的线程。 + +因为大多数服务端应用或多或少在连接时都会处理一些业务,如鉴权之类的,当连接的客户端越来越多时这一个线程依然会存在性能问题。 + +于是又有了下面的线程模型。 + +### 主从多线程 + +![](https://i.loli.net/2019/07/19/5d313937f2dbd55910.jpg) + +该模型将客户端连接那一块的线程也改为多线程,称为主线程。 + +同时也是多个子线程来处理事件响应,这样无论是连接还是事件都是高性能的。 + + +## Netty 实现 + +以上谈了这么多其实 Netty 的线程模型与之的类似。 + +我们回到之前 [SpringBoot 整合长连接心跳机制](https://crossoverjie.top/2018/05/24/netty/Netty(1)TCP-Heartbeat/) 中的服务端代码: + +```java + private EventLoopGroup boss = new NioEventLoopGroup(); + private EventLoopGroup work = new NioEventLoopGroup(); + + + /** + * 启动 Netty + * + * @return + * @throws InterruptedException + */ + @PostConstruct + public void start() throws InterruptedException { + + ServerBootstrap bootstrap = new ServerBootstrap() + .group(boss, work) + .channel(NioServerSocketChannel.class) + .localAddress(new InetSocketAddress(nettyPort)) + //保持长连接 + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childHandler(new HeartbeatInitializer()); + + ChannelFuture future = bootstrap.bind().sync(); + if (future.isSuccess()) { + LOGGER.info("启动 Netty 成功"); + } + } +``` + +其实这里的 boss 就相当于 Reactor 模型中处理客户端连接的线程池。 + +work 自然就是处理事件的线程池了。 + +那么如何来实现上文的三种模式呢?其实也很简单: + + +单线程模型: + +```java +private EventLoopGroup group = new NioEventLoopGroup(); +ServerBootstrap bootstrap = new ServerBootstrap() + .group(group) + .childHandler(new HeartbeatInitializer()); +``` + +多线程模型: + +```java +private EventLoopGroup boss = new NioEventLoopGroup(1); +private EventLoopGroup work = new NioEventLoopGroup(); +ServerBootstrap bootstrap = new ServerBootstrap() + .group(boss,work) + .childHandler(new HeartbeatInitializer()); +``` + +主从多线程: + +```java +private EventLoopGroup boss = new NioEventLoopGroup(); +private EventLoopGroup work = new NioEventLoopGroup(); +ServerBootstrap bootstrap = new ServerBootstrap() + .group(boss,work) + .childHandler(new HeartbeatInitializer()); +``` + +相信大家一看也明白。 + +## 总结 + +其实看过了 Netty 的线程模型之后能否对我们平时做高性能应用带来点启发呢? + +我认为是可以的: + +- 接口同步转异步处理。 +- 回调通知结果。 +- 多线程提高并发效率。 + +无非也就是这些,只是做了这些之后就会带来其他问题: + +- 异步之后事务如何保证? +- 回调失败的情况? +- 多线程所带来的上下文切换、共享资源的问题。 + +这就是一个博弈的过程,想要做到一个尽量高效的应用是需要不断磨合试错的。 + +上文相关的代码: + +[https://github.com/crossoverJie/netty-action](https://github.com/crossoverJie/netty-action) + + +**欢迎关注公众号一起交流:** diff --git a/docs/netty/cicada.md b/docs/netty/cicada.md new file mode 100644 index 00000000..4c02e9d8 --- /dev/null +++ b/docs/netty/cicada.md @@ -0,0 +1,291 @@ + +
          + + +
          + +[![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] 多种响应方式。 +- [x] 内置可插拔 `IOC` 容器。 +- [x] [`Cookie` 支持](#cookie-支持)。 +- [ ] 文件上传。 + + +## 快速启动 + +创建一个 maven 项目,引入核心依赖。 + +```java + + top.crossoverjie.opensource + cicada-core + x.y.z + +``` + +启动类: + +```java +public class MainStart { + + public static void main(String[] args) throws InterruptedException { + CicadaServer.start(MainStart.class,"/cicada-example") ; + } +} +``` + +### 配置业务 Action + +```java +@CicadaAction("routeAction") +public class RouteAction { + + private static final Logger LOGGER = LoggerBuilder.getLogger(RouteAction.class); + + + @CicadaRoute("getUser") + public void getUser(DemoReq req){ + + LOGGER.info(req.toString()); + WorkRes reqWorkRes = new WorkRes() ; + reqWorkRes.setMessage("hello =" + req.getName()); + CicadaContext.getContext().json(reqWorkRes) ; + } + + @CicadaRoute("getInfo") + public void getInfo(DemoReq req){ + + WorkRes reqWorkRes = new WorkRes() ; + reqWorkRes.setMessage("getInfo =" + req.toString()); + CicadaContext.getContext().json(reqWorkRes) ; + } + + @CicadaRoute("getReq") + public void getReq(CicadaContext context,DemoReq req){ + + WorkRes reqWorkRes = new WorkRes() ; + reqWorkRes.setMessage("getReq =" + req.toString()); + context.json(reqWorkRes) ; + } + + + +} +``` + +启动应用访问 [http://127.0.0.1:5688/cicada-example/routeAction/getUser?id=1234&name=zhangsan](http://127.0.0.1:5688/cicada-example/routeAction/getUser?id=1234&name=zhangsan) + +```json +{"message":"hello =zhangsan"} +``` + +## Cicada 上下文 + +通过 `context.json(),context.text()` 方法可以选择不同的响应方式。 + +```java +@CicadaAction("routeAction") +public class RouteAction { + + private static final Logger LOGGER = LoggerBuilder.getLogger(RouteAction.class); + + @CicadaRoute("getUser") + public void getUser(DemoReq req){ + + LOGGER.info(req.toString()); + WorkRes reqWorkRes = new WorkRes() ; + reqWorkRes.setMessage("hello =" + req.getName()); + CicadaContext.getContext().json(reqWorkRes) ; + } + + @CicadaRoute("hello") + public void hello() throws Exception { + CicadaContext context = CicadaContext.getContext(); + + String url = context.request().getUrl(); + String method = context.request().getMethod(); + context.text("hello world url=" + url + " method=" + method); + } + + +} +``` + + +## Cookie 支持 + +### 设置 Cookie + +```java +Cookie cookie = new Cookie() ; +cookie.setName("cookie"); +cookie.setValue("value"); +CicadaContext.getResponse().setCookie(cookie); +``` + +### 获取 Cookie + +```java +Cookie cookie = CicadaContext.getRequest().getCookie("cookie"); +logger.info("cookie = " + cookie.toString()); +``` + +## 自定义配置 + +`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://i.loli.net/2019/07/19/5d31392ece42e20923.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 boolean before(Param param) { + start = System.currentTimeMillis(); + return true; + } + + @Override + public void after(Param param) { + end = System.currentTimeMillis(); + + LOGGER.info("cast [{}] times", end - start); + } +} +``` + + +## 性能测试 + +![](https://i.loli.net/2019/07/19/5d31392f5efa350999.jpg) + +> 测试条件:100 threads and 100 connections ;1G RAM/4 CPU。 + +**每秒将近 10W 请求。** + +## 更新记录 + +### v2.0.1 +- 更新 Logo ,美化日志。 +- 支持 `Cookie` + +### v2.0.0 +- 修复 [#12](https://github.com/TogetherOS/cicada/issues/12) [#22](https://github.com/TogetherOS/cicada/issues/22) [#28](28) +- 更加灵活的路由方式。 +- 内置可插拔 `IOC` 容器。 + +### 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) diff --git a/docs/netty/cim.md b/docs/netty/cim.md new file mode 100755 index 00000000..54668249 --- /dev/null +++ b/docs/netty/cim.md @@ -0,0 +1,419 @@ + + +![](https://i.loli.net/2019/07/19/5d31392e8a50b75976.jpg) + +# 前言 + + +大家新年快乐! + +新的一年第一篇技术文章希望开个好头,所以元旦三天我也没怎么闲着,希望给大家带来一篇比较感兴趣的干货内容。 + +老读者应该还记得我在去年国庆节前分享过一篇[《设计一个百万级的消息推送系统》](https://crossoverjie.top/2018/09/25/netty/million-sms-push/);虽然我在文中有贴一些伪代码,依然有些朋友希望能直接分享一些可以运行的源码;这么久了是时候把坑填上了。 + +目录结构: + +![](https://i.loli.net/2019/07/19/5d31392f0ad2d76578.jpg) +![](https://i.loli.net/2019/07/19/5d31392f6182225602.jpg) + +> 本文较长,高能预警;带好瓜子板凳。 + + + + +![](https://i.loli.net/2019/07/19/5d31392fe669471794.jpg) +![](https://i.loli.net/2019/07/19/5d31393061d9624009.jpg) +![](https://i.loli.net/2019/07/19/5d313930bd66213776.jpg) + +于是在之前的基础上我完善了一些内容,先来看看这个项目的介绍吧: + +`CIM(CROSS-IM)` 一款面向开发者的 `IM(即时通讯)`系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 `IM` 。 + +借助 `CIM` 你可以实现以下需求: + +- `IM` 即时通讯系统。 +- 适用于 `APP` 的消息推送中间件。 +- `IOT` 海量连接场景中的消息透传中间件。 + +完整源码托管在 GitHub : [https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) + +# 演示 + +本次主要涉及到 IM 即时通讯,所以特地录了两段视频演示(群聊、私聊)。 + +> 点击下方链接可以查看视频版 Demo。 + +| YouTube | Bilibili| +| :------:| :------: | +| [群聊](https://youtu.be/_9a4lIkQ5_o) [私聊](https://youtu.be/kfEfQFPLBTQ) | [群聊](https://www.bilibili.com/video/av39405501) [私聊](https://www.bilibili.com/video/av39405821) | +| | + + +也在公网部署了一套演示环境,想要试一试的可以联系我加入内测群获取账号一起尬聊😋。 + +# 架构设计 + +下面来看看具体的架构设计。 + +![](https://i.loli.net/2019/07/19/5d313936e89ff75333.jpg) + +- `CIM` 中的各个组件均采用 `SpringBoot` 构建。 +- 采用 `Netty + Google Protocol Buffer` 构建底层通信。 +- `Redis` 存放各个客户端的路由信息、账号信息、在线状态等。 +- `Zookeeper` 用于 `IM-server` 服务的注册与发现。 + +整体主要由以下模块组成: + +## cim-server + +`IM` 服务端;用于接收 `client` 连接、消息透传、消息推送等功能。 + +**支持集群部署。** + +## cim-forward-route + +消息路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运营工具(获取在线用户数等)。 + +## cim-client + +`IM` 客户端;给用户使用的消息终端,一个命令即可启动并向其他人发起通讯(群聊、私聊);同时内置了一些常用命令方便使用。 + + +# 流程图 + +整体的流程也比较简单,流程图如下: + +![](https://i.loli.net/2019/07/19/5d31393783d9878382.jpg) + +- 客户端向 `route` 发起登录。 +- 登录成功从 `Zookeeper` 中选择可用 `IM-server` 返回给客户端,并保存登录、路由信息到 `Redis`。 +- 客户端向 `IM-server` 发起长连接,成功后保持心跳。 +- 客户端下线时通过 `route` 清除状态信息。 + + +所以当我们自己部署时需要以下步骤: + +- 搭建基础中间件 `Redis、Zookeeper`。 +- 部署 `cim-server`,这是真正的 IM 服务器,为了满足性能需求所以支持水平扩展,只需要注册到同一个 `Zookeeper` 即可。 +- 部署 `cim-forward-route`,这是路由服务器,所有的消息都需要经过它。由于它是无状态的,所以也可以利用 `Nginx` 代理提高可用性。 +- `cim-client` 真正面向用户的客户端;启动之后会自动连接 IM 服务器便可以在控制台收发消息了。 + +更多使用介绍可以参考[快速启动](https://github.com/crossoverJie/cim#%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8)。 + +# 详细设计 + +接下来重点看看具体的实现,比如群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。 + +## IM 服务端 + +先来看看服务端;主要是实现客户端上下线、消息下发等功能。 + +首先是服务启动: + +![](https://i.loli.net/2019/07/19/5d31393808e5864405.jpg) +![](https://i.loli.net/2019/07/19/5d313938780ae12933.jpg) + +由于是在 `SpringBoot` 中搭建的,所以在应用启动时需要启动 `Netty` 服务。 + +从 `pipline` 中可以看出使用了 `Protobuf` 的编解码(具体报文在客户端中分析)。 + +## 注册发现 + +需要满足 `IM` 服务端的水平扩展需求,所以 `cim-server` 是需要将自身数据发布到注册中心的。 + +这里参考之前分享的[《搞定服务注册与发现》](https://crossoverjie.top/2018/08/27/distributed/distributed-discovery-zk/)有具体介绍。 + +所以在应用启动成功后需要将自身数据注册到 `Zookeeper` 中。 + +![](https://i.loli.net/2019/07/19/5d313938d33fb47365.jpg) +![](https://i.loli.net/2019/07/19/5d31393ec08cc70862.jpg) + +最主要的目的就是将当前应用的 `ip + cim-server-port+ http-port` 注册上去。 + + +![](https://i.loli.net/2019/07/19/5d3139401722563891.jpg) + +上图是我在演示环境中注册的两个 `cim-server` 实例(由于在一台服务器,所以只是端口不同)。 + +这样在客户端(监听这个 `Zookeeper` 节点)就能实时的知道目前可用的服务信息。 + +## 登录 + +当客户端请求 `cim-forward-route` 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接,如之前的流程所示: + +![](https://i.loli.net/2019/07/19/5d313940c94a652003.jpg) + +这时客户端会发送一个特殊报文,表明当前是登录信息。 + +服务端收到后就需要将该客户端的 `userID` 和当前 `Channel` 通道关系保存起来。 + +![](https://i.loli.net/2019/07/19/5d3139429a08032119.jpg) +![](https://i.loli.net/2019/07/19/5d313943a25c029466.jpg) + +同时也缓存了用户的信息,也就是 `userID` 和 用户名。 + + +## 离线 + +当客户端断线后也需要将刚才缓存的信息清除掉。 + +![](https://i.loli.net/2019/07/19/5d313944af8c328873.jpg) + +同时也需要调用 `route` 接口清除相关信息(具体接口看下文)。 + + + +## IM 路由 + +![](https://i.loli.net/2019/07/19/5d313945a039126377.jpg) + +从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 `HTTP` 服务承接了客户端和服务端。 + +目前主要是以下几个接口。 + +### 注册接口 + +![](https://i.loli.net/2019/07/19/5d313946d089382853.jpg) +![](https://i.loli.net/2019/07/19/5d31394c795cc10022.jpg) + +由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。 + +这里就设计的比较简单,直接利用 `Redis` 来存储用户信息;用户信息也只有 `ID` 和 `userName` 而已。 + +只是为了方便查询在 `Redis` 中的 `KV` 又反过来存储了一份 `VK`,这样 `ID` 和 `userName` 都必须唯一。 + + +### 登录接口 + +这里的登录和 `cim-server` 中的登录不一样,具有业务性质, + +![](https://i.loli.net/2019/07/19/5d31394d5915e56923.jpg) + +- 登录成功之后需要判断是否是重复登录(一个用户只能运行一个客户端)。 +- 登录成功后需要从 `Zookeeper` 中获取服务列表(`cim-server`)并根据某种算法选择一台服务返回给客户端。 +- 登录成功之后还需要保存路由信息,也就是当前用户分配的服务实例保存到 `Redis` 中。 + +为了实现只能一个用户登录,使用了 `Redis` 中的 `set` 来保存登录信息;利用 `userID` 作为 `key` ,重复的登录就会写入失败。 + +![](https://i.loli.net/2019/07/19/5d31394de19fe32033.jpg) +![](https://i.loli.net/2019/07/19/5d31394e5a7cf72944.jpg) + +> 类似于 Java 中的 HashSet,只能去重保存。 + + +获取一台可用的路由实例也比较简单: + +![](https://i.loli.net/2019/07/19/5d31394ed00d392001.jpg) + +- 先从 `Zookeeper` 获取所有的服务实例做一个内部缓存。 +- 轮询选择一台服务器(目前只有这一种算法,后续会新增)。 + +当然要获取 `Zookeeper` 中的服务实例前自然是需要监听 `cim-server` 之前注册上去的那个节点。 + +具体代码如下: + +![](https://i.loli.net/2019/07/19/5d31394f4609531937.jpg) +![](https://i.loli.net/2019/07/19/5d31394f9ad3c50783.jpg) +![](https://i.loli.net/2019/07/19/5d31395006b0b64086.jpg) + +也是在应用启动之后监听 `Zookeeper` 中的路由节点,一旦发生变化就会更新内部缓存。 + +> 这里使用的是 Guava 的 cache,它基于 `ConcurrentHashMap`,所以可以保证`清除、新增缓存`的原子性。 + +### 群聊接口 + +这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其余所有客户端都能收到! + +流程肯定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 `SessionSocketHolder` 中遍历所有 `Channel`(通道)然后下发消息即可。 + +服务端是单机倒也可以,但现在是集群设计。所以所有的客户端会根据之前的轮询算法分配到不同的 `cim-server` 实例中。 + +因此就需要路由层来发挥作用了。 + +![](https://i.loli.net/2019/07/19/5d313955b870762733.jpg) +![](https://i.loli.net/2019/07/19/5d31395784bf477598.jpg) + +路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。 + +路由关系在 `Redis` 中的存放如下: + +![](https://i.loli.net/2019/07/19/5d313957c158a14876.jpg) + +由于 `Redis` 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 `cim-route:*` 数据,会导致 Redis 不能处理其他请求。 + +所以这里改为使用 scan 命令来遍历所有的 `cim-route:*`。 + +--- + +接着会挨个调用每个客户端所在的服务端的 `HTTP` 接口用于推送消息。 + +在 `cim-server` 中的实现如下: + +![](https://i.loli.net/2019/07/19/5d3139590e61247525.jpg) +![](https://i.loli.net/2019/07/19/5d31395979de822216.jpg) + +`cim-server` 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。 + + +### 在线用户接口 + +这是一个辅助接口,可以查询出当前在线用户信息。 + +![](https://i.loli.net/2019/07/19/5d313959c343668519.jpg) +![](https://i.loli.net/2019/07/19/5d31395a06ae762846.jpg) + +实现也很简单,也就是查询之前保存 "用户登录状态的那个去重 `set` "即可。 + +### 私聊接口 + +之所以说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。 + +一般我们使用私聊的前提肯定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。 + +类似于这样: + +![](https://i.loli.net/2019/07/19/5d31396246da062612.jpg) + +在我们这个场景中,私聊的前提就是需要获得在线用户的 `userID`。 + + +![](https://i.loli.net/2019/07/19/5d3139637537e14521.jpg) + +所以私聊接口在收到消息后需要查询到接收者所在的 `cim-server` 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 `HTTP` 接口下发信息。 + +只是群聊是遍历所有的在线用户,私聊只发送一个的区别。 + +### 下线接口 + +一旦客户端下线,我们就需要将之前存放在 `Redis` 中的一些信息删除掉(路由信息、登录状态)。 + +![](https://i.loli.net/2019/07/19/5d313963e388b20088.jpg) +![](https://i.loli.net/2019/07/19/5d3139642e6b729312.jpg) + + + + +## IM 客户端 + +客户端中的一些逻辑其实在上文已经谈到一些了。 + +### 登录 + +第一步也就是登录,需要在启动时调用 `route` 的登录接口,获得 `cim-server` 信息再创建连接。 + +![](https://i.loli.net/2019/07/19/5d3139645e63671036.jpg) + +![image-20190102001525565](https://i.loli.net/2019/07/19/5d313964a2d2194790.jpg) + +![](https://i.loli.net/2019/07/19/5d3139661c62598094.jpg) + +登录过程中 `route` 接口会判断是否为重复登录,重复登录则会直接退出程序。 + +![](https://i.loli.net/2019/07/19/5d31396b6301828962.jpg) + +接下来是利用 `route` 接口返回的 `cim-server` 实例信息(`ip+port`)创建连接。 + +最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 `Channel` 的关系。 + +![](https://i.loli.net/2019/07/19/5d31396ba9bfa44516.jpg) + +### 自定义协议 + +上文提到的一些`登录报文、真正的消息报文`这些其实都是在我们自定义协议中可以区别出来的。 + +由于是使用 `Google Protocol Buffer` 编解码,所以先看看原始格式。 + +![](https://i.loli.net/2019/07/19/5d31396be687915596.jpg) + +其实这个协议中目前一共就三个字段: + +- `requestId` 可以理解为 `userId`。 +- `reqMsg` 就是真正的消息。 +- `type` 也就是上文提到的消息类别。 + + +目前主要是三种类型,分别对应不同的业务: + +![](https://i.loli.net/2019/07/19/5d313aac604fa88452.jpg) + +### 心跳 + +为了保持客户端和服务端的连接,每隔一段时间没有发送消息都需要自动的发送心跳。 + +目前的策略是每隔一分钟就是发送一个心跳包到服务端: + +![](https://i.loli.net/2019/07/19/5d313aad7641038034.jpg) +![](https://i.loli.net/2019/07/19/5d313aae3e8ee56138.jpg) + +这样服务端每隔一分钟没有收到业务消息时就会收到 `ping` 的心跳包: + +![](https://i.loli.net/2019/07/19/5d313aaed8e5685298.jpg) + + +### 内置命令 + +客户端也内置了一些基本命令来方便使用。 + +| 命令 | 描述| +| ------ | ------ | +| `:q` | 退出客户端| +| `:olu` | 获取所有在线用户信息 | +| `:all` | 获取所有命令 | +| `:` | 更多命令正在开发中。。 | + +![](https://i.loli.net/2019/07/19/5d31396246da062612.jpg) + +比如输入 `:q` 就会退出客户端,同时会关闭一些系统资源。 + +![](https://i.loli.net/2019/07/19/5d313aaf6f05466906.jpg) +![](https://i.loli.net/2019/07/19/5d313aafe852815113.jpg) + +当输入 `:olu`(`onlineUser` 的简写)就会去调用 `route` 的获取所有在线用户接口。 + +![](https://i.loli.net/2019/07/19/5d313ab06dd6c35435.jpg) +![](https://i.loli.net/2019/07/19/5d313ab0ea75d16268.jpg) + +### 群聊 + +群聊的使用非常简单,只需要在控制台输入消息回车即可。 + +这时会去调用 `route` 的群聊接口。 + +![](https://i.loli.net/2019/07/19/5d313ab63223a26868.jpg) + +### 私聊 + +私聊也是同理,但前提是需要触发关键字;使用 `userId;;消息内容` 这样的格式才会给某个用户发送消息,所以一般都需要先使用 `:olu` 命令获取所以在线用户才方便使用。 + +![](https://i.loli.net/2019/07/19/5d313ab6ac1d016245.jpg) + +### 消息回调 + +为了满足一些定制需求,比如消息需要保存之类的。 + +所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。 + +![](https://i.loli.net/2019/07/19/5d313ab75333232387.jpg) +![](https://i.loli.net/2019/07/19/5d313ab7e6e3426627.jpg) + +因此先创建了一个 `caller` 的 `bean`,这个 `bean` 中包含了一个 `CustomMsgHandleListener` 接口,需要自行处理只需要实现此接口即可。 + +### 自定义界面 + +由于我自己不怎么会写界面,但保不准有其他大牛会写。所以客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及之后的业务)都是以接口形式提供。 + +也方便后面做页面集成,只需要调这些接口就行了;具体实现不用怎么关心。 + +# 总结 + +`cim` 目前只是第一版,BUG 多,功能少(只拉了几个群友做了测试);不过后续还会接着完善,至少这一版会给那些没有相关经验的朋友带来一些思路。 + +后续计划: + +![](https://i.loli.net/2019/07/19/5d313ab870c5834835.jpg) + +完整源码: + +[https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) + +如果这篇对你有所帮助还请不吝转发。 diff --git a/docs/soft-skills/Interview-experience.md b/docs/soft-skills/Interview-experience.md new file mode 100644 index 00000000..926e4e85 --- /dev/null +++ b/docs/soft-skills/Interview-experience.md @@ -0,0 +1,359 @@ +![](https://i.loli.net/2019/07/19/5d313e9cab8b192420.jpg) + +## 前言 + +最近有些朋友在面试阿里,加上 [https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) 项目的原因也有小伙伴和我讨论,近期也在负责部门的招聘,这让我想起年初那段长达三个月的奇葩面试经历🤣。 + +本来没想拿出来说的,毕竟最后也没成。 + +但由于那几个月的经历让我了解到了大厂的工作方式、对候选同学的考察重点以及面试官的套路等都有了全新的认识。 + +当然最重要的是这段时间的查漏补缺也让自己精进不少。 + + +先交代下背景吧: + + +从去年 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://i.loli.net/2019/07/19/5d313e9f5616854253.jpg) + +厂库地址: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) + +最后总结下这将近四个月的面试心得: + +- 一定要积极的推销自己,像在 A 部门的三面时,由于基础答得不是很好;所以最后我表达了自己的态度,对工作、技术的积极性。让面试官看到你的潜力值得一个 HC 名额。 +- 面试过程中遇到自己的不会的可以主动提出,切不可不懂装懂,这一问就露馅。可以将面试官引导到自己擅长的领域。比如当时我正好研究了锁,所以和面试官一聊就是半小时这就是加分项。 +- 平时要主动积累知识。写博客和参与开源项目就是很好的方式。 +- 博客可以记录自己踩过的坑,加深印象,而且在写的过程中可以查漏补缺,最后把整个知识体系巩固的比较牢固,良好的内容还可以得到意想不到的收获,比如我第一次面试的机会。 +- GitHub 是开发者的一张名片,积极参与开源项目可以和全球大佬头脑风暴,并且在面试过程中绝对是一个加分利器。 +- 面试官一般最后都会问你有什么要问我的?千万不要问一些公司福利待遇之类的问题。可以问下本次面试的表现?还有哪些需要完善的?从而知道自己答得如何也能补全自己。 + +还有一点:不要在某次面试失利后否定自己,有时真的不是自己能力不行。这个也讲缘分。 + +**塞翁失马焉知非福** + +我就是个例子,虽然最后没能去成阿里,现在在公司也是一个部门的技术负责人,在我们城市还有个窝,温馨的家,和女朋友一起为想要的生活努力奋斗。 + + +> 欢迎关注作者公众号于我交流🤗。 diff --git a/docs/soft-skills/TCP-IP.md b/docs/soft-skills/TCP-IP.md new file mode 100644 index 00000000..d37c58a6 --- /dev/null +++ b/docs/soft-skills/TCP-IP.md @@ -0,0 +1,24 @@ +# TCP/IP 协议 + +`TCP/IP` 总结起来就三个要点 +- 三次握手的意义。 +- 超时重发。 +- 滑动窗口。 + +## 三次握手 +![](https://i.loli.net/2019/07/19/5d313e983e24378832.jpg) + +如图类似: +1. 发送者问接收者我发消息了,你收到了嘛? +2. 接收者回复发送者我收到了,你发消息没问题,我收消息也没问题。但我不知道我的发消息有没有问题,你收到了回复我下。 +3. 发送者告诉接收者,我收到你的消息了,你发消息没问题。通信成功我们开始工作吧! + + +## 超时重发 + +当发送者向接收者发包后,如果过了一段时间(超时时间)依然没有收到消息,就当做本次包丢失,需要重新补发。 + +并且如果一次性发了三个包,只要最后一个包确认收到之后就默认前面两个也收到了。 + +## 滑动窗口 +假设一次性发送包的大小为3,那么每次可以发3个包,而且可以边发边接收,这样就会增强效率。这里的 3 就是滑动窗口的大小,这样的发送方式也叫滑动窗口协议。 diff --git a/docs/soft-skills/how-to-be-developer.md b/docs/soft-skills/how-to-be-developer.md new file mode 100644 index 00000000..7925ef12 --- /dev/null +++ b/docs/soft-skills/how-to-be-developer.md @@ -0,0 +1,345 @@ +![](https://i.loli.net/2019/07/19/5d313ea44cb8b81194.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://i.loli.net/2019/07/19/5d313eafdee9c64439.jpg) + +结合上文产出了一个思维导图更直观些。 + +## 总结 + +本文结合了自身的一些经验列举了一些方法,不一定对每位都有效需要自行判断。 + +也反反复复写了差不多一周的时间,希望对在这条路上和正在路上的朋友们起到一些作用。 + +大部分都只是谈了个思路,其实每一项单聊都能写很多。每个点都有推荐一本书籍,有更好建议欢迎留言讨论。 + +上文大部分的知识点都有维护在 GitHub 上,感兴趣的朋友可以自行查阅: + +![](https://i.loli.net/2019/07/19/5d313eb45ba5b49307.jpg) + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) diff --git a/docs/soft-skills/how-to-use-git-efficiently.md b/docs/soft-skills/how-to-use-git-efficiently.md new file mode 100755 index 00000000..7a2ceaf0 --- /dev/null +++ b/docs/soft-skills/how-to-use-git-efficiently.md @@ -0,0 +1,127 @@ +**[原文链接](https://medium.freecodecamp.org/how-to-use-git-efficiently-54320a236369)** + +![](https://i.loli.net/2019/07/19/5d313e9893e8f53523.jpg) + +> 代码昨天还是运行好好的今天就不行了。 + +> 代码被删了。 + +> 突然出现了一个奇怪的 bug,但是没人知道怎么回事。 + + +如果你出现过上面的任何一种情况,那本篇文章就是为你准备的。 + +除了知道 `git add`, `git commit` , `git push` 之外,Git 中还需要其他重要的技术需要掌握。长远来看对我们是有帮助的。这里我将向你展示 Git 的最佳实践。 + + + + +# Git 工作流 + +当有多个开发者同时涉及到一个项目时那么就非常有必要正确使用 Git 工作流。 + +这里我将介绍一种工作流,它在一个多人大型项目中将非常有用。 + +![](https://i.loli.net/2019/07/19/5d313e9b120b999387.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://i.loli.net/2019/07/19/5d313e9c5e34a14226.jpg) + +在分支名字的旁边有一个 "New pull request" 按钮,点击之后将会显示如下界面: + +![](https://i.loli.net/2019/07/19/5d313e9ed71a587054.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 代码是最新的。 + + + + +> 我们现在团队就是按照这样的方式进行开发,确实可以尽可能的减少代码管理上的问题。 + + +# 题外话 + +像之前那篇[《如何成为一位「不那么差」的程序员》](https://crossoverjie.top/2018/08/12/personal/how-to-be-developer/#English-%E6%8C%BA%E9%87%8D%E8%A6%81)说的那样,建议大家都多看看国外的优质博客。 + +甚至尝试和作者交流,经过沟通原作者也会在原文中贴上我的翻译链接。大家互惠互利使好的文章转播的更广。 + +![](https://i.loli.net/2019/07/19/5d313ea5e16f824179.jpg) + +![](https://i.loli.net/2019/07/19/5d313ea9407c511988.jpg) + + +**你的点赞与转发是最大的支持。** diff --git a/docs/thread/ArrayBlockingQueue.md b/docs/thread/ArrayBlockingQueue.md new file mode 100644 index 00000000..3fd0f55a --- /dev/null +++ b/docs/thread/ArrayBlockingQueue.md @@ -0,0 +1,283 @@ +![](https://i.loli.net/2019/07/19/5d313f289d57811656.jpg) + + + +# 前言 + +较长一段时间以来我都发现不少开发者对 jdk 中的 `J.U.C`(java.util.concurrent)也就是 Java 并发包的使用甚少,更别谈对它的理解了;但这却也是我们进阶的必备关卡。 + +之前或多或少也分享过相关内容,但都不成体系;于是便想整理一套与并发包相关的系列文章。 + +其中的内容主要包含以下几个部分: + +- 根据定义自己实现一个并发工具。 +- JDK 的标准实现。 +- 实践案例。 + + +基于这三点我相信大家对这部分内容不至于一问三不知。 + +既然开了一个新坑,就不想做的太差;所以我打算将这个列表下的大部分类都讲到。 + +![](https://i.loli.net/2019/07/19/5d313f2c7f91450086.jpg) + + +所以本次重点讨论 `ArrayBlockingQueue`。 + +# 自己实现 + +在自己实现之前先搞清楚阻塞队列的几个特点: + +- 基本队列特性:先进先出。 +- 写入队列空间不可用时会阻塞。 +- 获取队列数据时当队列为空时将阻塞。 + + +实现队列的方式多种,总的来说就是数组和链表;其实我们只需要搞清楚其中一个即可,不同的特性主要表现为数组和链表的区别。 + +这里的 `ArrayBlockingQueue` 看名字很明显是由数组实现。 + +我们先根据它这三个特性尝试自己实现试试。 + +## 初始化队列 + +我这里自定义了一个类:`ArrayQueue`,它的构造函数如下: + +```java + public ArrayQueue(int size) { + items = new Object[size]; + } +``` + +很明显这里的 `items` 就是存放数据的数组;在初始化时需要根据大小创建数组。 + +![](https://i.loli.net/2019/07/19/5d313f2fb8fe622692.jpg) + +## 写入队列 + +写入队列比较简单,只需要依次把数据存放到这个数组中即可,如下图: + +![](https://i.loli.net/2019/07/19/5d313f32aa77680089.jpg) + +但还是有几个需要注意的点: + +- 队列满的时候,写入的线程需要被阻塞。 +- 写入过队列的数量大于队列大小时需要从第一个下标开始写。 + +先看第一个`队列满的时候,写入的线程需要被阻塞`,先来考虑下如何才能使一个线程被**阻塞**,看起来的表象线程卡住啥事也做不了。 + +有几种方案可以实现这个效果: + +- `Thread.sleep(timeout)`线程休眠。 +- `object.wait()` 让线程进入 `waiting` 状态。 + +> 当然还有一些 `join、LockSupport.part` 等不在本次的讨论范围。 + +阻塞队列还有一个非常重要的特性是:当队列空间可用时(取出队列),写入线程需要被唤醒让数据可以写入进去。 + +所以很明显`Thread.sleep(timeout)`不合适,它在到达超时时间之后便会继续运行;达不到**空间可用时**才唤醒继续运行这个特点。 + +其实这样的一个特点很容易让我们想到 Java 的等待通知机制来实现线程间通信;更多线程见通信的方案可以参考这里:[深入理解线程通信](https://crossoverjie.top/2018/03/16/java-senior/thread-communication/#%E7%AD%89%E5%BE%85%E9%80%9A%E7%9F%A5%E6%9C%BA%E5%88%B6) + +所以我这里的做法是,一旦队列满时就将写入线程调用 `object.wait()` 进入 `waiting` 状态,直到空间可用时再进行唤醒。 + +```java + /** + * 队列满时的阻塞锁 + */ + private Object full = new Object(); + + /** + * 队列空时的阻塞锁 + */ + private Object empty = new Object(); +``` + +![](https://i.loli.net/2019/07/19/5d313f35038c649523.jpg) + +所以这里声明了两个对象用于队列满、空情况下的互相通知作用。 + + +在写入数据成功后需要使用 `empty.notify()`,这样的目的是当获取队列为空时,一旦写入数据成功就可以把消费队列的线程唤醒。 + + +> 这里的 wait 和 notify 操作都需要对各自的对象使用 `synchronized` 方法块,这是因为 wait 和 notify 都需要获取到各自的锁。 + +## 消费队列 + +上文也提到了:当队列为空时,获取队列的线程需要被阻塞,直到队列中有数据时才被唤醒。 + +![](https://i.loli.net/2019/07/19/5d313f3ad811825796.jpg) + +代码和写入的非常类似,也很好理解;只是这里的等待、唤醒恰好是相反的,通过下面这张图可以很好理解: + +![](https://i.loli.net/2019/07/19/5d313f3d9cf3f67442.jpg) + +总的来说就是: + +- 写入队列满时会阻塞直到获取线程消费了队列数据后唤醒**写入线程**。 +- 消费队列空时会阻塞直到写入线程写入了队列数据后唤醒**消费线程**。 + + +## 测试 + +先来一个基本的测试:单线程的写入和消费。 + +![](https://i.loli.net/2019/07/19/5d313f405d0e291936.jpg) + +```log +3 +123 +1234 +12345 +``` + +通过结果来看没什么问题。 + +--- + +当写入的数据超过队列的大小时,就只能消费之后才能接着写入。 + +![](https://i.loli.net/2019/07/19/5d313f41cf91223286.jpg) + +```log +2019年04月09日 16:24:41.040 [Thread-0] INFO c.c.concurrent.ArrayQueueTest - [Thread-0]123 +2019年04月09日 16:24:41.040 [main] INFO c.c.concurrent.ArrayQueueTest - size=3 +2019年04月09日 16:24:41.047 [main] INFO c.c.concurrent.ArrayQueueTest - 1234 +2019年04月09日 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 12345 +2019年04月09日 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 123456 +``` + +从运行结果也能看出只有当消费数据后才能接着往队列里写入数据。 + +--- + +![](https://i.loli.net/2019/07/19/5d313f4346e6458625.jpg) + +![](https://i.loli.net/2019/07/19/5d313f49e902d49687.jpg) + +而当没有消费时,再往队列里写数据则会导致写入线程被阻塞。 + + + +### 并发测试 + +![](https://i.loli.net/2019/07/19/5d313f4d00e9696823.jpg) + +三个线程并发写入300条数据,其中一个线程消费一条。 + +```log +=====0 +299 +``` + +最终的队列大小为 299,可见线程也是安全的。 + +> 由于不管是写入还是获取方法里的操作都需要获取锁才能操作,所以整个队列是线程安全的。 + + +# ArrayBlockingQueue + +下面来看看 JDK 标准的 `ArrayBlockingQueue` 的实现,有了上面的基础会更好理解。 + +## 初始化队列 + +![](https://i.loli.net/2019/07/19/5d313f5007ecc42909.jpg) + +看似要复杂些,但其实逐步拆分后也很好理解: + +第一步其实和我们自己写的一样,初始化一个队列大小的数组。 + + +第二步初始化了一个重入锁,这里其实就和我们之前使用的 `synchronized` 作用一致的; + +只是这里在初始化重入锁的时候默认是`非公平锁`,当然也可以指定为 `true` 使用公平锁;这样就会按照队列的顺序进行写入和消费。 + +> 更多关于 `ReentrantLock` 的使用和原理请参考这里:[ReentrantLock 实现原理](https://crossoverjie.top/2018/01/25/ReentrantLock/) + +三四两步则是创建了 `notEmpty notFull` 这两个条件,他的作用于用法和之前使用的 `object.wait/notify` 类似。 + +这就是整个初始化的内容,其实和我们自己实现的非常类似。 + + +## 写入队列 + +![](https://i.loli.net/2019/07/19/5d313f52b585671592.jpg) +![](https://i.loli.net/2019/07/19/5d313f5a1c28c84172.jpg) + +其实会发现阻塞写入的原理都是差不多的,只是这里使用的是 Lock 来显式获取和释放锁。 + +同时其中的 `notFull.await();notEmpty.signal();` 和我们之前使用的 `object.wait/notify` 的用法和作用也是一样的。 + + +当然它还是实现了超时阻塞的 `API`。 + +![](https://i.loli.net/2019/07/19/5d313f5b7a55b36447.jpg) + +也是比较简单,使用了一个具有超时时间的等待方法。 + +## 消费队列 + +再看消费队列: + +![](https://i.loli.net/2019/07/19/5d313f5e98db976041.jpg) +![](https://i.loli.net/2019/07/19/5d313f5fea44784743.jpg) + +也是差不多的,一看就懂。 + +而其中的超时 API 也是使用了 `notEmpty.awaitNanos(nanos)` 来实现超时返回的,就不具体说了。 + + +# 实际案例 + +说了这么多,来看一个队列的实际案例吧。 + +背景是这样的: + +> 有一个定时任务会按照一定的间隔时间从数据库中读取一批数据,需要对这些数据做校验同时调用一个远程接口。 + + +简单的做法就是由这个定时任务的线程去完成读取数据、消息校验、调用接口等整个全流程;但这样会有一个问题: + +假设调用外部接口出现了异常、网络不稳导致耗时增加就会造成整个任务的效率降低,因为他都是串行会互相影响。 + + +所以我们改进了方案: + +![](https://i.loli.net/2019/07/19/5d313f61e33f644196.jpg) + +其实就是一个典型的生产者消费者模型: + +- 生产线程从数据库中读取消息丢到队列里。 +- 消费线程从队列里获取数据做业务逻辑。 + +这样两个线程就可以通过这个队列来进行解耦,互相不影响,同时这个队列也能起到缓冲的作用。 + +但在使用过程中也有一些小细节值得注意。 + +因为这个外部接口是支持批量执行的,所以在消费线程取出数据后会在内存中做一个累加,一旦达到阈值或者是累计了一个时间段便将这批累计的数据处理掉。 + +但由于开发者的大意,在消费的时候使用的是 `queue.take()` 这个阻塞的 API;正常运行没啥问题。 + +可一旦原始的数据源,也就是 DB 中没数据了,导致队列里的数据也被消费完后这个消费线程便会被阻塞。 + +这样上一轮积累在内存中的数据便一直没机会使用,直到数据源又有数据了,一旦中间间隔较长时便可能会导致严重的业务异常。 + +所以我们最好是使用 `queue.poll(timeout)` 这样带超时时间的 api,除非业务上有明确的要求需要阻塞。 + +这个习惯同样适用于其他场景,比如调用 http、rpc 接口等都需要设置合理的超时时间。 + +# 总结 + +关于 `ArrayBlockingQueue` 的相关分享便到此结束,接着会继续更新其他并发容器及并发工具。 + +对本文有任何相关问题都可以留言讨论。 + + + +本文涉及到的所有源码: + +https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java + + +**你的点赞与分享是对我最大的支持** diff --git a/docs/thread/ConcurrentHashMap.md b/docs/thread/ConcurrentHashMap.md new file mode 100644 index 00000000..0a9d7546 --- /dev/null +++ b/docs/thread/ConcurrentHashMap.md @@ -0,0 +1,99 @@ +**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。** + +# ConcurrentHashMap 实现原理 + +由于 `HashMap` 是一个线程不安全的容器,主要体现在容量大于`总量*负载因子`发生扩容时会出现环形链表从而导致死循环。 + +因此需要支持线程安全的并发容器 `ConcurrentHashMap` 。 + +## JDK1.7 实现 + +### 数据结构 +![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) + +如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。 + +`ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。 + +### get 方法 +`ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。 + +只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值([volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#%E5%8F%AF%E8%A7%81%E6%80%A7))。 + +### put 方法 + +内部 `HashEntry` 类 : + +```java + static final class HashEntry { + final int hash; + final K key; + volatile V value; + volatile HashEntry next; + + HashEntry(int hash, K key, V value, HashEntry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + } +``` + +虽然 HashEntry 中的 value 是用 `volatile` 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。 + +首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。 + +而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。 + +### size 方法 + +每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap` 的 `size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。 + +但如果每次调用 `size` 方法将其余的修改操作加锁效率也很低。所以做法是先尝试两次将 `count` 累加,如果容器的 `count` 发生了变化再加锁来统计 `size`。 + +至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。 + + + +## JDK1.8 实现 + +![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) + +1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 + +其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 + +![](https://s2.loli.net/2024/05/21/MVr92SEeJI34fas.png) + + +也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 + +其中的 `val next` 都用了 volatile 修饰,保证了可见性。 + +### put 方法 + +重点来看看 put 函数: + +![](https://s2.loli.net/2024/05/21/EpBRMOQnD8bx2wH.png) + + +- 根据 key 计算出 hashcode 。 +- 判断是否需要进行初始化。 +- `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 +- 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 +- 如果都不满足,则利用 synchronized 锁写入数据。 +- 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 + +### get 方法 + +![](https://s2.loli.net/2024/05/21/CFvAuGp8BMUko6I.png) + + +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 都不满足那就按照链表的方式遍历获取值。 + +## 总结 + +1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 \ No newline at end of file diff --git a/docs/thread/Java-lock.md b/docs/thread/Java-lock.md new file mode 100644 index 00000000..ff764786 --- /dev/null +++ b/docs/thread/Java-lock.md @@ -0,0 +1,39 @@ +# 对锁的一些认知 有哪些锁 + +## 同一进程 + +### [重入锁](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ReentrantLock.md) +使用 `ReentrantLock` 获取锁的时候会判断当前线程是否为获取锁的线程,如果是则将同步的状态 +1 ,释放锁的时候则将状态 -1。只有将同步状态的次数置为 0 的时候才会最终释放锁。 + +### 读写锁 +使用 `ReentrantReadWriteLock` ,同时维护一对锁:读锁和写锁。当写线程访问时则其他所有锁都将阻塞,读线程访问时则不会。通过读写锁的分离可以很大程度的提高并发量和吞吐量。 + + +## 不同进程 + +分布式锁: + +### 基于数据库 +可以创建一张表,将其中的某个字段设置为`唯一索引`,当多个请求过来的时候只有新建记录成功的请求才算获取到锁,当使用完毕删除这条记录的时候即释放锁。 + +存在的问题: +- 数据库单点问题,挂了怎么办? +- 不是重入锁,同一进程无法在释放锁之前再次获得锁,因为数据库中已经存在了一条记录了。 +- 锁是非阻塞的,一旦 `insert` 失败则会立即返回,并不会进入阻塞队列只能下一次再次获取。 +- 锁没有失效时间,如果那个进程解锁失败那就没有请求可以再次获取锁了。 + +解决方案: +- 数据库切换为主从,不存在单点。 +- 在表中加入一个同步状态字段,每次获取锁的是加 1 ,释放锁的时候`-1`,当状态为 0 的时候就删除这条记录,即释放锁。 +- 非阻塞的情况可以用 `while` 循环来实现,循环的时候记录时间,达到 X 秒记为超时,`break`。 +- 可以开启一个定时任务每隔一段时间扫描找出多少 X 秒都没有被删除的记录,主动删除这条记录。 + +### 基于 Redis + +使用 `setNX(key) setEX(timeout)` 命令,只有在该 `key` 不存在的时候创建这个 `key`,就相当于获取了锁。由于有超时时间,所以过了规定时间会自动删除,这样也可以避免死锁。 + +可以参考: + +[基于 Redis 的分布式锁](http://crossoverjie.top/2018/03/29/distributed-lock/distributed-lock-redis/) + +### 基于 ZK diff --git a/docs/thread/ReentrantLock.md b/docs/thread/ReentrantLock.md new file mode 100644 index 00000000..802b0bbc --- /dev/null +++ b/docs/thread/ReentrantLock.md @@ -0,0 +1,299 @@ +# ReentrantLock 实现原理 + +使用 `synchronized` 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。 + +而 `ReentrantLock` 就是一个普通的类,它是基于 `AQS(AbstractQueuedSynchronizer)`来实现的。 + +是一个**重入锁**:一个线程获得了锁之后仍然可以**反复**的加锁,不会出现自己阻塞自己的情况。 + +> `AQS` 是 `Java` 并发包里实现锁、同步的一个重要的基础框架。 + + +## 锁类型 + +ReentrantLock 分为**公平锁**和**非公平锁**,可以通过构造方法来指定具体类型: + +```java + //默认非公平锁 + public ReentrantLock() { + sync = new NonfairSync(); + } + + //公平锁 + public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); + } +``` + +默认一般使用**非公平锁**,它的效率和吞吐量都比公平锁高的多(后面会分析具体原因)。 + +## 获取锁 + +通常的使用方式如下: + +```java + private ReentrantLock lock = new ReentrantLock(); + public void run() { + lock.lock(); + try { + //do bussiness + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } +``` + +### 公平锁获取锁 +首先看下获取锁的过程: + +```java + public void lock() { + sync.lock(); + } +``` + +可以看到是使用 `sync`的方法,而这个方法是一个抽象方法,具体是由其子类(`FairSync`)来实现的,以下是公平锁的实现: + +```java + final void lock() { + acquire(1); + } + + //AbstractQueuedSynchronizer 中的 acquire() + public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); + } +``` + +第一步是尝试获取锁(`tryAcquire(arg)`),这个也是由其子类实现: + +```java + protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; + } + } +``` + +首先会判断 `AQS` 中的 `state` 是否等于 0,0 表示目前没有其他线程获得锁,当前线程就可以尝试获取锁。 + +**注意**:尝试之前会利用 `hasQueuedPredecessors()` 方法来判断 AQS 的队列中中是否有其他线程,如果有则不会尝试获取锁(**这是公平锁特有的情况**)。 + +如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(`setExclusiveOwnerThread(current)`)。 + +如果 `state` 大于 0 时,说明锁已经被获取了,则需要判断获取锁的线程是否为当前线程(`ReentrantLock` 支持重入),是则需要将 `state + 1`,并将值更新。 + + +#### 写入队列 +如果 `tryAcquire(arg)` 获取锁失败,则需要用 `addWaiter(Node.EXCLUSIVE)` 将当前线程写入队列中。 + +写入之前需要将当前线程包装为一个 `Node` 对象(`addWaiter(Node.EXCLUSIVE)`)。 + +> AQS 中的队列是由 Node 节点组成的双向链表实现的。 + + +包装代码: + +```java + private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; + } + +``` + +首先判断队列是否为空,不为空时则将封装好的 `Node` 利用 `CAS` 写入队尾,如果出现并发写入失败就需要调用 `enq(node);` 来写入了。 + +```java + private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } + } +``` + +这个处理逻辑就相当于`自旋`加上 `CAS` 保证一定能写入队列。 + +#### 挂起等待线程 + +写入队列之后需要将当前线程挂起(利用`acquireQueued(addWaiter(Node.EXCLUSIVE), arg)`): + +```java + final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } + } +``` + +首先会根据 `node.predecessor()` 获取到上一个节点是否为头节点,如果是则尝试获取一次锁,获取成功就万事大吉了。 + +如果不是头节点,或者获取锁失败,则会根据上一个节点的 `waitStatus` 状态来处理(`shouldParkAfterFailedAcquire(p, node)`)。 + +`waitStatus` 用于记录当前节点的状态,如节点取消、节点等待等。 + +`shouldParkAfterFailedAcquire(p, node)` 返回当前线程是否需要挂起,如果需要则调用 `parkAndCheckInterrupt()`: + +```java + private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); + } +``` + +他是利用 `LockSupport` 的 `part` 方法来挂起当前线程的,直到被唤醒。 + + +### 非公平锁获取锁 +公平锁与非公平锁的差异主要在获取锁: + +公平锁就相当于买票,后来的人需要排到队尾依次买票,**不能插队**。 + +而非公平锁则没有这些规则,是**抢占模式**,每来一个人不会去管队列如何,直接尝试获取锁。 + +非公平锁: +```java + final void lock() { + //直接尝试获取锁 + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } +``` + +公平锁: +```java + final void lock() { + acquire(1); + } +``` + +还要一个重要的区别是在尝试获取锁时`tryAcquire(arg)`,非公平锁是不需要判断队列中是否还有其他线程,也是直接尝试获取锁: + +```java + final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + //没有 !hasQueuedPredecessors() 判断 + if (compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; + } +``` + +## 释放锁 + +公平锁和非公平锁的释放流程都是一样的: + +```java + public void unlock() { + sync.release(1); + } + + public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + //唤醒被挂起的线程 + unparkSuccessor(h); + return true; + } + return false; + } + + //尝试释放锁 + protected final boolean tryRelease(int releases) { + int c = getState() - releases; + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; + } +``` + +首先会判断当前线程是否为获得锁的线程,由于是重入锁所以需要将 `state` 减到 0 才认为完全释放锁。 + +释放之后需要调用 `unparkSuccessor(h)` 来唤醒被挂起的线程。 + + +## 总结 + +由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制。 + +所以也就能解释非公平锁的效率会被公平锁更高。 + + + + diff --git a/docs/thread/Synchronize.md b/docs/thread/Synchronize.md new file mode 100644 index 00000000..afbe5900 --- /dev/null +++ b/docs/thread/Synchronize.md @@ -0,0 +1,115 @@ +# synchronized 关键字原理 + +众所周知 `synchronized` 关键字是解决并发问题常用解决方案,有以下三种使用方式: + +- 同步普通方法,锁的是当前对象。 +- 同步静态方法,锁的是当前 `Class` 对象。 +- 同步块,锁的是 `()` 中的对象。 + + +实现原理: +`JVM` 是通过进入、退出对象监视器( `Monitor` )来实现对方法、同步块的同步的。 + +具体实现是在编译之后在同步方法调用前加入一个 `monitor.enter` 指令,在退出方法和异常处插入 `monitor.exit` 的指令。 + +其本质就是对一个对象监视器( `Monitor` )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。 + +而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 `monitor.exit` 之后才能尝试继续获取锁。 + +流程图如下: + +![](https://i.loli.net/2019/07/19/5d313f638492c49210.jpg) + + +通过一段代码来演示: + +```java + public static void main(String[] args) { + synchronized (Synchronize.class){ + System.out.println("Synchronize"); + } + } +``` + +使用 `javap -c Synchronize` 可以查看编译之后的具体信息。 + +``` +public class com.crossoverjie.synchronize.Synchronize { + public com.crossoverjie.synchronize.Synchronize(); + Code: + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + + public static void main(java.lang.String[]); + Code: + 0: ldc #2 // class com/crossoverjie/synchronize/Synchronize + 2: dup + 3: astore_1 + **4: monitorenter** + 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; + 8: ldc #4 // String Synchronize + 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 13: aload_1 + **14: monitorexit** + 15: goto 23 + 18: astore_2 + 19: aload_1 + 20: monitorexit + 21: aload_2 + 22: athrow + 23: return + Exception table: + from to target type + 5 15 18 any + 18 21 18 any +} +``` + +可以看到在同步块的入口和出口分别有 `monitorenter,monitorexit` +指令。 + + +## 锁优化 +`synchronized` 很多都称之为重量锁,`JDK1.6` 中对 `synchronized` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁`和`轻量锁`。 + + +### 轻量锁 +当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(`Lock Record`)区域,同时将锁对象的对象头中 `Mark Word` 拷贝到锁记录中,再尝试使用 `CAS` 将 `Mark Word` 更新为指向锁记录的指针。 + +如果更新**成功**,当前线程就获得了锁。 + +如果更新**失败** `JVM` 会先检查锁对象的 `Mark Word` 是否指向当前线程的锁记录。 + +如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。 + +不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,**轻量锁就会膨胀为重量锁**。 + +#### 解锁 +轻量锁的解锁过程也是利用 `CAS` 来实现的,会尝试锁记录替换回锁对象的 `Mark Word` 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为`重量锁`) + +轻量锁能提升性能的原因是: + +认为大多数锁在整个同步周期都不存在竞争,所以使用 `CAS` 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 `CAS` 的开销,甚至比重量锁更慢。 + +### 偏向锁 + +为了进一步的降低获取锁的代价,`JDK1.6` 之后还引入了偏向锁。 + +偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。 + +当线程访问同步块时,会使用 `CAS` 将线程 ID 更新到锁对象的 `Mark Word` 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。 + +#### 释放锁 +当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 `Mark Word` 设置为无锁或者是轻量锁状态。 + +偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 `-XX:-UseBiasedLocking` 来关闭偏向锁,并默认进入轻量锁。 + + +### 其他优化 + +#### 适应性自旋 +在使用 `CAS` 时,如果操作失败,`CAS` 会自旋再次尝试。由于自旋是需要消耗 `CPU` 资源的,所以如果长期自旋就白白浪费了 `CPU`。`JDK1.6`加入了适应性自旋: + +> 如果某个锁自旋很少成功获得,那么下一次就会减少自旋。 + diff --git a/docs/thread/Thread-common-problem.md b/docs/thread/Thread-common-problem.md new file mode 100644 index 00000000..905767c7 --- /dev/null +++ b/docs/thread/Thread-common-problem.md @@ -0,0 +1,30 @@ +# Java 多线程常见问题 + +## 上下文切换 +多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 +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 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。 + +> 上下文切换是非常耗效率的。 + +通常有以下解决方案: +- 采用无锁编程,比如将数据按照 `Hash(id)` 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。 +- 采用 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` 状态都是一次上下文切换。 + +## 死锁 + +死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。 + +常用的解决方案如下: + +- 尽量一个线程只获取一个锁。 +- 一个线程只占用一个资源。 +- 尝试使用定时锁,至少能保证锁最终会被释放。 + +## 资源限制 + +当在带宽有限的情况下一个线程下载某个资源需要 `1M/S`,当开 10 个线程时速度并不会乘 10 倍,反而还会增加时间,毕竟上下文切换比较耗时。 + +如果是受限于资源的话可以采用集群来处理任务,不同的机器来处理不同的数据,就类似于开始提到的无锁编程。 diff --git a/docs/thread/ThreadPoolExecutor.md b/docs/thread/ThreadPoolExecutor.md new file mode 100644 index 00000000..5e912626 --- /dev/null +++ b/docs/thread/ThreadPoolExecutor.md @@ -0,0 +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` 为线程池的基本大小。 +- `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://s2.loli.net/2024/05/21/uW1eDmV3CGipI2F.png) + + +还要注意的一点是: + +> 自定义的 Command 并不是一个单例,每次执行需要 new 一个实例,不然会报 ` This instance can only be executed once. Please instantiate a new instance.` 异常。 + +## 总结 + +池化技术确实在平时应用广泛,熟练掌握能提高不少效率。 + +文末的 hystrix 源码: + +[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/docs/thread/Threadcore.md b/docs/thread/Threadcore.md new file mode 100644 index 00000000..5411303f --- /dev/null +++ b/docs/thread/Threadcore.md @@ -0,0 +1,144 @@ +# Java 多线程三大核心 + +## 原子性 +`Java` 的原子性就和数据库事务的原子性差不多,一个操作中要么全部执行成功或者失败。 + +`JMM` 只是保证了基本的原子性,但类似于 `i++` 之类的操作,看似是原子操作,其实里面涉及到: + +- 获取 i 的值。 +- 自增。 +- 再赋值给 i。 + +这三步操作,所以想要实现 `i++` 这样的原子操作就需要用到 `synchronized` 或者是 `lock` 进行加锁处理。 + +如果是基础类的自增操作可以使用 `AtomicInteger` 这样的原子类来实现(其本质是利用了 `CPU` 级别的 的 `CAS` 指令来完成的)。 + +其中用的最多的方法就是: `incrementAndGet()` 以原子的方式自增。 +源码如下: + +```java +public final long incrementAndGet() { + for (;;) { + long current = get(); + long next = current + 1; + if (compareAndSet(current, next)) + return next; + } + } +``` + +首先是获得当前的值,然后自增 +1。接着则是最核心的 `compareAndSet() ` 来进行原子更新。 + +```java +public final boolean compareAndSet(long expect, long update) { + return unsafe.compareAndSwapLong(this, valueOffset, expect, update); + } +``` + +其逻辑就是判断当前的值是否被更新过,是否等于 `current`,如果等于就说明没有更新过然后将当前的值更新为 `next`,如果不等于则返回`false` 进入循环,直到更新成功为止。 + +还有其中的 `get()` 方法也很关键,返回的是当前的值,当前值用了 `volatile` 关键词修饰,保证了内存可见性。 + +```java + private volatile int value; +``` + + +## 可见性 + +现代计算机中,由于 `CPU` 直接从主内存中读取数据的效率不高,所以都会对应的 `CPU` 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。 + +![](https://i.loli.net/2019/07/19/5d313f69701ef45566.jpg) + +如上图所示。 + +`volatile` 关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。 + +使用 `volatile` 关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。 + +`synchronized`和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 `volatile` 相比开销较大。 + +## 顺序性 +以下这段代码: + +```java +int a = 100 ; //1 +int b = 200 ; //2 +int c = a + b ; //3 +``` + +正常情况下的执行顺序应该是 `1>>2>>3`。但是有时 `JVM` 为了提高整体的效率会进行指令重排导致执行的顺序可能是 `2>>1>>3`。但是 `JVM` 也不能是什么都进行重排,是在保证最终结果和代码顺序执行结果一致的情况下才可能进行重排。 + +重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。 + +Java 中可以使用 `volatile` 来保证顺序性,`synchronized 和 lock` 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。 + +除了通过 `volatile` 关键字显式的保证顺序之外, `JVM` 还通过 `happen-before` 原则来隐式的保证顺序性。 + +其中有一条就是适用于 `volatile` 关键字的,针对于 `volatile` 关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。 + +### volatile 的应用 + +#### 双重检查锁的单例模式 + +可以用 `volatile` 实现一个双重检查锁的单例模式: + +```java + public class Singleton { + private static volatile Singleton singleton; + + private Singleton() { + } + + public static Singleton getInstance() { + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } + } + return singleton; + } + + } +``` + +这里的 `volatile` 关键字主要是为了防止指令重排。 +如果不用 `volatile` ,`singleton = new Singleton();`,这段代码其实是分为三步: + +- 分配内存空间。(1) +- 初始化对象。(2) +- 将 `singleton` 对象指向分配的内存地址。(3) + +加上 `volatile` 是为了让以上的三步操作顺序执行,反之有可能第三步在第二步之前被执行就有可能导致某个线程拿到的单例对象还没有初始化,以致于使用报错。 + +#### 控制停止线程的标记 + +```java + private volatile boolean flag ; + private void run(){ + new Thread(new Runnable() { + @Override + public void run() { + while (flag) { + doSomeThing(); + } + } + }); + } + + private void stop(){ + flag = false ; + } +``` + +这里如果没有用 volatile 来修饰 flag ,就有可能其中一个线程调用了 `stop()`方法修改了 flag 的值并不会立即刷新到主内存中,导致这个循环并不会立即停止。 + +这里主要利用的是 `volatile` 的内存可见性。 + +总结一下: +- `volatile` 关键字只能保证可见性,顺序性,**不能保证原子性**。 + + + diff --git a/docs/thread/thread-communication.md b/docs/thread/thread-communication.md new file mode 100644 index 00000000..fd3e7090 --- /dev/null +++ b/docs/thread/thread-communication.md @@ -0,0 +1,588 @@ +# 深入理解线程通信 + +## 前言 + +开发中不免会遇到需要所有子线程执行完毕通知主线程处理某些逻辑的场景。 + +或者是线程 A 在执行到某个条件通知线程 B 执行某个操作。 + +可以通过以下几种方式实现: + + +## 等待通知机制 +> 等待通知模式是 Java 中比较经典的线程通信方式。 + +两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯。 + +如两个线程交替打印奇偶数: + +```java +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("A"); + + + Thread t2 = new Thread(new JiNum(twoThread)); + t2.setName("B"); + + 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(); + } + } + } + } + } + } +} +``` + +输出结果: + +``` +t2+-+奇数93 +t1+-+偶数94 +t2+-+奇数95 +t1+-+偶数96 +t2+-+奇数97 +t1+-+偶数98 +t2+-+奇数99 +t1+-+偶数100 +``` + +这里的线程 A 和线程 B 都对同一个对象 `TwoThreadWaitNotify.class` 获取锁,A 线程调用了同步对象的 wait() 方法释放了锁并进入 `WAITING` 状态。 + +B 线程调用了 notify() 方法,这样 A 线程收到通知之后就可以从 wait() 方法中返回。 + +这里利用了 `TwoThreadWaitNotify.class` 对象完成了通信。 + +有一些需要注意: + +- wait() 、notify()、notifyAll() 调用的前提都是获得了对象的锁(也可称为对象监视器)。 +- 调用 wait() 方法后线程会释放锁,进入 `WAITING` 状态,该线程也会被移动到**等待队列**中。 +- 调用 notify() 方法会将**等待队列**中的线程移动到**同步队列**中,线程状态也会更新为 `BLOCKED` +- 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。 + +等待通知有着一个经典范式: + +线程 A 作为消费者: + +1. 获取对象的锁。 +2. 进入 while(判断条件),并调用 wait() 方法。 +3. 当条件满足跳出循环执行具体处理逻辑。 + +线程 B 作为生产者: + +1. 获取对象锁。 +2. 更改与线程 A 共用的判断条件。 +3. 调用 notify() 方法。 + +伪代码如下: + +``` +//Thread A + +synchronized(Object){ + while(条件){ + Object.wait(); + } + //do something +} + +//Thread B +synchronized(Object){ + 条件=false;//改变条件 + Object.notify(); +} + +``` + + +## join() 方法 + +```java + private static void join() throws InterruptedException { + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + LOGGER.info("running"); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }) ; + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + LOGGER.info("running2"); + try { + Thread.sleep(4000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }) ; + + t1.start(); + t2.start(); + + //等待线程1终止 + t1.join(); + + //等待线程2终止 + t2.join(); + + LOGGER.info("main over"); + } +``` + +输出结果: + +``` +2018-03-16 20:21:30.967 [Thread-1] INFO c.c.actual.ThreadCommunication - running2 +2018-03-16 20:21:30.967 [Thread-0] INFO c.c.actual.ThreadCommunication - running +2018-03-16 20:21:34.972 [main] INFO c.c.actual.ThreadCommunication - main over + +``` + +在 `t1.join()` 时会一直阻塞到 t1 执行完毕,所以最终主线程会等待 t1 和 t2 线程执行完毕。 + +其实从源码可以看出,join() 也是利用的等待通知机制: + +核心逻辑: + +```java + while (isAlive()) { + wait(0); + } +``` + +在 join 线程完成后会调用 notifyAll() 方法,是在 JVM 实现中调用,所以这里看不出来。 + +## volatile 共享内存 + +因为 Java 是采用共享内存的方式进行线程通信的,所以可以采用以下方式用主线程关闭 A 线程: + +```java +public class Volatile implements Runnable{ + + private static volatile boolean flag = true ; + + @Override + public void run() { + while (flag){ + System.out.println(Thread.currentThread().getName() + "正在运行。。。"); + } + System.out.println(Thread.currentThread().getName() +"执行完毕"); + } + + public static void main(String[] args) throws InterruptedException { + Volatile aVolatile = new Volatile(); + new Thread(aVolatile,"thread A").start(); + + + System.out.println("main 线程正在运行") ; + + TimeUnit.MILLISECONDS.sleep(100) ; + + aVolatile.stopThread(); + + } + + private void stopThread(){ + flag = false ; + } +} +``` + +输出结果: +``` +thread A正在运行。。。 +thread A正在运行。。。 +thread A正在运行。。。 +thread A正在运行。。。 +thread A执行完毕 +``` + +这里的 flag 存放于主内存中,所以主线程和线程 A 都可以看到。 + +flag 采用 volatile 修饰主要是为了内存可见性,更多内容可以查看[这里](http://crossoverjie.top/2018/03/09/volatile/)。 + + +## CountDownLatch 并发工具 + +CountDownLatch 可以实现 join 相同的功能,但是更加的灵活。 + +```java + private static void countDownLatch() throws Exception{ + int thread = 3 ; + long start = System.currentTimeMillis(); + final CountDownLatch countDown = new CountDownLatch(thread); + for (int i= 0 ;i queue = new LinkedBlockingQueue(10) ; + ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1, TimeUnit.MILLISECONDS,queue) ; + poolExecutor.execute(new Runnable() { + @Override + public void run() { + LOGGER.info("running"); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + poolExecutor.execute(new Runnable() { + @Override + public void run() { + LOGGER.info("running2"); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + + poolExecutor.shutdown(); + while (!poolExecutor.awaitTermination(1,TimeUnit.SECONDS)){ + LOGGER.info("线程还在执行。。。"); + } + LOGGER.info("main over"); + } +``` + +输出结果: + +``` +2018年03月16日 20:18:01.273 [pool-1-thread-2] INFO c.c.actual.ThreadCommunication - running2 +2018年03月16日 20:18:01.273 [pool-1-thread-1] INFO c.c.actual.ThreadCommunication - running +2018年03月16日 20:18:02.273 [main] INFO c.c.actual.ThreadCommunication - 线程还在执行。。。 +2018年03月16日 20:18:03.278 [main] INFO c.c.actual.ThreadCommunication - 线程还在执行。。。 +2018年03月16日 20:18:04.278 [main] INFO c.c.actual.ThreadCommunication - main over +``` + +使用这个 `awaitTermination()` 方法的前提需要关闭线程池,如调用了 `shutdown()` 方法。 + +调用了 `shutdown()` 之后线程池会停止接受新任务,并且会平滑的关闭线程池中现有的任务。 + + +## 管道通信 + +```java + public static void piped() throws IOException { + //面向于字符 PipedInputStream 面向于字节 + PipedWriter writer = new PipedWriter(); + PipedReader reader = new PipedReader(); + + //输入输出流建立连接 + writer.connect(reader); + + + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + LOGGER.info("running"); + try { + for (int i = 0; i < 10; i++) { + + writer.write(i+""); + Thread.sleep(10); + } + } catch (Exception e) { + + } finally { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + }); + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + LOGGER.info("running2"); + int msg = 0; + try { + while ((msg = reader.read()) != -1) { + LOGGER.info("msg={}", (char) msg); + } + + } catch (Exception e) { + + } + } + }); + t1.start(); + t2.start(); + } +``` + +输出结果: + +``` +2018-03-16 19:56:43.014 [Thread-0] INFO c.c.actual.ThreadCommunication - running +2018-03-16 19:56:43.014 [Thread-1] INFO c.c.actual.ThreadCommunication - running2 +2018-03-16 19:56:43.130 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=0 +2018-03-16 19:56:43.132 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=1 +2018-03-16 19:56:43.132 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=2 +2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=3 +2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=4 +2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=5 +2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=6 +2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=7 +2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=8 +2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=9 +``` + +Java 虽说是基于内存通信的,但也可以使用管道通信。 + +需要注意的是,输入流和输出流需要首先建立连接。这样线程 B 就可以收到线程 A 发出的消息了。 + + +实际开发中可以灵活根据需求选择最适合的线程通信方式。 diff --git a/docs/thread/thread-gone.md b/docs/thread/thread-gone.md new file mode 100755 index 00000000..5e4df2ff --- /dev/null +++ b/docs/thread/thread-gone.md @@ -0,0 +1,189 @@ + +# 一个线程罢工的诡异事件 + + +![](https://i.loli.net/2019/07/19/5d313f4a3a31f18582.jpg) + +# 背景 + +事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。 + +虽然是前人写的代码,但作为 `Bug maker&killer` 只能咬着牙上了。 + + + +因为之前没有接触过出问题这块的逻辑,所以简单理了下如图: + +![](https://i.loli.net/2019/07/19/5d313f4c9d69456679.jpg) + +1. 有一个生产线程一直源源不断的往队列写数据。 +2. 消费线程也一直不停的取出数据后写入后续的业务线程池。 +3. 业务线程池里的线程会对每个任务进行入库操作。 + +整个过程还是比较清晰的,就是一个典型的生产者消费者模型。 + +# 尝试定位 + +接下来便是尝试定位这个问题,首先例行检查了以下几项: +- 是否内存有内存溢出? +- 应用 GC 是否有异常? + +通过日志以及监控发现以上两项都是正常的。 + +紧接着便 dump 了线程快照查看业务线程池中的线程都在干啥。 + +![](https://i.loli.net/2019/07/19/5d313f4f2a5fc61091.jpg) + +结果发现所有业务线程池都处于 `waiting` 状态,队列也是空的。 + + +同时生产者使用的队列却已经满了,没有任何消费迹象。 + +结合上面的流程图不难发现应该是消费队列的 `Consumer` 出问题了,导致上游的队列不能消费,下有的业务线程池没事可做。 + +## review 代码 + +于是查看了消费代码的业务逻辑,同时也发现消费线程是一个**单线程**。 + +![](https://i.loli.net/2019/07/19/5d313f5162ec253903.jpg) + +结合之前的线程快照,我发现这个消费线程也是处于 waiting 状态,和后面的业务线程池一模一样。 + +他做的事情基本上就是对消息解析,之后丢到后面的业务线程池中,没有发现什么特别的地方。 + +> 但是由于里面的分支特别多(switch case),看着有点头疼;所以我与写这个业务代码的同学沟通后他告诉我确实也只是入口处解析了一下数据,后续所有的业务逻辑都是丢到线程池中处理的,于是我便带着这个前提去排查了(埋下了伏笔)。 + +因为这里消费的队列其实是一个 `disruptor` 队列;它和我们常用的 `BlockQueue` 不太一样,不是由开发者自定义一个消费逻辑进行处理的;而是在初始化队列时直接丢一个线程池进去,它会在内部使用这个线程池进行消费,同时回调一个方法,在这个方法里我们写自己的消费逻辑。 + + +所以对于开发者而言,这个消费逻辑其实是一个黑盒。 + +于是在我反复 `review` 了消费代码中的数据解析逻辑发现不太可能出现问题后,便开始疯狂怀疑是不是 `disruptor` 自身的问题导致这个消费线程罢工了。 + +再翻了一阵 `disruptor` 的源码后依旧没发现什么问题后我咨询对 `disruptor` 较熟的@咖啡拿铁,在他的帮助下在本地模拟出来和生产一样的情况。 + +# 本地模拟 + +![](https://i.loli.net/2019/07/19/5d313f52c634323563.jpg) +![](https://i.loli.net/2019/07/19/5d313f5420dc952988.jpg) + +本地也是创建了一个单线程的线程池,分别执行了两个任务。 + +- 第一个任务没啥好说的,就是简单的打印。 +- 第二个任务会对一个数进行累加,加到 10 之后就抛出一个未捕获的异常。 + +接着我们来运行一下。 + +![](https://i.loli.net/2019/07/19/5d313f5a2c02c31627.jpg) +![](https://i.loli.net/2019/07/19/5d313f5d8ffa965140.jpg) + +发现当任务中抛出一个没有捕获的异常时,线程池中的线程就会处于 `waiting` 状态,同时所有的堆栈都和生产相符。 + +> 细心的朋友会发现正常运行的线程名称和异常后处于 waiting 状态的线程名称是不一样的,这个后续分析。 + +## 解决问题 + +![](https://i.loli.net/2019/07/19/5d313f5ec672d88094.jpg) + +当加入异常捕获后又如何呢? + +![](https://i.loli.net/2019/07/19/5d313f6231de819950.jpg) + +程序肯定会正常运行。 + +> 同时会发现所有的任务都是由一个线程完成的。 + +虽说就是加了一行代码,但我们还是要搞清楚这里面的门门道道。 + +# 源码分析 + +于是只有直接 `debug` 线程池的源码最快了; + +--- + +![](https://i.loli.net/2019/07/19/5d313f6973b8619302.jpg) + +![](https://i.loli.net/2019/07/19/5d313f6f57e9d51378.jpg) + +通过刚才的异常堆栈我们进入到 `ThreadPoolExecutor.java:1142` 处。 + +- 发现线程池已经帮我们做了异常捕获,但依然会往上抛。 +- 在 `finally` 块中会执行 `processWorkerExit(w, completedAbruptly)` 方法。 + + +![](https://i.loli.net/2019/07/19/5d313f759363b25554.jpg) + +看过之前[《如何优雅的使用和理解线程池》](https://crossoverjie.top/2018/07/29/java-senior/ThreadPool/)的朋友应该还会有印象。 + +线程池中的任务都会被包装为一个内部 `Worker` 对象执行。 + +`processWorkerExit` 可以简单的理解为是把当前运行的线程销毁(`workers.remove(w)`)、同时新增(`addWorker()`)一个 `Worker` 对象接着处理; + +> 就像是哪个零件坏掉后重新换了一个新的接着工作,但是旧零件负责的任务就没有了。 + + +接下来看看 `addWorker()` 做了什么事情: + +![](https://i.loli.net/2019/07/19/5d313f77c421b49964.jpg) + +只看这次比较关心的部分;添加成功后会直接执行他的 `start()` 的方法。 + + +![](https://i.loli.net/2019/07/19/5d313f7994c8b72107.jpg) + +由于 `Worker` 实现了 `Runnable` 接口,所以本质上就是调用了 `runWorker()` 方法。 + +--- + + + +在 `runWorker()` 其实就是上文 `ThreadPoolExecutor` 抛出异常时的那个方法。 + +![](https://i.loli.net/2019/07/19/5d313f7e6beff17180.jpg) +![](https://i.loli.net/2019/07/19/5d313f843771a14962.jpg) + +它会从队列里一直不停的获取待执行的任务,也就是 `getTask()`;在 `getTask` 也能看出它会一直从内置的队列取出任务。 + +而一旦队列是空的,它就会 `waiting` 在 `workQueue.take()`,也就是我们从堆栈中发现的 1067 行代码。 + + + +## 线程名字的变化 + +![](https://i.loli.net/2019/07/19/5d313f8734b2d13880.jpg) +![](https://i.loli.net/2019/07/19/5d313f8a0386d77948.jpg) +![](https://i.loli.net/2019/07/19/5d313f8ced57345869.jpg) + +上文还提到了异常后的线程名称发生了改变,其实在 `addWorker()` 方法中可以看到 `new Worker()`时就会重新命名线程的名称,默认就是把后缀的计数+1。 + +这样一切都能解释得通了,真相只有一个: + + +> 在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 `Worker`; +> 它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。 + +# 总结 + +所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是: + +> 既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。 + +这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。 + +结果发现在上文提到的众多 `switch case` 中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了🤬!! + +这事也给我个教训,还是得眼见为实啊。 + +虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的: + +1. 消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。 +2. 开发规范,防御式编程大家需要养成习惯。 +3. 未知的技术栈需要谨慎,比如 `disruptor`,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。 + +实例代码: + +[https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java) + + +**你的点赞与分享是对我最大的支持** + diff --git a/docs/thread/thread-gone2.md b/docs/thread/thread-gone2.md new file mode 100755 index 00000000..291fe90b --- /dev/null +++ b/docs/thread/thread-gone2.md @@ -0,0 +1,133 @@ +# 线程池中你不容错过的一些细节 + +![](https://i.loli.net/2019/07/19/5d313f2ad38b490450.jpg) + +# 背景 + +上周分享了一篇[《一个线程罢工的诡异事件》](docs/jvm/thread-gone.md),最近也在公司内部分享了这个案例。 + +无独有偶,在内部分享的时候也有小伙伴问了之前分享时所提出的一类问题: + + + +![](https://i.loli.net/2019/07/19/5d313f2da903922875.jpg) + +![](https://i.loli.net/2019/07/19/5d313f2fb8ab281501.jpg) + +![](https://i.loli.net/2019/07/19/5d313f31ae8dd83926.jpg) + +![](https://i.loli.net/2019/07/19/5d313f349d9f989541.jpg) + +这其实是一类共性问题,我认为主要还是两个原因: + +- 我自己确实也没讲清楚,之前画的那张图还需要再完善,有些误导。 +- 第二还是大家对线程池的理解不够深刻,比如今天要探讨的内容。 + + +# 线程池的工作原理 + +首先还是来复习下线程池的基本原理。 + +我认为线程池它就是一个**调度任务**的工具。 + +众所周知在初始化线程池会给定线程池的大小,假设现在我们有 1000 个线程任务需要运行,而线程池的大小为 10~20,在真正运行任务的过程中他肯定不会创建这1000个线程同时运行,而是充分利用线程池里这 10~20 个线程来调度这1000个任务。 + +而这里的 10~20 个线程最后会由线程池封装为 `ThreadPoolExecutor.Worker` 对象,而这个 `Worker` 是实现了 Runnable 接口的,所以他自己本身就是一个线程。 + +# 深入分析 + +![](https://i.loli.net/2019/07/19/5d313f3a4276e41232.jpg) + +这里我们来做一个模拟,创建了一个核心线程、最大线程数、阻塞队列都为2的线程池。 + +这里假设线程池已经完成了预热,也就是线程池内部已经创建好了两个线程 `Worker`。 + +当我们往一个线程池丢一个任务会发生什么事呢? + +![](https://i.loli.net/2019/07/19/5d313f3cd67dd15513.jpg) + +- 第一步是生产者,也就是任务提供者他执行了一个 execute() 方法,本质上就是往这个内部队列里放了一个任务。 +- 之前已经创建好了的 Worker 线程会执行一个 `while` 循环 ---> 不停的从这个`内部队列`里获取任务。(这一步是竞争的关系,都会抢着从队列里获取任务,由这个队列内部实现了线程安全。) +- 获取得到一个任务后,其实也就是拿到了一个 `Runnable` 对象(也就是 `execute(Runnable task)` 这里所提交的任务),接着执行这个 `Runnable` 的 **run() 方法,而不是 start()**,这点需要注意后文分析原因。 + +结合源码来看: + +![](https://i.loli.net/2019/07/19/5d313f3e7871333125.jpg) + +从图中其实就对应了刚才提到的二三两步: + +- `while` 循环,从 `getTask()` 方法中一直不停的获取任务。 +- 拿到任务后,执行它的 run() 方法。 + +这样一个线程就调度完毕,然后再次进入循环从队列里取任务并不断的进行调度。 + +# 再次解释之前的问题 + +接下来回顾一下我们上一篇文章所提到的,导致一个线程没有运行的根本原因是: + +> 在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 `Worker`; +> 它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,**根本没有生产者往里边丢任务**,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。 + +结合之前的那张图来看: + +![](https://i.loli.net/2019/07/19/5d313f41461af62841.jpg) + +这里大家问的最多的一个点是,为什么会没有是`根本没有生产者往里边丢任务`,图中不是明明画的有一个 `product` 嘛? + +这里确实是有些不太清楚,再次强调一次: + +**图中的 product 是往内部队列里写消息的生产者,并不是往这个 Consumer 所在的线程池中写任务的生产者。** + +因为即便 `Consumer` 是一个单线程的线程池,它依然具有一个常规线程池所具备的所有条件: + +- Worker 调度线程,也就是线程池运行的线程;虽然只有一个。 +- 内部的阻塞队列;虽然长度只有1。 + +再次结合图来看: + +![](https://i.loli.net/2019/07/19/5d313f43d9b0242820.jpg) + +所以之前提到的【没有生产者往里边丢任务】是指右图放大后的那一块,也就是内部队列并没有其他线程往里边丢任务执行 `execute()` 方法。 + +而一旦发生未捕获的异常后,`Worker1` 被回收,顺带的它所调度的线程 `task1`(这个task1 也就是在执行一个 while 循环消费左图中的那个队列) 也会被回收掉。 + +新创建的 `Worker2` 会取代 `Worker1` 继续执行 `while` 循环从内部队列里获取任务,但此时这个队列就一直会是空的,所以也就是处于 `Waiting` 状态。 + + +> 我觉得这波解释应该还是讲清楚了,欢迎还没搞明白的朋友留言讨论。 + +# 为什是 run() 而不是 start() + +问题搞清楚后来想想为什么线程池在调度的时候执行的是 `Runnable` 的 `run()` 方法,而不是 `start()` 方法呢? + +我相信大部分没有看过源码的同学心中第一个印象就应该是执行的 `start()` 方法; + +因为不管是学校老师,还是网上大牛讲的都是只有执行了` start()` 方法后操作系统才会给我们创建一个独立的线程来运行,而 `run()` 方法只是一个普通的方法调用。 + +而在线程池这个场景中却恰好就是要利用它**只是一个普通方法调用**。 + +回到我在文初中所提到的:我认为线程池它就是一个**调度任务**的工具。 + +假设这里是调用的 `Runnable` 的 `start` 方法,那会发生什么事情。 + +如果我们往一个核心、最大线程数为 2 的线程池里丢了 1000 个任务,**那么它会额外的创建 1000 个线程,同时每个任务都是异步执行的,一下子就执行完毕了**。 + +从而没法做到由这两个 `Worker` 线程来调度这 1000 个任务,而只有当做一个同步阻塞的 `run()` 方法调用时才能满足这个要求。 + +> 这事也让我发现一个奇特的现象:就是网上几乎没人讲过为什么在线程池里是 run 而不是 start,不知道是大家都觉得这是基操还是没人仔细考虑过。 + +# 总结 + +针对之前线上事故的总结上次已经写得差不多了,感兴趣的可以翻回去看看。 + +这次呢可能更多是我自己的总结,比如写一篇技术博客时如果大部分人对某一个知识点讨论的比较热烈时,那一定是作者要么讲错了,要么没讲清楚。 + +这点确实是要把自己作为一个读者的角度来看,不然很容易出现之前的一些误解。 + +在这之外呢,我觉得对于线程池把这两篇都看完同时也理解后对于大家理解线程池,利用线程池完成工作也是有很大好处的。 + +如果有在面试中加分的记得回来点赞、分享啊。 + + +**你的点赞与分享是对我最大的支持** + diff --git a/pom.xml b/pom.xml index 059dc1b7..7a2e2885 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.crossoverjie.interview - interview + JCSprout 1.0.0-SNAPSHOT jar @@ -23,7 +23,10 @@ UTF-8 2.5.3 - 1.8 + 11 + 11 + ${java.version} + ${java.version} 4.3.10.RELEASE UTF-8 1.5.6.RELEASE @@ -63,7 +66,6 @@ junit junit 4.8.2 - test @@ -86,14 +88,69 @@ com.google.guava guava - 19.0 + 22.0 com.alibaba fastjson - 1.1.40 + 1.2.31 + + + + com.netflix.hystrix + hystrix-core + 1.5.2 + + + + + + com.lmax + disruptor + 3.3.7 + + + + org.apache.kafka + kafka_2.11 + 0.10.0.1 + + + slf4j-log4j12 + org.slf4j + + + + org.apache.zookeeper + zookeeper + 3.4.14 + + + slf4j-log4j12 + org.slf4j + + + + + + org.openjdk.jmh + jmh-core + 1.9.3 + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.9.3 + + + javax.annotation + javax.annotation-api + 1.3.2 + + @@ -104,8 +161,8 @@ maven-compiler-plugin 2.3.2 - 1.8 - 1.8 + ${java.version} + ${java.version} ${project.build.sourceEncoding} diff --git a/src/main/java/com/crossoverjie/actual/FourThreadPrinter.java b/src/main/java/com/crossoverjie/actual/FourThreadPrinter.java new file mode 100644 index 00000000..703ea291 --- /dev/null +++ b/src/main/java/com/crossoverjie/actual/FourThreadPrinter.java @@ -0,0 +1,87 @@ +package com.crossoverjie.actual; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年11月15日 18:06 + * @since JDK 1.8 + */ +public class FourThreadPrinter extends Thread { + + private final static Logger LOGGER = LoggerFactory.getLogger(FourThreadPrinter.class); + + private static int count = 100; + + private static Lock lock = new ReentrantLock(); + + private static volatile int index = 0; + + private static volatile boolean flag = false; + + /** + * t1=1 + * t2=2 + * t3=3 + * t4=4 + */ + private static volatile int type = 1; + + /** + * 线程当前状态 + */ + private int currentType; + + public FourThreadPrinter(String name, int currentType) { + super(name); + this.currentType = currentType; + } + + + @Override + public void run() { + while (index < count) { + if (currentType == type) { + try { + lock.lock(); + index++; + LOGGER.info("print: " + index + " flag=" + flag); + updateCondition(); + } finally { + lock.unlock(); + } + } + } + } + + private void updateCondition() { + + if (Thread.currentThread().getName().equals("t1")) { + type = 2; + } else if (Thread.currentThread().getName().equals("t2")) { + type = 3; + } else if (Thread.currentThread().getName().equals("t3")) { + type = 4; + } else if (Thread.currentThread().getName().equals("t4")) { + type = 1; + } + + } + + public static void main(String[] args) { + Thread t1 = new FourThreadPrinter("t1", 1); + Thread t2 = new FourThreadPrinter("t2", 2); + Thread t3 = new FourThreadPrinter("t3", 3); + Thread t4 = new FourThreadPrinter("t4", 4); + t1.start(); + t2.start(); + t3.start(); + t4.start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/actual/LRUMap.java b/src/main/java/com/crossoverjie/actual/LRUMap.java index c0a45960..28946fa5 100644 --- a/src/main/java/com/crossoverjie/actual/LRUMap.java +++ b/src/main/java/com/crossoverjie/actual/LRUMap.java @@ -88,6 +88,7 @@ private void moveToHead(Node node){ if (node.tail != null && node.next != null){ //它的上一节点指向它的下一节点 也就删除当前节点 node.tail.next = node.next ; + node.next.tail = node.tail; nodeCount -- ; } diff --git a/src/main/java/com/crossoverjie/actual/NotifyAll.java b/src/main/java/com/crossoverjie/actual/NotifyAll.java new file mode 100644 index 00000000..ff3a255b --- /dev/null +++ b/src/main/java/com/crossoverjie/actual/NotifyAll.java @@ -0,0 +1,33 @@ +package com.crossoverjie.actual; + +/** + * Function: + * + * @author crossoverJie + * Date: 2021年7月1日 23:08 + * @since JDK 11 + */ +public class NotifyAll { + + + public static void main(String[] args) throws InterruptedException { + for (int i = 0; i < 10; i++) { + new Thread(() -> { + try { + synchronized (NotifyAll.class){ + NotifyAll.class.wait(); + } + System.out.println(Thread.currentThread().getName() + "done...."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + } + Thread.sleep(3000); + synchronized (NotifyAll.class){ + NotifyAll.class.notifyAll(); + } + + + } +} diff --git a/src/main/java/com/crossoverjie/actual/Search.java b/src/main/java/com/crossoverjie/actual/Search.java new file mode 100644 index 00000000..f1263b29 --- /dev/null +++ b/src/main/java/com/crossoverjie/actual/Search.java @@ -0,0 +1,186 @@ +package com.crossoverjie.actual; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月13日 20:00 + * @since JDK 1.8 + */ + +import org.junit.Assert; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Function: + * + 一个"."代表一个任意字母。 + + 注意事项:可以假设所有的单词只包含小写字母"a-z" + + 样例: + addWord("bad"); + addWord("dad"); + addWord("mad"); + search("pad"); // return false; + search("bad"); // return true; + search(".ad"); // return true; + search("b.."); // return true; + + + 如果有并发的情况下,addword() 怎么处理? + * + * @author crossoverJie + * @since JDK 1.8 + */ +public class Search { + + private static Map ALL_MAP = new ConcurrentHashMap(50000) ; + + + /** + * 换成 ascii码 更省事 + */ + private static final char[] dictionary = {'a','b','c','d','m','p'} ; + + public static void main(String[] args) throws InterruptedException { + + + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < 10000; i++) { + addWord(i + "ad"); + } + } + }); + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < 10000; i++) { + addWord(i + "bd"); + } + } + }); + Thread t3 = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < 10000; i++) { + addWord(i + "cd"); + } + } + }); + Thread t4 = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < 10000; i++) { + addWord(i + "dd"); + } + } + }); + Thread t5 = new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < 10000; i++) { + addWord(i + "ed"); + } + } + }); + + t1.start(); + t2.start(); + t3.start(); + t4.start(); + t5.start(); + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + System.out.println(ALL_MAP.size()); + Assert.assertEquals(50000,ALL_MAP.size()); + + + addWord("bad"); + addWord("dad"); + addWord("mad"); + boolean pad = search("pad"); + System.out.println(pad); + Assert.assertFalse(pad); + + boolean bad = search("bad"); + System.out.println(bad); + Assert.assertTrue(bad); + + + boolean ad = search(".ad"); + System.out.println(ad); + Assert.assertTrue(ad); + + + boolean bsearch = search("b.."); + System.out.println(bsearch); + Assert.assertTrue(bsearch); + + boolean asearch = search(".a."); + System.out.println(asearch); + + + boolean search = search(".af"); + System.out.println(search); + + + boolean search1 = search(null); + System.out.println(search1); + + } + + public static boolean search(String keyWord){ + boolean result = false ; + if (null == keyWord || keyWord.trim().equals("")){ + return result ; + } + + //做一次完整匹配 + String whole = ALL_MAP.get(keyWord) ; + if (whole != null){ + return true ; + } + + char[] wordChars = keyWord.toCharArray() ; + + for (int i = 0; i < wordChars.length; i++) { + char wordChar = wordChars[i] ; + + if (46 != (int)wordChar){ + continue ; + } + + for (char dic : dictionary) { + wordChars[i] = dic ; + boolean search = search(String.valueOf(wordChars)); + + if (search){ + return search ; + } + + String value = ALL_MAP.get(String.valueOf(wordChars)); + if (value != null){ + return true ; + } + } + + } + + + return result ; + } + + + public static void addWord(String word){ + ALL_MAP.put(word,word) ; + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/actual/ThreadCommunication.java b/src/main/java/com/crossoverjie/actual/ThreadCommunication.java index ecbda200..7b5cc89c 100644 --- a/src/main/java/com/crossoverjie/actual/ThreadCommunication.java +++ b/src/main/java/com/crossoverjie/actual/ThreadCommunication.java @@ -4,7 +4,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.PipedInputStream; import java.io.PipedReader; import java.io.PipedWriter; import java.util.concurrent.*; @@ -22,9 +21,9 @@ public class ThreadCommunication { public static void main(String[] args) throws Exception { //join(); //executorService(); - //countDownLatch(); + countDownLatch(); //piped(); - cyclicBarrier(); + //cyclicBarrier(); } /** @@ -82,22 +81,19 @@ public void run() { } private static void countDownLatch() throws Exception { - int thread = 3; + int thread = 2; long start = System.currentTimeMillis(); final CountDownLatch countDown = new CountDownLatch(thread); for (int i = 0; i < thread; i++) { - new Thread(new Runnable() { - @Override - public void run() { - LOGGER.info("thread run"); - try { - Thread.sleep(2000); - countDown.countDown(); + new Thread(() -> { + LOGGER.info("thread run"); + try { + Thread.sleep(2000); + countDown.countDown(); - LOGGER.info("thread end"); - } catch (InterruptedException e) { - e.printStackTrace(); - } + LOGGER.info("thread end"); + } catch (InterruptedException e) { + e.printStackTrace(); } }).start(); } diff --git a/src/main/java/com/crossoverjie/actual/TwoThread.java b/src/main/java/com/crossoverjie/actual/TwoThread.java index 90c83927..95bdaf5e 100644 --- a/src/main/java/com/crossoverjie/actual/TwoThread.java +++ b/src/main/java/com/crossoverjie/actual/TwoThread.java @@ -17,10 +17,10 @@ public class TwoThread { private int start = 1; /** - * 保证内存可见性 - * 其实用锁了之后也可以保证可见性 这里用不用 volatile 都一样 + * 对 flag 的写入虽然加锁保证了线程安全,但读取的时候由于 不是 volatile 所以可能会读取到旧值 + * */ - private boolean flag = false; + private volatile boolean flag = false; /** * 重入锁 @@ -54,7 +54,7 @@ public OuNum(TwoThread number) { @Override public void run() { - while (number.start <= 100) { + while (number.start <= 1000) { if (number.flag) { try { @@ -67,13 +67,6 @@ public void run() { } finally { LOCK.unlock(); } - } else { - try { - //防止线程空转 - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } } } } @@ -92,7 +85,7 @@ public JiNum(TwoThread number) { @Override public void run() { - while (number.start <= 100) { + while (number.start <= 1000) { if (!number.flag) { try { @@ -105,13 +98,6 @@ public void run() { } finally { LOCK.unlock(); } - } else { - try { - //防止线程空转 - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } } } } diff --git a/src/main/java/com/crossoverjie/actual/TwoThreadNonBlocking.java b/src/main/java/com/crossoverjie/actual/TwoThreadNonBlocking.java new file mode 100644 index 00000000..5379f8bf --- /dev/null +++ b/src/main/java/com/crossoverjie/actual/TwoThreadNonBlocking.java @@ -0,0 +1,53 @@ +package com.crossoverjie.actual; + +/** + * 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(); + } +} diff --git a/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotify.java b/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotify.java index 1c69da14..ecfcb63a 100644 --- a/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotify.java +++ b/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotify.java @@ -41,9 +41,9 @@ public OuNum(TwoThreadWaitNotify number) { @Override public void run() { - while (number.start <= 100) { + while (number.start <= 10) { synchronized (TwoThreadWaitNotify.class) { - System.out.println("偶数线程抢到锁了"); +// System.out.println("偶数线程抢到锁了"); if (number.flag) { System.out.println(Thread.currentThread().getName() + "+-+偶数" + number.start); number.start++; @@ -77,9 +77,9 @@ public JiNum(TwoThreadWaitNotify number) { @Override public void run() { - while (number.start <= 100) { + while (number.start <= 10) { synchronized (TwoThreadWaitNotify.class) { - System.out.println("奇数线程抢到锁了"); +// System.out.println("奇数线程抢到锁了"); if (!number.flag) { System.out.println(Thread.currentThread().getName() + "+-+奇数" + number.start); number.start++; diff --git a/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotifySimple.java b/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotifySimple.java new file mode 100644 index 00000000..1aae7657 --- /dev/null +++ b/src/main/java/com/crossoverjie/actual/TwoThreadWaitNotifySimple.java @@ -0,0 +1,97 @@ +package com.crossoverjie.actual; + +/** + * Function:两个线程交替执行打印 1~100 + * 等待通知机制版 + * + * @author crossoverJie + * Date: 07/03/2018 13:19 + * @since JDK 1.8 + */ +public class TwoThreadWaitNotifySimple { + + private boolean flag = false; + + public static void main(String[] args) { + TwoThreadWaitNotifySimple twoThread = new TwoThreadWaitNotifySimple(); + + 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 TwoThreadWaitNotifySimple number; + + public OuNum(TwoThreadWaitNotifySimple number) { + this.number = number; + } + + @Override + public void run() { + for (int i = 0; i < 11; i++) { + synchronized (TwoThreadWaitNotifySimple.class) { + if (number.flag) { + if (i % 2 == 0) { + System.out.println(Thread.currentThread().getName() + "+-+偶数" + i); + + number.flag = false; + TwoThreadWaitNotifySimple.class.notify(); + } + + } else { + try { + TwoThreadWaitNotifySimple.class.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + } + } + + + /** + * 奇数线程 + */ + public static class JiNum implements Runnable { + private TwoThreadWaitNotifySimple number; + + public JiNum(TwoThreadWaitNotifySimple number) { + this.number = number; + } + + @Override + public void run() { + for (int i = 0; i < 11; i++) { + synchronized (TwoThreadWaitNotifySimple.class) { + if (!number.flag) { + if (i % 2 == 1) { + System.out.println(Thread.currentThread().getName() + "+-+奇数" + i); + + number.flag = true; + TwoThreadWaitNotifySimple.class.notify(); + } + + } else { + try { + TwoThreadWaitNotifySimple.class.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + } + } +} diff --git a/src/main/java/com/crossoverjie/algorithm/ArrayKShift.java b/src/main/java/com/crossoverjie/algorithm/ArrayKShift.java new file mode 100644 index 00000000..0b4d9dce --- /dev/null +++ b/src/main/java/com/crossoverjie/algorithm/ArrayKShift.java @@ -0,0 +1,75 @@ +package com.crossoverjie.algorithm; + +import java.util.Arrays; + +/** + * 数组右移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); + }); + + } +} diff --git a/src/main/java/com/crossoverjie/algorithm/BinaryNodeTravel.java b/src/main/java/com/crossoverjie/algorithm/BinaryNodeTravel.java new file mode 100644 index 00000000..3b8ee9bd --- /dev/null +++ b/src/main/java/com/crossoverjie/algorithm/BinaryNodeTravel.java @@ -0,0 +1,121 @@ +package com.crossoverjie.algorithm; + +import java.util.LinkedList; + +/** + * Function: 层序遍历,需要将遍历的节点串联起来 + * + * @author crossoverJie + * Date: 2018年7月27日 23:37 + * @since JDK 1.8 + */ +public class BinaryNodeTravel { + + private Object data ; + private BinaryNodeTravel left ; + private BinaryNodeTravel right ; + public BinaryNodeTravel next; + + public BinaryNodeTravel() { + } + + public BinaryNodeTravel(Object data, BinaryNodeTravel left, BinaryNodeTravel right) { + this.data = data; + this.left = left; + this.right = right; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public BinaryNodeTravel getLeft() { + return left; + } + + public void setLeft(BinaryNodeTravel left) { + this.left = left; + } + + public BinaryNodeTravel getRight() { + return right; + } + + public void setRight(BinaryNodeTravel right) { + this.right = right; + } + + + public BinaryNodeTravel createNode(){ + BinaryNodeTravel nodeA = new BinaryNodeTravel("A",null,null) ; + BinaryNodeTravel nodeB = new BinaryNodeTravel("B",null,null) ; + BinaryNodeTravel nodeC = new BinaryNodeTravel("C",null,null) ; + BinaryNodeTravel nodeD = new BinaryNodeTravel("D",null,null) ; + BinaryNodeTravel nodeE = new BinaryNodeTravel("E",null,null) ; + BinaryNodeTravel nodeF = new BinaryNodeTravel("F",null,null) ; + + nodeA.setLeft(nodeB); + nodeB.setLeft(nodeD); + nodeA.setRight(nodeC); + nodeC.setLeft(nodeE); + nodeC.setRight(nodeF); + + return nodeA ; + } + + @Override + public String toString() { + return "BinaryNode{" + + "data=" + data + + ", left=" + left + + ", right=" + right + + '}'; + } + + + /** + * 二叉树的层序遍历 借助于队列来实现 借助队列的先进先出的特性 + * + * 首先将根节点入队列 然后遍历队列。 + * + * 暂时把上一个节点存起来,每次都把上一节点的 next 指向当前节点 + * + * 首先将根节点打印出来,接着判断左节点是否为空 不为空则加入队列 + * @param node + */ + public BinaryNodeTravel levelIterator(BinaryNodeTravel node){ + LinkedList queue = new LinkedList() ; + + + //暂时存放的上一节点 + BinaryNodeTravel pre = null; + + //先将根节点入队 + queue.offer(node) ; + BinaryNodeTravel current ; + while (!queue.isEmpty()){ + current = queue.poll(); + + //将上一节点指向当前节点 + if (pre == null){ + pre = current ; + }else { + pre.next = current ; + pre = current; + } + + if (current.getLeft() != null){ + queue.offer(current.getLeft()) ; + } + if (current.getRight() != null){ + queue.offer(current.getRight()) ; + } + } + + return node ; + } +} diff --git a/src/main/java/com/crossoverjie/algorithm/BloomFilters.java b/src/main/java/com/crossoverjie/algorithm/BloomFilters.java new file mode 100644 index 00000000..7d63bcc3 --- /dev/null +++ b/src/main/java/com/crossoverjie/algorithm/BloomFilters.java @@ -0,0 +1,155 @@ +package com.crossoverjie.algorithm; + +/** + * Function:布隆过滤demo + * + * @author crossoverJie + * Date: 2018年11月20日 22:32 + * @since JDK 1.8 + */ +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); + } + + + public static void main(String[] args) { + BloomFilters bloomFilter = new BloomFilters(10000); + long hashcode = bloomFilter.hashcode_1("1"); + long hashcode2 = bloomFilter.hashcode_1("2"); + System.out.println(hashcode); + System.out.println(hashcode2); + System.out.println("========="); + long hashcode3 = bloomFilter.hashcode_2("1"); + long hashcode4 = bloomFilter.hashcode_2("100"); + System.out.println(hashcode3); + System.out.println(hashcode4); + System.out.println("========="); + + long hashcode5 = bloomFilter.hashcode_3("1"); + long hashcode6 = bloomFilter.hashcode_3("100"); + System.out.println(hashcode5); + System.out.println(hashcode6); + System.out.println("========="); + + + bloomFilter.add("12345"); + bloomFilter.add("100"); + bloomFilter.add("1000"); + + + boolean check = bloomFilter.check("9000"); + System.out.println("check=" + check); + + boolean check1 = bloomFilter.check("12345"); + System.out.println("check1=" + check1); + } +} diff --git a/src/main/java/com/crossoverjie/algorithm/LinkedListMergeSort.java b/src/main/java/com/crossoverjie/algorithm/LinkedListMergeSort.java new file mode 100644 index 00000000..6d183bfb --- /dev/null +++ b/src/main/java/com/crossoverjie/algorithm/LinkedListMergeSort.java @@ -0,0 +1,141 @@ +package com.crossoverjie.algorithm; + +/** + * 链表排序, 建议使用归并排序, + * 问题描述,给定一个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); + } + + } +} diff --git a/src/main/java/com/crossoverjie/algorithm/TwoSum.java b/src/main/java/com/crossoverjie/algorithm/TwoSum.java index 08f08f0b..7382cb7b 100644 --- a/src/main/java/com/crossoverjie/algorithm/TwoSum.java +++ b/src/main/java/com/crossoverjie/algorithm/TwoSum.java @@ -19,14 +19,14 @@ public class TwoSum { * @return */ public int[] getTwo1(int[] nums,int target){ - int[] result = new int[2] ; + int[] result = null; for (int i= 0 ;i=0 ;j--){ int b = nums[j] ; - if ((a+b) == target){ + if (i != j && (a + b) == target) { result = new int[]{i,j} ; } } diff --git a/src/main/java/com/crossoverjie/basic/CollectionsTest.java b/src/main/java/com/crossoverjie/basic/CollectionsTest.java new file mode 100644 index 00000000..6c37fa26 --- /dev/null +++ b/src/main/java/com/crossoverjie/basic/CollectionsTest.java @@ -0,0 +1,74 @@ +package com.crossoverjie.basic; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年06月27日 00:11 + * @since JDK 1.8 + */ +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +public class CollectionsTest { + + private static final int TEN_MILLION = 10000000; + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void arrayList() { + + List array = new ArrayList(); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void arrayListSize() { + List array = new ArrayList(TEN_MILLION); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void linkedList() { + List array = new LinkedList(); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(CollectionsTest.class.getSimpleName()) + .forks(1) + .build(); + + + new Runner(opt).run(); + } +} diff --git a/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java b/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java new file mode 100644 index 00000000..a8dbe3d2 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java @@ -0,0 +1,129 @@ +package com.crossoverjie.concurrent; + +/** + * Function: 数组实现的线程安全阻塞队列 + * + * @author crossoverJie + * Date: 2019-04-04 15:02 + * @since JDK 1.8 + */ +public final class ArrayQueue { + + /** + * 队列数量 + */ + private int count = 0; + + /** + * 最终的数据存储 + */ + private Object[] items; + + /** + * 队列满时的阻塞锁 + */ + private Object full = new Object(); + + /** + * 队列空时的阻塞锁 + */ + private Object empty = new Object(); + + + /** + * 写入数据时的下标 + */ + private int putIndex; + + /** + * 获取数据时的下标 + */ + private int getIndex; + + public ArrayQueue(int size) { + items = new Object[size]; + } + + /** + * 从队列尾写入数据 + * @param t + */ + public void put(T t) { + + synchronized (full) { + while (count == items.length) { + try { + full.wait(); + } catch (InterruptedException e) { + break; + } + } + } + + synchronized (empty) { + //写入 + items[putIndex] = t; + count++; + + putIndex++; + if (putIndex == items.length) { + //超过数组长度后需要从头开始 + putIndex = 0; + } + + empty.notify(); + } + + } + + /** + * 从队列头获取数据 + * @return + */ + public T get() { + + synchronized (empty) { + while (count == 0) { + try { + empty.wait(); + } catch (InterruptedException e) { + return null; + } + } + } + + synchronized (full) { + Object result = items[getIndex]; + items[getIndex] = null; + count--; + + getIndex++; + if (getIndex == items.length) { + getIndex = 0; + } + + full.notify(); + + return (T) result; + } + } + + /** + * 获取队列大小 + * @return + */ + public synchronized int size() { + return count; + } + + + /** + * 判断队列是否为空 + * @return + */ + public boolean isEmpty() { + return size() == 0; + } + + +} diff --git a/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java b/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java new file mode 100644 index 00000000..a9713a93 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java @@ -0,0 +1,403 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import com.crossoverjie.concurrent.future.Callable; +import com.crossoverjie.concurrent.future.Future; +import com.crossoverjie.concurrent.future.FutureTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Function:线程池 + * + * @author crossoverJie + * Date: 2019年05月14日 10:51 + * @since JDK 1.8 + */ +public class CustomThreadPool { + + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPool.class); + private final ReentrantLock lock = new ReentrantLock(); + + /** + * 最小线程数,也叫核心线程数 + */ + private volatile int miniSize; + + /** + * 最大线程数 + */ + private volatile int maxSize; + + /** + * 线程需要被回收的时间 + */ + private long keepAliveTime; + private TimeUnit unit; + + /** + * 存放线程的阻塞队列 + */ + private BlockingQueue workQueue; + + /** + * 存放线程池 + */ + private volatile Set workers; + + /** + * 是否关闭线程池标志 + */ + private AtomicBoolean isShutDown = new AtomicBoolean(false); + + /** + * 提交到线程池中的任务总数 + */ + private AtomicInteger totalTask = new AtomicInteger(); + + /** + * 线程池任务全部执行完毕后的通知组件 + */ + private Object shutDownNotify = new Object(); + + private Notify notify; + + /** + * @param miniSize 最小线程数 + * @param maxSize 最大线程数 + * @param keepAliveTime 线程保活时间 + * @param unit + * @param workQueue 阻塞队列 + * @param notify 通知接口 + */ + public CustomThreadPool(int miniSize, int maxSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, Notify notify) { + this.miniSize = miniSize; + this.maxSize = maxSize; + this.keepAliveTime = keepAliveTime; + this.unit = unit; + this.workQueue = workQueue; + this.notify = notify; + + workers = new ConcurrentHashSet(); + } + + + /** + * 有返回值 + * + * @param callable + * @param + * @return + */ + public Future submit(Callable callable) { + FutureTask future = new FutureTask(callable); + execute(future); + return future; + } + + + /** + * 执行任务 + * + * @param runnable 需要执行的任务 + */ + public void execute(Runnable runnable) { + if (runnable == null) { + throw new NullPointerException("runnable nullPointerException"); + } + if (isShutDown.get()) { + LOGGER.info("线程池已经关闭,不能再提交任务!"); + return; + } + + //提交的线程 计数 + totalTask.incrementAndGet(); + + //小于最小线程数时新建线程 + if (workers.size() < miniSize) { + addWorker(runnable); + return; + } + + + boolean offer = workQueue.offer(runnable); + //写入队列失败 + if (!offer) { + + //创建新的线程执行 + if (workers.size() < maxSize) { + addWorker(runnable); + return; + } else { + LOGGER.error("超过最大线程数"); + try { + //会阻塞 + workQueue.put(runnable); + } catch (InterruptedException e) { + + } + } + + } + + + } + + /** + * 添加任务,需要加锁 + * + * @param runnable 任务 + */ + private void addWorker(Runnable runnable) { + Worker worker = new Worker(runnable, true); + worker.startTask(); + workers.add(worker); + } + + + /** + * 工作线程 + */ + private final class Worker extends Thread { + + private Runnable task; + + private Thread thread; + /** + * true --> 创建新的线程执行 + * false --> 从队列里获取线程执行 + */ + private boolean isNewTask; + + public Worker(Runnable task, boolean isNewTask) { + this.task = task; + this.isNewTask = isNewTask; + thread = this; + } + + public void startTask() { + thread.start(); + } + + public void close() { + thread.interrupt(); + } + + @Override + public void run() { + + Runnable task = null; + + if (isNewTask) { + task = this.task; + } + + boolean compile = true ; + + try { + while ((task != null || (task = getTask()) != null)) { + try { + //执行任务 + task.run(); + } catch (Exception e) { + compile = false ; + throw e ; + } finally { + //任务执行完毕 + task = null; + int number = totalTask.decrementAndGet(); + //LOGGER.info("number={}",number); + if (number == 0) { + synchronized (shutDownNotify) { + shutDownNotify.notify(); + } + } + } + } + + } finally { + //释放线程 + boolean remove = workers.remove(this); + //LOGGER.info("remove={},size={}", remove, workers.size()); + + if (!compile){ + addWorker(null); + } + tryClose(true); + } + } + } + + + /** + * 从队列中获取任务 + * + * @return + */ + private Runnable getTask() { + //关闭标识及任务是否全部完成 + if (isShutDown.get() && totalTask.get() == 0) { + return null; + } + //while (true) { + // + // if (workers.size()> miniSize) { + // boolean value = number.compareAndSet(number.get(), number.get() - 1); + // if (value) { + // return null; + // } else { + // continue; + // } + // } + + lock.lock(); + + try { + Runnable task = null; + if (workers.size()> miniSize) { + //大于核心线程数时需要用保活时间获取任务 + task = workQueue.poll(keepAliveTime, unit); + } else { + task = workQueue.take(); + } + + if (task != null) { + return task; + } + } catch (InterruptedException e) { + return null; + } finally { + lock.unlock(); + } + + return null; + //} + } + + /** + * 任务执行完毕后关闭线程池 + */ + public void shutdown() { + isShutDown.set(true); + tryClose(true); + //中断所有线程 + //synchronized (shutDownNotify){ + // while (totalTask.get()> 0){ + // try { + // shutDownNotify.wait(); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + // } + //} + } + + /** + * 立即关闭线程池,会造成任务丢失 + */ + public void shutDownNow() { + isShutDown.set(true); + tryClose(false); + + } + + /** + * 阻塞等到任务执行完毕 + */ + public void mainNotify() { + synchronized (shutDownNotify) { + while (totalTask.get()> 0) { + try { + shutDownNotify.wait(); + if (notify != null) { + notify.notifyListen(); + } + } catch (InterruptedException e) { + return; + } + } + } + } + + /** + * 关闭线程池 + * + * @param isTry true 尝试关闭 --> 会等待所有任务执行完毕 + * false 立即关闭线程池--> 任务有丢失的可能 + */ + private void tryClose(boolean isTry) { + if (!isTry) { + closeAllTask(); + } else { + if (isShutDown.get() && totalTask.get() == 0) { + closeAllTask(); + } + } + + } + + /** + * 关闭所有任务 + */ + private void closeAllTask() { + for (Worker worker : workers) { + //LOGGER.info("开始关闭"); + worker.close(); + } + } + + /** + * 获取工作线程数量 + * + * @return + */ + public int getWorkerCount() { + return workers.size(); + } + + /** + * 内部存放工作线程容器,并发安全。 + * + * @param + */ + private final class ConcurrentHashSet extends AbstractSet { + + private ConcurrentHashMap map = new ConcurrentHashMap(); + private final Object PRESENT = new Object(); + + private AtomicInteger count = new AtomicInteger(); + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public boolean add(T t) { + count.incrementAndGet(); + return map.put(t, PRESENT) == null; + } + + @Override + public boolean remove(Object o) { + count.decrementAndGet(); + return map.remove(o) == PRESENT; + } + + @Override + public int size() { + return count.get(); + } + } +} diff --git a/src/main/java/com/crossoverjie/concurrent/Volatile.java b/src/main/java/com/crossoverjie/concurrent/Volatile.java index 80b05512..a910c61e 100644 --- a/src/main/java/com/crossoverjie/concurrent/Volatile.java +++ b/src/main/java/com/crossoverjie/concurrent/Volatile.java @@ -1,6 +1,6 @@ package com.crossoverjie.concurrent; -import java.util.concurrent.TimeUnit; +import java.util.Scanner; /** * Function: @@ -16,7 +16,6 @@ public class Volatile implements Runnable{ @Override public void run() { while (flag){ - System.out.println(Thread.currentThread().getName() + "正在运行。。。"); } System.out.println(Thread.currentThread().getName() +"执行完毕"); } @@ -28,13 +27,28 @@ public static void main(String[] args) throws InterruptedException { System.out.println("main 线程正在运行") ; - TimeUnit.MILLISECONDS.sleep(100) ; + Scanner sc = new Scanner(System.in); + while(sc.hasNext()){ + String value = sc.next(); + if(value.equals("1")){ - aVolatile.stopThread(); + new Thread(new Runnable() { + @Override + public void run() { + aVolatile.stopThread(); + } + }).start(); + + break ; + } + } + + System.out.println("主线程退出了!"); } private void stopThread(){ flag = false ; } + } diff --git a/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java b/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java new file mode 100644 index 00000000..fe9d5f2e --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java @@ -0,0 +1,82 @@ +package com.crossoverjie.concurrent.communication; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年04月17日 19:35 + * @since JDK 1.8 + */ +public final class MultipleThreadCountDownKit { + + /** + * 计数器 + */ + private AtomicInteger counter; + + /** + * 通知对象 + */ + private Object notify ; + + private Notify notifyListen ; + + public MultipleThreadCountDownKit(int number){ + if (number < 0) { + throw new IllegalArgumentException("counter < 0"); + } + counter = new AtomicInteger(number) ; + notify = new Object() ; + } + + /** + * 设置回调接口 + * @param notify + */ + public void setNotify(Notify notify){ + notifyListen = notify ; + } + + + /** + * 线程完成后计数 -1 + */ + public void countDown(){ + + if (counter.get() <= 0){ + return; + } + + int count = this.counter.decrementAndGet(); + if (count < 0){ + throw new RuntimeException("concurrent error") ; + } + + if (count == 0){ + synchronized (notify){ + notify.notify(); + } + } + + } + + /** + * 等待所有的线程完成 + * @throws InterruptedException + */ + public void await() throws InterruptedException { + synchronized (notify){ + while (counter.get()> 0){ + notify.wait(); + } + + if (notifyListen != null){ + notifyListen.notifyListen(); + } + + } + } + +} diff --git a/src/main/java/com/crossoverjie/concurrent/communication/Notify.java b/src/main/java/com/crossoverjie/concurrent/communication/Notify.java new file mode 100644 index 00000000..ff7ab086 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/communication/Notify.java @@ -0,0 +1,16 @@ +package com.crossoverjie.concurrent.communication; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年04月17日 20:26 + * @since JDK 1.8 + */ +public interface Notify { + + /** + * 回调 + */ + void notifyListen() ; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/Callable.java b/src/main/java/com/crossoverjie/concurrent/future/Callable.java new file mode 100644 index 00000000..b28f8944 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/Callable.java @@ -0,0 +1,17 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年06月03日 23:54 + * @since JDK 1.8 + */ +public interface Callable { + + /** + * 执行任务 + * @return 执行结果 + */ + T call() ; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/Future.java b/src/main/java/com/crossoverjie/concurrent/future/Future.java new file mode 100644 index 00000000..acfbf83d --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/Future.java @@ -0,0 +1,18 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年06月03日 23:55 + * @since JDK 1.8 + */ +public interface Future { + + /** + * 获取 + * @return 结果 + * @throws InterruptedException + */ + T get() throws InterruptedException; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java b/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java new file mode 100644 index 00000000..15a6684a --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java @@ -0,0 +1,46 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019年06月03日 23:56 + * @since JDK 1.8 + */ +public class FutureTask implements Runnable,Future { + + private Callable callable ; + + private T result; + + private Object notify ; + + public FutureTask(Callable callable) { + this.callable = callable; + notify = new Object() ; + } + + @Override + public T get() throws InterruptedException { + + synchronized (notify){ + while (result == null){ + notify.wait(); + } + + return result; + } + } + + @Override + public void run() { + + T call = callable.call(); + + this.result = call ; + + synchronized (notify){ + notify.notify(); + } + } +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Main.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Main.java new file mode 100644 index 00000000..daf170b8 --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Main.java @@ -0,0 +1,25 @@ +package com.crossoverjie.design.pattern.chainofresponsibility; + +import com.crossoverjie.design.pattern.chainofresponsibility.impl.CopyrightProcess; +import com.crossoverjie.design.pattern.chainofresponsibility.impl.SensitiveWordProcess; +import com.crossoverjie.design.pattern.chainofresponsibility.impl.TypoProcess; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月21日 23:07 + * @since JDK 1.8 + */ +public class Main { + public static void main(String[] args) { + String msg = "内容内容内容==" ; + + MsgProcessChain chain = new MsgProcessChain() + .addChain(new SensitiveWordProcess()) + .addChain(new TypoProcess()) + .addChain(new CopyrightProcess()) ; + + chain.process(msg) ; + } +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/MsgProcessChain.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/MsgProcessChain.java new file mode 100644 index 00000000..561d24bc --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/MsgProcessChain.java @@ -0,0 +1,36 @@ +package com.crossoverjie.design.pattern.chainofresponsibility; + +import java.util.ArrayList; +import java.util.List; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月22日 00:08 + * @since JDK 1.8 + */ +public class MsgProcessChain { + + private List chains = new ArrayList() ; + + /** + * 添加责任链 + * @param process + * @return + */ + public MsgProcessChain addChain(Process process){ + chains.add(process) ; + return this ; + } + + /** + * 执行处理 + * @param msg + */ + public void process(String msg){ + for (Process chain : chains) { + chain.doProcess(msg); + } + } +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Process.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Process.java new file mode 100644 index 00000000..1a1fbaf7 --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/Process.java @@ -0,0 +1,17 @@ +package com.crossoverjie.design.pattern.chainofresponsibility; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月21日 23:06 + * @since JDK 1.8 + */ +public interface Process { + + /** + * 执行处理 + * @param msg + */ + void doProcess(String msg) ; +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/CopyrightProcess.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/CopyrightProcess.java new file mode 100644 index 00000000..64e5f733 --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/CopyrightProcess.java @@ -0,0 +1,18 @@ +package com.crossoverjie.design.pattern.chainofresponsibility.impl; + +import com.crossoverjie.design.pattern.chainofresponsibility.Process; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月21日 23:56 + * @since JDK 1.8 + */ +public class CopyrightProcess implements Process { + + @Override + public void doProcess(String msg) { + System.out.println(msg + "版权处理"); + } +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/SensitiveWordProcess.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/SensitiveWordProcess.java new file mode 100644 index 00000000..e8b17584 --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/SensitiveWordProcess.java @@ -0,0 +1,17 @@ +package com.crossoverjie.design.pattern.chainofresponsibility.impl; + +import com.crossoverjie.design.pattern.chainofresponsibility.Process; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月21日 23:56 + * @since JDK 1.8 + */ +public class SensitiveWordProcess implements Process { + @Override + public void doProcess(String msg) { + System.out.println(msg + "敏感词处理"); + } +} diff --git a/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/TypoProcess.java b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/TypoProcess.java new file mode 100644 index 00000000..3051b186 --- /dev/null +++ b/src/main/java/com/crossoverjie/design/pattern/chainofresponsibility/impl/TypoProcess.java @@ -0,0 +1,17 @@ +package com.crossoverjie.design.pattern.chainofresponsibility.impl; + +import com.crossoverjie.design.pattern.chainofresponsibility.Process; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年10月21日 23:56 + * @since JDK 1.8 + */ +public class TypoProcess implements Process { + @Override + public void doProcess(String msg) { + System.out.println(msg + "错别字处理"); + } +} diff --git a/src/main/java/com/crossoverjie/disruptor/LongEvent.java b/src/main/java/com/crossoverjie/disruptor/LongEvent.java new file mode 100644 index 00000000..fdeac3ae --- /dev/null +++ b/src/main/java/com/crossoverjie/disruptor/LongEvent.java @@ -0,0 +1,24 @@ +package com.crossoverjie.disruptor; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年8月29日 01:42 + * @since JDK 1.8 + */ +public class LongEvent { + private long value; + + public void set(long value) { + this.value = value; + } + + public long getValue() { + return value; + } + + public void setValue(long value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/disruptor/LongEventFactory.java b/src/main/java/com/crossoverjie/disruptor/LongEventFactory.java new file mode 100644 index 00000000..c6427b16 --- /dev/null +++ b/src/main/java/com/crossoverjie/disruptor/LongEventFactory.java @@ -0,0 +1,17 @@ +package com.crossoverjie.disruptor; + +import com.lmax.disruptor.EventFactory; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年8月29日 01:42 + * @since JDK 1.8 + */ +public class LongEventFactory implements EventFactory { + @Override + public LongEvent newInstance() { + return new LongEvent(); + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/disruptor/LongEventHandler.java b/src/main/java/com/crossoverjie/disruptor/LongEventHandler.java new file mode 100644 index 00000000..a2102887 --- /dev/null +++ b/src/main/java/com/crossoverjie/disruptor/LongEventHandler.java @@ -0,0 +1,21 @@ +package com.crossoverjie.disruptor; + +import com.lmax.disruptor.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年8月29日 01:43 + * @since JDK 1.8 + */ +public class LongEventHandler implements EventHandler { + private final static Logger LOGGER = LoggerFactory.getLogger(LongEventHandler.class); + @Override + public void onEvent(LongEvent event, long sequence, boolean endOfBatch) throws InterruptedException { + LOGGER.info("消费 Event=[{}]",event.getValue()) ; + //Thread.sleep(1000); + } +} diff --git a/src/main/java/com/crossoverjie/disruptor/LongEventMain.java b/src/main/java/com/crossoverjie/disruptor/LongEventMain.java new file mode 100644 index 00000000..aae2d7d7 --- /dev/null +++ b/src/main/java/com/crossoverjie/disruptor/LongEventMain.java @@ -0,0 +1,96 @@ +package com.crossoverjie.disruptor; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.lmax.disruptor.RingBuffer; +import com.lmax.disruptor.YieldingWaitStrategy; +import com.lmax.disruptor.dsl.Disruptor; +import com.lmax.disruptor.dsl.ProducerType; + +import java.util.concurrent.*; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年8月29日 01:45 + * @since JDK 1.8 + */ +public class LongEventMain { + public static void main(String[] args) throws Exception { + + TimeUnit.SECONDS.sleep(10); + // Executor that will be used to construct new threads for consumers + BlockingQueue queue = new LinkedBlockingQueue(); + ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() + .setNameFormat("consumer-%d") + .setDaemon(true) + .build(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(15, 15, 1, TimeUnit.MILLISECONDS, queue,namedThreadFactory); + + ThreadFactory product = new ThreadFactoryBuilder() + .setNameFormat("product-%d") + .setDaemon(true) + .build(); + ThreadPoolExecutor productExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, queue,product); + + + + // The factory for the event + LongEventFactory factory = new LongEventFactory(); + + // Specify the size of the ring buffer, must be power of 2. + int bufferSize = 8; + + for (int i = 0; i < 1; i++) { + // Construct the Disruptor + //Disruptor disruptor = new Disruptor(factory, bufferSize, executor); + Disruptor disruptor = new Disruptor(factory, bufferSize, executor, ProducerType.SINGLE, + new YieldingWaitStrategy()); + + // Connect the handler + disruptor.handleEventsWith(new LongEventHandler()); + + // Start the Disruptor, starts all threads running + disruptor.start(); + + // Get the ring buffer from the Disruptor to be used for publishing. + RingBuffer ringBuffer = disruptor.getRingBuffer(); + + LongEventProducer producer = new LongEventProducer(ringBuffer); + + for (long l = 0; l < 1000000; l++) { + //producer.onData(l); + //Thread.sleep(1000); + productExecutor.execute(new Work(producer,l)); + } + + } + + + + productExecutor.shutdown(); + while (!productExecutor.awaitTermination(1, TimeUnit.SECONDS)) { + System.out.println("线程还在执行。。。"); + } + System.out.println("main over"); + + } + + + + private static class Work implements Runnable{ + + private LongEventProducer producer ; + private long bb ; + + public Work(LongEventProducer producer,long bb) { + this.producer = producer; + this.bb = bb ; + } + + @Override + public void run() { + producer.onData(bb); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/disruptor/LongEventProducer.java b/src/main/java/com/crossoverjie/disruptor/LongEventProducer.java new file mode 100644 index 00000000..fc8abb05 --- /dev/null +++ b/src/main/java/com/crossoverjie/disruptor/LongEventProducer.java @@ -0,0 +1,36 @@ +package com.crossoverjie.disruptor; + +import com.lmax.disruptor.RingBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018/8/29 01:43 + * @since JDK 1.8 + */ +public class LongEventProducer { + private final static Logger LOGGER = LoggerFactory.getLogger(LongEventProducer.class); + private final RingBuffer ringBuffer; + + public LongEventProducer(RingBuffer ringBuffer) { + this.ringBuffer = ringBuffer; + } + + public void onData(long bb) { + + ringBuffer.getCursor(); + + long sequence = ringBuffer.next(); // Grab the next sequence + try { + LongEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor + // for the sequence + //LOGGER.info("product=[{}]",bb); + event.set(bb); // Fill with data、 + } finally { + ringBuffer.publish(sequence); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java b/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java index 7f9a367c..9eb08933 100644 --- a/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java +++ b/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java @@ -1,9 +1,6 @@ package com.crossoverjie.guava; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.Interner; +import com.google.common.cache.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +28,12 @@ public class CacheLoaderTest { private void init() throws InterruptedException { 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 { @@ -50,7 +52,7 @@ private void checkAlert(Integer integer) { //loadingCache.put(integer,new AtomicLong(integer)); - TimeUnit.SECONDS.sleep(5); + TimeUnit.SECONDS.sleep(3); LOGGER.info("当前缓存值={},缓存大小={}", loadingCache.get(KEY),loadingCache.size()); diff --git a/src/main/java/com/crossoverjie/guava/callback/CallBackListener.java b/src/main/java/com/crossoverjie/guava/callback/CallBackListener.java new file mode 100644 index 00000000..cddad089 --- /dev/null +++ b/src/main/java/com/crossoverjie/guava/callback/CallBackListener.java @@ -0,0 +1,17 @@ +package com.crossoverjie.guava.callback; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年7月15日 13:49 + * @since JDK 1.8 + */ +public interface CallBackListener { + + /** + * 回调通知函数 + * @param msg + */ + void callBackNotify(String msg) ; +} diff --git a/src/main/java/com/crossoverjie/guava/callback/Caller.java b/src/main/java/com/crossoverjie/guava/callback/Caller.java new file mode 100644 index 00000000..dc9f1885 --- /dev/null +++ b/src/main/java/com/crossoverjie/guava/callback/Caller.java @@ -0,0 +1,79 @@ +package com.crossoverjie.guava.callback; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年7月15日 13:52 + * @since JDK 1.8 + */ +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("提问完毕,我去干其他事了"); + } + + + public Notifier getNotifier() { + return notifier; + } + + public void setNotifier(Notifier notifier) { + this.notifier = notifier; + } + + + public CallBackListener getCallBackListener() { + return callBackListener; + } + + public void setCallBackListener(CallBackListener callBackListener) { + this.callBackListener = callBackListener; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + @Override + public String toString() { + return "Caller{" + + "callBackListener=" + callBackListener + + ", notifier=" + notifier + + ", question='" + question + '\'' + + '}'; + } +} diff --git a/src/main/java/com/crossoverjie/guava/callback/Main.java b/src/main/java/com/crossoverjie/guava/callback/Main.java new file mode 100644 index 00000000..9e900257 --- /dev/null +++ b/src/main/java/com/crossoverjie/guava/callback/Main.java @@ -0,0 +1,32 @@ +package com.crossoverjie.guava.callback; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年7月15日 14:18 + * @since JDK 1.8 + */ +public class Main { + + private final static Logger LOGGER = LoggerFactory.getLogger(Main.class); + + 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(); + } +} diff --git a/src/main/java/com/crossoverjie/guava/callback/Notifier.java b/src/main/java/com/crossoverjie/guava/callback/Notifier.java new file mode 100644 index 00000000..26966af3 --- /dev/null +++ b/src/main/java/com/crossoverjie/guava/callback/Notifier.java @@ -0,0 +1,30 @@ +package com.crossoverjie.guava.callback; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年7月15日 13:59 + * @since JDK 1.8 + */ +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("我在北京!"); + + } + +} diff --git a/src/main/java/com/crossoverjie/hystrix/CommandOrder.java b/src/main/java/com/crossoverjie/hystrix/CommandOrder.java new file mode 100644 index 00000000..b55cf989 --- /dev/null +++ b/src/main/java/com/crossoverjie/hystrix/CommandOrder.java @@ -0,0 +1,57 @@ +package com.crossoverjie.hystrix; + +import com.netflix.hystrix.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * 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; + } + + +} diff --git a/src/main/java/com/crossoverjie/hystrix/CommandTest.java b/src/main/java/com/crossoverjie/hystrix/CommandTest.java new file mode 100644 index 00000000..093adece --- /dev/null +++ b/src/main/java/com/crossoverjie/hystrix/CommandTest.java @@ -0,0 +1,40 @@ +package com.crossoverjie.hystrix; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Function: 线程隔离测试 + * + * @author crossoverJie + * Date: 2018年7月28日 16:58 + * @since JDK 1.8 + */ +public class CommandTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(CommandTest.class); + + + 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); + } +} diff --git a/src/main/java/com/crossoverjie/hystrix/CommandUser.java b/src/main/java/com/crossoverjie/hystrix/CommandUser.java new file mode 100644 index 00000000..d2e2a2fa --- /dev/null +++ b/src/main/java/com/crossoverjie/hystrix/CommandUser.java @@ -0,0 +1,58 @@ +package com.crossoverjie.hystrix; + +import com.netflix.hystrix.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * 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; + } + + +} diff --git a/src/main/java/com/crossoverjie/red/RedPacket.java b/src/main/java/com/crossoverjie/red/RedPacket.java index 402f29f0..bc709061 100644 --- a/src/main/java/com/crossoverjie/red/RedPacket.java +++ b/src/main/java/com/crossoverjie/red/RedPacket.java @@ -15,121 +15,129 @@ public class RedPacket { /** * 生成红包最小值 1分 */ - private static final int MIN_MONEY = 1 ; + private static final int MIN_MONEY = 1; /** * 生成红包最大值 200人民币 */ - private static final int MAX_MONEY = 200 * 100 ; + private static final int MAX_MONEY = 200 * 100; /** * 小于最小值 */ - private static final int LESS = -1 ; + private static final int LESS = -1; /** * 大于最大值 */ - private static final int MORE = -2 ; + private static final int MORE = -2; /** * 正常值 */ - private static final int OK = 1 ; + private static final int OK = 1; /** * 最大的红包是平均值的 TIMES 倍,防止某一次分配红包较大 */ - private static final double TIMES = 2.1F ; + private static final double TIMES = 2.1F; - private int recursiveCount = 0 ; + 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 ; + } - private List splitRedPacket(int money,int count){ - List moneys = new LinkedList() ; //计算出最大红包 - int max = (int) ((money / count) * TIMES) ; - max = max> MAX_MONEY ? MAX_MONEY : max ; + int max = (int) ((money / count) * TIMES); + max = max> MAX_MONEY ? MAX_MONEY : max; - for (int i = 0 ; i< count ; i++){ + for (int i = 0; i < count; i++) { //随机获取红包 - int redPacket = randomRedPacket(money, MIN_MONEY,max,count - i) ; + int redPacket = randomRedPacket(money, MIN_MONEY, max, count - i); moneys.add(redPacket); //总金额每次减少 - money -= redPacket ; + money -= redPacket; } - return moneys ; + return moneys; } private int randomRedPacket(int totalMoney, int minMoney, int maxMoney, int count) { //只有一个红包直接返回 - if (count == 1){ - return totalMoney ; + if (count == 1) { + return totalMoney; } - if (minMoney == maxMoney){ - return minMoney ; + if (minMoney == maxMoney) { + return minMoney; } //如果最大金额大于了剩余金额 则用剩余金额 因为这个 money 每分配一次都会减小 - maxMoney = maxMoney> totalMoney ? totalMoney : maxMoney ; + maxMoney = maxMoney> totalMoney ? totalMoney : maxMoney; //在 minMoney到maxMoney 生成一个随机红包 - int redPacket = (int) (Math.random() * (maxMoney - minMoney) + minMoney) ; + int redPacket = (int) (Math.random() * (maxMoney - minMoney) + minMoney); - int lastMoney = totalMoney - redPacket ; + int lastMoney = totalMoney - redPacket; - int status = checkMoney(lastMoney,count - 1) ; + int status = checkMoney(lastMoney, count - 1); //正常金额 - if (OK == status){ - return redPacket ; + if (OK == status) { + return redPacket; } //如果生成的金额不合法 则递归重新生成 - if (LESS == status){ - recursiveCount ++ ; + if (LESS == status) { + recursiveCount++; System.out.println("recursiveCount==" + recursiveCount); - return randomRedPacket(totalMoney,minMoney,redPacket,count) ; + return randomRedPacket(totalMoney, minMoney, redPacket, count); } - if (MORE == status){ - recursiveCount ++ ; + if (MORE == status) { + recursiveCount++; System.out.println("recursiveCount===" + recursiveCount); - return randomRedPacket(totalMoney,redPacket,maxMoney,count) ; + return randomRedPacket(totalMoney, redPacket, maxMoney, count); } - return redPacket ; + return redPacket; } /** * 校验剩余的金额的平均值是否在 最小值和最大值这个范围内 + * * @param lastMoney * @param count * @return */ private int checkMoney(int lastMoney, int count) { - double avg = lastMoney / count ; - if (avg < MIN_MONEY){ - return LESS ; + double avg = lastMoney / count; + if (avg < MIN_MONEY) { + return LESS; } - if (avg> MAX_MONEY){ - return MORE ; + if (avg> MAX_MONEY) { + return MORE; } - return OK ; + return OK; } public static void main(String[] args) { - RedPacket redPacket = new RedPacket() ; + RedPacket redPacket = new RedPacket(); List redPackets = redPacket.splitRedPacket(20000, 100); - System.out.println(redPackets) ; + System.out.println(redPackets); - int sum = 0 ; + int sum = 0; for (Integer red : redPackets) { - sum += red ; + sum += red; } System.out.println(sum); } diff --git a/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java b/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java index fd44aa6d..707a6cd1 100644 --- a/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java +++ b/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java @@ -1,8 +1,6 @@ package com.crossoverjie.spring; -import com.crossoverjie.concurrent.Singleton; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** diff --git a/src/main/java/com/crossoverjie/spring/annotation/AnnotationBean.java b/src/main/java/com/crossoverjie/spring/annotation/AnnotationBean.java index e848581b..319a1fff 100644 --- a/src/main/java/com/crossoverjie/spring/annotation/AnnotationBean.java +++ b/src/main/java/com/crossoverjie/spring/annotation/AnnotationBean.java @@ -1,6 +1,5 @@ package com.crossoverjie.spring.annotation; -import com.crossoverjie.spring.SpringLifeCycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -8,6 +7,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; + /** * Function:用注解的方法 * diff --git a/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java b/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java new file mode 100644 index 00000000..5deddd1f --- /dev/null +++ b/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java @@ -0,0 +1,85 @@ +package com.crossoverjie.thread; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Function:线程池异常测试 + * + * @author crossoverJie + * Date: 2019年03月07日 20:35 + * @since JDK 1.8 + */ +public class ThreadExceptionTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(ThreadExceptionTest.class); + + + public static void main(String[] args) throws InterruptedException { + + ExecutorService execute = new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + + execute.execute(new Runnable() { + @Override + public void run() { + LOGGER.info("=====11======="); + } + }); + + TimeUnit.SECONDS.sleep(5); + + + execute.execute(new Run1()); + + //TimeUnit.SECONDS.sleep(5); + // + //execute.execute(new Run2()); + //execute.shutdown(); + + } + + + private static class Run1 implements Runnable { + + @Override + public void run() { + int count = 0; + while (true) { + count++; + LOGGER.info("-------222-------------{}", count); + + if (count == 10) { + System.out.println(1 / 0); + try { + } catch (Exception e) { + LOGGER.error("Exception",e); + } + } + + if (count == 20) { + LOGGER.info("count={}", count); + break; + } + } + } + } + + private static class Run2 implements Runnable { + + public Run2() { + LOGGER.info("run2 构造函数"); + } + + @Override + public void run() { + LOGGER.info("run222222222"); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c9ef850c..68b4f531 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,3 @@ -spring.application.name=java-interview +spring.application.name=JCSprout logging.level.root=INFO \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 693796b6..422ffb31 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,6 +1,6 @@ - + diff --git a/src/test/java/com/crossoverjie/actual/LRUMapTest.java b/src/test/java/com/crossoverjie/actual/LRUMapTest.java index eb9dcc24..1c1d53e6 100644 --- a/src/test/java/com/crossoverjie/actual/LRUMapTest.java +++ b/src/test/java/com/crossoverjie/actual/LRUMapTest.java @@ -1,11 +1,7 @@ package com.crossoverjie.actual; -import com.alibaba.fastjson.JSON; -import com.sun.org.apache.bcel.internal.generic.LUSHR; import org.junit.Test; -import static org.junit.Assert.*; - public class LRUMapTest { @Test @@ -118,4 +114,26 @@ public void get3() throws Exception { System.out.println(lruMap.toString()); } + @Test + public void get4() throws Exception { + LRUMap lru = new LRUMap(5); + + + lru.put("1",1); + lru.put("2",2); + lru.put("3",3); + lru.put("4",4); + lru.put("5",5); + + System.out.println(lru.toString()); + + lru.get("2"); + lru.get("3"); + lru.get("4"); + lru.get("5"); + + lru.put("6",6); + System.out.println(lru.toString()); + } + } \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/algorithm/BinaryNodeTest.java b/src/test/java/com/crossoverjie/algorithm/BinaryNodeTest.java index b9e85191..1e4780a5 100644 --- a/src/test/java/com/crossoverjie/algorithm/BinaryNodeTest.java +++ b/src/test/java/com/crossoverjie/algorithm/BinaryNodeTest.java @@ -11,7 +11,7 @@ public void test1(){ node = node.createNode() ; System.out.println(node); - //中序遍历二叉树 + //层序遍历二叉树 node.levelIterator(node) ; } diff --git a/src/test/java/com/crossoverjie/algorithm/BinaryNodeTravelTest.java b/src/test/java/com/crossoverjie/algorithm/BinaryNodeTravelTest.java new file mode 100644 index 00000000..f24a9b43 --- /dev/null +++ b/src/test/java/com/crossoverjie/algorithm/BinaryNodeTravelTest.java @@ -0,0 +1,21 @@ +package com.crossoverjie.algorithm; + +import org.junit.Test; + +public class BinaryNodeTravelTest { + @Test + public void levelIterator() throws Exception { + BinaryNodeTravel node = new BinaryNodeTravel() ; + //创建二叉树 + node = node.createNode() ; + + //层序遍历二叉树 + BinaryNodeTravel binaryNodeTravel = node.levelIterator(node); + + while (binaryNodeTravel != null){ + System.out.print(binaryNodeTravel.getData() +"--->"); + binaryNodeTravel = binaryNodeTravel.next; + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/algorithm/BloomFiltersTest.java b/src/test/java/com/crossoverjie/algorithm/BloomFiltersTest.java new file mode 100644 index 00000000..83baf2fc --- /dev/null +++ b/src/test/java/com/crossoverjie/algorithm/BloomFiltersTest.java @@ -0,0 +1,67 @@ +package com.crossoverjie.algorithm; + +import com.google.common.hash.BloomFilter; +import com.google.common.hash.Funnels; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +public class BloomFiltersTest { + + private static int count = 10000000; + + @Test + public void guavaTest() { + long star = System.currentTimeMillis(); + BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + count, + 0.01); + + for (int i = 0; i < count; i++) { + filter.put(i); + } + + Assert.assertTrue(filter.mightContain(1)); + Assert.assertTrue(filter.mightContain(2)); + Assert.assertTrue(filter.mightContain(3)); + Assert.assertFalse(filter.mightContain(count)); + long end = System.currentTimeMillis(); + System.out.println("执行时间:" + (end - star)); + } + + + @Test + public void hashMapTest(){ + long star = System.currentTimeMillis(); + + Set hashset = new HashSet(10000000) ; + for (int i = 0; i < 10000000; 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)); + } + + @Test + public void bloomFilterTest(){ + long star = System.currentTimeMillis(); + BloomFilters bloomFilters = new BloomFilters(count) ; + for (int i = 0; i < count; 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)); + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java b/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java new file mode 100644 index 00000000..b553b35c --- /dev/null +++ b/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java @@ -0,0 +1,164 @@ +package com.crossoverjie.algorithm; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + + +public class LinkedListMergeSortTest { + + @Rule public Timeout globalTimeout = new Timeout(10000); + + @Test + public void constructorOutputVoid() { + + // Act, creating object to test constructor + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + + // Method returns void, testing that no exception is thrown + } + + // Test generated by Diffblue Cover. + @Test + public void mergeListNotNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(-2_147_483_647, null); + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(0, null); + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_647, retval.next.e); + Assert.assertNull(retval.next.next); + } + + // Test generated by Diffblue Cover. + @Test + public void mergeListInputNotNullNotNullOutputNotNull2() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(1, null); + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(-2_147_483_648, null); + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(1, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_648, retval.next.e); + Assert.assertNull(retval.next.next); + } + + + @Test + public void mergeListInputRightNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(-2_147_483_647,null); + final LinkedListMergeSort.Node right = null; + + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(-2_147_483_647, retval.e); + Assert.assertNull(retval.next); + } + + + @Test + public void mergeListInputLeftNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = null; + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(0, null); + + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNull(retval.next); + } + + + @Test + public void mergeListInputNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = null; + final LinkedListMergeSort.Node right = null; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNull(retval); + } + + @Test + public void mergeSortLength2() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node node = new LinkedListMergeSort.Node(-0, null); + final LinkedListMergeSort.Node first = new LinkedListMergeSort.Node(-2_147_483_647, node); + + final int length = 2; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeSort(first, length); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_647, retval.next.e); + Assert.assertNull(retval.next.next); + } + + @Test + public void mergeSortInputNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node first = null; + final int length = 1; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeSort(first, length); + + // Assert result + Assert.assertNull(retval); + } + @Test + public void mainInput0OutputVoid() throws Exception { + + // Arrange + final String[] args = {}; + + // Act + LinkedListMergeSort.main(args); + + // Method returns void, testing that no exception is thrown + } + +} diff --git a/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java b/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java new file mode 100644 index 00000000..011b982f --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java @@ -0,0 +1,232 @@ +package com.crossoverjie.concurrent; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class ArrayQueueTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(ArrayQueueTest.class) ; + + @Test + public void test() throws InterruptedException { + ArrayBlockingQueue queue = new ArrayBlockingQueue(3); + + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(2); + System.out.println("thread[" + Thread.currentThread().getName() + "]" + queue.take()); + } catch (Exception e) { + } + }).start(); + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + queue.size(); + + + } + + @Test + public void put() { + ArrayQueue queue = new ArrayQueue(3); + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + System.out.println(queue.size()); + + + while (!queue.isEmpty()) { + System.out.println(queue.get()); + } + + } + + @Test + public void put2() { + final ArrayQueue queue = new ArrayQueue(3); + + new Thread(() -> { + try { + LOGGER.info("[" + Thread.currentThread().getName() + "]" + queue.get()); + } catch (Exception e) { + } + }).start(); + + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + LOGGER.info("size=" + queue.size()); + + + while (!queue.isEmpty()) { + LOGGER.info(queue.get()); + } + + } + + @Test + public void put3() { + final ArrayQueue queue = new ArrayQueue(3); + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + System.out.println(queue.size()); + + + while (!queue.isEmpty()) { + System.out.println(queue.get()); + } + + } + + @Test + public void put4() throws InterruptedException { + final ArrayQueue queue = new ArrayQueue(299); + + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + + Thread t3 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + Thread t4 = new Thread(() -> { + System.out.println("=====" + queue.get()); + }); + + t1.start(); + t2.start(); + t3.start(); + t4.start(); + + t1.join(); + t2.join(); + t3.join(); + System.out.println(queue.size()); + + + } + + @Test + public void put5() throws InterruptedException { + final ArrayQueue queue = new ArrayQueue(1000000); + + long startTime = System.currentTimeMillis(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + queue.put(i + ""); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + queue.put(i + ""); + } + }); + + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + long end = System.currentTimeMillis(); + + System.out.println("cast = [" + (end - startTime) + "]" + queue.size()); + + } + + @Test + public void put6() throws InterruptedException { + final ArrayBlockingQueue queue = new ArrayBlockingQueue(1000000); + + long startTime = System.currentTimeMillis(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + try { + queue.put(i + ""); + } catch (InterruptedException e) { + } + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + try { + queue.put(i + ""); + } catch (InterruptedException e) { + } + } + }); + + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + long end = System.currentTimeMillis(); + + System.out.println("cast = [" + (end - startTime) + "]" + queue.size()); + + } + + + @Test + public void get2() throws InterruptedException { + ArrayQueue queue = new ArrayQueue(100); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 50; i++) { + try { + queue.put(i + ""); + } catch (Exception e) { + } + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 50; i < 100; i++) { + try { + queue.put(i + ""); + } catch (Exception e) { + } + } + }); + + Thread t3 = new Thread(() -> { + System.out.println("开始消费"); + while (true) { + System.out.println(queue.get()); + } + }); + + t3.start(); + t2.start(); + t1.start(); + + t3.join(); + t2.join(); + t1.join(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java new file mode 100644 index 00000000..4af12b33 --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java @@ -0,0 +1,64 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolExeceptionTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolExeceptionTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue(1); + CustomThreadPool pool = new CustomThreadPool(1, 1, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + + pool.execute(new Worker(0)); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + + } + + + + + private static class Worker implements Runnable { + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + + while (true){ + state ++ ; + + if (state == 1000){ + throw new NullPointerException("NullPointerException"); + } + } + + } catch (InterruptedException e) { + + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java new file mode 100644 index 00000000..176632ea --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java @@ -0,0 +1,75 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import com.crossoverjie.concurrent.future.Callable; +import com.crossoverjie.concurrent.future.Future; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolFutureTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolFutureTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue(10); + CustomThreadPool pool = new CustomThreadPool(3, 5, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + + List futures = new ArrayList() ; + for (int i = 0; i < 10; i++) { + Future future = pool.submit(new Worker(i)); + futures.add(future) ; + } + + pool.shutdown(); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + for (Future future : futures) { + Integer integer = future.get(); + LOGGER.info("future======{}" ,integer); + } + + + + + } + + + + + private static class Worker implements Callable { + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public Integer call() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + return state + 1 ; + } catch (InterruptedException e) { + + } + + return 0 ; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java new file mode 100644 index 00000000..c4c5f20d --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java @@ -0,0 +1,70 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue(10); + CustomThreadPool pool = new CustomThreadPool(3, 5, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + for (int i = 0; i < 10; i++) { + pool.execute(new Worker(i)); + } + + + LOGGER.info("=======休眠前线程池活跃线程数={}======",pool.getWorkerCount()); + + TimeUnit.SECONDS.sleep(5); + LOGGER.info("=======休眠后线程池活跃线程数={}======",pool.getWorkerCount()); + + for (int i = 0; i < 3; i++) { + pool.execute(new Worker(i + 100)); + } + + pool.shutdown(); + //pool.shutDownNow(); + //pool.execute(new Worker(100)); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + + } + + + + + private static class Worker implements Runnable{ + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + } catch (InterruptedException e) { + + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java b/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java new file mode 100644 index 00000000..35661a67 --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java @@ -0,0 +1,51 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.MultipleThreadCountDownKit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class MultipleThreadCountDownKitTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(MultipleThreadCountDownKitTest.class) ; + + + public static void main(String[] args) throws InterruptedException { + MultipleThreadCountDownKit multipleThreadKit = new MultipleThreadCountDownKit(3); + multipleThreadKit.setNotify(() -> LOGGER.info("三个线程完成了任务")); + + Thread t1= new Thread(() -> { + try { + //TimeUnit.SECONDS.sleep(5); + LOGGER.info("t1..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + Thread t2= new Thread(() -> { + try { + //TimeUnit.SECONDS.sleep(3); + LOGGER.info("t2..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + Thread t3= new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(2); + LOGGER.info("t3..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + + t1.start(); + t2.start(); + t3.start(); + + multipleThreadKit.await(); + LOGGER.info("======================"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java b/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java new file mode 100644 index 00000000..ebe47a0f --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java @@ -0,0 +1,61 @@ +package com.crossoverjie.concurrent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + + +public class ThreadPoolTest { + private final static Logger LOGGER = LoggerFactory.getLogger(ThreadPoolTest.class); + + + + public static void main(String[] args) throws Exception { + BlockingQueue queue = new ArrayBlockingQueue(10); + ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,1, TimeUnit.SECONDS,queue,new ThreadPoolExecutor.DiscardOldestPolicy()) ; + + List futures = new ArrayList() ; + for (int i = 0; i < 10; i++) { + Future future = pool.submit(new Worker(i)); + futures.add(future) ; + } + + pool.shutdown(); + + for (Future future : futures) { + LOGGER.info("执行结果={}",future.get()); + } + LOGGER.info("++++++++++++++"); + } + + + + + private static class Worker implements Callable{ + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public Integer call() { + try { + TimeUnit.SECONDS.sleep(2); + LOGGER.info("state={}",state); + return state ; + } catch (InterruptedException e) { + + } + + return -1; + } + } + + + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/kafka/KafkaTest.java b/src/test/java/com/crossoverjie/kafka/KafkaTest.java new file mode 100644 index 00000000..10e70855 --- /dev/null +++ b/src/test/java/com/crossoverjie/kafka/KafkaTest.java @@ -0,0 +1,106 @@ +package com.crossoverjie.kafka; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Function: + * + * @author crossoverJie + * Date: 2018年11月20日 02:36 + * @since JDK 1.8 + */ +public class KafkaTest { + + + @Test + public void consumer(){ + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("group.id", "test"); + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer(props); + List tPartitions = new ArrayList(); + TopicPartition tPartition0 = new TopicPartition("data-push", 0); + TopicPartition tPartition1 = new TopicPartition("data-push", 1); + TopicPartition tPartition2 = new TopicPartition("data-push", 2); + tPartitions.add(tPartition0); + tPartitions.add(tPartition1); + tPartitions.add(tPartition2); + consumer.assign(tPartitions); + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records){ + System.out.printf("offset = %d, key = %s,partition = %s value = %s%n", record.offset(), record.key(),record.partition(), record.value()); + + } + } + } + + @Test + public void threadConsumer() throws InterruptedException { + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("group.id", "test"); + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + + TopicPartition tPartition0 = new TopicPartition("data-push", 0); + Consumer c1 = new Consumer(tPartition0,props,"c1"); + + TopicPartition tPartition1 = new TopicPartition("data-push", 1); + Consumer c2 = new Consumer(tPartition1,props,"c2"); + + TopicPartition tPartition2 = new TopicPartition("data-push", 2); + Consumer c3 = new Consumer(tPartition2,props,"c3"); + + c1.start(); + c2.start(); + c3.start(); + + c1.join(); + c2.join(); + c3.join(); + + } + + private class Consumer extends Thread{ + private TopicPartition topicPartition ; + private Properties props; + + private KafkaConsumer consumer ; + private List tPartitions = new ArrayList(); + + public Consumer(TopicPartition topicPartition, Properties props,String name) { + super(name); + this.topicPartition = topicPartition; + this.props = props; + consumer = new KafkaConsumer(props) ; + tPartitions.add(topicPartition) ; + } + + @Override + public void run() { + consumer.assign(tPartitions); + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records){ + System.out.printf("thread= %s , offset = %d, key = %s,partition = %s value = %s%n",Thread.currentThread().getName(), record.offset(), record.key(),record.partition(), record.value()); + + } + } + } + } +} diff --git a/src/test/java/com/crossoverjie/proxy/JDKProxyTest.java b/src/test/java/com/crossoverjie/proxy/JDKProxyTest.java index 56aaab44..1efb6e47 100644 --- a/src/test/java/com/crossoverjie/proxy/JDKProxyTest.java +++ b/src/test/java/com/crossoverjie/proxy/JDKProxyTest.java @@ -4,11 +4,7 @@ import com.crossoverjie.proxy.jdk.ISubject; import com.crossoverjie.proxy.jdk.impl.ISubjectImpl; import org.junit.Test; -import sun.misc.ProxyGenerator; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; import java.lang.reflect.Proxy; /** @@ -29,16 +25,16 @@ public void test(){ @Test public void clazzTest(){ - byte[] proxyClassFile = ProxyGenerator.generateProxyClass( - "$Proxy1", new Class[]{ISubject.class}, 1); - try { - FileOutputStream out = new FileOutputStream("/Users/chenjie/Documents/$Proxy1.class") ; - out.write(proxyClassFile); - out.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } +// byte[] proxyClassFile = ProxyGenerator.generateProxyClass( +// "$Proxy1", new Class[]{ISubject.class}, 1); +// try { +// FileOutputStream out = new FileOutputStream("/Users/chenjie/Documents/$Proxy1.class") ; +// out.write(proxyClassFile); +// out.close(); +// } catch (FileNotFoundException e) { +// e.printStackTrace(); +// } catch (IOException e) { +// e.printStackTrace(); +// } } } diff --git a/src/test/java/com/crossoverjie/red/RedPacketTest.java b/src/test/java/com/crossoverjie/red/RedPacketTest.java new file mode 100644 index 00000000..f5cbe01e --- /dev/null +++ b/src/test/java/com/crossoverjie/red/RedPacketTest.java @@ -0,0 +1,47 @@ +package com.crossoverjie.red; + +import org.junit.Test; + +import java.util.List; + +public class RedPacketTest { + + @Test + public void right(){ + 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); + } + + @Test + public void right_(){ + RedPacket redPacket = new RedPacket() ; + List redPackets = redPacket.splitRedPacket(40000, 2); + System.out.println(redPackets) ; + + int sum = 0 ; + for (Integer red : redPackets) { + sum += red ; + } + System.out.println(sum); + } + + @Test + public void right__(){ + RedPacket redPacket = new RedPacket() ; + List redPackets = redPacket.splitRedPacket(100, 101); + System.out.println(redPackets) ; + + int sum = 0 ; + for (Integer red : redPackets) { + sum += red ; + } + System.out.println(sum); + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/reference/ReferenceTest.java b/src/test/java/com/crossoverjie/reference/ReferenceTest.java new file mode 100644 index 00000000..a08060d6 --- /dev/null +++ b/src/test/java/com/crossoverjie/reference/ReferenceTest.java @@ -0,0 +1,98 @@ +package com.crossoverjie.reference; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Function: + * + * @author crossoverJie + * Date: 2020年12月19日 00:18 + * @since JDK 11 + */ +public class ReferenceTest { + + @Test + public void testBasic() { + int a = 10; + modifyBasic(a); + System.out.println(String.format("最终结果 main a==%s", a)); + } + + + private void modifyBasic(int aa) { + System.out.println(String.format("修改之前 aa==%s", aa)); + aa = 20; + System.out.println(String.format("修改之后 aa==%s", aa)); + } + + @Test + public void testReference01(){ + Car car1 = new Car("benz"); + modifyCar1(car1); + System.out.println(String.format("最终结果 main car1==%s", car1)); + } + + private void modifyCar1(Car car){ + System.out.println(String.format("修改之前 car==%s", car)); + car.name = "bwm"; + System.out.println(String.format("修改之后 car==%s", car)); + } + + @Test + public void testList(){ + List list = new ArrayList(); + list.add(1); + addList(list); + System.out.println(list); + } + + private void addList(List list) { + list.add(2); + } + + @Test + public void test02(){ + Car car1 = new Car("benz"); + modifyCar(car1); + System.out.println(String.format("最终结果 main car1==%s", car1)); + } + + private void modifyCar(Car car2) { + System.out.println(String.format("修改之前 car2==%s", car2)); + car2 = new Car("bmw"); + System.out.println(String.format("修改之后 car2==%s", car2)); + } + + private class Car{ + private String name; + + public Car(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Car{" + + "name='" + name + '\'' + + '}'; + } + } + + @Test + public void testReference03() { + List a = new ArrayList(); + a.add(1); + modifyReference02(a); + System.out.println(String.format("Final main a==%s", a)); + + } + + private void modifyReference02(List aa) { + System.out.println(String.format("Before modify aa==%s", aa)); + aa.add(2); + System.out.println(String.format("After modify aa==%s", aa)); + } +}