Elastic Stack 開源的大數據解決方案

目的

本文主要介紹的內容有以下三點:
一. Elastic Stack是什麼以及組成部分
二. Elastic Stack前景以及業務應用
三. Elasticsearch原理(索引方向)
四. Elasticsearch相對薄弱的地方

一、Elastic Stack是什麼以及組成部分

介紹Elastic Stack是什麼,其實只要一句話就可以,就是: 一套完整的大數據處理堆棧,從攝入、轉換到存儲分析、可視化

它是不同產品的集合,各司其職,形成完整的數據處理鏈,因此Elastic Stack也可以簡稱為BLEK。

Beats 輕量型數據採集器

Logstash 輸入、過濾器和輸出

Elasticsearch 查詢和分析

Kibana 可視化,可自由選擇如何呈現數據

1. Beats – 全品類採集器,搞定所有數據類型

Filebeat(日誌文件):對成百上千、甚至上萬的服務器生成的日誌匯總,可搜索。

Metricbeat(指標): 收集系統和服務指標,CPU 使用率、內存、文件系統、磁盤 IO 和網絡 IO 統計數據。

Packetbeat(網絡數據):網絡數據包分析器,了解應用程序動態。

Heartbeat(運行時間監控):通過主動探測來監測服務的可用性
……
Beats支持許許多多的beat,這裏列的都是比較的常用的beat,了解更多可以點擊鏈接:

2. Logstash – 服務器端數據處理管道

介紹Logstash之前,我們先來看下Linux下常用的幾個命令

cat alldata.txt | awk ‘{print $1}’ | sort | uniq | tee filterdata.txt

只要接觸過Linux同學,應該都知道這幾個命名意思

cat alldata.txt #將alldata.txt的內容輸出到標準設備上
awk ‘{print $1}’ #對上面的內容做截取,只取每一行第一列數據
sort | uniq  #對截取后的內容,進行排序和唯一性操作
tee filterdata.txt #將上面的內容寫到filterdata.txt

上面的幾個簡單的命令就可以看出來,這是對數據進行了常規的處理,用名詞修飾的話就是:數據獲取/輸入數據清洗數據過濾數據寫入/輸出

而Logstash做的也是相同的事(看下圖)。

將系統的日誌文件、應用日誌文件、系統指標等數據,輸入到Input,再通過數據清洗以及過濾,輸入到存儲設備中,這裏當然是輸入到Elasticsearch

3. Elasticsearch – 分佈式文檔存儲、RESTful風格的搜索和數據分析引擎

Elasticsearch主要也是最原始的功能就是搜索和分析功能。這裏就簡單說一下,下面講原理的時候會着重講到Elasticsearch

搜索:全文搜索,完整的信息源轉化為計算機可以識別、處理的信息單元形成的數據集合 。

分析:相關度,搜索所有內容,找到所需的具體信息(詞頻或熱度等對結果排序)

4. Kibana- 可視化

可視化看下圖(來源官網)便知

可以對日誌分析、業務分析等做可視化

現在從總體上來了解下,在心中對Elastic Stack有個清楚的認知(下圖)。

二、Elastic Stack前景以及業務應用

1. DB-Engines 排名

Elasticsearch是Elastic Stack核心,由圖可以看出在搜索領域Elasticsearch暫時沒有對手。

2. ES社區

ES中文社區也是相當活躍的,會定期做一下分享,都是大公司的寶貴經驗,值得參考。

3. 2018年攜程的使用情況(讓我們看看能處理多大的數據)

集群數量是94個,最小的集群一般是3個節點。全部節點數量大概700+。

最大的一個集群是做日誌分析的,其中數據節點330個,最高峰一天產生1600億文檔,寫入值300w/s。

現在有2.5萬億文檔,大概是幾個PB的量

三、Elasticsearch(ES)原理

因為篇目有限,本篇只介紹ES的索引原理。

ES為什麼可以做全文搜索,主要就是用了倒排索引,先來看下面的一張圖

看圖可以簡單的理解倒排索引就是:關鍵字 + 頁碼

對倒排索引有個基本的認識后,下面來做個簡單的數據例子。

現在對Name做到排索引,記住:關鍵字 + ID(頁碼)。

對Age做到排索引。

對Intersets做到排索引。

現在搜索Age等於18的,通過倒排索引就可以快速得到1和3的id,再通過id就可以得到具體數據,看,這樣是不是快的狠。

如果是用Mysql等關係數據庫,現在有十多億數據(大數據嘛),就要一條一條的掃描下去找id,效率可想而知。而用倒排索引,找到所有的id就輕輕鬆鬆了。

在ES中,關鍵詞叫Term,頁碼叫Posting List。

但這樣就行了嗎? 如果Name有上億個Term,要找最後一個Term,效率豈不是還是很低?

再來看Name的倒排索引,你會發現,將Armani放在了第一個,Tyloo放在了第三個,可以看出來,對Term做了簡單的排序。雖然簡單,但很實用。這樣查找Term就可以用二分查找法來查找了,將複雜度由n變成了logn。

在ES中,這叫Term Dictionary。

到這裏,再來想想MySQL的b+tree, 你有沒有發現原理是差不多的,那為什麼說ES搜索比MySQL快很多,究竟快在哪裡? 接下來再看。

有一種數據結構叫Trie樹,又稱前綴樹或字典樹,是一種有序樹。這種數據結構的好處就是可以壓縮前綴和提高查詢數據。

現在有這麼一組Term: apps, apple, apply, appear, back, backup, base, bear,用Trie樹表示如下圖。

通過線路路徑字符連接就可以得到完成的Term,並且合用了前綴,比如apps, apple, apply, appear合用了app路徑,節省了大量空間。

這個時候再來找base單詞,立即就可以排除了a字符開頭的單詞,比Term Dictionary快了不知多少。

在ES中,這叫Term Index

現在我們再從整體看下ES的索引

