Silently9527

Silently9527 查看完整档案

成都编辑华北理工学院  |  软件工程 编辑成都某在线教育公司  |  JAVA 编辑 silently9527.cn 编辑
编辑

个人动态

Silently9527 发布了文章 · 2月24日

程序员常用的IDEA插件ToolSet版本更新啦

完全开源的淘客项目:https://github.com/silently9527/mall-coupons-server

微信公众号:贝塔学Java

前言

自己在开发的过程中经常会使用一些在线的工具,比如:时间戳转日期,JSON格式化等等;前几天思考了下想把这些常用的功能都做成IDEA插件,在使用的时候就不用去网上寻找工具,在IDEA中就可以快速完成提升开发人员开发效率,所以就熬夜肝了这个插件,欢迎大家都来使用。

Github地址: https://github.com/silently95...

Gitee地址: https://gitee.com/silently952...

觉得好用的小伙伴记得小手一抖 star 哟

版本更新

  • 修复了大家反馈的一些问题
  • 新增了md5加密功能
  • 新增了生成二维码,下载二维码,插入logo功能
  • 弹窗位置从右侧改到了下边

已实现功能

  • [x] SQL 转换成 ElasticSearch 查询语句
  • [x] 正则表达式
  • [x] Base64编码/解码
  • [x] JSON格式化
  • [x] URL编码/解码
  • [x] 手机号归属地
  • [x] IP地址
  • [x] 日期时间戳互转
  • [x] MD5
  • [x] 生成二维码

计划中的功能

  • [ ] Cron表达式
  • [ ] 图片base64编码
  • [ ] UUID生成器
  • [ ] 文件下载
  • [ ] js/css混淆压缩
  • [ ] 标签页支持手动设置

点关注,不迷路

白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

查看原文

赞 3 收藏 1 评论 5

Silently9527 发布了文章 · 2月22日

常见的初级排序算法,这次全搞懂

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

程序员常用的IDEA插件:https://github.com/silently9527/ToolsetIdeaPlugin

完全开源的淘客项目:https://github.com/silently9527/mall-coupons-server

微信公众号:贝塔学Java

前言

相信所有的程序员刚开始接触到的算法都会是排序算法,因为排序在对数据处理和计算有这重要的地位,排序算法往往是其他算法的基础;本文我们就先从初级排序算法开始学习算法。

排序算法的模板

在开始之前我们先定义一个排序算法通用的模板,在后面的排序算法都会实现这个模板

public interface SortTemplate {

    void sort(Comparable[] array);

    default void print(Comparable[] array) {
        for (Comparable a : array) {
            System.out.print(a + " ");
        }
    }

    default boolean less(Comparable a, Comparable b) {
        return a.compareTo(b) < 0;
    }

    default void exch(Comparable[] array, int i, int j) {
        Comparable tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    
}
  • Comparable: 为了让我们实现的排序算法更加的通用,可以排序任意的对象,所以我们这里使用了Comparable数组
  • sort: 不同的排序算法实现的方式不一样,子类自己去实现
  • less: 定义的公用方法,如果a < b就返回true
  • exch: 定义的公用方法,交换数组中的两个对象
  • print: 打印出数据中的每个元素

选择排序

算法实现的思路:

  • 首先找到数组中的最小元素,
  • 其实将它和数组中的第一个元素进行交换,这样就排定了一个元素;
  • 再次找出剩余元素中最小的元素与数组中的第二个元素进行交换,如此反复直到所有元素都是有序的

代码实现:

public class SelectionSort implements SortTemplate {

    @Override
    public void sort(Comparable[] array) {
        int length = array.length;
        for (int i = 0; i < length; i++) {
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (less(array[j], array[min])) {
                    min = j;
                }
            }
            exch(array, i, min);
        }
    }

}

假如输入的数组是有序的,我们会发现选择排序运行的时候和未排序的时间一样长!

对于N个元素的数组,使用选择排序的时间复杂度是O(n²)

选择排序的是数据移动最少的,交换的次数与数组的大小是线性关系,N个元素的数组需要N次交换

冒泡排序

算法实现的思路:

  • 比较相邻的两个元素,如果前一个比后一个大,那么就交换两个元素的位置
  • 对每一组相邻的元素执行同样的操作,直到最后一个元素,操作完成之后就可以排定一个最大的元素
  • 如此往复,直到数组中所有的元素都有序

代码实现:

public class BubbleSort implements SortTemplate {

    @Override
    public void sort(Comparable[] array) {
        int length = array.length - 1;
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length - i; j++) {
                if (less(array[j + 1], array[j])) {
                    exch(array, j, j + 1);
                }
            }
        }
    }

}

对于N个元素的数组,使用冒泡排序的时间复杂度是O(n²)

插入排序

想象我们在玩扑克牌时,整理扑克牌都是把每一张插入到左边已经排好序的牌中适当的位置。插入排序的思路类似

算法实现的思路:

  • 初始默认第一个元素就是有序的,当前索引的位置从0开始
  • 先后移动当前索引的位置,当前索引位置左边的元素是有序的,从后往前开始扫码与当前索引位置元素进行比较
  • 当确定当前索引位置上的元素在左边有序适合的位置之后,插入到该位置上
  • 如果当确定当前索引位置上的元素大于了已排序的最后一个元素,那么当前索引位置直接往后移动
  • 如此反复,直到所有元素有序

代码实现:

public class InsertionSort implements SortTemplate {

    @Override
    public void sort(Comparable[] array) {
        int length = array.length;
        for (int i = 1; i < length; i++) {
            for (int j = i; j > 0 && less(array[j], array[j - 1]); j--) {
                exch(array, j, j - 1);
            }
        }
    }

}

从代码的实现我们可以看出,当遇到了当前索引的元素大于了左边有序数组的最后一个元素时,内层循环就直接结束了,所以所我们排序的数组中存在着部分有序,那么插入排序算法会很快。

考虑最糟糕的情况,如果输入数组是一个倒置的,那么插入排序的效率和选择排序一样,时间复杂度是O(n²)

希尔排序

对于大规模的乱序数组插入排序很慢,是因为它只交换相邻的元素,元素只能一点一点的从数组中移动到正确的位置;插入排序对于部分有序的数组排序是的效率很高;

希尔排序基于这两个特点对插入排序进行了改进;

算法实现的思路

  • 首先设置一个步长h用来分隔出子数组
  • 用插入排序将h个子数组独立排序
  • 减小h步长继续排序子数组,直到h步长为1
  • 当步长为1时就成了普通的插入排序,这样数组一定是有序的

希尔排序高效的原因,在排序之初,各个子数组都很短,子数组排序之后都是部分有序的,这两种情况都很适合插入排序。

代码实现:

public class ShellSort implements SortTemplate {

    @Override
    public void sort(Comparable[] array) {
        int gap = 1;
        int length = array.length;

        while (gap < length / 3) {
            gap = 3 * gap + 1;
        }

        while (gap >= 1) {
            for (int i = gap; i < length; i++) {
                for (int j = i; j >= gap && less(array[j], array[j - gap]); j -= gap) {
                    exch(array, j, j - gap);
                }
            }
            gap = gap / 3;
        }
    }

}

最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,写作不易,请不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

文中所有源码已放入到了github仓库https://github.com/silently9527/JavaCore

图片来源于网络
参考书籍:算法第四版

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2月18日

如何检测社交网络中两个人是否是朋友关系(union-find算法)

前言

春节放假会了老家,停更了很多天,这是年后连夜肝出来的第一篇文章,先来聊聊春节放假期间发生的事,这次回家遇到了我学生时代的女神,当年她在我心目中那是

"出淤泥而不染、濯清涟而不妖"

没想到这次回家遇到了她,身体发福了,心目中女神的形象瞬间碎了,就好像达芬奇再次遇到了蒙娜丽莎

"菡萏香销翠叶残"

好了,言归正传。

有时候我们可以需要判断在大型网络中两台计算机是否相连,是否需要建立一条新的连接才能通信;或者是在社交网络中判断两个人是否是朋友关系(相连表示是朋友关系)。在这种应用中,通常我们可能需要处理数百万的对象和数亿的连接,如何能够快速的判断出是否相连呢?这就需要使用到union-find算法

概念

相连

假如输入一对整数,其中每个数字表示的是某种对象(人、地址或者计算机等等),整数对p,q理解为“p与q相连”,相连具有以下特性:

  • 自反性:p与p是相连的
  • 对称性:如果p与q相连,那么q与p相连
  • 传递性:如果p与q相连,q与r相连,那么p与r也相连
对象如何与数字关联起来,后面我们聊到一种算法符号表

等价类

假设相连是一个种等价关系,那么等价关系能够将对象划分为多个等价类,在该算法中,当且仅当两个对象相连时他们才属于同一个等价类

触点

整个网络中的某种对象称为触点

连通分量

将整数对称为连接,将等价类称作连通分量或者简称分量

动态连通性

union-find算法的目标是当程序从输入中读取了整数对p q时,如果已知的所有整数对都不能说明p q是相连的,那么将这一对整数输出,否则忽略掉这对整数;我们需要设计数据结构来保存已知的所有整数对的信息,判断出输入的整数对是否是相连的,这种问题叫做动态连通性问题。

union-find算法API定义

public interface UF {
    void union(int p, int q); //在p与q之间添加一条连接

    int find(int p); //返回p所在分量的标识符

    boolean connected(int p, int q); //判断出p与q是否存在于同一个分量中

    int count(); //统计出连通分量的数量
}

如果两个触点在不同的分量中,union操作会使两个分量归并。一开始我们有N个分量(每个触点表示一个分量),将两个分量归并之后数量减一。

抽象实现如下:

public abstract class AbstractUF implements UF {
    protected int[] id;
    protected int count;

    public AbstractUF(int N) {
        count = N;

        id = new int[N];
        for (int i = 0; i < N; i++) {
            id[i] = i;
        }
    }

    @Override
    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    @Override
    public int count() {
        return count;
    }
}

接下来我们就主要来讨论如何实现union方法和find方法

quick-find算法

这种算法的实现思路是在同一个连通分量中所有触点在id[]中的值都是相同的,判断是否连通的connected的方法就是判断id[p]是否等于id[q]。

public class QuickFindImpl extends AbstractUF {
    public QuickFindImpl(int N) {
        super(N);
    }

    @Override
    public int find(int p) {
        return id[p];
    }

    @Override
    public void union(int p, int q) {
        int pId = find(p);
        int qId = find(q);

        if (pId == qId) { //如果相等表示p与q已经属于同一分量中
            return;
        }

        for (int i = 0; i < id.length; i++) {
            if (id[i] == pId) {
                id[i] = qId; //把分量中所有的值都统一成qId
            }
        }
        count--; //连通分量数减一
    }

}
  • 算法分析:

find()操作显然是很快的,时间复杂度O(1), 但是union的算法是无法处理大型数据的,因为每次都需要变量整个数组,那么union方法的时间复杂度是O(n)

quick-union算法

为了提高union方法的速度,我们需要考虑另外一种算法;使用同样的数据结构,只是重新定义id[]表示的意义,每个触点所对应的id[]值都是在同一分量中的另一个触点的名称

在数组初始化之后,每个节点的链接都指向自己;id[]数组用父链接的形式表示了森林,每一次union操作都会找出每个分量的根节点进行归并。

public class QuickUnionImpl extends AbstractUF {
    public QuickUnionImpl(int N) {
        super(N);
    }

    @Override
    public int find(int p) {
        //找出p所在分量的根触点
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }

    @Override
    public void union(int p, int q) {
        int pRoot = find(p); //找出q p的根触点
        int qRoot = find(q);
        if (pRoot == qRoot) { //处于同一分量不做处理
            return;
        }
        id[pRoot] = qRoot; //根节点
        count--;
    }

}
  • 算法分析:

看起来quick-union算法比quick-find算法更快,因为union不需要为每对输入遍历整个数组,
考虑最佳情况下,find方法只需要访问一次数组就可以得到根触点,那么union方法的时间复杂度O(n);
考虑到最糟糕的输入情况,如下图:

find方法需要访问数组n-1次,那么union方法的时间复杂度是O(n²)

加权quick-union算法

为了保证quick-union算法最糟糕的情况不在出现,我需要记录每一个树的大小,在进行分量归并操作时总是把小的树连接到大的树上,这种算法构造出来树的高度会远远小于未加权版本所构造的树高度。

public class WeightedQuickUnionImpl extends AbstractUF {
    private int[] sz;

    public WeightedQuickUnionImpl(int N) {
        super(N);
        sz = new int[N];
        for (int i = 0; i < N; i++) {
            sz[i] = 1;
        }
    }

    @Override
    public void union(int p, int q) {
        int pRoot = find(p); //找出q p的根触点
        int qRoot = find(q);
        if (pRoot == qRoot) { //处于同一分量不做处理
            return;
        }
        //小树合并到大树
        if (sz[pRoot] < sz[qRoot]) {
            sz[qRoot] += sz[pRoot]; 
            id[pRoot] = qRoot;
        } else {
            sz[pRoot] += sz[qRoot];
            id[qRoot] = pRoot;
        }
        count--;
    }

    @Override
    public int find(int p) {
        //找出p所在分量的根触点
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }
}
  • 算法分析:

最坏的情况下,每次union归并的树都是大小相等的,他们都包含了2的n次方个节点,高度都是n,合并之后的高度变成了n+1,由此可以得出union方法的时间复杂度是O(lgN)

总结

union-find算法只能判断出给定的两个整数是否是相连的,无法给出具体达到的路径;后期我们聊到图算法可以给出具体的路径

算法union()find()
quick-find算法N1
quick-union算法树的高度树的高度
加权quick-union算法lgNlgN

最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,写作不易,请不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

文中所有源码已放入到了github仓库https://github.com/silently9527/JavaCore

参考书籍:算法第四版

程序员常用的IDEA插件:https://github.com/silently9527/ToolsetIdeaPlugin

完全开源的淘客项目:https://github.com/silently9527/mall-coupons-server

微信公众号:贝塔学Java

查看原文

赞 10 收藏 6 评论 0

Silently9527 发布了文章 · 2月8日

面试的季节到了,老哥确定不来复习下数据结构吗

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学Java

前言

在上一次《面试篇》Http协议中,面试官原本想的是http问的差不多了,想要继续问我JAVA相关的一些问题,结果我突然觉得嗓子不舒服咳嗽了几声,(在这个敏感的时候)吓退了面试官,面试官带起口罩后就说面试先暂时到这里,下次再聊;两周之后我又收到了HR的电话;

HR:感冒好了吗?上次面试的结果不错,你什么时候有时间来我们公司二面呢?

我:随时准备着

到公司后,我依然被带到了那个小黑屋,等待着面试官的到来。没想等来的是一位美女小姐姐。

我:人美声甜的小姐姐,你是本次的面试官?(我窃喜中)

美女面试官:是的,上次面试你的面试官开会去了,这次面试我来和你聊聊

面试官:看你这么会说话,让我先来帮你开个胃,说说二分查找吧

我:(果然是开胃啊,这位小姐姐竟然如此善良)

我:二分查找法是在一个有序的数组中查到一个值,如果存在就返回在数组中的索引,否则就返回-1;算法维护了两个变量lo(最小)和hi(最大),每次查找都与中间值(mid)进行比较,如果等于就返回mid,大于就查询右半边的数组,小于就查询左半边数组。二分查找法之所以快是因为每次一次查询都会排除掉一半的值。

No BB, show you the code(不废话,直接看代码)
public class BinarySearch {

    /**
     * 二分查找
     * @param key
     * @param arr
     * @return 存在返回对应的下标,不存在返回 -1
     */
    public static int search(int key, int[] arr) {
        int lo = 0, hi = arr.length - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (key > arr[mid]) {
                lo = mid + 1;
            } else if (key < arr[mid]) {
                hi = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

}

对于一个包含n个元素的列表,用二分查找法最多需要log2n(前面的2是底数)次就可以判断出元素是否存在;所以二分查找法的时间复杂度是O(log n)

面试官:说说使用数组如何实现栈?

我:小姐姐,栈的特点就是后进先出,使用数组和链表都可以实现栈的功能,首先看下栈的定义:

public interface Stack<T> extends Iterable {
    void push(T item); //向栈添加元素
    T pop(); //从栈中弹出
    boolean isEmpty();
    int size(); //返回元素的个数
}



栈在使用的时候有可能也会遍历全部的元素,所以继承了Iterable,使用数组实现栈的完整代码:

public class FixCapacityArrayStack<T> implements Stack<T> {
    private T[] arr;
    private int size;

    public FixCapacityArrayStack(int capacity) {
        this.arr = (T[]) new Object[capacity]; //初始化数组大小
    }

    @Override
    public void push(T item) {
        this.arr[size++] = item;
    }

    @Override
    public T pop() {
        return this.arr[--size];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            int index;

            @Override
            public boolean hasNext() {
                return index < size;
            }

            @Override
            public T next() {
                return arr[index++];
            }
        };
    }
}



面试官:你刚才实现的栈是定容的,那如何实现动态调整栈的大小

我:(已猜到你会问这个问题了,刚才故意没说动态调整大小;经过多年的面试经验总结,最和谐的面试过程就是与面试官你推我挡,一上来就说出了最优解,如何体现面试官的优越感?)

我:要实现动态的调整大小,首先需要在提供一个 resize 的方法,把数组扩容到指定的大小并拷贝原来的数据到新的数组中;

private void resize(int newCapacity) {
    Object[] tmp = new Object[newCapacity];
    for (int index = 0; index < size; index++) {
        tmp[index] = arr[index];
    }
    this.arr = (T[]) tmp;
}


需要push方法中检查当前的size是否已经达到了数组的最大容量,如果是,就把数组扩容2倍

@Override
public void push(T item) {
    if (this.arr.length == size) {
        this.resize(2 * this.arr.length);
    }
    this.arr[size++] = item;
}


pop方法中需要检查当前占用的空间是否是数组的四分之一,如果是,就需要把数据的容量缩小到原来的一半

@Override
public T pop() {
    T item = this.arr[--size];
    this.arr[size] = null;  //避免游离对象,让垃圾回收器回收无用对象
    if (size > 0 && size == this.arr.length / 4) {
        this.resize(this.arr.length / 2);
    }
    return item;
}



面试官:刚才你提到了链表,那么使用链表如何实现栈

我:(这就是你推我挡的结果,和小姐姐的互动很和谐)

我:使用链表,首先需要先定义个Node,单向的链表包含了值和下一个节点的引用

public class Node<T> {
    private T item;
    private Node next;
}


采用链表实现的栈相对于数组实现还较为简单一些,不需要考虑扩容的问题。

public class LinkedListStack<T> implements Stack<T> {
    private Node<T> first;
    private int size;

