Java多線程(二):Thread類

Thread類的實例方法

start()

start方法內部會調用方法start方法啟動一個線程,該線程返回start方法,同時Java虛擬機調用native start0啟動另一個線程調用run方法,此時有兩個線程并行執行;
我們來分析下start0方法,start0到底是如何調用run方法的

Thread類里有一個本地方法叫registerNatives,此方法註冊一些本地方法給Thread類使用
在OpenJDK官網找到Thread.c

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"

#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread}, //Java中Thread類的start方法所調用的start0方法
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

......

根據關鍵字”JVM_StartThread”再找到jvm.cpp

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
    MutexLocker mu(Threads_lock);
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz); //請看這裏,實例化了一個線程native_thread

      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

sz是大小參數,忽略之,我們看thread_entry是什麼

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          vmSymbols::run_method_name(),  //請看這裏,jvm調用run_method_name方法
                          vmSymbols::void_method_signature(),
                          THREAD);
}

run_method_name在vmSymbols.hpp被定義

  /* common method and field names */                                                             
  template(run_method_name,                           "run")      //run_method_name的名稱是"run"

簡言之:當前線程調用start方法通知ThreadGroup當前線程可以運行了,可以被加入了,當前線程啟動后,當前線程狀態為”Runnable”。另一個線程等待CPU時間片,調用run方法(線程真正執行)。產生一個異步執行的效果;
用start方法來啟動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼。
代碼如下

