Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

ReentrantLock非公平锁源码分析

发表于 2020-08-22 | 分类于 Java

hello~各位读者好,我是鸭血粉丝(大家可以称呼我为「阿粉」)。今天,阿粉带着大家来了解一下 ReentrantLock 锁的非公平锁的实现原理

1.锁

java中,加锁的方式

1
2
1. synchronized,这个是 java 底层实现的,也就是 C 语言实现的。
 	2. lock,这个是 java.util.concurrent 包下面的,是 java语言实现的。

2.ReentrantLock

ReentrantLock 是 Lock 的一种实现,是一种可重入的公平或非公平锁。默认是非公平锁。

2.1 Lock的创建

首先看下锁的创建和使用代码:

1
2
3
4
5
6
//创建锁
Lock lock  = new ReentrantLock();
//加锁
lock.lock();
//释放锁
lock.unlock();

然后看下创建的是 ReentrantLock 的构造函数:

1
2
3
public ReentrantLock() {
    sync = new NonfairSync();
}

NonfairSync 就是非公平锁。所以 ReentrantLock 默认是非公平锁的实现

2.2 lock()

加锁的逻辑就比较复杂了,因为存在线程竞争。所以有两种情况,一种是竞争到锁的处理,一种是没有竞争到锁的处理。

首先我们还是来看下 lock() 方法,因为最终是非公平的实现,所以直接看 NonfairSync 里面的 lock 方法。

1
2
3
4
5
6
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

2.3 没有获取到锁的逻辑 acquire()

直接上代码:

1
2
3
4
5
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

还是3个方法,阿粉一个一个的说。

  1. tryAcquire(arg) ,还是先看代码在分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    

    a. 获取 state ,如果等于0,说明之前获得锁的线程已经释放了,那么这个线程就会再次去竞争锁,这就是非公平锁的体现,如果是公平锁,是没有这个判断的。

    b. 如果前一个获得锁的线程没有释放锁,那么就判断是否是同一个线程,是的话就会将 state 加 1。这个就是重入锁的体现。

    c. 如果都不满足,那么返回 false。

  2. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) ,再次获取锁没有成功,并且又不是可重入锁,那么就存入一个阻塞队列里面。里面还有一点逻辑,就不展开了,有兴趣可以自己看下。

  3. selfInterrupt(); 这个是当前线程的中断标志,作用就是在线程在阻塞的是否,客户端通过调用了中断线程的方法 interrupt(),那么该线程被唤醒的时候,就会有响应的处理。具体要看这个线程 run 方法里面的代码逻辑。

2.4 unlock()

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放锁的步骤

  1. state - 1,如果大于0,说明释放的是重入锁,只需要修改 state 就行了
  2. 如果等于0,说明要释放锁,释放锁首先需要把独占线程设置为null,再把state设置为0。

3 总结

Lock 锁的实现:

  1. 互斥性:需要一个状态来判断是否竞争到锁: state 并且需要用 volatile修饰,保证线程之间的可见性。

  2. 可重入性:Thread exclusiveOwnerThread 这个成员变量来记录当前获得锁的线程。

  3. 公平或非公平:默认非公平,NonfairSync。

  4. 没有竞争到锁的线程怎么办?放到队列中。

  5. 没有竞争到锁的线程怎么释放CPU?park:阻塞线程释放CPU资源,这个操作在 acquireQueued(),阿粉没没有讲这个。

  6. 最后来张流程图:

    1

阅读全文 »

关于投资,阿粉有些话想要和你说

发表于 2020-08-21 | 分类于 程序生活

我相信各位读者朋友们,最近一定或多或少的听过投资这件事,阿粉也有些话想要和你唠唠,特别是关于基金定投这件事

互联网这个行业,忙起来的时候说是没有白天没有黑夜一点儿都不为过。没有时间去研读财报,没有时间去做分析,那么,最简单快捷的方法就是研究一下基金定投。