    @Override
    public void push(T item) {//先栈顶添加元素
        this.first = new Node<>(item, first);
        size++;
    }

    @Override
    public T pop() { //从栈顶删除元素
        T item = first.getItem();
        size--;
        first = first.getNext();
        return item;
    }

    @Override
    public boolean isEmpty() {
        return first == null;
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            private Node<T> current = first;

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public T next() {
                T item = current.getItem();
                current = current.getNext();
                return item;
            }
        };
    }
}


面试官:使用链表如何实现先进先出队列

我:与栈的实现过程类似,首先需要定义出队列

public interface Queue<T> extends Iterable {
    void enqueue(T item); //入队列

    T dequeue(); //出队列

    int size();

    boolean isEmpty();
}


使用链表实现队列需要维护两个变量first、last;first表示的是队列的头结点,last表示队列的尾结点;当入队列时enqueue向尾部结点添加元素,当出队列时dequeue从头结点删除元素。

public class LinkedListQueue<T> implements Queue<T> {
    private Node<T> first;
    private Node<T> last;
    private int size;

    @Override
    public void enqueue(T item) {
        Node<T> node = new Node<>(item, null);
        if (isEmpty()) {
            first = node; //当队列为空,first和last指向同一个元素
        } else {
            last.setNext(node);
        }
        last = node;
        size++;
    }

    @Override
    public T dequeue() {
        T item = first.getItem();
        first = first.getNext();
        if (isEmpty()) { //当队列为空时,需要把last设置为null
            last = null;
        }
        size--;
        return item;
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public boolean isEmpty() {
        return first == null;  //首节点为空
    }

    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            private Node<T> current = first;

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public T next() {
                T item = current.getItem();
                current = current.getNext();
                return item;
            }
        };
    }
}



面试官:胃开的差不多了,来聊一点算法吧;你来设计一个算法对算术表示式求值,比如:( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )

我:(昨天晚上熬夜看算法没白辛苦啊,刚好看到了这个解法。)

我:(挠挠头),这个问题有点麻烦,我需要思考一会。(这样显得我是没有提前准备的,属于临场发挥)

我:定义两个栈,一个用于保存运算符,一个用户保存操作数;具体的操作过程如下:

  • 忽略左边括号
  • 遇到数字就压入操作数栈
  • 遇到符号就压入符号栈
  • 遇到右括号,弹出一个运算符,弹出所需要的操作数,将计算的结果再次压入到操作数栈

public static int calculate(String expression) {
    Stack<String> operate = new LinkedListStack<>();
    Stack<Integer> numbers = new LinkedListStack<>();

    String[] split = expression.split(" ");
    for (String str : split) {
        if ("(".equals(str)) {
        } else if ("+".equals(str) || "-".equals(str) || "*".equals(str) || "/".equals(str)) {
            operate.push(str);
        } else if (")".equals(str)) {
            String op = operate.pop();
            int resut = 0;
            if ("+".equals(op)) {
                resut = numbers.pop() + numbers.pop();
            } else if ("-".equals(op)) {
                resut = numbers.pop() - numbers.pop();
            } else if ("*".equals(op)) {
                resut = numbers.pop() * numbers.pop();
            } else if ("/".equals(op)) {
                resut = numbers.pop() / numbers.pop();
            }
            numbers.push(resut);
        } else {
            numbers.push(Integer.valueOf(str));
        }
    }
    return numbers.pop();
}


面试官:一个int类型的数组,其中存在三个数字相加等于0,你来设计个算法帮我统计出有多少组这样的数字

我:这个简单,请看代码:

public static int count1(int[] arr) {
    int length = arr.length;
    int count = 0;
    for (int i = 0; i < length; i++) {
        for (int j = i + 1; j < length; j++) {
            for (int k = j + 1; k < length; k++) {
                if (arr[i] + arr[j] + arr[k] == 0) {
                    count++;
                }
            }
        }
    }
    return count;
}


面试官:假如这个数组有100万的int值,你这个算法得运行到什么时候

我:(对的哦,这个算法的时间复杂度是O(n³),在遇到数据量较大时效率极低)

(经过大脑快速思考后)

我:这个算法确实有问题,我大意了,没有考虑到大量数据的情况;用这个算法会浪费小姐姐的大好青春,所以刚才我思考了下,对这个算法进行改进一下;

首先把3-sum简化成2-sum

2-sum中,一个数a[i]要与另一个数相加等于0;有两种方法:

第一种:与3-sum实现类似使用两层循环,时间复杂度是O(n²)

第二种:只需要找出数组中是否有-a[i],查找的算法使用二分查找法

public static int count2(int[] arr) {
    Arrays.sort(arr); //首先排序
    int length = arr.length;
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (BinarySearch.search(-arr[i], arr) > i) {
            count++;
        }
    }
    return count;
}


二分查找法的时间复杂度是O(log n), 实现2-sum的算法多了一层循环,所以时间复杂度O(nlog n)

对待3-sum也是用类似的方法,直接上机撸代码:

public static int count3(int[] arr) {
    Arrays.sort(arr);
    int length = arr.length;
    int count = 0;
    for (int i = 0; i < length; i++) {
        for (int j = i + 1; j < length; j++) {
            if (BinarySearch.search(-arr[i]-arr[j], arr) > j) {
                count++;
            }
        }
    }
    return count;
}


我:小姐姐,这个算法改进之后的时间复杂度是O(n²log n),我已经尽力了,只能这么快了。(面试官露出迷人的微笑)

面试官:假如你是微信的开发人员,随便给你两个用户,如何判断这两个用户是否连通的。何为连通?A是B的好友,B是C的好友,那么A与C就是连通的

我:(这小姐姐的问题是越来越难了)

我:美丽的面试官,今天烧脑严重,我可以趴下休息一会吗?(其实是没想到好的解法,拖延时间战术)

面试官:可以,那你先休息10分钟。

面试未完,待续

最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,写作不易,请不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

文中所有源码已放入到了github仓库https://github.com/silently9527/JavaCore

参考书籍:算法第四版

程序员常用的IDEA插件:https://github.com/silently9527/ToolsetIdeaPlugin

完全开源的淘客项目:https://github.com/silently9527/mall-coupons-server

查看原文

赞 2 收藏 1 评论 0

Silently9527 发布了文章 · 2月3日

熬夜肝了个IDEA插件整合程序员常用的工具,总有你能用上的

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学Java

前言

自己在开发的过程中经常会使用一些在线的工具,比如:时间戳转日期,JSON格式化等等;前几天思考了下想把这些常用的功能都做成IDEA插件,在使用的时候就不用去网上寻找工具,在IDEA中就可以快速完成提升开发人员开发效率,所以就熬夜肝了这个插件,欢迎大家都来使用。

Github地址: https://github.com/silently95...

Gitee地址: https://gitee.com/silently952...

觉得好用的小伙伴记得小手一抖 star 哟

已实现功能

  • [x] SQL 转换成 ElasticSearch 查询语句
  • [x] 正则表达式
  • [x] Base64编码/解码
  • [x] JSON格式化
  • [x] URL编码/解码
  • [x] 手机号归属地
  • [x] IP地址
  • [x] 日期时间戳互转

计划中的功能

  • [ ] Cron表达式
  • [ ] MD5
  • [ ] 图片base64编码
  • [ ] 文件下载
  • [ ] js/css混淆压缩

插件安装步骤

  1. 从release中下载最新版本的代码
  2. 在IDEA中通过本地安装插件

功能介绍:

SQL转换成ElasticSearch查询语句

手写ElasticSearch的查询语句,语法记不住的可以使用下这个工具,常用的都能正常转换,如果遇到复杂的可能会转换出错,需要在再转换的结果中自行修改

Base64编码/解码

JSON格式化

IP地址

手机号归属地

URL编码/解码

日期时间戳互转

正则表达式

提供了常用的正则表达式匹配,当然自己也可以自己写表达式


写到最后(点关注,不迷路)

白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

欢迎关注公众号:贝塔学JAVA

查看原文

赞 7 收藏 3 评论 5

Silently9527 发布了文章 · 2月2日

精美的淘客项目完全开源啦,确定不来围观吗

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

项目介绍

Mall-Coupons是一个从前端到后端完全开源的淘宝客项目,当初学习完uniapp之后想做一个实战项目,所以才研发了这个项目。由于本人平时主要从事后端研发,界面样式非我所长,所以大家觉得界面效果不好的可以自己修改。目前项目已经支持打包成App、微信小程序、QQ小程序、Web站点;理论上其他小程序也支持,可能需要微调

Github地址:

Gitee地址:

效果预览

功能列表

  • 推荐穿衣搭配
  • 搭配筛选
  • 搭配详情
  • 相关搭配推荐
  • 用户点赞
  • 商品分类
  • 分类查询商品列表
  • 首页轮播
  • APP、Web支持唤醒淘宝
  • 9.9包邮
  • 疯抢排行榜
  • 首页优质商品推荐
  • 商品、优惠券搜索
  • 商品详情
  • 相似商品推荐
  • 商品收藏、收藏夹
  • 口令购买、领券购买
  • 用户登录、微信登录、QQ登录、手机验证码登录
  • 用户新手教程

技术选型

后端技术

技术备注地址
SpringBoot容器+MVC框架https://spring.io/projects/spring-boot
MyBatisORM框架http://www.mybatis.org/mybatis-3/zh/index.html
SpringSecurity认证和授权框架https://spring.io/projects/spring-security
SpringSocialOAuth2认证框架https://github.com/spring-projects/spring-social
Redis分布式缓存https://redis.io/
Druid数据库连接池https://github.com/alibaba/druid
Lombok简化对象封装工具https://github.com/rzwitserloot/lombok
FastjsonJSON工具https://github.com/alibaba/fa...
spring-data-mybatis封装Mybatis实现JPA部分功能https://github.com/easybest/spring-data-mybatis

前端技术

技术备注地址
Vue前端框架https://vuejs.org/
UniApp一个使用 Vue.js 开发所有前端应用的框架https://uniapp.dcloud.io/
Vuex全局状态管理框架https://vuex.vuejs.org/
colorui样式库https://github.com/weilanwl/ColorUI

开发环境

工具版本号下载
JDK1.8https://www.oracle.com/techne...
Mysql5.7https://www.mysql.com/
Redis5.0https://redis.io/download
Nginx1.10http://nginx.org/en/download....

部署文档

关注微信公众号:贝塔学JAVA ;回复文档获取部署文档

有任何部署疑问,欢迎给我留言

我的技术博客

https://silently9527.cn/


写到最后(点关注,不迷路)

白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

查看原文

赞 8 收藏 6 评论 0

Silently9527 发布了文章 · 1月27日

面试官常问的垃圾回收器,这次全搞懂

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

前言

前几天写了一篇《JVM性能调优实战:让你的IntelliJ Idea纵享丝滑》,其中有对GC垃圾回收器的选择尝试,本篇我们就来详细的看看JVM中常见的垃圾回收器有哪些以及每个垃圾回收器的特点,这也是面试的时候经常被问的内容

JVM堆内存概览

在聊垃圾回收器之前,我们先来看看JVM堆内存的区域划分是怎么样的,看下图

  • 因为虚拟机使用的垃圾回收算法是分代收集算法,所以堆内存被分为了新生代和老年代
  • 新生代使用的垃圾回收算法是复制算法,所以新生代又被分为了 Eden 和Survivor;空间大小比例默认为8:2
  • Survivor又被分为了S0、S1,这两个的空间大小比例为1:1

内存分配以及垃圾回收

  1. 对象优先在Eden区进行分配,如果Eden区满了之后会触发一次Minor GC
  2. Minor GC之后从Eden存活下来的对象将会被移动到S0区域,当S0内存满了之后又会被触发一次Minor GC,S0区存活下来的对象会被移动到S1区,S0区空闲;S1满了之后在Minor GC,存活下来的再次移动到S0区,S1区空闲,这样反反复复GC,每GC一次,对象的年龄就涨一岁,默认达到15岁之后就会进入老年代,对于晋身到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold设置
  3. 在Minor GC之后需要的发送晋身到老年代的对象没有空间安置,那么就会触发Full GC (这步非绝对,视垃圾回收器决定)
Minor GC和Full GC的区别:Minor GC是指发生在新生代的垃圾收集行为,由于对象优先在Eden区分配,并且很多对象都是朝生夕死,所以触发的频率相对较高;由于采用的复制算法,所以一般回收速度非常快。Full GC是指发生在老年代的垃圾收集行为,Full GC的速度一般会比Minor GC慢10倍以上;所以不能让JVM频繁的发生Full GC

为了能够更好的适应不同程序的内存情况,JVM也不一定要求必须达到年龄15岁才能晋身到老年代,如果在Survivor区中相同年龄的所有对象大小总和大于Survivor区空间的一半,年龄大于或者等于这个年龄的对象将会直接进入到老年代

Full GC触发条件

  • 代码中调用System.gc()
  • 老年代空间不足/满了
  • 持久区空间不足/满了
注意:大对象会直接在老年代分配内存,可以通过参数-XX:PretenureSizeThreshold控制对象的大小,通常遇到的大对象是很长的字符串或者数组,如果分配了一大群大对象只是临时使用,生命很短暂,那么就会频繁的发生Full GC,但是此时的新生代的空间还有空闲;写代码的时候,这种情况应该避免,特别是在创建数组的时候要当心

空间担保

在新生代发生Minor GC的时候,JVM会先检查老年代中可分配的连续空间是否大于新生代所有对象的总和,如果大于,那么本次Minor GC就可以安全的执行;如果不大于,那么JVM会先去检查参数HandlePromotionFailure设置值是否允许空间担保失败,如果允许,JVM会继续检查老年代可分配的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尽管这次Minor GC是有风险的,JVM也会尝试一次Minor GC;如果不允许担保失败,那么JVM直接进行Full GC

虽然担保有可能会失败,导致饶一圈才能进行GC,但是还是建议把这个参数打开,可以避免JVM频繁的Full GC

垃圾回收器概览

从上图可以看出:

  • 新生代可以使用的垃圾回收器:Serial、ParNew、Parallel Scavenge
  • 老年代可以适用的垃圾回收器:CMS、Serial Old、Parallel Old
  • G1回收器适用于新生代和老年代
  • 相互之间有连线的表示可以配合使用
CMS和Serial Old同为老年代回收器,为何相互会有连线呢?

Serial收集器

这是个单线程收集器,发展历史最悠久的收集器,当它在进行垃圾收集工作的时候,其他线程都必须暂停直到垃圾收集结束(Stop The World)。

虽然Serial收集器存在Stop The World的问题,但是在并行能力较弱的单CPU环境下往往表现优于其他收集器;因为它简单而高效,没有多余的线程交互开销;Serial对于运行在Client模式下的虚拟机来说是个很好的选择

使用-XX:+UseSerialGC参数可以设置新生代使用这个Serial收集器

ParNew收集器

ParNew收集器是Serial收集器的多线程版本;除了使用了多线程进行垃圾收集以外,其他的都和Serial一致;它默认开始的线程数与CPU的核数相同,可以通过参数-XX:ParallelGCThreads来设置线程数。

从上面的图可以看出,能够与CMS配合使用的收集器,除了Serial以外,就只剩下ParNew,所以ParNew通常是运行在Server模式下的首选新生代垃圾收集器

使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

Parallel Scavenge收集器