public class MyThread03 extends Thread{
    public void run()
    {
        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        MyThread03 mt = new MyThread03();
        mt.start();

        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

執行結果如下,可以看到,Thead-0和main線程交叉執行,是無序的。很好理解,因為main和Thread-0在爭搶CPU資源,這個過程是無序的。

run = main
run = Thread-0
run = main
run = main
run = Thread-0
run = Thread-0

再看一個例子,代碼如下

public class MyThread04 extends Thread{
    public void run()
    {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args)
    {
        MyThread04 mt0 = new MyThread04();
        MyThread04 mt1 = new MyThread04();
        MyThread04 mt2 = new MyThread04();

        mt0.start();
        mt1.start();
        mt2.start();
    }
}

執行結果如下

Thread-0
Thread-2
Thread-1

我們依次啟動mt0,mt1,mt2,這說明線程啟動順序也是無序的。因為start方法僅僅返回調用,線程想要執行必須得到CPU時間片再執行run方法,CPU時間片的獲得是無序的。

run()

run方法是Thread類的一個普通方法,執行run方法其實是單線程執行

public class MyThread05 extends Thread{

    public void run()
    {
        System.out.println("run = " + Thread.currentThread().getName());
    }

    public static void main(String[] args)
    {
        MyThread05 mt = new MyThread05();
        mt.run();

        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

輸出結果如下

run = main
run = main
run = main
run = main

main線程循環了3次,run方法1次,結果是main線程執行了四次,我們寫在run方法體內的被main線程執行,這說明調用run方法執行多線程是不可行的。

isAlive()

判斷線程是否存活

public class MyThread06 extends Thread{
    public void run()
    {
        System.out.println("run = " + this.isAlive());
    }


    public static void main(String[] args) throws Exception
    {
        MyThread06 mt = new MyThread06();
        System.out.println("begin == " + mt.isAlive());
        mt.start();
        Thread.sleep(100);
        System.out.println("end == " + mt.isAlive());
    }
}

輸出結果如下,增加0.1秒延遲,讓線程執行完

begin == false
run = true
end == false

可以看到,執行前false,執行中true,執行后false

getId()

返回線程的標識符,線程ID是正值,線程ID在生命周期內不會變化,當線程終止了,線程ID可能會被重用

getName()

返回線程名稱

getPriority()和setPriority(int)

返回優先級和設置優先級
優先級越高的線程獲取CPU時間片的概率越高
請看如下的例子

public class MyThread07_0 extends Thread{
    public void run()
    {
        System.out.println("MyThread07_0 run priority = " +
                this.getPriority());
    }

    public static void main(String[] args)
    {
        System.out.println("main thread begin, priority = " +
                Thread.currentThread().getPriority());
        System.out.println("main thread end, priority = " +
                Thread.currentThread().getPriority());
        MyThread07_0 thread = new MyThread07_0();
        thread.start();
    }
}

運行結果如下

main thread begin, priority = 5
main thread end, priority = 5
MyThread07_0 run priority = 5

線程的默認優先級是5
再看如下的例子

public class MyThread07_1 extends Thread {

    public void run()
    {
        System.out.println("MyThread07_1 run priority = " +
                this.getPriority());
        MyThread07_0 thread = new MyThread07_0();
        thread.start();
    }

    public static void main(String[] args)
    {
        System.out.println("main thread begin, priority = " +
                Thread.currentThread().getPriority());
        System.out.println("main thread end, priority = " +
                Thread.currentThread().getPriority());
        MyThread07_1 thread = new MyThread07_1();
        thread.start();
    }
}

我們在MyThread07_1線程內部啟動MyThread07_0線程,我們觀察MyThread07_1和MyThread07_0的優先級有什麼關係。
運行結果如下

main thread begin, priority = 5
main thread end, priority = 5
MyThread07_1 run priority = 5
MyThread07_0 run priority = 5

MyThread07_0和MyThread07_1線程的優先級一致,說明線程具有繼承性。
現在我們來設置優先級

public class MyThread08 {

    static class MyThread08_0 extends Thread {
        public void run() {
            long beginTime = System.currentTimeMillis();
            for (int j = 0; j < 1000000; j++) {}
            long endTime = System.currentTimeMillis();
            System.out.println("★★★★ MyThread08_0 use time = " +
                    (endTime - beginTime));
        }
    }

    static class MyThread08_1 extends Thread {
        public void run()
        {
            long beginTime = System.currentTimeMillis();
            for (int j = 0; j < 1000000; j++){}
            long endTime = System.currentTimeMillis();
            System.out.println("☆☆☆☆ MyThread08_1 use time = " +
                    (endTime - beginTime));
        }
    }

    public static void main(String[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            MyThread08_0 mt0 = new MyThread08_0();
            mt0.setPriority(5);
            mt0.start();
            MyThread08_1 mt1 = new MyThread08_1();
            mt1.setPriority(4);
            mt1.start();
        }
    }

}

我們給MyThread08_0線程設置更高的優先級5
運行結果如下

★★★★ MyThread08_0 use time = 7
☆☆☆☆ MyThread08_1 use time = 4
★★★★ MyThread08_0 use time = 18
★★★★ MyThread08_0 use time = 16
★★★★ MyThread08_0 use time = 20
★★★★ MyThread08_0 use time = 17
☆☆☆☆ MyThread08_1 use time = 0
☆☆☆☆ MyThread08_1 use time = 10
☆☆☆☆ MyThread08_1 use time = 9
☆☆☆☆ MyThread08_1 use time = 8

可以看到MyThread08_0先執行的次數更多,輸出結果為實心五角星的這個。
多運行幾次,都會是MyThread08_0先打印完,每次結果都不盡相同,CPU會盡量先讓MyThread08_0執行完。

isDaemon()和setDaemon(boolean)

isDaemon方法判斷是否是守護線程;
setDaemon設置守護線程
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
我們自定義的線程和main線程都是用戶線程,我們熟知的GC(垃圾回收器)就是守護線程。守護線程是用戶線程的“奴僕”,當用戶線程執行完畢,守護線程就會終止,因為它沒有存在的必要了。
如用戶線程執行結束,GC無垃圾可回收,它只能死亡
看如下代碼

public class MyThread09 extends Thread{
    private int i = 0;

    public void run()
    {
        try
        {
            while (true)
            {
                i++;
                System.out.println(Thread.currentThread().getName()+" i = " + i);
                Thread.sleep(1000);
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        try
        {
            MyThread09 mt = new MyThread09();
            mt.setDaemon(true);
            mt.start();
            Thread.sleep(5000);
            System.out.println("現在是"+Thread.currentThread().getName()+"線程");
            Thread.sleep(1);

        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

我們自定義MyThread09線程的run方法里是死循環,如果是用戶線程,它應該永遠地執行下去,現在把它設置成守護線程。
注意:mt.setDaemon(true);要在mt.start();之前,見

否則會拋出IllegalThreadStateException異常
運行結果如下

Thread-0 i = 1
Thread-0 i = 2
Thread-0 i = 3
Thread-0 i = 4
Thread-0 i = 5
現在是main線程
Thread-0 i = 6
MyThread09變成了守護線程,它的使命已經完成。現在是main線程

Thread.sleep(5000)的目的是使main線程沉睡5s,即用戶線程(main線程)仍在執行,此時main線程輸出,再沉睡1ms,當main線程執行完畢,守護線程就沒有存在的意義了,即死亡;
main線程總共執行了大約5001ms(略大於這個數值),Thread-0打印到i=6,說明守護線程在main線程之後死亡,這個時間差極小

interrupt()

設置中斷標誌位,無法中斷線程

public class MyThread10 extends Thread{
    public void run()
    {
        for (int i = 0; i < 500000; i++)
        {
            System.out.println("i = " + (i + 1));
        }
    }

    public static void main(String[] args)
    {
        try
        {
            MyThread10 mt = new MyThread10();
            mt.start();
            Thread.sleep(2000);
            mt.interrupt();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

輸出結果如下

......
i = 499993
i = 499994
i = 499995
i = 499996
i = 499997
i = 499998
i = 499999
i = 500000

可以看到,interrupt()沒有中斷線程,interrupt()後續將會詳細講解

isInterrupted()

判斷線程是否被中斷

join()

等待這個線程死亡,舉例說明:
線程A執行join方法,會阻塞線程B,線程A join方法執行完畢,才能執行線程B
代碼如下

public class MyThread11 extends Thread{
    public void run()
    {
        try
        {
            int secondValue = (int)(Math.random() * 1000);
            System.out.println(secondValue);
            Thread.sleep(secondValue);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception
    {
        MyThread11 mt = new MyThread11();
        mt.start();
        mt.join();
        System.out.println("MyThread11執行完畢之後我再執行");
    }
}

輸出結果如下

75
MyThread11執行完畢之後我再執行

可以看到,main線程在mt線程之後執行。mt調用join方法,使main線程阻塞,待mt線程執行完畢,方可執行main線程。

Thread類的靜態方法

currentThread()

返回當前正在執行線程的引用

public class MyThread12 extends Thread{

    static
    {
        System.out.println("靜態塊的打印:" +
                Thread.currentThread().getName());
    }

    public MyThread12()
    {
        System.out.println("構造方法的打印:" +
                Thread.currentThread().getName());
    }

    public void run()
    {
        System.out.println("run()方法的打印:" +
                Thread.currentThread().getName());
    }



    public static void main(String[] args)
    {
        MyThread12 mt = new MyThread12();
        mt.start();
    }


}

輸出結果

靜態塊的打印:main
構造方法的打印:main
run()方法的打印:Thread-0

可以看到,構造方法和靜態塊是main線程在調用,重寫的run方法是線程自己在調用。
再看個例子

public class MyThread13 extends Thread{
    public MyThread13()
    {
        System.out.println("MyThread13----->Begin");
        System.out.println("Thread.currentThread().getName()----->" +
                Thread.currentThread().getName());
        System.out.println("this.getName()----->" + this.getName());
        System.out.println("MyThread13----->end");
    }

    public void run()
    {
        System.out.println("run----->Begin");
        System.out.println("Thread.currentThread().getName()----->" +
                Thread.currentThread().getName());
        System.out.println("this.getName()----->" + this.getName());
        System.out.println("run----->end");
    }



    public static void main(String[] args)
    {
        MyThread13 mt = new MyThread13();
        mt.start();
    }


}

輸出結果

MyThread13----->Begin
Thread.currentThread().getName()----->main
this.getName()----->Thread-0
MyThread13----->end
run----->Begin
Thread.currentThread().getName()----->Thread-0
this.getName()----->Thread-0
run----->end

可以看到,執行MyThread13構造方法的線程是main,執行MyThread13的線程是Thread-0(當前線程),run方法就是被線程實例所執行。

sleep(long)

讓當前線程沉睡若干毫秒

public class MyThread14 extends Thread{
    public void run()
    {
        try
        {
            System.out.println("run threadName = " +
                    this.getName() + " begin");
            Thread.sleep(2000);
            System.out.println("run threadName = " +
                    this.getName() + " end");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        MyThread14 mt = new MyThread14();
        mt.start();
    }
}

輸出結果如下

run threadName = Thread-0 begin
run threadName = Thread-0 end

打印完第一句兩秒后打印第二句。

yield()

當前線程放棄CPU的使用權,這裏的放棄是指當前線程少用CPU資源,最後線程還是會執行完成

public class MyThread15 extends Thread {
    public void run()
    {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 5000000; i++)
        {
            Thread.yield();
            count = count + i + 1;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用時:" + (endTime - beginTime) + "毫秒!");
    }



    public static void main(String[] args)
    {
        MyThread15 mt = new MyThread15();
        mt.start();
    }


}

輸出結果如下

用時:4210毫秒!

可以看到,任務執行完畢,當我們把Thread.yield();註釋掉,執行時間只需要7ms。說明當前線程放棄了一些CPU資源。

interrupted()

判斷當前線程是否中斷,靜態版的isInterrupted方法。多線程中斷機制,後續會詳細解析。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

python算法與數據結構-希爾排序(35)

一、希爾排序的介紹

  希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的記錄越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。  

二、希爾排序的原理

  在前面文章中介紹的直接插入排序,它對於已經基本有序的數據進行排序,效率會很高,而如果對於最初的數據是倒序排列的,則每次比較都需要移動數據,導致算法效率降低。

      希爾排序的基本思想就是:將需要排序的序列邏輯上劃分為若干個較小的序列(但並非真的分割成若干分區),對這些邏輯上序列進行直接插入排序,通過這樣的操作可使需要排序的數列基本有序,最後再使用一次直接插入排序。

      在希爾排序中首先要解決的是怎樣劃分序列,對於子序列的構成不是簡單地分段,而是採取將相隔某個增量的數據組成一個序列。一般選擇增量的規則是:取上一個增量的一半作為此次子序列劃分的增量,一般初始值元素的總數量的一半。

三、希爾排序的圖解 

 

四、希爾排序的python代碼實現

# 創建一個希爾排序的函數
def shell_sort(alist):
    # 需要排序數組的個數
    N = len(alist)
    # 最初選取的步長
    gap = N//2
    
    # 根據每次不同的步長,對分組內的數據進行排序
    # 如果步長沒有減為1就繼續執行
    while gap>0:
        # 對每個分組進行插入排序,
        # 因為插入排序從第二個元素開始,而這裏第二個元素的下標就是gap
        # 所以i的起始點是gap
        for i in range(gap,N):
            # 控制每個分組內相鄰的兩個元素,邏輯上相鄰的兩個元素間距為gap,
            # j的前一個元素比它少一個gap距離,所以for循環中j的步長為 -gap
            for j in  range(i,0,-gap):
                # 判斷和邏輯上的分組相鄰的兩個數據大小
                if alist[j]<alist[j-gap] and j-gap>=0:
                    # 交換
                    temp = alist[j]
                    alist[j] = alist[j-gap]
                    alist[j-gap] = temp
        # 改變步長
        gap = gap//2
    
    
numlist = [5,7,8,3,1,2,4,6,9]
print("排序前:%s"%numlist)
shell_sort(numlist)
print("排序后:%s"%numlist)

運行結果為:

排序前:[5, 7, 8, 3, 1, 2, 4, 6, 9]
排序后:[1, 2, 3, 4, 5, 6, 7, 8, 9]

五、希爾排序的C語言實現

#include <stdio.h>
// 創建一個希爾排序的函數
void shell_sort(int arr[],int arrLength,int gap)
{
    // 根據每次不同的步長,對分組內的數據進行排序
    // 如果步長沒有減為1就繼續執行
    while (gap>0)
    {
        // 對每個分組進行插入排序,
        // 因為插入排序從第二個元素開始,而這裏第二個元素的下標就是gap,
        // 所以i的起始點是gap
        for (int i = gap; i<arrLength; i++)
        {
            // 控制每個分組內相鄰的兩個元素,邏輯上相鄰的兩個元素間距為gap,
            // j的前一個元素比它少一個gap距離,所以for循環中j每次減少一個gap
            // 因為j-gap是上一個元素的下標,也必須保證大於等於0
            for (int j = i; j>0&&j-gap>=0; j=j-gap)
            {
                // 判斷和邏輯上的分組相鄰的兩個數據大小
                if (arr[j]<arr[j-gap])
                {
                    // 交換
                    int temp = arr[j];
                    arr[j] = arr[j-gap];
                    arr[j-gap] = temp;
                }
            }
        }
        gap = gap/2;
    }
}

int main(int argc, const char * argv[]) {
   
    // 定義數組
    int array[] = {5,7,8,3,1,2,4,6,9};
    // 希爾排序的聲明
    void shell_sort(int arr[],int arrLength,int gap);
    // 計算數組長度
    int len = sizeof(array)/sizeof(int);
    // 制定gap為二分之一的長度
    int g = len/2;
    // 使用希爾排序
    shell_sort(array, len, g);
    // 驗證
    for (int i = 0; i<len; i++)
    {
        printf("%d ",array[i]);
    }
    
    return 0;
}

運行結果為:

1 2 3 4 5 6 7 8 9

 

六、希爾排序的時間複雜度

  • 最優時間複雜度:根據步長序列的不同而不同
  • 最壞時間複雜度:O(n2)

七、希爾排序的穩定性

  由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

HBase 系統架構及數據結構

一、基本概念

一個典型的Hbase Table 表如下:

1.1 Row Key (行鍵)

Row Key是用來檢索記錄的主鍵。想要訪問HBase Table中的數據,只有以下三種方式:

  • 通過指定的Row Key進行訪問;
  • 通過Row Key的range進行訪問,即訪問指定範圍內的行;
  • 進行全表掃描。

Row Key可以是任意字符串,存儲時數據按照Row Key的字典序進行排序。這裏需要注意以下兩點:

  • 因為字典序對Int排序的結果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作為行鍵,那麼為了保持整型的自然序,行鍵必須用0作左填充。
  • 行的一次讀寫操作時原子性的 (不論一次讀寫多少列)。

1.2 Column Family(列族)

HBase表中的每個列,都歸屬於某個列族。列族是表的Schema的一部分,所以列族需要在創建表時進行定義。列族的所有列都以列族名作為前綴,例如courses:history,courses:math都屬於courses這個列族。

1.3 Column Qualifier (列限定符)

列限定符,你可以理解為是具體的列名,例如courses:history,courses:math都屬於courses這個列族,它們的列限定符分別是history和math。需要注意的是列限定符不是表Schema的一部分,你可以在插入數據的過程中動態創建列。

1.4 Column(列)

HBase中的列由列族和列限定符組成,它們由:(冒號)進行分隔,即一個完整的列名應該表述為列族名 :列限定符。

1.5 Cell

Cell是行,列族和列限定符的組合,並包含值和時間戳。你可以等價理解為關係型數據庫中由指定行和指定列確定的一個單元格,但不同的是HBase中的一個單元格是由多個版本的數據組成的,每個版本的數據用時間戳進行區分。

1.6 Timestamp(時間戳)

HBase 中通過row key和column確定的為一個存儲單元稱為Cell。每個Cell都保存着同一份數據的多個版本。版本通過時間戳來索引,時間戳的類型是 64位整型,時間戳可以由HBase在數據寫入時自動賦值,也可以由客戶顯式指定。每個Cell中,不同版本的數據按照時間戳倒序排列,即最新的數據排在最前面。

二、存儲結構

2.1 Regions

HBase Table中的所有行按照Row Key的字典序排列。HBase Tables 通過行鍵的範圍(row key range)被水平切分成多個Region, 一個Region包含了在start key 和 end key之間的所有行。

每個表一開始只有一個Region,隨着數據不斷增加,Region會不斷增大,當增大到一個閥值的時候,Region就會等分為兩個新的Region。當Table中的行不斷增多,就會有越來越多的Region。

Region是HBase中分佈式存儲和負載均衡的最小單元。這意味着不同的Region可以分佈在不同的Region Server上。但一個Region是不會拆分到多個Server上的。

2.2 Region Server

Region Server運行在HDFS的DataNode上。它具有以下組件:

  • WAL(Write Ahead Log,預寫日誌):用於存儲尚未進持久化存儲的數據記錄,以便在發生故障時進行恢復。
  • BlockCache:讀緩存。它將頻繁讀取的數據存儲在內存中,如果存儲不足,它將按照最近最少使用原則清除多餘的數據。
  • MemStore:寫緩存。它存儲尚未寫入磁盤的新數據,並會在數據寫入磁盤之前對其進行排序。每個Region上的每個列族都有一個MemStore。
  • HFile :將行數據按照Key\Values的形式存儲在文件系統上。

Region Server存取一個子表時,會創建一個Region對象,然後對錶的每個列族創建一個Store實例,每個Store會有 0 個或多個StoreFile與之對應,每個StoreFile則對應一個HFile,HFile 就是實際存儲在HDFS上的文件。

三、Hbase系統架構

3.1 系統架構

HBase系統遵循Master/Salve架構,由三種不同類型的組件組成:

Zookeeper

  1. 保證任何時候,集群中只有一個Master;
  2. 存貯所有Region的尋址入口;
  3. 實時監控Region Server的狀態,將Region Server的上線和下線信息實時通知給Master;
  4. 存儲HBase的Schema,包括有哪些Table,每個Table有哪些Column Family等信息。

Master

  1. 為Region Server分配Region ;
  2. 負責Region Server的負載均衡 ;
  3. 發現失效的Region Server並重新分配其上的Region;
  4. GFS上的垃圾文件回收;
  5. 處理Schema的更新請求。

Region Server

  1. Region Server負責維護Master分配給它的Region ,並處理髮送到Region上的IO請求;
  2. Region Server負責切分在運行過程中變得過大的Region。

3.2 組件間的協作

HBase使用ZooKeeper作為分佈式協調服務來維護集群中的服務器狀態。 Zookeeper負責維護可用服務列表,並提供服務故障通知等服務:

  • 每個Region Server都會在ZooKeeper上創建一個臨時節點,Master通過Zookeeper的Watcher機制對節點進行監控,從而可以發現新加入的Region Server或故障退出的Region Server;
  • 所有Masters會競爭性地在Zookeeper上創建同一個臨時節點,由於Zookeeper只能有一個同名節點,所以必然只有一個Master能夠創建成功,此時該Master就是主Master,主Master會定期向Zookeeper發送心跳。備用Masters則通過Watcher機制對主HMaster所在節點進行監聽;
  • 如果主Master未能定時發送心跳,則其持有的Zookeeper會話會過期,相應的臨時節點也會被刪除,這會觸發定義在該節點上的Watcher事件,使得備用的Master Servers得到通知。所有備用的Master Servers在接到通知后,會再次去競爭性地創建臨時節點,完成主Master的選舉。

四、數據的讀寫流程簡述

4.1 寫入數據的流程

  1. Client向Region Server提交寫請求;
  2. Region Server找到目標Region;
  3. Region檢查數據是否與Schema一致;
  4. 如果客戶端沒有指定版本,則獲取當前系統時間作為數據版本;
  5. 將更新寫入WAL Log;
  6. 將更新寫入Memstore;
  7. 判斷Memstore存儲是否已滿,如果存儲已滿則需要flush為Store Hfile文件。

更為詳細寫入流程可以參考:HBase - 數據寫入流程解析

4.2 讀取數據的流程

以下是客戶端首次讀寫HBase上數據的流程:

  1. 客戶端從Zookeeper獲取META表所在的Region Server;
  2. 客戶端訪問META表所在的Region Server,從META表中查詢到訪問行鍵所在的Region Server,之後客戶端將緩存這些信息以及META表的位置;
  3. 客戶端從行鍵所在的Region Server上獲取數據。

如果再次讀取,客戶端將從緩存中獲取行鍵所在的Region Server。這樣客戶端就不需要再次查詢META表,除非Region移動導致緩存失效,這樣的話,則將會重新查詢並更新緩存。

注:META表是HBase中一張特殊的表,它保存了所有Region的位置信息,META表自己的位置信息則存儲在ZooKeeper上。

更為詳細讀取數據流程參考:

HBase原理-數據讀取流程解析

HBase原理-遲到的‘數據讀取流程部分細節

參考資料

本篇文章內容主要參考自官方文檔和以下兩篇博客,圖片也主要引用自以下兩篇博客:

  • HBase Architectural Components
  • Hbase系統架構及數據結構

官方文檔:

  • Apache HBase ™ Reference Guide

更多大數據系列文章可以參見個人 GitHub 開源項目: 大數據入門指南

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

從實踐者的角度看軟件架構的歷史

無論什麼東西,套用宋丹丹的話,就是都有它的過去、現在和將(jiǎng)來。因此學習一樣東西,如果能多學一點它的歷史,會讓我們對其為何有如此現狀少一些糾結,同時才有可能對其未來趨勢有靠譜一點點的洞見。昨夜窗外雨聲稀疏,難以入眠,突然想到軟件架構的發展史是怎樣的,於是今晨起來網上逛一圈,邂逅到這篇論文《The History of Software Architecture – In the Eye of the Practitioner》,因此,這是一篇譯文。

小弟不才,沒有能力自己去梳理這麼龐大的論題,因此只能翻譯了。不過我並沒有翻譯這篇論文的全部內容,比如附錄就沒有翻譯。在翻譯的過程中,一度覺得這論文的英文及其拗口,跟我閱讀過的其它英文書比實在是難讀。開弓沒有回頭箭,我還是把要譯的部分譯完了,難免有詞不達意之處,還望海涵。

以下是譯文:

 

1. 目標和動機

那些權威論文把軟件架構當作獨立的學科,一些科技期刊也把架構視角、架構描述語言、架構進化作為他們研究和實踐的基石,從這些角度來看的話,軟件架構正好迎來了它的25周年慶。

隨着基於雲交付的普及,分佈式系統的各個部分需要動態集成,商業和社會数字化的曲線越來越陡,使得一個合理的設計決策包含了更大和更複雜的問題空間。軟件架構的重要性前所未有,開展重要項目的組織離不開相應架構實踐的支撐。然而,這25年來,軟件架構的實踐是如何進化的呢?未來又將面臨哪些挑戰?

對軟件架構研究和實踐現狀的總結,曾有過各種不同的嘗試,然而都缺少了從實踐者的角度來看待上面的兩個問題。

為了填補這個空缺,我們首先從5622篇科技論文中抽出10大主題,如圖3。然後我們根據這些主題設計一個在線問卷調查,並由擁有5到20年不等經驗的57位軟件架構實踐者填寫了這份問卷調查。

 

2. 實踐者對過去25年軟件架構及未來之路的看法

這篇論文中,我們的調查聚焦於,在過去25年裡,軟件架構方面最突出的話題有哪些,以及在當下和不久的將來,軟件架構方面的哪些話題會具有最深遠的影響力。

調查問卷包含的問題有:

a) 參與者的背景、經驗和其它一些統計信息;

b) 目前他們供職的機構,過去幾年接手的項目類型;

c) 過去25年在軟件架構方面的實踐;

d) 最有影響力而且是目前軟件架構趨勢的話題(包括近兩年新興的);

e) 未來工業界可能在軟件架構方向的實踐(未來5年);

基於參與者的回答,我們精編得到以下結果。

過去:圖1.PAST 總結了實踐者們認為的過去25年最有影響力的10個軟件架構話題。我們可以看到:

1.最有影響力的話題屬於“軟件開發流程”、“面向服務的架構(SOA)”、“架構風格”、“物聯網(IoT)”。這些總共佔了38%的比重。

2.SOA就像圖中展示的那樣了,其它的話題則包含更多特定的子話題:“軟件開發流程”包含了敏捷開發、持續交付集成、DevOps、領域驅動設計、需求角色、遺留(系統/代碼)、風險和質量管理、溝通技能。“架構風格”包含隨着時間而演變的各種不同風格,從C-S和分佈式架構,到生產線架構、MVC、多層架構等等。最後,“IoT”包括数字化、Web、互聯網、工業4.0和移動優先。

圖1.PAST 標紅的數字錶示它是否出現在圖3中(科學文獻中前10的話題)。同樣的,我們可以看到:

1.工業界前4個話題同樣出現在學術界的前10個話題中,但影響力有些差別:“軟件開發流程”在工業界排第1,但在學術界只排第7,“SOA”在工業界和學術界都排第2,還有很明顯的,“架構風格”在工業界的影響力被認為是大得多,排在第3,而在學術界排第6。

2.真正有大分歧的話題是“架構描述&語言”,這在學術機構排第1,而在實踐者眼中只排到第8。不過這並不奇怪,符號和語言常常作為學術界深愛的研究課題,但工業界真正採用的並不多。不過也好,這正可以作為一個讓研究者和實踐者進行更好描述和高效溝通的契機。

還有一些話題,被實踐者們提及,但是卻不入專業研究者的法眼。最突出的比如:

1.軟件質量(第5):很顯然,質量成為軟件架構的屬性是過去25年的一個重要成果。“質量”有時候又被稱為質量符合性、性能、可擴展性、可維護性等等。

2.雲計算(第6)和微服務(第7):它們被認為非常(每樣14票)的有影響力。作為SOA的衍生品,它們被認為是同屬一個話題—這樣就一共獲得42票,變成了過去25年最有影響力的話題。

現在:圖1.PRESENT 使用同樣的分析方法得到現今軟件架構最有影響力的話題。這個結果中,“架構風格”排在第10(緊挨着“SOA”和“架構設計決策”)。另一方面:

1.“SOA”被“雲”和“微服務”替代:我們認為這是正常的技術進化,作為一種普遍的架構風格,SOA曾被應用到到各個應用領域。

2.“軟件開發流程”仍然保持穩定(第1)。相較於對過去25年的回答,今天“一切都是敏捷”:敏捷之後呢,是DevOps、持續架構(continuous-architecting)。我們注意到,在敏捷開發中,架構扮演更重要角色的意識正在增強。

3.同樣的,“IoT”保持穩定(第4)。然而相比過去,它被認為是有更大的影響力,關注點也從移動應用轉變到基於IoT的架構。這也和Gartner關於2018年重大技術策略趨勢的預測相符,預測中提到的“智能物件(intelligent things)”,就是將AI與IoT融合。

4.明顯的,“軟件質量”(第6)和“安全性(第7)”的影響力在下降。這或許是架構師們都知道了如何應對這些問題,又或許他們覺得有更重要的話題要關注。

總的來說,現今最有影響力的話題總共佔據了70%的答案。其中,流程、面向服務(雲和微服務)、IoT三者共佔了62%。

而且,在現今的前10個話題中,有一些新名詞引起了我們的注意:

1.“大數據”(第5):稱為大數據,或者AI、機器學習、機器分析。

2.“第三方軟件集成”(第8):在過去25年的部分曾被提及,但排在第14,不過都分別得到了5票。不過從答案中,可以看到軟件架構正從封閉走向開放。

未來:圖 1.FUTURE,我們從調查反饋中看到比較高的不確定性。即使實踐者們認為前4個話題在未來5年仍保持主流,但它們佔據總票數的52%,比現今的影響力要小10%。

圖1.FUTURE 展示了:

1.“軟件開發流程”、“大數據”、“微服務”和“雲計算”會繼續扮演非常重要的角色,然而:

2.對於“軟件開發流程”,實踐者們更關注如何管理不斷增加的複雜性,可能是跨組織的,並將注意力放到了更高的自動化上。

3.對於“大數據”,提到了AI將扮演的角色和大數據在我們日常生活中進行的各種預測。

4.“微服務”將成熟,新的“雲”將關注點放在基於雲架構的風格/模式,以及如何通過軟件架構來實現XaaS商業模型。

5.“自適應系統”(第5),在過去的幾年裡火熱於學術界,被認為在工業界也變得越來越重要。

6.在新的話題當中,“區塊鏈”也位於前10(不過反應平平,可能出乎你的意料)。

7.其它冒出的新鮮話題、不在前10名單中的有機器人、数字化轉型、智能互聯、綠色軟件、倫理學。

 

3. 反思與收穫

除了之前呈現的結果以外,我們還要求實踐者們根據自身經歷反饋哪些架構話題在他們過去的25年裡產生過最重大的影響,以每5年為一個周期,如下圖:

圖2 展示了過去(比如client-server架構,從1992-2001年)佔據主流的話題如何被新話題(比如“架構設計決策”、“架構知識體系”,從2002-2011年)超越的,以及最新的一些話題(“信息物理系統cyber-physical systems”和IoT,從2012-2017年)是如何湧現的。這讓我們得到至少以下的認知:

認知1. 在軟件架構的歷史中,架構的概念從先前的一系列系統結構,變成了處於大型的、複雜的、不斷進化的環境中的軟件系統。然而在這樣的環境中,不只是關乎技術,還包括人員、社交、組織生態和整個社會。軟件架構的實踐者關注的面比研究者們要廣泛,軟件架構實踐者同時追隨其它學科,從AI、IoT、自適應增強,到能源和倫理學。

認知2. “軟件開發流程”贏了。無論是過去,現在還是未來,軟件架構始終關注如何更敏捷的進行開發,怎樣的人員技能可以有助於開發。房間里的大象(明顯存在的問題)可以用來形容軟件架構溝通和形式化之間存在的窘境:“架構模型”和“架構設計”常常用來彌補“軟件開發流程”,但卻從來無法達到預期—將架構代碼化,使得架構可重用並且可靠。

認知3. 對於軟件架構這個話題,不存在革命性的新東西,只有在舊的東西之上默默地演化。比如“軟件開發流程”演化成各種形式的敏捷開發,“架構風格”從“SOA”演化到“微服務”和“雲”,“信息物理系統”進化融合到“IoT”和“自適應系統”。總的來說,我們討論的是軟件架構的全局趨勢,通向更便捷性的,可管理更多複雜性。

認知4. 軟件架構的研究和實踐始終保持一致。當我們對比圖3中的前10個研究課題和圖2中工業界的主流話題,我們看到比如“客戶端-服務器”和“架構風格”在研究和實踐方面有非常類似的趨勢。“軟件架構設計”和“架構設計決策”雖然研究和實踐方面有不同的趨勢,但都保持着重要的地位。最明顯的就是“架構描述&語言”,在研究領域炙手可熱,而實踐中少得多。

最後,我們希望從歷史角度看到的軟件架構演化史能給讀者帶來進一步的思考和靈感。

 

文章最初發表於:從實踐者的角度看軟件架構的歷史

歡迎關注微信公眾號:

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

基於vue + axios + lrz.js 微信端圖片壓縮上傳

業務場景

微信端項目是基於Vux + Axios構建的,關於圖片上傳的業務場景有以下幾點需求:

1、單張圖片上傳(如個人頭像,實名認證等業務)
2、多張圖片上傳(如某類工單記錄)
3、上傳圖片時期望能按指定尺寸壓縮處理
4、上傳圖片可以從相冊中選擇或者直接拍照

遇到的坑

採用微信JSSDK上傳圖片

在之前開發的項目中(mui + jquery),有使用過微信JSSDK的接口上傳圖片,本想應該能快速遷移至此項目。事實證明編程沒有簡單的事:
1、按指定尺寸壓縮圖片
JSSDK提供的接口wx.chooseImage 是不能指定圖片壓縮尺寸的,只能在後端的接口通過localId獲取圖片時,再轉換成指定的尺寸。
2、微信JSSDK的接口權限驗證
只要是單頁面應用項目,微信JSSDK注入權限驗證都會有這個坑,而這個與路由模式(hash 或 history)也有關聯。有關此坑, 後續會再次寫文總結。參考解決方案[微信JSSDK] 解決SDK注入權限驗證 安卓正常,IOS出現config fail
經過權衡考慮網頁可能需要在微信以外的瀏覽器上也能上傳文件,顧後來放棄了採用微信JSSDK接口上傳圖片的方式。

android版微信,input onchange事件不觸發

這個坑,圈內有很多人踩過了。在PC端測試是正常的,發布之後,微信端上傳時能選擇文件,但之後沒有任何效果。日誌跟蹤,後台的api都未調用,由此判斷是input的onchange事件未被觸發。
解決方案, 更改input的 accept屬性:

<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />

將以上代碼更改為:

<input ref="file" type="file" accept="image/*" @change="selectImgs" />

如果不允許從相冊中選擇,只能拍照,增加capture=”camera”:

<input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs" />

(注:如果場景支持從相冊選擇或拍照,測試發現某些機型拍照后返回到了主頁。哈哈,也有可能是其他因素引起的問題,未做深究了)

使用Lrz.js壓縮圖片

目前手機拍照的圖片文件大小一般在3-4M,如果在上傳時不做壓縮處理會相當浪費流量並且佔用服務器的存儲空間(期望上傳原圖的另做討論)。如果能夠在前端壓縮處理,那肯定是最理想的方案。而lrz.js則提供了前端圖片文件的壓縮方案,並且可以指定尺寸壓縮。實測:3M左右的圖片文件,按寬度450px尺寸壓縮上傳后的文件大小在500kb左右,上傳時間2s以內。
其核心源碼,如下:

selectImgs () {
  let file = this.$refs.file.files[0]
  lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
    var xhr = new XMLHttpRequest()
    xhr.open('POST', 'http://xxx.com/upload')

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 304) {
        // 無論後端拋出何種錯誤,都會走這裏
        try {
          // 如果後端跑異常,則能解析成功, 否則解析不成功
          let resp = JSON.parse(xhr.responseText)
          console.log('response: ', resp)
        } catch (e) {
          this.imageUrl = xhr.responseText
        }
      }
    }

    // 添加參數
    rst.formData.append('folder', 'wxAvatar') // 保存的文件夾
    rst.formData.append('base64', rst.base64)
    // 觸发上傳
    xhr.send(rst.formData)

    return rst
  })
}

單個圖片上傳組件完整代碼,如下(注: icon圖標使用的是svg-icon組件):

<template>
  <div class="imgUploader">
    <section v-if="imageUrl"
             class="file-item ">
      <img :src="imageUrl"
           alt="">
      <span class="file-remove"
            @click="remove()">+</span>
    </section>
    <section v-else
             class="file-item">
      <div class="add">
        <svg-icon v-if="!text"
                  class="icon"
                  icon-class="plus" />
        <span v-if="text"
              class="text">{{text}}</span>
        <input type="file"
               accept="image/*"
               @change="selectImgs"
               ref="file">
      </div>
    </section>
  </div>
</template>

<script>
import lrz from 'lrz'
export default {
  props: {
    text: String,
    // 壓縮尺寸,默認寬度為450px
    size: {
      type: Number,
      default: 450
    }
  },
  data () {
    return {
      img: {
        name: '',
        src: ''
      },
      uploadUrl:  'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
      imageUrl: ''
    }
  },
  watch: {
    imageUrl (val, oldVal) {
      this.$emit('input', val)
    },
    value (val) {
      this.imageUrl = val
    }
  },
  mounted () {
    this.imageUrl = this.value
  },
  methods: {
    // 選擇圖片
    selectImgs () {
      let file = this.$refs.file.files[0]
      lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
        var xhr = new XMLHttpRequest()
        xhr.open('POST', this.uploadUrl)

        xhr.onload = () => {
          if (xhr.status === 200 || xhr.status === 304) {
            // 無論後端拋出何種錯誤,都會走這裏
            try {
              // 如果後端跑異常,則能解析成功, 否則解析不成功
              let resp = JSON.parse(xhr.responseText)
              console.log('response: ', resp)
            } catch (e) {
              this.imageUrl = xhr.responseText
            }
          }
        }

        // 添加參數
        rst.formData.append('folder', this.folder) // 保存的文件夾
        rst.formData.append('base64', rst.base64)
        // 觸发上傳
        xhr.send(rst.formData)

        return rst
      })
    },
    // 移除圖片
    remove () {
      this.imageUrl = ''
    }
  }
}
</script>

<style lang="less" scoped>
.imgUploader {
  margin-top: 0.5rem;
  .file-item {
    float: left;
    position: relative;
    width: 100px;
    text-align: center;
    left: 2rem;
    img {
      width: 100px;
      height: 100px;
      border: 1px solid #ececec;
    }
    .file-remove {
      position: absolute;
      right: 0px;
      top: 4px;
      width: 14px;
      height: 14px;
      color: white;
      cursor: pointer;
      line-height: 12px;
      border-radius: 100%;
      transform: rotate(45deg);
      background: rgba(0, 0, 0, 0.5);
    }

    &:hover .file-remove {
      display: inline;
    }
    .file-name {
      margin: 0;
      height: 40px;
      word-break: break-all;
      font-size: 14px;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
  }
  .add {
    width: 100px;
    height: 100px;
    float: left;
    text-align: center;
    line-height: 100px;
    font-size: 30px;
    cursor: pointer;
    border: 1px dashed #40c2da;
    color: #40c2da;
    position: relative;
    background: #ffffff;
    .icon {
      font-size: 1.4rem;
      color: #7dd2d9;
      vertical-align: -0.25rem;
    }
    .text {
      font-size: 1.2rem;
      color: #7dd2d9;
      vertical-align: 0.25rem;
    }
  }
}
input[type="file"] {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border: 1px solid #000;
  opacity: 0;
}
</style>

後端圖片存儲處理

後端api對圖片的處理,是必不可少的環節,需要將前端提交過來的base64字符串轉換成圖片格式,並存放至指定的文件夾,接口返回圖片的Url路徑。各項目後端對圖片的處理邏輯都不一致,以下方案僅供參考(我們使用asp.net MVC 構建了獨立的文件存儲站點)。
其核心源碼,如下:

/// <summary>
/// 圖片文件base64上傳
/// </summary>
/// <param name="folder">對應文件夾位置</param>
/// <param name="base64">圖片文件base64字符串</param>
/// <returns></returns>
public ActionResult Base64FileUpload(string folder, string base64)
{
    var context = System.Web.HttpContext.Current;
    context.Response.ClearContent();
    // 因為前端調用時,需要做跨域處理
    context.Response.AddHeader("Access-Control-Allow-Origin", "*");
    context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
    context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
    context.Response.AddHeader("Access-Control-Max-Age", "30");
    if (context.Request.HttpMethod.Equals("OPTIONS"))
    {
        return Content("");
    }

    var resultStr = base64.Substring(base64.IndexOf(",") + 1);//需要去掉頭部信息,這很重要
    byte[] bytes = Convert.FromBase64String(resultStr);
    var fileName = Guid.NewGuid().ToString() + ".png";
    if (folder.IsEmpty()) folder = "folder";
    //本地上傳
    string root = string.Format("/Resource/{0}/", folder);
    string virtualPath = root + fileName;
    string path = Server.MapPath("~" + virtualPath);
    //創建文件夾
    if (!Directory.Exists(Path.GetDirectoryName(path)))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
    }
    System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);//轉換成無法調整大小的MemoryStream對象
    System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
    bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);//保存到服務器路徑
    ms.Close();//關閉當前流,並釋放所有與之關聯的資源
    return Content(Net.Url + virtualPath); //返迴文件路徑
}

結語

由於項目實際情況,上述的方案中還存在諸多未完善的點:
1、多張圖片上傳,還是採用的與單張圖片相同的接口處理, 更為完善的方案是,前端的多圖上傳組件只綁定一個關聯Id,即可通過實現上傳和將圖片列表查詢展示(注:該功能在微信端未實現)。
2、後端圖片上傳的接口,未做嚴格的安全校驗,更為完善的方案是,每個上傳的場景,都應該限制文件類型,限制文件大小,以及文件數據來源校驗(注: 如軟件需要按二級等保標準測評,則後端接口會檢測通不過)。
3、上傳組件,未显示上傳進度,體驗性稍差。
正如前文所述,出於項目實際情況考慮,只是簡單實現圖片壓縮上傳功能,如要支持更多的場景,還得細細雕琢。

參考

1、移動端H5實現圖片上傳
2、安卓版微信 input onchange事件不生效

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

來阿里有段時間了,談談這幾個月最真實的感受

作者:黃小斜

文章來源:微信公眾號【程序員江湖】

 

閱讀本文大概需要 4 分鐘。

 

​

 

有段時間沒寫過原創了,想了各種理由,發現其實理由就一個,沒時間。

 

我來阿里,已經幾個月了。這段時間,最大的感受就是累。我是在今年的四月份加入阿里的。

 

當初我沒有參加阿里巴巴的實習,而是選擇了直接進行校園招聘,這也是因為當時我對實習的部門不感興趣,於是在校招的時候我就選擇了自己感興趣的部門,也就是現在我所在的螞蟻金服。

 

之前就聽說過阿里的工作強度,可能會比較大,但是,當我在真正來到這家公司的時候,才發現996,並不是虛言,這倒不是說制度上規定的 996 ,而是因為,你手上的工作,是很難做完的,所以導致你不得不用加班的方式來完成,996 最終的目的只有一個,就是你要把手上的工作做完,對其他人有一個交代。

 

剛來一個月的時候只其實我是非常不適應的,畢竟從學校到公司,整個變化是顛覆性的,雖然之前有過實習經歷,但是是比較划水的,和正式工作差別還是比較大的,第一個月大抵的狀態就是,每天九點多上班,晚上九點及以後下班,這是生活規律上的不適應。

 

除此之外,在工作上也會有一些不適應,雖然前期給我的活兒都比較的零散,只要求這裏修修補補,那裡加點東西。這倒是其次。

 

​

 

主要的原因還是因為我對業務線的研發流程不太了解,以及對大家在做的事情不太了解,這會導致我在工作上遇到很多困難。因為我會聽不懂大家在說什麼。所以第一個月,更多的是思考。

 

如何去提高自己的效率,如何去融入這個公司,如何去熟悉部門的業務和技術戰,自己感覺是一個比較外向的人,所以一旦有問題就會諮詢,我的同事,我的師兄。

 

阿里的師兄是一種文化。就像武俠小說里的同門師兄弟一樣,師兄會帶領着你去做那些工作,並且讓你去熟悉整個部門的一些事情。我的師兄是一個寡言少語的人,但是技術能力非常強,也很有責任心。他總是對總是對工作要求特別高,不放過任何一個問題,不漏掉任何一行問題代碼。

 

在第一個月的時間里。我學習了很多阿里巴巴的一些中間件技術。其實這些技術。在學校複習的時候,多少也聽說過一些,看過一些文章。只不過外面用的都是開源的東西,而在阿里內部,更多的是自研的產品,比如說,消息中間件,分佈式事務,數據庫中間件,等等等等。這些東西,在平時的學習和研發中,是經常會用到的,如果你在學校或者是在其他小公司,可能根本沒有機會接觸到這些東西。

 

​

 

作為一個Java工程師,在這家公司的職責可不止是寫代碼,你需要熟悉整個研發流程,從系統設計,代碼開發,測試聯調,發布上線,問題排查都是你的職責,其實這很鍛煉人,這也是為什麼加班會這麼多的原因,寫代碼只佔你日常工作的一小部分時間,你需要花更多時間在解決各類問題上。

 

阿里對校招生有一個培訓,不同部門可能不太一樣,螞蟻的培訓長達一個月,這個月應該也是我覺得成長最快,過得最快樂的一個月,其實無非就是上課,拓展和各類活動,這段時間認識了很多人,其中也有很多大牛,大家一起上課,一起做項目,同甘共苦度過一個月,這樣的經歷也是非常值得紀念的。

 

培訓結束后,我也開始承擔更多的工作,當自己逐漸習慣這種節奏之後,才感覺自己逐漸在融入這家公司,每次搞懂一個業務問題或者技術問題都會覺得自己在成長,當肩頭上承擔更多責任的時候,同時也承擔了更多壓力,如果不能調整好自己的心態,我想在這家公司是很難待久的。

 

可能你在其他互聯網公司也會感受到相似的壓力,但這就是大部分互聯網公司的現狀,追求效率,追求極致,我們身在其中,就必須適應環境,尊重遊戲規則,馬上又是新一年的校招季,去年這個時候,這個公眾號才剛剛誕生,轉眼一年時間,多的是更多思考,希望公眾號的文章對你們有會有更多幫助。

 

下一篇文章應該也不會讓你們等太久。

 

​

 

文能碼字,武能coding,是我黃小斜,不是黃老邪噢。

 

 

推薦閱讀:

 

焦慮的 BAT、不安的編程語言,揭秘程序員技術圈生存現狀!

 

 

 

為什麼有些大公司技術弱爆了?

 

 

 

  

 

​你點的每個好看,我都認真當成了喜歡

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

自定義Starter:

  SpringBoot應用篇(一):自定義starter

一、前言

  上一篇文章,通過分析refresh()方法中的invokeBeanFactoryPostProcessors()方法,分析了IoC容器的初始化過程,這一節從代碼上如下所示,接上一節ConfigurationClassParser類中的parse()方法,接着分析SpringBoot的自動裝配原理。

 1 // ConfigurationClassParser類
 2 public void parse(Set<BeanDefinitionHolder> configCandidates) {
 3     this.deferredImportSelectors = new LinkedList<>();
 4     for (BeanDefinitionHolder holder : configCandidates) {
 5         BeanDefinition bd = holder.getBeanDefinition();
 6         try {
 7             // 如果是SpringBoot項目進來的,bd其實就是前面主類封裝成的 AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的實現類)
 8             if (bd instanceof AnnotatedBeanDefinition) {
 9                 parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
10             } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
11                 parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
12             } else {
13                 parse(bd.getBeanClassName(), holder.getBeanName());
14             }
15         } catch (BeanDefinitionStoreException ex) {
16             throw ex;
17         } catch (Throwable ex) {
18             throw new BeanDefinitionStoreException(
19                     "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
20         }
21     }
22     // 加載默認的配置---》(對springboot項目來說這裏就是自動裝配的入口了)
23     processDeferredImportSelectors();
24 }

 

二、SpringBoot自動裝配原理。

2.1、@SpringBootApplication註解

  對這個註解詳細大家一定非常熟悉了。再來好好看看這個註解。

 1 @Target(ElementType.TYPE)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 @Inherited
 5 @SpringBootConfiguration
 6 @EnableAutoConfiguration
 7 @ComponentScan(excludeFilters = {
 8         @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
 9         @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
10 public @interface SpringBootApplication {
11     ...
12 }

  接着看@EnableAutoConfiguration

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @AutoConfigurationPackage
6 @Import(AutoConfigurationImportSelector.class)
7 public @interface EnableAutoConfiguration {
8     ...
9 }

  OK,看到@Import(AutoConfigurationImportSelector.class)導入了一個重要的類AutoConfigurationImportSelector。

2.2、AutoConfigurationImportSelector

 1 // AutoConfigurationImportSelector類
 2 //自動裝配
 3 @Override
 4 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 5     if (!isEnabled(annotationMetadata)) {
 6         return NO_IMPORTS;
 7     }
 8     AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 9             .loadMetadata(this.beanClassLoader);
10     AnnotationAttributes attributes = getAttributes(annotationMetadata);
11     //獲取所有的自動配置類(META-INF/spring.factories中配置的key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的類)
12     List<String> configurations = getCandidateConfigurations(annotationMetadata,
13             attributes);
14     configurations = removeDuplicates(configurations);
15     //需要排除的自動裝配類(springboot的主類上 @SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})指定的排除的自動裝配類)
16     Set<String> exclusions = getExclusions(annotationMetadata, attributes);
17     checkExcludedClasses(configurations, exclusions);
18     //將需要排除的類從 configurations remove掉
19     configurations.removeAll(exclusions);
20     configurations = filter(configurations, autoConfigurationMetadata);
21     fireAutoConfigurationImportEvents(configurations, exclusions);
22     return StringUtils.toStringArray(configurations);
23 }

  至於怎麼從章節一中提到的ConfigurationClassParser類中的parse()===>processDeferredImportSelectors()==>AutoConfigurationImportSelector#selectImports(),篇幅有限不做過多介紹。

   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 

  我們來看一下getCandidateConfigurations()方法是怎麼拿到這些自動配置類的。

// AutoConfigurationImportSelector類
1
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, 2 AnnotationAttributes attributes) { 3 List<String> configurations = SpringFactoriesLoader.loadFactoryNames( 4 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); 5 Assert.notEmpty(configurations, 6 "No auto configuration classes found in META-INF/spring.factories. If you " 7 + "are using a custom packaging, make sure that file is correct."); 8 return configurations; 9 }

  是不是又看到一個十分熟悉的方法loadFactoryNames(),沒錯,其實我們在分析SpringBoot啟動流程的第一篇文章的時候,就已經分析了,SpringBoot是如何從META-INF/spring.factories中加載指定key的value的。ok,我們在這裏再次回顧一遍。

  看看loadFactoryNames()方法

// SpringFactoriesLoader類
1
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { 2 String factoryClassName = factoryClass.getName(); 3 return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); 4 }