关于基金定投,阿粉还是有些经验的,今天来分享下(要自己挖自己的黑历史了

1, 对于投资,不要 all in

阿粉刚开始投资的时候,什么都不懂,把自己当时手里面的钱全都投进去了,想着反正自己也是一单身狗嘛,平时也没啥支出,干脆都投进去得了(说到这里我的眼角就湿了

结果后来因为一些事情需要用钱,我所有的钱又都在基金里面,那个时候我的基金还没盈利,没办法只能忍痛割肉,想着等回来再有钱了重新定投就行了。

结果,还没等阿粉重新开始定投,原来买的基金蹭蹭的开始涨!!!(我的眼角又开始湿了

当时就很后悔,那段时间阿粉就总是想,如果自己当时定投的时候,拿出一部分钱去做,虽然赚的会少点儿,但是起码我还能有得赚,不至于因为用钱不得不在赔的时候抛出。

自从那次教训之后,阿粉就学聪明了嘛,永远都给自己留条后路~

2, 如果想要定投,做好长期持有的准备

大家肯定都知道,做定投相对股票来说是比较容易赚钱的,但是就算如此也还是会有人赔钱。

阿粉觉得大概率就是这些人持有的时间太短(前提是,你持有的基金是优质基金)

基金不适合短期频繁操作,它适合长期持有。第一是因为基金在短期内是有涨幅的,也就是今天可能会跌,明天可能会涨,但是从长期来看,基金的整个趋势是上涨的,所以如果想要定投基金的话,一定要做好长期持有的准备,我觉得这个年限可以是 3 年。如果持有一个基金, 3 年它还没有盈利,那真的可以考虑要不要继续持有了。

第二是因为,基金如果在短期内卖出的话,会有比较高的卖出率,但是如果是长期持有,就没有卖出率这一说

做好长期持有的准备,还有一个潜台词就是,做基金定投的钱,在长期内是不能动的。

或者可以这样认为,做基金定投的钱,你就当你从来都没有过这笔钱就好了,如果在此基础上,你的生活还不受影响的话,那这个做投资的钱,就是适合你的(这样比喻,我可真是个机智的崽崽

3, 别管别人怎样,要保持一个让自己舒服的仓位

每个人能够承受的风险不一样,所以不要看别人持有了多少的仓位,不要看别人买了多少多少的资金。

阿粉这个人还算是比较能够承受风险的,做基金定投的钱就是都赔光了,我也最多难受那么几秒钟。因为这些钱对我来说,不是很能影响我的生活质量,所以哪怕都赔光了,说不心疼是假的,但是起码我都可以接受,不会因为赔了 10%, 20% 就整天睡不着这种(阿粉吹个牛逼还不行…赔钱这事儿,怎么可能睡得着觉!!!

那么,如果你承受的风险能力比较弱,别说赔 10%, 20% 了,就是赔 5% 都心疼的不行,那就赶紧做一些动作,让自己舒服。

做投资只是让你的生活变得更好一些,但是如果因为这样导致自己睡不着,吃不好,我觉得是得不偿失,还不如卖出一些,保持一个让自己舒服的仓位。

毕竟,人生这么短暂,舒服最重要不是~

4, 关于投资这件事,你学多少知识,花多少时间都不为过

我有一点想不明白,就是大多数人在买东西的时候,都会货比三家,特别是女生买化妆品的时候,一定会挑好久好久。但是等到买基金,做投资的时候,却是想都不想跟着市面上那些所谓的“投资大师”们走。

就让我觉得,难道定投的钱,不是自己挣的钱嘛?买东西的时候,那么谨慎,怎么一到做投资的时候,就那么草率呢。

不要信市场上那些所谓的”投资大师”们的话,如果真的赚钱的话,没有人告诉你的。就算告诉,也是告诉自己的亲人,朋友这种,你和对方是什么关系,人家凭什么把赚钱这件事情交给你。

而且,一些投资方法如果真的公布出来,真的就没办法赚钱了,比如网格策略,如果公布出来,到最后可能大家都没得赚。

所以,不要相信任何人给你的投资建议。

要有自己的判断,这就需要你去学习投资相关的知识,比如什么是沪深 300 ,中证 500 ,什么是红利等等等等。在这上面学多少知识,花多少时间都不为过。

5, 要学会关注趋势

市场很大一部分都是受趋势影响的。

记得前段时间,网上有个段子:本来想定投基金赚一把,结果没想到天天拉着我见证历史。

学会关注趋势,关注国家大势。不是有句话说,站在风口上,猪都能飞起来。

怎么找风口呢?不关注趋势,就想找到正确的风口,想破头皮可能也想不到一个方向吧~

就算是股神巴菲特,也还保持着每天看报纸,分析趋势呢,更别说咱们这些普通人了。

不要问阿粉为啥有这么多经验分享,因为问着问着阿粉的眼泪就流下来了

阅读全文 »

你确定 LinkedList 在新增/删除元素时,效率比 ArrayList 高?

发表于 2020-08-20 | 分类于 集合

在面试的时候都会被问到集合相关的问题,比如:你能讲讲 ArrayList 和 LinkedList 的区别吗?

那么我相信你肯定能够答上来: ArrayList 是基于数组实现的, LinkedList 是基于链表实现的

接下来面试官就会连环问了,那你能讲讲,它们都用在什么场景下吗?

阿粉知道这种程度肯定难不倒咱们读者的:因为 ArrayList 是基于数组实现的,所以在遍历的时候, ArrayList 的效率是要比 LinkedList 高的, LinkedList 是基于链表实现的,所以在进行新增/删除元素的时候, LinkedList 的效率是要比 ArrayList 高的

面试官:哦哦,好的,我大概了解了,我这边没有什么想问的了,您回去等消息可以吗

???发生了什么?

哈哈,上面模拟了一个面试场景,是想引出来这篇文章的主题: LinkedList 在新增/删除元素时,效率比 ArrayList 高,这是真的吗?

我相信你也知道套路,一般这么一问,那肯定就不是真的了

放一张图片,这是经过我测试之后的真实结果

因为微信不能放外链的缘故,可以在公众号后台发送 “20200821” 获取测试代码

ArrayList 与 LinkedList 新增元素比较

从图中可以看出来, LinkedList 在新增元素时,它的效率不一定比 ArrayList 高,这是要分情况的

如果是从集合头部位置新增元素的话,那确实是 LinkedList 的效率要比 ArrayList 高

但是如果是从集合中间位置或者是尾部位置新增元素, ArrayList 效率反而要比 LinkedList 效率要高

Excuse me ?竟然和我以前学的不一样?阿粉我学的浅,你别骗我

哈哈哈,为什么会这样呢

这是因为 ArrayList 是基于数组实现的嘛,而数组是一块连续的内存空间,所以在添加元素到数组头部时,需要对头部后面的数据进行复制重排,所以效率是蛮低的

但是 LinkedList 是基于链表实现的,在添加元素的时候,首先会通过循环查找到添加元素的位置,如果要添加的位置处于 List 前半段,那就从前向后找;如果位置在后半段,那就从后往前找,所以 LinkedList 添加元素到头部是非常高效的(小声 BB ,这我知道

哦,这你知道?看来基础蛮不错的嘛~

所以当 ArrayList 在添加元素到数组中间时,有一部分数据需要复制重排,效率就不是很高,那为啥 LinkedList 比它还要低呢?这是因为 LinkedList 把元素添加到中间位置的时候,需要在添加之前先遍历查找,这个查找的时间比较耗时

添加元素到尾部操作中, ArrayList 的效率要比 LinkedList 的还要高,这是为啥嘞

因为 ArrayList 在添加的时候不需要什么操作,直接插入就好了,所以效率蛮高的

但是 LinkedList 就不一样了,对于 LinkedList 来说,也不需要查找啥的,直接插入就可以了,但是需要 new 对象,还有变换指针指向对象呀,这些过程耗时加起来可就比 ArrayList 长了

它是有前提的,那就是 ArrayList 初始化容量是足够的情况下,才有上述的特点,如果 ArrayList 涉及到动态扩容,那它的效率肯定会降低

ArrayList 与 LinkedList 删除元素比较

删除元素和新增元素的原理是一样的,所以删除元素的操作和新增元素的操作耗时也是很相近

这里就不再赘述

ArrayList 与 LinkedList 遍历元素比较

测试结果非常明显,对于 LinkedList 来说,如果使用 for 循环的话,效率特别低,但是 ArrayList 使用 for 循环去遍历的话,就比较高

为啥呢?

emmm ,得从源码说起

先来看 ArrayList 的源码吧,这个比较简单

1
2
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

能够看到, ArrayList 实现了 List , RandomAccess , Cloneable 还有 Serializable 接口

你是不是对 RandomAccess 这个接口挺陌生的?这是个啥?

但是通过查阅源码能够发现它也只是个空的接口罢了,那 ArrayList 为啥还要去实现它嘞

因为 RandomAccess 接口是一个标志接口,它标识着“只要实现该接口的 list 类,都可以实现快速随机访问”

实现快速随机访问?你能想到什么?这不就是数组的特性嘛!可以直接通过 index 来快速定位 & 读取

那你是不是就能想到, ArrayList 是数组实现的,所以实现了 RandomAccess 接口, LinkedList 是用链表实现的,所以它没有用 RandomAccess 接口实现吧?

beautiful ~就是这样

咱瞅瞅 LinkedList 源码

1
2
3
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

果然,跟咱们设想的一样,没有实现 RandomAccess 接口

那为啥 LinkedList 接口使用 for 循环去遍历的时候,慢的不行呢?

咱们瞅瞅 LinkedList 在 get 元素时,都干了点儿啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

在 get 方法中,主要调用了 node() 方法,因为 LinkedList 是双向链表,所以 if (index < (size » 1)) 在判断 i 是在前半段还是后半段,如果是前半段就正序遍历,如果是在后半段那就倒序遍历,那么为什么使用 for 循环遍历 LinkedList 时,会这么慢?(好像离真相越来越近了

原因就在两个 for 循环之中,以第一个 for 循环为例

  • get(0) :直接拿到了node0 地址,然后拿到 node0 的数据

  • get(1) :先拿到 node0 地址,然后 i < index ,开始拿 node1 的地址,符合条件,然后去拿 node1 的数据

  • get(2) :先拿到 node0 的地址,然后 i < index ,拿到 node1 的地址, i < index ,继续向下走,拿到 node2 的地址,符合条件,获取 node2 的数据

发现问题了嘛?我就是想要 2 的数据, LinkedList 在遍历时,将 0 和 1 也给遍历了,如果数据量非常大的话,那效率可不就唰唰的下来了嘛

那到现在,咱们也就非常明确了,如果是要遍历 ArrayList 的话,最好是用 for 循环去做,如果要遍历 LinkedList 的话,最好是用迭代器去做

我猜你一定会说,阿粉啊,那如果对方就给我传过来了一个 list ,我不知道它是 ArrayList 还是 LinkedList 呀?我该怎么办呢

还记得 ArrayList 和 LinkedList 有什么不同吗?是不是 ArrayList 实现了 RandomAccess 接口,但是 LinkedList 没有实现,所以可以从这点去着手解决

暖暖的阿粉在这里给个简单的小 demo ,可以参考下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<String>();
        arrayList.add("aaa");
        arrayList.add("bbb");
        isUseIterator(arrayList);

        List<String> linkedList = new LinkedList<String>();
        linkedList.add("ccc");
        linkedList.add("ddd");
        isUseIterator(linkedList);
    }

    public static void isUseIterator(List list){
        if (list instanceof RandomAccess){
            System.out.println("实现了 RandomAccess 接口,使用 for 循环遍历");

            for (int i = 0 ; i < list.size(); i++ ){
                System.out.println(list.get(i));
            }
        }else{
            System.out.println("没有实现 RandomAccess 接口,使用迭代器遍历");

            Iterator it = list.iterator();
            while (it.hasNext()){
                System.out.println(it.next());
            }
        }
    }
}

本篇文章用到的所有代码,都上传到了 github 上,因为微信不能放外链的缘故,可以在公众号后台发送 “20200821” 获取本文完整代码

所以,乖,下次面试官再问你 LinkedList 在新增/删除元素时,效率比 ArrayList 高吗,不要再傻傻的回答是了,拿阿粉这篇文章和他扯皮,保证没问题

参考:

极客时间 — 《Java 性能调优实战》

阅读全文 »

Consul-template, Nginx 实现Thrift Consul负载均衡

发表于 2020-08-17 | 分类于 java

Hello 大家好,我是阿粉,今天给大家分享一个采用 Consul 实现的负载均衡的方案,很多小伙伴都知道 Nginx 可以实现负载均衡,但是可能没实现过结合 Consul,今天就给大家分享一下。

阅读全文 »

CopyOnWrite 了解吗?

发表于 2020-08-17 | 分类于 多线程

概念

CopyOnWrite 只是看字面意思就能看出来,就是在写入时复制,说得轻巧,写入时复制,具体是怎么实现的呢?

先来说说思想,具体怎么实现等下分析

CopyOnWrite 的思想就是:当向一个容器中添加元素的时候,不是直接在当前这个容器里面添加的,而是复制出来一个新的容器,在新的容器里面添加元素,添加完毕之后再将原容器的引用指向新的容器,这样就实现了写入时复制

你还记得在提到数据库的时候,一般都会说主从复制,读写分离吗? CopyOnWrite 的设计思想是不是和经常说的主从复制,读写分离如出一撤?

优缺点

了解概念之后,对它的优缺点应该就比较好理解了

优点就是,读和写可以并行执行,因为读的是原来的容器,写的是新的容器,它们之间互不影响,所以读和写是可以并行执行的,在某些高并发场景下,可以提高程序的响应时间

但是呢,你也看到了, CopyOnWrite 是在写入的时候,复制了一个新的容器出来,所以要考虑它的内存开销问题,又回到了在学算法时一直强调的一个思想:拿空间换时间

还有一点就是,它只保证数据的最终一致性。因为在读的时候,读取的内容是原容器里面的内容,新添加的内容是读取不到的

基于它的优缺点应该就可以得出一个结论: CopyOnWrite 适用于写操作非常少的场景,而且还能够容忍读写的暂时不一致 如果你的应用场景不适合,那还是考虑使用别的方法来实现吧

还有一点需要注意的是:在写入时,它会复制一个新的容器,所以如果有写入需求的话,最好可以批量写入,因为每次写入的时候,容器都会进行复制,如果能够减少写入的次数,就可以减少容器的复制次数

在 JUC 包下,实现 CopyOnWrite 思想的就是 CopyOnWriteArrayList & CopyOnWriteArraySet 这两个方法,本篇文章侧重于讲清楚 CopyOnWriteArrayList

CopyOnWriteArrayList

在 CopyOnWriteArrayList 中,需要注意的是 add 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 在写入的时候,需要加锁,如果不加锁的话,在多线程场景下可能会被 copy 出 n 个副本出来
        // 加锁之后,就能保证在进行写时,只有一个线程在操作
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 复制原来的数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 将要添加的元素添加到新数组中
            newElements[len] = e;
            // 将对原数组的引用指向新的数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

在写的时候需要加锁,但是在读取的时候不需要添加

因为读取的是原数组的元素,对新数组没有什么影响,加了锁反而会增加性能开销

1
2
3
public E get(int index) {
	return get(getArray(), index);
}

举个例子:

CopyOnWrite 在 JUC 包下,那么它就保证了线程安全

咱们来做个小 demo 验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
public class ArrayListExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static List<Integer> list = new ArrayList<>();

    public static void  main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}",list.size());
    }
    private static void update(int i){
        list.add(i);
    }
}

上面是客户端请求 5000 次,有 200 个线程在同时请求,我使用的是 ArrayList 实现,咱们看下打印结果:

如果是线程安全的话,那么最后的结果应该是 5000 才对,多运行几次你会发现,每次程序的执行结果都是不一样的

如果是 CopyOnWriteArrayList 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
public class CopyOnWriteArrayListExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void  main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("excepiton",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}",list.size());
    }
    private static void update(int i){
        list.add(i);
    }
}