Parallel Scavenge收集器依然是个采用复制算法的多线程新生代收集器,它与其他的收集器的不同之处在于它主要关心的是吞吐量,而其他的收集器关注的是尽可能的减少用户线程的等待时间(缩短Stop The World的时间)。吞吐量=用户线程执行时间/(用户线程执行时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花费时间1分钟,那么吞吐量就是 99%

停顿时间越短适合需要和用户进行交互的程序,良好的响应能够提升用户的体验。而高效的吞吐量可以充分的利用CPU时间,尽快的完成计算任务,所以Parallel Scavenge收集器适用于后台计算型任务程序。

-XX:MaxGCPauseMillis可以控制垃圾收集的最大暂停时间,需要注意不要以为把这个时间设置的很小就可以减少垃圾收集暂用的时间,这可能会导致发生频繁的GC,反而降低了吞吐量

-XX:GCTimeRatio设置吞吐量大小,参数是取值范围0-100的整数,也就是垃圾收集占用的时间,默认是99,那么垃圾收集占用的最大时间 1%

-XX:+UseAdaptiveSizePolicy 如果打开这个参数,就不需要用户手动的控制新生代大小,晋升老年代年龄等参数,JVM会开启GC自适应调节策略

Serial Old收集器

Serial Old收集器也是个单线程收集器,适用于老年代,使用的是标记-整理算法,可以配合Serial收集器在Client模式下使用。

它可以作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。(后面CMS详细说明)

Parallel Old收集器

Parallel Old收集器可以配合Parallel Scavenge收集器一起使用达到“吞吐量优先”,它主要是针对老年代的收集器,使用的是标记-整理算法。在注重吞吐量的任务中可以优先考虑使用这个组合

-XX:+UseParallelOldGc设置老年代使用该回收器。

XX:+ParallelGCThreads设置垃圾收集时的线程数量。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,在互联网网站、B/S架构的中常用的收集器就是CMS,因为系统停顿的时间最短,给用户带来较好的体验。

-XX:+UseConcMarkSweepGC设置老年代使用该回收器。

-XX:ConcGCThreads设置并发线程数量。

CMS采用的是标记-清除算法,主要分为了4个步骤:

  • 初始化标记
  • 并发标记
  • 重新标记
  • 并发清除

初始化标记和重新标记这两个步骤依然会发生Stop The World,初始化标记只是标记GC Root能够直接关联到的对象,速度较快,并发标记能够和用户线程并发执行;重新标记是为了修正在并发标记的过程中用户线程产生的垃圾,这个时间比初始化标记稍长,比并发标记短很多。整个过程请看下图

优点

  • CMS是一款优秀的收集器,它的主要优点:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  • CMS收集器对CPU资源非常敏感。 在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,回收阀值可以通过参数-XX:CMSInitiatingoccupancyFraction来设置;如果回收阀值设置的太大,在CMS运行期间如果分配大的对象找不到足够的空间就会出现“Concurrent Mode Failure”失败,这时候会临时启动SerialOld GC来重新进行老年代的收集,这样的话停顿的时间就会加长。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。为了解决这个问题CMS提供了一个参数-XX:+UseCMSCompactAtFullCollecion,如果启用,在Full GC的时候开启内存碎片整理合并过程,由于内存碎片整理的过程无法并行执行,所以停顿的时间会加长。考虑到每次FullGC都要进行内存碎片合并不是很合适,所以CMS又提供了另一个参数-XX:CMSFullGCsBeforeCompaction来控制执行多少次不带碎片整理的FullGC之后,来一次带碎片整理GC

G1收集器

G1是一款面向服务端应用的垃圾回收器。

  • 并行与并发:与CMS类似,充分里用多核CPU的优势,G1仍然可以不暂停用户线程执行垃圾收集工作
  • 分代收集:分代的概念依然在G1保留,当时它不需要和其他垃圾收集器配合使用,可以独立管理整个堆内存
  • 空间的整合:G1整体上采用的是标记-整理算法,从局部(Region)采用的是复制算法,这两种算法都意味着G1不需要进行内存碎片整理
  • 可预测的停顿:能够让用户指定在时间片段内,消耗在垃圾收集的时间不超过多长时间。

Region

虽然在G1中依然保留了新生代和老年代的概念,但是采用的是一种完全不同的方式来组织堆内存,它把整个堆内存分割成了很多大小相同的区域(Region),并且新生代和老年代在物理上也不是连续的内存区域,请看下图:

每个Region被标记了E、S、O和H,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象,当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。Region区域的内存大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是2的幂次方,如:1M、2M、4M、8M

G1的GC模式

  • 新生代GC:与其他新生代收集器类似,对象优先在eden region分配,如果eden region内存不足就会触发新生代的GC,把存活的对象安置在survivor region,或者晋升到old region
  • 混合GC:当越来越多的对象晋升到了old region,当老年代的内存使用率达到某个阈值就会触发混合GC,可以通过参数-XX:InitiatingHeapOccupancyPercent设置阈值百分比,此参数与CMS中-XX:CMSInitiatingoccupancyFraction的功能类似;混合GC会回收新生代和部分老年代内存,注意是部分老年代而不是全部老年代;G1会跟踪每个Region中的垃圾回收价值,在用户指定的垃圾收集时间内优先回收价值最大的region
  • Full GC:如果对象内存分配速度过快,混合GC还未回收完成,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,此过程与CMS类似,会导致异常长时间的暂停时间,尽可能的避免full gc.
    • -

写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,请朋友们不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

    • -
我已经从零开始手写了简易版springmvc,以及编写了详细的说明文档,希望能够帮助伙伴们深入理解springmvc核心原理,有需要的朋友欢迎关注公众号:贝塔学JAVA ,回复源码即可

image

查看原文

赞 6 收藏 5 评论 0

Silently9527 发布了文章 · 1月25日

吐血整理:推荐几款顶级好用的IDEA插件

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学Java

前言

“工欲善其事必先利其器” 在实际的开发过程中,灵活的使用好开发工具,将让我们的工作事半功倍。今天给大家推荐几款好用的IDEA插件,写代码也可以“飞起来”

美化插件

Material Theme UI

相亲第一眼也得看眼缘,所以今天推荐的第一款是主题插件,可以让你的idea图标、配置搭配很到位,也可以切换不用的颜色,默认提供了很多的主题供选择,每一种都是狂拽酷炫;当前端小姐姐或者测试小姐姐看到了你这么炫酷的界面,她肯定会觉得原来男孩子也会这么精致呀,形象陡然上升~

就问你,这么绚丽多彩的颜色,哪个小姐姐不为你着迷~

Extra Icons

这也是一款美化插件,为一些文件类型提供官方没有的图标

Background Image Plus

设置idea背景图片的插件,不但可以设置固体的图片,还可以设置一段时间后随机变化背景图片,以及设置图片的透明度等等;接下来我设置一张女神的照片,看着女神照片撸代码,整天心情美滋滋

实用插件

Translation

像我这样英文很菜的人来说,这款插件就是神器,在看各种框架源码的时候十分有用; 选择右键就可以翻译,对于方法或者类上面的注释,只要按下F1就自动被翻译成中文

Maven Helper

依赖包冲突的问题,我相信大家都遇到过,一旦出现了冲突,启动或运行过程总是会出一些莫名其妙的错误,查找冲突过程十分痛苦,但如果你安装了这个插件,那这些都不是事,分分钟搞定

Code Glance

Sublime Text右侧的预览区相信很多人都用过吧, 此插件就实现了代码编辑区迷你缩放功能, 达到代码全局预览

MyBatis Log Plugin

Mybaits在运行的时候会把SQL打印出来,但是打印的是带占位符的SQL,如果遇到SQL复杂的话,那么要手动拼接出这个SQL还是比较麻烦的,好在这个插件帮我们搞定

菜单栏 -> Tools -> MyBatis Log Plugin

Free Mybatis plugin

可以在Mybatis的Mapper接口和xml文件之间很方便的来回切换,像是查看接口实现类一样简单,无需到xml中去搜索。

Lombok

神器级别的插件,可以让实体类更加简化,不在需要写getter/setter方法,通过注解就可以实现builder模式以及链式调用;在充血模型中可以不需要在和getter/setter方法混在一起

项目还需要添加maven依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
</dependency>

Key promoter X

回想刚开始从eclipse切换到idea那段时间实在是很痛苦,就是因为快捷键不熟悉,熟悉开发工具的快捷键能够很好的提高我们的开发效率,这款工具的目的就是为了帮助用户记住快捷键,操作窗口之后就会在右下角给出快捷键的提示,提醒多了自然你就记住了。

Grep Console

在开发的过程中,idea的控制台通常会打印出一大推的日志,想要快速找到自己关心的日志比较困难,通过这个插件可以给不同级别的日志设置不同的展示样式,帮助快速定位日志


写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

原创不易 转载请注明出处:https://silently9527.cn/archives/104

我已经从零开始手写了简易版springmvc,以及编写了详细的说明文档,希望能够和伙伴们深入理解springmvc核心原理,有需要的朋友欢迎关注公众号:贝塔学JAVA ,回复源码即可


公众号:贝塔学JAVA

查看原文

赞 7 收藏 5 评论 0

Silently9527 关注了用户 · 1月22日

SegmentFault @segmentfault

SegmentFault 社区管理媛 - 思否小姐姐

纯粹的技术社区离不开所有开发者的支持和努力 biubiu

更多技术内容与动态欢迎关注 @SegmentFault 官方微博与微信公众号!

点击添加思否小姐姐个人微信号

关注 84104

Silently9527 发布了文章 · 1月19日

JVM性能调优实战:让你的IntelliJ Idea纵享丝滑

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学Java

前言

在前面整理了一篇关于JVM故障诊断和处理工具,考虑到大部分的Java程序员都使用的时IntelliJ Idea,本篇就使用工具来实战演练对IntelliJ Idea运行速度调优

调优前的运行状态

原始配置内容

要查询idea原始配置文件的路径可以在VisualVM中的概述中查看

原始配置内容:

-XX:ReservedCodeCacheSize=240m
-XX:+UseCompressedOops
-Dfile.encoding=UTF-8
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow

-XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof

-Xmx512m

打印启动时间插件开发

需要直观的看到优化前和优化后启动时间的变化,所以需要简单做一个Idea的插件开发,关于Idea插件开发的流程建议参考我以前的文章《IDEA插件:多线程文件下载插件开发

JVM的启动时间到所有组件初始化完成后的时间就看做是IDEA的启动时间,代码如下

public class MyApplicationInitializedListener implements ApplicationInitializedListener {
    @Override
    public void componentsInitialized() {
        RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
        long startTime = bean.getStartTime();
        long costTime = System.currentTimeMillis() - startTime;

        Messages.showMessageDialog("毫秒:" + costTime, "启动耗时", Messages.getInformationIcon());
    }
}

plugin.xml中添加如下代码:

<extensions defaultextensionns="com.intellij">
    <applicationinitializedlistener id="MyApplicationInitializedListener" implementation="cn.silently9527.MyApplicationInitializedListener" />
</extensions>

优化前的启动信息与时间消耗

根据VisualGC和IDEA启动插件收集到的信息:

  • IDEA启动耗时 15s
  • 总共垃圾收集22次,耗时1.2s,其中新生代GC 17次,耗时324ms; 老年代GC 5次,耗时953ms
  • 加载类27526个,耗时 21s

> 按照这个数据来看也算是正常,15s 其实也在接受范围内,由于本文主要演示性能调优,所以需要测试能否在快一些

开始尝试优化

调整内存来控制垃圾回收频率

图上我们可以看出,启动参数指定的512m的内存被分配到新生代的只有169m,由于IDEA是我们开发常用的工具,平时的编译过程也需要足够的内存,所以我们需要先把总的内存扩大,这里我设置最大的内存-Xmx1024m,为了让JVM在GC期间不需要再浪费时间再动态计算扩容大小,同时也设置了-Xms1024m

在启动的过程中Eden共发生了17次GC,为了减少新生代gc次数,我把新生代的内存大小设置成-Xmn256m;

重新启动之后查看VisualGC,新生代gc次数从 17次 降低到了 7次,耗时从 324ms 降低到了 152ms。

在调整内存前发生了5次Full GC,调整内存后的依然还是有4次Full GC,但是从两张图我们可以看出,老年代的空间还有很多剩余,是不应该发生Full GC的;考虑是否是代码中有地方手动调用System.gc()出发了Full GC,所以添加了参数-XX:+DisableExplicitGC,再次重新启动IDEA,结果很失望,依然还有4次Full GC;

再次仔细观察优化前的图,注意看 Last Cause: Metadata GC Threshold , 最后一次gc是应该Metaspace区域内存不够发生的GC,为了验证我们的猜想,打印出GC日志来看看。在idea.vmoptions中添加打印日志相关的参数:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:../gc.log

> JVM的GC日志的主要参数包括如下几个:
> - -XX:+PrintGC 输出GC日志
> - -XX:+PrintGCDetails 输出GC的详细日志
> - -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
> - -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
> - -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
> - -Xloggc:../logs/gc.log 日志文件的输出路径

重新启动idea,查看gc.log

> 其中PSYoungGen:表示新生代使用的ParallelScavenge垃圾收集器,31416K-&gt;0K(181248K)表示 gc前已使用的内存大小 -> gc后已使用内存大小(该区域的总内存大小)

从日志中我们看出每次Full GC都是因为Metadata GC Threshold,而Metaspace每次gc回收的内存几乎没有,仅仅是扩大了该区域的容量;找到了原因那就好办了,添加如下的参数调整Metaspace的大小:

-XX:MetaspaceSize=256m

再次重启Idea之后,发现Full GC没有了,心情很爽

测试打开大项目点击编译代码,发现自己的idea卡死了,查看VisualGC之后发现堆内存都还有空闲,只有Metaspace被全部占满了,所以是自己给的最大空间设置太小,所以直接去掉了-XX:MaxMetaspaceSize=256m

选择垃圾收集器

从刚才的gc日志中,我们可以发现默认使用的是ParallelScavenge + Parallel Old垃圾收集器,这个组合注重的是吞吐量,这里我们尝试换一个注重低延时的垃圾收集器试一试

  • ParNew + CMS

idea.vmoptions中添加如下配置:

-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

重启IDEA之后查看VisualGC

很尴尬,同样发生了6次gc,ParallelScavenge + Parallel Old的组合耗时197ms,而ParNew + CMS的组合耗时379ms;虽然是这个结果,但是我们需要考虑当前只发生了MinorGC,如果发生FullGC了结果又会如何了,大家可以自己测试一下

  • G1

我们在换一个最新的G1垃圾回收器试试,在idea.vmoptions中添加如下配置:

-XX:+UseG1GC

这个结果好像也还是要慢一点点,自己多次测试过这两个垃圾回收器,虽然每次结果都不一样,相差不远,所以垃圾回收器可以自己选择,这里我们选择的是G1

类加载时间优化

根据之前的分析,idea启动加载类27526个,耗时 21s,这个我们有办法能优化一下吗?因为idea是常用的开发工具,经常很多人的使用,我们可以认为它的代码是安全的,是否符合当前虚拟机的要求,不会危害虚拟机的安全,所以我们使用参数-Xverify:none来禁用字节码的验证过程

重启IDEA

耗时下降到了11s,效果还是比较明显的

总结

做完了所有优化之后,经过多次重启测试,平均的启动时间下降到了11s,为了安慰我本次操作没有白辛苦,搞一张11s以下的图


写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

原创不易 转载请注明出处:https://silently9527.cn/archives/102

我已经从零开始手写了简易版springmvc,以及编写了详细的说明文档,希望能够帮助伙伴们深入理解springmvc核心原理,有需要的朋友欢迎关注公众号:贝塔学JAVA ,回复源码即可


查看原文

赞 11 收藏 8 评论 2

Silently9527 发布了文章 · 1月14日

JVM故障诊断和处理工具

本文已被Github仓库收录 https://github.com/silently9527/JavaCore
微信公众号:贝塔学Java

前言

前几天中午正在和同事最近聊股市较好,这几天每天都可以喝点肉汤,心里还是挺高兴的;正在这个时候收到了线上告警邮件和运维同学的消息,“你们有服务挂了!”,心里一紧,立马打开电脑看来下线上cat监控大盘,发现很多服务都在报错,根据cat上的监控日志很快发现了其中一个服务内存溢出导致其他调用服务也有问题,竟然已经定位到了出问题的服务,那就简单了,没有是重启解决不了的问题,重启之后很快服务都恢复正常了。几分钟之后又报错了,同样也是这个服务内存溢出,经过排查后发现该服务的堆内存被改小了,好家伙,运维同学不讲武德,搞偷袭,趁我没反应过来调了内存,内存调整回去之后服务就恢复了正常。

事后把线上的快照文件拖了下来分析,发现本身这个项目的代码也有些问题,本文就整理了一下JVM常用的分析工具。

命令行工具

在安装完JDK之后在JAVA_HOME/bin目录下JDK已经提供了很多命令行的工具

可能我们最常用的就是javajavac这两个命令,除了这两个命令之外还有提供很多其他的实用工具,本文主要来一起学习对JVM监控诊断工具

虚拟机进程状况工具(jps)

该工具的功能比较单一,与linux中的ps功能类似,用来列出正在运行的虚拟机进程,并显示出运行的主类和进程号

命令格式:jps [option] [hostid]

> 如果需要查看远程机器的jvm进程需要填写hostid,并且需要使用RMI,比如:rmi://192.168.2.128:12345

常用的选项:

  • -q : 只显示出虚拟机的进程id(lvmid),省略主类名
  • -m : 输出启动时传递给主类的参数
  • -l : 显示出主类的全名,包括jar包路径
  • -v : 输出虚拟机进程启动时的JVM参数

虚拟机统计信息监控工具(jstat)

用于监控虚拟机运行状态信息的命令行工具,可以提供内存,垃圾收集等云行时的数据

命令格式:jstat [option vmid] [interval [s|ms] [count]]

interval表示间隔多久时间查询一次,count表示查询多少次,比如:每个两秒查询一次进程52412的垃圾收集情况,共查询5次

jstat -gc 52412 2s 5

常用的选项:

  • -class: 监控类装载,卸载次数和总空间以及加载类的耗时
  • -gc: 监控java堆的情况
  • -gcutil: 主要输出各个空间使用的百分比
  • -gcnew: 主要是监控新生代的GC状况
  • -gcold: 监控老年代的GC状况
  • -compiler: 输出JIT编译器编译过的方法和耗时信息

查看堆空间的使用百分比: jstat -gcutil 52412 2s 5

java配置信息工具(jinfo)

可以通过jinfo实时的查看和调整虚拟机的各项参数;可以通过jps -v查看虚拟机启动时候指定的参数信息,如果需要查看未显示指定的参数默认值也可以通过jinfo -flag

jinfo -flag CMSInitiatingOccupancyFraction 52412

jinfo除了可以查看参数以外,还可以在运行时修改一些允许被修改的参数

Java内存映像工具(jmap)

jmap用于生成JVM堆的快照文件,除了使用jmap工具,我们通常也会在配置JVM的启动参数 -XX:+HeapDumpOnOutOfMemoryError 让JVM在发送内存溢出之后自动生成dump文件。

命令格式:jmap [option] vmid

比如生成java堆的快照文件

jmap -dump:live,format=b,file=/Users/huaan9527/Desktop/heap.hprof 59950

常用的选项:

  • -F: 当虚拟机对-dump选项没有响应时可用选择使用这个参数强制生成快照
  • -histo: 显示出堆中对象统计信息。

堆栈跟踪工具(jstack)

用于生成JVM当前线程的快照信息。通常用于查询什么原因导致线程长时间的停顿,比如:线程死循环,死锁,等待网络/IO

命令格式:jstack [option] vmid

常用的选项:

  • -F: 当请求不被响应时强制输出
  • -l: 除了显示堆栈外,还需要显示锁的信息
  • -m: 如果调用到本地方法,显示出C/C++的堆栈

VisualVM 可视化工具

VisualVM是目前JDK自带的功能最强的运行监视和故障处理程序,在VisualVM之前,JDK也提供了一款可视化工具JConsole,由于JConsole的所有功能在VisualVM都有,所以可视化工具大家几乎都选择使用VisualVM。

VisualVM本身是基于Netbean开发的,所以具备了插件扩展功能,安装插件之后上面介绍的所有命令行的工具的功能都可以在VisualVM中使用。可以在在JAVA_HOME/bin目录下执行jvisualvm启动。

  • 插件安装

默认情况VisualVM提供的功能很少,需要我们在菜单栏->工具->插件里面安装插件,我这是全部插件都安装了

功能演示

  • 应用程序、概述、监视

显示出当前本机所有的JVM进程,这里显示的内容和前面说的命令行jps显示的内容一样

当前虚拟机启动信息的展示,比如:JVM启动参数、系统参数

这个页面相当于命令jstat的功能,显示出了CPU, 内存,线程,类装载当前处于什么情况

生成dump文件可以在应用程序窗口右键菜单中选择,也可以在这个页面点击右上角的堆dump

  • Visual GC

此页主要展示了GC相关的信息,这是在性能调优时常用的页面之一

我们可以写个程序来观看下这个截图各个内存区域的变化情况,为了让图的效果明显需要修改JVM的启动参数

-Xmx100m -Xms100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/huaan9527/Desktop
public static void main(String[] args) {
    List<datatest> datas = new ArrayList&lt;&gt;();

    IntStream.range(0, 10000).forEach(index -&gt; {
        datas.add(new DataTest());

        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    System.gc();
}

static class DataTest {
    byte[] bytes = new byte[1024];
}

  • 线程

本页的功能相当于命令行工具jstack,主要是用于检查什么原因导致线程长时间等待,我们写程序来演示下等待外部资源、锁等待、死循环这几种请求

等待外部资源

public static void main(String[] args) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    System.out.println(reader.readLine());
    try {
        Thread.sleep(1000000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

选中main线程,右侧会看到当前线程运行到了readBytes,等待键盘输入

当我们在控制台输入之后再次查看main线程的状态,此时进入了TIME_WAIT状态

锁等待

public static void main(String[] args) throws IOException, InterruptedException {
    Thread thread = createLockThread(new Object());
    thread.join();
}

public static Thread createLockThread(final Object lock) {
    Thread lockThread = new Thread(() -&gt; {
        synchronized (lock) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "lockThread");
    lockThread.start();
    return lockThread;
}

lockThread线程在等待lock对象的notify方法被调用,此时处于WAITING状态,在被唤醒之前是不会再分配执行时间

死循环

public static void main(String[] args) throws IOException, InterruptedException {
    while (true) ;
}

线程一直处于运行状态,从堆栈追踪里可以看出代码一直停留在了191行,在空循环上用尽分配的执行时间

总结

本篇介绍了命令行工具和可视化工具,下篇实战演示下如何通过这些工具对Idea运行速度调优


写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

原创不易 转载请注明出处:https://silently9527.cn/archives/100
查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 1月12日

一致性Hash算法Java版实现

前言

在之前写了两篇关于缓存的文章《万字长文聊缓存(上)- http缓存》《万字长文聊缓存(下)- 应用级缓存》,谈到缓存不说一下一致性Hash算法那就是在耍流氓。

分布式缓存集群的访问模型

现在通常使用Redis来做分布式缓存,下面我们就以Redis为例:

假如当前我们系统的业务发展很快,需要缓存的数据很多,所以我们做了一个由三组主从复制的redis组成的高可用的redis集群,如何将请求路由的不同的redis集群上,这是我们需要考虑的,常用的路由算法:

随机算法:每次将请求随机的发送到其中一组Redis集群中,这种算法的好处是请求会被均匀的分发到每组Redis集群上;缺点也很明显,由于随机分发请求,为了提高缓存的命中率,所以同一份数据需要在每组集群中都存在,这样就会造成了数据的冗余,浪费了存储空间

Hash算法:针对随机算法的问题,我们可以考虑Hash算法,举例:
现在有三组redis集群,我们可以对每次缓存key的hash值取模,公式:index=hash(key) % 3,index的值就对应着3组集群,这样就可以保证同一个请求每次都被分发到同一个redis集群上,无需对数据做冗余,完美的解决了刚才随机算法的缺点;

但是hash算法也有缺点:对于容错性和伸缩性支持很差,举例:当我们三组redis集群中其中一组节点宕机了,那么此时的redis集群中可用的数量变成了2,公式变成了index=hash(key) % 2, 所有数据缓存的节点位置就发生了变化,造成缓存的命中率直线下降;

同理,当我们需要扩展一组新的redis机器,计算的公式index=hash(key) % 4,大量的key会被重新定位到其他服务器,也会造成缓存的命中率下降。

为了解决hash算法容错性和伸缩性的问题,一致性hash算法由此而生~

一致性哈希算法

具体的算法过程

  1. 先构造一个长度为2^32-1的整数环(称为一致性hash环),然后给每组redis集群命名,根据名字的hash值计算出每组集群应该放在什么位置

  1. 根据缓存数据的key计算出hash值,计算出出来的hash值同样也分布在一致性hash环上; 假如现在有5个数据需要缓存对应的key分别为key1、key2、key3、key4、key5,计算hash值之后的分部如下图

  1. 然后顺着hash环顺时针方向查找reids集群,把数据存放到最近的集群上

最后所有key4、key5存放在了集群2,key1、key3存放在了集群1,key2存放在了集群3

容错性

还是继续沿用上面的例子,我们来看下一致性哈希算法的容错性如何呢?假如其中 集群1 跪了,那么影响的数据只有key1和key3,其他数据存放的位置不受影响;当再次缓存key1、key3的时候根据顺时针查找,会把数据存放到集群3上面

伸缩性

如果我们需要在当前的基础上再添加一组redis集群4,根据名字hash之后的位置在集群1和集群2之间

新加redis集群4之后影响的只有key1数据,其他数据不受影响。

数据倾斜问题

经过容错性、伸缩性的验证证明了一致性哈希算法确实能解决Hash算法的问题,但是现在的算法就是完美的吗?让我们继续来看刚才容错性的例子,加入集群1跪了,那么原来落在集群1上的所有数据会直接落在集群3上面,如果说每组redis集群的配置都是一样的,那么集群3的压力会增大,数据分布不均匀造成数据倾斜问题。

怎么搞呢?

歪果仁的脑子就是好使,给的解决方案就是加一层虚拟层,假如每组集群都分配了2个虚拟节点

集群虚拟节点
集群1vnode1, vnode2
集群2vnode3, vnode4
集群3vnode5, vnode6

接下来就是把虚拟节点放入到一致性hash环上,在缓存数据的时候根据顺时针查找虚拟节点,在根据虚拟节点的和实际集群的对应关系把数据存放到redis集群,这样数据就会均匀的分布到各组集群中。

这时候如果有一组redis集群出现了问题,那么这组集群上面的key会相对均匀的分摊到其他集群上。

从上面的结果来看,只要每组集群对应的虚拟节点越多,那么各个物理集群的数据分布越均匀,当新增加或者减少物理集群影响也会最小,但是如果虚拟节点太多会影响查找的性能,太少数据又会不均匀,那么多少合适呢?根据一些大神的经验给出的建议是 150 个虚拟节点。

一致性Hash算法Java版实现

实现思路:在每次添加物理节点的时候,根据物理节点的名字生成虚拟节点的名字,把虚拟节点的名字求hash值,然后把hash值作为key,物理节点作为value存放到Map中;这里我们选择使用TreeMap,因为需要key是顺序的存储;在计算数据key需要存放到哪个物理节点时,先计算出key的hash值,然后调用TreeMap.tailMap()返回比hash值大的map子集,如果子集为空就需要把TreeMap的第一个元素返回,如果不为空,那么取子集中的第一个元素。

不扯废话,直接上代码,No BB . Show me the code

核心代码:

测试代码:

  1. 测试删除节点node3,对比命中率影响了多少 添加如下代码:

执行结果:

  1. 测试添加节点node5,对比命中率影响了多少 添加如下代码:

执行结果:

其他使用场景

看上图,在Nginx请求的分发过程中,为了让应用本地的缓存命中率最高,我们希望根据请求的URL或者URL参数将相同的请求转发到同一个应用服务器中,这个时候也可以选择使用一致性hash算法。具体配置可以参考官方文档:
https://www.nginx.com/resources/wiki/modules/consistent_hash/


写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

本文已被Github仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学Java

原创不易 转载请注明出处:https://mp.weixin.qq.com/s/eCxGPqrfIeFY_E_CnFRfMw

查看原文

赞 6 收藏 3 评论 0

Silently9527 发布了文章 · 1月5日

万字长文聊缓存(下)- 应用级缓存

深入解析SpringMVC核心原理:从手写简易版MVC框架开始(SmartMvc) : https://github.com/silently9527/SmartMvc

IDEA多线程文件下载插件: https://github.com/silently9527/FastDownloadIdeaPlugin

公众号:贝塔学JAVA

摘要

在上一篇文章 万字长文聊缓存(上)中,我们主要如何围绕着Http做缓存优化,在后端服务器的应用层同样有很多地方可以做缓存,提高服务的效率;本篇我们就来继续聊聊应用级的缓存。

缓存的命中率

缓存的命中率是指从缓存中获取到数据的次数和总读取次数的比率,命中率越高证明缓存的效果越好。这是一个很重要的指标,应该通过监控这个指标来判断我们的缓存是否设置的合理。

缓存的回收策略

基于时间

  • 存活期:在设置缓存的同时设置该缓存可以存活多久,不论在存活期内被访问了多少次,时间到了都会过期
  • 空闲期:是指缓存的数据多久没有被访问就过期

基于空间

设置缓存的存储空间,比如:设置缓存的空间是 1G,当达到了1G之后就会按照一定的策略将部分数据移除

基于缓存数量

设置缓存的最大条目数,当达到了设置的最大条目数之后按照一定的策略将旧的数据移除

基于Java对象引用

  • 弱引用:当垃圾回收器开始回收内存的时候,如果发现了弱引用,它将立即被回收。
  • 软引用:当垃圾回收器发现内存已不足的情况下会回收软引用的对象,从而腾出一下空间,防止发生内存溢出。软引用适合用来做堆缓存

缓存的回收算法

  • FIFO 先进先出算法
  • LRU 最近最少使用算法
  • LFU 最不常用算法

Java缓存的类型

堆缓存

堆缓存是指把数据缓存在JVM的堆内存中,使用堆缓存的好处是没有序列化和反序列化的操作,是最快的缓存。如果缓存的数据量很大,为了避免造成OOM通常情况下使用的时软引用来存储缓存对象;堆缓存的缺点是缓存的空间有限,并且垃圾回收器暂停的时间会变长。

Gauva Cache实现堆缓存

Cache<String, String> cache = CacheBuilder.newBuilder()
                .build();

通过CacheBuilder构建缓存对象

Gauva Cache的主要配置和方法

  • put : 向缓存中设置key-value
  • V get(K key, Callable<? extends V> loader) : 获取一个缓存值,如果缓存中没有,那么就调用loader获取一个然后放入到缓存
  • expireAfterWrite : 设置缓存的存活期,写入数据后指定时间之后失效
  • expireAfterAccess : 设置缓存的空闲期,在给定的时间内没有被访问就会被回收
  • maximumSize : 设置缓存的最大条目数
  • weakKeys/weakValues : 设置弱引用缓存
  • softValues : 设置软引用缓存
  • invalidate/invalidateAll: 主动失效指定key的缓存数据
  • recordStats : 启动记录统计信息,可以查看到命中率
  • removalListener : 当缓存被删除的时候会调用此监听器,可以用于查看为什么缓存会被删除

Caffeine实现堆缓存

Caffeine是使用Java8对Guava缓存的重写版本,高性能Java本地缓存组件,也是Spring推荐的堆缓存的实现,与spring的集成可以查看文档https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration-caffeine

由于是对Guava缓存的重写版本,所以很多的配置参数都是和Guava缓存一致:

  • initialCapacity: 初始的缓存空间大小
  • maximumSize: 缓存的最大条数
  • maximumWeight: 缓存的最大权重
  • expireAfterAccess: 最后一次写入或访问后经过固定时间过期
  • expireAfterWrite: 最后一次写入后经过固定时间过期
  • expireAfter : 自定义过期策略
  • refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
  • weakKeys: 打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开启统计功能

Caffeine的官方文档:https://github.com/ben-manes/caffeine/wiki

  1. pom.xml中添加依赖
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.4</version>
</dependency>
  1. Caffeine Cache提供了三种缓存填充策略:手动、同步加载和异步加载。
  • 手动加载:在每次get key的时候指定一个同步的函数,如果key不存在就调用这个函数生成一个值
public Object manual(String key) {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .expireAfterAccess(1, TimeUnit.SECONDS) //设置空闲期时长
            .maximumSize(10)
            .build();
    return cache.get(key, t -> setValue(key).apply(key));
}

public Function<String, Object> setValue(String key){
    return t -> "https://silently9527.cn";
}
  • 同步加载:构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。
public Object sync(String key){
    LoadingCache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.MINUTES) //设置存活期时长
            .build(k -> setValue(key).apply(key));
    return cache.get(key);
}

public Function<String, Object> setValue(String key){
    return t -> "https://silently9527.cn";
}
  • 异步加载:AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture
public CompletableFuture async(String key) {
    AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .buildAsync(k -> setAsyncValue().get());
    return cache.get(key);
}

public CompletableFuture<Object> setAsyncValue() {
    return CompletableFuture.supplyAsync(() -> "公众号:贝塔学JAVA");
}
  1. 监听缓存被清理的事件
public void removeListener() {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .removalListener((String key, Object value, RemovalCause cause) -> {
                System.out.println("remove lisitener");
                System.out.println("remove Key:" + key);
                System.out.println("remove Value:" + value);
            })
            .build();
    cache.put("name", "silently9527");
    cache.invalidate("name");
}
  1. 统计
public void recordStats() {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(10000)
            .recordStats()
            .build();
    cache.put("公众号", "贝塔学JAVA");
    cache.get("公众号", (t) -> "");
    cache.get("name", (t) -> "silently9527");

    CacheStats stats = cache.stats();
    System.out.println(stats);
}

通过 Cache.stats() 获取到CacheStatsCacheStats提供以下统计方法:

  • hitRate(): 返回缓存命中率
  • evictionCount(): 缓存回收数量
  • averageLoadPenalty(): 加载新值的平均时间

EhCache实现堆缓存

EhCache 是老牌Java开源缓存框架,早在2003年就已经出现了,发展到现在已经非常成熟稳定,在Java应用领域应用也非常广泛,而且和主流的Java框架比如Srping可以很好集成。相比于 Guava Cache,EnCache 支持的功能更丰富,包括堆外缓存、磁盘缓存,当然使用起来要更重一些。使用 Ehcache 的Maven 依赖如下:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.6.3</version>
</dependency>
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);

ResourcePoolsBuilder resource = ResourcePoolsBuilder.heap(10); //设置最大缓存条目数

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10)))
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
  • ResourcePoolsBuilder.heap(10)设置缓存的最大条目数,这是简写方式,等价于ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
  • ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB)设置缓存最大的空间10MB
  • withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10))) 设置缓存空闲时间
  • withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) 设置缓存存活时间
  • remove/removeAll主动失效缓存,与Guava Cache类似,调用方法后不会立即去清除回收,只有在get或者put的时候判断缓存是否过期
  • withSizeOfMaxObjectSize(10,MemoryUnit.KB)限制单个缓存对象的大小,超过这两个限制的对象则不被缓存