  debug,看看要從META-INF/spring.factories中加載的類的key,如下圖所示:org.springframework.boot.autoconfigure.EnableAutoConfiguration

  回到selectImports()方法,debug,跳過List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);看一下configurations

  竟然有110個,那這些類都在哪裡呢?看spring-boot-autoconfigure(當然在SpringBoot的工程中,也不止這一個依賴包中存在該配置文件)工程下的META-INF/spring.factories,我們能看到org.springframework.boot.autoconfigure.EnableAutoConfiguration定義了一大堆。

  其中還有一個com.demo.starter.config.DemoConfig是我自定義的starter。如下所示,我在測試工程中添加了自定義starter的依賴,所以SpringBoot就能掃描到。

1 <dependency>
2     <groupId>com.demo</groupId>
3     <artifactId>demo-spring-boot-starter</artifactId>
4     <version>0.0.1-RELEASE</version>
5 </dependency>

   繼續看Set<String> exclusions = getExclusions(annotationMetadata, attributes);方法,該方法是排除主類上@SpringBootApplication註解上排除的自動裝配的類。比如我們在該註解上排除我們自定義starter的自動裝配的類,@SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})(當然也可以用excludeName進行排除),那麼在後面的configurations.removeAll(exclusions);方法中將會刪除我們的com.demo.starter.config.DemoConfig.class。

  configurations = filter(configurations, autoConfigurationMetadata);該行代碼將會過濾掉不需要裝配的類。過濾的邏輯有很多,比如我們常用的@ConditionXXX註解。如下所示:

 1 @ConditionalOnBean:容器中有指定的Bean 
 2 @ConditionalOnClass:當類路徑下有指定的類
 3 @ConditionalOnExpression:基於SpEL表達式作為判斷條件  
 4 @ConditionalOnJava:基於JVM版本作為判斷條件  
 5 @ConditionalOnJndi:在JNDI存在的條件下查找指定的位置  
 6 @ConditionalOnMissingBean:當容器中沒有指定Bean的情況下  
 7 @ConditionalOnMissingClass:當類路徑下沒有指定的類
 8 @ConditionalOnNotWebApplication:當前項目不是Web項目
 9 @ConditionalOnProperty:配置文件中指定的屬性是否有指定的值  
