小米11 正式發表:首款搭載高通 S888 5G 旗艦手機降臨!售價約 17,217 元起_網頁設計公司

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

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

今(28)日晚間,小米搶在 2020 年的倒數幾天發表了「小米11」新機,也是全球首款搭載高通最新的 Snapdragon 888 行動平台的 5G 旗艦手機。除了強悍的性能,這次包括螢幕、玻璃、音效都有明顯升級,相機配置 1.08 億像素三鏡頭主相機維持旗艦水準。至於售價則是比起預估便宜許多,小米11 起售價僅約 17,217 元起。

小米11 正式發表:首款搭載高通 S888 5G 旗艦手機降臨!售價約 17,217 元起

在迎接 2021 年之前,小米搶在 2020 年再次發表新一代的小米旗艦新機「小米11」,也是之前就已知道是首款搭載高通 S888 新旗艦處理器的機種。雖然小米11 的外型在之前就已經提早被洩露,不過實際發表的機身尺厚度和重量還是比預期來得更吸引人。小米11 在機身厚度僅 8.06mm(比起前一代薄了 0.9mm)、機身重量僅 196g(比起前一代減輕 12g):

效能

小米11 為全球首款搭載 Qualcomm Snapdragon 888 5G 行動平台、配備最高 12GB LPDDR5 RAM、256GB ROM:

小米公布實測安兔兔跑分達到 745,942 分:

Geekbench 效能實測單核跑分 1135 分、多核跑分 3818 分:

Wi-Fi 連線方面,小米11 採用全新的 WiFi6 增強版,在網速比起之前 WiFi6 標準版快 2.1 倍、最大下載速度高達 3.5Gbps:

螢幕

螢幕方面,小米11 配備 6.81 吋 2K(3200×1440 WQHD)AMOLED 四曲面柔性螢幕,螢幕支持最高 120Hz 螢幕更新率、480Hz 觸控採樣率, 1500nit 峰值亮度、480Hz  觸控採樣率、5,000,000:1 對比度,螢幕也擁有 100% P3 色域和 HDR10+ 認證。螢幕也支持自動亮度調整,小米11 前後配置雙感光感應器,可支持 8192 級亮度調節。

螢幕採用 E4 發光材料,也獲得 DisplayMate 的 A+ 頂級評價:

此外,小米11 螢幕保護的玻璃則採用康寧最新大猩猩玻璃 Gorilla Glass Victus 保護,抗摔性相較前代提升 1.5倍、耐刮性能提升 2 倍。

相機

相機方面,小米11 配備 1.08 億像素三鏡頭主相機,分別為 1.08 億像素(1 / 1.33″超大感光元件)、1300 萬像素 123° 超廣角鏡頭、 500 萬像素 50mm 微距長焦鏡頭,前置鏡頭則配備 2000 萬像素自拍相機。

1.08 億像素的 1/1.33″ 超大感光元件尺寸為 iPhone 12 Pro Max 的兩倍:

也支援 OIS 四軸光學防手震:

鏡頭採用 7P 光學鏡片:

有著 Snapdragon 888 強大的性能,小米11 在拍攝 108MP 的相片速度相較過去提升 30% 的表現:

除了支持最高 8K 30fps 錄影外,在影片錄製也加入超級夜景模式:

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

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

電量

電量方面,小米11 內建等效 4600mAh 大電池,支持 55W 有線閃充、50W 無線閃充以及10W 反向無線充電, 55W 有線閃充可在 45 分鐘充滿 100% 電量、50W 無線閃充可在 53 分鐘充滿 100% 電量。

音效&其他硬玉

在這次小米 找來了 harman/kardon 金耳朵團隊為小米11 的立體聲雙揚聲器調校音效,同時音效方面也通過 Hi-Res 認證並支持藍牙音效共享,可同時與兩組藍牙耳機連接使用:

震動方面小米11 配備超大橫向線性馬達,震動量更大、啟停時間更短:

小米11 配備超薄螢幕下指紋感應器,也首次支持指紋檢測心率功能,只需在螢幕指紋感應區即可檢測心率:

配色

小米11 在機身背蓋分為「磨砂玻璃」與「素皮(環保皮革)」兩種材質處理:

「磨砂玻璃版」提供黑色、白色、藍色三種顏色選擇:

另外在「素皮(環保皮革)」版則推出卡其色、煙紫色兩種顏色選擇:

另外,小米11 也推出限量的雷軍簽名版:

售價

小米為響應 Apple 的環保政策,今日也終於在小米11 取消附贈充電器甚至連同 Type-C 傳輸線都取消附贈。然而,小米日前在微博預告這項消息後也引起眾人討論,雷軍表示原先計劃以套裝方式讓消費者「加購」 55W 小米 GaN 充電器,但最終決定分為無充電器、傳輸線的「標準版」,以及包含 55W 小米 GaN 充電器的「套裝版」,兩款版本以相同的建議售價進行販售,等同購買套裝版的消費者免費贈送 55W 小米 GaN 充電器套裝。而雷軍也呼籲已經有快充充電器的消費者,盡量選擇「標準版」來響應環保政策。

小米11 共推出三種規格配置, 8GB+128GB 售價人民幣 3,999 元(約合新台幣 17,217 元)、8GB+256GB 售價人民幣 4,299 元(約合新台幣 18,509 元)、12GB+256GB 售價人民幣 4,699 元(約合新台幣 20,231 元)。

圖片/消息來源:小米手機(微博)|小米(中國)

延伸閱讀:
小米路由器AX6000 正式發表:首款 Wi-Fi 6E 標準的小米高階路由器,售價僅約 2,580 元

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

小米路由器AX6000 正式發表:首款 Wi-Fi 6E 標準的小米高階路由器,售價僅約 2,580 元_台北網頁設計

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

之前在九月初,華碩推出了全球首款 Wi-Fi 6E 路由器「ROG Rapture GT-AXE11000」,隨後在稍早小米11 發表會同時也帶來了小米的 Wi-Fi 6E 旗艦路由器「小米路由器AX6000」,除了是小米首款以 Wi-Fi 6E 為標準的高階路由器產品,在價格方面依舊相當有競爭力。

小米路由器AX6000 正式發表:首款 Wi-Fi 6E 標準的小米高階路由器,售價僅約 2,580 元

在小米11 旗艦新機發表會期間,也提到了小米11 支持 Wi-Fi 6 增強版(也就是 Wi-Fi 6E)的連接能力,小米也分享了使用 Wi-Fi 6E 路由器「小米路由器AX6000」實測在不同距離條件下小米11 和 iPhone 12 Pro Max 的網路測速結果。

今年 1月 Wi-Fi 聯盟就正式公布了 Wi-Fi 6E 的標準,將頻段從 WiFi 6 的 2.4GHz 和 5GHz 頻段擴展至 6GHz頻段。而多出來的「E」即為「Extended」。 WiFi 6E 相比 WiFi 6 主要是加入了6GHz 頻段(5925-7125 MHz),6GHz 頻段寬度為 1.2GHz,可以容納 7 個 160MHz 的頻道或 14 個80MHz的頻道,相比 WiFi 6來說它的容量呈現倍數提升。

小米路由器AX6000 採用了 7 根天線設計,其中包含了 6 根高增益天線和 1 根 AIoT 智能天線,可覆蓋家中更大面積的範圍、支持4×4 160MHz 規格:

小米路由器AX6000 除了支持 4K QAM 調製調度技術,讓同一時間傳輸的數據量增加 20%。在速率方面 2.4G 頻寬的理論最高速率為 574Mbps,5G 頻寬的理論最高速率為 4804Mbps ,配合 Multilink 技術 2.4G + 5G 最高併發速度能達到 3456Mbps 。

接口方面,小米路由器AX6000 擁有一個WAN/LAN 可切換的 2.5G 高速接口,另外還有三個網路接口。

WiFi 6E 實際上主要功能是提升了數據容量、可以讓更多的設備同時上網,並同時進行高速下載。小米路由器AX6000 也支持 MU-MIMO + OFDMA ,可一次傳輸就完成多達 16 台設備的數據傳送。

此外,小米路由器AX6000 也支持 Mesh 組網,可實現全屋訊號無死角覆蓋:

硬體規格方面,小米路由器AX6000 搭載高通 IPQ5018 處理器,這顆處理器擁有 A53 構架的雙核CPU 搭配單核 NPU,運行內存為 512MB。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

小米路由器AX6000 在機身頂部、側面、底部均設有散熱孔,形成一個快速循環的散熱通道,能有效讓路由器溫度和性能保持在理想狀態。

小米路由器AX6000 也支援小米暢快連功能,只需打開米家App 就能輕鬆將身邊尚未配對的小米智慧家電完成網路連接:

當路由器修改 Wi-Fi 密碼後,小米路由器AX6000 的 WiFI 改密同步功能則會將新的密碼自動同步連接給所有小米智慧家電設備:

此外,作為小米路由器的旗艦新機,小米路由器AX6000 還支持智慧識別小米手機實現遊戲加速等功能。

價格方面,小米路由器AX6000 也維持小米一貫的超高性價比特色,建議售價只要人民幣 599 元(約合新台幣 2,580 元),倘若之後有機會入手一款 Snapdragon 888 處理器的手機/平板,那麼小米路由器AX6000 將是目前最適合搭配的路由器選擇。
不過,單單升級路由器也不代表裝置可使用到 Wi-Fi 6 ,還必須購買支持 Wi-Fi 6E 標準的裝置,目前市面上支持 Wi-Fi 6E 的裝置選擇還相當稀少,這次發表小米路由器AX6000 有一大主因也是為了小米11 而誕生。

圖片/消息來源:小米(中國)|小米路由器(微博)|小米手機(微博)

延伸閱讀:
小米11 正式發表:首款搭載高通 S888 5G 旗艦手機降臨!售價約 17,217 元起

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

小米氮化鎵GaN充電器Type-C 55W 推出:體積更小、價格更親民_網頁設計公司

※想知道最厲害的網頁設計公司嚨底家"!

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

除了今年九月引進台灣市場販售的小米 GaN 充電器 Type-C 65W ,在小米11 新機發表會也同步推出新款小米氮化鎵充電器「小米氮化鎵 GaN 充電器 Type-C 55W」。小米氮化鎵 GaN 充電器 Type-C 55W 擁有更小巧的體積、輸入接口改為 USB-A  ,這款充電器不僅搭配在小米11「套裝版」附贈,同時以約新台幣 426 元的價格單獨販售(附帶 USB 轉 Type-C 傳輸線)。

小米氮化鎵GaN充電器Type-C 55W 推出:體積更小、價格更親民

因應小米11 支持 55W 超級閃充的快充能力,小米也對應推出了全新的「小米氮化鎵 GaN 充電器 Type-C 55W」,可支持最大 55W 功率輸出。外觀延續過往小米充電器一樣採用白色的 PC 材質外殼,在側邊印著 GaN 字樣代表這是款氮化鎵充電器,插頭部分與之前 65W 版本一樣不可收折。

小米氮化鎵 GaN 充電器 Type-C 55W 可為小米11 提供最高 55W 的充電功率,能在 46 分鐘為小米11 充電至 100% :

機身尺寸方面,小米氮化鎵 GaN 充電器 Type-C 55W 擁有更短小的體積,方便放入背包外出攜帶使用,而不可收折插頭也是未來有意入手這款充電器的消費者得額外留意的。
充電安全部分,支援包括短路保護、過流保護、過壓保護、過溫保護、欠壓保護等,能確保正常電流範圍內充電保持安全的充電過程。另外,採用 100-240V 國際電壓,出國旅行也能使用。

另外,內建智慧晶片也可用於手機、筆電等裝置充電使用。輸出方面,小米氮化鎵 GaN 充電器 Type-C 55W 支持 5V/3A、9V/3A、11V/5A、20V/2.5A 。雖然小米氮化鎵 GaN 充電器 Type-C 55W 名為 Type-C ,但與 65W 版本不同的還有接口採用的是 USB-A 輸出接口,並附贈 USB-A 轉 USB-C 傳輸線。

圖片/消息來源:小米(中國)

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

延伸閱讀:
小米路由器AX6000 正式發表:首款 Wi-Fi 6E 標準的小米高階路由器,售價僅約 2,580 元

小米11 正式發表:首款搭載高通 S888 5G 旗艦手機降臨!售價約 17,217 元起

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

abp(net core)+easyui+efcore實現倉儲管理系統——出庫管理之一(四十九)_潭子電動車

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

abp(net core)+easyui+efcore實現倉儲管理系統目錄

abp(net core)+easyui+efcore實現倉儲管理系統——ABP總體介紹(一)
abp(net core)+easyui+efcore實現倉儲管理系統——解決方案介紹(二)
abp(net core)+easyui+efcore實現倉儲管理系統——領域層創建實體(三)
 abp(net core)+easyui+efcore實現倉儲管理系統——定義倉儲並實現 (四)
abp(net core)+easyui+efcore實現倉儲管理系統——創建應用服務(五)
abp(net core)+easyui+efcore實現倉儲管理系統——展現層實現增刪改查之控制器(六)
abp(net core)+easyui+efcore實現倉儲管理系統——展現層實現增刪改查之列表視圖(七)
abp(net core)+easyui+efcore實現倉儲管理系統——展現層實現增刪改查之增刪改視圖(八)
abp(net core)+easyui+efcore實現倉儲管理系統——展現層實現增刪改查之菜單與測試(九)
abp(net core)+easyui+efcore實現倉儲管理系統——使用 WEBAPI實現CURD (十一)

abp(net core)+easyui+efcore實現倉儲管理系統——EasyUI前端頁面框架 (十八)