堆外缓存

堆外缓存即缓存数据在堆外内存中,空间大小只受本机内存大小限制,不受GC管理,使用堆外缓存可以减少GC暂停时间,但是堆外内存中的对象都需要序列化和反序列化,KEY和VALUE必须实现Serializable接口,因此速度会比堆内缓存慢。在Java中可以通过 -XX:MaxDirectMemorySize 参数设置堆外内存的上限

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
// 堆外内存不能按照存储条目限制,只能按照内存大小进行限制,超过限制则回收缓存
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB);

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withDispatcherConcurrency(4)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withSizeOfMaxObjectSize(10, MemoryUnit.KB)
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo2", cacheConfig);
cache.put("website", "https://silently9527.cn");
System.out.println(cache.get("website"));

磁盘缓存

把缓存数据存放到磁盘上,在JVM重启时缓存的数据不会受到影响,而堆缓存和堆外缓存都会丢失;并且磁盘缓存有更大的存储空间;但是缓存在磁盘上的数据也需要支持序列化,速度会被比内存更慢,在使用时推荐使用更快的磁盘带来更大的吞吐率,比如使用闪存代替机械磁盘。

CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
        .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache"));

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .with(persistentManagerConfig).build(true);

//disk 第三个参数设置为 true 表示将数据持久化到磁盘上
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().disk(100, MemoryUnit.MB, true);

CacheConfiguration<String, String> config = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource).build();
Cache<String, String> cache = persistentCacheManager.createCache("userInfo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

cache.put("公众号", "贝塔学JAVA");
System.out.println(cache.get("公众号"));
persistentCacheManager.close();

在JVM停止时,一定要记得调用persistentCacheManager.close(),保证内存中的数据能够dump到磁盘上。


这是典型 heap + offheap + disk 组合的结构图,上层比下层速度快,下层比上层存储空间大,在ehcache中,空间大小设置 heap > offheap > disk,否则会报错; ehcache 会将最热的数据保存在高一级的缓存。这种结构的代码如下:

CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
        .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache"));

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .with(persistentManagerConfig).build(true);

ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(10, MemoryUnit.MB)
        .offheap(100, MemoryUnit.MB)
        //第三个参数设置为true,支持持久化
        .disk(500, MemoryUnit.MB, true);

CacheConfiguration<String, String> config = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource).build();