10 @ConditionalOnResource:類路徑下是否有指定的資源  
11 @ConditionalOnSingleCandidate:當指定Bean在容器中只有一個,或者雖然有多個但是指定首選Bean
12 @ConditionalOnWebApplication:當前項目是Web項目的條件下

   至於如何將這些類解析成BeanDefinition並註冊進beanDefinition中的,和上一節講的過程是一樣的,不再贅述了。

  debug,跳過refresh()方法中的invokeBeanFactoryPostProcessors(beanFactory);方法。如下圖所示,最終在beanFactory的BeanDefinitionMap中找到了自定義starter中的自動裝配的類。

 

  綜合本文和上一篇博文我們詳細的梳理了IoC容器的初始化過程,到此IoC容器的初始化過程就結束了。

 

  

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Prometheus 入門

簡介

Prometheus 是一套開源的系統監控報警框架。它啟發於 Google 的 borgmon 監控系統,由工作在 SoundCloud 的 google 前員工在 2012 年創建,作為社區開源項目進行開發,並於 2015 年正式發布。

特點

作為新一代的監控框架,Prometheus 具有以下特點:

  • 強大的多維度數據模型:
  1. 時間序列數據通過 metric 名和鍵值對來區分。
  2. 所有的 metrics 都可以設置任意的多維標籤。
  3. 數據模型更隨意,不需要刻意設置為以點分隔的字符串。
  4. 可以對數據模型進行聚合,切割和切片操作。
  5. 支持雙精度浮點類型,標籤可以設為全 unicode。
  • 靈活而強大的查詢語句(PromQL):在同一個查詢語句,可以對多個 metrics 進行乘法、加法、連接、取分數位等操作。
  • 易於管理: Prometheus server 是一個單獨的二進制文件,可直接在本地工作,不依賴於分佈式存儲。
  • 高效:平均每個採樣點僅占 3.5 bytes,且一個 Prometheus server 可以處理數百萬的 metrics。
    使用 pull 模式採集時間序列數據,這樣不僅有利於本機測試而且可以避免有問題的服務器推送壞的 metrics。
  • 可以採用 push gateway 的方式把時間序列數據推送至 Prometheus server 端。
  • 可以通過服務發現或者靜態配置去獲取監控的 targets。
  • 有多種可視化圖形界面。
  • 易於伸縮。