abp(net core)+easyui+efcore實現倉儲管理系統——EasyUI之貨物管理一 (十九)
abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之一(二十七)
abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之三(二十九)
abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之八(三十四)
abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之十(三十六)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之一(三十七)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之二(三十八)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之三存儲過程(三十九)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之四(四十)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之五(四十一)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之六(四十二)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之七(四十三)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之八(四十四)   
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之九(四十五)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之十(四十六)

abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之十一(四十七)
abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之十二(四十八)

.前言

        出庫單的功能。能學習了出庫單管理之後,WMS的 最基本的功能算是完成了。當然一個成熟的WMS還包括了盤點,報表,策略規則,移庫功能及與其他系統(ERP、TMS等)的接口,實現無縫集成,打破信息孤島,讓數據實時、準確和同步。

二、出庫單的流程

    1.一般情況下會有一個前置的OMS系統——即訂單管理系統。主要功能之一是由客戶填寫訂單。

      2.客戶把訂單下第三方物流公司時,第三方物流公司會生成出貨單推送到倉庫時,系統會自動生成揀貨單,理貨員根據揀貨單揀貨,並製作出庫單,然後打印標籤,粘貼條碼標籤,分配托盤,核驗條碼標籤,貨物裝箱,訂艙出庫,並在系統中對出庫單進行審核通過。整個流程如下圖。

 

     當然我們接下來要實現的出庫單功能,沒有這麼複雜。

 

三、創建出庫單實體

    1. 做為一個出庫單,在數據庫中一般存在兩張表,表頭OutStockOrder,表體OutStockDetail

    2.Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“ABP.TPLMS.Core”項目的“Entitys”文件夾,在彈出菜單中選擇“添加” >

 > “類”。 將類命名為 OutStockOrder,然後選擇“添加”。

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

    3.創建OutStockOrder類繼承自Entity<int>,通過實現審計模塊中的IHasCreationTime來實現保存創建時間。代碼如下:

using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text; namespace ABP.TPLMS.Entitys { public class OutStockOrder: Entity<int>, IHasCreationTime { public const int MaxLength = 255; public OutStockOrder() { No = string.Empty; CustomerCode = string.Empty; CustomerName = string.Empty; WarehouseNo = string.Empty; DeliveryNo = string.Empty; TallyClerk = string.Empty; TallyTime = string.Empty; CreationTime = DateTime.Now; Oper = string.Empty; Checker = string.Empty; CheckTime = string.Empty; Gwt = 0; Nwt = 0; PackageQty = 0; OwnerCode = string.Empty; OwnerName = string.Empty; Remark = string.Empty; Status = 0; PreOutStockTime = string.Empty; } [StringLength(50)] [Required] public string No { get; set; } /// <summary>
        /// 客戶名稱 /// </summary>
 [StringLength(MaxLength)] [Required] public string CustomerName { get; set; } /// <summary>
        /// 車牌號 /// </summary>
        public string VehicleNo { get; set; } /// <summary>
        /// 客戶代碼 /// </summary>
        [StringLength(50)] [Required] public string CustomerCode { get; set; } /// <summary>
        /// 收貨人代碼 /// </summary>
        public string ConsigneeCode { get; set; } /// <summary>
        /// 收貨人 /// </summary>
        public string Consignee{get ;set;} /// <summary>
        /// 收貨人社會信用代碼 /// </summary>
        public string ConsigneeSCCD { get; set; } /// <summary>
        /// 託運人,發貨人 /// </summary>
        public string Shipper { get; set; } /// <summary>
        /// 託運人,發貨人代碼 /// </summary>
        public string ShipperCode { get; set; } /// <summary>
        /// 託運人,發貨人社會信用代碼 /// </summary>
        public string ShipperSCCD { get; set; } /// <summary>
        /// 通知人 /// </summary>
        public string Notify { get; set; } /// <summary>
        /// 通知人代碼 /// </summary>
        public string NotifyCode { get; set; } /// <summary>
        /// 通知人社會信用代碼 /// </summary>
        public string NotifySCCD { get; set; } /// <summary>
        /// 出貨單號 /// </summary>
        public string DeliveryNo { get; set; } /// <summary>
        /// 倉庫號 /// </summary>
        public string WarehouseNo { get; set; } /// <summary>
        /// 貨主 /// </summary>
 [StringLength(MaxLength)] [Required] public string OwnerName { get; set; } public decimal Gwt { get; set; } public decimal Nwt { get; set; } public int PackageQty { get; set; } /// <summary>
        /// 理貨時間 /// </summary>
        [StringLength(20)] public string TallyTime { get; set; } /// <summary>
        /// 理貨員 /// </summary>
        [StringLength(50)] public string TallyClerk { get; set; } [StringLength(50)] public string Oper { get; set; } public int Status { get; set; } [StringLength(50)] public string OwnerCode { get; set; } /// <summary>
        /// 預計出庫時間 /// </summary>
        [StringLength(20)] public string PreOutStockTime { get; set; } /// <summary>
        /// 審核人 /// </summary>
        [StringLength(50)] public string Checker { get; set; } [StringLength(20)] public string CheckTime { get; set; } [StringLength(1000)] public string Remark { get; set; } public DateTime CreationTime { get; set; } [StringLength(20)] public string LastUpdateTime { get; set; } [StringLength(50)] public string LastOper { get; set; } } } 

    4. 重得第2,3步,我們在“ABP.TPLMS.Core”項目的“Entitys”文件夾,創建OutStockOrderDetail類。代碼如下:

using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text; namespace ABP.TPLMS.Entitys { public class OutStockOrderDetail : Entity<int>, IHasCreationTime { public const int MaxLength = 255; public OutStockOrderDetail() { this.Qty = 0; this.CargoCode = string.Empty; this.CargoName = string.Empty; this.Brand = string.Empty; this.Country = string.Empty; this.CreationTime = DateTime.Now; this.Curr = string.Empty; this.GrossWt = 0; this.Height = 0; this.HSCode = string.Empty; this.Length = 0; this.SecdLawfQty = 0; this.LawfQty = 0; this.NetWt = 0; this.Package = string.Empty; this.Price = 0; this.Spcf = string.Empty; this.Unit = string.Empty; this.InStockNo = string.Empty; this.LawfUnit = string.Empty; this.Vol = 0; this.Width = 0; this.LawfUnit = string.Empty; this.SecdLawfUnit = string.Empty; this.Batch = string.Empty; this.InStockOrderDetailId = 0; } public int SupplierId { get; set; } [MaxLength(50)] public string CargoCode { get; set; } [MaxLength(10)] public string HSCode { get; set; } [MaxLength(MaxLength)] public string CargoName { get; set; } [MaxLength(MaxLength)] public string Spcf { get; set; } [MaxLength(20)] public string Unit { get; set; } /// <summary>
        /// 目的國 /// </summary>
        [MaxLength(20)] public string DestCountry { get; set; } /// <summary>
        /// 原產國 /// </summary>
 [MaxLength(20)] public string Country { get; set; } [MaxLength(50)] public string Brand { get; set; } [MaxLength(20)] public string Curr { get; set; } [MaxLength(20)] public string Package { get; set; } public decimal Length { get; set; } public decimal Width { get; set; } public decimal Height { get; set; } public decimal Vol { get; set; } public decimal Price { get; set; } public decimal TotalAmt { get; set; } public decimal GrossWt { get; set; } public decimal NetWt { get; set; } public DateTime CreationTime { get; set; } [MaxLength(20)] public string InStockNo { get; set; } public int InStockOrderDetailId { get; set; } public decimal Qty { get; set; } public decimal LawfQty { get; set; } public decimal SecdLawfQty { get; set; } [MaxLength(20)] public string LawfUnit { get; set; } [MaxLength(20)] public string SecdLawfUnit { get; set; } [MaxLength(20)] public string Batch { get; set; } public string  Loc { get; set; } } }

5.定義入庫單的實體之後,我們去“ABP.TPLMS.EntityFrameworkCore”項目中的“TPLMSDbContext”類中定義實體對應的DbSet,以應用Code First 數據遷移。添加以下代碼

using Microsoft.EntityFrameworkCore; using Abp.Zero.EntityFrameworkCore; using ABP.TPLMS.Authorization.Roles; using ABP.TPLMS.Authorization.Users; using ABP.TPLMS.MultiTenancy; using ABP.TPLMS.Entitys; namespace ABP.TPLMS.EntityFrameworkCore { public class TPLMSDbContext : AbpZeroDbContext<Tenant, Role, User, TPLMSDbContext> { /* Define a DbSet for each entity of the application */
      

        public TPLMSDbContext(DbContextOptions<TPLMSDbContext> options) : base(options) { } public DbSet<Module> Modules { get; set; } public DbSet<Supplier> Suppliers { get; set; } public DbSet<Cargo> Cargos { get; set; } public DbSet<Org> Orgs { get; set; } public virtual DbSet<InStockOrder> InStockOrder { get; set; } public virtual DbSet<InStockOrderDetail> InStockOrderDetail { get; set; } public virtual DbSet<InStockOrderDetailLoc> InStockOrderDetailLoc { get; set; } public virtual DbSet<OutStockOrder> OutStockOrder { get; set; } public virtual DbSet<OutStockOrderDetail> OutStockOrderDetail { get; set; } } }

     6.從菜單中選擇“工具->NuGet包管理器器—>程序包管理器控制台”菜單。

    7. 在PMC中,默認項目選擇EntityframeworkCore對應的項目后。輸入以下命令:Add-Migration AddEntityOutStock,創建遷移。如下圖。

 

    8. 在上面的命令執行完畢之後,創建成功后,會在Migrations文件夾下創建時間_AddEntityOutStock格式的類文件,這些代碼是基於DbContext指定的模型。如下圖。

 

    9.在程序包管理器控制台,輸入Update-Database,回車執行遷移。執行成功后,如下圖。

 

    10. 在SQL Server Management Studio中查看數據庫,OutStockOrder、OutStockOrderDetail兩張表創建成功。

 

 

 

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

吐血輸出:2萬字長文帶你細細盤點五種負載均衡策略。_包裝設計

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

2020 年 5 月 15 日,Dubbo 發布 2.7.7 release 版本。其中有這麼一個 Features

新增一個負載均衡策略。

熟悉我的老讀者肯定是知道的,Dubbo 的負載均衡我都寫過專門的文章,對每個負載均衡算法進行了源碼的解讀,還分享了自己調試過程中的一些騷操作。

新的負載均衡出來了,那必須的得解讀一波。

先看一下提交記錄:

https://github.com/chickenlj/incubator-dubbo/commit/6d2ba7ec7b5a1cb7971143d4262d0a1bfc826d45

負載均衡是基於 SPI 實現的,我們看到對應的文件中多了一個名為 shortestresponse 的 key。

這個,就是新增的負載均衡策略了。看名字,你也知道了這個策略的名稱就叫:最短響應。

所以截止 2.7.7 版本,官方提供了五種負載均衡算法了,他們分別是:

  1. ConsistentHashLoadBalance 一致性哈希負載均衡
  2. LeastActiveLoadBalance 最小活躍數負載均衡
  3. RandomLoadBalance 加權隨機負載均衡
  4. RoundRobinLoadBalance 加權輪詢負載均衡
  5. ShortestResponseLoadBalance 最短響應時間負載均衡

前面四種我已經在之前的文章中進行了詳細的分析。有的讀者反饋說想看合輯,所以我會在這篇文章中把之前文章也整合進來。

所以,需要特彆強調一下的是,這篇文章集合了之前寫的三篇負載均衡的文章。看完最短響應時間負載均衡這一部分后,如果你看過我之前的那三篇文章,你可以溫故而知新,也可以直接拉到文末看看我推薦的一個活動,然後點個贊再走。如果你沒有看過那三篇,這篇文章如果你細看,肯定有很多收穫,以後談起負載均衡的時候若數家珍,但是肯定需要看非常非常長的時間,做好心理準備。

我已經預感到了,這篇文章妥妥的會超過 2 萬字。屬於硬核勸退文章,想想就害怕。

最短響應時間負載均衡

首先,我們看一下這個類上的註解,先有個整體的認知。

org.apache.dubbo.rpc.cluster.loadbalance.ShortestResponseLoadBalance

我來翻譯一下是什麼意思:

  1. 從多個服務提供者中選擇出調用成功的且響應時間最短的服務提供者,由於滿足這樣條件的服務提供者有可能有多個。所以當選擇出多個服務提供者后要根據他們的權重做分析。
  2. 但是如果只選擇出來了一個,直接用選出來這個。
  3. 如果真的有多個,看它們的權重是否一樣,如果不一樣,則走加權隨機算法的邏輯。
  4. 如果它們的權重是一樣的,則隨機調用一個。

再配個圖,就好理解了,可以先不管圖片中的標號:

有了上面的整體概念的鋪墊了,接下來分析源碼的時候就簡單了。

源碼一共就 66 行,我把它分為 5 個片段去一一分析。

這裏一到五的標號,對應上面流程圖中的標號。我們一個個的說。

標號為①的部分

這一部分是定義並初始化一些參數,為接下來的代碼服務的,翻譯一下每個參數對應的註釋:

length 參數:服務提供者的數量。

shortestResponse 參數:所有服務提供者的估計最短響應時間。(這個地方我覺得註釋描述的不太準確,看後面的代碼可以知道這隻是一個零時變量,在循環中存儲當前最短響應時間是多少。)

shortCount 參數:具有相同最短響應時間的服務提供者個數,初始化為 0。

shortestIndexes 參數:數組裡面放的是具有相同最短響應時間的服務提供者的下標。

weights 參數:每一個服務提供者的權重。

totalWeight 參數:多個具有相同最短響應時間的服務提供者對應的預熱(預熱這個點還是挺重要的,在下面講最小活躍數負載均衡的時候有詳細說明)權重之和。