先通過Trie樹快速定位block(相當於頁碼), 再到Term Dictionary 做二分查找,得到Posting List。

索引優化

ES是為了大數據而生的,這意味着ES要處理大量的數據,它的Term數據量也是不可想象的。比如一篇文章,要做全文索引,就會對全篇的內容做分詞,會產生大量的Term,而ES查詢的時候,這些Term肯定要放在內存裏面的。

雖然Trie樹對前綴做了壓縮,但在大量Term面前還是不夠,會佔用大量的內存使用,於是就有ES對Trie樹進一步演化。

FST(Finite State Transducer )確定無環有限狀態轉移器 (看下圖)

可以看appear、bear 對共同的後綴做了壓縮。

Posting List磁盤壓縮

假設有一億的用戶數據,現在對性別做搜索,而性別無非兩種,可能”男”就有五千萬之多,按int4個字節存儲,就要消耗50M左右的磁盤空間,而這僅僅是其中一個Term。

那麼面對成千上萬的Term,ES究竟是怎麼存儲的呢?接下來,就來看看ES的壓縮方法。

Frame Of Reference (FOR) 增量編碼壓縮,將大數變小數,按字節存儲

只要能把握“增量,大數變小數,按字節存儲”這幾個關鍵詞,這個算法就很好理解,現在來具體看看。

現在有一組Posting List:[60, 150, 300,310, 315, 340], 按正常的int型存儲,size = 6 * 4(24)個字節。

  1. 按增量存儲:60 + 90(150)+ 150(300) + 10(310) + 5(315)+ 25(340),也就是[60, 90, 150, 10, 5, 25],這樣就將大數變成了小數。

  2. 切分成不同的block:[60, 90, 150]、[10, 5, 25],為什麼要切分,下面講。

  3. 按字節存儲:對於[60, 90, 150]這組block,究竟怎麼按字節存儲,其實很簡單,就是找其中最大的一個值,看X個比特能表示這個最大的數,那麼剩下的數也用X個比特表示(切分,可以盡可能的壓縮空間)。

[60, 90, 150]最大數150 < 2^8 = 256,也就是這組每個數都用8個比特表示,也就是 3*8 = 24個比特,再除以8,也就是3個字節存在,再加上一個8的標識位(說明每個數是8個比特存儲),佔用一個字節,一共4個字節。

[10, 5, 25]最大數25 < 2^5 = 32,每個數用5個比特表示,3*5=15比特,除以8,大約2個字節,加上5的標識位,一共3個字節。

那麼總體size = 4 + 3(7)個字節,相當於24個字節,大大壓縮了空間。

再看下圖表示

Posting List內存壓縮

同學們應該都知道越複雜的算法消耗的CPU性能就越大,比如常見的https,第一次交互會用非對稱密碼來驗證,驗證通過後就轉變成了對稱密碼驗證,FOR同樣如此,那麼ES是用什麼算法壓縮內存中的Posting List呢?

Roaring Bitmaps 壓縮位圖索引

Roaring Bitmaps 涉及到兩種數據結構 short[] 、bitmap。

short好理解就是2個字節的整型。

bitmap就是用比特表示數據,看下面的例子。

Posting List:[1, 2, 4, 7, 10] -> [1, 1, 0, 1, 0, 0, 1,0, 0, 1],取最大的值10,那麼就用10個比特表示這組Posting List,第1, 2, 4, 7, 10位存在,就將相對應的“位”置為1,其他的為0。

但這種bitmap數據結構有缺陷,看這組Posting List: [1, 3, 100000000] -> [1, 0, 1, 0, 0, 0, …, 0, 0, 1 ],最大數是1億,就要1億的比特表示,這麼算下來,反而消耗了更多的內存。

那如何解決這個問題,其實也很簡單,跟上面一樣,將大數變成小數

看下圖:

第一步:將每個數除以65536,得到(商,餘數)。

第二步:按照商,分成不同的block,也就是相同的商,放在同一個block裏面,餘數就是這個值在這個block裏面的位置(永遠不會超過65536,餘數嘛)。

第三步:判斷底層block用什麼數據結構存儲數據,如果block裏面的餘數的個數超過4096個,就用short存儲,反之bitmap。

上面那個圖是官網的圖,看下我畫的圖,可能更好理解些。

到這裏,ES的索引原理就講完了,希望大家都能理解。

四、Elasticsearch(ES)相對薄弱的地方

1. 多表關聯

其實ES有一個很重要的特性這裏沒有介紹到,也就是分佈式,每一個節點的數據和,才是整體數據。

這也導致了多表關聯問題,雖然ES裏面也提供了Nested& Join 方法解決這個問題,但這裏還是不建議用。

那這個問題在實際應用中應該如何解決? 其實也很簡單,裝換思路,ES無法解決,就到其他層解決,比如:應用層,用面向對象的架構,拆分查詢。

2. 深度分頁

分佈式架構下,取數據便不是那麼簡單,比如取前1000條數據,如果是10個節點,那麼每個節點都要取1000條,10個節點就是10000條,排序后,返回前1000條,如果是深度分頁就會變的相當的慢。

ES提供的是Scroll + Scroll_after,但這個採取的是緩存的方式,取出10000條后,緩存在內存里,再來翻頁的時候,直接從緩存中取,這就代表着存在實時性問題。

來看看百度是怎麼解決這個問題的。

一樣在應用層解決,翻頁到一定的深度后,禁止翻頁。

3. 更新應用

頻繁更新的應用,用ES會有瓶頸,比如一些遊戲應用,要不斷的更新數據,用ES不是太適合,這個看大家自己的應用情況。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

Java 8 Streams API 詳解

流式編程作為Java 8的亮點之一,是繼Java 5之後對集合的再一次升級,可以說Java 8幾大特性中,Streams API 是作為Java 函數式的主角來設計的,誇張的說,有了Streams API之後,萬物皆可一行代碼。

什麼是Stream