多运行几次,结果都是一样的:

由此可见, CopyOnWriteArrayList 是线程安全的

以上,感谢阅读~

阅读全文 »

面试官提问:说说你对消息队列的理解

发表于 2020-08-14 | 分类于 java

关于消息队列,断断续续的看了很多资料,一直想抽个时间把这些知识整理记录下来,但是没腾出时间来写,正好所在的项目在实际业务中使用到了消息队列,索性就将这方面的知识整理一下,可能有理解不到位的地方,望网友批评指出!

阅读全文 »

手把手教你搭建一个单一机器的Hadoop,你要看一下么?

发表于 2020-08-10 | 分类于 Java

说实话,之前已经很久没接触大数据的东西了,阿粉前几天看了一个新闻,说某应届生在毕业时间拿到了华为的大数据的岗位提供的Offer,并且月薪资竟然给出了30K的高价,瞬间让我把之前放在一边的大数据重现捡了起来,于是开始自己手动搭建Hadoop框架。而由于条件有限,阿粉在这里只能给大家搭建一个关于单机的环境了。

<–more–>

1.什么是大数据

来自百度百科的解释是这样的:

大数据(big data),IT行业术语,是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

其实这么说,谁都不理解,不知道大家有没有在逛淘宝的时候发现,你之前在其他的地方搜索过的所有的东西,然后在淘宝的下面会给你关联出很多的内容,你之前因为女友生日,你搜索了口红,结果下面出现了各种 MAC ,TF ,Dior,各种各样的推荐,而这就是大数据的结果。

2.Hadoop是什么?

Hadoop是一个开源框架,允许使用简单的编程模型在跨计算机集群的分布式环境中存储和处理大数据。它的设计是从单个服务器扩展到数千个机器,每个都提供本地计算和存储。

而今天我们只是做单机版本的Hadoop,也就是先不做集群处理,那么我们就准备开始把。

