() ;
+ 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 @@
+
+
+
+# 前言
+
+记得一年前分享过一篇[《一致性 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) 是干啥的:
+
+
+
+其中有一个场景是在客户端登录成功后需要从可用的服务端列表中选择一台服务节点返回给客户端使用。
+
+而这个选择的过程就是一个负载策略的过程;第一版本做的比较简单,默认只支持轮询的方式。
+
+虽然够用,但不够优雅😏。
+
+**因此我的规划是内置多种路由策略供使用者根据自己的场景选择,同时提供简单的 API 供用户自定义自己的路由策略。**
+
+
+先来看看一致性 Hash 算法的一些特点:
+
+- 构造一个 `0 ~ 2^32-1` 大小的环。
+- 服务节点经过 hash 之后将自身存放到环中的下标中。
+- 客户端根据自身的某些数据 hash 之后也定位到这个环中。
+- 通过顺时针找到离他最近的一个节点,也就是这次路由的服务节点。
+- 考虑到服务节点的个数以及 hash 算法的问题导致环中的数据分布不均匀时引入了虚拟节点。
+
+
+
+# 自定义有序 Map
+
+根据这些客观条件我们很容易想到通过自定义一个**有序**数组来模拟这个环。
+
+这样我们的流程如下:
+
+1. 初始化一个长度为 N 的数组。
+2. 将服务节点通过 hash 算法得到的正整数,同时将节点自身的数据(hashcode、ip、端口等)存放在这里。
+3. 完成节点存放后将整个数组进行排序(排序算法有多种)。
+4. 客户端获取路由节点时,将自身进行 hash 也得到一个正整数;
+5. 遍历这个数组直到找到一个数据大于等于当前客户端的 hash 值,就将当前节点作为该客户端所路由的节点。
+6. 如果没有发现比客户端大的数据就返回第一个节点(满足环的特性)。
+
+先不考虑排序所消耗的时间,单看这个路由的时间复杂度:
+- 最好是第一次就找到,时间复杂度为`O(1)`。
+- 最差为遍历完数组后才找到,时间复杂度为`O(N)`。
+
+理论讲完了来看看具体实践。
+
+我自定义了一个类:`SortArrayMap`
+
+他的使用方法及结果如下:
+
+
+
+
+
+可见最终会按照 `key` 的大小进行排序,同时传入 `hashcode = 101` 时会按照顺时针找到 `hashcode = 1000` 这个节点进行返回。
+
+----
+下面来看看具体的实现。
+
+成员变量和构造函数如下:
+
+
+
+其中最核心的就是一个 `Node` 数组,用它来存放服务节点的 `hashcode` 以及 `value` 值。
+
+其中的内部类 `Node` 结构如下:
+
+
+
+----
+
+写入数据的方法如下:
+
+
+
+相信看过 `ArrayList` 的源码应该有印象,这里的写入逻辑和它很像。
+
+- 写入之前判断是否需要扩容,如果需要则复制原来大小的 1.5 倍数组来存放数据。
+- 之后就写入数组,同时数组大小 +1。
+
+但是存放时是按照写入顺序存放的,遍历时自然不会有序;因此提供了一个 `Sort` 方法,可以把其中的数据按照 `key` 其实也就是 `hashcode` 进行排序。
+
+
+
+排序也比较简单,使用了 `Arrays` 这个数组工具进行排序,它其实是使用了一个 `TimSort` 的排序算法,效率还是比较高的。
+
+最后则需要按照一致性 Hash 的标准顺时针查找对应的节点:
+
+
+
+代码还是比较简单清晰的;遍历数组如果找到比当前 key 大的就返回,没有查到就取第一个。
+
+这样就基本实现了一致性 Hash 的要求。
+
+> ps:这里并不包含具体的 hash 方法以及虚拟节点等功能(具体实现请看下文),这个可以由使用者来定,SortArrayMap 可作为一个底层的数据结构,提供有序 Map 的能力,使用场景也不局限于一致性 Hash 算法中。
+
+# TreeMap 实现
+
+`SortArrayMap` 虽说是实现了一致性 hash 的功能,但效率还不够高,主要体现在 `sort` 排序处。
+
+下图是目前主流排序算法的时间复杂度:
+
+
+
+最好的也就是 `O(N)` 了。
+
+这里完全可以换一个思路,不用对数据进行排序;而是在写入的时候就排好顺序,只是这样会降低写入的效率。
+
+比如二叉查找树,这样的数据结构 `jdk` 里有现成的实现;比如 `TreeMap` 就是使用红黑树来实现的,默认情况下它会对 key 进行自然排序。
+
+---
+
+来看看使用 `TreeMap` 如何来达到同样的效果。
+
+运行结果:
+
+```
+127.0.0.1000
+```
+
+效果和上文使用 `SortArrayMap` 是一致的。
+
+只使用了 TreeMap 的一些 API:
+
+- 写入数据候,`TreeMap` 可以保证 key 的自然排序。
+- `tailMap` 可以获取比当前 key 大的部分数据。
+- 当这个方法有数据返回时取第一个就是顺时针中的第一个节点了。
+- 如果没有返回那就直接取整个 `Map` 的第一个节点,同样也实现了环形结构。
+
+> ps:这里同样也没有 hash 方法以及虚拟节点(具体实现请看下文),因为 TreeMap 和 SortArrayMap 一样都是作为基础数据结构来使用的。
+
+## 性能对比
+
+为了方便大家选择哪一个数据结构,我用 `TreeMap` 和 `SortArrayMap` 分别写入了一百万条数据来对比。
+
+先是 `SortArrayMap`:
+
+
+
+**耗时 2237 毫秒。**
+
+TreeMap:
+
+
+
+**耗时 1316毫秒。**
+
+结果是快了将近一倍,所以还是推荐使用 `TreeMap` 来进行实现,毕竟它不需要额外的排序损耗。
+
+# cim 中的实际应用
+
+下面来看看在 `cim` 这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。
+
+## 模板方法
+
+在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展自己的一致性 hash 算法因此我定义了一个抽象类;其中定义了一些模板方法,这样大家只需要在子类中进行不同的实现即可完成自己的算法。
+
+AbstractConsistentHash,这个抽象类的主要方法如下:
+
+
+
+- `add` 方法自然是写入数据的。
+- `sort` 方法用于排序,但子类也不一定需要重写,比如 `TreeMap` 这样自带排序的容器就不用。
+- `getFirstNodeValue` 获取节点。
+- `process` 则是面向客户端的,最终只需要调用这个方法即可返回一个节点。
+
+
+下面我们来看看利用 `SortArrayMap` 以及 `AbstractConsistentHash` 是如何实现的。
+
+
+
+就是实现了几个抽象方法,逻辑和上文是一样的,只是抽取到了不同的方法中。
+
+只是在 add 方法中新增了几个虚拟节点,相信大家也看得明白。
+
+> 把虚拟节点的控制放到子类而没有放到抽象类中也是为了灵活性考虑,可能不同的实现对虚拟节点的数量要求也不一样,所以不如自定义的好。
+
+但是 `hash` 方法确是放到了抽象类中,子类不用重写;因为这是一个基本功能,只需要有一个公共算法可以保证他散列地足够均匀即可。
+
+因此在 `AbstractConsistentHash` 中定义了 hash 方法。
+
+
+
+> 这里的算法摘抄自 xxl_job,网上也有其他不同的实现,比如 `FNV1_32_HASH` 等;实现不同但是目的都一样。
+
+---
+
+这样对于使用者来说就非常简单了:
+
+
+
+他只需要构建一个服务列表,然后把当前的客户端信息传入 `process` 方法中即可获得一个一致性 hash 算法的返回。
+
+
+
+---
+
+同样的对于想通过 `TreeMap` 来实现也是一样的套路:
+
+
+
+他这里不需要重写 sort 方法,因为自身写入时已经排好序了。
+
+而在使用时对于客户端来说只需求修改一个实现类,其他的啥都不用改就可以了。
+
+
+
+运行的效果也是一样的。
+
+这样大家想自定义自己的算法时只需要继承 `AbstractConsistentHash` 重写相关方法即可,**客户端代码无须改动。**
+
+## 路由算法扩展性
+
+但其实对于 `cim` 来说真正的扩展性是对路由算法来说的,比如它需要支持轮询、hash、一致性hash、随机、LRU等。
+
+只是一致性 hash 也有多种实现,他们的关系就如下图:
+
+
+
+应用还需要满足对这一类路由策略的灵活支持,比如我也想自定义一个随机的策略。
+
+因此定义了一个接口:`RouteHandle`
+
+```java
+public interface RouteHandle {
+
+ /**
+ * 再一批服务器里进行路由
+ * @param values
+ * @param key
+ * @return
+ */
+ String routeServer(List values,String key) ;
+}
+```
+
+其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息即可。
+
+而对于一致性 hash 算法来说也是只需要实现这个接口,同时在这个接口中选择使用 `SortArrayMapConsistentHash` 还是 `TreeMapConsistentHash` 即可。
+
+
+
+这里还有一个 `setHash` 的方法,入参是 AbstractConsistentHash;这就是用于客户端指定需要使用具体的那种数据结构。
+
+---
+
+而对于之前就存在的轮询策略来说也是同样的实现 `RouteHandle` 接口。
+
+
+
+这里我只是把之前的代码搬过来了而已。
+
+
+接下来看看客户端到底是如何使用以及如何选择使用哪种算法。
+
+> 为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。
+
+
+
+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` 的时候完成的。
+
+
+
+也比较简单,需要读取之前的配置文件来动态生成具体的实现类,主要是利用反射完成的。
+
+这样处理之后就比较灵活了,比如想新建一个随机的路由策略也是同样的套路;到时候只需要修改配置即可。
+
+> 感兴趣的朋友也可提交 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 @@
+
+
+
+# 前言
+
+最近有朋友问我这么一个面试题目:
+
+> 现在有一个非常庞大的数据,假设全是 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 数据试试:
+
+
+
+执行后马上就内存溢出。
+
+
+
+可见在内存有限的情况下我们不能使用这种方式。
+
+实际情况也是如此;既然要判断一个数据是否存在于集合中,考虑的算法的效率以及准确性肯定是要把数据全部 `load` 到内存中的。
+
+
+# Bloom Filter
+
+基于上面分析的条件,要实现这个需求最需要解决的是`如何将庞大的数据 load 到内存中。`
+
+而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。
+
+伟大的科学家们已经帮我们想到了这样的需求。
+
+`Burton Howard Bloom` 在 1970 年提出了一个叫做 `Bloom Filter`(中文翻译:布隆过滤)的算法。
+
+它主要就是用于解决判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。
+
+所以在这个场景下在合适不过了。
+
+## Bloom Filter 原理
+
+下面来分析下它的实现原理。
+
+> 官方的说法是:它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。
+
+听起来比较绕,但是通过一个图就比较容易理解了。
+
+
+
+如图所示:
+
+- 首先需要初始化一个二进制的数组,长度设为 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));
+ }
+```
+
+执行结果如下:
+
+
+
+只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。
+
+---
+
+
+
+当让我把数组长度缩小到了 100W 时就出现了一个误报,`400230340` 这个数明明没在集合里,却返回了存在。
+
+这也体现了 `Bloom Filter` 的误报率。
+
+我们提高数组长度以及 `hash` 计算次数可以降低误报率,但相应的 `CPU、内存`的消耗就会提高;这就需要根据业务需要自行权衡。
+
+
+# Guava 实现
+
+
+
+刚才的方式虽然实现了功能,也满足了大量数据。但其实观察 `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 的数据,执行没有问题。
+
+
+
+观察 GC 日志会发现没有一次 `fullGC`,同时老年代的使用率很低。和刚才的一对比这里明显的要好上很多,也可以写入更多的数据。
+
+## 源码分析
+
+那就来看看 `Guava` 它是如何实现的。
+
+构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。
+我这里的测试 demo 分别是 1000W 以及 0.01。
+
+
+
+`Guava` 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 `numBits` 以及需要计算几次 Hash 函数 `numHashFunctions` 。
+
+这个算法计算规则可以参考维基百科。
+
+### put 写入函数
+
+真正存放数据的 `put` 函数如下:
+
+
+
+- 根据 `murmur3_128` 方法的到一个 128 位长度的 `byte[]`。
+- 分别取高低 8 位的到两个 `hash` 值。
+- 再根据初始化时的到的执行 `hash` 的次数进行 `hash` 运算。
+
+
+```java
+bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
+```
+
+其实也是 `hash取模`拿到 `index` 后去赋值 1.
+
+重点是 `bits.set()` 方法。
+
+
+
+其实 set 方法是 `BitArray` 中的一个函数,`BitArray` 就是真正存放数据的底层数据结构。
+
+利用了一个 `long[] data` 来存放数据。
+
+所以 `set()` 时候也是对这个 `data` 做处理。
+
+
+
+- 在 `set` 之前先通过 `get()` 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。
+- 接下来就是通过位运算进行`位或赋值`。
+- `get()` 方法的计算逻辑和 set 类似,只要判断为 0 就直接返回存在该值。
+
+### mightContain 是否存在函数
+
+
+
+前面几步的逻辑都是类似的,只是调用了刚才的 `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 @@
+# 设计一个百万级的消息推送系统
+
+
+
+# 前言
+
+首先迟到的祝大家中秋快乐。
+
+最近一周多没有更新了。其实我一直想憋一个大招,分享一些大家感兴趣的干货。
+
+鉴于最近我个人的工作内容,于是利用这三天小长假憋了一个出来(其实是玩了两天🤣)。
+
+
+---
+
+先简单说下本次的主题,由于我最近做的是物联网相关的开发工作,其中就不免会遇到和设备的交互。
+
+最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。
+
+所以本次分享的内容不但可以满足物联网领域同时还支持以下场景:
+
+- 基于 `WEB` 的聊天系统(点对点、群聊)。
+- `WEB` 应用中需求服务端推送的场景。
+- 基于 SDK 的消息推送平台。
+
+# 技术选型
+
+要满足大量的连接数、同时支持双全工通信,并且性能也得有保障。
+
+在 Java 技术栈中进行选型首先自然是排除掉了传统 `IO`。
+
+那就只有选 NIO 了,在这个层面其实选择也不多,考虑到社区、资料维护等方面最终选择了 Netty。
+
+最终的架构图如下:
+
+
+
+
+现在看着蒙没关系,下文一一介绍。
+
+# 协议解析
+
+既然是一个消息系统,那自然得和客户端定义好双方的协议格式。
+
+常见和简单的是 HTTP 协议,但我们的需求中有一项需要是双全工的交互方式,同时 HTTP 更多的是服务于浏览器。我们需要的是一个更加精简的协议,减少许多不必要的数据传输。
+
+因此我觉得最好是在满足业务需求的情况下定制自己的私有协议,在我这个场景下其实有标准的物联网协议。
+
+如果是其他场景可以借鉴现在流行的 `RPC` 框架定制私有协议,使得双方通信更加高效。
+
+不过根据这段时间的经验来看,不管是哪种方式都得在协议中预留安全相关的位置。
+
+协议相关的内容就不过讨论了,更多介绍具体的应用。
+
+# 简单实现
+
+首先考虑如何实现功能,再来思考百万连接的情况。
+
+## 注册鉴权
+
+在做真正的消息上、下行之前首先要考虑的就是鉴权问题。
+
+就像你使用微信一样,第一步怎么也得是登录吧,不能无论是谁都可以直接连接到平台。
+
+所以第一步得是注册才行。
+
+如上面架构图中的 `注册/鉴权` 模块。通常来说都需要客户端通过 `HTTP` 请求传递一个唯一标识,后台鉴权通过之后会响应一个 `token`,并将这个 `token` 和客户端的关系维护到 `Redis` 或者是 DB 中。
+
+客户端将这个 token 也保存到本地,今后的每一次请求都得带上这个 token。一旦这个 token 过期,客户端需要再次请求获取 token。
+
+鉴权通过之后客户端会直接通过`TCP 长连接`到图中的 `push-server` 模块。
+
+这个模块就是真正处理消息的上、下行。
+
+## 保存通道关系
+
+在连接接入之后,真正处理业务之前需要将当前的客户端和 Channel 的关系维护起来。
+
+假设客户端的唯一标识是手机号码,那就需要把手机号码和当前的 Channel 维护到一个 Map 中。
+
+这点和之前 [SpringBoot 整合长连接心跳机制](http://t.cn/EPcNHFZ) 类似。
+
+
+
+同时为了可以通过 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()` 方法中。
+
+
+
+在这里可以解析消息,区分类型。
+
+但如果我们的业务逻辑也写在里面,那这里的内容将是巨多无比。
+
+甚至我们分为好几个开发来处理不同的业务,这样将会出现许多冲突、难以维护等问题。
+
+所以非常有必要将消息解析与业务处理完全分离开来。
+
+
+> 这时面向接口编程就发挥作用了。
+
+这里的核心代码和 [「造个轮子」——cicada(轻量级 WEB 框架)](https://crossoverjie.top/2018/09/03/wheel/cicada1/#%E9%85%8D%E7%BD%AE%E4%B8%9A%E5%8A%A1-Action) 是一致的。
+
+都是先定义一个接口用于处理业务逻辑,然后在解析消息之后通过反射创建具体的对象执行其中的`处理函数`即可。
+
+这样不同的业务、不同的开发人员只需要实现这个接口同时实现自己的业务逻辑即可。
+
+伪代码如下:
+
+
+
+
+
+想要了解 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://github.com/crossoverJie/netty-action/](https://github.com/crossoverJie/netty-action/)
+
+
+# 分布式方案
+
+单机版的实现了,现在着重讲讲如何实现百万连接。
+
+百万连接其实只是一个形容词,更多的是想表达如何来实现一个分布式的方案,可以灵活的水平拓展从而能支持更多的连接。
+
+再做这个事前首先得搞清楚我们单机版的能支持多少连接。影响这个的因素就比较多了。
+
+- 服务器自身配置。内存、CPU、网卡、Linux 支持的最大文件打开数等。
+- 应用自身配置,因为 Netty 本身需要依赖于堆外内存,但是 JVM 本身也是需要占用一部分内存的,比如存放通道关系的大 `Map`。这点需要结合自身情况进行调整。
+
+结合以上的情况可以测试出单个节点能支持的最大连接数。
+
+单机无论怎么优化都是有上限的,这也是分布式主要解决的问题。
+
+## 架构介绍
+
+在将具体实现之前首先得讲讲上文贴出的整体架构图。
+
+
+
+先从左边开始。
+
+上文提到的 `注册鉴权` 模块也是集群部署的,通过前置的 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 中的节点,从而可以获取最新的服务列表。结构如下:
+
+
+
+以下是一些伪代码:
+
+应用启动注册 Zookeeper。
+
+
+
+
+
+对于`注册鉴权`模块来说只需要订阅这个 Zookeeper 节点:
+
+
+
+### 路由策略
+
+既然能获取到所有的服务列表,那如何选择一台刚好合适的 `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 中的数据。
+
+伪代码如下:
+
+
+
+这里存放路由关系的时候会有并发问题,最好是换为一个 `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 这些工具都得用起来才行。
+
+# 总结
+
+本次是结合我日常经验得出的,有些坑可能在工作中并没有踩到,所有还会有一些遗漏的地方。
+
+就目前来看想做一个稳定的推送系统其实是比较麻烦的,其中涉及到的点非常多,只有真正做过之后才会知道。
+
+看完之后觉得有帮助的还请不吝转发分享。
+
+**欢迎关注公众号一起交流:**
+
+
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 @@
+
+
+## 前言
+
+之前在 [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)
+
+最终架构图:
+
+
+
+
+
+先简单根据这个图谈下请求的流转,因为后面不管怎么改进这个都是没有变的。
+
+- 前端请求进入 `web` 层,对应的代码就是 `controller`。
+- 之后将真正的库存校验、下单等请求发往 `Service` 层(其中 RPC 调用依然采用的 `dubbo`,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 [基于dubbo的分布式架构](https://crossoverjie.top/%2F2017%2F04%2F07%2FSSM11%2F))。
+- `Service` 层再对数据进行落地,下单完成。
+
+
+## 无限制
+
+其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步:
+
+- 校验库存
+- 扣库存
+- 创建订单
+- 支付
+
+基于上文的架构所以我们有了以下实现:
+
+先看看实际项目的结构:
+
+
+
+还是和以前一样:
+
+- 提供出一个 `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` 接口发现:
+
+库存表:
+
+
+订单表:
+
+
+一切看起来都没有问题,数据也正常。
+
+但是当用 `JMeter` 并发测试时:
+
+
+
+测试配置是:300个线程并发,测试两轮来看看数据库中的结果:
+
+
+
+
+
+
+
+请求都响应成功,库存确实也扣完了,但是订单却生成了 **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`:
+
+
+
+
+
+
+
+这次发现无论是库存订单都是 OK 的。
+
+查看日志发现:
+
+
+
+很多并发请求会响应错误,这就达到了效果。
+
+### 提高吞吐量
+
+为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 Service 都进行了横向扩展。
+
+- web 利用 Nginx 进行负载。
+- Service 也是多台应用。
+
+
+
+
+
+再用 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 是两个应用。
+
+
+
+
+
+
+数据库也有 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`:
+
+
+
+
+
+
+
+
+
+
+
+首先是看结果没有问题,再看数据库连接以及并发请求数都有**明显的下降**。
+
+
+## 乐观锁更新 + 分布式限流 + Redis 缓存
+
+其实仔细观察 Druid 监控数据发现这个 SQL 被多次查询:
+
+
+
+其实这是实时查询库存的 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`:
+
+
+
+
+
+
+
+
+
+最后发现数据没问题,数据库的请求与并发也都下来了。
+
+
+
+## 乐观锁更新 + 分布式限流 + 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 0) {
+ // be like clone(), allocate array based upon size not capacity
+ ensureCapacityInternal(size);
+
+ Object[] a = elementData;
+ // Read in all elements in the proper order.
+ for (int i=0; i 当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。
+
+
+从实现中可以看出 ArrayList 只序列化了被使用的数据。
+
+
+## Vector
+
+`Vector` 也是实现于 `List` 接口,底层数据结构和 `ArrayList` 类似,也是一个动态数组存放数据。不过是在 `add()` 方法的时候使用 `synchronized` 进行同步写数据,但是开销较大,所以 `Vector` 是一个同步容器并不是一个并发容器。
+
+以下是 `add()` 方法:
+```java
+ public synchronized boolean add(E e) {
+ modCount++;
+ ensureCapacityHelper(elementCount + 1);
+ elementData[elementCount++] = e;
+ return true;
+ }
+```
+
+以及指定位置插入数据:
+```java
+ public void add(int index, E element) {
+ insertElementAt(element, index);
+ }
+ public synchronized void insertElementAt(E obj, int index) {
+ modCount++;
+ if (index > elementCount) {
+ throw new ArrayIndexOutOfBoundsException(index
+ + " > " + elementCount);
+ }
+ ensureCapacityHelper(elementCount + 1);
+ System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
+ elementData[index] = obj;
+ elementCount++;
+ }
+```
+
+
+
diff --git a/docs/collections/HashMap.md b/docs/collections/HashMap.md
new file mode 100644
index 00000000..a038f075
--- /dev/null
+++ b/docs/collections/HashMap.md
@@ -0,0 +1,77 @@
+**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。**
+
+# HashMap 底层分析
+
+> 以下基于 JDK1.7 分析。
+
+
+
+如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数:
+
+- 容量
+- 负载因子
+
+容量的默认大小是 16,负载因子是 0.75,当 `HashMap` 的 `size > 16*0.75` 时就会发生扩容(容量和负载因子都可以自由调整)。
+
+## put 方法
+首先会将传入的 Key 做 `hash` 运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。
+
+由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为 `2^n` 。这样用 `2^n - 1` 做位运算与取模效果一致,并且效率还要高出许多。
+
+由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 `table[index]`处形成链表,采用头插法将数据插入到链表中。
+
+## get 方法
+
+get 和 put 类似,也是将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 `key.equals(k)` 来找到对应的元素。
+
+## 遍历方式
+
+
+```java
+ Iterator> 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` 正好是环形链表的下标时就会出现死循环。
+
+
+
+> 所以 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` 的组成:
+
+
+
+
+打开源码可以看到:
+
+```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 总结成一张图如下:
+
+
+
+第一个类似于 `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 底层分析
+
+
+
+如图所示 `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)
+
+[](https://jq.qq.com/?_wv=1027&k=5HPYvQk)
+
+**欢迎我的关注公众号一起交流:**
+
+
+
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 的数据结构
+
+
+
+如图所示是 `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 @@
+
+
+# 前言
+
+之前不少人问我“能否分享一些分库分表相关的实践”,其实不是我不分享,而是真的经验不多🤣;和大部分人一样都是停留在理论阶段。
+
+
+不过这次多少有些可以说道了。
+
+先谈谈背景,我们生产数据库随着业务发展量也逐渐起来;好几张单表已经突破**亿级**数据,并且保持每天 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 @@
+
+
+## 前言
+
+本文接着上文[应用限流](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 应用。
+
+
+
+效果如下:
+
+
+
+
+
+
+
+
+## 实现原理
+实现原理其实很简单。既然要达到分布式全局限流的效果,那自然需要一个第三方组件来记录请求的次数。
+
+其中 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 @@
+
+
+## 前言
+分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三。
+
+首先谈到分布式锁自然也就联想到分布式应用。
+
+在我们将应用拆分为分布式应用之前的单机系统中,对一些并发场景读取公共资源时如扣库存,卖车票之类的需求可以简单的使用[同步](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 的话可以很直接的看出来:
+
+
+
+这里我们所依赖的 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` 方法动态创建代理类。第一个参数为类加载器,第二个参数为代理类需要实现的接口列表,最后一个则是处理器。
+
+其实代理类是由
+
+
+
+这个方法动态创建出来的。将 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 @@
+
+
+## 前言
+
+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)
+
+
+
+
+为了能看出 Guava 是怎么删除过期数据的在获取缓存之前休眠了 5 秒钟,达到了超时条件。
+
+
+
+最终会发现在 `com.google.common.cache.LocalCache` 类的 2187 行比较关键。
+
+再跟进去之前第 2182 行会发现先要判断 count 是否大于 0,这个 count 保存的是当前缓存的数量,并用 volatile 修饰保证了可见性。
+
+> 更多关于 volatile 的相关信息可以查看 [你应该知道的 volatile 关键字](https://crossoverjie.top/%2F2018%2F03%2F09%2Fvolatile%2F)
+
+
+接着往下跟到:
+
+
+
+2761 行,根据方法名称可以看出是判断当前的 Entry 是否过期,该 entry 就是通过 key 查询到的。
+
+
+
+
+这里就很明显的看出是根据根据构建时指定的过期方式来判断当前 key 是否过期了。
+
+
+
+如果过期就往下走,尝试进行过期删除(需要加锁,后面会具体讨论)。
+
+
+
+到了这里也很清晰了:
+
+- 获取当前缓存的总数量
+- 自减一(前面获取了锁,所以线程安全)
+- 删除并将更新的总数赋值到 count。
+
+其实大体上就是这个流程,Guava 并没有按照之前猜想的另起一个线程来维护过期数据。
+
+应该是以下原因:
+
+- 新起线程需要资源消耗。
+- 维护过期数据还要获取额外的锁,增加了消耗。
+
+而在查询时候顺带做了这些事情,但是如果该缓存迟迟没有访问也会存在数据不能被回收的情况,不过这对于一个高吞吐的应用来说也不是问题。
+
+## 总结
+
+最后再来总结下 Guava 的 Cache。
+
+其实在上文跟代码时会发现通过一个 key 定位数据时有以下代码:
+
+
+
+如果有看过 [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()
+
+来自定义键和值的引用关系。
+
+
+
+在上文的分析中可以看出 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();` 的实现:
+
+
+
+具有强引用和弱引用的不同实现。
+
+key 也是相同的道理:
+
+
+
+当使用这样的构造方式时,弱引用的 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