上周熱點回顧(6.17-6.23)

熱點隨筆:

· 升維打擊,設計之道(Artech)
· 藉助FreeHttp為任意移動端web網頁添加vConsole調試(lulianqi15)
· 你為什麼有那麼多時間寫博客?(純潔的微笑)
· 看完此文,媽媽還會擔心你docker入不了門?(sullivan06)
· 我和 HelloGitHub(削微寒)
· 簡歷上如果出現過於高大上的項目,反而過猶不及:再論如何通過項目引出技術(hsm_computer)
· 程序員與醫生(道友留步`)
· 一個理想主義的程序員(沉默王二)
· 高考完?入門級的開源項目帶你開啟編程之旅(削微寒)
· 短信驗證碼“最佳實踐”(GUOKUN)
· .NET CORE下最快比較兩個文件內容是否相同的方法(WAKU)
· 從CLR GC到CoreCLR GC看.NET Core對雲原生的支持(艾心❤)

熱點新聞:

· “地震波還有61秒到達”,08年籌建的技術,在這次四川地震中立功了
· 微軟,奪回王位
· 我們從未見過它的真面目,直到一群科學家拍了張照片
· 華為自研SSD揭秘:國內唯一殺入全球TOP10
· 無需固態電池,一條假魚靠“血液”續航36小時
· 5G來了,需要更換SIM卡嗎?
· 屠呦呦團隊放“大招”:“青蒿素抗藥性”等研究獲新進展
· 羅永浩再談收購蘋果:還需要一點時間
· 國產最先進X86處理器KX-6000發布:8核3.0GHz 力壓酷睿i5
· 美國欲剝奪華為在美專利權,涉及至少 3195 件專利
· 四川發生強震 成都提前61秒收到預警
· 聯想 ThinkPad P 系列筆記本預裝 Ubuntu 系統

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

[WPF自定義控件庫]排序、篩選以及高亮

1. 如何讓列表的內容更容易查找

假設有這麼一個列表(數據源在本地),由於內容太多,要查找到其中某個想要的數據會比較困難。要優化這個列表,無非就是排序、篩選和高亮。

改造過的結果如上。

2. 排序

在WPF中要實現數據排序的功能有很多種,例如用Linq,但這種場景的標準做法是使用CollectionViewSource。

CollectionViewSource是一種數據集合的代理類。它有兩個很重要的屬性:

  • Source 是數據源的集合;

  • View 是經過處理后的數據視圖。

看上去感覺是不是很像數據庫里的Table和View的關係?

在這個例子里使用CollectionViewSource排序的代碼如下:

private readonly CollectionViewSource _viewSource;

public HighlightSample()
{
    InitializeComponent();
    _viewSource = new CollectionViewSource
    {
        Source = Employee.AllExecutives
    };

    _viewSource.View.Culture = new System.Globalization.CultureInfo("zh-CN");
    _viewSource.View.SortDescriptions.Add(new SortDescription(nameof(Employee.FirstName), ListSortDirection.Ascending));
    EmployeeElement.ItemsSource = _viewSource.View;
}

這段代碼為CollectionViewSource的Source賦值后,把CollectionViewSource的View作為ListBox的數據源。其中SortDescriptions用於描述View的排序方式。如果包含中文,別忘記將Culture設置為zh-cn

至此排序的功能就實現了。文檔中還提到CollectionViewSource的其它信息:

您可以將集合視圖作為綁定源集合,可用於導航和显示集合中基於排序、 篩選和分組查詢,而無需操作基礎源集合本身的所有頂層。 如果Source實現INotifyCollectionChanged接口,所做的更改引起CollectionChanged事件傳播到View。

由於View不會更改Source,因此每個Source都可以有多個關聯的View。 使用View,可以通過不同方式显示相同數據。 例如,可能希望在頁面左側显示按優先級排序的任務,而在頁面右側显示按區域分組的任務。

3. 篩選

CollectionViewSource的View屬性類型為ICollectionView接口,它提供了Filter屬性用於實現數據的過濾。在這個例子里實現如下:

_viewSource.View.Filter = (obj) => (obj as Employee).DisplayName.ToLower().Contains(FilterElement.Text);

private void OnFilterTextChanged(object sender, TextChangedEventArgs e)
{
    if (_viewSource != null)
        _viewSource.View.Refresh();
}

這段代碼實現了當輸入框的文字改變時刷新View的功能。其中Refresh方法用於重新創建View,也就是刷新視圖。

ICollectionView還提供了一個DeferRefresh函數,這個函數用於進入延遲循環,該循環可用於將更改合併到視圖並延遲自動刷新,在需要多次操作並刷新數據量大的集合時可以用這個函數。

4. 高亮

<TextBox x:Name="FilterElement"
         TextChanged="OnFilterTextChanged"/>
<ListBox Name="EmployeeElement"
         Grid.Row="1"
         Height="200"
         Margin="0,8,0,0">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding DisplayName}"
                           kino:TextBlockService.HighlightText="{Binding ElementName=FilterElement,Path=Text}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

UWP的高亮可以使用TextHighlighter這個類,實現起來很簡單。WPF中的高亮則是使用自定義的TextBlockService.HighlightText附加屬性聲明要高亮的文字,然後將TextBlock的Text替換為處理過的Inlines,使用方式如上。

private static void MarkHighlight(TextBlock target, string highlightText)
{
    var text = target.Text;
    target.Inlines.Clear();
    if (string.IsNullOrWhiteSpace(text))
        return;

    if (string.IsNullOrWhiteSpace(highlightText))
    {
        target.Inlines.Add(new Run { Text = text });
        return;
    }

    while (text.Length > 0)
    {
        var runText = string.Empty;
        var index = text.IndexOf(highlightText, StringComparison.InvariantCultureIgnoreCase);
        if (index > 0)
        {
            runText = text.Substring(0, index);
            target.Inlines.Add(new Run { Text = runText, Foreground = _noHighlightBrush });
        }
        else if (index == 0)
        {
            runText = text.Substring(0, highlightText.Length);
            target.Inlines.Add(new Run { Text = runText });
        }
        else if (index == -1)
        {
            runText = text;
            target.Inlines.Add(new Run { Text = runText, Foreground = _noHighlightBrush });
        }

        text = text.Substring(runText.Length);
    }
}

這是實現代碼。其實用Regex.Split代碼會好看很多,但懶得改了。
本來應該是高亮匹配的文字,但實際使用中發覺把未匹配的文字置灰更好看,就這樣實現了。

5. 結語

這篇文章介紹了使用CollectionViewSource實現的排序、篩選功能,以及使用附加屬性和Inlines實現高亮功能。

不過這樣實現的高亮功能有個問題:不能定義高亮(或者低亮)的顏色,不管在代碼中還是在XAML中。一種可行的方法是參考ToolTipService定義一大堆附加屬性,例如這樣:

<TextBox x:Name="FilterElement" 
         ToolTipService.ToolTip="Filter Text"
         ToolTipService.HorizontalOffset="10"
         ToolTipService.VerticalOffset="10"
         TextChanged="OnFilterTextChanged"/>

這種方式的缺點是這一大堆附加屬性會導致代碼變得很複雜,難以維護。ToolTipService還可以創建一個ToolTip類,把這個類設置為附加屬性的值:

<TextBox x:Name="FilterElement" 
         TextChanged="OnFilterTextChanged">
    <ToolTipService.ToolTip>
        <ToolTip Content="Filter Text"
                 HorizontalOffset="10" 
                 VerticalOffset="10"/>
    </ToolTipService.ToolTip>
</TextBox>

這種方式比較容易維護,但有人可能不明白ToolTipService.ToolTip屬性的值為什麼既可以是文本(或圖片等其它內容),又可以是ToolTip類型,XAML如何識別。關於這一點我在下一篇文章會講解,並且重新實現高亮的功能以支持Style等功能。

也可以參考SearchableTextBlock寫一個高亮的文本框,一了百了,但我希望通過這個有趣的功能多介紹幾種知識。

6. 參考

CollectionViewSource Class (System.Windows.Data) Microsoft Docs

TextBlock.Inlines Property (System.Windows.Controls) Microsoft Docs

A WPF Searchable TextBlock Control with Highlighting WPF

7. 源碼

TextBlockService.cs at master

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Jest — ElasticSearch Java 客戶端

1. 介紹

任何使用過Elasticsearch的人都知道,使用基於rest的搜索API構建查詢可能是單調乏味且容易出錯的。

在本教程中,我們將研究Jest,一個用於Elasticsearch的HTTP Java客戶端。Elasticsearch提供了自己原生的Java客戶端,然而 Jest提供了更流暢的API和更容易使用的接口。

2. Maven 依賴

我們需要做的第一件事是導入Jest庫到我們的POM文件中:

<dependency> <groupId>io.searchbox</groupId> <artifactId>jest</artifactId> <version>6.3.1</version> </dependency> 

Jest的版本是遵循Elasticsearch的主版本號的。
這將確保客戶端和服務端之間的兼容性。

通過包含Jest依賴項,相應的[Elasticsearch庫](https://search.maven.org/search?q=g:org.elasticsearch a:elasticsearch)將被包含為傳遞依賴項。

3. 使用Jest Client

在本節中,我們將研究如何使用Jest client執行Elasticsearch中的常見任務。

要使用Jest client,我們只需使用 JestClientFactory 創建一個 JestClient 對象。這些對象的創建開銷很高,而且是線程安全的,因此我們將創建一個可以在整個應用程序中共享的單例實例:

public JestClient jestClient() { JestClientFactory factory = new JestClientFactory(); factory.setHttpClientConfig( new HttpClientConfig.Builder("http://localhost:9200") .multiThreaded(true) .defaultMaxTotalConnectionPerRoute(2) .maxTotalConnection(10) .build()); return factory.getObject(); } 

這裏將創建一個Jest client,該客戶端連接到本地運行的Elasticsearch。雖然這個連接示例很簡單,但是Jest還完全支持代理、SSL、身份驗證,甚至節點發現。

JestClient 類是通用類,只有少數公共方法。我們將使用的一個主要方法是execute,它接受Action接口的一個實例。Jest客戶端提供了幾個構建器類來幫助創建與Elasticsearch交互的不同操作。

所有Jest調用的結果都是JestResult的一個實例。 我們可以通過調用 issucceeded 方法來檢查是否成功。對於失敗的操作,我們可以調用GetErrorMessage方法來獲取更多詳細信息:

JestResult jestResult = jestClient.execute(new Delete.Builder("1").index("employees").build()); if (jestResult.isSucceeded()) { System.out.println("Success!"); } else { System.out.println("Error: " + jestResult.getErrorMessage()); } 

3.1. 管理索引

檢查索引是否存在,我們使用IndicatesExists操作:

JestResult result = jestClient.execute(new IndicesExists.Builder("employees").build()) 

創建一個索引,我們使用CreateIndex操作:

jestClient.execute(new CreateIndex.Builder("employees").build()); 

這將創建一個具有默認設置的索引。我們可以在創建索引時覆蓋特定的設置:

Map<String, Object> settings = new HashMap<>(); settings.put("number_of_shards", 11); settings.put("number_of_replicas", 2); jestClient.execute(new CreateIndex.Builder("employees").settings(settings).build()); 

使用ModifyAliases操作創建或更改別名也很簡單:

jestClient.execute(new ModifyAliases.Builder( new AddAliasMapping.Builder("employees", "e").build()).build()); jestClient.execute(new ModifyAliases.Builder( new RemoveAliasMapping.Builder("employees", "e").build()).build()); 

3.2. 創建文檔

Jest client使用索引操作類索引或創建新文檔變得容易。Elasticsearch中的文檔只是JSON數據,有多種方法可以將JSON數據傳遞給Jest client 進行索引。

對於這個例子,讓我們使用一個虛構的僱員文檔:

{ "name": "Michael Pratt", "title": "Java Developer", "skills": ["java", "spring", "elasticsearch"], "yearsOfService": 2 } 

表示JSON文檔的第一種方法是使用Java字符串。雖然我們可以手動創建JSON字符串,但我們必須注意正確的格式、大括號和轉義引號字符。因此,更容易的方式是使用一個JSON庫(如Jackson)來構建我們的JSON結構,然後將其轉換為字符串:

ObjectMapper mapper = new ObjectMapper(); JsonNode employeeJsonNode = mapper.createObjectNode() .put("name", "Michael Pratt") .put("title", "Java Developer") .put("yearsOfService", 2) .set("skills", mapper.createArrayNode() .add("java") .add("spring") .add("elasticsearch")); jestClient.execute(new Index.Builder(employeeJsonNode.toString()).index("employees").build()); 

我們還可以使用Java Map 來表示JSON數據,並將其傳遞給索引操作:

Map<String, Object> employeeHashMap = new LinkedHashMap<>(); employeeHashMap.put("name", "Michael Pratt"); employeeHashMap.put("title", "Java Developer"); employeeHashMap.put("yearsOfService", 2); employeeHashMap.put("skills", Arrays.asList("java", "spring", "elasticsearch")); jestClient.execute(new Index.Builder(employeeHashMap).index("employees").build()); 

最後,Jest client 可以接受表示要索引的文檔的任何POJO。假設我們有一個Employee類:

public class Employee { String name; String title; List<String> skills; int yearsOfService; } 

我們可以把這個類的一個實例直接傳遞給Index builder:

Employee employee = new Employee(); employee.setName("Michael Pratt"); employee.setTitle("Java Developer"); employee.setYearsOfService(2); employee.setSkills(Arrays.asList("java", "spring", "elasticsearch")); jestClient.execute(new Index.Builder(employee).index("employees").build()); 

3.3. 讀取文檔

使用Jest client從Elasticsearch訪問文檔有兩種主要方法。首先,如果我們知道文檔ID,我們可以使用get操作直接訪問它:

jestClient.execute(new Get.Builder("employees", "17").build()); 

要訪問返回的文檔,我們必須調用其中一個getSource方法。我們可以將結果作為原始JSON獲取,或者將其反序列化為DTO:

Employee getResult = jestClient.execute(new Get.Builder("employees", "1").build()) .getSourceAsObject(Employee.class); 

訪問文檔的其他方法是使用搜索查詢,這種方式在Jest中是通過搜索操作實現的。

Jest client 支持全部的 Elasticsearch query DSL。 與索引操作一樣,查詢被表示為JSON文檔,並且有多種執行搜索的方法。

首先,我們可以傳遞一個表示搜索查詢的JSON字符串。提醒一下,我們必須確保字符串是正確轉義的,並且是有效的JSON:

String search = "{" + " \"query\": {" + " \"bool\": {" + " \"must\": [" + " { \"match\": { \"name\": \"Michael Pratt\" }}" + " ]" + " }" + " }" + "}"; jestClient.execute(new Search.Builder(search).build()); 

與上面的索引操作一樣,我們可以使用Jackson之類的庫來構建JSON查詢字符串。此外,我們還可以使用原生的Elasticsearch查詢操作API。這樣做的一個缺點是,我們的應用程序必須依賴於完整的Elasticsearch庫。

我們可以使用 getSource 方法來訪問搜索操作中匹配的文檔。然而,Jest還提供了Hit類,它包裝了匹配的文檔並提供有關結果的元數據。 使用Hit類,我們可以訪問每個結果的附加元數據:得分、路由和解釋結果,舉幾個例子:

List<SearchResult.Hit<Employee, Void>> searchResults = jestClient.execute(new Search.Builder(search).build()) .getHits(Employee.class); searchResults.forEach(hit -> { System.out.println(String.format("Document %s has score %s", hit.id, hit.score)); }); 

3.4. 更新文檔

Jest為更新文檔提供了一個簡單的Update操作:

employee.setYearOfService(3); jestClient.execute(new Update.Builder(employee).index("employees").id("1").build()); 

它接受與我們前面看到的索引操作相同的JSON表示,這使得在兩個操作之間共享代碼變得很容易。

3.5. 刪除文檔

從索引中刪除文檔是使用Delete操作完成的。它只需要索引名和文檔ID:

jestClient.execute(new Delete.Builder("17") .index("employees") .build()); 

4. 批量操作

Jest client 同樣支持批量操作。 這意味着我們可以通過同時發送多個操作來節省時間和帶寬。

使用批量操作,我們可以將任意數量的請求組合成單個調用。我們甚至可以將不同類型的請求組合在一起:

jestClient.execute(new Bulk.Builder() .defaultIndex("employees") .addAction(new Index.Builder(employeeObject1).build()) .addAction(new Index.Builder(employeeObject2).build()) .addAction(new Delete.Builder("17").build()) .build()); 

5. 異步操作

Jest client 同樣支持異步操作。 這意味着我們可以使用非阻塞I/O執行上述任何操作。

要異步調用操作,只需使用客戶端的executeAsync方法:

jestClient.executeAsync( new Index.Builder(employeeObject1).build(), new JestResultHandler<JestResult>() { @Override public void completed(JestResult result) { // handle result } @Override public void failed(Exception ex) { // handle exception } }); 

注意,除了(本例中是索引)操作之外,異步流還需要一個JestResultHandler。當操作完成時,Jest client 將調用該對象。該接口有兩個方法—完成和失敗—分別允許處理操作的成功或失敗。

6. 結論

在本教程中,我們簡要介紹了Jest client,一個用於Elasticsearch的RESTful Java客戶端。雖然我們只介紹了它的一小部分功能,但很明顯Jest是一個健壯的Elasticsearch客戶端。它的流暢的構建器類和RESTful接口使其易於學習,並且它對Elasticsearch接口的完全支持使其成為原生客戶端的一個有力的替代方案。

和往常一樣,本教程中的所有代碼示例都在我們的Github頁面上。

原文:https://www.baeldung.com/elasticsearch-jest

作者:Michael Pratt

譯者:huowolf/

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

編程哲理小故事:Tina的運動會方陣

自從接到任務后,Tina一直 煩惱着如何讓這群繁忙又缺乏才藝的程序員在運動會開幕式上做出一個有趣的方陣表演。

 

接到了運動會的方陣表演的任務

時間回到1個月前。

Tina正在工位上繁忙地進行着下一期準備上線系統的測試,這時候老大跑了過來

“Tina,我們公司要舉行運動會,開幕式時有方陣的表演,你幫忙組織下?”

Tina心裏咯噔一下,來了一個苦差。

“方陣?就是像奧運會開幕的時候,運動員在體育場跑道上走,展示風採的那種?”

“對,到時要一個6*6共36人的方陣,到時會在市裡最好的體育館進行,公司的大領導會在主席台看我們表演,為了我們部門,你要加油呀”

“嗯,我會加油的,但36個人不少呀,大家最近都挺忙的,感覺我拉不動他們參加呀”,Tina苦笑道。

“怎麼會呢,我看你在男生中還挺有人氣的,不過你不好意思拉人的話,人的問題我解決,你策劃組織好表演就行”

 

 

 

進展緩慢的方陣表演

方陣的排練一周一次,一周一周的過去了,但整體的效果並沒有得到質的提升,排練時總是湊不齊人,舞蹈也相對複雜,交互很多,大家也沒能很好的磨合,方陣表演這件事幾乎成了Tina的心病

“萬一到時我們成了最差的方陣怎麼辦?我會讓老大很失望的”

今天還沒到排練的時間,但她提前到了場地,擔心着,糾結着。

“方陣排練成怎樣啦?”今天老大居然親自過來了。

“我們今晚繼續排練,但效果還不太理想”

“有遇到什麼困難么?”

“唉,一言難盡,待會你看下把,也提一下意見,這個是我們排練模仿的視頻,你也可以看下”

老大把脈

老大在旁邊看着大夥排練,沒有做任何評價與指導,只是在久不久時用言語激勵一下大家。

今天的排練結束了,大夥都散去,Tina和老大留了下來。

“今天的排練我看了,我們的整齊度還略有欠缺,我們的隊伍就像你描述的一樣,經常會有人臨時有事,沒辦法很好的達到一個整體的效果,磨合度有待提升”

“嗯,對呀,但我已經儘力選擇大家都有空的時間了,但要每個人都有空是在是太難了”

“對,這是一個問題,人越多越難協調與同步,我們通過初中數學就可以知道,協調的難度是隨着人數的增加指數級上漲的”

“嗯嗯,但也沒辦法呀,表演方陣要求就是要這麼多人”,Tina應答着,心裏倒翻了個白眼,好好的幹嘛突然扯上數學

“對,總人數是沒辦法減少,但如果分成多個小組,小組人數比較小,是不是集齊一個小組的人更為簡單點?我們可以讓各個小組各自排練,然後每周一次像現在這樣合起來排練。對於類似規模導致的問題,我們大多都可以用分而治之的策略解決,就像公司里把我們的軟件分成了很多子系統一樣”

“啊?那這樣子要怎麼分組,這是一隻完整的舞蹈呀”

“一個完整的舞蹈必然是每個人的交互的有機結合,但舞蹈里必然會有一群人之間的交互更為密切,我們可以把這群人合成一組,那隻涉及他們內部的舞蹈叫可以他們內部自行排練了。”

“但這個界限不是很清晰把?”

“對,更為密切這個概念確實不是很清晰,所以這也是組織設計的藝術之一,甚至於這是一個需要試錯的過程。我們的軟件編碼一樣,強調高內聚低耦合,讓通訊交互成本降到最低的理念是一致的。但我們不能因為無法達到最優的設計而不去分組,分組可能不是最優,但進行了分組就已經在進步了,我們可以在繼續排練的過程中繼續優化”

“嗯嗯,想不到編碼與方陣排練還有這樣類似的東西,哈哈”

“嗯,世界有很多東西是相似的,像公司的組織架構、各種天體系統、人類的各種器官組成等等都有類似高內聚低耦合的特性存在,所以在我看來一個優秀的有悟性的碼農能將其知識遷移到很多領域”

“好,那我認真研究下視頻,然後對團隊進行分組,一個分組的大小多少合適呢?”

“太陽系有8個行星,地球只有1個月球衛星,這都是由於它們質量決定的。一個分組最大能有多少人這實質是由組長的個人能力決定的,組長能協調的人數就是這個組人數的上限,但當然啦,具體人數要和舞蹈自身的需要相結合。組的大小可以取‘舞蹈分組所需人數’和‘組長最大可管理人數’的較小值。”

“這是讓一個組盡可能大的意思么?”

“對的,一個組應該在可控範圍內盡可能的大,因為分組小意味着分組多,而分組多,協調多個分組就會產生更大的成本。當分組很多,甚至於把一個人看成一個組的時候,就跟你當前面臨的情況一樣。對應於我們軟件領域也是一樣的,對於微服務/組件應該拆成多大這個問題,個人理解組件的大小應該在一個普通程序員能理解、控制的複雜度範圍內,程序員就是這個微服務的類、模塊的組長”

“嗯,明白了。我盡量將其分組大小控制在合適範圍內”

“還有一件事就是,每個小組指定了組長之後,之後組內的排練可以適當程度的放權,這樣的話,你才有更多的精力去考慮我們組與組之間的交互應該怎麼進行。但當然啦,如果你得精力足夠的話,去了解和支持某些個特別舞蹈特別複雜的小組的進展也是挺好的。對於人的能力來說,能做高層設計同時也能做底層執行當然是最理想的,但在程序代碼里,一個模塊既處理高層邏輯又處理底層邏輯的話,是一個不好的表現,這會讓我們的代碼更難以理解”

“哈哈,我本來只想看下怎麼排練好方陣,想不到還順便學習了這麼多編碼知識,感謝老大呀!”

“嗯,剛剛的說的都是些簡單的理論,至於落地到實踐還會複雜的多,細節的處理還是得靠你呀,加油~”

最後的展示

有了一些基礎理論的指導,Tina的方陣隊伍的組織架構在排練中演進,各個小組有了組長能自發的組織組內的訓練,各組的表演水平得到了很大的提升。到集體匯總排練的時候,實際上就是以組的維度進行交互,而非之前的以人維度的磨合,因此集體匯總排練的效率也得到了提升。

“起步走!”,方陣前進的音樂和口令在體育館正式響起,方陣邁着整齊的步伐往主席台走去。Tina看着隊伍,再回頭看來下老大,嘴角泛起了自信的微笑。

 

(最近看了本如何寫小說的書,實踐一下小說風格,哈哈,若本文有錯誤缺陷,請批評負責幫助我進步。若本文對你有所啟發和幫助請不吝點贊轉發。這對我真的很重要,拜託了~)

 

作者簡介

多年金融行業經驗,現為某Top2互聯網銀行高級搬磚工,曾在兩家TOP3股份制商業銀行及一家互金創業公司工作(架構、核心業務主程),EasyTransaction作者,歡迎關注個人公眾號,在這裏我會分享日常工作、生活中對於架構、編碼和業務的思考

 

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Swoole 啟動一個服務,開啟了哪些進程和線程?

目錄

  • 概述
  • 代碼
  • 小結

概述

Swoole 啟動一個服務,開啟了哪些進程和線程?

為了解決這個問題,咱們啟動一個最簡單的服務,一起看看究竟啟動了哪些進程和線程?

然後結合官網運行流程圖,對每個進程和線程進行歸類。

服務啟動后打印出當前 Swoole 版本 和 當前 CPU 核數。

打印 Swoole 版本,是讓大家可以下載這個版本 去運行代碼。

打印 CPU 核數,是因為這個參數下面會用到。

廢話不多說,直接看代碼吧。

代碼

serv.php

<?php

class Server
{
    private $serv;

    public function __construct() {
        $this->serv = new swoole_server("0.0.0.0", 9502);
        $this->serv->set([
            'worker_num'      => 3,
            'task_worker_num' => 3,
        ]);
        $this->serv->on('Start', function ($serv) {
            echo "SWOOLE:".SWOOLE_VERSION . " 服務已啟動".PHP_EOL;
            echo "SWOOLE_CPU_NUM:".swoole_cpu_num().PHP_EOL;
        });
        $this->serv->on('Receive', function ($serv, $fd, $from_id, $data) { });
        $this->serv->on('Task', function ($serv, $task) { });
        $this->serv->on('Finish', function ($serv, $task_id, $data) {});
        $this->serv->start();
    }
}
$server = new Server();

上面的代碼簡單說下,創建了一個 TCP 服務器,啟動了 3 個 worker 進程, 3 個 task 進程,因為啟用了 task 功能,所以必須註冊 onTask、onFinish 2 個事件的回調函數。

咱們運行一下:

使用 ps 查看下:

16390 的父進程是 16389。

16393、16394、16395、16396、16397、16398 的父進程是 16390。

有沒有發現,16391、16392 去哪啦?是不是很奇怪。

再用 pstree 查看下:

出來了吧,16391、16392 是線程 與 16390 進程一個層級。

現在我們了解了,啟動的這個服務使用了 8 個進程、2 個線程。

我們一起看下官方 Swoole Server 的文檔:

https://wiki.swoole.com/wiki/page/p-server.html

看下這張圖:

通過上面的圖,我們可以得到結論:

16389 是 Master 進程。

16390 是 Manager 進程。

16391、16392 是 Reactor 線程。

16393、16394、16395、16396、16397、16398 包括 3 個 Worker 進程,3 個 Task 進程。

小結

一、為什麼是 3 個 Worker 進程、3 個 Task 進程?

因為,在創建服務的時候我們進行了設置 worker_num = 3, task_worker_num = 3。

worker_num 如果不進行設置,默認為 SWOOLE_CPU_NUM,在上面咱們打印出來了,默認為 2,最大不超過,SWOOLE_CPU_NUM * 1000,具體詳情,看官方文檔。

worker_num 文檔:

https://wiki.swoole.com/wiki/page/275.html

task_worker_num 文檔:

https://wiki.swoole.com/wiki/page/276.html

二、為什麼是 2 個 Reactor 線程?它是干什麼的?

因為,Reactor 線程數,默認為 SWOOLE_CPU_NUM,也可以通過 reactor_num 參數進行設置。

reactor_num 文檔:

https://wiki.swoole.com/wiki/page/281.html

它是真正處理 TCP 連接,收發數據的線程。

Reactor線程 文檔:

https://wiki.swoole.com/wiki/page/347.html

三、Reactor、Worker、TaskWorker 的關係是什麼樣的?

一個通俗的比喻,假設Server就是一個工廠,那Reactor就是銷售,接受客戶訂單。而Worker就是工人,當銷售接到訂單后,Worker去工作生產出客戶要的東西。而TaskWorker可以理解為行政人員,可以幫助Worker幹些雜事,讓Worker專心工作。

官方已經解釋的很詳細了,看官方文檔吧:

https://wiki.swoole.com/wiki/page/163.html

如果你想學習 Swoole 可以看下這個 《Swoole 文章匯總(10 篇)》。

本文歡迎轉發,轉發請註明作者和出處,謝謝!

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

代碼榮辱觀-以運用風格為榮,以隨意編碼為恥

編寫代碼的八榮八恥

1. 產品命名:以簡單有趣為榮,以平庸難記為恥。

2. 單個函數:以短小精悍為榮,以冗長費神為恥。

3. 代碼維護:以持續重構為榮,以停滯不前為恥。

4. 編程風格:以運用風格為榮,以隨意編碼為恥。

5. 程序設計:以開關上線為榮,以自信編碼為恥。

6. 接口定義:以用戶易用為榮,以複雜歧義為恥。

7. 斷言分支:以實時報警為榮,以忽略分支為恥。

8. 監控報警:以定時調整為榮,以放棄維護為恥。

5Why分析

(一)

Q: 誰需要學習編寫代碼的八榮八恥?

A: 項目中的開發人員、項目經理、架構師

(二)

Q: 為什麼學習編寫代碼的八榮八恥?

A: 可以作為實際代碼編寫和review(複查)的指導規範

(三)

Q: 什麼人什麼時候需要review代碼?

A: 

對開發人員來說,需要在時間允許的條件下定期的review自己和別人的代碼,加深對項目的整體理解。對自己的成長做總結。如果過了一段時間,還看到自己之前的代碼,覺得寫的很好的話,就需要質疑自己的成長,更努力的學習了。

對於項目經理和架構師來說,鼓勵所有上線的功能都每周抽出時間來做個組內review。或者定期抽取一些模塊做review。鼓勵大家重構代碼。在review過程中,作為領導者需要對大家有輸出,對代碼怎麼寫是更好的有一些理論基礎。這時候就需要使用編寫代碼的八榮八恥作為review的指導規範。

(四)

Q: 怎麼用作review的指導規範?

A: 八榮八恥中不但介紹了每個條目的意義,而且有通俗易懂的代碼實例便於和實際中的代碼在頭腦中做對比。文中明確的指出了哪些寫法是鼓勵的、哪些是不鼓勵的,是基於什麼理由不鼓勵這樣做。

(五)

Q: 編寫代碼的八榮八恥對於高可用有什麼意義?

A: 我利用美團的內部運維平台對自己參與過的項目可用性做過統計。將影響可用性的case(具體事件)分成:開發因素和設計因素。開發因素包括系統bug、開發不規範、上線不規範、監控報警不及時(影響可用性的恢復時長)等由於具體開發者在設計階段覆蓋不到的階段發生的。設計因素包括機器故障、網絡中斷、異常流量、中間件故障等可以通過設計做容災的。結果95%以上的可用性問題都是開發因素造成的。編寫代碼的八榮八恥是對避免開發因素產生可用性問題的指導規範。

 

編程風格:以運用風格為榮,以隨意編碼為恥

引子

在工作中,經常發現有些程序員用面向對象的語言寫出了面向過程的代碼而自己並沒有感覺到:

前面提到有個java軟件工程師,叫Margaret。她對工作有三個要求:錢多、有趣、離家近。HR想針對這些要求和她具體溝通,問她最低標準是什麼。每一項最低要求回復一個星級。

 

星級

錢多

有趣

離家近

1

年薪10萬

出差+旅遊占工時1%

40公里

☆☆

2

年薪20萬

出差+旅遊占工時10%

20公里

☆☆☆

3

年薪50萬

出差+旅遊占工時20%

10公里

☆☆☆☆

4

年薪100萬

出差+旅遊占工時50%

2公里

☆☆☆☆☆

5

年薪500萬

出差+旅遊占工時80%

1公里

Margaret在外地,所以用了一個常用的數據交換格式json給HR回復如下:

{"moreMoney”:4,"moreFun”:2,"closerToHome”:3}

拿到這個回復時面向過程的解析方式是這樣寫的:

Map json = (HashMap) JSONUtils.parse("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}");
int moreMoney = (int)json.get("moreMoney");int moreFun = (int)json.get("moreFun");int closerToHome = (int)json.get("closerToHome");

接收方將接收到的數據轉成了json,代碼里一堆get完成了功能。為什麼說這是面向過程的呢?map是一種數據結構,沒有直接的業務意義。功能實現了,表達的意義卻不清晰。

這段代碼更好的一個實現方式是將接收的數據結構定義成一個對象,在java里可以使用jackson等工具直接將json轉成有業務含義的對象。

ObjectMapper objectMapper = new ObjectMapper();
Requirement requirement = objectMapper.readValue("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}",JavaSoftwareEngineerMargaretRequirement.class);

這樣做,HR拿到的requirement不是一列列数字,需要自己對核對每一項都是什麼意思。而是一個有完整語義的對象,利於理解。而以這種思路來進行編寫的代碼我經常稱他們叫面向對象風格的代碼。

WHY

來看一段寫赤壁山旅行的文章:

今天有幸登上赤壁山,看到這山上的景物,不禁想起了當前戰場上廝殺的場面。想起當年要不是周瑜運氣好,大冬天颳起了東風,恐怕吳國就被曹操滅了。

再來看唐代詩人杜牧經過赤壁山這個著名的古戰場,有感於三國時代的英雄成敗而寫下的《赤壁》:

折戟沉沙鐵未銷,自將磨洗認前朝。

東風不與周郎便,銅雀春深鎖二喬。

前兩句意思是在沙子底下找到一隻斷戟,磨洗之後發現“made in 赤壁”。讀者讀了這兩句不禁會聯想起當前戰場上廝殺的場面吧。

后兩句意思是要不是周瑜運氣好,大冬天颳起了東風。那孫權的老婆大喬和周瑜的老婆小喬這兩位絕世美女都要被曹操這個色老頭關進銅雀台了。因為曹操久仰大小喬的美貌,提前為二人修築銅雀台,作為打敗吳國的戰利品。

杜牧隻字未提戰場和如果沒有東風的運氣,將會亡國的下場。但是讀者卻能心領神會,印象深刻。這是因為杜牧採用了以小見大的風格手法。

代碼與代碼的區別如同文章與文章的區別。能否讓讀者以更短的時間、更輕鬆的讀懂?代碼是給人整體感還是噁心感?這些都決定了代碼的可維護性。而它和系統可用性、穩定性的最直接關係在工作中非常常見:“爺爺的!這是誰寫的代碼這麼爛?忍不了了,老子不幹了。”而這個代碼的作者之所以離職也是因為忍受不了自己的爛代碼。頻繁的人員更替,新接手人員要有學習的成本。成本就包括要踩坑來加深對系統的理解。

HOW

除了開頭提到的面向對象的風格,編寫java代碼時下面三種風格也很常見。

1.fluent風格

fluent風格的代碼常以Builder結尾。比如StringBuilder就是典型的fluent風格。定義一個人的對象,這個對象使用fluent風格代碼這麼寫:

public class Person {    private String name;    private int armCount=2;//胳膊數默認為2 private int legCount=2;//腿數默認為2
    public static Person builder() {        return new Person.Builder(); }
    public Person setName(String name) {        this.name = name;        return this; }
    public Person armCount(int armCount) {        this.armCount = armCount;        return this; }
    public void legCount(int legCount) {        this.legCount = legCount;    }}

如上,就是每次給對象賦屬性的時候同時返回對象本身。這樣調用的時候:

Person.builder().name("Jane").armCount(2).legCount(2);

這樣寫的好處是比每個屬性都用一句set簡潔。在屬性多的時候,用構造函數。調用時容易表達不清楚屬性的含義。方法名起到了解釋的作用。現在流行的做法是代碼即註釋,註釋不用在每個方法都寫。這時候能表達自身意義的代碼就更加重要。注意:我們也可以保留setXXX、getXXX的命名規範,因為jackson等序列化反序列化的組件會根據set、get方法對參數賦值,上面的明明風格在序列化時會有問題。

當然,這個類也可以直接用lombok註解得到。

@Data@Buildrpublic class Person {    private String name;    private int armCount=2;//胳膊數默認為2    private int legCount=2;//腿數默認為2}

2.lambda函數式編程風格

lambda函數式編程比傳統的命令式編程更加簡潔。比如:現在有一群人。

List<Person> personList = Lists.newArrayList();
personList.add(Person.builder().name("Jane"));personList.add(Person.builder().name("Joe").armCount(1));personList.add(Person.builder().name("Stark").legCount(1));

要找出所有的殘疾人:

List<Person> disabledPersonList = Lists.newArrayList();for(Person person : personList) {    if(person.legCount()!=2 || person.armCount()!=2) {        disabledPersonList.add(person);    }}

或者使用lambda函數式編程:

List<Person> disabledPersonList = personList.stream().filter(person -> person.legCount()!=2 || person.armCount()!=2).collect(Collectors.toList());

3.設計模式風格

1995年,GoF(Gang of Four,四人幫)合作出版了《設計模式:可復用面向對象軟件的基礎》一書,共收錄了23中設計模式,人稱“GoF設計模式”。這23種設計模式的本質是面向對象設計原則的實際運用,是一種最佳實踐。

在各種java源碼中,經常看到以設計模式命名的類名和方法名。在我們日常編碼中,設計模式也非常實用。設計模式風格的例子請參考:平時代碼中用不到設計模式?Are you kidding me?

總結

寫有技術追求的代碼

 

相關閱讀

編寫代碼的「八榮八恥」- 以用戶易用為榮,以複雜歧義為恥

編寫代碼的「八榮八恥」- 以開關上線為榮,以自信編碼為恥

編寫代碼的「八榮八恥」(上篇)

 

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Python 爬蟲從入門到進階之路(四)

之前的文章我們做了一個簡單的例子爬取了百度首頁的 html,我們用到的是 urlopen 來打開請求,它是一個特殊的opener(也就是模塊幫我們構建好的)。但是基本的 urlopen() 方法不支持代理、cookie等其他的HTTP/HTTPS高級功能,所以我們需要用到 Python 的 opener 來自定義我們的請求內容。

具體步驟:

  1. 使用相關的 Handler處理器 來創建特定功能的處理器對象;
  2. 然後通過 build_opener()方法使用這些處理器對象,創建自定義opener對象;
  3. 使用自定義的opener對象,調用open()方法發送請求。

我們先來回顧一下使用 urlopen 獲取百度首頁的 html 代碼實例:

 1 # 導入urllib 庫
 2 import urllib.request
 3 
 4 # url 作為Request()方法的參數,構造並返回一個Request對象
 5 request = urllib.request.Request("http://www.baidu.com")
 6 # Request對象作為urlopen()方法的參數,發送給服務器並接收響應
 7 response = urllib.request.urlopen(request)
 8 # 類文件對象支持 文件對象的操作方法,如read()方法讀取文件全部內容,返回字符串
 9 html = response.read().decode("utf-8")
10 # 打印字符串
11 print(html)

接下來我們看一下使用 opener 的處理方式:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler()
 5 
 6 # 構建一個HTTPSHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler()
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

 

在上面的第一段代碼中,我們是通過直接  import urllib.request   來導入我們需要的包,這樣當我們要使用時需要   urllib.request   來使用,第二段代碼我們是通過  from urllib import request  來導入我們需要的包,這樣當我們使用時直接  request 來使用就可以了。

第一段代碼在前面的文章中我們已經說過了,這裏就不多做解釋了。

第二段代碼中,我們使用了 opener 的方法來處理我們的請求,這樣我們就可以對代理,cookie 等做進一步的操作,後續文章會講到。最終結果如下:

在  http_handler = request.HTTPHandler() 中,我們還可以添加一個  debuglevel=1 參數,會將 Debug Log 打開,這樣程序在執行的時候,會把收包和發包的報頭在屏幕上自動打印出來,方便調試,有時可以省去抓包的工作。

代碼如下:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler(debuglevel=1)
 5 
 6 # 構建一個HTTPHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler(debuglevel=1)
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

輸出結果如下:

可以看出在響應結果的時候會為我們打印輸出一些請求信息。

 

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

一次線上Redis類轉換異常排查引發的思考

之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:

XxxClass1 cannot be cast to XxxClass2

已知信息如下:

  • 該異常不是必現的,偶爾才會出現;
  • 出現該異常后重啟應用或者過一會就好了;
  • 序列化協議使用了hessian。

因為偶爾出現,首先看了報異常那塊業務邏輯是不是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在Redis讀超時之後才出現的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發現獲取/釋放redis連接如下代碼:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedisPool.returnResourceObject(jedis);
10     }
11 }

初步認定原因為:發生了讀寫超時的連接,直接歸還給連接池,下次使用該連接時讀取到了上一次Redis返回的數據。因此本地驗證下,示例代碼如下:

 1 @Data
 2 @NoArgsConstructor
 3 @AllArgsConstructor
 4 static class Person implements Serializable {
 5     private String name;
 6     private int age;
 7 }
 8 @Data
 9 @NoArgsConstructor
10 @AllArgsConstructor
11 static class Dog implements Serializable {
12     private String name;
13 }
14 
15 public static void main(String[] args) throws Exception {
16     JedisPoolConfig config = new JedisPoolConfig();
17     config.setMaxTotal(1);
18     JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456");
19 
20     Jedis jedis = jedisPool.getResource();
21     jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
22     jedis.set("key2".getBytes(), serialize(new Dog("tom")));
23     jedisPool.returnResourceObject(jedis);
24 
25     try {
26         jedis = jedisPool.getResource();
27         Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
28         System.out.println(person);
29     } catch (Exception e) {
30         // 發生了異常之後,未對該連接做任何處理
31         System.out.println(e.getMessage());
32     } finally {
33         if (jedis != null) {
34             jedisPool.returnResourceObject(jedis);
35         }
36     }
37 
38     try {
39         jedis = jedisPool.getResource();
40         Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
41         System.out.println(dog);
42     } catch (Exception e) {
43         System.out.println(e.getMessage());
44     } finally {
45         if (jedis != null) {
46             jedisPool.returnResourceObject(jedis);
47         }
48     }
49 }

連接超時時間設置2000ms,為了方便測試,可以在redis服務器上使用gdb命令斷住redis進程(如果redis部署在Linux系統上的話,還可以使用iptable命令在防火牆禁止某個回包),比如在執行 jedis.get("key1".getBytes() 代碼前,對redis進程使用gdb命令斷住,那麼就會導致讀取超時,然後就會觸發如下異常:

Person cannot be cast to Dog

既然已經知道了該問題原因並且本地復現了該問題,對應解決方案是,在發生異常時歸還給連接池時關閉該連接即可(jedis.close內部已經做了判斷),代碼如下:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedis.close();
10     }
11 }

至此,該問題解決。注意,因為使用了hessian序列化(其包含了類型信息,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含對象屬性信息),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化后的對象屬性為空或者屬性值混亂,使用時會導致問題,並且這種問題因為沒有報異常所以更不容易發現。

 

既然說到了Redis的連接,要知道的是,Redis基於RESP(Redis Serialization Protocol)協議來通信,並且通信方式是停等方式,也就說一次通信獨佔一個連接直到client讀取到返回結果之後才能釋放該連接讓其他線程使用。小夥伴們可以思考一下,Redis通信能否像dubbo那樣使用單連接+序列號(標識單次通信)通信方式呢?理論上是可以的,不過由於RESP協議中並沒有一個”序列號”的字段,所以直接靠原生的通信方法來實現是不現實的。不過我們可以通過echo命令傳遞並返回”序列號”+正常的讀寫方式來實現,這裏要保證二者執行的原子性,可以通過lua腳本或者事務來實現,事務方式如下:

MULTI
ECHO "唯一序列號"
GET key1
EXEC

然後客戶端收到的結果是一個 [ "唯一序列號", "value1" ]的列表,你可以根據前一項識別出這是你發送的哪個請求。

為什麼Redis通信方式並沒有採用類似於dubbo這種通信方式呢,個人認為有以下幾點:

  • 使用停等這種通信方式實現簡單,並且協議字段盡可能緊湊;
  • Redis都是內存操作,處理性能較強,停等協議不會造成客戶端等待時間較長;
  • 目前來看,通信方式這塊不是Redis使用上的性能瓶頸,這一點很重要。

 

推薦閱讀:

  • 別再問我ConcurrentHashMap了
  • 分佈式鎖設計與實現

  • ConcurrentHashMap竟然也有死循環問題?

  • 你的ThreadLocal線程安全么

 歡迎小夥伴掃描以下二維碼閱讀更多精彩好文。

 

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高級Java工程師必備 —– 深入分析 Java IO (一)BIO

BIO編程

最原始BIO

網絡編程的基本模型是C/S模型,即兩個進程間的通信。

服務端提供IP和監聽端口,客戶端通過連接操作想服務端監聽的地址發起連接請求,通過三次握手連接,如果連接成功建立,雙方就可以通過套接字進行通信。

傳統的同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口;Socket負責發起連接操作。連接成功后,雙方通過輸入和輸出流進行同步阻塞式通信。
最原始BIO通信模型圖:

存在的問題:

  • 同一時間,服務器只能接受來自於客戶端A的請求信息;雖然客戶端A和客戶端B的請求是同時進行的,但客戶端B發送的請求信息只能等到服務器接受完A的請求數據后,才能被接受。(acceptor只有在接受完client1的請求后才能接受client2的請求)
  • 由於服務器一次只能處理一個客戶端請求,當處理完成並返回后(或者異常時),才能進行第二次請求的處理。很顯然,這樣的處理方式在高併發的情況下,是不能採用的。

一請求一線程BIO

那有沒有方法改進呢? ,答案是有的。改進后BIO通信模型圖:

此種BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理沒處理完成后,通過輸出流返回應答給客戶端,線程銷毀。即典型的一請求一應答通宵模型。

代碼演示

服務端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServer {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               SocketServerThread socketServerThread = new SocketServerThread(socket);
               new Thread(socketServerThread).start();
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServer.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThread implements Runnable {
   private Socket socket;
   public SocketServerThread (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

客戶端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch;

public class BioSocketClient{
   public static void main(String[] args) throws Exception {
       Integer clientNumber = 20;
       CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

       // 分別開始啟動這20個客戶端,併發訪問
       for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
           ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
           new Thread(client).start();
       }

       // 這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (BioSocketClient.class) {
           BioSocketClient.class.wait();
       }
   }
}



/**
* 一個ClientRequestThread線程模擬一個客戶端請求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable {


   private CountDownLatch countDownLatch;

   /**
    * 這個線程的編號
    * @param countDownLatch
    */
   private Integer clientIndex;

   /**
    * countDownLatch是java提供的同步計數器。
    * 當計數器數值減為0時,所有受其影響而等待的線程將會被激活。這樣保證模擬併發請求的真實性
    * @param countDownLatch
    */
   public ClientRequestThread(CountDownLatch countDownLatch , Integer clientIndex) {
       this.countDownLatch = countDownLatch;
       this.clientIndex = clientIndex;
   }

   @Override
   public void run() {
       Socket socket = null;
       OutputStream clientRequest = null;
       InputStream clientResponse = null;

       try {
           socket = new Socket("localhost",8083);
           clientRequest = socket.getOutputStream();
           clientResponse = socket.getInputStream();

           //等待,直到SocketClientDaemon完成所有線程的啟動,然後所有線程一起發送請求
           this.countDownLatch.await();

           //發送請求信息
           clientRequest.write(("這是第" + this.clientIndex + " 個客戶端的請求。 over").getBytes());
           clientRequest.flush();

           //在這裏等待,直到服務器返回信息
          System.out.println("第" + this.clientIndex + "個客戶端的請求發送完成,等待服務器返回信息");
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           int realLen;
           String message = "";
           //程序執行到這裏,會一直等待服務器返回信息(注意,前提是in和out都不能close,如果close了就收不到服務器的反饋了)
           while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
               message += new String(contextBytes , 0 , realLen);
           }
           //String messageEncode = new String(message , "UTF-8");
           message = URLDecoder.decode(message, "UTF-8");
           System.out.println("第" + this.clientIndex + "個客戶端接收到來自服務器的信息:" + message);
       } catch (Exception e) {

       } finally {
           try {
               if(clientRequest != null) {
                   clientRequest.close();
               }
               if(clientResponse != null) {
                   clientResponse.close();
               }
           } catch (IOException e) {

           }
       }
   }
}   

存在的問題:

  • 雖然在服務器端,請求的處理交給了一個獨立線程進行,但是操作系統通知accept()的方式還是單個的。也就是,實際上是服務器接收到數據報文後的“業務處理過程”可以多線程,但是數據報文的接受還是需要一個一個的來(acceptor只有在接受完client1的請求后才能接受client2的請求),下文會驗證。
  • 在linux系統中,可以創建的線程是有限的。我們可以通過cat /proc/sys/kernel/threads-max命令查看可以創建的最大線程數。當然這個值是可以更改的,但是線程越多,CPU切換所需的時間也就越長,用來處理真正業務的需求也就越少。
  • 另外,如果您的應用程序大量使用長連接的話,線程是不會關閉的。這樣系統資源的消耗更容易失控。

偽異步I/O編程

為了改進這種一連接一線程的模型,我們可以使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱為“偽異步I/O模型“。

偽異步I/O模型圖:

代碼演示

只給出服務端,客戶端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServerThreadPool {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  
   //線程池 懶漢式的單例  
   private static ExecutorService executorService = Executors.newFixedThreadPool(60);  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               //當然業務處理過程可以交給一個線程(這裏可以使用線程池),並且線程的創建是很耗資源的。
               //最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
               SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
               executorService.execute(socketServerThreadPool);
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServerThreadPool.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThreadPool implements Runnable {
   private Socket socket;
   public SocketServerThreadPool (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

服務器端的執行效果

在 Socket socket = serverSocket.accept(); 處打了斷點,有20個客戶端同時發出請求,可服務端還是一個一個的處理,其它線程都處於阻塞狀態

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

阻塞的問題根源

 那麼重點的問題並不是“是否使用了多線程、或是線程池”,而是為什麼accept()、read()方法會被阻塞。API文檔中對於 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服務器線程發起一個accept動作,詢問操作系統 是否有新的socket套接字信息從端口xx發送過來。

注意,是詢問操作系統。也就是說socket套接字的IO模式支持是基於操作系統的,那麼自然同步IO/異步IO的支持就是需要操作系統級別的了。如下圖:

 如果操作系統沒有發現有套接字從指定的端口xx來,那麼操作系統就會等待。這樣serverSocket.accept()方法就會一直等待。這就是為什麼accept()方法為什麼會阻塞:它內部的實現是使用的操作系統級別的同步IO。

  • 阻塞IO 和 非阻塞IO
    這兩個概念是程序級別的。主要描述的是程序請求操作系統IO操作后,如果IO資源沒有準備好,那麼程序該如何處理的問題:前者等待;後者繼續執行(並且使用線程一直輪詢,直到有IO資源準備好了)
  • 同步IO 和非同步IO
    這兩個概念是操作系統級別的。主要描述的是操作系統在收到程序請求IO操作后,如果IO資源沒有準備好,該如何處理相應程序的問題:前者不響應,直到IO資源準備好以後;後者返回一個標記(好讓程序和自己知道以後的數據往哪裡通知),當IO資源準備好以後,再用事件機制返回給程序。

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

【讀後感1】SQL2008技術內幕- SQL邏輯查詢處理

引言觀點

1. 編程語言日新月異,但是從沒有人否定sql 在現代編程中的巨大作用和 持續的可利用性。SQL以對人類友好的閱讀體驗提供數據查詢能力( 相比其他編程語言 ), 同時在各種數據庫平台中,基礎SQL元素是相同或大同小異的,

從我們最早接觸的SQL,Mysql到公司大數據impala 支持SQL, Es也提供類似SQL的查詢, 阿里提出SQLFlow AI框架, SQL的生命力極其頑強。

2. 在我近6年的開發生涯中,確實覺得SQL語言沒有得到開發者足夠的重視,尤其是流行的ORM概念使得了編寫SQL機會越來越少,使用ORM映射框架是需要一些代碼的, 另外ORM只能用於基礎的關係型二維查詢,對於複雜的查詢無能為力,部分工作可通過巧妙的SQL查詢,存儲過程,觸發器來完成。

3. SQL編程有許多獨特之處: 面向集合的思維方式、 查詢元素的邏輯處理順序、三值邏輯(three value logic),理解不透的話在實際編寫SQL時會產生很多錯誤的寫法、性能低下的代碼。

1987年SQL稱為ISO標準,ANSI宣布該語言發音為“ess kyoo ell”, 但由於歷史原因,很多專業人士還是將SQL發音成sequel,而且從英文習慣上,sequel發音更為流暢。 互聯網如此之大,容得下不同的聲音。

結合《SQL2008 技術內幕 T-SQL查詢》和工作經驗,提煉出Web開發者需要熟練掌握以下SQL查詢。

  • SQL邏輯查詢處理

  • SQL 面向集合的思維方式

不敢妄自宣稱是高級編程經驗, 只是認為Web開發者應該Cover這些常見SQL用法。  

SQL邏輯查詢處理

  開發者、數據分析師每天都在寫【SELECT 列a,聚合函數 FROM 表名 WHERE 過濾條件 GROUP BY 列a HAVING 篩選條件】這樣的查詢語句。

  SQL與其他語言不同的最明顯特徵是代碼的處理順序,大多數編程語言中,代碼是按照編寫順序來處理的,但在SQL中第一個要處理的子句是FROM子句,儘管SELECT語句第一個出現,但基本都在最後處理。

       每一步都會生成一個虛擬表,該虛擬表會作為下一步的輸入, 這些虛擬表對於調用者(客戶端應用程序或者外部查詢)都是不可用的,只有最後一步生成的虛擬表才會返回給調用者,這種形態可對比LINQ理解。

 

①FROM        FROM階段負責標識表或要查詢的表,如果指定了表運算符(JOIN, APPLY,PIVOT,UNPIVOT ),還要進行表運算符的處理。

              例如:表聯接運算中涉及的階段是 笛卡爾積、ON篩選器和 添加外部行,FROM階段生成虛擬表VT1.

②WHERE           這個階段根據在WHERE子句中出現的謂詞對VT1中進行篩選,只有讓謂詞計算結果為TRUE的行,才會插入VT2中。

③GROUP BY     按照GROUP BY 子句中指定的列名列表,對VT2中的行進行分組,生成VT3, 最終每個分組只有一個結果行。

④HAVING          根據HAVING子句中出現的謂詞,對VT3中行記錄進行篩選,只有讓謂詞結果為TRUE的行記錄,才會進入VT4, Having 篩選器是唯一可用於分組數據的篩選器。

⑤SELECT    處理SELECT子句中字段(某些字段可能進行一些操作,形成新的字段),形成虛擬表VT5

⑥ORDER BY  根據ORDER BY子句中指定的列名列表,對VT5 中行進行排序,輸出最後結果。

 

着重理解:

  • 第一步的FROM表運算, 一般情況下是TABLE、TempTable,CTE, 還有可能是表運算符(我們常用的是聯接運算符), 所以不能單純認為FROM後面是一個表結構。

  • 表聯接運算符  ON篩選器 與 WHERE有所不同,若採用OUTER JOIN, 應用ON篩選出來的結果不一定是此階段最終結果,因為涉及【添加外部行】, 而WHERE過濾出的結果是此階段的最終結果。 

  • GROUP BY x,y 意味着將(x,y)作為一個整體來分組

  • 有SELECT 和WHERE的時候,先執行WHERE,再執行SELECT,這樣就很容易理解以下SQL的業務含義:

SELECT page_original_url,server_session_id,access_order-1 as access_order FROM PageViewMeasure WHERE access_order >= 2
--- 查詢過濾出access_order>=2的基礎數據集,然後將(原列值-1)重命名為原列名,重命名的用法業務上也許是為了形成新的SQL聯接

SELECT keyword_id,Coalesce(full_keywords,keywords)  as  not_nullField,profile_id,session_server_time,count (*) over () as Count FROM pageview 
WHERE  profile_id =5254 and keyword_id != '-' and day =20181008  and  not_nullField !='-'   
ORDER BY session_server_time 
--- SQL報錯:Could not resolve column/field reference: 'not_nullfield' 也容易理解了:先執行where, 執行where的時候not_nullField字段還沒有形成
  • ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY PageViewServerTime)排名函數中ORDER BY 與SQL語句最後的ORDER BY 同時存在,哪個ORDER BY起最終排序作用?

SELECT page_original_url as name,page_view_server_time, ROW_NUMBER() OVER(PARTITION BY page_original_url ORDER BY page_view_server_time ) as partition_rank ,wd3_page_duration
  FROM pageview WHERE profile_id=5198 AND day between 20190616 and 20190621   
  ORDER  BY wd3_page_duration desc 
  LIMIT 100

  可以認為 ROW_NUMBER() OVER(PARTITION BY col1 ORDER BY col2) as rank 本質上還是產生一個列值,實際是對應以上的第⑤步,因此SQL最後的ORDER BY起最終排序作用,例證如下:

           某些轉載文章寫有: 以上over函數里的分組及排序的執行晚於“where,group by,order by”的執行 ,這樣的結論是錯誤的

  • 若存在LIMIT子句,則LIMIT子句必須在ORDER BY 語法之後

 

      上圖來自《SQL技術內幕T-SQL查詢》邏輯查詢處理一章

 

 這裏拋出一個困惑點:

  在FROM子句中,若存在JOIN表運算符, 可能會按照 【計算笛卡爾積】 【應用ON篩選】【添加外部行】的順序來完成 JOIN的過程, 但是試想一下: 如果兩個表都為大表,先計算笛卡爾積,再篩選 豈不很費內存,

  我也搜索了很多資料,某些資料認為先進行【ON篩選】再進行【JOIN】運算:

https://www.cnblogs.com/liuzhendong/archive/2011/10/27/2226805.html

https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008/ms189499(v=sql.100)

我更願意相信《SQL技術內幕T-SQL查詢》書中所言:

本章描述的某些邏輯處理步驟可能看起來非常低效,但要記住, 在實踐中,
查詢的實際物理處理可能與邏輯處理有很大不同

在SQL Server 中負責生成實際工作計劃的組件是查詢優化器,以何種順序訪問表、使用什麼訪問方法和索引,應用哪種聯接算法等都是查詢優化器來決定的,優化器會生成多個有效執行計劃並選擇一個開銷最低的計劃。

邏輯查詢處理中各個階段都有其特定的順序,而優化器缺經常可以在它生成的物理執行計劃中走捷徑。

 我們思考一個簡單的SQL:

SELECT * FROM pageview LEFT JOIN  share  ON  pageview.share_pv_id = share.page_view_id
 WHERE pageview.profile_id =5313 AND pageview.day  between 20190615 and 20190624

若實際物理查詢按照上面描述的 邏輯查詢處理, 先進行 FROM 子句中的 LEFT JOIN 計算,再進行 WHERE過濾, 根本無法查出(在FROM子句可能內存就爆滿了)

  現在我們能夠查詢出來,能夠印證 實際物理查詢確實與邏輯查詢處理有很大不同。 

PS: 以上是個人從現象上推斷書中理論,對於實際物理查詢處理並沒有理論支持,若網友們有相關資料,可留言給我。

 

作者: JulianHuang

感謝您的認真閱讀,如有問題請大膽斧正;覺得有用,請下方或加關注。

本文歡迎轉載,但請保留此段聲明,且在文章頁面明顯位置註明本文的作者及原文鏈接。

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"