組成及架構

Prometheus 生態圈中包含了多個組件,其中許多組件是可選的:

  • Prometheus Server: 用於收集和存儲時間序列數據。
  • Client Library: 客戶端庫,為需要監控的服務生成相應的 metrics 並暴露給 Prometheus server。當 Prometheus server 來 pull 時,直接返回實時狀態的 metrics。
  • Push Gateway: 主要用於短期的 jobs。由於這類 jobs 存在時間較短,可能在 Prometheus 來 pull 之前就消失了。為此,這次 jobs 可以直接向 Prometheus server 端推送它們的 metrics。這種方式主要用於服務層面的 metrics,對於機器層面的 metrices,需要使用 node exporter。
  • Exporters: 用於暴露已有的第三方服務的 metrics 給 Prometheus。
  • Alertmanager: 從 Prometheus server 端接收到 alerts 后,會進行去除重複數據,分組,並路由到對收的接受方式,發出報警。常見的接收方式有:电子郵件,pagerduty,OpsGenie, webhook 等。
  • 一些其他的工具。

下圖為 Prometheus 官方文檔中的架構圖:

從上圖可以看出,Prometheus 的主要模塊包括:Prometheus server, exporters, Pushgateway, PromQL, Alertmanager 以及圖形界面。

其大概的工作流程是:

  1. Prometheus server 定期從配置好的 jobs 或者 exporters 中拉 metrics,或者接收來自Pushgateway 發過來的 metrics,或者從其他的 Prometheus server 中拉 metrics。
  2. Prometheus server 在本地存儲收集到的 metrics,並運行已定義好的 alert.rules,記錄新的時間序列或者向 Alertmanager 推送警報。
  3. Alertmanager 根據配置文件,對接收到的警報進行處理,發出告警。
    在圖形界面中,可視化採集數據。