Cache<String, String> cache = persistentCacheManager.createCache("userInfo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

//写入缓存
cache.put("name", "silently9527");
// 读取缓存
System.out.println(cache.get("name"));

// 再程序关闭前,需要手动释放资源
persistentCacheManager.close();

分布式集中缓存

前面提到的堆内缓存和堆外缓存如果在多个JVM实例的情况下会有两个问题:1.单机容量毕竟有限;2.多台JVM实例缓存的数据可能不一致;3.如果缓存数据同一时间都失效了,那么请求都会打到数据库上,数据库压力增大。这时候我们就需要引入分布式缓存来解决,现在使用最多的分布式缓存是redis

当引入分布式缓存之后就可以把应用缓存的架构调整成上面的结构。

缓存使用模式的实践

缓存使用的模式大概分为两类:Cache-Aside、Cache-As-SoR(SoR表示实际存储数据的系统,也就是数据源)

Cache-Aside

业务代码围绕着缓存来写,通常都是从缓存中来获取数据,如果缓存没有命中,则从数据库中查找,查询到之后就把数据放入到缓存;当数据被更新之后,也需要对应的去更新缓存中的数据。这种模式也是我们通常使用最多的。

  • 读场景
value = cache.get(key); //从缓存中读取数据
if(value == null) {
    value = loadFromDatabase(key); //从数据库中查询
    cache.put(key, value); //放入到缓存中
}
  • 写场景
wirteToDatabase(key, value); //写入到数据库
cache.put(key, value); //放入到缓存中 或者 可以删除掉缓存 cache.remove(key) ,再读取的时候再查一次

Spring的Cache扩展就是使用的Cache-Aside模式,Spring为了把业务代码和缓存的读取更新分离,对Cache-Aside模式使用AOP进行了封装,提供了多个注解来实现读写场景。官方参考文档:[](https://docs.spring.io/spring...

  • @Cacheable : 通常是放在查询方法上,实现的就是Cache-Aside读的场景,先查缓存,如果不存在在查询数据库,最后把查询出来的结果放入到缓存。
  • @CachePut : 通常用在保存更新方法上面,实现的就是Cache-Aside写的场景,更新完成数据库后把数据放入到缓存中。
  • @CacheEvict : 从缓存中删除指定key的缓存
对于一些允许有一点点更新延迟基础数据可以考虑使用canal订阅binlog日志来完成缓存的增量更新。

Cache-Aside还有个问题,如果某个时刻热点数据缓存失效,那么会有很多请求同时打到后端数据库上,数据库的压力会瞬间增大

Cache-As-SoR

Cache-As-SoR模式也就会把Cache看做是数据源,所有的操作都是针对缓存,Cache在委托给真正的SoR去实现读或者写。业务代码中只会看到Cache的操作,这种模式又分为了三种

Read Through

应用程序始终从缓存中请求数据,如果缓存中没有数据,则它负责使用提供的数据加载程序从数据库中检索数据,检索数据后,缓存会自行更新并将数据返回给调用的应用程序。Gauva Cache、Caffeine、EhCache都支持这种模式;

  1. Caffeine实现Read Through

由于Gauva Cache和Caffeine实现类似,所以这里只展示Caffeine的实现,以下代码来自Caffeine官方文档

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Lookup and compute an entry if absent, or null if not computable
Graph graph = cache.get(key);
// Lookup and compute entries that are absent
Map<Key, Graph> graphs = cache.getAll(keys);

在build Cache的时候指定一个CacheLoader

  • [1] 在应用程序中直接调用cache.get(key)
  • [2] 首先查询缓存,如果缓存存在就直接返回数据
  • [3] 如果不存在,就会委托给CacheLoader去数据源中查询数据,之后在放入到缓存,返回给应用程序
CacheLoader不要直接返回null,建议封装成自己定义的Null对像,在放入到缓存中,可以防止缓存击穿

为了防止因为某个热点数据失效导致后端数据库压力增大的情况,我可以在CacheLoader中使用锁限制只允许一个请求去查询数据库,其他的请求都等待第一个请求查询完成后从缓存中获取,在上一篇 《万字长文聊缓存(上)》中我们聊到了Nginx也有类似的配置参数

value = loadFromCache(key);
if(value != null) {
    return value;
}
synchronized (lock) {
    value = loadFromCache(key);
    if(value != null) {
        return value;
    }
    return loadFromDatabase(key);
}
  1. EhCache实现Read Through
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数
CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withLoaderWriter(new CacheLoaderWriter<String, String>(){
            @Override
            public String load(String key) throws Exception {
                //load from database
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {

            }

            @Override
            public void delete(String key) throws Exception {

            }
        })
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
System.out.println(cache.get("name"));

在EhCache中使用的是CacheLoaderWriter来从数据库中加载数据;解决因为某个热点数据失效导致后端数据库压力增大的问题和上面的方式一样,也可以在load中实现。

Write Through

和Read Through模式类似,当数据进行更新时,先去更新SoR,成功之后在更新缓存。

  1. Caffeine实现Write Through
Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .writer(new CacheWriter<String, String>() {
            @Override
            public void write(@NonNull String key, @NonNull String value) {
                //write data to database
                System.out.println(key);
                System.out.println(value);
            }

            @Override
            public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause removalCause) {
                //delete from database
            }
        })
        .build();

cache.put("name", "silently9527");

Caffeine通过使用CacheWriter来实现Write Through,CacheWriter可以同步的监听到缓存的创建、变更和删除操作,只有写成功了才会去更新缓存

  1. EhCache实现Write Through
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数
CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withLoaderWriter(new CacheLoaderWriter<String, String>(){
            @Override
            public String load(String key) throws Exception {
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {
                //write data to database
                System.out.println(key);
                System.out.println(value);
            }

            @Override
            public void delete(String key) throws Exception {
                //delete from database
            }
        })
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
System.out.println(cache.get("name"));

cache.put("website","https://silently9527.cn");

EhCache还是通过CacheLoaderWriter来实现的,当我们调用cache.put("xxx","xxx")进行写缓存的时候,EhCache首先会将写的操作委托给CacheLoaderWriter,有CacheLoaderWriter.write去负责写数据源

Write Behind

这种模式通常先将数据写入缓存,再异步地写入数据库进行数据同步。这样的设计既可以减少对数据库的直接访问,降低压力,同时对数据库的多次修改可以合并操作,极大地提升了系统的承载能力。但是这种模式也存在风险,如当缓存机器出现宕机时,数据有丢失的可能。

  1. Caffeine要想实现Write Behind可以在CacheLoaderWriter.write方法中把数据发送到MQ中,实现异步的消费,这样可以保证数据的安全,但是要想实现合并操作就需要扩展功能更强大的CacheLoaderWriter
  2. EhCache实现Write Behind
//1 定义线程池
PooledExecutionServiceConfiguration testWriteBehind = PooledExecutionServiceConfigurationBuilder
        .newPooledExecutionServiceConfigurationBuilder()
        .pool("testWriteBehind", 5, 10)
        .build();

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .using(testWriteBehind)
        .build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //设置最大缓存条目数

//2 设置回写模式配置
WriteBehindConfiguration testWriteBehindConfig = WriteBehindConfigurationBuilder
        .newUnBatchedWriteBehindConfiguration()
        .queueSize(10)
        .concurrencyLevel(2)
        .useThreadPool("testWriteBehind")
        .build();

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withLoaderWriter(new CacheLoaderWriter<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {
                //write data to database
            }

            @Override
            public void delete(String key) throws Exception {
            }
        })
        .add(testWriteBehindConfig)
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);

首先使用PooledExecutionServiceConfigurationBuilder定义了线程池配置;然后使用WriteBehindConfigurationBuilder设置会写模式配置,其中newUnBatchedWriteBehindConfiguration表示不进行批量写操作,因为是异步写,所以需要把写操作先放入到队列中,通过queueSize设置队列大小,useThreadPool指定使用哪个线程池; concurrencyLevel设置使用多少个并发线程和队列进行Write Behind

EhCache实现批量写的操作也很容易

  • 首先把newUnBatchedWriteBehindConfiguration()替换成newBatchedWriteBehindConfiguration(10, TimeUnit.SECONDS, 20),这里设置的是数量达到20就进行批处理,如果10秒内没有达到20个也会进行处理
  • 其次在CacheLoaderWriter中实现wirteAll 和 deleteAll进行批处理
如果需要把对相同的key的操作合并起来只记录最后一次数据,可以通过enableCoalescing()来启用合并

写到最后 点关注,不迷路

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

源码地址:https://github.com/silently9527/CacheTutorial

公众号:贝塔学JAVA

原文地址:https://silently9527.cn/archives/94
查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 1月4日

万字长文聊缓存(上)- Http缓存

深入解析SpringMVC核心原理:从手写简易版MVC框架开始(SmartMvc) : https://github.com/silently9527/SmartMvc

IDEA多线程文件下载插件: https://github.com/silently9527/FastDownloadIdeaPlugin

公众号:贝塔学JAVA

摘要

缓存的目的是为了提高系统的访问速度,让数据更加接近于使用者,通常也是提升性能的常用手段。缓存在生活中其实也是无处不在,比如物流系统,他们基本上在各地都有分仓库,如果本地仓库有数据,那么送货的速度就会很快;CPU读取数据也采用了缓存,寄存器->高速缓存->内存->硬盘/网络;我们经常使用的maven仓库也同样有本地仓库和远程仓库。现阶段缓存的使用场景也越来越多,比如:浏览器缓存、反向代理层缓存、应用层缓存、数据库查询缓存、分布式集中缓存。

本文我们就先从浏览器缓存和Nginx缓存开始聊起。

浏览器缓存

浏览器缓存是指当我们去访问一个网站或者Http服务的时候,服务器可以设置Http的响应头信息,其中如果设置缓存相关的头信息,那么浏览器就会缓存这些数据,下次再访问这些数据的时候就直接从浏览器缓存中获取或者是只需要去服务器中校验下缓存时候有效,可以减少浏览器与服务器之间的网络时间的开销以及节省带宽。

Htpp相关的知识,欢迎去参观 《面试篇》Http协议

Cache-Control

该命令是通用首部字段(请求首部和响应首部都可以使用),用于控制缓存的工作机制,该命令参数稍多,常用的参数:

  • no-cache: 表示不需要缓存该资源
  • max-age(秒): 缓存的最大有效时间,当max-age=0时,表示不需要缓存

Expires

控制资源失效的日期,当浏览器接受到Expires之后,浏览器都会使用本地的缓存,在过期日期之后才会向务器发送请求;如果服务器同时在响应头中也指定了Cache-Controlmax-age指令时,浏览器会优先处理max-age
如果服务器不想要让浏览器对资源缓存,可以把Expires和首部字段Date设置相同的值

Last-Modified / If-Modified-Since

Last-Modified

Last-Modified 用于指明资源最终被修改的时间。配合If-Modified-Since一起使用可以通过时间对缓存是否有效进行校验;后面实战会使用到这种方式。

If-Modified-Since

如果请求头中If-Modified-Since的日期早于请求资源的更新日期,那么服务会进行处理,返回最新的资源;如果If-Modified-Since指定的日期之后请求的资源都未更新过,那么服务不会处理请求并返回304 Mot Modified的响应,表示缓存的文件有效可以继续使用。

实战事例

使用SpringMVC做缓存的测试代码:

@ResponseBody
@RequestMapping("/http/cache")
public ResponseEntity<String> cache(@RequestHeader(value = "If-Modified-Since", required = false)
                                            String ifModifiedSinceStr) throws ParseException {

    DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    Date ifModifiedSince = dateFormat.parse(ifModifiedSinceStr);

    long lastModifiedDate = getLastModifiedDate(ifModifiedSince);//获取文档最后更新时间
    long now = System.currentTimeMillis();
    int maxAge = 30; //数据在浏览器端缓存30秒

    //判断文档是否被修改过
    if (Objects.nonNull(ifModifiedSince) && ifModifiedSince.getTime() == lastModifiedDate) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间
        headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //设置过期时间
        headers.add("Cache-Control", "max-age=" + maxAge);
        return new ResponseEntity<>(headers, HttpStatus.NOT_MODIFIED);
    }

    //文档已经被修改过
    HttpHeaders headers = new HttpHeaders();
    headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间
    headers.add("Last-Modified", dateFormat.format(new Date(lastModifiedDate))); //设置最近被修改的日期
    headers.add("Expires", dateFormat.format(new Date(now + maxAge * 1000))); //设置过期时间
    headers.add("Cache-Control", "max-age=" + maxAge);

    String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn"));
    return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);

}

//获取文档的最后更新时间,方便测试,每15秒换一次;去掉毫秒值
private long getLastModifiedDate(Date ifModifiedSince) {
    long now = System.currentTimeMillis();

    if (Objects.isNull(ifModifiedSince)) {
        return now;
    }

    long seconds = (now - ifModifiedSince.getTime()) / 1000;
    if (seconds > 15) {
        return now;
    }
    return ifModifiedSince.getTime();
}
  1. 当第一次访问http://localhost:8080/http/cache的时候,我们可以看到如下的响应头信息:

前面我们已提到了Cache-Control的优先级高于Expires,实际的项目中我们可以同时使用,或者只使用Cache-ControlExpires的值通常情况下都是系统当前时间+缓存过期时间

  1. 当我们在15秒之内再次访问http://localhost:8080/http/cache会看到如下的请求头:

此时发送到服务器端的头信息If-Modified-Since就是上次请求服务器返回的Last-Modified,浏览器会拿这个时间去和服务器校验内容是否发送了变化,由于我们后台程序在15秒之内都表示没有修改过内容,所以得到了如下的响应头信息

响应的状态码304,表示服务器告诉浏览器,你的缓存是有效的可以继续使用。

If-None-Match / ETag

If-None-Match

请求首部字段If-None-Match传输给服务器的值是服务器返回的ETag值,只有当服务器上请求资源的ETag值与If-None-Match不一致时,服务器才去处理该请求。

ETag

响应首部字段ETag能够告知客服端响应实体的标识,它是一种可将资源以字符串的形式做唯一标识的方式。服务器可以为每份资源指定一个ETag值。当资源被更新时,ETag的值也会被更新。通常生成ETag值的算法使用的是md5。

  • 强ETag值:不论实体发生了多么细微的变化都会改变其值
  • 弱ETag值:只用于提示资源是否相同,只有当资源发送了根本上的变化,ETag才会被改变。使用弱ETag值需要在前面添加W/
ETag: W/"etag-xxxx"

通常建议选择弱ETag值,因为大多数时候我们都会在代理层开启gzip压缩,弱ETag可以验证压缩和不压缩的实体,而强ETag值要求响应实体字节必须完全一致。

实战事例

@ResponseBody
@RequestMapping("/http/etag")
public ResponseEntity<String> etag(@RequestHeader(value = "If-None-Match", required = false)
                                           String ifNoneMatch) throws ParseException {
    long now = System.currentTimeMillis();
    int maxAge = 30; //数据在浏览器端缓存30秒

    String responseBody = JSON.toJSONString(ImmutableMap.of("website", "https://silently9527.cn"));
    String etag = "W/\"" + MD5Encoder.encode(responseBody.getBytes()) + "\""; //弱ETag值

    if (etag.equals(ifNoneMatch)) {
        return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
    }

    DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    HttpHeaders headers = new HttpHeaders();
    headers.add("ETag", etag);
    headers.add("Date", dateFormat.format(new Date(now))); //设置当前时间
    headers.add("Cache-Control", "max-age=" + maxAge);

    return new ResponseEntity<>(responseBody, headers, HttpStatus.OK);
}

ETag是用于发送到服务器端进行内容变更验证的,第一次请求http://localhost:8080/http/etag,获取到的响应头信息:

在30秒之内,我们再次刷新页面,可以看到如下的请求头信息:

这里的If-None-Match就是上一次请求服务返回的ETag值,服务器校验If-None-Match值与ETag值相等,所以返回了304告诉浏览器缓存可以用。

ETag与Last-Modified两者应该如何选择?

通过上面的两个事例我们可以看出ETag需要服务器先查询出需要响应的内容,然后计算出ETag值,再与浏览器请求头中If-None-Match来比较觉得是否需要返回数据,对于服务器来说仅仅是节省了带宽,原本应该服务器调用后端服务查询的信息依然没有被省掉;而Last-Modified通过时间的比较,如果内容没有更新,服务器不需要调用后端服务查询出响应数据,不仅节省了服务器的带宽也降低了后端服务的压力;

对比之后得出结论:通常来说为了降低后端服务的压力ETag适用于图片/js/css等静态资源,而类似用户详情信息需要调用后端服务的数据适合使用Last-Modified来处理

Nginx

通常情况下我们都会使用到Nginx来做反向代理服务器,我们可以通过缓冲、缓存来对Nginx进行调优,本篇我们就从这两个方面来聊聊Nginx调优

缓冲

默认情况下,Nginx在返回响应给客户端之前会尽可能快的从上游服务器获取数据,Nginx会尽可能的将上有服务器返回的数据缓冲到本地,然后一次性的全部返回给客户端,如果每次从上游服务器返回的数据都需要写入到磁盘中,那么Nginx的性能肯定会降低;所以我们需要根据实际情况对Nginx的缓存做优化。

  • proxy_buffer_size: 设置Nginx缓冲区的大小,用来存储upstream端响应的header。
  • proxy_buffering: 启用代理内容缓冲,当该功能禁用时,代理一接收到上游服务器的返回就立即同步的发送给客户端,proxy_max_temp_file_size被设置为0;通过设置proxy_buffering为on,proxy_max_temp_file_size为0 可以确保代理的过程中不适用磁盘,只是用缓冲区; 开启后proxy_buffersproxy_busy_buffers_size参数才会起作用
  • proxy_buffers: 设置响应上游服务器的缓存数量和大小,当一个缓冲区占满后会申请开启下一个缓冲区,直到缓冲区数量到达设置的最大值
  • proxy_busy_buffers_size: proxy_busy_buffers_size不是独立的空间,他是proxy_buffersproxy_buffer_size的一部分。nginx会在没有完全读完后端响应就开始向客户端传送数据,所以它会划出一部分busy状态的buffer来专门向客户端传送数据(建议为proxy_buffers中单个缓冲区的2倍),然后它继续从后端取数据。
    proxy_busy_buffer_size参数用来设置处于busy状态的buffer有多大。

1)如果完整数据大小小于busy_buffer大小,当数据传输完成后,马上传给客户端;

2)如果完整数据大小不小于busy_buffer大小,则装满busy_buffer后,马上传给客户端;

典型是设置成proxy_buffers的两倍。

Nginx代理缓冲的设置都是作用到每一个请求的,想要设置缓冲区的大小到最佳状态,需要测量出经过反向代理服务器器的平均请求数和响应的大小;proxy_buffers指令的默认值 8个 4KB 或者 8个 8KB(具体依赖于操作系统),假如我们的服务器是1G,这台服务器只运行了Nginx服务,那么排除到操作系统的内存使用,保守估计Nginx能够使用的内存是768M

  1. 每个活动的连接使用缓冲内存:8个4KB = 8 4 1024 = 32768字节
  2. 系统可使用的内存大小768M: 768 1024 1024 = 805306368字节
  3. 所以Nginx能够同时处理的连接数:805306368 / 32768 = 24576

经过我们的粗略估计,1G的服务器只运行Nginx大概可以同时处理24576个连接。

假如我们测量和发现经过反向代理服务器响应的平均数据大小是 900KB , 而默认的 8个4KB的缓冲区是无法满足的,所以我们可以调整大小

http {
    proxy_buffers 30 32k;
}

这样设置之后每次请求可以达到最快的响应,但是同时处理的连接数减少了,(768 * 1024 * 1024) / (30 * 32 * 1024)=819个活动连接;

如果我们系统的并发数不是太高,我们可以将proxy_buffers缓冲区的个数下调,设置稍大的proxy_busy_buffers_size加大往客户端发送的缓冲区,以确保Nginx在传输的过程中能够把从上游服务器读取到的数据全部写入到缓冲区中。

http {
    proxy_buffers 10 32k;
    proxy_busy_buffers_size 64k;
}

缓存

Nignx除了可以缓冲上游服务器的响应达到快速返回给客户端,它还可以是实现响应的缓存,通过上图我们可以看到

  • 1A: 一个请求到达Nginx,先从缓存中尝试获取
  • 1B: 缓存不存在直接去上游服务器获取数据
  • 1C: 上游服务器返回响应,Nginx把响应放入到缓存
  • 1D: 把响应返回到客户端
  • 2A: 另一个请求达到Nginx, 到缓存中查找
  • 2B: 缓存中有对应的数据,直接返回,不去上游服务器获取数据

Nginx的缓存常用配置:

  • proxy_cache_path: 放置缓存响应和共享的目录。levels 设置缓存文件目录层次, levels=1:2 表示两级目录,最多三层,其中 1 表示一级目录使用一位16进制作为目录名,2 表示二级目录使用两位16进制作为目录名,如果文件都存放在一个目录中,文件量大了会导致文件访问变慢。keys_zone设置缓存名字和共享内存大小,inactive 当被放入到缓存后如果不被访问的最大存活时间,max_size设置缓存的最大空间
  • proxy_cache: 定义响应应该存放到哪个缓存区中(keys_zone设置的名字)
  • proxy_cache_key: 设置缓存使用的Key, 默认是完整的访问URL,可以自己根据实际情况设置
  • proxy_cache_lock: 当多个客户端同时访问一下URL时,如果开启了这个配置,那么只会有一个客户端会去上游服务器获取响应,获取完成后放入到缓存中,其他的客户端会等待从缓存中获取。
  • proxy_cache_lock_timeout: 启用了proxy_cache_lock之后,如果第一个请求超过了proxy_cache_lock_timeout设置的时间默认是5s,那么所有等待的请求会同时到上游服务器去获取数据,可能会导致后端压力增大。
  • proxy_cache_min_uses: 设置资源被请求多少次后才会被缓存
  • proxy_cache_use_stale: 在访问上游服务器发生错误时,返回已经过期的数据给客户端;当缓存内容对于过期时间不敏感,可以选择采用这种方式
  • proxy_cache_valid: 为不同响应状态码设置缓存时间。如果设置proxy_cache_valid 5s,那么所有的状态码都会被缓存。

设置所有的响应被缓存后最大不被访问的存活时间6小时,缓存的大小设置为1g,缓存的有效期是1天,配置如下:

http {
    proxy_cache_path /export/cache/proxy_cache keys_zone=CACHE:10m levels=1:2 inactive=6h max_size=1g;
    server {
        location / {
            proxy_cache CACHE; //指定存放响应到CACHE这个缓存中
            proxy_cache_valid 1d; //所有的响应状态码都被缓存1d
            proxy_pass: http://upstream;
        }
    }
}

如果当前响应中设置了Set-Cookie头信息,那么当前的响应不会被缓存,可以通过使用proxy_ignore_headers来忽略头信息以达到缓存

proxy_ignore_headers Set-Cookie

如果这样做了,我们需要把cookie中的值作为proxy_cache_key的一部分,防止同一个URL响应的数据不同导致缓存数据被覆盖,返回到客户端错误的数据

proxy_cache_key "$host$request_uri$cookie_user"

注意,这种情况还是有问题,因为在缓存的key中添加cookie信息,那么可能导致公共资源被缓存多份导致浪费空间;要解决这个问题我们可以把不同的资源分开配置,比如:

server {
    proxy_ignore_headers Set-Cookie;
    
    location /img {
        proxy_cache_key "$host$request_uri";
        proxy_pass http://upstream;
    }
    
    
    location / {
        proxy_cache_key "$host$request_uri$cookie_user";
        proxy_pass http://upstream;
    }
}

清理缓存

虽然我们设置了缓存加快了响应,但是有时候会遇到缓存错误的请求,通常我们需要为自己开一个后面,方便发现问题之后通过手动的方式及时的清理掉缓存。Nginx可以考虑使用ngx_cache_purge模块进行缓存清理。

location ~ /purge/.* {
    allow 127.0.0.1;
    deny all;
    proxy_cache_purge cache_one $host$1$is_args$args
}

该方法要限制访问权限proxy_cache_purge缓存清理的模块,cache_one指定的key_zone,$host$1$is_args$args 指定的生成缓存key的参数

存储

如果有大的静态文件,这些静态文件基本不会别修改,那么我们就可以不用给它设置缓存的有效期,让Nginx直接存储这些文件直接。如果上游服务器修改了这些文件,那么可以单独提供一个程序把对应的静态文件删除。

http {
    proxy_temp_path /var/www/tmp;
    
    server {
        root /var/www/data;
        
        location /img {
            error_page 404 = @store
        }
        
        location @store {
            internal;
            proxy_store on;
            proxy_store_access group:r all:r;
            proxy_pass http://upstream;
        }
    }
}

请求首先会去/img中查找文件,如果不存在再去上游服务器查找;internal 指令用于指定只允许来自本地 Nginx 的内部调用,来自外部的访问会直接返回 404 not found 状态。proxy_store表示需要把从上游服务器返回的文件存储到 /var/www/dataproxy_store_access设置访问权限

总结

  • Cache-ControlExpires 设置资源缓存的有效期
  • 使用Last-Modified / If-Modified-Since判断缓存是否有效
  • 使用If-None-Match / ETag判断缓存是否有效
  • 通过配置Nginx缓冲区大小对Nginx调优
  • 使用Nginx缓存加快请求响应速度

如何加快请求响应的速度,本篇我们主要围绕着Http缓存和Nignx反向代理两个方面来聊了缓存,你以为这样就完了吗,不!下一篇我们将从应用程序的维度来聊聊缓存

写到最后 点关注,不迷路

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

公众号:贝塔学JAVA

原文地址:https://silently9527.cn/archives/94

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-30

IDEA插件:多线程文件下载插件开发

摘要

上周使用Java开发了大文件多线程下载工具类,自己平时的文件下载也在使用这个工具,下载速度确实提升不少,但是每次下载都要去打开项目运行代码,觉得实在不是很方便;考虑到每天我们都会使用到IDEA开发工具,所以就决定把这个下载工具做成IDEA的插件,文章末尾附上插件下载地址。

Java实现大文件多线程下载

IDEA多线程文件下载插件

不要忘记star哟

IDEA插件介绍

IntelliJ IDEA是目前最好用的JAVA开发IDE,它本身的功能已经非常强大了,但是可能我们会遇到一些定制的需求,比如说:自定义代码生成器;这时候就需要我们自己动手来写一个插件,如果只是想要开发简单的功能其实只要掌握了Java Swing,那么开发IDEA的插件是很容易的,如果想学习更多的原理和设计理念可以看IntelliJ Platform SDK的官方文档。

IDEA插件开发步骤

1. 创建Gradle的插件工程

创建完成项目之后,我们可以看一下resource/META-INF/plugin.xml

<idea-plugin>
    <id>cn.silently9527.fast-download-idea-plugin</id>   <!-- 插件的ID -->
    <name>FastDownloadPlugin</name> <!-- 插件的名字,会在插件中心展示 -->
    <vendor email="380303318@qq.com" url="https://silently9527">Silently9527</vendor>
    <!--插件说明-->
    <description><![CDATA[
    多线程文件下载器
    ]]></description>

    <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
         on how to target different products -->
    <!-- uncomment to enable plugin in all products
    <depends>com.intellij.modules.lang</depends>
    -->

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
    </extensions>

    <actions>
        <!-- Add your actions here -->
    </actions>
</idea-plugin>

2. 创建一个Action

在IDEA的插件开发中,基本都会使用到Action,Action其实就是事件的处理器,就好比JS中的onClick方法。在IDEA中创建一个Action十分简单,通过图形化界面就可以完成

创建完成后就可以看到Action类

public class FastDownloadAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {

}
}

plugin.xml中可以看到生成的Action信息

<action id="fast.download" class="cn.silently9527.FastDownloadAction" text="FastDownload" description="文件多线程下载">
    <add-to-group group-id="ToolsMenu" anchor="last"/>
    <keyboard-shortcut keymap="$default" first-keystroke="shift D"/>
</action>

3. 创建输入下载信息的弹窗

IDEA插件的SDK已经对弹窗进行的封装,只需要继承DialogWrapper即可,界面上的绘制工作都在createCenterPanel方法中,组件的布局与JavaSwing类似

@Nullable
@Override
protected JComponent createCenterPanel() {
    Box verticalBox = Box.createVerticalBox();
    verticalBox.add(createUrlBox());
    verticalBox.add(Box.createVerticalStrut(10));
    verticalBox.add(createFileDirJPanel());
    verticalBox.add(Box.createVerticalStrut(10));
    verticalBox.add(createThreadNumJPanel());
    return verticalBox;
}

我们需要对输入的下载地址和存放的路径的参数进行校验,判断输入是否正确,可以实现方法doValidate,校验通过返回null,校验不通过返回ValidationInfo对象

@Nullable
@Override
protected ValidationInfo doValidate() {
    if (StringUtils.isBlank(downloadUrlField.getText())) {
        return new ValidationInfo("文件下载地址必填");
    }
    if (StringUtils.isBlank(fileDirField.getText())) {
        return new ValidationInfo("文件保存目录必填");
    }
    if (StringUtils.isBlank(threadNumField.getText())) {
        return new ValidationInfo("下载线程数必填");
    }
    return null;
}

最终界面完成后的效果

4. 在FastDownloadAction中获取弹窗输入的下载信息

DownloadDialog downloadDialog = new DownloadDialog();
if (downloadDialog.showAndGet()) {
    // 用户点击OK之后进入到这里
}

当用户点击了OK,输入信息检验通过后我们就可以开始下载文件了,由于之前做的下载组件是同步调用,为了不阻塞界面操作,需要使用线程异步下载

CompletableFuture.runAsync(() -> {
    try {
        Downloader downloader = new MultiThreadFileDownloader(threadNum, downloadProgressPrinter);
        downloader.download(downloadURL, downloadDir);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
})

在下载的过程中,需要给用户反馈,让用户知道当前下载的进度是多少,以及当前下载的速度是多少

//使用SDK开启一个后台任务线程
ProgressManager.getInstance().run(new Task.Backgroundable(project, "File Downloading") {
    private long tmpAlreadyDownloadLength; //当前已下载字节数
    private long speed; //每秒下载速度

    public void run(@NotNull ProgressIndicator progressIndicator) {
        // start your process
        while (true) {
            long alreadyDownloadLength = downloadProgressPrinter.getAlreadyDownloadLength();
            long contentLength = downloadProgressPrinter.getContentLength();
            if (alreadyDownloadLength != 0 && alreadyDownloadLength >= contentLength) {
                // 下载已完成,进度条显示100%
                progressIndicator.setFraction(1.0);
                progressIndicator.setText("finished");
                break;
            }
            setProgressIndicator(progressIndicator, contentLength, alreadyDownloadLength);
            sleep();
        }
    }

    private void setProgressIndicator(ProgressIndicator progressIndicator, long contentLength,
                                      long alreadyDownloadLength) {
        if (alreadyDownloadLength == 0 || contentLength == 0) {
            return;
        }
        speed = alreadyDownloadLength - tmpAlreadyDownloadLength;
        tmpAlreadyDownloadLength = alreadyDownloadLength;

        double value = (double) alreadyDownloadLength / (double) contentLength;

        double fraction = Double.parseDouble(String.format("%.2f", value));
        progressIndicator.setFraction(fraction);
        String text = "already download " + fraction * 100 + "% ,speed: " + (speed / 1000) + "KB";
        progressIndicator.setText(text); //进度条显示已下载百分比,下载速度
    }
});

测试多线程下载文件

测试下载820M的idea ,地址:https://download.jetbrains.86...

插件安装

下载插件之后,选择本地安装

总结

  • IDEA插件介绍
  • IDEA插件开发的基本步骤
  • 实现了多线程文件下载插件
目前测试过程中发现文件下载速度计算不太准确,个别线程的下载速度未能统计在内,后期继续优化。

插件下载链接: https://pan.baidu.com/s/1cmzKgmu8JwUa-liWmNl5jw 提取码: 3f4t

写到最后 点关注,不迷路

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,创作不易,请不要白嫖,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

微信公众号:贝塔学JAVA

原文地址:https://silently9527.cn/archives/93

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-28

深入解析SpringMVC核心原理:从手写简易版MVC框架开始(SmartMvc)

简介

SpringMVC可以说的上是当前最优秀的MVC框架,采用了松散耦合可插拔组件结构,比其他MVC框架更具扩展性和灵活性;为了提高框架的扩展性和灵活性,设计了松耦合可插拔的组件。理解SpringMVC的原理,在面试或工作中都十分的重要。

SpringMVC的原理在网络上到处都可以找得到,但是写的都很概括、零散;对应阅读源码经验较少的小伙伴来说,
自己去看源码被很多细节所干扰阻碍,不能够很好的抽离出springMVC原理的主线。

自己想和小伙伴一起从手写简易版的SmartMVC框架出发,理出SpringMVC的主线并深入理解SpringMVC的原理。框架代码开发加上文档编写大概花费时间一个月


项目结构

SmartMvc
├── docs -- 开发文档
├── smart-mvc -- 实现mvc功能的核心代码
├── smartmvc-springboot-autoconfigure -- SmartMvc的自动化配置
├── smartmvc-springboot-demo -- SmartMvc的demo项目
├── smartmvc-springboot-starter -- SmartMvc的starter
└── spring-mvc-demo -- SpringMVC的demo

IDE、源码、依赖版本

大家记得顺手给个star哦

约定

  • 为了便于后期理解和使用SpringMVC,所以在SmartMVC中所有组件的名称都和SpringMVC的保持一致
  • 为了让SpringMVC的核心流程更加的清晰,减少的干扰,我拿出了自己18米的砍刀大胆的砍掉了SpringMVC中很多细节流程,达到去枝干立主脑,让我们能够更加顺畅的理解请求的处理过程

文档目录

所有开发文档都在项目的docs目录下

  • 01 SmartMVC总体架构规划
  • 02 RequestMappingHandlerMapping初始化过程
  • 03 拦截器HandlerInterceptor
  • 04 HandlerMapping获取对应的Handler
  • 05 参数解析器HandlerMethodArgumentResolver
  • 06 返回解析器HandlerMethodReturnValueHandler
  • 07 Handler执行器InvocableHandlerMethod
  • 08 实现RequestMappingHandlerAdapter
  • 09 视图InternalResourceView、RedirectView
  • 10 视图解析器ViewResolver
  • 11 DispatcherServlet实现doDispatch来完成请求逻辑
  • 12 全局异常处理器HandlerExceptionResolver
  • 13 核心配置类WebMvcConfigurationSupport
  • 14 SmartMvc与SpringBoot集成(一)
  • 15 SmartMvc与SpringBoot集成(二)
  • 16 SmartMvc项目实战

SpringBoot项目中引入SmartMVC的步骤

1. 新建一个SpringBoot项目,在pom.xml中加入SmartMVC的starter

<dependency>
    <groupId>com.silently9527</groupId>
    <artifactId>smartmvc-springboot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2. 修改SpringBoot生成的启动类,指定SmartMVC的ApplicationContextClass

@SpringBootApplication
public class SmartmvcSpringbootDemoApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SmartmvcSpringbootDemoApplication.class);
        application.setApplicationContextClass(ServletWebServerApplicationContext.class);
        application.run(args);
    }
}