Stream被翻譯為流,它的工作過程像將一瓶水導入有很多過濾閥的管道一樣,水每經過一個過濾閥,便被操作一次,比如過濾,轉換等,最後管道的另外一頭有一個容器負責接收剩下的水。

示意圖如下:

首先通過source產生流,然後依次通過一些中間操作,比如過濾,轉換,限制等,最後結束對流的操作。

Stream也可以理解為一個更加高級的迭代器,主要的作用便是遍歷其中每一個元素。

為什麼需要Stream

Stream作為Java 8的一大亮點,它專門針對集合的各種操作提供各種非常便利,簡單,高效的API,Stream API主要是通過Lambda表達式完成,極大的提高了程序的效率和可讀性,同時Stram API中自帶的并行流使得併發處理集合的門檻再次降低,使用Stream API編程無需多寫一行多線程的大門就可以非常方便的寫出高性能的併發程序。使用Stream API能夠使你的代碼更加優雅。

流的另一特點是可無限性,使用Stream,你的數據源可以是無限大的。

在沒有Stream之前,我們想提取出所有年齡大於18的學生,我們需要這樣做:

List<Student> result=new ArrayList<>();
for(Student student:students){
 
    if(student.getAge()>18){
        result.add(student);
    }
}
return result;

使用Stream,我們可以參照上面的流程示意圖來做,首先產生Stream,然後filter過濾,最後歸併到容器中。

轉換為代碼如下:

return students.stream().filter(s->s.getAge()>18).collect(Collectors.toList());
  • 首先stream()獲得流
  • 然後filter(s->s.getAge()>18)過濾
  • 最後collect(Collectors.toList())歸併到容器中

是不是很像在寫sql?

如何使用Stream

我們可以發現,當我們使用一個流的時候,主要包括三個步驟:

  • 獲取流
  • 對流進行操作
  • 結束對流的操作

獲取流

獲取流的方式有多種,對於常見的容器(Collection)可以直接.stream()獲取
例如:

  • Collection.stream()
  • Collection.parallelStream()
  • Arrays.stream(T array) or Stream.of()

對於IO,我們也可以通過lines()方法獲取流:

  • java.nio.file.Files.walk()
  • java.io.BufferedReader.lines()

最後,我們還可以從無限大的數據源中產生流:

  • Random.ints()

值得注意的是,JDK中針對基本數據類型的昂貴的裝箱和拆箱操作,提供了基本數據類型的流:

  • IntStream
  • LongStream
  • DoubleStream

這三種基本數據類型和普通流差不多,不過他們流裏面的數據都是指定的基本數據類型。

Intstream.of(new int[]{1,2,3});
Intstream.rang(1,3);

對流進行操作

這是本章的重點,產生流比較容易,但是不同的業務系統的需求會涉及到很多不同的要求,明白我們能對流做什麼,怎麼做,才能更好的利用Stream API的特點。

流的操作類型分為兩種:

  • Intermediate:中間操作,一個流可以後面跟隨零個或多個intermediate操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然後會返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,並沒有真正開始流的遍歷。

    map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:終結操作,一個流只能有一個terminal操作,當這個操作執行后,流就被使用“光”了,無法再被操作。所以這必定是流的最後一個操作。Terminal操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個 side effect。

    forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

IntermediateTerminal完全可以按照上圖的流程圖理解,Intermediate表示在管道中間的過濾器,水會流入過濾器,然後再流出去,而Terminal操作便是最後一個過濾器,它在管道的最後面,流入Terminal的水,最後便會流出管道。

下面依次詳細的解讀下每一個操作所能產生的效果:

中間操作

對於中間操作,所有的API的返回值基本都是Stream<T>,因此以後看見一個陌生的API也能通過返回值判斷它的所屬類型。

map/flatMap

map顧名思義,就是映射,map操作能夠將流中的每一個元素映射為另外的元素。

 <R> Stream<R> map(Function<? super T, ? extends R> mapper);

可以看到map接受的是一個Function,也就是接收參數,並返回一個值。

比如:

//提取 List<Student>  所有student 的名字 
List<String> studentNames = students.stream().map(Student::getName)
                                             .collect(Collectors.toList());

上面的代碼等同於以前的:

List<String> studentNames=new ArrayList<>();
for(Student student:students){
    studentNames.add(student.getName());
}

再比如:將List中所有字母轉換為大寫:

List<String> words=Arrays.asList("a","b","c");
List<String> upperWords=words.stream().map(String::toUpperCase)
                                      .collect(Collectors.toList());

flatMap顧名思義就是扁平化映射,它具體的操作是將多個stream連接成一個stream,這個操作是針對類似多維數組的,比如容器裡面包含容器等。

List<List<Integer>> ints=new ArrayList<>(Arrays.asList(Arrays.asList(1,2),
                                          Arrays.asList(3,4,5)));
List<Integer> flatInts=ints.stream().flatMap(Collection::stream).
                                       collect(Collectors.toList());

可以看到,相當於降維。

filter

filter顧名思義,就是過濾,通過測試的元素會被留下來並生成一個新的Stream

Stream<T> filter(Predicate<? super T> predicate);

同理,我們可以filter接收的參數是Predicate,也就是推斷型函數式接口,接收參數,並返回boolean值。

比如:

//獲取所有大於18歲的學生
List<Student> studentNames = students.stream().filter(s->s.getAge()>18)
                                              .collect(Collectors.toList());

distinct

distinct是去重操作,它沒有參數

  Stream<T> distinct();

sorted

sorted排序操作,默認是從小到大排列,sorted方法包含一個重載,使用sorted方法,如果沒有傳遞參數,那麼流中的元素就需要實現Comparable<T>方法,也可以在使用sorted方法的時候傳入一個Comparator<T>

Stream<T> sorted(Comparator<? super T> comparator);

Stream<T> sorted();

值得一說的是這個ComparatorJava 8之後被打上了@FunctionalInterface,其他方法都提供了default實現,因此我們可以在sort中使用Lambda表達式