相關概念

下面將對 Prometheus 中的數據模型(時間序列),metric 類型,instance 和 jobs等概念進行介紹。

數據模型

Prometheus 中存儲的數據為時間序列,是由 metric 的名字和一系列的標籤(鍵值對)唯一標識的,不同的標籤則代表不同的時間序列。

  • metric 名字:該名字應該具有語義,一般用於表示 metric 的功能,例如:http_requests_ total, 表示 http 請求的總數。其中,metric 名字由 ASCII 字符,数字,下劃線,以及冒號組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 標籤:使同一個時間序列有了不同維度的識別。例如 http_requests_total{method=”Get”} 表示所有 http 請求中的 Get 請求。當 method=”post” 時,則為新的一個 metric。標籤中的鍵由 ASCII 字符,数字,以及下劃線組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 樣本:實際的時間序列,每個序列包括一個 float64 的值和一個毫秒級的時間戳。
  • 格式: { =, …},例如:http_requests_total{method=”POST”,endpoint=”/api/tracks”}。

Metrics種類

Prometheus客戶端庫提供了四種核心Metrics類型。

Counter(計數器)

  • 說明:Counter是一個累積度量,它表示一個單調遞增的 Metrics,其值只能在重啟時遞增或重置為零
  • 場景:可以使用Counter來表示http的請求數、已完成的任務數或錯誤數、下單數。

Gauge(測量儀)

  • 說明:當前值的一次快照(snapshot)測量,可增可減。
  • 場景:磁盤使用率,當前同時在線用戶數。

Histogram(直方圖)

  • 說明:通過區間統計樣本分佈。
  • 場景:請求延遲時間的統計。例如統計 0~200ms、200ms~400ms、400ms~800ms 區間的請求數有多。

Summary(匯總)

  • 說明:根據樣本統計出百分位。
  • 場景:請求延遲時間的統計。例如統計 95%的請求延遲 < xxx ms ,99%的請求延遲 < xxx ms

instance 和 jobs

在Prometheus術語中,你可以scrape(刮擦)的端點稱為 實例,通常對應於單個進程。一組同種類型的 instances(主要用於保證可擴展性和可靠性),例如:具有四個複製instances(實例)的API服務器job作業:

  • job: api-server
    • instance 1: 1.2.3.4:5670
    • instance 2: 1.2.3.4:5671
    • instance 3: 5.6.7.8:5670
    • instance 4: 5.6.7.8:5671