写到最后(点关注,不迷路)

在开发文档中可能会存在错误或不足之处,欢迎大家指出。

创作不易,希望朋友们可以点赞评论关注三连

原文地址,转载请注明出处:https://silently9527.cn/archives/88
查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-22

突破某度云盘下载限速,提速30倍!想学?我教你啊

前言

在上一篇文章 《面试官不讲武德》对Java初级程序猿死命摩擦Http协议 中,我们有提到大文件下载和断点续传,本篇我们就来开发一个多线程文件下载器,最后我们用这个多线程下载器来突破百度云盘下载的限速。

兄弟们看到这个标题可能会觉得是个标题党,为了解决疑虑,我们先来看下最终的测试结果:

测试百度云下载的文件 46M,自己本地最大下载速度 2M

1. 单线程下载,总耗时: 603s

2. 多线程下载,50个线程,总耗时:13s

测试结果,提速46倍,我还是太谦虚了,只说提速30倍,此处我们觉得应该有掌声(我听不到,还是点赞实在)

源码地址: https://gitee.com/silently9527/fast-download

喜欢请记得star哦


HTTP协议Range请求头

Range主要是针对只需要获取部分资源的范围请求,通过指定Range即可告知服务器资源的指定范围。格式: Range: bytes=start-end

比如:
获取字节范围 5001-10000

Range: bytes=5001-10000

也可以指定开始位置不指定结束位置,表示获取开始位置之后的全部数据

Range: bytes=5001-

服务器接收到带有Range的请求,会在处理请求之后返回状态码为206 Partial Content的响应。

基于Range的特性,我们就可以实现文件的多线程下载,文件的断点续传

准备工作

本文我们使用的SpringMVC中的RestTemplate;由于百度云的链接是Https,所以我们需要设置RestTemplate绕过证书验证

  1. pom.xml

pom.xml

  1. 编写RestTemplate的构造器,以及绕过https的证书验证

RestTemplateBuilder

  1. 在下载的过程中,我们需要知道当前下载的速度是多少,所以需要定义一个显示下载速度的接口

DisplayDownloadSpeed

因为计算下载速度,我们需要知道每秒传输的字节数是多少,为了监控传输数据的过程,我们需要了解SpringMVC中的接口ResponseExtractor

ResponseExtractor

该接口只有一个方法,当客户端和服务器端连接建立之后,会调用这个方法,我们可以在这个方法中监控下载的速度。

  1. DisplayDownloadSpeed接口的抽象实现 AbstractDisplayDownloadSpeedResponseExtractor

AbstractDisplayDownloadSpeedResponseExtractor

  1. 整个项目主要涉及到的类图

简单的文件下载器

这里使用的是restTemplate调用execute, 先文件获取到字节数组, 再将字节数组直接写到目标文件。

这里我们需要注意的点是: 这种方式会将文件的字节数组全部放入内存中, 及其消耗资源;我们来看看如何实现。

  1. 创建ByteArrayResponseExtractor类继承AbstractDisplayDownloadSpeedResponseExtractor

ByteArrayResponseExtractor

  1. 调用restTemplate.execute执行下载,保存字节数据到文件中

  1. 测试下载819M的idea

执行一段时间之后,我们可以看到内存已经使用了800M左右,所以这种方式只能使用于小文件的下载,如果我们下载几G的大文件,内存肯定是不够用的。至于下载时间,因为文件太大也没有等下载完成就结束了程序。

单线程大文件下载

上面的方式只能下载小的文件,那大文件的下载我们该用什么方式呢?我们可以把流输出到文件而不是内存中。接下来我们来实现我们大文件的下载。

  1. 创建FileResponseExtractor类继承AbstractDisplayDownloadSpeedResponseExtractor,把流输出到文件中

  1. 文件下载器,先把流输出到临时下载文件(xxxxx.download),下载完成后在重命名文件

  1. 测试下载819M的idea

执行一段时间之后,我们再看看下内存的使用情况,发现这种方式内存消耗较少,效果比较理想,下载时间:199s

多线程文件下载

如果服务器不限速的话,通常能够把自己本地的带宽给跑满,那么使用单线程下载就够了,但是如果遇到服务器限速,下载速度远小于自己本地的带宽,那么可以考虑使用多线程下载。多线程我们使用CompletableFuture(可以参考文章 CompletableFuture让你的代码免受阻塞之苦)。

实现多线程文件下载的基本流程:

  1. 首先我们通过Http协议的Head方法获取到文件的总大小
  2. 然后根据设置的线程数均分文件的大小,计算每个线程的下载的字节数据开始位置和结束位置
  3. 开启线程,设置HTTP请求头Range信息,开始下载数据到临时文件
  4. 下载完成后把每个线程下载完成的临时文件合并成一个文件

完成代码如下:

  1. 开启30个线程测试下载819M的idea

从执行的结果上来看,因为开启了30个线程同时在下载,内存的占用要比单线程消耗的多,但是也在接受范围内,下载时间:81s,速度提升2.5倍,这是因为idea的下载服务器没有限速,本次多线程速度的提升仅仅是在充分的压榨本地的带宽,所以提示的幅度不大。

单线程下载和对线程下载对比测试

因为百度云盘对单个线程的下载速度做了限制,大概是在100kb,所以我们使用百度云盘的下载链接,来测试多线程和单线程的下载速度。

  1. 测试 百度云盘中 46M 的文件的下载速度,自己本地最大下载速度 2M
  2. 获取文件的下载地址

注意:从浏览器中获取的链接需要先使用URLDecode解码,否则下载会失败,并且百度云盘文件的下载链接是有时效性的,过期后就不能在下载,需要重新生成下载链接

测试单线程下载文件

执行的结果可以看出,百度云对单线程的下载限速真的是丧心病狂, 46M的文件下载需要耗时: 600s

测试多线程下载文件

为了充分的压榨网速,找出最合适的线程数,所以测试了不同线程数的下载速度

线程数下载总耗时
1060s
2030s
3021s
4015s
5013s

从测试的结果上来看,对于自己的运行环境把线程数设置在30个左右比较合适


文件断点续传如何实现,欢迎在大家评论区说出自己的思路。

写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,创作不易,请不要白嫖,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

原创不易 转载请注明出处:

本文纯粹用于学习

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-21

《面试官不讲武德》对Java初级程序猿死命摩擦Http协议

前言

我被Hr领进了一个小黑屋,让我在这里等面试官,过来一会,一位穿着拖鞋的中年男子走了进来,看着他绝顶聪明的发际线,知道这肯定是位大佬,我心里倍感到了压力;

面试官果然不是盖的,刚坐下后就开始立即暴力输出了


面试官:我看你简历上写了熟悉Http协议,当我们使用浏览器访问网址: https://silently9527.cn会发生什么?

我:(这尼玛就是怕被搞事情所以没写精通,这也被搞。会发生什么,当然是展示出网页啊,大脑飞速旋转,脖子快断的时候,终于想到面试官可能想要问什么了)

我:英俊潇洒的面试官,你好!

首先浏览器会去访问DNS服务器,查询到域名对应的ip地址是多少,然后浏览器再去访问这个ip地址;如果还要往底层在说的话,就会涉及到tcp/ip的分层,我还是来画张图吧。

服务器返回资源的过程也是类似的方式


面试官:你刚才有谈到tcp/ip的分层,能详细说下吗?

我:(还好前前大学女友没把我当年上课的笔记给扔掉,刚好昨晚找回来温习了一下,温故而知新!只是笔记而已,大家别想歪了!)
插图

我:TCP/IP协议族分为4层:应用层、传输层、网络层、数据链路层

  • 应用层:主要是与应用通信使用到的协议,比如:FTP、DNS、HTTP
  • 传输层:为应用层提供在两台机器之间数据传输,有两种协议:TCP、UDP
  • 网络层:两台机器之间在传输的过程中会经过多个路由器有多条路线,网络层主要是从中选择一条路线
  • 数据链路层:用来处理连接网络的硬件部分,比如说网卡、设备驱动等

面试官:在tcp/ip的分层里面,当客户端发起http请求到达服务端的过程中,数据包的封装以及解包的过程是怎样的?

我:在这个分层中,每次网络请求都会按照分层的顺序与对方进行通信,发送端从应用层往下走,接收端从数据链路层往上走;以Http来举例的话:

  1. 客户端在应用层(Http协议)发起一个HTTP请求
  2. 在传输层(TCP协议)把从应用层收到的Http请求数据包分隔成小的数据包,并打好序
  3. 网络层(IP协议)收到数据包后选择发送路径
  4. 服务器收到数据后按照顺序往上发送,直到应用层收到数据

在发送方每经过一层,就会被加上该层的首部信息,当接收方接受到数据后,在每一个层会去掉对应的首部信息


面试官:TCP如何保证数据可靠到达目的地?

我:TCP协议采用的三次握手策略

  • 第一次握手:建立连接时,客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;
  • 第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
  • 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。


面试官:为什么是三次握手,而不是两次或者4次呢?

我:假如说是两次握手,如果客户端自己处理异常或者是服务器返回的ack消息丢失,那么客服端会认为连接建立失败,再次重新发送请求建立连接,但是服务端却无感知,以为连接以及正常建立,导致服务器建立无用的连接,浪费资源

假如四次握手,如果三次已经足够,那就不需要四次了。如果四次的话,最后一个ACK丢失,那么又会出现两次握手的问题。


面试官:竟然都到说了三次握手,那就说说四次挥手吧?

我:

  • 客户端向服务器发送FIN希望断开连接请求。
  • 服务器向客户端发送ACK,表示同意释放连接。
  • 服务器向客户端发送一个FIN表示希望断开连接。
  • 客户端向服务器返回一个ACK表示同意释放连接。


面试官:为什么断开连接需要四次而不是三次呢?

我:因为当服务器收到客户端断开连接的请求后,服务器不能立即断开连接,因为可能服务器端还有数据未发送完成,所以只能回复一个ACK表示我已收到消息;等服务器端数据发送完成之后再发送一个FIN希望端开连接的消息,客户端回复ACK之后,就可以安全断开了


面试官:为什么说Http协议无状态协议?怎么解决Http协议无状态?

我:本身HTTP协议是不保存状态的,自身不对请求和响应直接的通信状态进行保存,所以是无状态的协议。因为在有些场景下我们需要保存用户的登录信息,所以引入了cookie来管理状态。客户端第一次请求服务器的时候,服务器会生成cookie添加在响应头里面,以后客户端的每次请求都会带上这个cookie信息。


面试官:Cookie与Session有什么区别?

我:

  • Cookie是有服务器生成,写入到请求的响应头中,浏览器会保存;服务器通过Set-Cookie字段向客户端设置Cookie,属性:
  1. Name=value 设置cookie的名字和值
  2. expires 设置Cookie的有效期
  3. domain=域名 表示只能在哪个域名下生效
  4. secure表示只能在Https的通信时才发送cookie
  5. HttpOnly 表示不能被javascript访问
  • Session也是服务器生成的,表示服务器中的一片内存,每个客服端对应一个Session,客户端之间的Session相互独立;每次客户端发起请求,都会带上Cookie,Cookie里面一般都会有一个JSESSIONID,服务器就是通过这个JESSIONID来找到客户端对应Session,所以一般用户的登录信息都会存放在Session;这样也解决了Http协议无状态的问题

面试官:Http协议中有那些请求方式?如何选择使用什么方法?

我:

  • GET : 获取资源; 所以查询操作一般用GET
  • POST: 传输实体主体, 创建更新操作用POST
  • PUT: 传输文件
  • HEAD: 获取报文首部,如果想要查询某个请求的头信息可以用这个方法
  • DELETE: 删除资源,所以删除操作用DELETE
  • OPTIONS: 询问服务器支持哪些方法,响应头中返回 Allow: GET,POST,HEAD
  • TRACE: 追踪路径;在请求头中在Max-Forwards字段设置数字,每经过一个服务器该数字就减一,当到0的时候就直接返回,一般通过该方法检查请求发送出去是否被篡改

面试官:Http如何实现持久连接的呢?

我:(毛线啊,我只是个来面试Java的初级程序员,干嘛要反复拿Http来摩擦我呢?!不过没事,我皮的很,这道题我又会)
我:在HTTP协议的早期,每进行一次HTTP通信就要断开一次tcp连接,当时传输的内容少还能接受,现在每个网页一般的会包含大量的图片,每次请求都会造成TCP连接的连接和断开,增加通信的开销。

为了解决这个问题所以想出了持久连接的方法,也叫做keep-alive,只要一端没有提出断开连接就会一直保持TCP连接的状态。持久化连接使的客户端可以同时并发发送多个请求,不用一个接着一个的等待响应。


面试官:大文件的断点续传是如何实现的呢?

我:HTTP请求头有个Range字段;我们下载文件的时候如果遇到网络中断,如果重头开始下载会浪费时间,所以我们可以从上一次中断处继续开始下载;具体的操作:

Range: bytes=5001-10000

或者指定5001以后的所有数据

Range: bytes=5001-

响应返回的状态码是206


面试官:刚才你有提到状态码,那常见Http协议状态码有哪些?

我:(面试官我简历上忘记写了,我曾经是学霸,记忆力好,背书没输过)

我:HTTP的状态码主要分为了四类:

  • 2xx: 成功状态码,表示请求正常处理完毕
  • 3xx: 重定向状态码,表示需要附加操作才能完成成请求
  • 4xx: 客户端错误状态码
  • 5xx: 服务器错误状态码

常见的状态码有: 200(请求正常处理完成)、204(请求处理成功,但是没有资源返回)、206(表示客户端进行了范围请求,响应报文中包含了Content-Range)、301(永久性重定向,请求的资源以及被分配到了新的地址)、302(临时重定向,希望用户并且请求新地址)、400(客户端请求报文出现错误,通常是参数错误)、401(客户端未认证错误)、403(没有权限访问该资源)、404(未找到请求的资源)、405(不支持该请求方法,如果服务器支持GET,客户端用POST请求就会出现这个错误码)、500(服务器异常)、503(服务器不能提供服务)

我:(这我都能记住,是不是的给我点个赞)(已疯狂暗示兄弟们点赞,不要白嫖哦)


面试官:HTTP报文由哪些部分组成?

我:报文的类型分为了请求报文和响应报文两种;

  • 请求报文包含三部分:
  1. 请求行:包含请求方法、URI、HTTP版本信息
  2. 请求首部字段
  3. 请求内容实体
  • 响应报文包含三部分:
  1. 状态行:包含HTTP版本、状态码、状态码的原因短语
  2. 响应首部字段
  3. 响应内容实体

面试官:Http有哪些问题,什么是https?

我:Http的问题

  • 通信使用明文不加密,内容可能被窃听
  • 不验证通信方身份,可能遭到伪装
  • 无法验证报文完整性,可能被篡改

HTTPS就是HTTP加上SSL加密处理(一般是SSL安全通信线路)+认证+完整性保护


面试官:HTTPS是如何保证数据安全的?

我:首先需要说到两种加密机制

  • 对称加密:客户端和服务器都使用了同一个密钥加密,效率较高
  • 非对称加密:分为了公开密钥和私有密钥,公开密钥可以在网络上传输,使用公开密钥加密后的内容只有私有密钥才能解密,效率较低

由于这两个加密的特别,HTTPS采用的时候混合加密机制,在交换密钥的阶段使用的是非对称加密,在建立通信交换报文阶段采用的是对称加密

以访问 https://silently9527.cn 举例

  1. 浏览器向服务器发起请求,服务器在接收到请求之后,返回证书和密钥
  2. 浏览器向第三方证书机构验证证书是否合法,如果不合法浏览器将会弹出警告页面,让用户选择是否继续访问
  3. 如果证书合法浏览器生成随机串,使用公钥加密发送给服务器,服务器使用私钥解密出随机串,服务器使用随机串加密内容返回给客户端
  4. 之后客户端和服务器端都将通过随机串进行对称加密


面试官:为什么需要证书认证机构,不要https就不安全了吗?

我:虽然https是可以加密的,但是因为请求还是可以被拦截,如何让客户端知道返回给自己的公钥是真实服务器给的而不是攻击者给的;这就需要验证证书的合法性,所以需要引入第三方认证机构。通常https的证书需要到第三方机构去申请购买,如果是我们自己生成的https证书浏览器验证不过会弹出警告。


面试官:那浏览器是如何保证证书验证的过程是安全的呢?


我:浏览器在向证书认证中心验证证书的过程使用的也是非对称加密,这里想要让公钥能够安全的转交给客户端,是非常困难的,所以浏览器的开发商通常会在浏览器内部植入常用认证机构的公开密钥


面试官:http相关的协议掌握的还可以,我们继续聊聊Java.....

能撑到现在,你自己都忍不住自己给自己点个赞了!(再次暗示点赞)


写到最后(点关注,不迷路)

本篇面试故事纯属虚构,请大家不要当真,玩笑归玩笑,莫拿面试开玩笑。

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

原创不易 转载请注明出处:https://silently9527.cn/archives/68

参考:《图解HTTP》

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-16

从零开始学习Java8 Stream,看这篇就够了

image.png


为何需要引入流

在我们平常的开发中几乎每天都会有到List、Map等集合API,若是问Java什么API使用最多,我想也应该是集合了。举例:假如我有个集合List,里面元素有1,7,3,8,2,4,9,需要找出里面大于5的元素,具体实现代码:

public List<Integer> getGt5Data() {
    List<Integer> data = Arrays.asList(1, 7, 3, 8, 2, 4, 9);
    List<Integer> result = new ArrayList<>();
    for (Integer num : data) {
        if (num > 5) {
            result.add(num);
        }
    }
    return result;
}

这个实现让我们感觉到了集合的操作不是太完美,如果是数据库的话,我们只需要简单的在where后面加一个条件大于5就可以得到我们想要的结果,为什么Java的集合就没有这种API呢?
其次,如果我们遇到有大集合需要处理,为了提高性能,我们可能需要使用到多线程来处理,但是写并行程序的复杂度有提高了不少。