而关于如何安装虚拟机这一步骤,阿粉就不在给大家一一讲述了,毕竟这个虚拟机在百分之80的开发人员的电脑中都是存在的,那么就开始吧。

2.1 安装Ubuntu系统

先下载Ubuntu系统的iso镜像,如图所示:

下载完成,这时候我们要安装虚拟机了,新建虚拟机:

注意这个地方可以选择稍后安装操作系统,因为直接选择镜像的时候,阿粉遇到过好几次启动中途直接不动的情况。

而安装完成之后,接下来我们去选择虚拟机设置中的CD/DVD,这个时候我们再去选择镜像文件,

安装虚拟机安装完成,接下来阿粉就是该把系统搞好了,

安装的时候一定要勾选上清除整个磁盘,并且安装系统,

安装到输入用户和密码的时候,我选择的是输入自己的用户名 master ,密码123456,及其简单,但是好记,经过漫长的等待,等待,阿粉终于等他安装完成了,

安装完成重启之后,我们就要进行接下来的安装了。

2.2安装JDK

在我们安装这个JDK这些内容之前,我们先把这个 VmWare-Tools 安装上 sudo ./vmware-install.pl,这是在我们虚拟机上的有一个安装VmWare-Tools,点击之后,桌面会出现一个光盘累死的文件,提取里面的文件之后执行命令即可,然后关机重启一次。

首先,我们做 Java 开发的是不是肯定得先把 Java 环境给他装上呢?毕竟 Hadoop也是使用 Java 开发的,必须得给他 Java 的环境呀,我们还是选择 jdk8,不选择 jdk11,毕竟版本新,虽然功能会多,但是有太多的坑阿粉不想去踩。

在我们输入 java -version 的时候,提示我们找不到,并且给出了,我们不是root的用户,所以我们每次要在前面加上sudo,

sudo apt-get update 而我们在安装 VmWare-Tools 的时候也是需要我们去安装这个套件,那就来,直接给他安装了

不得不说,这网络是真的墨迹,中间有一些东西,阿粉表示安装的时候会占用你绝大部分的时间在网络下载上,而离线安装呢,又非常的烦,所以,那还是继续网络安装吧。

那我们继续来安装 JDK吧,之前看到提示说,sudo apt install openjdk-8-jre-headless,

安装完成之后继续校验一下 java -version 吧,

我们可以看一下这个 Java 到底是安装在哪哪一个目录下,顺带着可以去配置一下环境变量不是,命令来了

  • update-alternatives –display java

我们先把这个路径复制一下,在配置 JAVA_HOME 的时候是要用到的,在这之前我们需要设置一下 SSH ,

2.3 安装 SSH

我们先来安装 SSH 等安装结束之后,阿粉再给大家解释为什么要安装 SSH 设置无密码登录是个什么意思。

sudo apt-get install ssh 安装SSH

sudo apt-get install rsync 安装 rsync

ssh-keygen -t dsa -P '' -f ~/.ssh/id_dsa 产生 SSH 密钥之后继续进行后续的身份验证

cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys 把产生的密钥放入许可文件中。

看完这个之后,大家有没有想起来我们在使用 GitHub的时候,也使用过 SSH,那时候是有公钥和私钥的存在,而我们设置 SSH 无密码登录,是为了在我们安装完 Hadoop之后,我们启动的时候不需要频繁的去输入账号和密码,而那时候就是通过我们设置的 SSH 来验证用户的,所以我们在这里设置了一波。

2.4 下载安装Hadoop

这里我们不使用离线安装,因为离线安装很多是因为没时间,而阿粉在这里下载着,顺带着可以去干点其他的事情,时间最大化,

这也是阿粉选择使用 Ubuntu的原因,之前使用 CentOS的时候,没有桌面,去寻找网址,是在不太好弄,我们这里直接百度 Hadoop ,网站直接 Download ,然后找一个相对来说稳定的版本,我还是选择了 Hadoop2.x 的版本,没有选择 3.x ,我们这里使用 2.9 的版本把,把网址复制,

wget https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-2.9.2/hadoop-2.9.2.tar.gz

接下来我们就开始解压了,sudo tar -zxvf hadoop-2.9.2.tar.gz ,解压完,我们找个目录给他移动过去,sudo mv hadoop-2.9.2 /usr/local/hadoop

这时候我们看一下这个 Hadoop 安装目录里面都有个啥东西

  • bin 存各种运行文件 Hadoop,HDFS,YARN

  • sbin 各种shell脚本文件

  • etc Hadoop的配置文件

  • logs 日志,

剩下的文件阿粉就不解释了,毕竟lib这文件夹里面放的,大家心里都清楚是个什么东西。

2.5 Hadoop的环境变量配置

sudo gedit ~/.bashrc 这时候会打开一个文件,我们在末尾补充上

1
2
3
4
5
6
7
8
9
10
11
12
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export HADOOP_OPTS="-DJava.library.path=$HADOOP_HOME/lib"
export JAVA_LIBRARY_PATH=$HADOOP_HOME/lib/native:$JAVA_LIBRARY_PATH

而这些内容是分别用来设置 java 的路径,还有 Hadoop的环境变量的一些配置,而接下来我们在不重启机器的情况下,我们用命令让我们的配置生效,

source ~/.bashrc

那么接下来我们就该修改 Hadoop配置文件了,

把这里的环境变量修改为我们之前复制的路径 修改环境变量/usr/lib/jvm/java-8-openjdk-amd64

接下来我们修改 core-site.xml 文件

sudo gedit /usr/local/hadoop/etc/hadoop/core-site.xml 修改HDFS的名称,

我们在配置文件里面的 中加入如下的代码

1
2
3
4
5
<property>
	<name>fs.default.name</name>
	<value>hdfs://localhost:9000</value>
</property>

同样我们也要修改yarn-site.xml 文件

sudo gedit /usr/local/hadoop/etc/hadoop/yarn-site.xml

1
2
3
4
5
6
7
8
<property>
	<name>yarn.nodemanager.aux-services</name>
	<value>mapreduce_shuffle</value>
</property>
<property>
	<name>yarn.nodemanager.aux-services.mapreduce_shuffle.class</name>
	<value>org.apache.hadoop.mapred.ShuffleHandler</value>
</property>

接下来,我们设置一下,mapred-site.xml ,这个是用来监控任务执行情况的,

sudo cp /usr/local/hadoop/etc/hadoop/mapred-site.xml.template /usr/local/hadoop/etc/hadoop/mapred-site.xml 复制模板文件到site文件中

同时进行修改

sudo gedit /usr/local/hadoop/etc/hadoop/mapred-site.xml

1
2
3
4
<property>
	<name>mapreduce.framework.name</name>
	<value>yarn</value>
</property>

最后我们就来设置一下数据存储的目录

sudo gedit /usr/local/hadoop/etc/hadoop/hdfs-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
<property>
	<name>dfs.replication</name>
	<value>3</value>
</property>
<property>
	<name>dfs.namenode.name.dir</name>
	<value>file:/usr/local/hadoop/hadoop_data/hdfs/namenode</value>
</property>
<property>
	<name>dfs.datanode.data.dir</name>
	<value>file:/usr/local/hadoop/hadoop_data/hdfs/datanode</value>
</property>

2.6 格式化HDFS的目录

sudo mkdir -p /usr/local/hadoop/hadoop_data/hdfs/namenode

sudo mkdir -p /usr/local/hadoop/hadoop_data/hdfs/datanode