firstWeight 參數:第一個具有最短響應時間的服務提供者的權重。

sameWeight 參數:多個滿足條件的提供者的權重是否一致。

標號為②的部分

這一部分代碼的關鍵,就在上面框起來的部分。而框起來的部分,最關鍵的地方,就在於第一行。

獲取調用成功的平均時間。

成功調用的平均時間怎麼算的?

調用成功的請求數總數對應的總耗時 / 調用成功的請求數總數 = 成功調用的平均時間。

所以,在下面這個方法中,首先獲取到了調用成功的請求數總數:

這個 succeeded 參數是怎麼來的呢?

答案就是:總的請求數減去請求失敗的數量,就是請求成功的總數!

那麼為什麼不能直接獲取請求成功的總數呢?

別問,問就是沒有這個選項啊。你看,在 RpcStatus 裏面沒有這個參數呀。

請求成功的總數我們有了,接下來成功總耗時怎麼拿到的呢?

答案就是:總的請求時間減去請求失敗的總時間,就是請求成功的總耗時!

那麼為什麼不能直接獲取請求成功的總耗時呢?

別問,問就是……

我們看一下 RpcStatus 中的這幾個參數是在哪裡維護的:

org.apache.dubbo.rpc.RpcStatus#endCount(org.apache.dubbo.rpc.RpcStatus, long, boolean)

其中的第二個入參是本次請求調用時長,第三個入參是本次調用是否成功。

具體的方法不必細說了吧,已經顯而易見了。

再回去看框起來的那三行代碼:

  1. 第一行獲取到了該服務提供者成功請求的平均耗時。
  2. 第二行獲取的是該服務提供者的活躍數,也就是堆積的請求數。
  3. 第三行獲取的就是如果當前這個請求發給這個服務提供者預計需要等待的時間。乘以 active 的原因是因為它需要排在堆積的請求的後面嘛。

這裏,我們就獲取到了如果選擇當前循環中的服務提供者的預計等待時間是多長。

後面的代碼怎麼寫?

當然是出來一個更短的就把這個踢出去呀,或者出來一個一樣長時間的就記錄一下,接着去 pk 權重了。

所以,接下來 shortestIndexes 參數和 weights 參數就排上用場了:

另外,多說一句的,它裏面有這樣的一行註釋:

和 LeastActiveLoadBalance 負載均衡策略一致,我給你截圖對比一下:

可以看到,確實是非常的相似,只是一個是判斷誰的響應時間短,一個是判斷誰的活躍數低。

標號為③的地方

標號為③的地方是這樣的:

裏面參數的含義我們都知道了,所以,標號為③的地方的含義就很好解釋了:經過選擇后只有一個服務提供者滿足條件。所以,直接使用這個服務提供者。

標號為④的地方

這個地方我就不展開講了(後面的加權隨機負載均衡那一小節有詳細說明),熟悉的朋友一眼就看出來這是加權隨機負載均衡的寫法了。

不信?我給你對比一下:

你看,是不是一模一樣的。

標號為⑤的地方

一行代碼,沒啥說的。就是從多個滿足條件的且權重一樣的服務提供者中隨機選擇一個。

如果一定要多說一句的話,我截個圖吧:

可以看到,這行代碼在最短響應時間、加權隨機、最小活躍數負載均衡策略中都出現了,且都在最後一行。

好了,到這裏最短響應時間負載均衡策略就講完了,你再回過頭去看那張流程圖,會發現其實流程非常的清晰,完全可以根據代碼結構畫出流程圖。一個是說明這個算法是真的不複雜,另一個是說明好的代碼會說話。

優雅

你知道 Dubbo 加入這個新的負載均衡算法提交了幾個文件嗎?

四個文件,其中還包含兩個測試文件:

這裏就是策略模式和 SPI 的好處。對原有的負載均衡策略沒有任何侵略性。只需要按照規則擴展配置文件,實現對應接口即可。

這是什麼?

這就是值得學習優雅!

那我們優雅的進入下一議題。

最小活躍數負載均衡

這一小節所示源碼,沒有特別標註的地方均為 2.6.0 版本。

為什麼沒有用截止目前(我當時寫這段文章的時候是2019年12月01日)的最新的版本號 2.7.4.1 呢?因為 2.6.0 這個版本裏面有兩個 bug 。從 bug 講起來,印象更加深刻。

最後會對 2.6.0/2.6.5/2.7.4.1 版本進行對比,通過對比學習,加深印象。

我這裏補充一句啊,僅僅半年的時間,版本號就從 2.7.4.1 到了 2.7.7。其中還包含一個 2.7.5 這樣的大版本。

所以還有人說 Dubbo 不夠活躍?(幾年前的文章現在還有人在發。)

對吧,我們不吵架,我們擺事實,聊數據嘛。

Demo 準備

我看源碼的習慣是先搞個 Demo 把調試環境搭起來。然後帶着疑問去抽絲剝繭的 Debug,不放過在這個過程中在腦海裏面一閃而過的任何疑問。

這一小節分享的是Dubbo負載均衡策略之一最小活躍數(LeastActiveLoadBalance)。所以我先搭建一個 Dubbo 的項目,並啟動三個 provider 供 consumer 調用。

三個 provider 的 loadbalance 均配置的是 leastactive。權重分別是默認權重、200、300。

**默認權重是多少?**後面看源碼的時候,源碼會告訴你。

三個不同的服務提供者會給調用方返回自己是什麼權重的服務。

啟動三個實例。(注:上面的 provider.xml 和 DemoServiceImpl 其實只有一個,每次啟動的時候手動修改端口、權重即可。)

到 zookeeper 上檢查一下,服務提供者是否正常:

可以看到三個服務提供者分別在 20880、20881、20882 端口。(每個紅框的最後5個数字就是端口號)。

最後,我們再看服務消費者。消費者很簡單,配置consumer.xml

直接調用接口並打印返回值即可。

斷點打在哪?

相信很多朋友也很想看源碼,但是不知道從何處下手。處於一種在源碼裏面”亂逛”的狀態,一圈逛下來,收穫並不大。

這一部分我想分享一下我是怎麼去看源碼。首先我會帶着問題去源碼裏面尋找答案,即有針對性的看源碼。

如果是這種框架類的,正如上面寫的,我會先翻一翻官網(Dubbo 的官方文檔其實寫的挺好了),然後搭建一個簡單的 Demo 項目,然後 Debug 跟進去看。Debug 的時候當然需要是設置斷點的,那麼這個斷點如何設置呢?

第一個斷點,當然毋庸置疑,是打在調用方法的地方,比如本文中,第一個斷點是在這個地方:

接下里怎麼辦?

你當然可以從第一個斷點處,一步一步的跟進去。但是在這個過程中,你發現了嗎?大多數情況你都是被源碼牽着鼻子走的。本來你就只帶着一個問題去看源碼的,有可能你Debug了十分鐘,還沒找到關鍵的代碼。也有可能你Debug了十分鐘,問題從一個變成了無數個。

所以不要慌,我們點支煙,慢慢分析。

首先怎麼避免被源碼牽着四處亂逛呢?

我們得找到一個突破口,還記得我在《很開心,在使用mybatis的過程中我踩到一個坑》這篇文章中提到的逆向排查的方法嗎?這次的文章,我再次展示一下該方法。

看源碼之前,我們的目標要十分明確,就是想要找到 Dubbo 最小活躍數算法的具體實現類以及實現類的具體邏輯是什麼。

根據我們的 provider.xml 裏面的:

很明顯,我們知道 loadbalance 是關鍵字。所以我們拿着 loadbalance 全局搜索,可以看到 Dubbo 包下面的 LoadBalance。

這是一個 SPI 接口 com.alibaba.dubbo.rpc.cluster.LoadBalance:

其實現類為:

com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance

AbstractLoadBalance 是一個抽象類,該類裏面有一個抽象方法doSelect。這個抽象方法其中的一個實現類就是我們要分析的最少活躍次數負載均衡的源碼。

同時,到這裏我們知道了 LoadBalance 是一個 SPI 接口,說明我們可以擴展自己的負載均衡策略。抽象方法 doSelect 有四個實現類。這個四個實現類,就是 Dubbo 官方提供的負載均衡策略(截止 2.7.7 版本之前),他們分別是:

  1. ConsistentHashLoadBalance 一致性哈希算法
  2. LeastActiveLoadBalance 最小活躍數算法
  3. RandomLoadBalance 加權隨機算法
  4. RoundRobinLoadBalance 加權輪詢算法

我們已經找到了 LeastActiveLoadBalance 這個類了,那麼我們的第二個斷點打在哪裡已經很明確了。

目前看來,兩個斷點就可以支撐我們的分析了。

有的朋友可能想問,那我想知道 Dubbo 是怎麼識別出我們想要的是最少活躍次數算法,而不是其他的算法呢?其他的算法是怎麼實現的呢?從第一個斷點到第二個斷點直接有着怎樣的調用鏈呢?

在沒有徹底搞清楚最少活躍數算法之前,這些統統先記錄在案但不予理睬。一定要明確目標,帶着一個問題進來,就先把帶來的問題解決了。之後再去解決在這個過程中碰到的其他問題。在這樣環環相扣解決問題的過程中,你就慢慢的把握了源碼的精髓。這是我個人的一點看源碼的心得。供諸君參考。

模擬環境

既然叫做最小活躍數策略。那我們得讓現有的三個消費者都有一些調用次數。所以我們得改造一下服務提供者和消費者。

服務提供者端的改造如下:

!

PS:這裏以權重為 300 的服務端為例。另外的兩個服務端改造點相同。

客戶端的改造點如下:

一共發送 21 個請求:其中前 20 個先發到服務端讓其 hold 住(因為服務端有 sleep),最後一個請求就是我們需要 Debug 跟蹤的請求。

運行一下,讓程序停在斷點的地方,然後看看控制台的輸出:

權重為300的服務端共計收到9個請求

權重為200的服務端共計收到6個請求

默認權重的服務端共計收到5個請求

我們還有一個請求在 Debug。直接進入到我們的第二個斷點的位置,並 Debug 到下圖所示的一行代碼(可以點看查看大圖):

正如上面這圖所說的:weight=100 回答了一個問題,active=0 提出的一個問題。

weight=100 回答了什麼問題呢?

默認權重是多少?是 100。

我們服務端的活躍數分別應該是下面這樣的

  • 權重為300的服務端,active=9
  • 權重為200的服務端,active=6
  • 默認權重(100)的服務端,active=5

但是這裏為什麼截圖中的active會等於 0 呢?這是一個問題。

繼續往下 Debug 你會發現,每一個服務端的 active 都是 0。所以相比之下沒有一個 invoker 有最小 active 。於是程序走到了根據權重選擇 invoker 的邏輯中。

active為什麼是0?

active 為 0 說明在 Dubbo 調用的過程中 active 並沒有發生變化。那 active 為什麼是 0,其實就是在問 active 什麼時候發生變化

要回答這個問題我們得知道 active 是在哪裡定義的,因為在其定義的地方,必有其修改的方法。

下面這圖說明了active是定義在RpcStatus類裏面的一個類型為AtomicInteger 的成員變量。

在 RpcStatus 類中,有三處()調用 active 值的方法,一個增加、一個減少、一個獲取:

很明顯,我們需要看的是第一個,在哪裡增加。

所以我們找到了 beginCount(URL,String) 方法,該方法只有兩個 Filter 調用。ActiveLimitFilter,見名知意,這就是我們要找的東西。

com.alibaba.dubbo.rpc.filter.ActiveLimitFilter具體如下:

看到這裏,我們就知道怎麼去回答這個問題了:為什麼active是0呢?因為在客戶端沒有配置ActiveLimitFilter。所以,ActiveLimitFilter沒有生效,導致active沒有發生變化。

怎麼讓其生效呢?已經呼之欲出了。

好了,再來試驗一次:

加上Filter之後,我們通過Debug可以看到,對應權重的活躍數就和我們預期的是一致的了。

1.權重為300的活躍數為6

2.權重為200的活躍數為11

3.默認權重(100)的活躍數為3

根據活躍數我們可以分析出來,最後我們Debug住的這個請求,一定會選擇默認權重的invoker去執行,因為他是當前活躍數最小的invoker。如下所示:

雖然到這裏我們還沒開始進行源碼的分析,只是把流程梳理清楚了。但是把Demo完整的搭建了起來,而且知道了最少活躍數負載均衡算法必須配合ActiveLimitFilter使用,位於RpcStatus類的active字段才會起作用,否則,它就是一個基於權重的算法。

比起其他地方直接告訴你,要配置ActiveLimitFilter才行哦,我們自己實驗得出的結論,能讓我們的印象更加深刻。

我們再仔細看一下加上ActiveLimitFilter之後的各個服務的活躍數情況:

  • 權重為300的活躍數為6
  • 權重為200的活躍數為11
  • 默認權重(100)的活躍數為3

你不覺得奇怪嗎,為什麼權重為200的活躍數是最高的

其在業務上的含義是:我們有三台性能各異的服務器,A服務器性能最好,所以權重為300,B服務器性能中等,所以權重為200,C服務器性能最差,所以權重為100。

當我們選擇最小活躍次數的負載均衡算法時,我們期望的是性能最好的A服務器承擔更多的請求,而真實的情況是性能中等的B服務器承擔的請求更多。這與我們的設定相悖。

如果你說20個請求數據量太少,可能是巧合,不足以說明問題。說明你還沒被我帶偏,我們不能基於巧合編程。

所以為了驗證這個地方確實有問題,我把請求擴大到一萬個。

同時,記得擴大 provider 端的 Dubbo 線程池:

由於每個服務端運行的代碼都是一樣的,所以我們期望的結果應該是權重最高的承擔更多的請求。但是最終的結果如圖所示:

各個服務器均攤了請求。這就是我文章最開始的時候說的Dubbo 2.6.0 版本中最小活躍數負載均衡算法的Bug之一。

接下來,我們帶着這個問題,去分析源碼。

剖析源碼

com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance的源碼如下,我逐行進行了解讀。可以點開查看大圖,細細品讀,非常爽:

下圖中紅框框起來的部分就是一個基於權重選擇invoker的邏輯:

我給大家畫圖分析一下:

請仔細分析圖中給出的舉例說明。同時,上面這圖也是按照比例畫的,可以直觀的看到,對於某一個請求,區間(權重)越大的服務器,就越可能會承擔這個請求。所以,當請求足夠多的時候,各個服務器承擔的請求數,應該就是區間,即權重的比值。

其中第 81 行有調用 getWeight 方法,位於抽象類 AbstractLoadBalance 中,也需要進行重點解讀的代碼。

com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance 的源碼如下,我也進行了大量的備註:

在 AbstractLoadBalance 類中提到了一個預熱的概念。官網中是這樣的介紹該功能的:

權重的計算過程主要用於保證當服務運行時長小於服務預熱時間時,對服務進行降權,避免讓服務在啟動之初就處於高負載狀態。服務預熱是一個優化手段,與此類似的還有 JVM 預熱。主要目的是讓服務啟動后“低功率”運行一段時間,使其效率慢慢提升至最佳狀態。

從上圖代碼裏面的公式(演變后):*計算后的權重=(uptime/warmup)weight 可以看出:隨着服務啟動時間的增加(uptime),計算后的權重會越來越接近weight。從實際場景的角度來看,隨着服務啟動時間的增加,服務承擔的流量會慢慢上升,沒有一個陡升的過程。所以這是一個優化手段。同時 Dubbo 接口還支持延遲暴露。

在仔細的看完上面的源碼解析圖后,配合官網的總結加上我的靈魂畫作,相信你可以對最小活躍數負載均衡算法有一個比較深入的理解:

  1. 遍歷 invokers 列表,尋找活躍數最小的 Invoker
  2. 如果有多個 Invoker 具有相同的最小活躍數,此時記錄下這些 Invoker 在 invokers 集合中的下標,並累加它們的權重,比較它們的權重值是否相等
  3. 如果只有一個 Invoker 具有最小的活躍數,此時直接返回該 Invoker 即可
  4. 如果有多個 Invoker 具有最小活躍數,且它們的權重不相等,此時處理方式和 RandomLoadBalance 一致
  5. 如果有多個 Invoker 具有最小活躍數,但它們的權重相等,此時隨機返回一個即可

所以我覺得最小活躍數負載均衡的全稱應該叫做:有最小活躍數用最小活躍數,沒有最小活躍數根據權重選擇,權重一樣則隨機返回的負載均衡算法。

Bug在哪裡?

Dubbo2.6.0最小活躍數算法Bug一

問題出在標號為 ① 和 ② 這兩行代碼中:

標號為 ① 的代碼在url中取出的是沒有經過 getWeight 方法降權處理的權重值,這個值會被累加到權重總和(totalWeight)中。

標號為 ② 的代碼取的是經過 getWeight 方法處理后的權重值。

取值的差異會導致一個問題,標號為 ② 的代碼的左邊,offsetWeight 是一個在 [0,totalWeight) 範圍內的隨機數,右邊是經過 getWeight 方法降權后的權重。所以在經過 leastCount 次的循環減法后,offsetWeight 在服務啟動時間還沒到熱啟動設置(默認10分鐘)的這段時間內,極大可能仍然大於 0。導致不會進入到標號為 ③ 的代碼中。直接到標號為 ④ 的代碼處,變成了隨機調用策略。這與設計不符,所以是個 bug。

前面章節說的情況就是這個Bug導致的。

這個Bug對應的issues地址和pull request分為:

https://github.com/apache/dubbo/issues/904

https://github.com/apache/dubbo/pull/2172

那怎麼修復的呢?我們直接對比 Dubbo 2.7.4.1 的代碼:

可以看到獲取weight的方法變了:從url中直接獲取變成了通過getWeight方法獲取。獲取到的變量名稱也變了:從weight變成了afterWarmup,更加的見名知意。

還有一處變化是獲取隨機值的方法的變化,從Randmo變成了ThreadLoaclRandom,性能得到了提升。這處變化就不展開講了,有興趣的朋友可以去了解一下。

Dubbo2.6.0最小活躍數算法Bug二

這個Bug我沒有遇到,但是我在官方文檔上看了其描述(官方文檔中的版本是2.6.4),引用如下:

官網上說這個問題在2.6.5版本進行修復。我對比了2.6.0/2.6.5/2.7.4.1三個版本,發現每個版本都略有不同。如下所示:

圖中標記為①的三處代碼:

2.6.0版本的是有Bug的代碼,原因在上面說過了。

2.6.5版本的修復方式是獲取隨機數的時候加一,所以取值範圍就從**[0,totalWeight)變成了[0,totalWeight]**,這樣就可以避免這個問題。

2.7.4.1版本的取值範圍還是[0,totalWeight),但是它的修復方法體現在了標記為②的代碼處。2.6.0/2.6.5版本標記為②的地方都是if(offsetWeight<=0),而2.7.4.1版本變成了if(offsetWeight<0)

你品一品,是不是效果是一樣的,但是更加優雅了。

朋友們,魔鬼,都在細節里啊!

好了,進入下一議題。

一致性哈希負載均衡

這一部分是對於Dubbo負載均衡策略之一的一致性哈希負載均衡的詳細分析。對源碼逐行解讀、根據實際運行結果,配以豐富的圖片,可能是東半球講一致性哈希算法在Dubbo中的實現最詳細的文章了。

本小節所示源碼,沒有特別標註的地方,均為2.7.4.1版本。

在撰寫本文的過程中,發現了Dubbo2.7.0版本之後的一個bug。會導致性能問題,如果你們的負載均衡配置的是一致性哈希或者考慮使用一致性哈希的話,可以了解一下。

哈希算法

在介紹一致性哈希算法之前,我們看看哈希算法,以及它解決了什麼問題,帶來了什麼問題。

如上圖所示,假設0,1,2號服務器都存儲的有用戶信息,那麼當我們需要獲取某用戶信息時,因為我們不知道該用戶信息存放在哪一台服務器中,所以需要分別查詢0,1,2號服務器。這樣獲取數據的效率是極低的。

對於這樣的場景,我們可以引入哈希算法。

還是上面的場景,但前提是每一台服務器存放用戶信息時是根據某一種哈希算法存放的。所以取用戶信息的時候,也按照同樣的哈希算法取即可。

假設我們要查詢用戶號為100的用戶信息,經過某個哈希算法,比如這裏的userId mod n,即100 mod 3結果為1。所以用戶號100的這個請求最終會被1號服務器接收並處理。

這樣就解決了無效查詢的問題。

但是這樣的方案會帶來什麼問題呢?

擴容或者縮容時,會導致大量的數據遷移。最少也會影響百分之50的數據。

為了說明問題,我們加入一台服務器3。服務器的數量n就從3變成了4。還是查詢用戶號為100的用戶信息時,100 mod 4結果為0。這時,請求就被0號服務器接收了。

當服務器數量為3時,用戶號為100的請求會被1號服務器處理。

當服務器數量為4時,用戶號為100的請求會被0號服務器處理。

所以,當服務器數量增加或者減少時,一定會涉及到大量數據遷移的問題。可謂是牽一發而動全身。

對於上訴哈希算法其優點是簡單易用,大多數分庫分表規則就採取的這種方式。一般是提前根據數據量,預先估算好分區數。

缺點是由於擴容或收縮節點導致節點數量變化時,節點的映射關係需要重新計算,會導致數據進行遷移。所以擴容時通常採用翻倍擴容,避免數據映射全部被打亂,導致全量遷移的情況,這樣只會發生50%的數據遷移。

假設這是一個緩存服務,數據的遷移會導致在遷移的時間段內,有緩存是失效的。

緩存失效,可怕啊。還記得我之前的文章嗎,《當周杰倫把QQ音樂干翻的時候,作為程序猿我看到了什麼?》就是講緩存擊穿、緩存穿透、緩存雪崩的場景和對應的解決方案。

一致性哈希算法

為了解決哈希算法帶來的數據遷移問題,一致性哈希算法應運而生。

對於一致性哈希算法,官方說法如下:

一致性哈希算法在1997年由麻省理工學院提出,是一種特殊的哈希算法,在移除或者添加一個服務器時,能夠盡可能小地改變已存在的服務請求與處理請求服務器之間的映射關係。一致性哈希解決了簡單哈希算法在分佈式哈希表( Distributed Hash Table,DHT) 中存在的動態伸縮等問題。

什麼意思呢?我用大白話加畫圖的方式給你簡單的介紹一下。

一致性哈希,你可以想象成一個哈希環,它由0到2^32-1個點組成。A,B,C分別是三台服務器,每一台的IP加端口經過哈希計算后的值,在哈希環上對應如下:

當請求到來時,對請求中的某些參數進行哈希計算后,也會得出一個哈希值,此值在哈希環上也會有對應的位置,這個請求會沿着順時針的方向,尋找最近的服務器來處理它,如下圖所示:

一致性哈希就是這麼個東西。那它是怎麼解決服務器的擴容或收縮導致大量的數據遷移的呢?

看一下當我們使用一致性哈希算法時,加入服務器會發什麼事情。

當我們加入一個D服務器后,假設其IP加端口,經過哈希計算后落在了哈希環上圖中所示的位置。

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

這時影響的範圍只有圖中標註了五角星的區間。這個區間的請求從原來的由C服務器處理變成了由D服務器請求。而D到C,C到A,A到B這個區間的請求沒有影響,加入D節點后,A、B服務器是無感知的。

所以,在一致性哈希算法中,如果增加一台服務器,則受影響的區間僅僅是新服務器(D)在哈希環空間中,逆時針方向遇到的第一台服務器(B)之間的區間,其它區間(D到C,C到A,A到B)不會受到影響。

在加入了D服務器的情況下,我們再假設一段時間后,C服務器宕機了:

當C服務器宕機后,影響的範圍也是圖中標註了五角星的區間。C節點宕機后,B、D服務器是無感知的。

所以,在一致性哈希算法中,如果宕機一台服務器,則受影響的區間僅僅是宕機服務器(C)在哈希環空間中,逆時針方向遇到的第一台服務器(D)之間的區間,其它區間(C到A,A到B,B到D)不會受到影響。

綜上所述,在一致性哈希算法中,不管是增加節點,還是宕機節點,受影響的區間僅僅是增加或者宕機服務器在哈希環空間中,逆時針方向遇到的第一台服務器之間的區間,其它區間不會受到影響。

是不是很完美?

不是的,理想和現實的差距是巨大的。

一致性哈希算法帶來了什麼問題?

當節點很少的時候可能會出現這樣的分佈情況,A服務會承擔大部分請求。這種情況就叫做數據傾斜。

怎麼解決數據傾斜呢?加入虛擬節點。

怎麼去理解這個虛擬節點呢?

首先一個服務器根據需要可以有多個虛擬節點。假設一台服務器有n個虛擬節點。那麼哈希計算時,可以使用IP+端口+編號的形式進行哈希值計算。其中的編號就是0到n的数字。由於IP+端口是一樣的,所以這n個節點都是指向的同一台機器。

如下圖所示:

在沒有加入虛擬節點之前,A服務器承擔了絕大多數的請求。但是假設每個服務器有一個虛擬節點(A-1,B-1,C-1),經過哈希計算后落在了如上圖所示的位置。那麼A服務器的承擔的請求就在一定程度上(圖中標註了五角星的部分)分攤給了B-1、C-1虛擬節點,實際上就是分攤給了B、C服務器。

一致性哈希算法中,加入虛擬節點,可以解決數據傾斜問題。

當你在面試的過程中,如果聽到了類似於數據傾斜的字眼。那大概率是在問你一致性哈希算法和虛擬節點。

在介紹了相關背景后,我們可以去看看一致性哈希算法在Dubbo中的應用了。

一致性哈希算法在Dubbo中的應用

前面我們說了Dubbo中負載均衡的實現是通過org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance中的 doSelect 抽象方法實現的,一致性哈希負載均衡的實現類如下所示:

org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

由於一致性哈希實現類看起來稍微有點抽象,不太好演示,所以我想到了一個”騷”操作。前面的文章說過 LoadBalance 是一個 SPI 接口:

既然是一個 SPI 接口,那我們可以自己擴展一個一模一樣的算法,只是在算法裏面加入一點輸出語句方便我們觀察情況。怎麼擴展 SPI 接口就不描述了,只要記住代碼裏面的輸出語句都是額外加的,此外沒有任何改動即可,如下:

整個類如下圖片所示,請先看完整個類,有一個整體的概念后,我會進行方法級別的分析。

圖片很長,其中我加了很多註釋和輸出語句,可以點開大圖查看,一定會幫你更加好的理解一致性哈希在Dubbo中的應用:

改造之後,我們先把程序跑起來,有了輸出就好分析了。

服務端代碼如下:

其中的端口是需要手動修改的,我分別啟動服務在20881和20882端口。

項目中provider.xml配置如下:

consumer.xml配置如下:

然後,啟動在20881和20882端口分別啟動兩個服務端。客戶端消費如下:

運行結果輸出如下,可以先看個大概的輸出,下面會對每一部分輸出進行逐一的解讀

好了,用例也跑起來了,日誌也有了。接下來開始結合代碼和日誌進行方法級別的分析。

首先是doSelect方法的入口:

從上圖我們知道了,第一次調用需要對selectors進行put操作,selectors的 key 是接口中定義的方法,value 是 ConsistentHashSelector 內部類