基于以上的问题,所有Java8推出了Stream


Stream简介

Stream有哪些特点:

  • 元素的序列:与集合一样可以访问里面的元素,集合讲的是数据,而流讲的是操作,比如:filter、map
  • 源: 流也需要又一个提供数据的源,顺序和生成时的顺序一致
  • 数据的操作:流支持类似于数据库的操作,支持顺序或者并行处理数据;上面的例子用流来实现会更加的简洁
public List<Integer> getGt5Data() {
    return Stream.of(1, 7, 3, 8, 2, 4, 9)
            .filter(num -> num > 5)
            .collect(toList());
}
  • 流水线操作:很多流的方法本身也会返回一个流,这样可以把多个操作连接起来,形成流水线操作
  • 内部迭代:与以往的迭代不同,流使用的内部迭代,用户只需要专注于数据处理
  • 只能遍历一次: 遍历完成之后我们的流就已经消费完了,再次遍历的话会抛出异常

使用Stream

Java8中的Stream定义了很多方法,基本可以把他们分为两类:中间操作、终端操作;要使用一个流一般都需要三个操作:

  1. 定义一个数据源
  2. 定义中间操作形成流水线
  3. 定义终端操作,执行流水线,生成计算结果

构建流

  1. 使用Stream.of方法构建一个流
Stream.of("silently","9527","silently9527.cn")
        .forEach(System.out::println);
  1. 使用数组构建一个流
int[] nums = {3, 5, 2, 7, 8, 9};
Arrays.stream(nums).sorted().forEach(System.out::println);
  1. 通过文件构建一个流

使用java.nio.file.Files.lines方法可以轻松构建一个流对象

Files.lines(Paths.get("/Users/huaan9527/Desktop/data.txt"))
                .forEach(System.out::println);

中间操作

中间操作会返回另外一个流,这样可以让多个操作连接起来形成一个流水线的操作,只要不触发终端操作,那么这个中间操作都不会实际执行。

filter

该操作接受一个返回boolean的函数,当返回false的元素将会被排除掉

举例:假如我们100个客户,需要筛选出年龄大于20岁的客户

List<Customer> matchCustomers = allCustomers.stream()
                .filter(customer -> customer.getAge()>20)
                .collect(toList());
distinct

该操作将会排除掉重复的元素

List<Integer> data = Stream.of(1, 7, 3, 8, 2, 4, 9, 7, 9)
        .filter(num -> num > 5)
        .distinct()
        .collect(toList());
limit

该方法限制流只返回指定个数的元素

List<Integer> data = Stream.of(1, 7, 3, 8, 2, 4, 9, 7, 9)
        .filter(num -> num > 5)
        .limit(2)
        .collect(toList());
skip

扔掉前指定个数的元素;配合limit使用可以达到翻页的效果

List<Integer> data = Stream.of(1, 7, 3, 8, 2, 4, 9, 7, 9)
        .filter(num -> num > 5)
        .skip(1)
        .limit(2)
        .collect(toList());
map

该方法提供一个函数,流中的每个元素都会应用到这个函数上,返回的结果将形成新类型的流继续后续操作。
举例:假如我们100个客户,需要筛选出年龄大于20岁的客户,打印出他们的名字

allCustomers.stream()
            .filter(customer -> customer.getAge() > 20)
            .map(Customer::getName)
            .forEach(System.out::println);

在调用map之前流的类型是Stream<Customer>,执行完map之后的类型是Stream<String>

flatMap

假如我们需要把客户的名字中的每个字符打印出来,代码如下:

List<Customer> allCustomers = Arrays.asList(new Customer("silently9527", 30));
allCustomers.stream()
        .filter(customer -> customer.getAge() > 20)
        .map(customer -> customer.getName().split(""))
        .forEach(System.out::println);

执行本次结果,你会发现没有达到期望的结果,打印的结果

[Ljava.lang.String;@38cccef

这是因为调用map之后返回的流类型是Stream<String[]>,所有forEach的输入就是String[];这时候我们需要使用flatMap把String[]中的每个元素都转换成一个流,然后在把所有的流连接成一个流,修改后的代码如下

List<Customer> allCustomers = Arrays.asList(new Customer("silently9527", 30));
allCustomers.stream()
        .filter(customer -> customer.getAge() > 20)
        .map(customer -> customer.getName().split(""))
        .flatMap(Arrays::stream)
        .forEach(System.out::println);

执行结果:

sorted

对所有的元素进行排序

List<Integer> numbers = Arrays.asList(1, 7, 3, 8, 2, 4, 9);
numbers.stream().sorted(Integer::compareTo).forEach(System.out::println);

终端操作

终端操作会执行所有的中间操作生成执行的结果,执行的结果不在是一个流。

anyMatch

如果流中有一个元素满足条件将返回true

if (allCustomers.stream().anyMatch(customer -> "silently9527".equals(customer.getName()))) {
    System.out.println("存在用户silently9527");
}
allMatch

确保流中所有的元素都能满足

if (allCustomers.stream().allMatch(customer -> customer.getAge() > 20)) {
    System.out.println("所有用户年龄都大于20");
}
noneMatch

与allMatch操作相反,确保流中所有的元素都不满足

if (allCustomers.stream().noneMatch(customer -> customer.getAge() < 20)) {
    System.out.println("所有用户年龄都大于20");
}
findAny

返回流中的任意一个元素,比如返回大于20岁的任意一个客户

Optional<Customer> optional = allCustomers.stream()
        .filter(customer -> customer.getAge() > 20)
        .findAny();
findFirst

返回流中的第一个元素

Optional<Customer> optional = allCustomers.stream()
        .filter(customer -> customer.getAge() > 20)
        .findFirst();
reduce

接受两个参数:一个初始值,一个BinaryOperator<T> accumulator将两个元素合并成一个新的值
比如我们对一个数字list累加

List<Integer> numbers = Arrays.asList(1, 7, 3, 8, 2, 4, 9);
Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);

上面的代码,我们可以简写

Integer reduce = numbers.stream().reduce(0, Integer::sum);

找出流中的最大值、最小值 min、max

numbers.stream().reduce(Integer::max)
numbers.stream().reduce(Integer::min)
count

统计流中元素的个数

numbers.stream().count()

数据收集器collect

在Java8中已经预定义了很多收集器,我们可以直接使用,所有的收集器都定义在了Collectors中,基本上可以把这些方法分为三类:

  • 将元素归约和汇总成一个值
  • 分组
  • 分区

归约和汇总

先看下我们之前求最大值和最小值采用收集器如何实现

  1. 找出年龄最大和最小的客户
Optional<Customer> minAgeCustomer = allCustomers.stream().collect(minBy(Comparator.comparing(Customer::getAge)));
Optional<Customer> maxAgeCustomer = allCustomers.stream().collect(maxBy(Comparator.comparing(Customer::getAge)));
  1. 求取年龄的平均值
Double avgAge = allCustomers.stream().collect(averagingInt(Customer::getAge));
  1. 进行字符串的连接

把客户所有人的名字连接成一个字符串用逗号分隔

allCustomers.stream().map(Customer::getName).collect(joining(","));

分组

在数据库的操作中,我们可以轻松的实现通过一个属性或者多个属性进行数据分组,接下来我们看看Java8如何来实现这个功能。

  1. 根据客户的年龄进行分组
Map<Integer, List<Customer>> groupByAge = allCustomers.stream().collect(groupingBy(Customer::getAge));

Map的key就是分组的值年龄,List<Customer>就是相同年龄的用户

  1. 我们需要先按照用户的地区分组,在按年龄分组
Map<String, Map<Integer, List<Customer>>> groups = allCustomers.stream()
                .collect(groupingBy(Customer::getArea, groupingBy(Customer::getAge)));

在相对于普通的分组,这里多传了第二个参数又是一个groupingBy;理论上我们可以通过这个方式扩展到n层分组

  1. 分组后再统计数量
Map<String, Long> groupByCounting = allCustomers.stream()
            .collect(groupingBy(Customer::getArea, counting()));
  1. 以用户所在地区分组后找出年龄最大的用户
Map<String, Optional<Customer>> optionalMap = allCustomers.stream()
                .collect(groupingBy(Customer::getArea, maxBy(Comparator.comparing(Customer::getAge))));

这时候返回的Map中的value被Optional包裹,如果我们需要去掉Optional,可以使用collectingAndThen

Map<String, Customer> customerMap = allCustomers.stream()
        .collect(groupingBy(Customer::getArea,
                collectingAndThen(maxBy(Comparator.comparing(Customer::getAge)), Optional::get)
        ));

写在最后(看完不点赞,你们想白嫖我吗)

  • 首先感谢大家可以耐心地读到这里。点关注,不迷路
  • 当然,文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。
  • 最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏
原创不易 转载请注明出处:https://silently9527.cn/archives/66

参数资料《Java8实战》

查看原文

赞 0 收藏 0 评论 0

Silently9527 发布了文章 · 2020-12-07

8张图带你了解大型应用架构演进历程


前言

先点赞再观看,要有好习惯

几乎所有的大型应用都是从一个小应用开始的,好的互联网产品是慢慢运营出来的,不是一开始就开发好的,所以本篇我们来聊聊应用架构的演进历程。

如何打造一个高可用,高性能,易扩展的应用?首先我们了解一下大型应用的特点:

  • 高可用:系统需要不间断的提供服务,不能出现单点故障
  • 高并发:在大流量的冲击下,系统依然稳定提供服务
  • 大数据:应用每天都会产生大量的数据,需要存储和管理好这些数据

最简单的架构

刚开始应用没有太多访问量,所以只需要一台服务器,这时候的架构如下图:

最简单的架构

应用程序、文件、数据库往往都部署在一台服务器上。应用程序可以采用Java开发,部署在Tomcat服务器上,数据库可以使用开源的MySQL


应用与数据服务分隔

随着应用的业务越来越复杂,应用访问量越来越大,导致性能越来越差,存储空间严重不足,这时候我们考虑把服务增加到三台(能通过加机器解决的问题都不是问题);分离出应用服务器、数据库服务器、文件服务器。

  • 应用服务器需要处理大量的访问,所以需要性能更好的CPU
  • 数据库服务器需要存储大量的数据以及快速的检索,所以需磁盘的检索速度较快以及存储空间大
  • 文件服务器需要存储上传的文件,需要更大的磁盘;现在通常情况下会选择第三方的存储服务

应用与数据访问服务分隔

根据每个服务器对应的场景,配置服务器后应用的性能能够大大提高,更好的支持业务的发展。但是随之业务的发展,访问量的增大,这种架构又将再次面临挑战,应用服务器处理能力下降,存储空间不足


应用服务器集群

在高并发,大流量的情况下,一台服务器是肯定处理不过来的,这个时候增加服务器,部署集群提供服务,来分担每台服务器的压力。部署集群的另一个好处是可伸缩行,比如当遇到了双11大流量的场景下,可以增加服务器分摊流量,等双11过后,减少服务器节约成本。架构如下:

应用服务器集群

如果应用服务器是Tomcat,那么可以部署一个Tomcat的集群,外部在部署一个负载均衡器,可以采用随机、轮询或者一致性哈希算法达将用户的请求分发到不同应用服务集群;通常选择的免费的负载均衡是nginx。在这种架构下,应用服务器的负载将不会是整个应用的瓶颈点;

虽然应用程序的处理速度在这种架构下提升了许多,但是又会暴露一个问题,数据库的压力大大增大,导致访问响应延迟,影响整个应用的性能。
这种架构还有个问题,通常应用是有状态的,需要记录用户的登录信息,如果每次用户的请求都是随机路由到后端的应用服务器,那么用户的会话将会丢失;解决这个问题两个方案:

  • 采用一致性hash把用户的请求路由到同一个Tomcat,如果有一台服务器跪了,那么这台服务器上面的用户信息将会丢失
  • Tomcat集群之间通过配置session复制,达到共享,此方案效率较低

两个方案都不是很好,那么还有其他的方案吗?请继续往下看


缓存

根据二八原则,80%的的业务都是集中访问20%的数据,这20%的数据通常称为热点数据,但是这20%的数据占用的内存也不会小,如果每个应用服务器都存放一份,有些浪费存储空间,所以这时候需要考虑加入分布式缓存服务器(常用的是Redis);当引入了分布式缓存服务器,再来看上面那个方案的问题,就可以解决了,把用户的会话存放到缓存服务器,不仅可以防止用户数据丢失,效率也不低;架构图如下:

缓存

由于分布式缓存服务器毕竟存放在远程,需要经过网络,所以取数据还是要花一点时间;本地缓存访问速度更快,但是内存空间有限,并且还会出现和应用程序争抢资源;所以这种架构搭配了分布式缓存和本地缓存,本地缓存存放少量常用热点数据,当本地缓存中没有命中时在去集中式缓存取

在引进缓存之后,数据库的访问压力可以的一定的缓解


数据库读写分离

虽然在加入了缓存之后,部分数据可以直接走缓存,不需要访问数据库,但是任然会有一些请求,会访问数据库,比如:缓存失效,缓存未命中;当流量大的时候,数据库的访问量也不小。这时候我们需要考虑搭建数据库集群,读写分离

数据库读写分离

当应用服务器有写操作时,访问主库,当应用程序有读操作时,访问从库;大多数的应用都是读的操作远远大于写的操作,所以可以配置数据库一主多从来分担数据库的压力;为了让应用程序对应主库和从库无感知,通常需要引入一些读写分离的框架做一个统一的数据访问模块。

这种架构通常需要警惕的一个问题是主从延迟,当在高并发的场景下,主库刚写成功,数据库还未成功同步完从库,这时候另一个请求进入读取数据发现不存在;解放方案是在应用程序中高并发的场景下设置强制走主库查询

兄弟们,请不要白嫖哦,文章看一半,请先点个赞

反向代理和CDN

假如随着业务的不断扩大,全国各地都会使用到我们的应用,由于各地区的网络情况不同,所以有的人请求响应速度快,有的人请求响应速度慢,这会严重的影响到用户的体验。为了提高响应速度需要引入反向代理和CDN;CDN和反向代理都是采用的缓存,目的:

  • 尽可能快的把数据呈现给用户
  • 减轻后端服务器的压力

架构图如下:

反向代理和CDN

CDN: 部署在网络提供商的机房,当用户来访问的时候,从距离用户最近的服务器返回数据,尽快呈现给用户;通常情况下在CDN中缓存的是静态资源(html,js,css),达到动静分离;但是有时候遇到了某些数据访问量特别大的时候,后端会生成静态资源放入到CDN,比如:商城的首页,每个用户进入都需要访问的页面,如果每次请求都进入到后端,那么服务器的压力肯定不小,这种情况下会把首页生成静态的文件缓存到cdn和反向代理服务器

反向代理:部署在应用的中心机房,通常也是缓存的静态资源,当用户通过CDN未请求到需要的数据时,先进入反向代理服务器,如果有缓存用户访问的数据,那么直接返回给用户;这里也有特殊情况,对于有些场景下的热点数据,在这里根据用户的请求去分布式缓存服务器中获取,能拿到就直接返回。

这种架构已经把缓存做到了4级

  • 第一级:CDN 缓存静态资源
  • 第二级:反向代理缓存静态资源以及部分热点数据
  • 第三级:应用服务器的本地缓存
  • 第四级:分布式缓存服务器

通常情况下经过了这4级缓存,能够进入到数据库的请求也不多了,很好的释放了数据库的压力


搜索引擎和NoSQL

随着业务的不断扩大,对于数据的存储和查询的需求也越来越复杂,通常情况我们需要引入非关系型数据库,比如搜索引擎和NoSQL数据库

搜索引擎和NoSQL

有时候我们的查询场景很复杂,需要查询很多数据表,经过一系列的计算才能完成,这时候可以考虑通过数据同步工具(比如canal)拉去数据到大数据平台,使用批处理框架离线计算,把输出的结果存放到搜索引擎或者NoSQL数据库中,应用程序直接查询计算的结果返回给用户。也有可能我们需要汇总多个表的数据做一张宽表,方便应用程序查询

由于引入的数据存储方式增多,为了减轻应用程序的管理多个数据源的麻烦,需要封装统一数据访问模块,如果使用的时Java,可以考虑spring-data


业务纵向拆分

互联网公司通常的宗旨是小步迭代试错快跑,当业务发展到足够大,对于单体应用想要达到这个宗旨是有难度的,随着业务的发展,应用程序越来越大,研发、维护、发布的成本也越来越大,这时候就需要考虑根据业务把单体应用拆分为多个服务,服务之间可以通过RPC远程调用和消息队列来一起完成用户的请求。

由于业务的拆分,通常情况下也会相应的对数据库进行拆分,达到一个服务对应一个数据库的理想状态

业务纵向拆分

引入MQ的好处:

  • 提高系统的可用性:当消费服务器发送故障时,消息还在消息队列中,数据不会丢失
  • 加快请求的响应:当用户请求到达服务器后,把请求中可以异步处理的数据放入到MQ,让系统逐一消费,不需要用户等待,加快了响应速度
  • 削峰填谷:当大量请求都同时进入到系统之后,会全部放入到消息队列,系统逐一消费,不会对系统造成很大的冲击

总结

还有一个情况未谈及到,就是数据库的水平拆分,这也是数据库拆分的最后手段,只有当单表数据特别大,不能满足业务的需要才使用。使用最多的还是进行数据库的业务纵向拆分,把数据库中不同业务的数据放到不同的物理服务器上。

应用当前到底选择什么架构,一定要根据实际业务的需求进行灵活的选择,驱动技术架构发展的主要动力还是在于业务的发展,不要为了技术而技术。


写在最后
  • 首先感谢大家可以耐心地读到这里。点关注,不迷路
  • 当然,文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。
  • 最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏
原创不易 转载请注明出处:https://silently9527.cn/archi...

参考资料《大型网站技术架构》

查看原文

赞 3 收藏 1 评论 0