例如:

//以年齡排序
students.stream().sorted((s,o)->Integer.compare(s.getAge(),o.getAge()))
                                  .forEach(System.out::println);;

然而還有更方便的,Comparator默認也提供了實現好的方法引用,使得我們更加方便的使用:

例如上面的代碼可以改成如下:

//以年齡排序 
students.stream().sorted(Comparator.comparingInt(Student::getAge))
                            .forEach(System.out::println);;

或者:

//以姓名排序
students.stream().sorted(Comparator.comparing(Student::getName)).
                          forEach(System.out::println);

是不是更加簡潔。

peek

peek有遍歷的意思,和forEach一樣,但是它是一个中間操作。

peek接受一個消費型的函數式接口。

Stream<T> peek(Consumer<? super T> action);

例如:

//去重以後打印出來,然後再歸併為List
List<Student> sortedStudents= students.stream().distinct().peek(System.out::println).
                                                collect(Collectors.toList());

limit

limit裁剪操作,和String::subString(0,x)有點先溝通,limit接受一個long類型參數,通過limit之後的元素只會剩下min(n,size)個元素,n表示參數,size表示流中元素個數

 Stream<T> limit(long maxSize);

例如:

//只留下前6個元素並打印
students.stream().limit(6).forEach(System.out::println);

skip

skip表示跳過多少個元素,和limit比較像,不過limit是保留前面的元素,skip是保留後面的元素

Stream<T> skip(long n);

例如:

//跳過前3個元素並打印 
students.stream().skip(3).forEach(System.out::println);

終結操作

一個流處理中,有且只能有一個終結操作,通過終結操作之後,流才真正被處理,終結操作一般都返回其他的類型而不再是一個流,一般來說,終結操作都是將其轉換為一個容器。

forEach

forEach是終結操作的遍歷,操作和peek一樣,但是forEach之後就不會再返迴流

 void forEach(Consumer<? super T> action);

例如:

//遍歷打印
students.stream().forEach(System.out::println);

上面的代碼和一下代碼效果相同:

for(Student student:students){
    System.out.println(sudents);
}

toArray

toArrayList##toArray()用法差不多,包含一個重載。

默認的toArray()返回一個Object[]

也可以傳入一個IntFunction<A[]> generator指定數據類型

一般建議第二種方式。

Object[] toArray();

<A> A[] toArray(IntFunction<A[]> generator);

例如:

 Student[] studentArray = students.stream().skip(3).toArray(Student[]::new);

max/min

max/min即使找出最大或者最小的元素。max/min必須傳入一個Comparator

Optional<T> min(Comparator<? super T> comparator);

Optional<T> max(Comparator<? super T> comparator);

count

count返迴流中的元素數量

long count();

例如:

long  count = students.stream().skip(3).count();

reduce

reduce為歸納操作,主要是將流中各個元素結合起來,它需要提供一個起始值,然後按一定規則進行運算,比如相加等,它接收一個二元操作 BinaryOperator函數式接口。從某種意義上來說,sum,min,max,average都是特殊的reduce

reduce包含三個重載:

T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

 <U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

例如:

List<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
        
long count = integers.stream().reduce(0,(x,y)->x+y);

以上代碼等同於:

long count = integers.stream().reduce(Integer::sum).get();

reduce兩個參數和一個參數的區別在於有沒有提供一個起始值,

如果提供了起始值,則可以返回一個確定的值,如果沒有提供起始值,則返回Opeational防止流中沒有足夠的元素。

anyMatch allMatch noneMatch

測試是否有任意元素\所有元素\沒有元素匹配表達式

他們都接收一個推斷類型的函數式接口:Predicate

 boolean anyMatch(Predicate<? super T> predicate);

 boolean allMatch(Predicate<? super T> predicate);

 boolean noneMatch(Predicate<? super T> predicate)

例如:

 boolean test = integers.stream().anyMatch(x->x>3);

findFirst、 findAny

獲取元素,這兩個API都不接受任何參數,findFirt返迴流中第一個元素,findAny返迴流中任意一個元素。

Optional<T> findFirst();

Optional<T> findAny();

也有有人會問findAny()這麼奇怪的操作誰會用?這個API主要是為了在并行條件下想要獲取任意元素,以最大性能獲取任意元素

例如:

int foo = integers.stream().findAny().get();

collect

collect收集操作,這個API放在後面將是因為它太重要了,基本上所有的流操作最後都會使用它。

我們先看collect的定義:

 <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);

可以看到,collect包含兩個重載:

一個參數和三個參數,

三個參數我們很少使用,因為JDK提供了足夠我們使用的Collector供我們直接使用,我們可以簡單了解下這三個參數什麼意思:

  • Supplier:用於產生最後存放元素的容器的生產者
  • accumulator:將元素添加到容器中的方法
  • combiner:將分段元素全部添加到容器中的方法

前兩個元素我們都很好理解,第三個元素是幹嘛的呢?因為流提供了并行操作,因此有可能一個流被多個線程分別添加,然後再將各個子列表依次添加到最終的容器中。

↓ – – – – – – – – –

↓ — — —

↓ ———

如上圖,分而治之。

例如:

List<String> result = stream.collect(ArrayList::new, List::add, List::addAll);

接下來看只有一個參數的collect

一般來說,只有一個參數的collect,我們都直接傳入Collectors中的方法引用即可:

List<Integer> = integers.stream().collect(Collectors.toList());

Collectors中包含很多常用的轉換器。toList(),toSet()等。

Collectors中還包括一個groupBy(),他和Sql中的groupBy一樣都是分組,返回一個Map

例如:

//按學生年齡分組
Map<Integer,List<Student>> map= students.stream().
                                collect(Collectors.groupingBy(Student::getAge));