ConsistentHashSelector通過調用其構造函數進行初始化的。invokers(服務端)作為參數傳遞到了構造函數中,構造函數裏面的邏輯,就是把服務端映射到哈希環上的過程,請看下圖,結合代碼,仔細分析輸出數據:

從上圖可以看出,當 ConsistentHashSelector 的構造方法調用完成后,8個虛擬節點在哈希環上已經映射完成。兩台服務器,每一台4個虛擬節點組成了這8個虛擬節點。

doSelect方法繼續執行,並打印出每個虛擬節點的哈希值和對應的服務端,請仔細品讀下圖:

說明一下:上面圖中的哈希環是沒有考慮比例的,僅僅是展現了兩個服務器在哈希環上的相對位置。而且為了演示說明方便,僅僅只有8個節點。假設我們有4台服務器,每台服務器的虛擬節點是默認值(160),這個情況下哈希環上一共有160*4=640個節點。

哈希環映射完成后,接下來的邏輯是把這次請求經過哈希計算后,映射到哈希環上,並順時針方向尋找遇到的第一個節點,讓該節點處理該請求:

還記得地址為 468e8565 的 A 服務器是什麼端口嗎?前面的圖片中有哦,該服務對應的端口是 20882 。

最後我們看看輸出結果:

和我們預期的一致。整個調用就算是完成了。

再對兩個方法進行一個補充說明。

第一個方法是 selectForKey,這個方法裏面邏輯如下圖所示:

虛擬節點都存儲在 TreeMap 中。順時針查詢的邏輯由 TreeMap 保證。看一下下面的 Demo 你就明白了。

第二個方法是 hash 方法,其中的 & 0xFFFFFFFFL 的目的如下:

&是位運算符,而 0xFFFFFFFFL 轉換為四字節表現后,其低32位全是1,所以保證了哈希環的範圍是 [0,Integer.MAX_VALUE]:

所以這裏我們可以改造這個哈希環的範圍,假設我們改為 100000。十進制的 100000 對於的 16 進製為 186A0 。所以我們改造后的哈希算法為:

再次調用后可以看到,計算后的哈希值都在10萬以內。但是分佈極不均勻,說明修改數據后這個哈希算法不是一個優秀的哈希算法:

以上,就是對一致性哈希算法在Dubbo中的實現的解讀。需要特殊說明一下的是,一致性哈希負載均衡策略和權重沒有任何關係

我又發現了一個BUG

前面我介紹了Dubbo 2.6.5版本之前,最小活躍數算法的兩個 bug。

很不幸,這次我又發現了Dubbo 2.7.4.1版本,一致性哈希負載均衡策略的一個bug,我提交了issue 地址如下:

https://github.com/apache/dubbo/issues/5429

我在這裏詳細說一下這個Bug現象、原因和我的解決方案。

現象如下,我們調用三次服務端:

輸出日誌如下(有部分刪減):

可以看到,在三次調用的過程中並沒有發生服務的上下線操作,但是每一次調用都重新進行了哈希環的映射。而我們預期的結果是應該只有在第一次調用的時候進行哈希環的映射,如果沒有服務上下線的操作,後續請求根據已經映射好的哈希環進行處理。

上面輸出的原因是由於每次調用的invokers的identityHashCode發生了變化:

我們看一下三次調用invokers的情況:

經過debug我們可以看出因為每次調用的invokers地址值不是同一個,所以System.identityHashCode(invokers)方法返回的值都不一樣。

接下來的問題就是為什麼每次調用的invokers地址值都不一樣呢?

經過Debug之後,可以找到這個地方:

org.apache.dubbo.rpc.cluster.RouterChain#route

問題就出在這個TagRouter中:

org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker

所以,在TagRouter中的stream操作,改變了invokers,導致每次調用時其

System.identityHashCode(invokers)返回的值不一樣。所以每次調用都會進行哈希環的映射操作,在服務節點多,虛擬節點多的情況下會有一定的性能問題。

到這一步,問題又發生了變化。這個TagRouter怎麼來的呢

如果了解Dubbo 2.7.x版本新特性的朋友可能知道,標籤路由是Dubbo2.7引入的新功能。

通過加載下面的配置加載了RouterFactrory:

META-INF\dubbo\internal\org.apache.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0版本之前)

META-INF\dubbo\internal\com.alibaba.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0之前)

下面是Dubbo 2.6.7(2.6.x的最後一個版本)和Dubbo 2.7.0版本該文件的對比:

可以看到確實是在 Dubbo 2.7.0 之後引入了 TagRouter。

至此,Dubbo 2.7.0 版本之後,一致性哈希負載均衡算法的 Bug 的來龍去脈也介紹清楚了。

解決方案是什麼呢?特別簡單,把獲取 identityHashCode 的方法從 System.identityHashCode(invokers) 修改為 invokers.hashCode() 即可。

此方案是我提的 issue 裏面的評論,這裏 System.identityHashCode 和 hashCode 之間的聯繫和區別就不進行展開講述了,不清楚的大家可以自行了解一下。

(我的另外一篇文章:夠強!一行代碼就修復了我提的Dubbo的Bug。)

改完之後,我們再看看運行效果:

可以看到第二次調用的時候並沒有進行哈希環的映射操作,而是直接取到了值,進行調用。

加入節點,畫圖分析

最後,我再分析一種情況。在A、B、C三個服務器(20881、20882、20883端口)都在正常運行,哈希映射已經完成的情況下,我們再啟動一個D節點(20884端口),這時的日誌輸出和對應的哈希環變化情況如下:

根據日誌作圖如下:

根據輸出日誌和上圖再加上源碼,你再細細回味一下。我個人覺得還是講的非常詳細了。

一致性哈希的應用場景

當大家談到一致性哈希算法的時候,首先的第一印象應該是在緩存場景下的使用,因為在一個優秀的哈希算法加持下,其上下線節點對整體數據的影響(遷移)都是比較友好的。

但是想一下為什麼 Dubbo 在負載均衡策略裏面提供了基於一致性哈希的負載均衡策略?它的實際使用場景是什麼?

我最開始也想不明白。我想的是在 Dubbo 的場景下,假設需求是想要一個用戶的請求一直讓一台服務器處理,那我們可以採用一致性哈希負載均衡策略,把用戶號進行哈希計算,可以實現這樣的需求。但是這樣的需求未免有點太牽強了,適用場景略小。

直到有天晚上,我睡覺之前,電光火石之間突然想到了一個稍微適用的場景了。

如果需求是需要保證某一類請求必須順序處理呢?

如果你用其他負載均衡策略,請求分發到了不同的機器上去,就很難保證請求的順序處理了。比如A,B請求要求順序處理,現在A請求先發送,被負載到了A服務器上,B請求后發送,被負載到了B服務器上。而B服務器由於性能好或者當前沒有其他請求或者其他原因極有可能在A服務器還在處理A請求之前就把B請求處理完成了。這樣不符合我們的要求。

這時,一致性哈希負載均衡策略就上場了,它幫我們保證了某一類請求都發送到固定的機器上去執行。比如把同一個用戶的請求發送到同一台機器上去執行,就意味着把某一類請求發送到同一台機器上去執行。所以我們只需要在該機器上運行的程序中保證順序執行就行了,比如你加一個隊列。

一致性哈希算法+隊列,可以實現順序處理的需求。

好了,一致性哈希負載均衡算法就寫到這裏。

繼續進入下一個議題。

加權輪詢負載均衡

這一小節是對於Dubbo負載均衡策略之一的加權隨機算法的詳細分析。

從 2.6.4 版本聊起,該版本在某些情況下存在着比較嚴重的性能問題。由問題入手,層層深入,了解該算法在 Dubbo 中的演變過程,讀懂它的前世今生。

什麼是輪詢?

在描述加權輪詢之前,先解釋一下什麼是輪詢算法,如下圖所示:

假設我們有A、B、C三台服務器,共計處理6個請求,服務處理請求的情況如下:

  1. 第一個請求發送給了A服務器
  2. 第二個請求發送給了B服務器
  3. 第三個請求發送給了C服務器
  4. 第四個請求發送給了A服務器
  5. 第五個請求發送給了B服務器
  6. 第六個請求發送給了C服務器
  7. ……

上面這個例子演示的過程就叫做輪詢。可以看出,所謂輪詢就是將請求輪流分配給每台服務器

輪詢的優點是無需記錄當前所有服務器的鏈接狀態,所以它一種無狀態負載均衡算法,實現簡單,適用於每台服務器性能相近的場景下。

輪詢的缺點也是顯而易見的,它的應用場景要求所有服務器的性能都相同,非常的局限。

大多數實際情況下,服務器性能是各有差異,針對性能好的服務器,我們需要讓它承擔更多的請求,即需要給它配上更高的權重。

所以加權輪詢,應運而生。

什麼是加權輪詢?

為了解決輪詢算法應用場景的局限性。當遇到每台服務器的性能不一致的情況,我們需要對輪詢過程進行加權,以調控每台服務器的負載。

經過加權后,每台服務器能夠得到的請求數比例,接近或等於他們的權重比。比如服務器 A、B、C 權重比為 5:3:2。那麼在10次請求中,服務器 A 將收到其中的5次請求,服務器 B 會收到其中的3次請求,服務器 C 則收到其中的2次請求。

這裏要和加權隨機算法做區分哦。直接把前面介紹的加權隨機算法畫的圖拿過來:

上面這圖是按照比例畫的,可以直觀的看到,對於某一個請求,區間(權重)越大的服務器,就越可能會承擔這個請求。所以,當請求足夠多的時候,各個服務器承擔的請求數,應該就是區間,即權重的比值。

假設有A、B、C三台服務器,權重之比為5:3:2,一共處理10個請求。

那麼負載均衡採用加權隨機算法時,很有可能A、B服務就處理完了這10個請求,因為它是隨機調用。

採用負載均衡採用輪詢加權算法時,A、B、C服務一定是分別承擔5、3、2個請求。

Dubbo2.6.4版本的實現

對於Dubbo2.6.4版本的實現分析,可以看下圖,我加了很多註釋,其中的輸出語句都是我加的:

示例代碼還是沿用之前文章中的Demo,這裏分別在 20881、20882、20883 端口啟動三個服務,各自的權重分別為 1,2,3。

客戶端調用 8 次:

輸出結果如下:

可以看到第七次調用后mod=0,回到了第一次調用的狀態。形成了一個閉環。

再看看判斷的條件是什麼:

其中mod在代碼中扮演了極其重要的角色,mod根據一個方法的調用次數不同而不同,取值範圍是[0,weightSum)。

因為weightSum=6,所以列舉mod不同值時,最終的選擇結果和權重變化:

可以看到20881,20882,20883承擔的請求數量比值為1:2:3。同時我們可以看出,當 mod >= 1 后,20881端口的服務就不會被選中了,因為它的權重被減為0了。當 mod >= 4 后,20882端口的服務就不會被選中了,因為它的權重被減為0了。

結合判斷條件和輸出結果,我們詳細分析一下(下面內容稍微有點繞,如果看不懂,多結合上面的圖片看幾次):

第一次調用

mod=0,第一次循環就滿足代碼塊①的條件,直接返回當前循環的invoker,即20881端口的服務。此時各端口的權重情況如下:

第二次調用

mod=1,需要進入代碼塊②,對mod進行一次遞減。

第一次循環對20881端口的服務權重減一,mod-1=0。

第二次循環,mod=0,循環對象是20882端口的服務,權重為2,滿足代碼塊①,返回當前循環的20882端口的服務。

此時各端口的權重情況如下:

第三次調用

mod=2,需要進入代碼塊②,對mod進行兩次遞減。

第一次循環對20881端口的服務權重減一,mod-1=1;

第二次循環對20882端口的服務權重減一,mod-1=0;

第三次循環時,mod已經為0,當前循環的是20883端口的服務,權重為3,滿足代碼塊①,返回當前循環的20883端口的服務。

此時各端口的權重情況如下:

第四次調用

mod=3,需要進入代碼塊②,對mod進行三次遞減。

第一次循環對20881端口的服務權重減一,從1變為0,mod-1=2;

第二次循環對20882端口的服務權重減一,從2變為1,mod-1=1;

第三次循環對20883端口的服務權重減一,從3變為2,mod-1=0;

第四次循環的是20881端口的服務,此時mod已經為0,但是20881端口的服務的權重已經變為0了,不滿足代碼塊①和代碼塊②,進入第五次循環。

第五次循環的是20882端口的服務,當前權重為1,mod=0,滿足代碼塊①,返回20882端口的服務。

此時各端口的權重情況如下:

第五次調用

mod=4,需要進入代碼塊②,對mod進行四次遞減。

第一次循環對20881端口的服務權重減一,從1變為0,mod-1=3;

第二次循環對20882端口的服務權重減一,從2變為1,mod-1=2;

第三次循環對20883端口的服務權重減一,從3變為2,mod-1=1;

第四次循環的是20881端口的服務,此時mod為1,但是20881端口的服務的權重已經變為0了,不滿足代碼塊②,mod不變,進入第五次循環。

第五次循環時,mod為1,循環對象是20882端口的服務,權重為1,滿足代碼塊②,權重從1變為0,mod從1變為0,進入第六次循環。

第六次循環時,mod為0,循環對象是20883端口的服務,權重為2,滿足條件①,返回當前20883端口的服務。

此時各端口的權重情況如下:

第六次調用

第六次調用,mod=5,會循環九次,最終選擇20883端口的服務,讀者可以自行分析一波,分析出來了,就了解的透透的了。

第七次調用

第七次調用,又回到mod=0的狀態:

2.6.4版本的加權輪詢就分析完了,但是事情並沒有這麼簡單。這個版本的加權輪詢是有性能問題的。

該問題對應的issue地址如下:

https://github.com/apache/dubbo/issues/2578

問題出現在invoker返回的時機上:

截取issue裏面的一個回答:

10分鐘才選出一個invoker,還怎麼玩?

有時間可以讀一讀這個issue,裏面各路大神針對該問題進行了激烈的討論,第一種改造方案被接受后,很快就被推翻,被第二種方案代替,可以說優化思路十分值得學習,很精彩,接下來的行文路線就是按照該issue展開的。

推翻,重建。

上面的代碼時間複雜度是O(mod),而第一次修復之後時間複雜度降低到了常量級別。可以說是一次非常優秀的優化,值得我們學習,看一下優化之後的代碼:

其關鍵優化的點是這段代碼,我加入輸出語句,便於分析。

輸出日誌如下:

把上面的輸出轉化到表格中去,7次請求的選擇過程如下:

該算法的原理是:

把服務端都放到集合中(invokerToWeightList),然後獲取服務端個數(length),並計算出服務端權重最大的值(maxWeight)。

index表示本次請求到來時,處理該請求的服務端下標,初始值為0,取值範圍是[0,length)。

currentWeight表示當前調度的權重,初始值為0,取值範圍是[0,maxWeight)。

當請求到來時,從index(就是0)開始輪詢服務端集合(invokerToWeightList),如果是一輪循環的開始(index=0)時,則對currentWeight進行加一操作(不會超過maxWeight),在循環中找出第一個權重大於currentWeight的服務並返回。

這裏說的一輪循環是指index再次變為0所經歷過的循環,這裏可以把index=0看做是一輪循環的開始。每一輪循環的次數與Invoker的數量有關,Invoker數量通常不會太多,所以我們可以認為上面代碼的時間複雜度為常數級。

從issue上看出,這個算法最終被merged了。

但是很快又被推翻了:

這個算法不夠平滑。什麼意思呢?

翻譯一下上面的內容就是:服務器[A, B, C]對應權重[5, 1, 1]。進行7次負載均衡后,選擇出來的序列為[A, A, A, A, A, B, C]。前5個請求全部都落在了服務器A上,這將會使服務器A短時間內接收大量的請求,壓力陡增。而B和C此時無請求,處於空閑狀態。而我們期望的結果是這樣的[A, A, B, A, C, A, A],不同服務器可以穿插獲取請求。

我們設置20881端口的權重為5,20882、20883端口的權重均為1。

進行實驗,發現確實如此:可以看到一共進行7次請求,第1次到5次請求都分發給了權重為5的20881端口的服務,前五次請求,20881和20882都處於空閑狀態:

轉化為表格如下:

從表格的最終結果一欄也可以直觀的看出,七次請求對應的服務器端口為:

分佈確實不夠均勻。

再推翻,再重建,平滑加權。

從issue中可以看到,再次重構的加權算法的靈感來源是Nginx的平滑加權輪詢負載均衡

看代碼之前,先介紹其計算過程。

假設每個服務器有兩個權重,一個是配置的weight,不會變化,一個是currentWeight會動態調整,初始值為0。當有新的請求進來時,遍歷服務器列表,讓它的currentWeight加上自身權重。遍歷完成后,找到最大的currentWeight,並將其減去權重總和,然後返回相應的服務器即可。

如果你還是不知道上面的表格是如何算出來的,我再給你詳細的分析一下第1、2個請求的計算過程:

第一個請求計算過程如下:

第二個請求計算過程如下:

後面的請求你就可以自己分析了。

從表格的最終結果一欄也可以直觀的看出,七次請求對應的服務器端口為:

可以看到,權重之比同樣是5:1:1,但是最終的請求分發的就比較的”平滑”。對比一下:

對於平滑加權算法,我想多說一句。我覺得這個算法非常的神奇,我是徹底的明白了它每一步的計算過程,知道它最終會形成一個閉環,但是我想了很久,我還是不知道背後的數學原理是什麼,不明白為什麼會形成一個閉環,非常的神奇。

很正常,我不糾結的,程序猿的工作不就是這樣嗎?我也不知道為什麼,它能工作。別問,問就是玄學,如果一定要說出點什麼的話,我想,我願稱之為:絕活吧。

但是我們只要能夠理解我前面所表達的平滑加權輪詢算法的計算過程,知道其最終會形成閉環,就能理解下面的代碼。配合代碼中的註釋食用,效果更佳。

以下代碼以及註釋來源官網:

http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html

總結

好了,到這裏關於Dubbo的五種負載均衡策略就講完了。簡單總結一下:(加權隨機算法在講最小活躍數算法的時候提到過,因為原理十分簡單,這裏就不專門拿出章節來描述了。)

最短響應時間負載均衡:在所有服務提供者中選出平均響應時間最短的一個,如果能選出來,則使用選出來的一個。如果不能選出來多個,再根據權重選,如果權重也一樣,則隨機選擇。

一致性哈希負載均衡:在一致性哈希算法中,不管是增加節點,還是宕機節點,受影響的區間僅僅是增加或者宕機服務器在哈希環空間中,逆時針方向遇到的第一台服務器之間的區間,其它區間不會受到影響。為了解決數據傾斜的問題,引入了虛擬節點的概念。一致性哈希算法是 Dubbo 中唯一一個與權重沒有任何關係的負載均衡算法,可以保證相同參數的請求打到同一台機器上。

最小活躍數負載均衡:需要配合 activeFilter 使用,活躍數在方法調用前後進行維護,響應越快的服務器堆積的請求越少,對應的活躍數也少。Dubbo 在選擇的時候遵循下面的規則,有最小活躍數用最小活躍數,沒有最小活躍數根據權重選擇,權重一樣則隨機返回的負載均衡算法。

加權隨機算法:隨機,顧名思義,就是從多個服務提供者中隨機選擇一個出來。加權,就是指需要按照權重設置隨機概率。常見場景就是對於性能好的機器可以把對應的權重設置的大一點,而性能相對較差的,權重設置的小一點。哎,像極了這個社會上的某些現象,對外宣傳是隨機搖號,背後指不定有一群權重高的人呢。

加權輪詢負載均衡:輪詢就是雨露均沾的意思,所有的服務提供者都需要調用。而當輪詢遇到加權則可以讓請求(不論多少)嚴格按照我們的權重之比進行分配。比如有A、B、C三台服務器,權重之比為5:3:2,一共處理10個請求。那麼採用負載均衡採用輪詢加權算法時,A、B、C服務一定是分別承擔5、3、2個請求。同時需要注意的是加權輪詢算法的兩次升級過程,以及最終的“平滑”的解決方案。

再說一件事

本文主要聊的是負載均衡嘛,讓我想起了 2019 年阿里巴巴第五屆中間件挑戰賽的初賽賽題也是實現一個負載均衡策略。

具體的賽題可以看這裏:

https://tianchi.aliyun.com/competition/entrance/231714/information

這種比賽還是很有意思的,你報名之後僅僅是讀懂賽題,然後自己多想想怎麼實現,哪怕是不提交代碼,在比賽完成后看前幾名的賽題分析,再去把他們的代碼拉下來看看,你就會發現,其實最終的思路都大同小異,差別會體現在參數調優和代碼優化程度上。

當然最大的差別還是會體現在語言的層面。如果不限制參數語言的話,Java 系的选手一定是被 C 系选手吊打的。

但是,被吊打不重要,重要的是真的能學到很多的東西,而這些東西,在絕大部分工作中是很難學到的。

最近,阿里巴巴第六屆中間件挑戰賽也開始了,可以看一下這個鏈接:

https://tianchi.aliyun.com/competition/entrance/231790/introduction?spm=5176.12281968.1008.3.65818188YmzFqa

這次是分為三個賽道,選擇性更多了。

作為這個比賽的長期關注者(持續關注三年了吧),這次作為一個自來水免費宣傳一波。

朋友,我真心建議你去看一下,報個名,玩一玩,收穫真的很大的。

當然,如果能在報名的時候邀請人那一欄填【why技術】,我真心感謝你。

最後說一句(求關注)

點個“贊”吧,周更很累的,不要白嫖我,需要一點正反饋。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言指出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。

歡迎關注我的微信公眾號:why技術。在這裏我會分享一些java技術相關的知識,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評、影評。感謝你的關注,願你我共同進步。

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

Elasticsearch到底哪點好?_台中搬家

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

少點代碼,多點頭髮

本文已經收錄至我的GitHub,歡迎大家踴躍star 和 issues。

https://github.com/midou-tech/articles

從今天開始準備給大家帶來全新的一系列文章,Elasticsearch系列

新系列肯定會有很多疑惑,先為大家答疑解惑,下面是今天要講的問題

為什麼寫Elasticsearch系列文章?

之前在文章中也陸陸續續的提到過,龍叔是做搜索引擎的。搜索引擎技術屬於商業技術,大家耳熟能詳的百度搜索,Google搜索,這可都是因為把握核心搜索技術,從而誕生了商業帝國。

每個互聯網大廠都想去分一杯搜索的羹,360搜索、神馬、頭條、搜狗搜索等等,由此可見搜索技術的商業作用和機密性了。

搜索把握用戶的入口

蘑菇街的搜索引擎是一款使用C++開發、完全自研、沒有開源的搜索引擎,沒有開源就是不能隨便寫出來的。

但是現在不一樣了

第一、我離職了,離開了意味着不在持有那些商業機密了,就算不講出來我也沒啥心理負擔(但還是不能講的,離職協議寫的很清楚,不能泄露公司商業機密)。

第二、去新的公司還是在搜索領域,他們用Es Elasticsearch是一個開源搜索,開源的東西可以隨便說,但還是不能說公司的商業數據

自己一直在搜索領域做,輸出搜索相關的文章,第一個可以讓自己更好的學習和總結,第二個可以讓粉絲們了解到搜索這個神秘的技術,增加大家自身的核心競爭力。

後面會說到,Elasticsearch是搜索引擎,但不簡單隻能使用在搜索領域,他可以作用的場景非常多。

Elasticsearch是什麼?

Elasticsearch 是一個分佈式的開源搜索分析引擎,適用於所有類型的數據,包括文本、数字、地理空間、結構化和非結構化數據。

Elasticsearch 在 Apache Lucene 的基礎上開發而成,Elasticsearch 以其簡單的 REST 風格 API、分佈式特性、速度和可擴展性而聞名,是 Elastic Stack 的核心組件。

Elastic Stack 是適用於數據採集、充實、存儲、分析和可視化的一組開源工具。人們通常將 Elastic Stack 稱為 ELK Stack(代指 Elasticsearch、Logstash 和 Kibana),目前 Elastic Stack 包括一系列豐富的輕量型數據採集代理,這些代理統稱為 Beats,可用來向 Elasticsearch 發送數據。

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

Elasticsearch 的實現原理主要分為以下幾個步驟,首先用戶將數據提交到Elasticsearch 數據中心,再通過分詞控制器去將對應的數據分詞,將其權重和分詞結果一併存入數據,當用戶搜索數據時候,再根據權重將結果排名,打分,再將返回結果呈現給用戶。

是什麼差不多搞清楚了,再說說ES都哪些成熟的應用以及在哪些領域使用。

Elasticsearch在哪些領域使用?

  • 應用程序搜索
  • 網站搜索
  • 企業搜索
  • 日誌處理和分析
  • 基礎設施指標和容器監測
  • 應用程序性能監測
  • 地理空間數據分析和可視化
  • 安全分析
  • 業務分析

Elasticsearch有哪些特點?

Elasticsearch 很快。 由於 Elasticsearch 是在 Lucene 基礎上構建而成的,所以在全文本搜索方面表現十分出色。Elasticsearch 同時還是一個近實時的搜索平台,這意味着從文檔索引操作到文檔變為可搜索狀態之間的延時很短,一般只有一秒。因此,Elasticsearch 非常適用於對時間有嚴苛要求的用例,例如安全分析和基礎設施監測。

Elasticsearch 具有分佈式的本質特徵。 Elasticsearch 中存儲的文檔分佈在不同的容器中,這些容器稱為分片,可以進行複製以提供數據冗餘副本,以防發生硬件故障。Elasticsearch 的分佈式特性使得它可以擴展至數百台(甚至數千台)服務器,並處理 PB 量級的數據。

Elasticsearch 包含一系列廣泛的功能。 除了速度、可擴展性和彈性等優勢以外,Elasticsearch 還有大量強大的內置功能(例如數據匯總和索引生命周期管理),可以方便用戶更加高效地存儲和搜索數據。

Elastic Stack 簡化了數據採集、可視化和報告過程。 通過與 Beats 和 Logstash 進行集成,用戶能夠在向 Elasticsearch 中索引數據之前輕鬆地處理數據。同時,Kibana 不僅可針對 Elasticsearch 數據提供實時可視化,同時還提供 UI 以便用戶快速訪問應用程序性能監測 (APM)、日誌和基礎設施指標等數據。

學習Elasticsearch能提高哪些競爭力?

看到Elasticsearch在這麼多的領域在使用,特點也這麼明顯。看到這裏估計都不用在說什麼核心競爭力,你已經意識到了。

Elastic 於 2018 年 6 月 29 日正式推出 Elastic Certified Engineer 認證考試,認證通過可以獲得官方頒發的證書和徽章,title就是 Elastic認證工程師

具體認證的細節和含金量,沒有具體研究過,但是可以很明顯的感受到官方出了這樣一個認證,表明社會需要大量這樣的人才,而這方面人才的培養和考核指標還欠缺。

有沒有必要一定要考這個認證?

個人覺得,和英語四六級一樣,通過了再說沒用。

如果你是學生,可以考慮去考一個認證,因為你很難有業務場景驅使你去做這方面的成長,認證一定是有難度的,一個一個的困難會驅使你成長,最終這個認證也會成為招聘時一個非常大的亮點。