同时授权给我们的用户 master ,sudo chown master:master -R /usr/local/hadoop/

下面我们格式化之后,就可以启动了

hadoop namenode -format 格式化HDFS

start-yarn.sh 启动Hadoop

start-yarn.sh 启动MapReduce的Yarn

那么我们来看看安装的吧,Hadoop ResourceManager Web 界面 http://localhost:8088

HDFS WEB UI界面 –http://localhost:50070

安装完了,启动好了,你学会了么?

文献参考

《Hadoop Spark大数据巨量分析与机器学习整合开发实战 》

阅读全文 »

数据库迁移神器——Flyway

发表于 2020-08-09 | 分类于 java

Hello 大家好,我是阿粉,不知道你有没有遇到过这种场景,一套代码部署在不同的环境中,随着时间的过去,各个环境代码有版本差异,代码层面可以通过不同的版本来控制,但是数据库层面经常容易忘记更新!

阅读全文 »

手把手教你给 SQL 做个优化

发表于 2020-08-08 | 分类于 mysql

在开始之前,咱们要知道:如果我的 SQL 语句执行的足够快,还有没有必要去做优化?

完全没有必要对吧

所以我们一般说,要给 SQL 做个优化,那肯定就是这条 SQL 语句执行的比较慢了

那么,为什么它会执行比较慢呢?

SQL 语句执行较慢的 3 个原因

没有建立索引,或者索引失效导致了 SQL 语句执行较慢