groupingBy可以接受3個參數,分別是

  1. 第一個參數:分組按照什麼分類
  2. 第二個參數:分組最後用什麼容器保存返回(當只有兩個參數是,此參數默認為HashMap
  3. 第三個參數:按照第一個參數分類后,對應的分類的結果如何收集

有時候單參數的groupingBy不滿足我們需求的時候,我們可以使用多個參數的groupingBy

例如:

//將學生以年齡分組,每組中只存學生的名字而不是對象
Map<Integer,List<String>> map =  students.stream().
  collect(Collectors.groupingBy(Student::getAge,Collectors.mapping(Student::getName,Collectors.toList())));

toList默認生成的是ArrayList,toSet默認生成的是HashSet,如果想要指定其他容器,可以如下操作:

 students.stream().collect(Collectors.toCollection(TreeSet::new));

Collectors還包含一個toMap,利用這個API我們可以將List轉換為Map

  Map<Integer,Student> map=students.stream().
                           collect(Collectors.toMap(Student::getAge,s->s));

值得注意的一點是,IntStreamLongStream,DoubleStream是沒有collect()方法的,因為對於基本數據類型,要進行裝箱,拆箱操作,SDK並沒有將它放入流中,對於基本數據類型流,我們只能將其toArray()

優雅的使用Stream

了解了Stream API,下面詳細介紹一下如果優雅的使用Steam

  • 了解流的惰性操作

    前面說到,流的中間操作是惰性的,如果一個流操作流程中只有中間操作,沒有終結操作,那麼這個流什麼都不會做,整個流程中會一直等到遇到終結操作操作才會真正的開始執行。

    例如:

    students.stream().peek(System.out::println);

    這樣的流操作只有中間操作,沒有終結操作,那麼不管流裡面包含多少元素,他都不會執行任何操作。

  • 明白流操作的順序的重要性

    Stream API中,還包括一類Short-circuiting,它能夠改變流中元素的數量,一般這類API如果是中間操作,最好寫在靠前位置:

    考慮下面兩行代碼:

    students.stream().sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      limit(3).              
                      collect(Collectors.toList());
    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());

    兩段代碼所使用的API都是相同的,但是由於順序不同,帶來的結果都非常不一樣的,

    第一段代碼會先排序所有的元素,再依次打印一遍,最後獲取前三個最小的放入list中,

    第二段代碼會先截取前3個元素,在對這三個元素排序,然後遍歷打印,最後放入list中。

  • 明白Lambda的局限性

    由於Java目前只能Pass-by-value,因此對於Lambda也和有匿名類一樣的final的局限性。

    具體原因可以參考

    因此我們無法再lambda表達式中修改外部元素的值。

    同時,在Stream中,我們無法使用break提前返回。

  • 合理編排Stream的代碼格式

    由於可能在使用流式編程的時候會處理很多的業務邏輯,導致API非常長,此時最後使用換行將各個操作分離開來,使得代碼更加易讀。

    例如:

    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());

    而不是:

    students.stream().limit(3).sorted(Comparator.comparingInt(Student::getAge)).peek(System.out::println).collect(Collectors.toList());

    同時由於Lambda表達式省略了參數類型,因此對於變量,盡量使用完成的名詞,比如student而不是s,增加代碼的可讀性。

    盡量寫出敢在代碼註釋上留下你的名字的代碼!

總結

總之,Stream是Java 8 提供的簡化代碼的神器,合理使用它,能讓你的代碼更加優雅。

尊重勞動成功,轉載註明出處

參考鏈接:

《Effective Java》3th

如果覺得寫得不錯,歡迎關注微信公眾號:逸游Java ,每天不定時發布一些有關Java乾貨的文章,感謝關注

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

取道對歐輸送天然氣 俄烏達成初步協議

摘錄自2019年12月20日中央社報導

俄羅斯天然氣工業公司2019年向歐洲供應2008億立方公尺的天然氣,其中有約40%是借道烏克蘭運送,讓烏克蘭每年賺得約30億美元(約新台幣904億元)的過境費,此輸送合約將於年底到期,但自莫斯科2014年併吞克里米亞並支持烏克蘭東部的分離主義分子叛亂活動後,雙方關係急轉直下。

俄羅斯與烏克蘭歷經數個月的艱難談判,在即將到來的新年截止期限前,簽署取道烏克蘭將俄羅斯天然氣運往歐洲的初步協議。

俄羅斯通訊社引述俄羅斯天然氣工業公司(Gazprom)發言人說法報導:「俄羅斯與烏克蘭已經簽署諒解備忘錄。」但沒有提供合約細節。法新社報導,俄羅斯能源部長諾瓦克(Alexander Novak)說,這是5年合約,將在月底前簽署。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

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

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

築起和平之牆 修復河壩 達佛「氣候變遷戰爭」平息有望

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

不甩歐盟反對 川普批准制裁俄歐天然氣管線

摘錄自2019年12月21日中央社報導

法新社報導,美國總統川普21日批准,可制裁替俄國建設天然氣管線到德國的企業。美國國會擔心,管線將讓俄國獲得影響歐洲盟邦的危險籌碼;但歐盟反對制裁,認為自己有權決定能源政策。

美國的制裁鎖定在波羅的海建造北溪天然氣2號管線(Nord Stream 2)的相關公司;造價近110億美元的這條管線,可讓俄國輸送至歐洲經濟體龍頭德國的天然氣量倍增。美國國會議員警告,管線會助長一個含敵意的俄國政府,並在歐陸緊張升溫之際,大增俄國總統蒲亭(Vladimir Putin)的影響力。

不過華府此舉已激怒莫斯科及歐盟,後者表示他們有權決定自己的能源政策。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

日產聆風在美“充電不花錢”項目將新增三個城市

日產聆風在美國進行的“充電不花錢”(No Charge to Charge)項目將新增三個城市。

據報導,此前有26個不同市城市消費者可以享受這個項目,不過好消息是,現在位於紐約市、費城與聖巴巴拉市的聆風用戶也可以進行免費充電了。用戶可以在指定的公共充電站使用贈送的費用充電,同樣,用戶也可以使用EZ-Charge網站或APP輕鬆的定位合作的充電站。