這個認證會有哪些幫助?

  • 對於快速的構建知識體系幫助。

  • 對於全面的熟悉官方文檔幫助。

  • 對於實戰解決線上問題幫助。(遇到了相關技術問題基本上不需要再求助於社區,80%以上的問題自己基本就能解決。)

  • 對於增強信心、克服英文恐懼幫助。

Elasticsearch 支持哪些編程語言?

  • Java
  • JavaScript (Node.js)
  • Go
  • .NET (C#)
  • PHP
  • Perl
  • Python
  • Ruby

哪裡可以找到有關 Elasticsearch 的更多信息?

  • Elasticsearch GitHub 存儲庫:https://github.com/elastic
  • Elasticsearch 官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  • Elasticsearch中文社區:https://elasticsearch.cn

我是龍叔,一個分享互聯網技術和心路歷程的star。

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

Netty學習筆記(番外篇) – ChannelHandler、ChannelPipeline和ChannelHandlerContext的聯繫_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

這一篇是 ChannelHandler 和 ChannelPipeline 的番外篇,主要從源碼的角度來學習 ChannelHandler、ChannelHandler 和 ChannelPipeline 相互之間是如何建立聯繫和運行的。

一、添加 ChannelHandler

從上一篇的 demo 中可以看到在初始化 Server 和 Client 的時候,都會通過 ChannelPipeline 的 addLast 方法將 ChannelHandler 添加進去

// Server.java

// 部分代碼片段
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
serverBootstrap.group(group)
        .channel(NioServerSocketChannel.class)Channel
        .localAddress(new InetSocketAddress("localhost", 9999))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                // 添加ChannelHandler
                socketChannel.pipeline().addLast(new OneChannelOutBoundHandler());

                socketChannel.pipeline().addLast(new OneChannelInBoundHandler());
                socketChannel.pipeline().addLast(new TwoChannelInBoundHandler());
            }
        });

在上面的代碼片段中,socketChannel.pipeline()方法返回的是一個類型是 DefaultChannelPipeline 的實例,DefaultChannelPipeline 實現了 ChannelPipeline 接口

DefaultChannelPipeline 的 addLast 方法實現如下:

// DefaultChannelPipeline.java

@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
    return addLast(null, handlers);
}

@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
    ObjectUtil.checkNotNull(handlers, "handlers");

    for (ChannelHandler h: handlers) {
        if (h == null) {
            break;
        }
        addLast(executor, null, h);
    }

    return this;
}

經過一系列重載方法調用,最終進入到下面的 addLast 方法

// DefaultChannelPipeline.java

@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    synchronized (this) {
        checkMultiplicity(handler);

        newCtx = newContext(group, filterName(name, handler), handler);

        addLast0(newCtx);

        // If the registered is false it means that the channel was not registered on an eventLoop yet.
        // In this case we add the context to the pipeline and add a task that will call
        // ChannelHandler.handlerAdded(...) once the channel is registered.
        if (!registered) {
            newCtx.setAddPending();
            callHandlerCallbackLater(newCtx, true);
            return this;
        }

        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }
    callHandlerAdded0(newCtx);
    return this;
}

在這個方法實現中,利用傳進來的 ChannelHandler 在 newContext 創建了一個 AbstractChannelHandlerContext 對象。newContext 方法實現如下:

// DefaultChannelPipeline.java

private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
    return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}

這裏創建並返回了一個類型為 DefaultChannelHandlerContext 的對象。從傳入的參數可以看到,在這裏將 ChannelHandlerContext、ChannelPipeline(this)和 ChannelHandler 三者建立了關係。
最後再看看 addLast0 方法實現:

// DefaultChannelPipeline.java

private void addLast0(AbstractChannelHandlerContext newCtx) {
    AbstractChannelHandlerContext prev = tail.prev;
    newCtx.prev = prev;
    newCtx.next = tail;
    prev.next = newCtx;
    tail.prev = newCtx;
}

這裏出現了 AbstractChannelHandlerContext 的兩個屬性 prev 和 next,而 DefaultChannelPipeline 有一個屬性 tail。從實現邏輯上看起來像是建立了一個雙向鏈表的結構。下面的代碼片段是關於 tail 和另一個相關屬性 head:

// DefaultChannelPipeline.java

public class DefaultChannelPipeline implements ChannelPipeline {
    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;

    // ......
    protected DefaultChannelPipeline(Channel channel) {
        // ......

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

    // ......
}

// HeaderContext.java
final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
    // ......

    @Override
    public ChannelHandler handler() {
        return this;
    }

    //......
}

// TailContext.java
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
    // ......

    @Override
    public ChannelHandler handler() {
        return this;
    }

    // ......
}

DefaultChannelPipeline 內部維護了兩個 AbstractChannelHandlerContext 類型的屬性 head、tail,而這兩個屬性又都實現了 ChannelHandler 的子接口。構造方法里將這兩個屬性維護成了一個雙向鏈表。結合上面的 addLast0 方法實現,可以知道在添加 ChannelHandler 的時候,其實是在對 ChannelPipeline 內部維護的雙向鏈表做插入操作。
下面是 ChannelHandlerContext 相關類的結構

所以,對 ChannelPipeline 做 add 操作添加 ChannelHandler 后,內部結構大體是這樣的:

所有的 ChannelHandlerContext 組成了一個雙向鏈表,頭部是 HeadContext,尾部是 TailContext,因為它們都實現了 ChannelHandler 接口,所以它們內部的 Handler 也是自己。每次添加一個 ChannelHandler,將會新創建一個 DefaultChannelHandler 關聯,並按照一定的順序插入到鏈表中。
在 AbstractChannelHandlerContext 類里有一個屬性 executionMask,在構造方法初始化時會對它進行賦值

// AbstractChannelHandlerContext.java

// 省略部分代碼

AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor,
                                String name, Class<? extends ChannelHandler> handlerClass) {
    this.name = ObjectUtil.checkNotNull(name, "name");
    this.pipeline = pipeline;
    this.executor = executor;
    this.executionMask = mask(handlerClass);
    // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
    ordered = executor == null || executor instanceof OrderedEventExecutor;
}

// 省略部分代碼

mask 是一個靜態方法,來自於 ChannelHandlerMask 類

// ChannelHandlerMask.java

// 省略部分代碼

/**
* Return the {@code executionMask}.
*/
static int mask(Class<? extends ChannelHandler> clazz) {
    // Try to obtain the mask from the cache first. If this fails calculate it and put it in the cache for fast
    // lookup in the future.
    Map<Class<? extends ChannelHandler>, Integer> cache = MASKS.get();
    Integer mask = cache.get(clazz);
    if (mask == null) {
        mask = mask0(clazz);
        cache.put(clazz, mask);
    }
    return mask;
}

/**
* Calculate the {@code executionMask}.
*/
private static int mask0(Class<? extends ChannelHandler> handlerType) {
    int mask = MASK_EXCEPTION_CAUGHT;
    try {
        if (ChannelInboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_INBOUND;

            if (isSkippable(handlerType, "channelRegistered", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_REGISTERED;
            }
            if (isSkippable(handlerType, "channelUnregistered", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_UNREGISTERED;
            }
            if (isSkippable(handlerType, "channelActive", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_ACTIVE;
            }
            if (isSkippable(handlerType, "channelInactive", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_INACTIVE;
            }
            if (isSkippable(handlerType, "channelRead", ChannelHandlerContext.class, Object.class)) {
                mask &= ~MASK_CHANNEL_READ;
            }
            if (isSkippable(handlerType, "channelReadComplete", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_READ_COMPLETE;
            }
            if (isSkippable(handlerType, "channelWritabilityChanged", ChannelHandlerContext.class)) {
                mask &= ~MASK_CHANNEL_WRITABILITY_CHANGED;
            }
            if (isSkippable(handlerType, "userEventTriggered", ChannelHandlerContext.class, Object.class)) {
                mask &= ~MASK_USER_EVENT_TRIGGERED;
            }
        }

        if (ChannelOutboundHandler.class.isAssignableFrom(handlerType)) {
            mask |= MASK_ALL_OUTBOUND;

            if (isSkippable(handlerType, "bind", ChannelHandlerContext.class,
                    SocketAddress.class, ChannelPromise.class)) {
                mask &= ~MASK_BIND;
            }
            if (isSkippable(handlerType, "connect", ChannelHandlerContext.class, SocketAddress.class,
                    SocketAddress.class, ChannelPromise.class)) {
                mask &= ~MASK_CONNECT;
            }
            if (isSkippable(handlerType, "disconnect", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_DISCONNECT;
            }
            if (isSkippable(handlerType, "close", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_CLOSE;
            }
            if (isSkippable(handlerType, "deregister", ChannelHandlerContext.class, ChannelPromise.class)) {
                mask &= ~MASK_DEREGISTER;
            }
            if (isSkippable(handlerType, "read", ChannelHandlerContext.class)) {
                mask &= ~MASK_READ;
            }
            if (isSkippable(handlerType, "write", ChannelHandlerContext.class,
                    Object.class, ChannelPromise.class)) {
                mask &= ~MASK_WRITE;
            }
            if (isSkippable(handlerType, "flush", ChannelHandlerContext.class)) {
                mask &= ~MASK_FLUSH;
            }
        }

        if (isSkippable(handlerType, "exceptionCaught", ChannelHandlerContext.class, Throwable.class)) {
            mask &= ~MASK_EXCEPTION_CAUGHT;
        }
    } catch (Exception e) {
        // Should never reach here.
        PlatformDependent.throwException(e);
    }

    return mask;
}

@SuppressWarnings("rawtypes")
private static boolean isSkippable(
        final Class<?> handlerType, final String methodName, final Class<?>... paramTypes) throws Exception {
    return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
        @Override
        public Boolean run() throws Exception {
            Method m;
            try {
                m = handlerType.getMethod(methodName, paramTypes);
            } catch (NoSuchMethodException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        "Class {} missing method {}, assume we can not skip execution", handlerType, methodName, e);
                }
                return false;
            }
            return m != null && m.isAnnotationPresent(Skip.class);
        }
    });
}

// 省略部分代碼

以上代碼實現邏輯是這樣的:當創建一個 ChannelHandlerContext 時,會與一個 ChannelHandler 綁定,同時會將傳遞進來的 ChannelHandler 進行解析,解析當前 ChannelHandler 支持哪些回調方法,並通過位運算得到一個結果保存在 ChannelHandlerContext 的 executionMask 屬性里。注意 m.isAnnotationPresent(Skip.class)這裏,ChannelHandler 的基類 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 里的回調方法上都有@Skip 註解,當繼承了這兩個類並重寫了某個回調方法后,這個方法上的註解就會被覆蓋掉,解析時就會被認為當前 ChannelHandler 支持這個回調方法。
下面是每個回調方法對應的掩碼

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

// ChannelHandlerMask.java

final class ChannelHandlerMask {
    // Using to mask which methods must be called for a ChannelHandler.
    static final int MASK_EXCEPTION_CAUGHT = 1;
    static final int MASK_CHANNEL_REGISTERED = 1 << 1;
    static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
    static final int MASK_CHANNEL_ACTIVE = 1 << 3;
    static final int MASK_CHANNEL_INACTIVE = 1 << 4;
    static final int MASK_CHANNEL_READ = 1 << 5;
    static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
    static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
    static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;
    static final int MASK_BIND = 1 << 9;
    static final int MASK_CONNECT = 1 << 10;
    static final int MASK_DISCONNECT = 1 << 11;
    static final int MASK_CLOSE = 1 << 12;
    static final int MASK_DEREGISTER = 1 << 13;
    static final int MASK_READ = 1 << 14;
    static final int MASK_WRITE = 1 << 15;
    static final int MASK_FLUSH = 1 << 16;

    static final int MASK_ONLY_INBOUND =  MASK_CHANNEL_REGISTERED |
            MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
            MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;
    private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND;
    static final int MASK_ONLY_OUTBOUND =  MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
            MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;
    private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
}

二、ChannelHandler 處理消息

我們以消息讀取和寫入為例,來看看在 ChannelPipeline 里的各個 ChannelHandler 是如何按照順序處理消息和事件的。

讀取消息

當 Channel 讀取到消息后,會在以下地方調用 ChannelPipeline 的 fireChannelRead 方法:

// AbstractNioMessageClient.java

private final class NioMessageUnsafe extends AbstractNioUnsafe {

    // 省略代碼

    @Override
    public void read() {
        // ......

        for (int i = 0; i < size; i ++) {
            readPending = false;
            pipeline.fireChannelRead(readBuf.get(i));
        }

        // ......
    }

    // 省略代碼
}

// DefaultChannelPipeline.java

// 省略代碼

@Override
public final ChannelPipeline fireChannelRead(Object msg) {
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}

// 省略代碼

可以看到,通過 AbstractChannelHandlerContext 的 invokeChannelRead 方法,傳遞 head,從頭部開始觸發讀取事件。

// AbstractChannelHandlerContext.java

// 省略代碼

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

private void invokeChannelRead(Object msg) {
    if (invokeHandler()) {
        try {
            ((ChannelInboundHandler) handler()).channelRead(this, msg);
        } catch (Throwable t) {
            invokeExceptionCaught(t);
        }
    } else {
        fireChannelRead(msg);
    }
}

/**
    * Makes best possible effort to detect if {@link ChannelHandler#handlerAdded(ChannelHandlerContext)} was called
    * yet. If not return {@code false} and if called or could not detect return {@code true}.
    *
    * If this method returns {@code false} we will not invoke the {@link ChannelHandler} but just forward the event.
    * This is needed as {@link DefaultChannelPipeline} may already put the {@link ChannelHandler} in the linked-list
    * but not called {@link ChannelHandler#handlerAdded(ChannelHandlerContext)}.
    */
private boolean invokeHandler() {
    // Store in local variable to reduce volatile reads.
    int handlerState = this.handlerState;
    return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
}

// 省略代碼

在這裏通過 invokeHandler 方法對當前 ChannelHandler 進行狀態檢查,通過了就將調用當前 ChannelHandler 的 channelRead 方法,沒有通過將調用 fireChannelRead 方法將事件傳遞到下一個 ChannelHandler 上。而 head 的類型是 HeadContext,本身也實現了 ChannelInBoundHandler 接口,所以這裏調用的是 HeadContext 的 channelRead 方法。

// DefaultChannelPipeline.java

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.fireChannelRead(msg);
    }
}

這裏對消息沒有做任何處理,直接將讀取消息傳遞下去。接下來看看 ChannelHandlerContext 的 fireChannelRead 做了什麼

// AbstractChannelHandlerContext.java

@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
    invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
    return this;
}