當Prometheus scrape(刮擦)目標時,它會自動在scrape的時間序列上附加一些標籤,用來識別scrape的目標。

  • job:目標所屬的已配置job名稱。
  • instance: : 已刮擦的目標URL 的一部分。

對於每次實例 scrape(刮取,Prometheus都會在以下時間序列中存儲樣本:

  • up{job=”<job-name>”, instance=”<instance-id>”}:1如果實例是健康的,即可達,或者0刮擦失敗。
  • scrape_duration_seconds{job=”<job-name>”, instance=”<instance-id>”}:刮擦持續時間。
  • scrape_samples_post_metric_relabeling{job=”<job-name>”, instance=”<instance-id>”}:應用度量標準重新標記后剩餘的樣本數。
  • scrape_samples_scraped{job=”<job-name>”, instance=”<instance-id>”}:目標暴露的樣本數。
  • scrape_series_added{job=”<job-name>”, instance=”<instance-id>”}:該刮擦中新系列的大致數量。v2.10中的新功能。

up時間序列對於實例可用性監視非常有用。

安裝和配置

安裝

你可以在官網 https://prometheus.io/download/ 下載 安裝包,解壓后使用。為了方便,我使用docker 鏡像的方式 運行Prometheus。

docker run --name prometheus -d -p 9090:9090 prom/prometheus

瀏覽器輸入http://localhost:9090 ,訪問 Prometheus 的 Web UI:

點擊菜單欄 “Status” 下的 Targets ,界面如下:

可以看大Prometheus 自身 metrics 處於UP狀態 ,說明 安裝成功。

配置

Prometheus 的配置文件 prometheus.yml 內容如下:

# 全局設置,可以被覆蓋
global:
  scrape_interval:     15s
  evaluation_interval: 15s
  
rule_files:
  # - "first.rules"
  # - "second.rules"

scrape_configs:
  - job_name: prometheus
    static_configs:
    - targets: ['localhost:9090']

該global塊控制 Prometheus 的全局配置。我們有兩種選擇。第一個,scrape_interval控制Prometheus 刮擦目標的頻率。你可以為單個目標覆蓋此值。在這種情況下,全局設置是每15秒刮一次。該evaluation_interval選項控制普羅米修斯評估規則的頻率。Prometheus 使用規則創建新的時間序列並生成警報。

該rule_files塊指定我們希望 Prometheus 加載的任何規則的位置。現在我們沒有規則。

最後一個塊scrape_configs控制 Prometheus 監視的資源。由於 Prometheus 還將自己的數據公開為HTTP端點,因此它可以抓取並監控自身的健康狀況。在默認配置中有一個名為 prometheus 的job,它抓取 prometheus 服務器 公開的時間序列數據。該作業包含一個靜態配置的目標,即端口9090上的本地主機。返回的時間序列數據將詳細說明Prometheus服務器的狀態和性能。

實驗

Prometheus HTTP 度量模擬器

為了演示 Prometheus 的簡單使用,這裏運行一個 Prometheus HTTP 度量模擬器。模擬一個簡單的HTTP微服務,生成Prometheus Metrics,通過 docker 運行。

docker run -p 8080:8080 pierrevincent/prom-http-simulator:0.1

它在/metrics端點下公開以下Prometheus指標:

  • http_requests_total:請求計數器,標籤endpoint和status
  • http_request_duration_milliseconds:請求延遲直方圖

可以開啟流量高峰模式,更改流量高峰模式可以通過以下方式完成:

# ON
curl -X POST http://127.0.0.1:8080/spike/on

# OFF
curl -X POST http://127.0.0.1:8080/spike/off

# RANDOM
curl -X POST http://127.0.0.1:8080/spike/random

錯誤率默認為1%。它可以更改為0到100之間的数字:

# 例如將錯誤率設置為50%
curl -H 'Content-Type: application/json' -X PUT -d '{"error_rate": 50}' http://127.0.0.1:8080/error_rate

修改Prometheus配置

需要將 HTTP 度量模擬器 的 metrics端點 配置到 Prometheus的配置文件 prometheus.yml 中。

創建一個 prometheus.yml 文件 內容如下:

global:
  scrape_interval: 5s
  evaluation_interval: 5s
  scrape_timeout: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']
  - job_name: 'http-simulator'
    metrics_path: /metrics
    static_configs:
    - targets: ['172.16.1.232:8080']

通過docker up 命令替換 容器中的配置文件:

docker cp prometheus.yml prometheus:/etc/prometheus/

重啟容器:

docker restart prometheus

訪問 http://localhost:9090/targets ,發現已經出現了 target “http-simulator” ,並且為UP狀態。

查詢

請求率(Request Rate)查詢

查詢http請求數

http_requests_total{job="http-simulator"}

查詢成功login請求數

http_requests_total{job="http-simulator", status="200", endpoint="/login"}

查詢成功請求數,以endpoint區分

http_requests_total{job="http-simulator", status="200"}

查詢總成功請求數

sum(http_requests_total{job="http-simulator", status="200"})

查詢成功請求率,以endpoint區分

rate(http_requests_total{job="http-simulator", status="200"}[5m])

查詢總成功請求率

sum(rate(http_requests_total{job="http-simulator", status="200"}[5m]))

延遲分佈(Latency distribution)查詢

查詢http-simulator延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator"}

查詢成功login延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}

不超過200ms延遲的成功login請求佔比

sum(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login", le="200"}) / sum(http_request_duration_milliseconds_count{job="http-simulator", status="200", endpoint="/login"})

成功login請求延遲的99百分位

histogram_quantile(0.99, rate(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}[5m]))

上面給出的這些查詢表達式,在 prometheus 的 查詢界面上自行測試下 ,這裏就不一一測試了,

總結

本篇對 Prometheus 的組成,架構和基本概念進行了介紹,並實例演示了 Prometheus 的查詢表達式的應用。本篇是 Prometheus 系列的第一篇, 後續還會有Prometheus與其他圖形界面的集成,與 springboot 應用的集成等 。

參考

https://prometheus.io/docs/introduction/overview/
https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-prometheus-getting-started-and-practice/index.html

歡迎掃碼或微信搜索公眾號《程序員果果》關注我,關注有驚喜~

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Spring 中的Null-Safety

之前一直在某些代碼中看到過使用@Nullable 標註過的註釋,當時也沒有在意到底是什麼意思,所以這篇文章來談談Spring中關於Null的那些事。

在Java中不允許讓你使用類型表示其null的安全性,但Spring Framework 現在在org.sprinngframework.lang包提供以下註釋,以便聲明API和字段的可空性:

  • @Nullable: 用於指定參數、返回值或者字段可以作為null的註釋。
  • @NonNull: 與上述註釋相反,表明指定參數、返回值或者字段不允許為null。(不需要@NonNullApi和@NonNullFields適用的參數/返回值和字段)
  • @NonNullApi: 包級別的註釋聲明非null作為參數和返回值。
  • @NonNullFields:包級別的註釋聲明字段默認非空

Spring Framework 本身利用了上面這幾個註釋,但它們也可以運用在任何基於Spring的Java 項目中,以聲明空安全api 和 空安全字段。尚未支持泛型和數組元素的可空性,但應也即將發布在後來的版本。Spring Null-Safety出現在Spring5中,讓我們更方便的編寫空安全的代碼,這叫做null-safety,null-safety不是讓我們逃脫不安全的代碼,而是在編譯時產生警告。 此類警告可以在運行時防止災難性空指針異常(NPE)。

@NonNull

@NonNull註釋是null-safety的所有註釋中最重要的一個,我們可以使用此註釋在期望對象引用的任何地方聲明非空約束:字段、方法參數或者方法返回值。

先來看一個例子

public class Student {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

上述代碼對name的校驗是有效的,但是存在一個缺陷,如果name被設置為null的話,那麼當我們使用name的時候,就會以NullPointerException來結尾。

使用@NonNull

Spring 的null-safety特性能夠允許idea或者eclipse報告這個潛在的威脅,例如,如果我們用IDEA對屬性加上@NonNull會出現如下的效果。

奇怪,並沒有什麼變化啊,沒看見有潛在的安全提示啊,那是因為你沒有在idea進行設置

設置安全檢查

如果你也沒有提示的話,可以通過如下的方式設置安全檢查

如果還不好使的話,那就在右側 configuration annotations 添加一下 @NonNull和 @Nullable 所在的jar包,如下:

添加上,打上 ✅ 即可看到如下效果。

現在fullName 已經被@NonNull 註釋添加編譯器檢查null值的功能了!

如果你不相信的話,可以把@NonNull 註釋去掉,你的鼠標再放在fullName 上,已經沒有這句提示了。

@NonNullFields

@NonNull 註解能夠幫助你確保null-safety。然而,如果此註釋直接裝飾所有的字段的話,就會污染整個代碼庫。

Spring提供了另外一個不允許為null的註解 — @NonNullFields。這個註解適合用在包級別上,通知我們的開發工具註釋包中所有的字段,默認的,不允許為null

新建一個Parent類,並在該類所屬包下創建一個名為package-info.java的類,創建的不是Java類,而是創建的file,名為package-info.java,如下

package-info.java

@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullFields;

新建一個Parent.java 類

public class Parent {

    private String son;
    private String age;
    private String name;

    public void setSon(String son) {
        if(son != null && son.isEmpty()){
            son = null;
        }
        this.son = son;
    }

    public void setAge(String age) {
        if(age != null && age.isEmpty()){
            age = null;
        }
        this.age = age;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

package-info.java 中的@NonNullFields能夠對Parent類中所有的屬性起作用,把鼠標放在任意一個屬性上,會出現編譯期檢查的提示

@Nullable

@NonNullFields註釋通常比@NonNull更好,因為它有助於減少樣板。 但是,有時我們想要從包級別指定的非null約束中免除某些字段,這時候就會使用到@Nullable註解

改造一下Person.java,Person.java 與pack-info.java 處於同一包下

public class Person {

    @NonNull
    private String fullName;

    @Nullable
    private String nickName;

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        if(nickName != null && nickName.isEmpty()){
            nickName = null;
        }
        this.nickName = nickName;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        if(fullName != null && fullName.isEmpty()){
            fullName = null;
        }
        this.fullName = fullName;
    }
}

在這種情況下,我們使用@Nullable註釋來覆蓋字段上@NonNullFields的語義。

@NonNullApi

@NonNullFields註釋僅適用於其名稱所示的字段。 如果我們想對方法的參數和返回值產生相同的影響,我們需要@NonNullApi。

添加 @NonNullApi和 @NonNullFields 在 configure annotations 中,並選用NonNullApi

與@NonNullFields一樣,我們需要在package-info.java 中定義@NonNullApi

package-info.java

@NonNullApi
@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

加上如下註釋后的效果如下: 可以在返回值的時候接受到編譯期的提示。

後記:

看完文章,你至少應該了解