“充電不花錢”項目針對日產聆風電動汽車的購買者或租賃者,並提供免費兩年的充電機會。新車主將受到一張EZ-Charge卡,這張卡可以介入充電點(ChargePoint)。

除了新增的城市,其他26個城市是三藩市、洛杉磯、沙加緬度、聖地牙哥、夫勒斯諾市、波特蘭、芝加哥、達拉斯-沃思堡、惠斯頓、印度安納波利斯、那什維爾、鳳凰城、丹佛、華府、巴爾的摩、波斯頓、蒙特利、亞特蘭大、 羅利、鹽湖城和明尼阿波利斯—聖保羅都會區。
 

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

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

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

騰訊、和諧汽車、富士康“互聯網+智慧電動汽車”專案公佈企業負責人

和諧富騰互聯網加智慧電動汽車公司(以下簡稱“和諧富騰”)日前宣佈旗下“互聯網+智慧電動汽車”企業負責人,畢福康博士(Dr. Carsten Breitfeld)將擔任該企業首席執行官,戴雷博士(Dr. Daniel Kirchert)將擔任首席運營官。兩位管理層到任後將即刻啟動企業的運營。畢福康博士和戴雷博士亦將是該企業的事業合夥人和公司董事會成員。

左:畢福康博士(Dr. Carsten Breitfeld);右:戴雷博士(Dr. Daniel Kirchert)

和諧富騰是由中國和諧新能源汽車控股有限公司、鴻海集團與騰訊集團透過各自附屬實體聯合創立的創新投資平臺,“互聯網+智慧電動汽車”是該平臺旗下核心戰略專案和獨立企業,旨在開發面向未來的個人出行解決方案,塑造源自中國、佈局世界的高端品牌,為消費者提供智慧、愉悅、生態友好的駕乘體驗。

畢福康博士擁有機械工程學博士學位,是全球電動汽車研發領域的一流專家。畢福康博士此前在寶馬集團總部工作20年,擔任過底盤開發、傳動系統開發及產品戰略等方面的多個高級管理崗位,2010年起擔任寶馬集團新一代電動超級跑車i8項目總監,成為這一世界汽車行業劃時代旗艦車型的研發主腦。通過引入革命性的開發流程,畢福康博士帶領團隊實現了i8車型于2014年成功面世,在產品性能、材料、技術革新及研發速度等各個方面顯著優於傳統汽車產品,創下了全球汽車行業新的標杆。在畢福康博士的推動下,i8車型還引進了創新的銷售和客戶體驗機制。

戴雷博士是中國豪華汽車領域擁有最豐富銷售、運營和品牌塑造經驗的高層主管之一,也是業內公認的“中國通”。戴雷博士此前曾擔任東風英菲尼迪汽車有限公司總經理和華晨寶馬汽車有限公司行銷高級副總裁,相關品牌在其任內均創下豪華車市場的銷售增長紀錄,在品牌塑造和市場行銷方面也創造了若干標杆性的案例。戴雷博士在產品戰略、銷售網路發展和合資企業組建運營方面也擁有深厚的經驗。

畢福康博士和戴雷博士將在就任後與傳媒見面,並就“互聯網+智慧電動汽車”的戰略規劃和企業具體運營資訊與傳媒和公眾溝通。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

戴姆勒總裁蔡澈:電動汽車續航里程應達499公里以上

日前,戴姆勒集團總裁迪特•蔡澈(Dieter Zetsche)稱,電動汽車一次充電必須能供應至少310英里(499公里)的行程,才能替代燃油汽車,成為主流。

但蔡澈在接受美國媒體採訪時稱,在短時期內讓所有消費者接受電動汽車不太可能,並稱這是一個連續的過程。首先要降低車載電池的成本。蔡澈估計該價格在170美元每千瓦時左右,而110-130美元則會使汽車很有競爭優勢。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

HashMap7淺析

一、概述

  HashMap,基於哈希結構的Map接口的一個實現,無序,允許null鍵值對,線程不安全的。可以使用集合工具類Collections中的synchronizedMap方法,去創建一個線程安全的集合map。

  在jdk1.7中,HashMap主要是基於 數組+鏈表 的結構實現的。鏈表的存在主要是解決 hash 衝突而存在的。插入數據的時候,計算key的hash值,取得存儲的數組下標,如果衝突已有元素,則會在衝突地址上生成個鏈表,再通過key的比較,鏈表是否已存在,存在則覆蓋,不存在則鏈表上添加。這種方式,如果存在大量衝突的時候,會導致鏈表過長,那麼直接導致的就是犧牲了查詢和添加的效率。所以在jdk1.8版本之後,使用的就是 數組 + 鏈表 + 紅黑樹,當鏈表長度超過 8(實際加上初始的節點,整個有效長度是 9) 的時候,轉為紅黑樹存儲。

  本文中內容,主要基於jdk1.7版本,單線程環境下使用的HahsMap沒有啥問題,但是當在多線程下使用的時候,則可能會出現併發異常,具體表象是CPU會直線上升100%。下面是主要介紹相關的存取以及為什麼會出現線程安全性問題。

二、結構

  

  HashMap默認初始化size=16的哈希數組,然後通過計算待存儲的key的hash值,去計算得到哈希數組的下標值,然後放入鏈表中(新增節點或更新)。鏈表的存在即是解決hash衝突的。

三、源碼實現分析

  1、存儲具體數據的table數組:

      

    Entry為HashMap中的靜態內部類,其具體結構如下圖

      

    key、value屬性就是存儲鍵值對的,next則是指向鏈表的下一個元素節點。

     2、 默認初始化方法:

    

    默認構造方法,不對table進行初始化new(真正初始化動作放在put中,後面會看到),只是設置參數的默認值,hashmap長度和table長度初始化成DEFAULT_INITIAL_CAPACITY(16),加載因子loadFactor默認DEFAULT_LOAD_FACTOR(0.75f,至於為什麼是0.75,這個可以參見 )。

    加載因子:默認情況下,16*0.75=12,也就是在存儲第13個元素的時候,就會進行擴容(jdk1.7的threshold真正計算放在第一次初始化中,後面會再提及)。此元素的設置,直接影響到的是key的hash衝突問題。

  3、put方法

 public V put(K key, V value) {
   
if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }

  3.1、EMPTY_TABLE是HashMap中的一個靜態的空的Entry數組,table也是HashMap的一個屬性,默認就是EMPTY_TABLE(這兩句可參見上面源碼),table就是我們真正數據存儲使用的。
  3.2、前面提及,無參構造的時候,並未真正完成對HashMap的初始化new操作,而僅僅只是設置幾個常量,所以在第一次put數據的時候,table是空的。則會進入下面的初始化table方法中。

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //計算加載因子,默認情況下結果為12
    table = new Entry[capacity];  //真正的初始化table數組
    initHashSeedAsNeeded(capacity);
}

  3.3、key的null判斷

if (key == null)
    return putForNullKey(value);

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

  具體步驟解析:

    1、key為null,取出table[0]的鏈表結構Enrty,如果取出的元素不為null,則對其進行循環遍歷,查找其中是否存在key為null的節點元素。

       2、如果存在key == null的節點,則使用新的value去更新節點的oldValue,並且將oldValue返回。

    3、如果不存在key == null的元素,則執行新增元素addEntry方法:

      (1)判斷是否需要擴容,size為當前數組table中,已存放的Entry鏈表個數,更直接點說,就是map.size()方法的返回值。threshold上面的真正初始化HashMap的時候已經提到,默認情況下,計算得到 threshold=12。若同時滿足  (size >= threshold) && (null != table[bucketIndex]) ,則對map進行2倍的擴容,然後對key進行重新計算hash值和新的數組下標。

      (2)創建新的節點原色createEntry方法,首先獲取table數組中下標為bucketIndex的鏈表的表頭元素,然後新建個Entry作為新的表頭,並且新表頭其中的next指向老的表頭數據。

  3.4、key不為null的存儲  
    原理以及過程上通key==null的大體相同,只不過,key==null的時候,固定是獲取table[0]的鏈表進行操作,而在不為key != null的時候,下標位置是通過
  int hash = hash(key); int i = indexFor(hash, table.length); 計算得到的

  static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

  很清晰的就能看明白,先計算key的hash,然後與當前table的長度進行相與,這樣計算得到待存放數據的下標。得到下標后,過程就與key==null一致了,遍歷是否存在,存在則更新並返回oldVlaue,不存在則新建Entry。

  4、get方法

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    如果key == null,則調用getForNullKey方法,遍歷table[0]處的鏈表。
private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

  如果key != null,則調用getEntry,根據key計算得到在table數組中的下標,獲取鏈表Entry,然後遍歷查找元素,key相等,則返回該節點元素。

 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