private AbstractChannelHandlerContext findContextInbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        ctx = ctx.next;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
    return ctx;
}

private static boolean skipContext(
        AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {
    // Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT
    return (ctx.executionMask & (onlyMask | mask)) == 0 ||
            // We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload
            // everything to preserve ordering.
            //
            // See https://github.com/netty/netty/issues/10067
            (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);
}

這裏實現的邏輯是這樣的:在雙向鏈表中,從當前 ChannelHandlerContext 節點向後尋找,直到找到匹配 MASK_CHANNEL_READ 這個掩碼的 ChannelHandlerContext。從上面的章節里可以直到 ChannelHandlerContext 的屬性里保存了當前 ChannelHandler 支持(重寫)的所有方法掩碼的位運算值,通過位運算的結果來找到實現了對應方法的最近的 ChannelHandlerContext。
鏈表最後一個節點是 TailContext

// DefaultChannelPipeline.java

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        onUnhandledInboundMessage(ctx, msg);
    }

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
        onUnhandledInboundMessage(msg);
        if (logger.isDebugEnabled()) {
            logger.debug("Discarded message pipeline : {}. Channel : {}.",
                         ctx.pipeline().names(), ctx.channel());
        }
    }

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}

可以看到,tail 節點的 channelRead 方法沒有將事件繼續傳遞下去,只是釋放了 msg。

寫入消息

我們通過 OneChannelInBoundHandler 的 channelReadComplete 方法里的 ctx.write 方法來看

// AbstractChannelHandlerContext.java

// 省略代碼

@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}

@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
    write(msg, false, promise);

    return promise;
}

private void write(Object msg, boolean flush, ChannelPromise promise) {
    ObjectUtil.checkNotNull(msg, "msg");
    try {
        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return;
        }
    } catch (RuntimeException e) {
        ReferenceCountUtil.release(msg);
        throw e;
    }

    final AbstractChannelHandlerContext next = findContextOutbound(flush ?
            (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        final WriteTask task = WriteTask.newInstance(next, m, promise, flush);
        if (!safeExecute(executor, task, promise, m, !flush)) {
            // We failed to submit the WriteTask. We need to cancel it so we decrement the pending bytes
            // and put it back in the Recycler for re-use later.
            //
            // See https://github.com/netty/netty/issues/8343.
            task.cancel();
        }
    }
}

private AbstractChannelHandlerContext findContextOutbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        ctx = ctx.prev;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
    return ctx;
}

// 省略代碼

通過調用一系列重載的 write 方法后,通過 findContextOutbound 方法在雙向鏈表裡向前尋找最近的實現了 write 或 writeAndFlush 方法的 ChannelHandlerContext,調用它的 invokeWrite 或 invokeWriteAndFlush 方法。

// AbstractChannelHandlerContext.java

// 省略代碼

void invokeWrite(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}

// 省略代碼

同理於讀取消息,這裏經過 invokeHandler 方法檢查通過後調用找到的 ChannelHandlerContext 的 ChannelHandler,沒有通過檢查,則繼續向前傳遞寫入事件。當寫入消息傳遞到頭部,調用 HeadContext 的 write 方法

// DefaultChannelPipeline.java

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {

    private final Unsafe unsafe;

    // 省略代碼

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        unsafe.write(msg, promise);
    }

    @Override
    public void flush(ChannelHandlerContext ctx) {
        unsafe.flush();
    }

    // 省略代碼
}

最終通過調用 unsafe 的 write 方法寫入消息。
最後,從上面的實現里可以發現,在將 ChannelHandler 加入到 ChannelPipeline 時,要把 ChannelOutBoundHandler 類型的 ChannelHandler 進來添加在前面,否則在 ChannelInBoundHandler 寫入消息時,在它後面的 ChannelOutBoundHandler 將無法獲取到事件。

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

千年彝綉出深山_網頁設計

※推薦評價好的iphone維修中心

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

本報北京1月19日電  (記者鄭海鷗)日前,“絲路雲裳·七彩雲南民族賽裝文化節”在京舉行新聞發布會,“‘築夢深山——千年彝綉’深山集市雲南省楚雄彝族自治州專場”舉行。源遠流長的楚雄傳統彝綉手工藝商品,帶來傳統與現代交融、民族與時尚牽手的文化之旅。

網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

彝綉是彝族文化寶庫中的明珠,其工藝獨特、構圖精美,具有很強的實用、觀賞和收藏價值。在楚雄州永仁縣崇山峻岭深處的彝族小山村直苴,每年農曆正月十五,當地村民都會自發組織盛大的“彝族賽裝節”,這一習俗傳承了1300多年。七彩雲南民族賽裝文化節就是對千年賽裝節的傳承和發展。據了解,“深山集市雲南省楚雄彝族自治州專場”活動將持續至2月16日,通過展示,助推以彝族刺繡、服裝服飾為代表的彝州名特優產品更好地走向市場、走向全國。

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

台北網頁設計公司這麼多該如何選擇?

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

史上參与人數最多最大福字在福建德化誕生_貨運

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

中國社會新聞網訊(姚逸寧 陳為德 庄芷萱)為积極響應中共中央辦公廳、國務院辦公廳《關於實施中華優秀傳統文化傳承發展工程的意見》精神,1月19日,由福建德化仙峰寺和泉州市禮儀協會等多個部門聯合舉辦福建省首屆”千福千聯”迎新賜福大型文化活動,在九仙山仙峰寺隆重舉行。期間,創造了史上參与書寫人數最多而且宣紙面積最大的福字,藉此共同祝福祖國繁榮昌盛,祈禱世界永久和平。並且,開展了群眾喜聞樂見的系列文化活動,受到人們的熱烈歡迎和由衷喝彩。

圖:福建省首屆千福千聯迎新賜福大型文化活動現場

在海拔1658米的九仙山上,天寒地凍,雲霧迷濛,但是來自全國各地的書法愛好者和群眾們,熱情高漲,喜笑顏開。活動總策劃庄金泉說,本次活動的目的是堅守中華文化立場、傳承中華文化基因,汲取中國智慧、弘揚中國精神、傳播中國價值,讓具有中國特色、中國風格、中國氣派的文化產品更加豐富,讓人們享有健康豐富的精神文化生活。

圖:參會人員集體向全市與全國人民祝福

“福”,自古以來就是中國人祝吉道福的絕妙佳詞,也是中國人共同追求的人生目標之一。在中國傳統文化里,”福”字寓意美好,既是古代人民對美好生活的嚮往,也是當代中華民族最基本的精神追求。所以,對福字藝術的傳承與創新,一直是藝術家們孜孜不倦的追求與樂在其中的探索。

圖:史上參与人數最多、宣紙面積最大的福字作品

此次,來自全國各地組成的13位書法愛好者,齊心協力共同完成的史上參与人數最多、宣紙面積最大的”福”字作品,堪稱世界一絕。該作品使用的萬年紅宣紙尺寸為長5米、寬5米,總面積25平方米。使用的特大毛筆總長1.8米,毛長35厘米,毛直徑12.5厘米,筆桿直徑4.5厘米,浸泡墨汁后毛筆總重約15公斤。值得一提的是,25平方米的特大宣紙和1.8米長的毛筆均為德化藝峰文化傳播有限公司周小平免費提供。

在悠揚的空靈鼓聲中,伴隨着王韻雅的禪舞表演,每位書法愛好者使用了同一根特大毛筆,採用正楷的書寫方式,一人一筆,歷時近一個小時完成。書法愛好者書寫順序分別為:褚子良、方長溪、何子軒、羅美花、陳能倡、吳明君、蘇和平、陳棟、賴開派、張金水、王文聯、庄金泉、朱雅宣。

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

圖:老中青書法家在現場為人們寫福

接着,圍繞這個特大的”福”字四周,還有幾百位來自香港、澳門、廈門、漳州和其它各省趕來的遊客與當地群眾等人組成的”書畫團”,在”福”字上面簽上了自己和家人的姓名,藉此抒發了對美好生活的嚮往和追求。

在活動中,擅長空心字的洪志鵬一氣呵成,一以貫之,毫不停歇,書寫了一幅16平方米的特大的空心福字,作為參加本次活動的獻禮。令人鼓舞的是,來自廈門的畫家曹夢海和丈夫王少武,一起把聯合創作的100幅牡丹作品獻給寺廟,與大眾結緣。

中國眾星書畫院院長王強、文化部國禮畫家曾鎮溪等國際著名書畫家,專程為活動送來墨寶福字。中國國際藝術研究會常務副會長潘贊盛、齊白石畫院常務副院長何子軒、光林寺禪畫院天石大師與天星大師等國內實力派書畫家,專程趕赴現場揮毫潑墨,助力為民造福文化活動。

圖:放假的學生在現場求福

據了解,本次活動由德化縣九仙山仙峰寺、德化九仙山旅遊度假風景區、泉州市禮儀協會大力主辦;由德化九仙山氣象站、九仙山靈鷲岩、九仙山永安岩、中國翰林院書畫家協會、德化縣書法家協會、德化穎達瓷藝研究所、德化縣航宇陶瓷有限公司、德化藝峰文化傳播有限公司、九仙山華裕飯店、晉江市海豐國際酒店、晉江五店市片仔癀體驗店、廈門大學附屬廈門眼科中心、健清堂企業總部管理有限公司、福建僑豐企業有限公司、廈門恆達昌投資集團、廈門沅舟綠文化傳播有限公司、泉州翰墨緣文化傳媒有限公司、泉州天強農林科技有限公司、泉州合世加陶瓷有限公司、三明市大田縣草木緣茶業有限公司等單位聯合協辦;由泉州市咱厝閩南文化研究中心、泉州市助學幫扶協會傾情承辦。

圖:少年書法愛好者在現場寫福

當日,主辦方還邀請了廈門大學附屬廈門眼科中心醫生、晉江五店市片仔癀體驗店到現場為群眾和遊客提供免費的眼科義診和健康知識普及等專項服務。晉江五店市片仔癀體驗店總經理張文波說,在此新春佳節來臨之際,能夠參加這種祈福送福活動,他感到很高興,也很振奮。在現場,他榮幸”請”到了幾張大師們即興書寫的”福”字。

福建省級工藝美術大師連德理,聞訊积極捐獻陶瓷作品,用於慈善義賣。據連德理介紹,他的德化穎達瓷藝研究所是一家集陶瓷產品自主研發、生產和銷售為一體的陶瓷文化研究機構,以德化傳統雕塑中國白瓷為研究方向,用非物質文化傳承技藝和獨特的藝術風格,創作出富有藝術和經濟價值的陶瓷系列精品,作品多次榮獲國家、省、市級金獎。在現場,連德理祝福活動圓滿成功,恭祝全市與全國人民新年快樂,福滿人間。

世界文化使者、國禮書法師、著名國際毛體書法家方長溪,在百忙之中專程趕赴寒冷的九仙山,熱情地參与盛會。方長溪在現場也獨立寫了一幅16平方米的特大福字。用原創精神舉辦這種特殊方式的大型文化活動,祝福祖國繁榮富強、人民安居樂業、世界永久和平,具有非常寶貴的社會意義。方長溪說,這對於弘揚中華傳統文化,發揚社會正能量,講好中國故事,激發人們”惜福”、”愛福”情懷,都具有积極昂揚的意義。

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

“歌唱北京”展首都新風貌_網頁設計公司

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

新時代的北京有新時代的歌曲,1月18日晚,原創音樂情景劇“歌唱北京”在中華世紀壇劇場上演,用情景劇、獨唱、合唱、舞蹈等形式展現新北京風貌。

2018年,北京廣播電視檯面向全球發起“歌唱北京”原創歌曲徵集活動,最終產生20首獲獎作品。2019年6月,“歌唱北京”原創歌曲徵集活動再次開啟。這次音樂情景劇中的歌曲就是“歌唱北京”征歌中的優秀作品。

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

情景劇以這三位年輕人的視角為切入點,男主角鄭能量是北京青年,女主角唱唱是海歸女孩,京京是一位海外華裔。三人在大興國際機場相遇,鄭能量和唱唱帶着京京領略新北京的風光,遊覽的過程融入了《北京樂章》《我們愛北京》《我愛的城市叫北京》《好客的北京》等歌曲。從前門的特色書店到大柵欄,從南鑼鼓巷到煙袋斜街,這些地標不僅出現在舞台背後的大屏幕上,也出現在“歌唱北京”的歌曲中。歌曲風格也十分多樣,有的有北京大鼓書的風韻,有的是快節奏的說唱,也有深受廣場舞愛好者喜歡的旋律。最終,三位年輕人被北京的高速發展震撼,決定一起為建設新時代的北京而奮鬥。

據悉,2019年度的“歌唱北京”征歌活動仍在進行中,主辦方希望更多市民加入描繪心中的北京,徵集時間將截至2020年3月30日。(記者 韓軒)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。