  • 四個註解 @NonNull, @Nullable, @NonNullFields, @NonNullApi 四個註解各自的作用範圍
  • 如何設置編譯期的Null-safety檢查

歡迎關注

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

簡潔方便的集合處理——Java 8 stream流

背景

java 8已經發行好幾年了,前段時間java 12也已經問世,但平時的工作中,很多項目的環境還停留在java1.7中。而且java8的很多新特性都是革命性的,比如各種集合的優化、lambda表達式等,所以我們還是要去了解java8的魅力。

今天我們來學習java8的Stream,並不需要理論基礎,直接可以上手去用。

我接觸stream的原因,是我要搞一個用戶收入消費的數據分析。起初的統計篩選分組都是打算用sql語言直接從mysql里得到結果來展現的。但在操作中我們發現這樣頻繁地訪問數據庫,性能會受到很大的影響,分析速度會很慢。所以我們希望能通過訪問一次數據庫就拿到所有數據,然後放到內存中去進行數據分析統計過濾。

接着,我看了stream的API,發現這就是我想要的。

一、Stream理解

在java中我們稱Stream為『流』,我們經常會用流去對集合進行一些流水線的操作。stream就像工廠一樣,只需要把集合、命令還有一些參數灌輸到流水線中去,就可以加工成得出想要的結果。這樣的流水線能大大簡潔代碼,減少操作。

二、Stream流程

原集合 —> 流  —> 各種操作(過濾、分組、統計) —> 終端操作

 

Stream流的操作流程一般都是這樣的,先將集合轉為流,然後經過各種操作,比如過濾、篩選、分組、計算。最後的終端操作,就是轉化成我們想要的數據,這個數據的形式一般還是集合,有時也會按照需求輸出count計數。下文會一一舉例。

三、API功能舉例

首先,定義一個用戶對象,包含姓名、年齡、性別和籍貫四個成員變量:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Log4j
@Builder
public class User {
    //姓名
    private String name;
    //年齡
    private Integer age;
    //性別
    private Integer sex;
    //所在省市
    private String address;
}

 

這裏用lombok簡化了實體類的代碼。

然後創建需要的集合數據,也就是源數據:

//1.構建我們的list
List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤",20,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

3.1 過濾

1)創建流 stream() / parallelStream()

  • stream() : 串行流
  • parallelStream(): 并行流

2)filter 過濾(T-> boolean)

比如要過濾年齡在40歲以上的用戶,就可以這樣寫:

List<User> filterList = list.stream().filter(user -> user.getAge() >= 40)
        .collect(toList());

 

filter裏面,->箭頭後面跟着的是一個boolean值,可以寫任何的過濾條件,就相當於sql中where後面的東西,換句話說,能用sql實現的功能這裏都可以實現

打印結果:

3)distinct 去重

和sql中的distinct關鍵字很相似。為了看到效果,此處在原集合中加入一個重複的人,就選擇鋼鐵俠吧,復聯4鋼鐵俠不幸遇害,大家還是比較傷心的。

List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤”,18,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

//distinct 去重
List<User> distinctList = filterList.stream().distinct()
        .collect(toList());

 

打印結果:

4)sorted排序

如果流中的元素的類實現了 Comparable 接口,即有自己的排序規則,那麼可以直接調用 sorted() 方法對元素進行排序,如: 

Comparator.comparingInt

 

反之, 需要調用 sorted((T, T) -> int) 實現 Comparator 接口。

//sorted()
List<User> sortedList = distinctList.stream().sorted(Comparator.comparingInt(User::getAge))
        .collect(toList());

 

打印結果:

結果按照年齡從小到大進行排序。

5)limit() 返回前n個元素

如果想知道這裏面年齡最小的是誰,可作如下操作:

//limit 返回前n個元素
List<User> limitList = sortedList.stream().limit(1)
        .collect(toList());

 

6)skip()

與limit恰恰相反,skip的意思是跳過,也就是去除前n個元素。

打印結果:

果然,前兩個人都被去除了,只剩下最老的葫蘆娃爺爺。

3.2 映射

1)map(T->R)

map是將T類型的數據轉為R類型的數據,比如我們想要設置一個新的list,存儲用戶所有的城市信息。

//map(T->R)
List<String> cityList = list.stream().map(User::getAddress).distinct().collect(toList());

 

打印結果:

2)flatMap(T -> Stream)

將流中的每一個元素 T 映射為一個流,再把每一個流連接成為一個流。

//flatMap(T -> Stream<R>)
List<String> flatList = new ArrayList<>();
flatList.add("唱,跳");
flatList.add("rape,籃球,music");
flatList = flatList.stream().map(s -> s.split(",")).flatMap(Arrays::stream).collect(toList());

 

打印結果:

這裏原集合中的數據由逗號分割,使用split進行拆分后,得到的是Stream<string[]>,字符串數組組成的流,要使用flatMap的

Arrays::stream

將Stream<string[]>轉為Stream,然後把流相連接,組成了完整的唱、跳、rap、籃球和music。

3.3 查找

1)allMatch(T->boolean)

檢測是否全部滿足參數行為,假如這些用戶是網吧上網的用戶名單,那就需要檢查是不是每個人都年滿18周歲了。

boolean isAdult = list.stream().allMatch(user -> user.getAge() >= 18);

 

打印結果:

true

 

2)anyMatch(T->boolean)

檢測是否有任意元素滿足給定的條件,比如,想知道同學名單里是否有女生。

//anyMatch(T -> boolean) 是否有任意一個元素滿足給定的條件
boolean isGirl = list.stream().anyMatch(user -> user.getSex() == 1);

 

打印結果:

true

 

說明集合中有女生存在。

3)noneMatch(T -> boolean)

流中是否有元素匹配給定的 T -> boolean 條件。

比如檢測有沒有來自巴黎的用戶。

boolean isLSJ = list.stream().noneMatch(user -> user.getAddress().contains("巴黎"));

 

打印結果:

true

 

打印true說明沒有巴黎的用戶。

4)findFirst( ):找到第一個元素

Optional<User> fristUser  = list.stream().findFirst();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

5)findAny():找到任意一個元素

Optional<User> anyUser  = list.stream().findAny();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

這裏我們發現findAny返回的也總是第一個元素,那麼為什麼還要進行區分呢?因為在并行流 parallelStream() 中找到的確實是任意一個元素。

Optional<User> anyParallelUser  = list.parallelStream().findAny();

 

打印結果 :

Optional[User(name=李世民, age=60, sex=0, address=山西省太原市)]

 

3.4 歸納計算

1)求用戶的總人數

long count = list.stream().collect(Collectors.counting());

 

我們可以簡寫為:

long count = list.stream().count();

 

運行結果:

 8

 

2)得到某一屬性的最大最小值

// 求最大年齡
Optional<User> max = list.stream().collect(Collectors.maxBy(
Comparator.comparing(User::getAge)));

// 求最小年齡
Optional<User> min = list.stream().collect(Collectors.minBy(
Comparator.comparing(User::getAge)));

 

運行結果:

3)求年齡總和是多少

// 求年齡總和
int totalAge = list.stream().collect(Collectors.summingInt(User::getAge));

 

運行結果:

 313

 

我們經常會用BigDecimal來記錄金錢,假設想得到BigDecimal的總和:

// 獲得列表對象金額, 使用reduce聚合函數,實現累加器
BigDecimal sum = myList.stream() .map(User::getMoney)
.reduce(BigDecimal.ZERO,BigDecimal::add);

 

4)求年齡平均值

//求年齡平均值
double avgAge = list.stream().collect(
Collectors.averagingInt(User::getAge));

 

運行結果:

 39.125

 

5)一次性得到元素的個數、總和、最大值、最小值

IntSummaryStatistics statistics = list.stream().collect(
Collectors.summarizingInt(User::getAge));

 

運行結果:

6)字符串拼接

要將用戶的姓名連成一個字符串並用逗號分割。

String names = list.stream().map(User::getName)
.collect(Collectors.joining(", "));

 

運行結果:

 鋼鐵俠, 鋼鐵俠, 蜘蛛俠, 趙麗穎, 詹姆斯, 李世民, 蔡徐坤, 葫蘆娃的爺爺

 

3.5 分組

在數據庫操作中,我們經常通過GROUP BY關鍵字對查詢到的數據進行分組,java8的流式處理也提供了分組的功能。使用Collectors.groupingBy來進行分組。

1)可以根據用戶所在城市進行分組

Map<String, List<User>> cityMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress));

 

結果是一個map,key為不重複的城市名,value為屬於該城市的用戶列表。已經實現了分組。

2)二級分組,先根據城市分組再根據性別分組

Map<String, Map<Integer, List<User>>> group = list.stream().collect(
        Collectors.groupingBy(User::getAddress, // 一級分組,按所在地區
                Collectors.groupingBy(User::getSex))); // 二級分組,按性別

 

運行結果:

3)如果僅僅想統計各城市的用戶個數是多少,並不需要對應的list

按城市分組並統計人數:

Map<String, Long> cityCountMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

4)當然,也可以先進行過濾再分組並統計人數

Map<String,Long> map = list.stream().filter(user -> user.getAge() <= 30)
        .collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

5)partitioningBy 分區

分區與分組的區別在於,分區是按照 true 和 false 來分的,因此partitioningBy 接受的參數的 lambda 也是 T -> boolean

//根據年齡是否小於等於30來分區
Map<Boolean, List<User>> part = list.stream()
        .collect(partitioningBy(user -> user.getAge() <= 30));

 

運行結果:

總結

到目前為止,stream的功能我們已經用了很多了,感覺有點眼花繚亂卻無所不能,stream能做的事情遠遠不止這些。

我們可以多學習使用stream,把原來複雜的sql查詢,一遍又一遍地for循環的複雜代碼重構,讓代碼更簡潔易懂,可讀性強。

拓展閱讀:Redis專題(1):構建知識圖譜

Redis專題(2):Redis數據結構底層探秘

作者:楊亨

來源:宜信技術學院

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!