四、線程不安全分析

  上述,主要淺析了下HashMap的存取過程,HashMap的線程安全性問題主要也就是在上述的擴容resize方法上,下面來看看在高併發下,擴容后,是如何引起100%問題的。

  1、在進行新元素 put 的時候,這在上面中的3.3的代碼片段中可以查看,addEntry 添加新節點的時候,會計算是否需要擴容處理:(size >= threshold) && (null != table[bucketIndex]) 。

  2、如果擴容的話,會接下來調用 resize 方法

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //關鍵性代碼,構建新hashmap並將老的數據移動過來
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

  3、其中,出現100%問題的關鍵就是上面的 transfer 方法,新建hashmap移動複製老數據

 1  void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             // 遍歷老的HashMap,當遇到不為空的節點的是,進入移動方法
 5             while(null != e) {
 6                 // 首先創建個Entry節點 指向該節點所在鏈表的下一個節點數據
 7                 Entry<K,V> next = e.next;
 8                 if (rehash) {
 9                     e.hash = null == e.key ? 0 : hash(e.key);
10                 }
11               // 計算老的數據在新Hashmap中的下標位置
12                 int i = indexFor(e.hash, newCapacity);
13              // 將新HashMap中相應位置的元素,掛載到老數據的後面(不管有無數據)
14                 e.next = newTable[i];
15                 // 將新HashMap中相應位置指向上面已經成功掛載新數據的老數據
16              newTable[i] = e;
17              // 移動到鏈表節點中的下一個數據,繼續複製節點
18                 e = next;
19             }
20         }
21     }    

  問題的關鍵就在上述的14、15行上,這兩行的動作,在高併發下可能就會造成循環鏈表,循環鏈表在等待下一個嘗試 get 獲取數據的時候,就悲劇了。下面舉例模擬說說這個過程:

  (1)假設目前某個位置的鏈表存儲結構為 A -> B -> C,有兩個線程同時進行擴容操作

  (2)線程1執行到第7行 Entry<K,V> next = e.next; 的時候被掛起了,此時,線程1的 e 指向 A , next 指向的是 B

  (3)線程2執行完成了整個的擴容過程,那麼此時的鏈表結構應該是變為了 C -> B -> A

  (4)線程1喚醒繼續執行,而需要操作的鏈表實際就變成了了上述線程2完成后的 C ->B -> A,下面分為幾步去完成整個操作:

      第一次循環:

        (i)執行 e.next = newTable[i] ,將 A 的 next 指向線程1的新的HashMap,由於此時無數據,所以 e.next = null

        (ii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 A 

        (iii)執行e = next,移動到鏈表中的下一個元素,也就是上面的(2)中的 線程掛起的時候的 B

      第二次循環:

        (i)執行 Entry<K,V> next = e.next,此時的 e 指向 B,next指向 A

        (ii)執行 e.next = newTable[i] ,將 B 的 next 指向線程1的新的HashMap,由於此時有數據A,所以 e.next = A

        (iii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 B,此時線程1的新Hashmap鏈表結構為B -> A

        (iiii)執行e = next,移動到鏈表中的下一個元素 A

      第三次循環:

        (i)執行 Entry<K,V> next = e.next,此時的 e 指向 A,next指向 null

        (ii)執行 e.next = newTable[i] ,將 A 的 next 指向線程1的新的HashMap,由於此時有數據B,所以 e.next = B

        (iii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 A ,此時線程1的新Hashmap鏈表結構為 A -> B -> A

        (iiii)執行e = next,移動到鏈表中的下一個元素,已移動到鏈表結尾,結束 while 循環,完成鏈表的轉移。

  (5)上述過程中,很顯然的,最終的鏈表結構中,出現了 A -> B -> A 的循環結構。擴容完成了,剩下的等待的是get獲取的時候, getEntry 方法中 for循環e = e.next中就永遠出不來了。

  注意:擴容過程中,newTable是每個擴容線程獨有的,共享的只是每個Entry節點數據,最終的擴容是會調用 table = newTable 賦值操作完成。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

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

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

9012年了,再不會Https就老了

前言  

       合格的web後端程序員,在開發之外,必須給IIS、Nginx、各種原生web服務器上配置Https,不然你就僅僅是個碼農,本博最近將專題記錄

  • 如何為IIS,Nginx配置Https

  • 如何申請適用於生產的免費SSL證書

本博客小試牛刀,先實操在Nginx for Docker上添加自簽名SSL證書

為啥先倒騰自簽名SSL證書,申請公網SSL證書需要公網可識別的域名或者公網IP;

如果有實際SSL證書, 按照本文替換即可。

手繪Https原理

長話短說:  目前常見的Http請求明文傳輸, 報文可被截取並篡改,請求可被偽造; 

因此基於常見HTTP(HTTP-TCP-IP)協議棧引入SSL/TSL(Transport Secure Layer) ,HTTPS在進行加密傳輸之前會進行一次握手,確定傳輸密鑰。

流程解讀:

① 傳輸密鑰是對稱密鑰,用於雙方對傳輸數據的加解密

② 怎麼在傳輸之前確立傳輸密鑰呢? 針對普遍的多客戶端訪問受信web服務器的場景, 提出非對稱密鑰(公鑰存於客戶端,私鑰存於web服務器),雙方能互相加解密,說明中間數據(傳輸密鑰)沒被篡改。

③ 再拋出疑問,怎麼認定下發的公鑰是這個web服務器匹配的密鑰?怎麼確定這個公鑰下發過程沒被截取篡改? 這就是追溯到握手階段的下發證書過程,瀏覽器內置的CA機構認定該證書是其有效下發,並通過簽名認定該證書沒被篡改,最終認定該 證書下發的公鑰是受信web服務器準確下發。

④ 如果面向面試記憶Https原理,恐怕有些難度,所以個人用一種 【雞生蛋還是蛋生雞】的方式向上追溯流程, 方便大家知其然更知其所以然。

前置準備

 >   CentOS機器上安裝Docker、 Docker-Compose

 >   常規操作構建 Nginx for Docker網站, 項目結構如下: 

ssl-docker-nginx
    ├── docker-compose.yml
    ├── nginx   
    │      └── nginx.conf
    └── site
           └── index.html

    該項目將會使用  nginx/nginx.conf、site/index.html替換Nginx鏡像默認配置文件和默認啟動頁,docker-compose.yml 如下:

version: '2'
services:
  server:
    image: nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./site:/usr/share/nginx/html
    ports:
    - "8080:80"

    docker-compose  up -d 啟動Nginx容器,還是那樣熟悉的味道: 【chrome默認將http連接認定為不安全

添加SSL自簽名證書

很明顯: web服務器需要存儲證書(內置了公鑰)和私鑰

 ① 創建自簽名證書 (什麼叫自簽名,就是自己給自己頒發 SSL證書)

[nodotnet@gs-server-5809 ssl-docker-nginx]$ openssl req -newkey rsa:2048 -nodes -keyout nginx/my-site.com.key -x509 -days 365out nginx/my-site.com.crt

req是證書請求的子命令,-newkey rsa:2048 -keyout nginx/my-site.com.key表示生成私鑰(PKCS8格式),

          -nodes 表示私鑰不加密,

           -x509表示輸出證書,-days365 為有效期,此後根據提示輸入證書擁有者信息;

       之後會在nginx目錄下生產2個文件, 分別是私鑰、證書

 ② 將證書和密鑰掛載到Nginx Image, 修改docker-compose.yml

version: '2'
services:
  server:
    image: nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./site:/usr/share/nginx/html
      - ./nginx/my-site.com.crt:/etc/nginx/my-site.com.crt    #新行 - ./nginx/my-site.com.key:/etc/nginx/my-site.com.key    #新行
    ports:
    - "8080:80"
    - "443:443"        // 容器開啟HTTPS默認的443端口

 ③  修改nginx/nginx.conf,接受Https請求

events {
  worker_connections  4096;  ## Default: 1024
}

http {
    server {
        listen 80;
        root         /usr/share/nginx/html/;
    }

    server {            # 新Server接受來自443端口的Https請求
        listen              443 ssl;
        ssl_certificate     /etc/nginx/my-site.com.crt;
        ssl_certificate_key /etc/nginx/my-site.com.key;
        root        /usr/share/nginx/html;
    }
}

執行docker-compose down && docker-compose up -d 發起https://10.201.80.126:443請求,當前自簽名證書頒發機構不在瀏覽器內置的CA機構,所以該證書目前被瀏覽器認為是無效。

理論上將 該自簽名證書導出,之後在 【chrome瀏覽器】-【高級設置】-【管理證書】中導入該證書,即可讓 chrome接受自簽名SSL證書。

That‘s All,  Https作為以後web的主流配置,碼農進階資深必須掌握;後續會記錄Https & HSTS, 申請免費SSL證書,盡請關注。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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