这个应该是比较好理解的,如果数据比较多,在千万级别以上,然后呢又没有建立索引,在这千万级别的数据中查找你想要的内容,简直就是在肉搏啊(哎呦,可了不得,竟然敢肉搏

索引失效这块内容说起来就比较多了,比如在查询的时候,让 like 通配符在前面了,比如经常念叨的“最左匹配原则”,又比如我们在查询条件中使用 or ,而且 or 前后条件中有一个列没有索引,等等这些情况都会导致索引失效

锁等待

常用的存储引擎主要有 InnoDB 和 MyISAM 这两种了,前者支持行锁和表锁,后者就只支持表锁

如果数据库操作都是基于表锁的话,意思就是说,现在有个更新操作,就会把整张表锁起来,那么查询的操作都不被允许,所以就不要说提高系统的并发性能了

  • 聪明的你肯定就知道了,既然 MyISAM 只支持表锁,那么使用 InnoDB 不就好了?你以为 InnoDB 的行锁不会升级成表锁嘛? too young too simple !

  • 如果对一张表进行大量的更新操作, mysql 就觉得你这样用会让事务的执行效率降低,到最后还是会导致性能下降,这样的话,还不如把你的行锁升级成表锁呢

  • 还有一点,行锁可是基于索引加的锁,在执行更新操作时,条件索引都失效了,那么这个锁也会执行从行锁升级为表锁

不恰当的 SQL 语句

这个也比较常见了,啥是不恰当的 SQL 语句呢?就比如,明明你需要查找的内容是 name , age ,但是呢,为了省事,直接 select * ,或者在 order by 时,后面的条件不是索引字段,这就是不恰当的 SQL 语句

优化 SQL 语句

在知道了 SQL 语句执行比较慢的原因之后,接下来要做的就是对症下药了

针对 没有索引/索引失效 这块,最有效的办法就是 EXPLAIN 语法了,那你知不知道 Show Profile 也可以嘞

针对 锁等待 这块,没办法了,只能自己多注意

针对 不恰当的 SQL 语句 这块,介绍几个常用的 SQL 优化,比如分页查询怎么优化一下可以查询的更快一些呀,你不是说 select * 不是正确的打开方式嘛?那什么是正确的 select 方式呢?别急嘛,阿粉下面都会说到的

废话不多说,咱们开始了

先来个表

为了确保优化后的结果和我写的一样(起码 90% 是相符的

所以咱们用一样的数据库好不好?乖~

首先建个 demo 的数据库

接下来咱们建表,就建个非常简单的表好不好

1
2
3
4
5
6
CREATE TABLE demo.table(
	id int(11) NOT NULL,
	a int(11) DEFAULT NULL,
	b int(11) DEFAULT NULL,
	PRIMARY KEY(id)
) ENGINE = INNODB

然后插入 10 万条数据

1
2
3
4
5
6
7
8
9
10
11
DROP PROCEDURE IF EXISTS demo_insert;
CREATE PROCEDURE demo_insert()
BEGIN
    DECLARE i INT; 
		SET i = 1;
    WHILE i <= 100000 DO
        INSERT INTO demo.`table` VALUES (i, i, i);
        SET i = i + 1 ;
    END WHILE;
END;
CALL demo_insert();

OK ,准备工作做好了,接下来开始实战

通过 EXPLAIN 分析 SQL 是怎样执行的

只要说 SQL 调优,那就离不开 EXPLAIN

EXPLAIN SELECT * FROM table WHERE id < 100 ORDER BY a;

咱们能够看到有好几个参数:

  • id :每个执行计划都会有一个 id ,如果是一个联合查询的话,这里就会显示好多个 id

  • select_type :表示的是 select 查询类型,常见的就是 SIMPLE (普通查询,也就是没有联合查询/子查询), PRIMARY (主查询), UNION ( UNION 中后面的查询), SUBQUERY (子查询)

  • table :执行查询计划的表,在这里我查的就是 table ,所以显示的是 table, 那如果我给 table 起了别名 a ,在这里显示的就是 a

  • type :查询所执行的方式,这是咱们在分析 SQL 优化的时候一个非常重要的指标,这个值从好到坏依次是: system > const > eq_ref > ref > range > index > ALL

    • system/const :说明表中只有一行数据匹配,这个时候根据索引查询一次就能找到对应的数据

    • eq_ref :使用唯一索引扫描,这个经常在多表连接里面,使用主键和唯一索引作为关联条件时可以看到

    • ref :非唯一索引扫描,也可以在唯一索引最左原则匹配扫描看到

    • range :索引范围扫描,比如查询条件使用到了 < , > , between 等条件

    • index :索引全表扫描,这个时候会遍历整个索引树

    • ALL :表示全表扫描,也就是需要遍历整张表才能找到对应的行

  • possible_keys :表示可能使用到的索引

  • key :实际使用到的索引

  • key_len :使用的索引长度

  • ref :关联 id 等信息

  • rows :找到符合条件时,所扫描的行数,在这里虽然有 10 万条数据,但是因为索引的缘故,所以扫描了 99 行的数据

  • Extra :额外的信息,常见的有以下几种

    • Using where :不用读取表里面的所有信息,只需要通过索引就可以拿到需要的数据,这个过程发生在对表的全部请求列都是同一个索引部分时

    • Using temporary :表示 mysql 需要使用临时表来存储结果集,常见于 group by / order by

    • Using filesort :当查询的语句中包含 order by 操作的时候,而且 order by 后面的内容不是索引,这样就没有办法利用索引完成排序,就会使用”文件排序”,就像例子中给出的,建立的索引是 id , 但是我的查询语句 order by 后面是 a ,没有办法使用索引

    • Using join buffer :使用了连接缓存

    • Using index :使用了覆盖索引

如果对这些参数了解的非常不错,那么 EXPLAIN 这块内容就难不住你了

Show Profile 分析下 SQL 执行性能

通过 EXPLAIN 分析执行计划,只能说明 SQL 的外部执行情况,如果想要知道 mysql 具体是如何查询的,需要通过 Show Profile 来分析

可以通过 SHOW PROFILES; 语句来查询最近发送给服务器的 SQL 语句,默认情况下是记录最近已经执行的 15 条记录,如下图我们可以看到:

我想看具体的一条语句,看到 Query_ID 了嘛?然后运行下 SHOW PROFILE FOR QUERY 82; 这条命令就可以了:

可以看到,在结果中, Sending data 耗时是最长的,这是因为此时 mysql 线程开始读取数据并且把这些数据返回到客户端,在这个过程中会有大量磁盘 I/O 操作

通过这样的分析,我们就能知道, SQL 语句在查询过程中,到底是 磁盘 I/O 影响了查询速度,还是 System lock 影响了查询速度,知道了病症所在,接下来对症下药就容易多了

分页查询怎么可以更快一些

在使用分页查询时,都会使用 limit 关键字

但是对于分页查询,其实还可以优化一步

我这里给出的数据库不是太好,因为它太简单了,看不出来有什么区别,我使用目前项目上正在用的表来做个实验,可以看下区别(使用的 SQL 语句如下面):

1
2
3
4
EXPLAIN SELECT * FROM `te_paper_record` ORDER BY id LIMIT 10000, 20;

EXPLAIN SELECT * FROM `te_paper_record` WHERE id >= ( SELECT id FROM `te_paper_record` ORDER BY id LIMIT 10000, 1) LIMIT 20;

上面一张图片,我没有使用子查询,可以看到执行了 0.033s ,下面的查询语句,我使用了子查询去做优化,能够看到执行了 0.007s ,优化的结果还是很显而易见的

那么,为什么使用了子查询,查询的速度就提上来了呢,这是因为当我们没有使用子查询时,查询到的 10020 行数据都返回回来了,接下来要对这 10020 行数据再进行过滤操作

那可不可以直接就返回需要的 20 行数据呢,这样就不需要再做过滤操作了,直接返回就可以了嘛

你也太聪明了吧。子查询就是在做这件事情

所以查询时间上有了一个很大的优化

正确的 select 打开方式

在查询时,有时为了省事,直接使用 select * from table where id = 1 这样的 SQL 语句,但是这样的写法在一些环境下是会存在一定的性能损耗的

所以最好的 select 查询就是,需要什么字段就查询什么字段

一般在查询时,都会有条件,按照条件查找

这个时候正确的 select 打开方式是什么呢?

如果可以通过主键索引的话, where 后面的条件,优先选择主键索引

为什么呢?这就要知道 MySQL 的存储规则

MySQL 常用的存储引擎有 MyISAM 和 InnoDB , InnoDB 会创建主键索引,而主键索引属于聚簇索引,也就是在存储数据时,索引是基于 B+ 树构成的,具体的行数据则存储在叶子节点

也就是说,如果是通过主键索引查询的,会直接搜索 B+ 树,从而查询到数据

如果不是通过主键索引查询的,需要先搜索索引树,得到在 B+ 树上的值,再到 B+ 树上搜索符合条件的数据,这个过程就是“回表”

很显然,回表能够产生时间。

这也是为什么建议, where 后面的条件,优先选择主键索引

其他调优

看完上面的,心里应该就大概有数了, SQL 调优主要就是建立索引/防止产生锁等待/使用恰当的 SQL 语句去查询

但是,如果问你除了索引,除了上面这些手段,还有没有其他调优方式

啥?竟然还有?!

有的,这就需要跳出来,不要局限在具体的 SQL 语句上了,需要在数据库设计之初就考虑好

比如说,我们常说的要遵循三范式,但是在有的业务场景里面,如果在数据库里面多几个冗余字段的话,可能要比严格遵循三范式带来的性能要好很多。

但是这点就及其考验平时的积累了,阿粉在这里把这一点提出来之后,希望读者们可以看看自己项目上目前用的数据库有没有多余的字段,为什么要这样设计呢?这样多去观察,你的技术能力想不提高都很难

以上,就这样啦~

阅读全文 »

面试官:生产服务器变慢了,你能谈谈诊断思路吗

发表于 2020-08-04 | 分类于 linux

面试官都这么问了,我能说不能吗?

阅读全文 »

公司差点因为代码写得差把阿粉直接给开掉

发表于 2020-08-03 | 分类于 CodeReview

前几天阿粉的公司不是在进行CodeReview吗,而在此期间,不光进行了CodeReview,而且间接的也进行了代码的重构,这时候阿粉就不得自己开始研究这个代码重构这块的事情了,于是砸锅卖铁的买了这个《重构:改善既有的代码设计》这本书,看了几篇文章,确实有一些心得,阿粉也是把自己的心得体会一点点的分享给大家。

阅读全文 »

这是花的最值得的一笔钱了!

发表于 2020-08-02 | 分类于 java

Hello 大家好,我是阿粉,转眼已经到八月份了,好久也没有给大家介绍知识星球的动态了,今天给大家看下星球七月份的一些优质的分享,有专业技术的,有职场生涯的,有干货分享的,也有解答疑惑的,下面我们来看下吧。

阅读全文 »

单例模式

发表于 2020-08-02 | 分类于 java

作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类。

阅读全文 »

模版模式

发表于 2020-08-02 | 分类于 java

模版模式又叫模板方法模式,在一个方法中定义算法的骨架,而将一些步骤延迟到子类中去实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

阅读全文 »

List 中的元素去重操作

发表于 2020-08-02 | 分类于 java

本文主要介绍List集合中的元素去重操作

阅读全文 »

三次握手和四次挥手说完了,还让我手动写个HTTP协议代码

发表于 2020-07-31 | 分类于 HTTP

最近阿粉的同事们在准备面试,其中也有收到offer的几个不错的人,毕竟疫情稳定了,而阿粉在电话面试的时候,被问到关于HTTP协议的内容的时候,却显得有点麻木了,为什么呢?因为套路太深了,让阿粉猝不及防呀。 <–more–>

面试官:你了解TCP/IP协议么?

说实话在阿粉听到这个问题的时候,阿粉的第一想法就是,我回答了这个问题,接下来肯定还有一个三次握手和四次挥手等着我,但是还是得回答呀,于是阿粉就开始作答了。

阿粉开始作答:TCP/IP协议虽然会放在一起说,但是他们其实呢是属于两个不同的协议。

  • IP协议:

    1
      IP协议实际上是用来查找地址的,而它对应的层级也是网络层,也可以称之为网际互联层,区别不大,叫法不同而已。
    
  • TCP协议:

    1
      TCP协议是用来规范传输规则的,和IP协议是不同的,而它对应的层级是传输层,而这样的话,也就是IP去寻找地址,把所有的传输任务都交给TCP,而TCP这时候就相当于一个快递员的身份出现并且存在。
    

面试官:那你说说什么是三次握手,什么是四次挥手吧

三次握手

大家看这个图,图是来自于百度搜索,而且百度上有各种各样的图,当你看到图的时候第一时间肯定是看不懂的,也就是只能通过这个画的标志的“线”来进行分析,其实这仅仅只是一个方面。

那么我们就来根据图来解析一下这个图中都代表了什么意思,图中存在着两个序号和三个不同的标志位其中有大小写容易混淆的呦。

序号:

  • seq:sequence number 的缩写,直译的话,序号,对没错,它就是序号,你没有翻译错,相信自己,而这个seq表示的则是自己传递的序号,TCP在传输的时候,其中的每一个字节,都会有一个序号,发送数据的时候,会把第一个数据的第一个序号发送给对方,就是我们所看到的第一步,而接收的这一方面,会按照这个序号来检查是否是一个连接完整的数据,如果说你数据是完整的,那么好,我们可以继续下一步,如果你不是完整的,那就重新传送呗,而这样的话也能保证数据的完整性不被破坏。

  • ack:注意,这是小写的ack,也就是acknoledgement number的缩写,而他表示的是确认号,这个要和ACK(确认位)进行区分,接收端这时候用它来给发送端返回成功接收消息的数据信息,而这时候,它的值就是表明,我现在想接收下一个数据包了,而这个值就是下一个数据包的开始的序号,而这个ack所代表的的值的序号前面的数据都已经接收成功了。

  • ACK:确认位,确认位来了,只有当ACK=1的时候ack才会起到自己应该起的作用,而在我们第一次发起请求的时候,因为没有需要我们确认的接收的数据,所以这个时候的ACK就是0,而正常通信的情况下,ACK就1.

  • SYN:同步位,而同步位的作用就是用于建立连接时同步序号,而刚连接的时候,说ACK是0,那么ack就不起作用,这时候SYN就来说,你看没我你们不行了把,要你们有何用,当接收端接收到SYN=1的报文的时候,就会将ack设置为接收到的seq+1的值,这也是大家在看百度上提供的内容的时候看到的,各种seq=k,ACK=k+1,这玩意就是这么来的,这时候ack的值就是根据SYN来直接设置的,这样你才能正常的进行传输,而SYN有时候会被面试官问到为什么在前两次握手的时候都是1呢?其实这是因为传输数据的双方的ack都是要一个初始值的,不然你还怎么传输,还怎么玩。

  • FIN:终止位,这个在本图中,并没有完全的体现,在四次挥手的时候就能完全的体现出来了。而它则是用来在数据传输都完成之后来释放连接的。

那么关于这个图,我们怎么给面试官说呢?

(1) 第一次握手(SYN=1, seq=x):

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

(2) 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

(3) 第三次握手(ACK=1,ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

你如果这么说,面试官有可能还会问,你这也太官方了,能不能说说你的理解,那么你可以用一个实际上的例子来给他说一下,

阿粉:鸡丁,嘿,我是阿粉,你听的到我说话么?

鸡丁:吵吵啥,听到了,除了你我还能认识谁。

阿粉:你听的到你还不赶紧回复,怪不得你没有女朋友呢。那我们再继续交流一下吧。

而这三次的对话过程就是通俗的三次握手,期间对话三次,以此来确定两个方向上的数据传输通道是否正常。

四次挥手

那么四次挥手怎么来回答呢?

(1)第一次挥手(FIN=1,seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

(2) 第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

(3) 第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

(4) 第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。两次后会重传直到超时。如果多了会有大量半链接阻塞队列。

那么怎么去通俗的给面试官说呢?

阿粉:鸡丁呀,我要说的都说完了,你还有啥事么?

鸡丁:你说的我都明白了,但是别断,我还有要嘱咐你的,给我找女朋友的事情。

鸡丁:xxxxx,我说完了。

阿粉,行啦,别BB了,记住了,挂了把。

如果面试官问你的时候,你这么回答的话,既有官方的解释,还有本身自己的理解,那么这个问题就已经算是差不多了,

而面试官显然不可能会这么放过你,肯定再给你来个雷,为啥是三次握手,而是四次挥手呢?为啥不是三次呢?

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方ACK和FIN一般都会分开发送。所以这时候挥手的时候就是四次,而不再是三次了。

那么我们怎么去手写一个HTTP协议呢?代码送上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            if (selector.select(3000)==0){
                continue;
            }
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                new Thread(new HttpHandler(key)).run();
                keyIterator.remove();
            }
        }
    }
    private static class HttpHandler implements Runnable{

        private int bufferSize = 1024;
        private String localCharset = "UTF-8";
        private SelectionKey key;
        public HttpHandler(SelectionKey key){
            this.key=key;
        }

        public void handleAccept()throws IOException{
            SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept();
            clientChannel.configureBlocking(false);
            clientChannel.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        @Override
        public void run() {
            try {
                if (key.isAcceptable()){
                    handleAccept();
                }
                if (key.isReadable()){
                    handleRead();
                }
            }catch (IOException e){
               e.printStackTrace();
            }
        }

        public void handleRead() throws IOException{
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            if (sc.read(buffer)==-1){
                sc.close();
            }else {
                buffer.flip();
                String receiveString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                String[] requestMessage = receiveString.split("\r\n");
                for (String s:requestMessage) {
                    System.out.println(s);
                    if (s.isEmpty()){
                        break;
                    }
                    String[] firstLine = requestMessage[0].split(" ");
                    System.out.println();
                    System.out.println("Method:\t"+firstLine[0]);
                    System.out.println("url:\t"+firstLine[1]);
                    System.out.println("HTTP Version:\t"+firstLine[2]);
                    System.out.println();

                    StringBuffer sendString = new StringBuffer();
                    sendString.append("HTTP/1.1 200 OK\r\n");
                    sendString.append("Content-Type:text/html;charset="+localCharset+"\r\n");
                    sendString.append("\r\n");
                    sendString.append("<html><head><title>显示报文</title></head><body>");
                    sendString.append("接受到的请求报文是:<br/>");
                    for (String s1:requestMessage) {
                        sendString.append(s1+"<br/>");
                    }
                    sendString.append("</body></html>");
                    buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
                    sc.write(buffer);
                    sc.close();
                }
            }

        }

    }
}

这这是一个简单的实现,只是实现思路,并不是真正的处理请求,而大家也要注意设置Content-Type的类型,不然容易出问题的,毕竟长度是有限制的。 文章参考 《HTTP协议详解》 《TCP/IP协议详解》

阅读全文 »

SpringBoot2.x 整合 邮件发送

发表于 2020-07-27 | 分类于 SpringBoot

在实际项目中,经常需要用到邮件通知功能。比如,用户通过邮件注册,通过邮件找回密码等;又比如通过邮件发送系统情况,通过邮件发送报表信息等等,实际应用场景很多。

正常我们会用 JavaMail 相关 api 来写发送邮件的相关代码,但现在 SpringBoot 提供了一套更简易使用的封装。这篇文章,阿粉就带大家通过 SpringBoot 快速的实现发送邮件的功能。

阅读全文 »

那些你应该知道的,但是你一定不知道的 Git 骚操作

发表于 2020-07-24 | 分类于 java

Hello 大家好,我是阿粉,作为团队中的主程阿粉经常参与很多核心功能的开发,而且很多时候一个需求没做好中间又插入新的紧急的需求或者 bug 修复,每次遇到这种情况,如果两个地方代码不冲突的话还好,可以直接在本分支修改然后提交,但是当遇到需要修改同一个类文件的时候就比较麻烦了。这种情况如何优雅的处理呢?让阿粉来带你了解 Git 的高级骚操作!

阅读全文 »

面试官:假如说我们现在要做一个千万级用户量网站,你怎么设计高并发架构?

发表于 2020-07-24 | 分类于 面试

之前的时候,阿粉一直在看同事面试,但是呢,阿粉并没有自己去面试,而无意间打开Boss的时候,发现一家公司私聊了我,我回复了一下之后,竟然说我可以去面试,不曾想,面试一个问题,让我的薪资瞬间被砍掉了5K,你如果不想自己出去要的薪资被砍,那么你要会设计这个。 <–more–>

一般的普通项目架构

像阿粉的朋友所在的公司属于一个中小型的公司,公司的项目都是按照客户的要求来定制进行开发的,而服务器的数量那是少的可怜,什么高并发,不考虑,什么高可用,也不考虑,一台服务器上面部署了自己的项目,有时候连个图片服务器都没有,他们的图就是这个样子的。

是不是看着很简陋的样子,直接浏览器客户端和单一服务器之间进行交流了,如果访问人数在前人以上,比如秒杀个限量款,那估计可能直接就凉了,“Boom”的一下就访问失败了。

稍微进阶版本的项目架构

这个时候一般网站架构还是采用单体架构,但是服务器也相对的增加了,终于增加了数据库服务器和应用类服务器在加上图片服务器,算是组成了一个小小的进阶版本的项目架构。

也就是说我们在部署应用的时候,手动把应用服务器上的Tomcat给关掉,然后替换系统的代码war包,接着重新启动Tomcat。

这时候把数据单独的部署在另外的一个服务器上面,存放网站的全部核心数据。

然后在另外一台独立的服务器上部署NFS作为图片服务器,存放网站的全部图片。

这时候呢,代码执行的是请求,数据访问在另外一台服务器,而图片在另外一台服务器上面,这样就通过增加了服务器来部署的普通版本的项目架构就完成了,而大部分的项目都是这个样子的吧,毕竟小公司人力有限呀。

简单的支持高并发,高可用的项目架构

不知道大家在面试的时候说这个的时候面试官有没有问过,如果你们的应用服务器挂了,你们怎么处理?

是的,应用服务器真的会出事,那在项目的应用服务器真的出事之后,我们怎么处理,在换个服务器,那么你很多想对应的配置就要做出更改,需要更改一大堆的东西,那么你会有很长时间的维护期,维护代价很高了呀。

这时候我们就出现了这个:

如果说我们有一台其中的应用服务器宕机了,不干活了,那么接下来,另外的一台服务器就会正常的使用,并且,在同时使用的时候,我们还能够把从浏览器来的大量的请求分发一下,直接对半劈成2份,如果说其中有一个服务器的配置“高的飞起”,那么我们还可以通过配置,给他的请求让他多一点,“设置权重”,这样让他分担更多的压力,而那个比较 low 的服务器的话,承担的相应的请求就会减少一点,毕竟性能比较低。

而在这个时候还会出现另外一种意外,那就是数据库服务器宕机了,怎么办?相同的原理,增加另外的一个数据库服务器,也就是主从数据库。

那么为什么要做主从复制,不单单是因为这一个服务器宕机的问题,还有如果对数据库的读和写都在同一个数据库服务器中操作,业务系统性能会降低。

而我们很多的项目在数据上面需要的就是比较重要的,就很多就是做读写分离,而为了提升业务系统性能,优化用户体验,可以通过做主从复制(读写分离)来减轻主数据库的负载。

一台用于写入数据,一台用于同步主的数据并用于数据查询操作。

配置好主从复制之后,同一张表,只能对一个服务器写操作。

业务分离

现在很多项目的架构都是所有人的业务代码都写在了一起,乱七八糟的,好几个人的代码,维护起来那叫一个崩溃,当你看到这样的项目的时候,你就会发现,人都傻了,这是个什么鬼,改动一点,其他的不好用了,那叫一个崩溃呀。

这时候我们就需要把我们的业务彻底的做出分割,比如商城里面的,订单属于一块的业务,积分属于一部分的业务,物流属于另外一部分的业务,这时候我们就需要分成三块的业务模块。

也就是相当于每一块的内容都属于一个服务,而这些服务叠加起来可就不是1+1=2的问题了,这些业务服务相加起来,这时候完整的项目和你把所有的内容同一写到一个部分的内容差距是非常大的,尤其是在代码冗余方面,做的那是相当透彻的。

而说到这给内容的话,其实已经算差不多了,但是再更深的阿粉是真的没有了解到那么多,而阿粉之前没想过这些内容会在你面试的时候问到,可能阿粉当时只是想自己做个咸鱼,不去关心架构方面的事情,而架构方面的事情确实很多公司很看重的一点。

在这里阿粉给大家再次贡献出此次面试的其他的问题,并且给出详细的解答,并且阿粉也是很欣慰的,只是压了工资,而没有说直接拒绝。

1.Redis分区实现方案

  • 客户端分区:

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。

  • 代理分区:

代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。

  • 查询路由:

查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

这些都是阿粉死记硬背背下来的,关于这种东西,没有实际亲身实践过的,没有遇到过的问题的,都是属于被diss的,但是面试官好像理解也不是很深,也是知道,但是具体的也没有仔细的深挖我这块的内容。

2.SpringBean的生命周期

说实话,这个问题,面试好像现在都是必问的知识点了,阿粉的同事面试也是被问,但是巧了,之前阿粉就写过关于SpringBean的生命周期的,文章链接给大家送上,大家可以仔细查看,可以共同交流呦。

面试官:兄弟你来阐述一下Spring框架中Bean的生命周期?

所以大家一定要把这个准备好呦,SpringBean的生命周期,从初始化到最终的销毁中间经历了什么,过程是什么,流程怎么理解。

3.HashMap 和 Hashtable

这个问题我在回答的时候我就分开了,分门别类的比较比如说:

  • 线程安全

  • 性能优劣

  • NULL

  • 实现方式

  • 容量扩容

我分成了五块的内容作答,如果说你只能够刚刚想起来其中的三到四点也是不错的,至少比你只会说他们的线程安全和是否允许键为空来的好很多,如果大家有兴趣,请寻找集合系列文章,在过往的历史当中寻找一下,有很多关于HashMap 和 Hashtable的面试点。

4.JVM内存模型和优化

这个我相信只要是工作了两到三年的程序员肯定都会,我就给大家简单说说,之前的文章图解,

关于两篇文章通俗易懂,简单明了,几次面试,回答没有遗漏,面试官觉得还行,文章连接送上:

你还在为了JVM而烦恼么?(内存结构和垃圾回收算法)

老年代的垃圾回收算法

关于其他的问题,都不是比较经典的,都是属于项目业务中的了,阿粉就不再一一给大家介绍了,总结起来就一句话,基础你要掌握,扩展你更要会,不然面试想要高工资?那是不可能的,毕竟不是每家公司都缺“大爷”,不是么?

阿粉在这里希望大家找到自己理想的工作,等疫情稳定了,直接跳槽工资“Double”。

阅读全文 »

面试不说点分布式的东西,面试官都有点看不起我呀

发表于 2020-07-19

现在的面试和几年前的面试差距很大了,现在培训机构出来的同学很多都占一大部分,而且那张口要的可是真的有点多,甚至比一些工作两三年的朋友们还多,不排除有一些大牛的存在,但是还是有一些不是很给力的,但是想一口吃撑的,而有经验的面试官,分分钟就能看出来你到底是有工作经验还是没有工作经验的。而分布式中的内容,将会是极其重要的一点内容。

阅读全文 »
1 … 10 11 12 … 32
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

633 日志
116 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2022 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist