標籤: 網頁設計公司

  • 001.OpenShift介紹

    001.OpenShift介紹

    一 OpenShift特性

    1.1 OpenShift概述


    Red Hat OpenShijft Container Platform (OpenShift)是一個容器應用程序平台,它為開發人員和IT組織提供了一個雲應用程序平台,用於在安全的、可伸縮的資源上部署新應用程序,而配置和管理開銷最小。

    OpenShift構建於Red Hat Enterprise Linux、Docker和Kubernetes之上,為當今的企業級應用程序提供了一個安全且可伸縮的多租戶操作系統,同時還提供了集成的應用程序運行時和庫。

    OpenShift帶來了健壯、靈活和可伸縮的特性。容器平台到客戶數據中心,使組織能夠實現滿足安全性、隱私性、遵從性和治理需求的平台。不願意管理自己的OpenShift集群的客戶可以使用Red Hat提供的公共雲平台OpenShift Online。

    1.2 OpenShift特性


    OpenShift容器平台和OpenShift Online都是基於OpenShift Origin開源軟件項目的,該項目本身使用了許多其他開源項目,如Docker和Kubernetes。

    應用程序作為容器運行,容器是單個操作系統內的隔離分區。容器提供了許多與虛擬機相同的好處,比如安全性、存儲和網絡隔離,同時需要的硬件資源要少得多,啟動和終止也更快。OpenShift使用容器有助於提高平台本身及其承載的應用程序的效率、靈活性和可移植性。

    OpenShift的主要特性如下:

    • 自助服務平台:OpenShift允許開發人員使用Source-to-Image(S21)從模板或自己的源代碼管理存儲庫創建應用程序。系統管理員可以為用戶和項目定義資源配額和限制,以控制系統資源的使用。
    • 多語言支持:OpenShift支持Java、Node.js、PHP、Perl以及直接來自Red Hat的Ruby。同時也包括來自合作夥伴和更大的Docker社區的許多其他代碼。MySQL、PostgreSQL和MongoDB數據庫。Red Hat還支持在OpenShift上本地運行的中間件產品,如Apache httpd、Apache Tomcat、JBoss EAP、ActiveMQ和Fuse。
    • 自動化:OpenShift提供應用程序生命周期管理功能,當上游源或容器映像發生更改時,可以自動重新構建和重新部署容器。根據調度和策略擴展或故障轉移應用程序。
    • 用戶界面:OpenShift提供用於部署和監視應用程序的web UI,以及用於遠程管理應用程序和資源的CLi。它支持Eclipse IDE和JBoss Developer Studio插件,以便開發人員可以繼續使用熟悉的工具,並支持REST APl與第三方或內部工具集成。
    • 協作:OpenShift允許在組織內或與更大的社區共享項目。
    • 可伸縮性和高可用性:OpenShift提供了容器多租戶和一個分佈式應用程序平台,其中包括彈性,以處理隨需增加的流量。它提供了高可用性,以便應用程序能夠在物理機器宕機等事件中存活下來。OpenShift提供了對容器健康狀況的自動發現和自動重新部署。
    • 容器可移植性:在OpenShift中,應用程序和服務使用標準容器映像進行打包,組合應用程序使用Kubernetes進行管理。這些映像可以部署到基於這些基礎技術的其他平台上。
    • 開源:沒有廠商鎖定。
    • 安全性:OpenShift使用SELinux提供多層安全性、基於角色的訪問控制以及與外部身份驗證系統(如LDAP和OAuth)集成的能力。
    • 動態存儲管理:OpenShift使用Kubernetes持久卷和持久卷聲明的方式為容器數據提供靜態和動態存儲管理
    • 基於雲(或不基於雲):可以在裸機服務器、活來自多個供應商的hypervisor和大多數IaaS雲提供商上部署OpenShift容器平台。
    • 企業級:Red Hat支持OpenShift、選定的容器映像和應用程序運行時。可信的第三方容器映像、運行時和應用程序由Red Hat認證。可以在OpenShift提供的高可用性的強化安全環境中運行內部或第三方應用程序。
    • 日誌聚合和metrics:可以在中心節點收集、聚合和分析部署在OpenShift上的應用程序的日誌信息。OpenShift能夠實時收集關於應用程序的度量和運行時信息,並幫助不斷優化性能。
    • 其他特性:OpenShift支持微服務體繫結構,OpenShift的本地特性足以支持DevOps流程,很容易與標準和定製的持續集成/持續部署工具集成。

    二 OpenShift架構

    2.1 OpenShift架構概述


    OpenShift容器平台是一組構建在Red Hat Enterprise Linux、Docker和Kubernetes之上的模塊化組件和服務。OpenShift增加了遠程管理、多租戶、增強的安全性、應用程序生命周期管理和面向開發人員的自服務接口。

    OpenShift的架構:



    • RHEL:基本操作系統是Red Hat Enterprise Linux;
    • Docker:提供基本的容器管理API和容器image文件格式;
    • Kubernetes:管理運行容器的主機集群(物理或虛擬主機)。它處理描述由多個資源組成的多容器應用程序的資源,以及它們如何互連;
    • Etcd:一個分佈式鍵值存儲,Kubernetes使用它來存儲OpenShift集群中容器和其他資源的配置和狀態信息。


    OpenShift在Docker + Kubernetes基礎設施之上添加了提供容器應用程序平台所需的更富豐的功能:

    OpenShift-Kubernetes extensions:其它資源類型存儲在Etcd中,由Kubernetes管理。這些額外的資源類型形成OpenShift內部狀態和配置,以及由標準 Kubernetes管理的應用程序資源;

    Containerized services:完成許多基礎設施功能,如網絡和授權。其中一些一直運行,另一些則按需啟動。OpenShift使用Docker和Kubernetes來實現大多數內部功能。即大多數OpenShift內部服務作為由Kubernetes管理的容器;

    Runtimes and xPaaS:供開發人員使用的 base image,每個image都預配置了特定的runtime或db。xPaaS提供了一組用於JBoss中間件產品(如JBoss EAP和ActiveMQ)的 base image;

    DevOps tools and user experience:OpenShift提供了Web UI和CLI管理工具,從而實現配置和監視應用程序、OpenShift服務和資源。Web和CLI工具都是由相同的REST api構建的,可供IDE和CI平台等外部工具使用。OpenShift 還可以訪問外部SCM存儲庫和容器registry,並將它們的構件引入OpenShift Cloud。

    OpenShift不會向開發人員和系統管理員屏蔽Docker和Kubernetes的核心基礎設施。相反,它將它們用於內部服務,並允許將Docker和Kubernetes資源導入OpenShift集群,同時原始Docker和資源可以從OpenShift集群導出,並導入到其他基於docker的基礎設施中。

    OpenShift添加到Docker + Kubernetes的主要價值是自動化開發工作流,因此應用程序的構建和部署在OpenShift集群中按照標準流程進行。開發者不需要知道底層Docker的細節。OpenShift接受應用程序,打包它,並將其作為容器啟動。

    2.2 Master和nodes


    OpenShift集群是一組節點服務器,它們運行容器,並由一組主服務器集中管理。服務器可以同時充當master和node,但是為了增加穩定性,這些角色通常是分開的。

    OpenShift工作原理和交互視圖:




    master節點運行OpenShift核心服務,如身份驗證,並未管理員提供API入口。

    nodes節點運行包含應用程序的容器,容器又被分組成pod。

    OpenShift master運行Kubernetes master服務和Etcd守護進程;

    node運行Kubernetes kubelet和kube-proxy守護進程。

    雖然在描述中通常沒有聲明,但實際上master本身也是node。

    scheduler和management/replication是Kubernetes主服務,而Data Store是Etcd守護進程。

    Kubernetes的調度單元是pod,它是一組共享虛擬網絡設備、內部IP地址、TCP/UDP端口和持久存儲的容器。pod可以是任何東西,從完整的企業應用程序(包括作為不同容器的每一層)到單個容器中的單個微服務。例如,一個pod,一個容器在Apache下運行PHP,另一個容器運行MySQL。

    Kubernetes管理replicas來縮放pods。副本是一組共享相同定義的pod。

    三 管理OpenShift

    3.1 OpenShift項目及應用


    除了Kubernetes的資源(如pods和services)之外,OpenShift還管理projects和users。一個projects對Kubernetes資源進行分組,以便用戶可以使用訪問權限。還可以為projects分配配額,從而限制了已定義的pod、volumes、services和其他資源。

    OpenShift中沒有application的概念,OpenShift client提供了一個new-app命令。此命令在projects中創建資源,但它們都不是應用程序資源。這個命令是為標準開發人員工作流配置帶有公共資源的proiect的快捷方式。

    OpenShift使用lables(標籤)對集群中的資源進行分類。默認情況下,OpenShift使用app標籤將相關資源分組到應用程序中。

    3.2 使用Source-to-image構建映像


    OpenShift允許開發人員使用標準源代碼管理倉庫(SCM)和集成開發環境(ide)來發布應用。

    OpenShift中的source -to-lmage (S2I)流程從SCM倉庫中提取代碼,自動判斷所需的runtime,基於runtime啟動一個pod,在pod中編譯應用。

    當編譯成功時,將在runtime image中添加層並形成新的image,推送進入OpenShift internal registry倉庫,接着基於這個image將創建新的pod,運行應用程序。

    S2I可被視為已經內置到OpenShift中的完整的CI/CD管道。

    CI/CD有不同的形式,根據具體場景表現不同。例如,可以使用外部CI工具(如Jenkins)啟動構建並運行測試,然後將新構建的映像標記為成功或失敗,將其推送到QA或生產。

    3.2 管理OpenShift資源

    OpenShift資源定義,如image、container、pod、service、builder、template等,都存儲在Etcd中,可以由OpenShift CLI, web控制台或REST API進行管理。

    OpenShift的資源科通過JSON或YAML文件查看,並且在類似Git或版本控制的SCM中共享。OpenShift甚至可以直接從外部SCM檢索這些資源定義。

    大多數OpenShift操作不需要實時響應,OpenShift命令和APIs通常創建或修改存儲在Etcd中的資源描述。Etcd然後通知OpenShift控制器,OpenShift控制器會就更改警告這些資源。

    這些控制器採取行動,以便使得資源的最終態反應達到更改效果。例如,如果創建了一個新的pod資源,Kubernetes將在node上調度並啟動該pod,使用pod資源確定要使用哪個映像、要公開哪個端口,等等。或者一個模板被更改,從而指定應該有更多的pod來處理負載,OpenShift會安排額外的pod(副本)來滿足更新后的模板定義。

    注意:雖然Docker和Kubernetes是OpenShift的底層,但是必須主要使用OpenShift CLi和OpenShift APls來管理應用程序和基礎設施。OpenShift增加了額外的安全和自動化功能,當直接使用Docker或Kubernetes命令和APls時,這些功能必須手動配置,或者根本不可用。因此強烈建議不要使用docker或Kubernetes的命令直接管理應用。

    四 OpenShift網絡

    4.1 OpenShift網絡概述


    Docker網絡相對簡單,Docker創建一個虛擬內核橋接器(docker0網卡),並將每個容器網絡接口連接到它。

    Docker本身沒有提供允許一個主機上的pod連接到另一個主機上的pod的方法。Docker也沒有提供嚮應用程序分配公共固定IP地址的方法,以便外部用戶可以訪問它。

    但Kubernetes提供service和route資源來管理pods之間的網絡,以及從外部到pods的路由流量。service在不同pods之間提供負載均衡用於接收網絡請求,同時為service的所有客戶機(通常是其他pods)提供一個內部IP地址。

    container和pods不需要知道其他pods在哪裡,它們只連接到service。route為service提供一個固定的惟一DNS名稱,使其對OpenShift集群之外的客戶端可見。

    Kubernetes service和route資源需要外部(功能)插件支持。service需要軟件定義的網絡(SDN),它將在不同主機上的pod之間提供通信,route需要轉發或重定向來自外部客戶端的包到服務內部IP。

    OpenShift提供了一個基於Open vSwitch的SDN,路由由分佈式HAProxy farm提供。

    五 OpenShift持久性存儲

    5.1 永久存儲


    pod可以在一個節點上停止,並隨時在另一個節點上重新啟動。同時pod的默認存儲是臨時存儲,通過對於類似數據庫需要永久保存數據的應用不適合。

    Kubernetes為管理容器的外部持久存儲提供了一個框架。Kubernetes提供了PersistentVolume資源,它可以在本地或網絡中定義存儲。pod資源可以使用PersistentVolumeClaim資源來訪問對應的持久存儲卷。

    Kubernetes還指定了一個PersistentVolume資源是否可以在pod之間共享,或者每個pod是否需要具有獨佔訪問權的自己PersistentVolume。當pod移動到另一個節點時,它將保持與相同的PersistentVolumeClaim和PersistentVolumne資源的關聯。這意味着pod的持久存儲數據跟隨它,而不管它將在哪個節點上運行。

    OpenShift向Kubernetes提供了多種VolumeProvider,如NFS、iSCSI、FC、Gluster或OpenStack Cinder。

    OpenShift還通過StorageClass資源為應用程序提供動態存儲。使用動態存儲,可以選擇不同類型的後端存儲。後面存儲根據應用程序的需要劃分為不同的“tiers”。例如,可以定義一個名為“fast”的存儲類和另一個名為“slow”的存儲類,前者使用更高速的後端存儲,後者提供普通的存儲。當請求存儲時,最終用戶可以指定一個Persistentvolumeclaim,並使用一個註釋指定他們所需的StorageClass。

    六 OpenShift高可用

    6.1 OpenShift高可用概述


    OpenShift平台集群的高可用性(HA)有兩個不同的方面:

    OpenShift基礎設施本身的HA(即主機);

    以及在OpenShift集群中運行的應用程序的HA。

    默認情況下,OpenShift為master提供了完全支持的本機HA機制。

    對於應用程序或“pods”,如果pod因任何原因丟失,Kubernetes將調度另一個副本,將其連接到服務層和持久存儲。如果整個節點丟失,Kubernetes會為它所有的pod安排替換節點,最終所有的應用程序都會重新可用。pod中的應用程序負責它們自己的狀態,因此它們需要自己維護應用程序狀態(如HTTP會話複製或數據庫複製)。

    七 Image Streams

    7.1 Image Streams


    要在OpenShift中創建一個新的應用程序,除了應用程序源代碼之外,還需要一個base image(S2I builder image)。如果源代碼或image任何一個更新,就會生成一個新的image,並且基於此新image創建新的pod,同時替換舊的pod。

    即當應用程序代碼發生更改時,容器映像需要更新,但如果構建器映像發生更改,則部署的pod也需要更新。

    Image Streams包括由tag標識的大量的image。應用程序是針對Image Streams構建的。Image Streams可用於在創建新image時自動執行操作。構建和部署可以監視Image Streams,以便在添加新image時接收通知,並分別執行構建或部署。

    OpenShift默認情況下提供了幾個Image Streams,包括許多流行的runtime和frameworks。

    Image Streams tag是指向Image Streams中的image的別名。通常縮寫為istag。它包含一個image歷史記錄,表示為tag曾經指向的所有images的堆棧。

    每當使用特定的istag標記一個新的或現有的image時,它都會被放在歷史堆棧的第一個位置(標記為latest)。之前tag再次指向舊的image。同時允許簡單的回滾,使標籤再次指向舊的image。 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

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

    聚甘新

  • EOS基礎全家桶(十三)智能合約基礎

    簡介

    智能合約是現在區塊鏈的一大特色,而不同的鏈使用的智能合約的虛擬機各不相同,編碼語言也有很大差異。而今天我們開始學習EOS的智能合約,我也是從EOS初期一直開發合約至今,期間踩過無數坑,也在Stack Overflow上提過問(最後自己解決了),在實際生產中也積累了很多經驗,所以我會連續幾周分多次分享合約開發的經驗,今天先來點基礎的。

    一些C++的編程基礎

    EOS就是使用C++開發的,這也為它帶來了諸多好處,而合約也沿用C++作為開發語言,雖然合約中無法直接使用Boost等框架(你可以自己引入,但這也意味着合約會很大,會佔用大量賬號的內存),但是我們還是可以使用很多C++的小型庫,並伴隨着eosio.cdt的發展,融入了更多實用的合約功能。

    如果你之前沒有使用C系列的開發語言做過開發,比如:C語言、C++或者是C#,那麼你需要先學習下C語言的基本語法和數據結構,這裏我不做展開,在我們的系列文章的開篇就介紹了我推薦的Learn EOS – c/c++ 教程英文版,有一定英語基礎的朋友可以直接看這個,其他朋友也可以在網上找一些C++的入門教程看下。

    如果你已經有了一定的C語言基礎,那麼寫合約的話,你會發現需要的基礎也並不多,依葫蘆畫瓢就能寫出各種基礎功能了,所以,你並不需要擔心太多語言上的門檻,畢竟合約只是一個特定環境下運行的程序,你能用到的東西並不會很多。

    CDT選擇

    EOS的早期版本進行合約開發還沒有CDT工具,那時的合約藉助的是源碼中的工具eosiocpp,所以你看2018年的博客,進行合約編譯都是用它,但你現在是見不到了。隨着官方CDT的迭代,在CDT的1.4版本開始被官方推薦使用,CDT後面也經歷了幾個大的版本更新,逐步改善合約編寫方式,更加趨於簡潔、直觀。

    但是不同的CDT版本,也意味着編譯器的不同,所以合約開發也會有所區別,比如一些語法變了,一些庫名稱變了,增加了一些新的標註……

    我們的教程側重還是介紹最新的語法,所以推薦使用1.6以上的版本。我也會盡量在後面的介紹中補充說明老的CDT的寫法,方便大家對照網上其他老博客的合約。

    來個HelloWorld

    學習任何編程,我們都不能少了Mr.HelloWorld,先來給大家打個招呼吧。

    #include <eosio/eosio.hpp>
    
    using namespace eosio;
    
    class [[eosio::contract]] hello : public contract
    {
    public:
        using contract::contract;
    
        [[eosio::action]] void hi(name user)
        {
            print("Hello, ", user);
        }
    };
    

      

    基本合約結構及類型

    hello合約就是一個最簡單的合約了,而且還有一個可調用的action為hi。我們首先還是來介紹下一個合約的程序結構吧。

    • 程序頭

    包含了引入的頭文件、庫文件等,還有全局的命名空間的引入等。

    #include <eosio/eosio.hpp>
    
    using namespace eosio;
    

      

    這裏eosio庫是我們的合約基礎庫,所有和eos相關的類型和方法,都在這個庫裏面,而這個庫裏面eosio.hpp是基礎,包含了contract等的定義,所以所有的合約都要引入。

    【CDT老版本】早期cdt版本中庫名稱不是eosio,而是eosiolib

    默認的,我們引入了eosio的命名空間,因為eosio的所有內容都是在這個命名空間下的,所以我們全局引入,會方便我們後續的代碼編寫。

    • 合約類定義

    其實就是定義了一個class,繼承contract,並通過[[eosio::contract]]標註這個類是一個合約。使用using引入contract也是為了後續代碼可以更簡潔。

    class [[eosio::contract]] hello : public contract{
    public:
        using contract::contract;
    }
    

      

    【CDT老版本】早期cdt版本中直接使用了CONTRACT來定義合約類,比如:CONTRACT hello: public contract {}

    • action定義

    寫一個public的方法,參數盡量用簡單或者是eosio內置的類型定義,無返回值(合約調用無法返回任何結果,除非報錯),然後在用[[eosio::action]]標註這個方法是一個合約action就行。

    注意:action的名稱要求符合name類型的規則,name規則請看下面的常用類型中的說明。

    [[eosio::action]]
    void hi( name user ) {
        print( "Hello, ", user);
    }
    

      

    因為合約無法調試,所以只能通過print來打印信息,或者直接通過斷言拋出異常來進行調試。

    【CDT老版本】早期cdt版本中直接使用ACTION來定義方法,比如:ACTION hi( name user ){}

    • 常用類型
    類型 說明 示例
    name 名稱類型,賬號名、表名、action名都是該類型,只能使用26個小寫字母和1到5的数字,特殊可以使用小數點,總長不超過13。 name("hi") 或者 "hi"_n
    asset 資產類型,Token都是使用該類型,包含了Token符號和小數位,是一個複合類型,字符形式為1.0000 EOS asset(10000, symbol("TADO", 4)就是1.0000 TADO)
    uint64_t 無符號64位整型,主要數據類型,表主鍵、name實質都是改類型 uint64_t amount = 10000000;
    • 內置常用對象或方法

    在合約中,contract基類提供了一些方便的內置對象。

    首先是get_self()或者是_self,這個方法可以獲取到當前合約所在的賬號,比如你把hello合約部署到了helloworld111這個賬號,那麼get_self()就可以獲取到helloworld111。

    然後是get_code()或者是_code,這個方法可以獲取到當前交易請求的action方法名,這個在進行內聯action調用時可以用於判斷入口action。

    最後是get_datastream()或者_ds,這個方法獲取的是數據流,如果你使用的是複雜類型,或者是自定義類型,那麼你無法在方法的參數上直接獲取到反序列化的變量值,你必須自己通過數據流來解析。

    常用的還有獲取當前時間current_time_point(),這個需要引入#include <eosio/transaction.hpp>

    數據持久化

    當然,合約裏面,我們總會有些功能需要把數據存下來,在鏈上持久化存儲。所以我們就需要定義合約表了。

    合約的表存在相應的合約賬號中,可以劃分表範圍(scope),每個表都有一個主鍵,uint64_t類型的,還可以有多個其他索引,表的查詢都是基於索引的。

    這裏先提一句,表數據所佔用的內存,默認是合約賬號的內存,也可以使用其他賬號的,但需要權限,這個以後我們再介紹。

    我們擴展一下hello合約。

    #include <eosio/eosio.hpp>
    #include <eosio/transaction.hpp>
    
    using namespace eosio;
    
    class [[eosio::contract]] hello : public contract
    {
    public:
        using contract::contract;
    
        hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
        {
        }
    
        [[eosio::action]] void hi(name user)
        {
            print("Hello, ", user);
    
            uint32_t now = current_time_point().sec_since_epoch();
    
            auto friend_itr = friend_table.find(user.value);
            if (friend_itr == friend_table.end())
            {
                friend_table.emplace(get_self(), [&](auto &f) {
                    f.friend_name = user;
                    f.visit_time = now;
                });
            }
            else
            {
                friend_table.modify(friend_itr, get_self(), [&](auto &f) {
                    f.visit_time = now;
                });
            }
        }
    
        [[eosio::action]] void nevermeet(name user)
        {
            print("Never see you again, ", user);
    
            auto friend_itr = friend_table.find(user.value);
            check(friend_itr != friend_table.end(), "I don't know who you are.");
    
            friend_table.erase(friend_itr);
        }
    
    private:
        struct [[eosio::table]] my_friend
        {
            name friend_name;
            uint64_t visit_time;
    
            uint64_t primary_key() const { return friend_name.value; }
        };
    
        typedef eosio::multi_index<"friends"_n, my_friend> friends;
    
        friends friend_table;
    };
    

      

    可以看到,我們已經擴充了不少東西了,包括構造函數,表定義,多索引表配置,並完善了原先的hi方法,增加了nevermeet方法。

    我們現在模擬的是這樣一個使用場景,我們遇到一個朋友的時候,就會和他打招呼(調用hi),如果這個朋友是一個新朋友,就會插入一條記錄到我們的朋友表中,如果是一個老朋友了,我們就會更新這個朋友的記錄中的訪問時間。當我們決定不再見這個朋友了,就是絕交了(調用nevermeet),我們就會把這個朋友的記錄刪除。

    • 表定義

    首先我們需要聲明我們的朋友表。定義一個結構體,然後用[[eosio::table]]標註這個結構體是一個合約表。在結構體里定義一個函數名primary_key,返回uint64_t類型,作為主鍵的定義。

    private:
        struct [[eosio::table]] my_friend
        {
            name friend_name;
            uint64_t visit_time;
    
            uint64_t primary_key() const { return friend_name.value; }
        };
    

      

    我們這裏聲明了一個my_friend的表,合約的表名不在這裏定義,所以結構體的名稱不必滿足name的規則。我們定義了兩個字段,friend_name(朋友的名稱)和visit_time(拜訪時間),主鍵我們直接使用了friend_name,這個字段是name類型的,而name類型的實質就是一個uint64_t的類型(所以name的規則那麼苛刻)。

    【CDT老版本】早期cdt版本中直接使用TABLE來定義合約表,比如:TABLE my_friend{}

    • 多索引表配置

    合約里的表都是通過多索引來定義的,這是合約表的結構基礎。所以這裏才是定義表名和查詢索引的地方。

    typedef eosio::multi_index<"friends"_n, my_friend> friends;
    

      

    我們現在只介紹最簡單的單索引的定義,以後再介紹多索引的定義方式,這裏的"friends"_n就是定義表名,所以使用了name類型,之後my_friend是表的結構類型,typedef實質上就是聲明了一個類型別名,名字是friends的類型。

    • 構造函數

    構造函數這裏並不是必須,但是為了我們能在全局直接使用合約表,所以我們要在構造函數進行表對象的實例化。

    public:
        hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
        {
        }
    
    private:
        friends friend_table;
    

      

    這一段是標準合約構造函數,hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds),合約類型實例化時會傳入receiver也就是我們的合約賬號(一般情況下),code就是我們的action名稱,ds就是數據流。

    friend_table(get_self(), get_self().value)這一段就是對我們定義的friend_table變量的實例化,friend_table變量就是我們定義的多索引表的friends類型的實例。在合約里我們就可以直接使用friend_table變量來進行表操作了。實例化時傳遞的兩個參數正是表所在合約的名稱和表範圍(scope),這裏都使用的是當前合約的名稱。

    • 查詢記錄

    查詢有多種方式,也就是多索引表提供了多種查詢的方式,默認的,使用findget方法是直接使用主鍵進行查詢,下次我們會介紹使用第二、第三等索引來進行查詢。find返回的是指針,數據是否存在,需要通過判斷指針是否是指到了表末尾,如果等於表末尾,就說明數據不存在,否則,指針的值就是數據對象。get直接返回的就是數據對象,所以在調用get時,就必須傳遞數據不存在時的錯誤信息。

    auto friend_itr = friend_table.find(user.value);
    if (friend_itr == friend_table.end())
    {
        //數據不存在
    }else
    {
        //數據存在
    }
    

      

    我們在hi方法中先查詢了user是否存在。如果不存在,我們就添加數據,如果存在了,就修改數據中的visit_time字段的值為當前時間。

    • 添加記錄

    多索引的表對象添加記錄使用emplace方法,第一個參數就是內存使用的對象,第二個參數就是添加表對象時的委託方法。

    uint32_t now = current_time_point().sec_since_epoch();
    
    auto friend_itr = friend_table.find(user.value);
    if (friend_itr == friend_table.end())
    {
        friend_table.emplace(get_self(), [&](auto &f) {
            f.friend_name = user;
            f.visit_time = now;
        });
    }
    else
    {
        //數據存在
    }
    

      

    這裏先定義了一個變量now來表示當前時間,正是使用的內置方法current_time_point(),這個還是用了它的sec_since_epoch()方法,是為了直接獲取秒單位的值。

    我們查詢后發現這個user的數據不存在,所以就進行插入操作,內存直接使用的合約賬號的,所以使用get_self(),然後對錶數據對象進行賦值。

    • 修改記錄

    多索引的表對象修改記錄使用modify方法,第一個參數是傳遞需要修改的數據指針,第二個參數是內存使用的對象,第二個參數就是表對象修改時的委託方法。

    friend_table.modify(friend_itr, get_self(), [&](auto &f) {
        f.visit_time = now;
    });
    

      

    我們將查詢到的用戶對象的指針friend_itr傳入,然後內存還是使用合約賬號的,委託中,我們只修改visit_time的值(主鍵是不能修改的)。

    • 刪除記錄
    • 多索引的表對象刪除記錄使用erase方法,只有一個參數,就是要刪除的對象指針,有返回值,是刪除數據后的指針偏移,也就是下一條數據的指針。
    auto friend_itr = friend_table.find(user.value);
    check(friend_itr != friend_table.end(), "I don't know who you are.");
    
    friend_table.erase(friend_itr);
    

      

    我們的示例中,將查詢到的這條數據直接刪除,併為使用變量來接收下一條數據的指針,在連續刪除數據時,你會需要獲取下一條數據的指針,因為已刪除的數據的指針已經失效了。

    編譯

    編譯我們再之前也有過介紹,安裝了eosio.cdt后,我們就有了eosio-cpp命令,進入到合約文件夾中,直接執行以下命令就會在當前目錄生成wasm和abi文件。

    eosio-cpp -abigen hello.cpp -o hello.wasm

    注意:替換命令中使用的hello.cpp為實際合約代碼文件名,而hello.wasm為實際合約的wasm文件名。

    當然,編譯不通過的時候,你就要看看錯誤是什麼了,這可能會考驗一下你的C++功底。

    發布

    決定了要發布的賬號后,記得要購買足夠的內存和抵押足夠的資源。合約的內存消耗我們可以大致這樣估算,看下編譯好了的合約wasm文件有多大,然後乘以10,就是你發布到鏈上大概所需的內存大小了。

    發布合約我們使用cleos set contract命令,其後跟合約賬號名和合約目錄,為了方便,我建議你把合約的目錄名保持和合約文件名一致。

    cleos set contract helloworld111 ./hello -p helloworld111
    

    這裏我們給出的代碼是將hello目錄下的hello合約發布到helloworld111。我這裏的文件夾是hello,裏面的abi和wasm也都是hello,這樣你不用手動指定合約文件了。

    總結

    至此,我想大家應該對合約的編寫有了一個大致的了解了,至少你可以參照着寫個簡單的合約出來了,這其中還有很多技巧和高級用法,我會在後續的文章中繼續和大家分享。

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

    【其他文章推薦】

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

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

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

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

    ※教你寫出一流的銷售文案?

    聚甘新

  • 人臉識別和手勢識別應用(face++)開發

    人臉識別和手勢識別應用(face++)開發

    基礎認識

    本項目使用的是face++平台,人臉識別+手勢識別雙確認显示。

    python編程,代碼簡介,方便擴展。

     

    該項目適用於Windows系統和Linux系統,但必須安裝相應的模塊,其中包括

     

    l  Python3  python 庫,邏輯編寫

    l  Pillow   窗口開發實現

    l  opencv-python python的opencv接口

    l  Opencv庫   用於人臉檢測

     

    本次測試是在win 10電腦上

     

    視頻演示:

    https://www.bilibili.com/video/BV1Wk4y1z7H7

     

    安裝python3

    這個網上到處都是資料,找一找就知道啦

    官網:

    https://www.python.org/

     安裝pillow

    該庫用於python做界面開發,詳細參考:https://www.cnblogs.com/dongxiaodong/p/9971974.html

    這個庫一般電腦都自帶有了,可以先不安裝,直接運行代碼。

    如果出現以下錯誤,則必須手動安裝

    ModuleNotFoundError: No module named ‘PIL’

    安裝命令:

    pip install pillow

    安裝opencv-python

    Opencv可以實現人臉檢測、人臉對比識別等功能,但在次只是用它來實現了人臉檢測並做人臉框圖,並沒有更多功能的實現,想要獲取更多功能的學習參考,請訪問:https://www.cnblogs.com/dongxiaodong/p/10134904.html

    pip install opencv-python

    如果出現紅色字體,表示安裝出錯了,必須從新運行安裝命令

     

     Face++

    Face++在項目中用於人臉識別和手勢識別

    系統流程主要為如下:

     

    測試

    (一)  獲取人臉標識

    工程目錄:

     

    l  運行項目,攝像頭將開啟,實時展示所拍攝的畫面

    l  按下空格鍵即可獲取人臉標識,輸出人臉標識和存儲到data文件目錄下

    l  此時按下ESC鍵則退出程序

    l  同一個人的人臉標識很有可能是不一樣的,因為它更多的是基於本次照片計算

     

    (二)  創建人臉庫&人臉標識添加到人臉庫

     

     

     

    l  創建人臉標識庫,標識名自定義,但同一用戶內不可有相同的人臉標識庫

    l  在函數填寫自己賬戶下唯一的人臉庫標識名

     

     

    l  將人臉標識添加到人臉庫中

    l  在函數中填寫人臉庫標識和我們第一步獲取的人臉標識,將人臉標識添加到人臉庫中

    l  人臉庫可以添加多個不同的人臉標識

     

    (三)  人臉庫搜索結果比對

     

    l  修改為我們剛剛所創建的人臉庫,進行接下來的人臉識別查找

    l  運行工程后將開啟攝像頭進行照片實時捕獲識別,並在屏幕中显示識別結果

    l  識別包括人臉識別和手勢識別

    l  只有在人臉識別正確的情況下才會開啟手勢識別

     

    人臉識別失敗

    人臉識別成功,無手勢

    人臉識別成功,手勢為合攏

    人臉識別成功,手勢為打開

     

     

    視頻演示:

    https://www.bilibili.com/video/BV1Wk4y1z7H7

     

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

    【其他文章推薦】

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

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

    聚甘新

  • 【Spring】循環依賴 Java Vs Spring

    菜瓜:水稻,這次我特意去看了java的循環依賴

    水稻:喲,有什麼收穫

    菜瓜:兩種情況,構造器循環依賴,屬性循環依賴

    • 構造器循環依賴在邏輯層面無法通過。對象通過構造函數創建時如果需要創建另一個對象,就會存在遞歸調用。棧內存直接溢出
    • 屬性循環依賴可以解決。在對象創建完成之後通過屬性賦值操作。
    • package club.interview.base;
      
      /**
       * 構造器循環依賴 - Exception in thread "main" java.lang.StackOverflowError
       * toString()循環打印也會異常 - Exception in thread "main" java.lang.StackOverflowError
       * @author QuCheng on 2020/6/18.
       */
      public class Circular {
      
          class A {
              B b;
      
      //        public A() {
      //            b = new B();
      //        }
      
      //        @Override
      //        public String toString() {
      //            return "A{" +
      //                    "b=" + b +
      //                    '}';
      //        }
          }
      
          class B {
              A a;
      
      //        public B() {
      //            a = new A();
      //        }
      
      //        @Override
      //        public String toString() {
      //            return "B{" +
      //                    "a=" + a +
      //                    '}';
      //        }
          }
      
          private void test() {
              B b = new B();
              A a = new A();
              a.b = b;
              b.a = a;
              System.out.println(a);
              System.out.println(b);
          }
      
          public static void main(String[] args) {
              new Circular().test();
          }
      }

    水稻:厲害啊,Spring也不支持構造函數的依賴注入,而且也不支持多例的循環依賴。同樣的,它支持屬性的依賴注入。

    • 看效果 – 如果toString()打印同樣會出現棧內存溢出。
    • package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("a")
      public class CircularA {
      
          @Resource
          private CircularB circularB;
      
      //    @Override
      //    public String toString() {
      //        return "CircularA{" +
      //                "circularB=" + circularB +
      //                '}';
      //    }
      }
      
      
      package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("b")
      public class CircularB {
      
          @Resource
          private CircularA circularA;
      
      //    @Override
      //    public String toString() {
      //        return "CircularB{" +
      //                "circularA=" + circularA +
      //                '}';
      //    }
      }
      
      
          @Test
          public void testCircular() {
              String basePackages = "com.vip.qc.circular";
              new AnnotationConfigApplicationContext(basePackages);
          }

    菜瓜:看來spring的實現應該也是通過屬性注入的吧

    水稻:你說的對。先給思路和demo,之後帶你掃一遍源碼,follow me !

    • spring的思路是給已經初始化的bean標記狀態,假設A依賴B,B依賴A,先創建A
      • 先從緩存容器(總共三層,一級拿不到就拿二級,二級拿不到就從三級緩存中拿正在創建的)中獲取A,未獲取到就執行創建邏輯
      • 對象A在創建完成還未將屬性渲染完之前標記為正在創建中,放入三級緩存容器。渲染屬性populateBean()會獲取依賴的對象B。
      • 此時B會走一次getBean邏輯,B同樣會先放入三級緩存,然後渲染屬性,再次走getBean邏輯注入A,此時能從三級緩存中拿到A,並將A放入二級容器。B渲染完成放入一級容器
      • 回到A渲染B的方法populateBean(),拿到B之後能順利執行完自己的創建過程。放入一級緩存
    •  為了證實結果,我把源碼給改了一下,看結果

      • package com.vip.qc.circular;
        
        import org.springframework.stereotype.Component;
        
        import javax.annotation.Resource;
        
        /**
         * @author QuCheng on 2020/6/18.
         */
        @Component("a")
        public class CircularA {
        
            @Resource
            private CircularB circularB;
        
            @Override
            public String toString() {
                return "CircularA{" +
                        "circularB=" + circularB +
                        '}';
            }
        }
        
        
        package com.vip.qc.circular;
        
        import org.springframework.stereotype.Component;
        
        import javax.annotation.Resource;
        
        /**
         * @author QuCheng on 2020/6/18.
         */
        @Component("b")
        public class CircularB {
        
            @Resource
            private CircularA circularA;
        
            @Override
            public String toString() {
                return "CircularB{" +
                        "circularA=" + circularA +
                        '}';
            }
        }
        
        
        測試代碼
        @Test
            public void testCircular() {
                String basePackages = "com.vip.qc.circular";
                new AnnotationConfigApplicationContext(basePackages);
        }
        
        測試結果(我改過源碼了)
        ---- 
        將a放入三級緩存
        將b放入三級緩存
        將a放入二級緩存
        將b放入一級緩存
        從二級緩存中拿到了a
        將a放入一級緩存

          

    • 再看源碼
      • 關鍵類處理getSingleton邏輯 – 緩存容器
        • public class DefaultSingletonBeanRegistry 
          
            /** Cache of singleton objects: bean name to bean instance. */
            // 一級緩存
              private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
          
              /** Cache of singleton factories: bean name to ObjectFactory. */
            // 三級緩存
              private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
          
              /** Cache of early singleton objects: bean name to bean instance. */
            // 二級緩存
              private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
      • 主流程 AbstractApplicationContext#refresh() -> DefaultListableBeanFactory#preInstantiateSingletons() -> AbstractBeanFactory#getBean() & #doGetBean()
        • protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
             /**
              * 處理FactoryBean接口名稱轉換 {@link BeanFactory#FACTORY_BEAN_PREFIX }
              */
             final String beanName = transformedBeanName(name);
                  ...
             // ①從緩存中拿對象(如果對象正在創建中且被依賴注入,會放入二級緩存)
             Object sharedInstance = getSingleton(beanName);
             if (sharedInstance != null && args == null) {
                ...
             }else {      
                      ...
                   if (mbd.isSingleton()) {
                     // ② 將創建的對象放入一級緩存
                      sharedInstance = getSingleton(beanName, () -> {
                         try {
                               // ③ 具體創建的過程,每個bean創建完成之後都會放入三級緩存,然後渲染屬性
                            return createBean(beanName, mbd, args);
                         }catch (BeansException ex) {
                           ...
             ...
             return (T) bean;
          } 
      • ①getSingleton(beanName) – 二級緩存操作
        • protected Object getSingleton(String beanName, boolean allowEarlyReference) {
             // 實例化已經完成了的放在singletonObjects
             Object singletonObject = this.singletonObjects.get(beanName);
             // 解決循環依賴
             if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
                synchronized (this.singletonObjects) {
                   singletonObject = this.earlySingletonObjects.get(beanName);
                   if (singletonObject == null && allowEarlyReference) {
                      ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                      if (singletonFactory != null) {
                         singletonObject = singletonFactory.getObject();
                         this.earlySingletonObjects.put(beanName, singletonObject);
                         if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                            System.out.println("將"+beanName+"放入二級緩存");;
                         this.singletonFactories.remove(beanName);
                      }
                   }else if(singletonObject != null){
                      System.out.println("從二級緩存中拿到了"+beanName);
                   }
                }
             }
             return singletonObject;
          }
      • ② getSingleton(beanName,lamdba) – 一級緩存操作
        • public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
                Assert.notNull(beanName, "Bean name must not be null");
                synchronized (this.singletonObjects) {
                   Object singletonObject = this.singletonObjects.get(beanName);
                   if (singletonObject == null) {
                      if (this.singletonsCurrentlyInDestruction) {
                      ...
                      // 正在創建的bean加入singletonsCurrentlyInCreation - 保證只有一個對象創建,阻斷循環依賴
                      beforeSingletonCreation(beanName);
                                ...
                      try {
                         singletonObject = singletonFactory.getObject();
                      ...
                      finally {
                      ...
                         // 從singletonsCurrentlyInCreation中移除
                         afterSingletonCreation(beanName);
                      }
                      if (newSingleton) {
                         // 對象創建完畢 - 放入一級緩存(從其他緩存移除)
                         addSingleton(beanName, singletonObject);
                      }
                   }
                   return singletonObject;
                }
             }
                  
           //  -----  內部調用一級緩存操作
              protected void addSingleton(String beanName, Object singletonObject) {
                  synchronized (this.singletonObjects) {
                      if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                          System.out.println("將"+beanName+"放入一級緩存");;
                      this.singletonObjects.put(beanName, singletonObject);
                      this.singletonFactories.remove(beanName);
                      this.earlySingletonObjects.remove(beanName);
                      this.registeredSingletons.add(beanName);
                  }
              }        
             
      • ③createBean(beanName, mbd, args) – 三級緩存操作
        • protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {
               ...
             if (instanceWrapper == null) {
                // 5* 實例化對象本身
                instanceWrapper = createBeanInstance(beanName, mbd, args);
             }
             ...
             boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                   isSingletonCurrentlyInCreation(beanName));
             if (earlySingletonExposure) {
                ...
                // 將創建好還未渲染屬性的bean 放入三級緩存
                addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
             }
          
             Object exposedObject = bean;
             try {
                // 渲染bean自身和屬性
                populateBean(beanName, mbd, instanceWrapper);
                // 實例化之後的後置處理 - init
                exposedObject = initializeBean(beanName, exposedObject, mbd);
             }
             catch (Throwable ex) {
             ...
             return exposedObject;
          }
            
            
             // ------------- 內部調用三級緩存操作 
             protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
                  Assert.notNull(singletonFactory, "Singleton factory must not be null");
                  synchronized (this.singletonObjects) {
                      if (!this.singletonObjects.containsKey(beanName)) {
                          if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                              System.out.println("將"+beanName+"放入三級緩存");;
                          this.singletonFactories.put(beanName, singletonFactory);
                          this.earlySingletonObjects.remove(beanName);
                          this.registeredSingletons.add(beanName);
                      }
                  }
              }       

    菜瓜:demo比較簡單,流程大致明白,源碼我還需要斟酌一下,整體有了概念。這個流程好像是摻雜在bean的創建過程中,結合bean的生命周期整體理解可能會更深入一點

    水稻:是的。每個知識點都不是單一的,拿着bean的生命周期再理解一遍可能會更有收穫。

     

    討論

    • 為什麼是三級緩存,兩級不行嗎?
      • 猜測:理論上兩級也可以實現。多一個二級緩存可能是為了加快獲取的速度。假如A依賴B,A依賴C,B依賴A,C依賴A,那麼C在獲取A的時候只需要從二級緩存中就能拿到A了

    總結

    • Spring的處理方式和java處理的思想一致,構造器依賴本身是破壞語義和規範的
    • 屬性賦值–> 依賴注入 。 先創建對象,再賦值屬性,賦值的時候發現需要創建便生成依賴對象,被依賴對象需要前一個對象就從緩存容器中拿取即可

     

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

    【其他文章推薦】

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    聚甘新

  • 菜渣開源一個基於 EMIT 的 AOP 庫(.NET Core)

    目錄

    • 1,快速入門
      • 1.1 繼承 ActionAttribute 特性
      • 1.2 標記代理類型
      • 2,如何創建代理類型
        • 2.1 通過API直接創建
    • 2,創建代理類型
      • 通過API
      • 通過 Microsoft.Extensions.DependencyInjection
        • 通過 Autofac
    • 3,深入使用
      • 代理類型
      • 方法、屬性代理
      • 上下文
        • 攔截方法或屬性的參數
      • 非侵入式代理

    Nuget 庫地址:https://www.nuget.org/packages/CZGL.AOP/

    Github 庫地址:https://github.com/whuanle/CZGL.AOP

    CZGL.AOP 是 基於 EMIT 編寫的 一個簡單輕量的AOP框架,支持非侵入式代理,支持.NET Core/ASP.NET Core,以及支持多種依賴注入框架。

    1,快速入門

    CZGL.AOP 使用比較簡單,你只需要使用 [Interceptor] 特性標記需要代理的類型,然後使用繼承 ActionAttribute 的特性標記要被代理的方法或屬性。

    1.1 繼承 ActionAttribute 特性

    ActionAttribute 是用於代理方法或屬性的特性標記,不能直接使用,需要繼承后重寫方法。

    示例如下:

        public class LogAttribute : ActionAttribute
        {
            public override void Before(AspectContext context)
            {
                Console.WriteLine("執行前");
            }
    
            public override object After(AspectContext context)
            {
                Console.WriteLine("執行后");
                if (context.IsMethod)
                    return context.MethodResult;
                else if (context.IsProperty)
                    return context.PropertyValue;
                return null;
            }
        }
    

    Before 會在被代理的方法執行前或被代理的屬性調用時生效,你可以通過 AspectContext 上下文,獲取、修改傳遞的參數。

    After 在方法執行后或屬性調用時生效,你可以通過上下文獲取、修改返回值。

    1.2 標記代理類型

    在被代理的類型中,使用 [Interceptor] 特性來標記,在需要代理的方法中,使用 繼承了 ActionAttribute 的特性來標記。

    此方法是侵入式的,需要在編譯前完成。

    [Interceptor]
    public class Test : ITest
    {
        [Log] public virtual string A { get; set; }
        [Log]
        public virtual void MyMethod()
        {
            Console.WriteLine("運行中");
        }
    }
    

    注意的是,一個方法或屬性只能設置一個攔截器。

    2,如何創建代理類型

    CZGL.AOP 有多種生成代理類型的方式,下面介紹簡單的方式。

    請預先創建如下代碼:

        public class LogAttribute : ActionAttribute
        {
            public override void Before(AspectContext context)
            {
                Console.WriteLine("執行前");
            }
    
            public override object After(AspectContext context)
            {
                Console.WriteLine("執行后");
                if (context.IsMethod)
                    return context.MethodResult;
                else if (context.IsProperty)
                    return context.PropertyValue;
                return null;
            }
        }
    
        public interface ITest
        {
            void MyMethod();
        }
    
        [Interceptor]
        public class Test : ITest
        {
            [Log] public virtual string A { get; set; }
            public Test()
            {
                Console.WriteLine("構造函數沒問題");
            }
            [Log]
            public virtual void MyMethod()
            {
                Console.WriteLine("運行中");
            }
        }
    

    2.1 通過API直接創建

    通過 CZGL.AOP 中的 AopInterceptor 類,你可以生成代理類型。

    示例如下:

                ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
                Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
                test1.MyMethod();
                test2.MyMethod();
    

    CreateProxyOfInterface 通過接口創建代理類型;CreateProxyOfClass 通過類創建代理類型;

    默認調用的是無參構造函數。

    2,創建代理類型

    通過API

    你可以參考源碼解決方案

    中的 ExampleConsole 項目。

    如果要直接使用 AopInterceptor.CreateProxyOfInterfaceAopInterceptor.CreateProxyOfClass 方法,通過接口或類來創建代理類型。

            ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
            Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
    

    如果要指定實例化的構造函數,可以這樣:

                // 指定構造函數
                test2 = AopInterceptor.CreateProxyOfClass<Test>("aaa", "bbb");
                test2.MyMethod();
    

    通過 Microsoft.Extensions.DependencyInjection

    Microsoft.Extensions.DependencyInjection 是 .NET Core/ASP.NET Core 默認的依賴注入容器。

    如果需要支持 ASP.NET Core 中使用 AOP,你可以在 Nuget 包中安裝 CZGL.AOP.MEDI

    如果你在控制台下使用 Microsoft.Extensions.DependencyInjection,你可以使用名為 BuildAopProxyIServiceCollection 拓展方法來為容器中的類型,生成代理類型。

    示例如下:

                IServiceCollection _services = new ServiceCollection();
                _services.AddTransient<ITest, Test>();
                var serviceProvider = _services.BuildAopProxy().BuildServiceProvider();
                serviceProvider.GetService<ITest>();
                return serviceProvider;
    

    你可以參考源碼解決方案中的 ExampleMEDI 項目。

    如果你要在 ASP.NET Core 中使用,你可以在 Startup 中,ConfigureServices 方法的最後一行代碼使用 services.BuildAopProxy();

            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllers();
                services.BuildAopProxy();
            }
    

    還可以在 ProgramIHostBuilder 中使用 .UseServiceProviderFactory(new AOPServiceProxviderFactory()) 來配置使用 CZGL.AOP。

    示例:

            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AOPServiceProxviderFactory())
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    });
    

    可以參考解決方案中的 ExampleConsoleExampleWebMEDI 兩個項目。

    你不必擔心引入 CZGL.AOP 后,使用依賴注入會使程序變慢或者破壞容器中的原有屬性。CZGL.AOP 只會在創建容器時處理需要被代理的類型,不會影響容器中的服務,也不會幹擾到依賴注入的執行。

    通過 Autofac

    如果需要在 Autofac 中使用 AOP,則需要引用 CZGL.AOP.Autofac 包。

    如果你在控制台程序中使用 Autofac,則可以在 Build() 後面使用 BuildAopProxy()

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<Test>().As<ITest>();
                var container = builder.Build().BuildAopProxy();
    
                using (ILifetimeScope scope = container.BeginLifetimeScope())
                {
                    // 獲取實例
                    ITest myService = scope.Resolve<ITest>();
                    myService.MyMethod();
                }
    
                Console.ReadKey();
            }
    

    要注意的是,在已經完成的組件註冊創建一個新的容器后,才能調用 BuildAopProxy() 方法,

    這樣針對一個新的容器你可以考慮是否需要對容器中的組件進行代理。

    如果在 ASP.NET Core 中使用 Autofac,你需要在 Program 類的 IHostBuilder 中使用:

    .UseServiceProviderFactory(new AutofacServiceProviderFactory())
    

    如果需要代理已經註冊的組件,則將其替換為:

     .UseServiceProviderFactory(new CZGL.AOP.Autofac.AOPServiceProxviderFactory())
    

    請參考 源碼解決方案中的 ExampleAutofacExampleWebAutofac 兩個項目。

    3,深入使用

    代理類型

    要被代理的類型,需要使用 [Interceptor]來標記,例如:

        [Interceptor]
        public class Test : ITest
        {
        }
    

    支持泛型類型。

    被代理的類型必須是可被繼承的。

    類型的構造函數沒有限制,你可以隨意編寫。

    在使用 API 創建代理類型並且實例化時,你可以指定使用哪個構造函數。

    例如:

    			string a="",b="",c="";
    			ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>(a,b,c);
    

    API 會根據參數的多少以及參數的類型自動尋找合適的構造函數。

    方法、屬性代理

    為了代理方法或屬性,你需要繼承 ActionAttribute 特性,然後為方法或屬性標記此特性,並且將方法或屬性設置為 virtual

    一個類型中的不同方法,可以使用不同的攔截器。

            [Log1]
            public virtual void MyMethod1(){}
            
            [Log2]
            public virtual void MyMethod2(){}
    

    對於屬性,可以在屬性上直接使用特性,或者只在 get 或 set 構造器使用。

            [Log] public virtual string A { get; set; }
            
            // 或
            public virtual string A { [Log] get; set; }
            
            // 或
            public virtual string A { get; [Log] set; }
    

    如果在屬性上使用特性,相當於 [Log] get; [Log] set;

    上下文

    一個簡單的方法或屬性攔截標記是這樣的:

        public class LogAttribute : ActionAttribute
        {
            public override void Before(AspectContext context)
            {
                Console.WriteLine("執行前");
            }
    
            public override object After(AspectContext context)
            {
                Console.WriteLine("執行后");
                if (context.IsMethod)
                    return context.MethodResult;
                else if (context.IsProperty)
                    return context.PropertyValue;
                return null;
            }
        }
    
    

    AspectContext 的屬性說明如下:

    字段 說明
    Type 當前被代理類型生成的代理類型
    ConstructorParamters 類型被實例化時使用的構造函數的參數,如果構造函數沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null。
    IsProperty 當前攔截的是屬性
    PropertyInfo 當前被執行的屬性的信息,可為 null。
    PropertyValue 但調用的是屬性時,返回 get 的結果或 set 的 value 值。
    IsMethod 當前攔截的是方法
    MethodInfo 當前方法的信息
    MethodValues 方法被調用時傳遞的參數,如果此方法沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null
    MethodResult 方法執行返回的結果(如果有)

    攔截方法或屬性的參數

    通過上下文,你可以修改方法或屬性的參數以及攔截返回結果:

        public class LogAttribute : ActionAttribute
        {
            public override void Before(AspectContext context)
            {
                // 攔截並修改方法的參數
                for (int i = 0; i < context.MethodValues.Length; i++)
                {
                    context.MethodValues[i] = (int)context.MethodValues[i] + 1;
                }
                Console.WriteLine("執行前");
            }
    
            public override object After(AspectContext context)
            {
                Console.WriteLine("執行后");
    
                // 攔截方法的執行結果
                context.MethodResult = (int)context.MethodResult + 664;
    
                if (context.IsMethod)
                    return context.MethodResult;
                else if (context.IsProperty)
                    return context.PropertyValue;
                return null;
            }
        }
    
        [Interceptor]
        public class Test
        {
            [Log]
            public virtual int Sum(int a, int b)
            {
                Console.WriteLine("運行中");
                return a + b;
            }
        }
    
                Test test = AopInterceptor.CreateProxyOfClass<Test>();
    
                Console.WriteLine(test.Sum(1, 1));
    

    方法的參數支持 inrefout;支持泛型方法泛型屬性;支持異步方法;

    非侵入式代理

    此種方式不需要改動被代理的類型,你也可以代理程序集中的類型。

        public class LogAttribute : ActionAttribute
        {
            public override void Before(AspectContext context)
            {
                Console.WriteLine("執行前");
            }
    
            public override object After(AspectContext context)
            {
                Console.WriteLine("執行后");
                if (context.IsMethod)
                    return context.MethodResult;
                else if (context.IsProperty)
                    return context.PropertyValue;
                return null;
            }
        }
    
        public class TestNo
        {
            public virtual string A { get; set; }
            public virtual void MyMethod()
            {
                Console.WriteLine("運行中");
            }
        }
    
                TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(new ProxyTypeBuilder()
                    .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                    .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));
    

    通過 ProxyTypeBuilder 來構建代理類型。

    代理方法或屬性都是使用 AddProxyMethod,第一個參數是要使用的攔截器,第二個參數是要攔截的方法。

    如果要攔截屬性,請分開設置屬性的 getset 構造。

    如果多個方法或屬性使用同一個攔截器,則可以這樣:

                TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                    new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                    .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                    .AddProxyMethod("LogAttribute", typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));
    
                TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                    new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                    .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                    .AddProxyMethod(typeof(LogAttribute2), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));
    

    在構造函數中傳遞過去所需要的攔截器,然後在攔截時使用。

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

    聚甘新

  • 文本挖掘之情感分析(一)

    一、文本挖掘  

         文本挖掘則是對文本進行處理,從中挖掘出來文本中有用的信息和關鍵的規則,在文本挖掘領域應用最往廣泛的是對文本進行分類和聚類,其挖掘的方法分為無監督學習和監督學習。文本挖掘還可以劃分為7大類:關鍵詞提取、文本摘要、文本主題模型、文本聚類、文本分類、觀點提取、情感分析。

       關鍵詞提取:對長文本的內容進行分析,輸出能夠反映文本關鍵信息的關鍵詞。

       文本摘要:許多文本挖掘應用程序需要總結文本文檔,以便對大型文檔或某一主題的文檔集合做出簡要概述。

       文本聚類:主要是對未標註的文本進行標註,常見的有 K均值聚類和層次聚類。

       文本分類:文本分類使用監督學習的方法,以對未知數據的分類進行預測的機器學習方法。

       文本主題模型 LDA:LDA(Latent Dirichlet Allocation)是一種文檔主題生成模型,也稱為一個三層貝恭弘=叶 恭弘斯概率模型,包含詞、主題和文檔三層結構,該模型可以用於獲取語料的主題提取和對不同類別的文檔進行分類。

       觀點抽取:對文本(主要針對評論)進行分析,抽取出核心觀點,並判斷極性(正負面),主要用於電商、美食、酒店、汽車等評論進行分析。

       情感分析:對文本進行情感傾向判斷,將文本情感分為正向、負向、中性。用於口碑分析、話題監控、輿情分析。

       因為自己的論文寫的是關於情感分析方面的內容,因此打算接下來主要寫情感分析系列的內容,今天主要寫關於情感分析的介紹以及發展史。

    二、情感分析

    1. 含義

         情感分析主要是通過分析人們對於服務、產品、事件、話題來挖掘出說話人/作者觀點、情感、情緒等的研究。情感分析按照研究內容的不同,可以分為:意見挖掘 / 意見提取 / 主觀性分析 / 情感傾向分析、情感情緒分析、情感打分等。情感傾向問題,即是指挖掘出一段語料中說話人/作者對於某一話題/事件所持有的態度,如褒義、貶義、中性、兩者兼有。情感情緒,則是將情感傾向進行更進一步的細化,依據“大連理工大學的情感詞彙本體庫”可知,可以將情感傾向可以細化為:“喜歡”、“憤怒”、“討厭”等具體的7個大類——21個小類別。情感打分,即根據情感態度對於某一事物進行評分,如淘寶系統的1-5分。 文本中的情感分析還可以分為:顯式情感、隱式情感,顯式情感是指包含明顯的情感詞語(如:高興、漂亮、討厭等),隱式情感則是指不包含情感詞語的情感文本,如:“這個杯子上面有一層灰”。由於隱式情感分析難度較大,因此目前的工作多集中在顯式情感分析領域。

         情感分析按照不同的分析對象,可以分為:文章級別的情感分析、句子級別的情感分析、詞彙級別的情感分析。按照不同的研究內容以及研究的粒度的不同,其研究情感分析的方法也有很大的變化。

         目前的情感分析研究可歸納為:情感資源構建、情感元素抽取、情感分類及情感分析應用系統;

         情感資源構建:情感資源一般來說有:情感詞典、情感語料庫。情感詞典的構建即是將現有的、整理好的情感詞典資源進行整合,比如中文情感詞典有:大連理工大學的情感詞彙本體庫、知網Hownet情感詞典、台灣大學的NTUSD簡體中文情感詞典等,根據不同的需求,應用這些情感詞典。情感語料庫,則是我們要分析的文本,如關於新聞的文本、微博評論文本、商品評論文本、電影評論文本等,這些語料的獲取可以是尋找已經整理好的數據,或者自己爬蟲獲取。推薦一個比較全的中文語料庫網站:中文NLP語料庫。

        情感元素抽取:情感元素抽取則是從語料中抽取出來能夠代表說話人/作者情感態度問題的詞彙,也稱為細粒度情感分析。語料中的評價對象和表達抽取是情感元素抽取的核心內容。評價對象是指語料中被討論的主題,比如對於商品評論來說,用戶常提起的“外觀”、“快遞”、“包裝”等方面;表達抽取主要針對顯式情感表達的文本,是指文本抽取出來能夠代表說話人/作者情感、情緒、意見等的詞彙,比如“漂亮”、“贊同”、“不贊同”等。一般來說,評價對象和表達抽取也可以作為相互獨立的兩個任務。一般來說,分析這兩者的方法有:基於規則、基於機器學習。對於評價對象來說,現如今使用最多的方法是利用主題模型中的LDA(Latent Dirichlet Allocation)模型進行分析;對於表達抽取則有:深度學習的方法、基於JST (Joint Sentiment/Topic )模型的方法等。

         情感分類:情感分類則是將文本分為一個具體的類別,比如情感傾向分析,則是將文檔分為:褒義、貶義、中性等。一般來說,進行情感分類的方法有,基於情感詞典、基於機器學習。基於情感詞典,最典型的方法則是基於知網Hownet情感詞典的So-Hownet指標進行情感分類,基於機器學習的方法則有監督學習方法、半監督學習方法等。

        針對於情感分析,現已經存在一些專有平台,如:基於Boson 數據的情感分析平台,基於產品評論的平台Google Shopping。情感分析除了在電商平台應用廣泛之外,情感分析技術還被引入到對話機器人領域。例如,微軟的“小冰”機器人 可以通過分析用戶的文本輸入和表情貼圖,理解用戶當前的情緒狀況,並據此回復文本或者語音等情感回應。部分研究機構還將情感分析技術融入實體機器人中。

     2. 發展史

         V. H. 和 K. R. McKeown 於 1997 年發表的論文 [1],該論文使用對數線性回歸模型從大量語料庫中識別形容詞的正面或負面語義,同時藉助該模型對語料中出現的形容詞進行分類預測。

         Peter Turney在 2002年在論文 [2] 提出了一種無監督學習的算法,其可以很好的將語料中的詞語分類成正面情感詞和負面情感詞。

        2002 年 Bo Pang 等人在論文 [3] 中使用了傳統的機器學習方法對電影評論數據進行分類,同時也驗證了機器學習的方法的確要比之前基於規則的方法要優。

        2003 年 Blei 等人在論文 [4] 中提出了 LDA(Latent Dirichlet Allocation)模型,在之後的情感分析領域的工作中,很多學者/研究人員都使用主題模型來進行情感分析,當然也不只是基於主題模型來進行情感分析研究,還有很多利用深度學習方法來進行情感分析的研究。

       Lin 和 He在 2009 年的論文 [5] 提出了一種基於主題模型的模型 —JST(Joint Sentiment/Topic),其有效的將情感加入到了經典的主題模型當中,因此利用該模型可以獲取到不同情感極性標籤下不同主題的分佈情況。傳統的主題模型獲取文檔的主題以及詞的分佈情況,但並沒有關注到情感的存在,因此基於該模型可以對文檔的情感傾向進行分析。利用 JST 模型可以有效的直接將語料的主題、情感信息挖掘出來,同時 JST 模型還考慮到了主題、文檔、情感、詞之間的聯繫。

        基於對於語義和句法的考慮,Jo 和 H.OH 在 2011 年提出了 ASUM(Aspect and Sentiment Unification Model)模型,該模型和 JST 模型很相似都是四層的貝恭弘=叶 恭弘斯網絡結構 [6] 。

        基於神經網絡的語義組合算法被驗證是一種非常有效的特徵學習手段,2013年,Richard Socher和Christopher Potts等人提出多個基於樹結構的Recursive Neural Network,該方法通過迭代運算的方式學習變量長度的句子或短語的語義表示,在斯坦福情感分析樹庫(Stanford Sentiment Treebank)上驗證了該方法的有效性 [7]。Nal Kalchbrenner等人描述了一個卷積體繫結構,稱為動態卷積神經網絡(DCNN),他們採用它來進行句子的語義建模。 該網絡使用動態k-Max池,這是一種線性序列的全局池操作。 該網絡處理不同長度的輸入句子,並在句子上引入能夠明確捕獲短程和長程關係的特徵圖。 網絡不依賴於解析樹,並且很容易適用於任何語言。該模型在句子級情感分類任務上取得了非常出色的效果[8]。2015年,Kai Sheng Tai,Richard Socher, Christopher D. Manning在序列化的LSTM (Long Short-Term Memory)模型的基礎上加入了句法結構的因素,該方法在句法分析的結果上進行語義組合,在句子級情感分類和文本蘊含任務(text entailment)上都取得了很好的效果[9]。

       2016年,Qiao Qian, Xiaoyan Zhu等人在LSTM和Bi-LSTM模型的基礎上加入四種規則約束,這四種規則分別是: Non-Sentiment Regularizer,Sentiment Regularizer, Negation Regularizer, Intensity Regularizer,利用語言資源和神經網絡相結合來提升情感分類問題的精度。

      除了上面的一些研究,關於情感分析領域的應用仍然有很多,比如:2015 年鄭祥雲等人通過主題模型提取出來圖書館用戶的主題信息,最後利用這些信息來進行個性化圖書的有效推薦。將 JST 模型中直接引入了情感詞典作為外部先驗知識來對新聞文本進行分析,獲取其中的主旨句,並對主旨句進行情感打分,同時利用情感主旨句來代替全文,這樣能夠使得用戶更有效、更快速的閱讀文章。

      總的來說,情感分析在很多的領域被應用,當然情感分析也有很多的局限性,就是過多的依賴於語料庫信息,同時還需要使用自然語言、人工智能的方法來能夠最大化的挖掘出來其中的信息。

     

     參考文獻:

     [1] :Predicting the semantic orientation of adjectives

     [2]:  Thumbs up or thumbs down? Semantic orientation applied to unsupervised classification of reviews 

     [3] : Thumbs up? Sentiment Classification using Machine Learning Techniques

     [4] :   Latent Dirichlet Allocation

     [5] : Joint sentiment/topic model for sentiment analysis

     [6] : Aspect and sentiment unification model for online review analysis

     [7] : Recursive Deep Models for Semantic Compositionality Over a Sentiment Treebank

     [8]:  A Convolutional Neural Network for Modelling Sentences

     [9]: Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks

     [10] : Linguistically Regularized LSTMs for Sentiment Classification

     

     

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    聚甘新

  • 服務設計思考:平台化

    服務設計思考:平台化

    平台是一套完整的服務。也是一套內部自洽的系統。核心在於分離,業務與通用服務隔離,業務與通用功能隔離。

    目標:

    • 對需求方: 快速響應。可以敏捷地進行需求迭代。

    • 對第三方業務方: 以產品的方式提供服務。所見即所得。所有功能對業務方透明。

    • 對測試方: 簡易明了的測試方式。利於自動化測試,灰度測試。

    • 對運維方: 持續集成,自動化編排,自動化部署。

    • 數據方: 提供多維度,詳盡的服務數據。可以給數據方提供簡便的數據分析。

    • 內部開發: 敏捷開發。迅速集成。

    實現:

    • 如何實現需求的快速響應?
      明確的方向,清晰的邊界。確認通用語言,核心領域。敏捷開發,快速迭代。AB 測試。

    • 如何為第三方提供產品式的服務?

      所見即所得。詳盡的文檔。第三方調試平台,第三方管理平台。

    • mock 服務,自動化測試,swagger 文檔。

    • Devops,CI,DI 等持續集成,服務監控。

    • 業務數據與分析數據異構存儲。提供易於分析的數據服務。

    • 組內服務負責制度,人類最佳的合作人數是 2-3 人。所以兩人維護一個項目,一人主導,一人輔助,兩人交叉合作是一個很好的團隊合作模式。如圖形成一個網狀模式(紅色線代表主導,黑色線輔助)。這樣每一個項目都將有兩個熟悉的人。

    原則

    1. 單一職責。
    2. 業務關注業務,功能關注功能。
    3. 確認邊界,確認核心領域。
    4. 所見即所得。

    實施

    如何推進業務開發快速響應?

    1. 抽離變化與不變。形成基礎服務

      如下面一套用戶體系,將服務抽離,將變與不變隔離。

      用戶 api: 主要提供用戶相關的接口,變化大,更偏向於業務;

      用戶中心: 主要管理用戶核心領域,變動不大,需穩定高可用的服務;

      鑒權授權中心: 變動不大,主要管理用戶憑證核心領域;

    1. 抽離通用功能。

      那些非業務的通用功能應隔離於業務之外:組件化工具化服務化

      來源監控接口限流日誌分析應用監控服務依賴配置管理系統部署等(業務人員不必關心這些功能相關的事情,只需要關注於具體的業務領域)。關注點分離。

      如上面所涉及的,從Spring Cloud的各大組件可以看出,最終的方案都將走上相近的道路。

    1. 領域上下文劃分。劃分微服務項目。業務隔離,數據去中心化。服務組件化。

      Spring cloud 技術棧:

      • 服務治理: 註冊中心,服務調用,衍生的容錯(熔斷器)
      • api 網關: 來源監控,接口限流(Spring Cloud gateway、zuul)
      • **配置中心: ** 配置管理(Apollo)
      • 自動化部署: Jenkins、docker、k8s
      • 日誌與監控: prometheus、influxdb、skywalking、elk
      • 數據可視化: druid、kylin、superset
    1. 細節管控

      接口版本管理, gitflow 管理,項目迭代 release 版本管理,標準化,敏捷開發。

    歡迎關注我的公眾號。

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

    【其他文章推薦】

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

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

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

    聚甘新

  • 從一個計算器開始說起——C#中的工廠方法模式

    從一個計算器開始說起——C#中的工廠方法模式

    工廠模式作為很常見的設計模式,在日常工作中出鏡率非常高,程序員們一定要掌握它的用法喲,今天跟着老胡一起來看看吧。

    舉個例子

    現在先讓我們來看一個例子吧,比如,要開發一個簡單的計算器,完成加減功能,通過命令行讀入形如1+1的公式,輸出2這個結果,讓我們看看怎麼實現吧。

     

    第一個版本

    這個版本裏面,我們不考慮使用模式,就按照最簡單的結構,怎麼方便怎麼來。

    思路非常簡單,僅需要實現以下幾個方法

    • 取運算數
    • 取運算符
    • 輸出結果
         class Program
        {
            static int GetOperatorIndex(string input)
            {
                int operatorIndex = 0;
                for (; operatorIndex < input.Length; operatorIndex++)
                {
                    if (!char.IsDigit(input[operatorIndex]))
                        break;
                }
                return operatorIndex;
            }
    
            static int GetOp(string input, int startIndex, int size = -1)
            {
                string subStr;
                if (size == -1)
                {
                    subStr = input.Substring(startIndex);
                }
                else
                {
                    subStr = input.Substring(startIndex, size);
                }
                return int.Parse(subStr);
            }
    
            static int CalculateExpression(string input)
            {
                var operatorIndex = GetOperatorIndex(input); //得到運算符索引
                var op1 = GetOp(input, 0, operatorIndex); //得到運算數1
                var op2 = GetOp(input, operatorIndex + 1); //得到運算數2
                switch (input[operatorIndex])
                {
                    case '+':
                        return op1 + op2;
                    case '-':
                        return op1 - op2;
                    default:
                        throw new Exception("not support");
                }
            }
    
            static void Main(string[] args)
            {
                string input = Console.ReadLine();
                while(!string.IsNullOrEmpty(input))
                {
                    var result = CalculateExpression(input);
                    Console.WriteLine("={0}", result);
                    input = Console.ReadLine();
                }            
            }
    }
    

    代碼非常簡單,毋庸置疑,這個運算器是可以正常工作的。這也可能是我們大部分人剛剛踏上工作崗位的時候可能會寫出的代碼。但它有着以下這些缺點:

    • 缺乏起碼的抽象,至少加和減應該能抽象出操作類。
    • 缺乏抽象造成了巨型客戶端,所有的邏輯都嵌套在了客戶端裏面。
    • 使用switch case缺乏擴展性,同時switch case也暗指了這部分代碼是屬於變化可能性比較高的地方,我們應該把它們封裝起來。而且不能把他們放在和客戶端代碼一起

    接下來,我們引入我們的主題,工廠方法模式。
     

    工廠方法模式版本

    工廠方法模式使用一個虛擬的工廠來完成產品構建(在這裡是運算符的構建,因為運算符是我們這個程序中最具有變化的部分),通過把可變化的部分封裝在工廠類中以達到隔離變化的目的。我們看看UML圖:

    依葫蘆畫瓢,我們設計思路如下:

    • 設計一個IOperator接口,對應抽象的Product
    • 設計AddOperator和SubtractOperator,對應具體Product
    • 設計IOperatorFactory接口生產Operator
    • 設計OperatorFactory實現抽象IFactory

    關鍵代碼如下,其他讀取操作數之類的代碼就不在贅述。

    • IOperator接口
           interface IOperator
           {
               int Calculate(int op1, int p2);
           }
    
    • 具體Operator
    	class AddOperator : IOperator
            {
                public int Calculate(int op1, int op2)
                {
                    return op1 + op2;
                }
            }
    
            class SubtractOperator : IOperator
            {
                public int Calculate(int op1, int op2)
                {
                    return op1 - op2;
                }
            }
    
    • Factory接口
    	interface IOperatorFactory
            {
                IOperator CreateOperator(char c);
            }
    
    • 具體Factory
    	class OperatorFactory : IOperatorFactory
            {
                public IOperator CreateOperator(char c)
                {
                    switch(c)
                    {
                        case '+':
                            return new AddOperator();
                        case '-':
                            return new SubtractOperator();
                        default:
                            throw new Exception("Not support");
                    }
                }
            }
    
    • 在CalculateExpression裏面使用他們
       static IOperator GetOperator(string input, int operatorIndex)
           {
               IOperatorFactory f = new OperatorFactory();
               return f.CreateOperator(input[operatorIndex]);
           }
    
           static int CalculateExpression(string input)
           {
               var operatorIndex = GetOperatorIndex(input);
               var op1 = GetOp(input, 0, operatorIndex);
               var op2 = GetOp(input, operatorIndex + 1);
               IOperator op = GetOperator(input, operatorIndex);
               return op.Calculate(op1, op2);                    
           }
    

    這樣,我們就用工廠方法重新寫了一次計算器,現在看看,好處有

    • 容易變化的創建部分被工廠封裝了起來,工廠和客戶端以接口的形式依賴,工廠內部邏輯可以隨時變化而不用擔心影響客戶端代碼
    • 工厂部分可以放在另外一個程序集,項目規劃會更加合理
    • 客戶端僅僅需要知道工廠和抽象的產品類,不需要再知道每一個具體的產品(不需要知道如何構建每一個具體運算符),符合迪米特法則
    • 擴展性增強,如果之後需要添加乘法multiple,那麼僅需要添加一個Operator類代表Multiple並且修改Facotry裏面的生成Operator邏輯就可以了,不會影響到客戶端
       

    自此,我們已經在代碼裏面實現了工廠方法模式,但可能有朋友就會想,雖然現在擴展性增強了,但是新添加運算符還是需要修改已有的工廠,這不是違反了開閉原則么。。有沒有更好的辦法呢?當然是有的。
     

    反射版本

    想想工廠方法那個版本,我們為什麼增加新的運算符就會不可避免的修改現有工廠?原因就是我們通過switch case來硬編碼“教導”工廠如何根據用戶輸入產生正確的運算符,那麼如果有一種方法可以讓工廠自動學會發現新的運算符,那麼我們的目的不就達到了?

    嗯,我想聰明的朋友們已經知道了,用屬性嘛,在C#中,這種方法完成類的自描述,是最好不過了的。

    我們的設計思路如下:

    • 定義一個描述屬性以識別運算符
    • 在運算符中添加該描述屬性
    • 在工廠啟動的時候,掃描程序集以註冊所有運算符

    代碼如下:

    • 描述屬性
        class OperatorDescriptionAttribute : Attribute
        {
            public char Symbol { get; }
            public OperatorDescriptionAttribute(char c)
            {
                Symbol = c;
            }
        }
    
    • 添加描述屬性到運算符
        [OperatorDescription('+')]
        class AddOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 + op2;
            }
        }
    
        [OperatorDescription('-')]
        class SubtractOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 - op2;
            }
        }
    
    • 讓工廠使用描述屬性
        class OperatorFactory : IOperatorFactory
        {
            private Dictionary<char, IOperator> dict = new Dictionary<char, IOperator>();
            public OperatorFactory()
            {
                Assembly assembly = Assembly.GetExecutingAssembly();
                foreach (var type in assembly.GetTypes())
                {
                    if (typeof(IOperator).IsAssignableFrom (type) 
                        && !type.IsInterface)
                    {
                        var attribute = type.GetCustomAttribute<OperatorDescriptionAttribute>();
                        if(attribute != null)
                        {
                            dict[attribute.Symbol] = Activator.CreateInstance(type) as IOperator;
                        }
                    }
                }
            }
            public IOperator CreateOperator(char c)
            {
                if(!dict.ContainsKey(c))
                {
                    throw new Exception("Not support");
                }
                return dict[c];
            }
        }
    

    經過這種改造,現在程序對擴展性支持已經很友好了,需要添加Multiple,只需要添加一個Multiple類就可以,其他代碼都不用修改,這樣就完美符合開閉原則了。

      [OperatorDescription('*')]
      class MultipleOperator : IOperator
      {
          public int Calculate(int op1, int op2)
          {
              return op1 * op2;
          }
      }
    

    這就是我們怎麼一步步從最原始的代碼走過來,一點點重構讓代碼實現工廠方法模式,最終再完美支持開閉原則的過程,希望能幫助到大家。

    其實關於最後那個通過標記屬性實現擴展,微軟有個MEF框架支持的很好,原理跟這個有點相似,有機會我們再聊聊MEF。

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

    【其他文章推薦】

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    聚甘新

  • 【Flutter實戰】六大布局組件及半圓菜單案例

    【Flutter實戰】六大布局組件及半圓菜單案例

    老孟導讀:Flutter中布局組件有水平 / 垂直布局組件( RowColumn )、疊加布局組件( StackIndexedStack )、流式布局組件( Wrap )和 自定義布局組件(Flow)。

    水平、垂直布局組件

    Row 是將子組件以水平方式布局的組件, Column 是將子組件以垂直方式布局的組件。項目中 90% 的頁面布局都可以通過 Row 和 Column 來實現。

    將3個組件水平排列:

    Row(
      children: <Widget>[
        Container(
          height: 50,
          width: 100,
          color: Colors.red,
        ),
        Container(
          height: 50,
          width: 100,
          color: Colors.green,
        ),
        Container(
          height: 50,
          width: 100,
          color: Colors.blue,
        ),
      ],
    )
    

    將3個組件垂直排列:

    Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Container(
          height: 50,
          width: 100,
          color: Colors.red,
        ),
        Container(
          height: 50,
          width: 100,
          color: Colors.green,
        ),
        Container(
          height: 50,
          width: 100,
          color: Colors.blue,
        ),
      ],
    )
    

    在 Row 和 Column 中有一個非常重要的概念:主軸( MainAxis )交叉軸( CrossAxis ),主軸就是與組件布局方向一致的軸,交叉軸就是與主軸方向垂直的軸。

    具體到 Row 組件,主軸 是水平方向,交叉軸 是垂直方向。而 Column 與 Row 正好相反,主軸 是垂直方向,交叉軸 是水平方向。

    明白了 主軸 和 交叉軸 概念,我們來看下 mainAxisAlignment 屬性,此屬性表示主軸方向的對齊方式,默認值為 start,表示從組件的開始處布局,此處的開始位置和 textDirection 屬性有關,textDirection 表示文本的布局方向,其值包括 ltr(從左到右) 和 rtl(從右到左),當 textDirection = ltr 時,start 表示左側,當 textDirection = rtl 時,start 表示右側,

    Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.black)),
      child: Row(
        children: <Widget>[
          Container(
            height: 50,
            width: 100,
            color: Colors.red,
          ),
          Container(
            height: 50,
            width: 100,
            color: Colors.green,
          ),
          Container(
            height: 50,
            width: 100,
            color: Colors.blue,
          ),
        ],
      ),
    )
    

    黑色邊框是Row控件的範圍,默認情況下Row鋪滿父組件。

    主軸對齊方式有6種,效果如下圖:

    spaceAround 和 spaceEvenly 區別是:

    • spaceAround :第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
    • spaceEvenly : 所有間距一樣。

    和主軸對齊方式相對應的就是交叉軸對齊方式 crossAxisAlignment ,交叉軸對齊方式默認是居中。Row控件的高度是依賴子控件高度,因此子控件高都一樣時,Row的高和子控件高相同,此時是無法體現交叉軸對齊方式,修改3個顏色塊高分別為50,100,150,這樣Row的高是150,代碼如下:

    Container(
          decoration: BoxDecoration(border: Border.all(color: Colors.black)),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              Container(
                height: 50,
                width: 100,
                color: Colors.red,
              ),
              Container(
                height: 100,
                width: 100,
                color: Colors.green,
              ),
              Container(
                height: 150,
                width: 100,
                color: Colors.blue,
              ),
            ],
          ),
        )
    

    主軸對齊方式效果如下圖:

    mainAxisSize 表示主軸尺寸,有 min 和 max 兩種方式,默認是 maxmin 表示盡可能小,max 表示盡可能大。

    Container(
    	decoration: BoxDecoration(border: Border.all(color: Colors.black)),
    	child: Row(
    		mainAxisSize: MainAxisSize.min,
    		...
    	)
    )
    

    看黑色邊框,正好包裹子組件,而 max 效果如下:

    textDirection 表示子組件主軸布局方向,值包括 ltr(從左到右) 和 rtl(從右到左)

    Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.black)),
      child: Row(
        textDirection: TextDirection.rtl,
        children: <Widget>[
          ...
        ],
      ),
    )
    

    verticalDirection 表示子組件交叉軸布局方向:

    • up :從底部開始,並垂直堆疊到頂部,對齊方式的 start 在底部,end 在頂部。
    • down: 與 up 相反。
    Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.black)),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        verticalDirection: VerticalDirection.up,
        children: <Widget>[
          Container(
            height: 50,
            width: 100,
            color: Colors.red,
          ),
          Container(
            height: 100,
            width: 100,
            color: Colors.green,
          ),
          Container(
            height: 150,
            width: 100,
            color: Colors.blue,
          ),
        ],
      ),
    )
    

    想一想這種效果完全可以通過對齊方式實現,那麼為什麼還要有 textDirectionverticalDirection 這兩個屬性,官方API文檔已經解釋了這個問題:

    This is also used to disambiguate start and end values (e.g. [MainAxisAlignment.start] or [CrossAxisAlignment.end]).

    用於消除 MainAxisAlignment.start 和 CrossAxisAlignment.end 值的歧義的。

    疊加布局組件

    疊加布局組件包含 StackIndexedStack,Stack 組件將子組件疊加显示,根據子組件的順利依次向上疊加,用法如下:

    Stack(
      children: <Widget>[
        Container(
          height: 200,
          width: 200,
          color: Colors.red,
        ),
        Container(
          height: 170,
          width: 170,
          color: Colors.blue,
        ),
        Container(
          height: 140,
          width: 140,
          color: Colors.yellow,
        )
      ],
    )
    

    Stack 對未定位(不被 Positioned 包裹)子組件的大小由 fit 參數決定,默認值是 StackFit.loose ,表示子組件自己決定,StackFit.expand 表示盡可能的大,用法如下:

    Stack(
      fit: StackFit.expand,
      children: <Widget>[
        Container(
          height: 200,
          width: 200,
          color: Colors.red,
        ),
        Container(
          height: 170,
          width: 170,
          color: Colors.blue,
        ),
        Container(
          height: 140,
          width: 140,
          color: Colors.yellow,
        )
      ],
    )
    

    效果只有黃色(最後一個組件的顏色),並不是其他組件沒有繪製,而是另外兩個組件被黃色組件覆蓋。

    Stack 對未定位(不被 Positioned 包裹)子組件的對齊方式由 alignment 控制,默認左上角對齊,用法如下:

    Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        Container(
          height: 200,
          width: 200,
          color: Colors.red,
        ),
        Container(
          height: 170,
          width: 170,
          color: Colors.blue,
        ),
        Container(
          height: 140,
          width: 140,
          color: Colors.yellow,
        )
      ],
    )
    

    通過 Positioned 定位的子組件:

    Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        Container(
          height: 200,
          width: 200,
          color: Colors.red,
        ),
        Container(
          height: 170,
          width: 170,
          color: Colors.blue,
        ),
        Positioned(
          left: 30,
          right: 40,
          bottom: 50,
          top: 60,
          child: Container(
            color: Colors.yellow,
          ),
        )
      ],
    )
    

    topbottomleftright 四種定位屬性,分別表示距離上下左右的距離。

    如果子組件超過 Stack 邊界由 overflow 控制,默認是裁剪,下面設置總是显示的用法:

    Stack(
      overflow: Overflow.visible,
      children: <Widget>[
        Container(
          height: 200,
          width: 200,
          color: Colors.red,
        ),
        Positioned(
          left: 100,
          top: 100,
          height: 150,
          width: 150,
          child: Container(
            color: Colors.green,
          ),
        )
      ],
    )
    

    IndexedStack 是 Stack 的子類,Stack 是將所有的子組件疊加显示,而 IndexedStack 通過 index 只显示指定索引的子組件,用法如下:

    class IndexedStackDemo extends StatefulWidget {
      @override
      _IndexedStackDemoState createState() => _IndexedStackDemoState();
    }
    
    class _IndexedStackDemoState extends State<IndexedStackDemo> {
      int _index = 0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            SizedBox(height: 50,),
            _buildIndexedStack(),
            SizedBox(height: 30,),
            _buildRow(),
          ],
        );
      }
    
      _buildIndexedStack() {
        return IndexedStack(
          index: _index,
          children: <Widget>[
            Center(
              child: Container(
                height: 300,
                width: 300,
                color: Colors.red,
                alignment: Alignment.center,
                child: Icon(
                  Icons.fastfood,
                  size: 60,
                  color: Colors.blue,
                ),
              ),
            ),
            Center(
              child: Container(
                height: 300,
                width: 300,
                color: Colors.green,
                alignment: Alignment.center,
                child: Icon(
                  Icons.cake,
                  size: 60,
                  color: Colors.blue,
                ),
              ),
            ),
            Center(
              child: Container(
                height: 300,
                width: 300,
                color: Colors.yellow,
                alignment: Alignment.center,
                child: Icon(
                  Icons.local_cafe,
                  size: 60,
                  color: Colors.blue,
                ),
              ),
            ),
          ],
        );
      }
    
      _buildRow() {
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            IconButton(
              icon: Icon(Icons.fastfood),
              onPressed: () {
                setState(() {
                  _index = 0;
                });
              },
            ),
            IconButton(
              icon: Icon(Icons.cake),
              onPressed: () {
                setState(() {
                  _index = 1;
                });
              },
            ),
            IconButton(
              icon: Icon(Icons.local_cafe),
              onPressed: () {
                setState(() {
                  _index = 2;
                });
              },
            ),
          ],
        );
      }
    }
    

    流式布局組件

    Wrap 為子組件進行水平或者垂直方向布局,且當空間用完時,Wrap 會自動換行,也就是流式布局。

    創建多個子控件做為 Wrap 的子控件,代碼如下:

    Wrap(
      children: List.generate(10, (i) {
        double w = 50.0 + 10 * i;
        return Container(
          color: Colors.primaries[i],
          height: 50,
          width: w,
          child: Text('$i'),
        );
      }),
    )
    

    direction 屬性控制布局方向,默認為水平方向,設置方向為垂直代碼如下:

    Wrap(
      direction: Axis.vertical,
      children: List.generate(4, (i) {
        double w = 50.0 + 10 * i;
        return Container(
          color: Colors.primaries[i],
          height: 50,
          width: w,
          child: Text('$i'),
        );
      }),
    )
    

    alignment 屬性控制主軸對齊方式,crossAxisAlignment 屬性控制交叉軸對齊方式,對齊方式只對有剩餘空間的行或者列起作用,例如水平方向上正好填充完整,則不管設置主軸對齊方式為什麼,看上去的效果都是鋪滿。

    說明 :主軸就是與當前組件方向一致的軸,而交叉軸就是與當前組件方向垂直的軸,如果Wrap的布局方向為水平方向 Axis.horizontal,那麼主軸就是水平方向,反之布局方向為垂直方向 Axis.vertical ,主軸就是垂直方向。

    Wrap(
    	alignment: WrapAlignment.spaceBetween,
    	...
    )
    

    主軸對齊方式有6種,效果如下圖:

    spaceAroundspaceEvenly 區別是:

    • spaceAround:第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
    • spaceEvenly:所有間距一樣。

    設置交叉軸對齊代碼如下:

    Wrap(
    	crossAxisAlignment: WrapCrossAlignment.center,
    	...
    )
    

    如果 Wrap 的主軸方向為水平方向,交叉軸方向則為垂直方向,如果想要看到交叉軸對齊方式的效果需要設置子控件的高不一樣,代碼如下:

    Wrap(
      spacing: 5,
      runSpacing: 3,
      crossAxisAlignment: WrapCrossAlignment.center,
      children: List.generate(10, (i) {
        double w = 50.0 + 10 * i;
        double h = 50.0 + 5 * i;
        return Container(
          color: Colors.primaries[i],
          height: h,
          alignment: Alignment.center,
          width: w,
          child: Text('$i'),
        );
      }),
    )
    

    runAlignment 屬性控制 Wrap 的交叉抽方向上每一行的對齊方式,下面直接看 runAlignment 6中方式對應的效果圖,

    runAlignmentalignment 的區別:

    • alignment :是主軸方向上對齊方式,作用於每一行。
    • runAlignment :是交叉軸方向上將每一行看作一個整體的對齊方式。

    spacingrunSpacing 屬性控制Wrap主軸方向和交叉軸方向子控件之間的間隙,代碼如下:

    Wrap(
    	spacing: 5,
        runSpacing: 2,
    	...
    )
    

    textDirection 屬性表示 Wrap 主軸方向上子組件的方向,取值範圍是 ltr(從左到右) 和 rtl(從右到左),下面是從右到左的代碼:

    Wrap(
    	textDirection: TextDirection.rtl,
    	...
    )
    

    verticalDirection 屬性表示 Wrap 交叉軸方向上子組件的方向,取值範圍是 up(向上) 和 down(向下),設置代碼如下:

    Wrap(
    	verticalDirection: VerticalDirection.up,
    	...
    )
    

    注意:文字為0的組件是在下面的。

    自定義布局組件

    大部分情況下,不會使用到 Flow ,但 Flow 可以調整子組件的位置和大小,結合Matrix4繪製出各種酷炫的效果。

    Flow 組件對使用轉換矩陣操作子組件經過系統優化,性能非常高效。

    基本用法如下:

    Flow(
      delegate: SimpleFlowDelegate(),
      children: List.generate(5, (index) {
        return Container(
          height: 100,
          color: Colors.primaries[index % Colors.primaries.length],
        );
      }),
    )
    

    delegate 控制子組件的位置和大小,定義如下 :

    class SimpleFlowDelegate extends FlowDelegate {
      @override
      void paintChildren(FlowPaintingContext context) {
        for (int i = 0; i < context.childCount; ++i) {
          context.paintChild(i);
        }
      }
    
      @override
      bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
        return false;
      }
    }
    

    delegate 要繼承 FlowDelegate,重寫 paintChildrenshouldRepaint 函數,上面直接繪製子組件,效果如下:

    只看到一種顏色並不是只繪製了這一個,而是疊加覆蓋了,和 Stack 類似,下面讓每一個組件有一定的偏移,SimpleFlowDelegate 修改如下:

    class SimpleFlowDelegate extends FlowDelegate {
      @override
      void paintChildren(FlowPaintingContext context) {
        for (int i = 0; i < context.childCount; ++i) {
          context.paintChild(i,transform: Matrix4.translationValues(0,i*30.0,0));
        }
      }
    
      @override
      bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
        return false;
      }
    }
    

    每一個子組件比上一個組件向下偏移30。

    仿 掘金-我的效果

    效果如下:

    到拿到一個頁面時,先要將其拆分,上面的效果拆分如下:

    總體分為3個部分,水平布局,紅色區域圓形頭像代碼如下:

    _buildCircleImg() {
      return Container(
        height: 60,
        width: 60,
        decoration: BoxDecoration(
            shape: BoxShape.circle,
            image: DecorationImage(image: AssetImage('assets/images/logo.png'))),
      );
    }
    

    藍色區域代碼如下:

    _buildCenter() {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('老孟Flutter', style: TextStyle(fontSize: 20),),
          Text('Flutter、Android', style: TextStyle(color: Colors.grey),)
        ],
      );
    }
    

    綠色區域是一個圖標,代碼如下:

    Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
    

    將這3部分組合在一起:

    Container(
      color: Colors.grey.withOpacity(.5),
      alignment: Alignment.center,
      child: Container(
        height: 100,
        color: Colors.white,
        child: Row(
          children: <Widget>[
            SizedBox(
              width: 15,
            ),
            _buildCircleImg(),
            SizedBox(
              width: 25,
            ),
            Expanded(
              child: _buildCenter(),
            ),
            Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
            SizedBox(
              width: 15,
            ),
          ],
        ),
      ),
    )
    

    最終的效果就是開始我們看到的效果圖。

    水平展開/收起菜單

    使用Flow實現水平展開/收起菜單的功能,代碼如下:

    class DemoFlowPopMenu extends StatefulWidget {
      @override
      _DemoFlowPopMenuState createState() => _DemoFlowPopMenuState();
    }
    
    class _DemoFlowPopMenuState extends State<DemoFlowPopMenu>
        with SingleTickerProviderStateMixin {
      //動畫必須要with這個類
      AnimationController _ctrlAnimationPopMenu; //定義動畫的變量
      IconData lastTapped = Icons.notifications;
      final List<IconData> menuItems = <IconData>[
        //菜單的icon
        Icons.home,
        Icons.new_releases,
        Icons.notifications,
        Icons.settings,
        Icons.menu,
      ];
    
      void _updateMenu(IconData icon) {
        if (icon != Icons.menu) {
          setState(() => lastTapped = icon);
        } else {
          _ctrlAnimationPopMenu.status == AnimationStatus.completed
              ? _ctrlAnimationPopMenu.reverse() //展開和收攏的效果
              : _ctrlAnimationPopMenu.forward();
        }
      }
    
      @override
      void initState() {
        super.initState();
        _ctrlAnimationPopMenu = AnimationController(
          //必須初始化動畫變量
          duration: const Duration(milliseconds: 250), //動畫時長250毫秒
          vsync: this, //SingleTickerProviderStateMixin的作用
        );
      }
    
    //生成Popmenu數據
      Widget flowMenuItem(IconData icon) {
        final double buttonDiameter =
            MediaQuery.of(context).size.width * 2 / (menuItems.length * 3);
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0),
          child: RawMaterialButton(
            fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
            splashColor: Colors.amber[100],
            shape: CircleBorder(),
            constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
            onPressed: () {
              _updateMenu(icon);
            },
            child: Icon(icon, color: Colors.white, size: 30.0),
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Flow(
            delegate: FlowMenuDelegate(animation: _ctrlAnimationPopMenu),
            children: menuItems
                .map<Widget>((IconData icon) => flowMenuItem(icon))
                .toList(),
          ),
        );
      }
    }
    

    FlowMenuDelegate 定義如下:

    class FlowMenuDelegate extends FlowDelegate {
      FlowMenuDelegate({this.animation}) : super(repaint: animation);
      final Animation<double> animation;
    
      @override
      void paintChildren(FlowPaintingContext context) {
        double x = 50.0; //起始位置
        double y = 50.0; //橫向展開,y不變
        for (int i = 0; i < context.childCount; ++i) {
          x = context.getChildSize(i).width * i * animation.value;
          context.paintChild(
            i,
            transform: Matrix4.translationValues(x, y, 0),
          );
        }
      }
    
      @override
      bool shouldRepaint(FlowMenuDelegate oldDelegate) =>
          animation != oldDelegate.animation;
    }
    

    半圓菜單展開/收起

    代碼如下:

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    
    class DemoFlowMenu extends StatefulWidget {
      @override
      _DemoFlowMenuState createState() => _DemoFlowMenuState();
    }
    
    class _DemoFlowMenuState extends State<DemoFlowMenu>
        with TickerProviderStateMixin {
      //動畫需要這個類來混合
      //動畫變量,以及初始化和銷毀
      AnimationController _ctrlAnimationCircle;
    
      @override
      void initState() {
        super.initState();
        _ctrlAnimationCircle = AnimationController(
            //初始化動畫變量
            lowerBound: 0,
            upperBound: 80,
            duration: Duration(milliseconds: 300),
            vsync: this);
        _ctrlAnimationCircle.addListener(() => setState(() {}));
      }
    
      @override
      void dispose() {
        _ctrlAnimationCircle.dispose(); //銷毀變量,釋放資源
        super.dispose();
      }
    
      //生成Flow的數據
      List<Widget> _buildFlowChildren() {
        return List.generate(
            5,
            (index) => Container(
                  child: Icon(
                    index.isEven ? Icons.timer : Icons.ac_unit,
                    color: Colors.primaries[index % Colors.primaries.length],
                  ),
                ));
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: <Widget>[
            Positioned.fill(
              child: Flow(
                delegate: FlowAnimatedCircle(_ctrlAnimationCircle.value),
                children: _buildFlowChildren(),
              ),
            ),
            Positioned.fill(
              child: IconButton(
                icon: Icon(Icons.menu),
                onPressed: () {
                  setState(() {
                    //點擊后讓動畫可前行或回退
                    _ctrlAnimationCircle.status == AnimationStatus.completed
                        ? _ctrlAnimationCircle.reverse()
                        : _ctrlAnimationCircle.forward();
                  });
                },
              ),
            ),
          ],
        );
      }
    }
    

    FlowAnimatedCircle 代碼如下:

    class FlowAnimatedCircle extends FlowDelegate {
      final double radius; //綁定半徑,讓圓動起來
      FlowAnimatedCircle(this.radius);
    
      @override
      void paintChildren(FlowPaintingContext context) {
        if (radius == 0) {
          return;
        }
        double x = 0; //開始(0,0)在父組件的中心
        double y = 0;
        for (int i = 0; i < context.childCount; i++) {
          x = radius * cos(i * pi / (context.childCount - 1)); //根據數學得出坐標
          y = radius * sin(i * pi / (context.childCount - 1)); //根據數學得出坐標
          context.paintChild(i, transform: Matrix4.translationValues(x, -y, 0));
        } //使用Matrix定位每個子組件
      }
    
      @override
      bool shouldRepaint(FlowDelegate oldDelegate) => true;
    }
    

    交流

    老孟Flutter博客地址(330個控件用法):http://laomengit.com

    歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

    聚甘新

  • 環境篇:Kylin3.0.1集成CDH6.2.0

    環境篇:Kylin3.0.1集成CDH6.2.0

    環境篇:Kylin3.0.1集成CDH6.2.0

    Kylin是什麼?

    Apache Kylin™是一個開源的、分佈式的分析型數據倉庫,提供Hadoop/Spark 之上的 SQL 查詢接口及多維分析(OLAP)能力以支持超大規模數據,最初由 eBay 開發並貢獻至開源社區。它能在亞秒內查詢巨大的表。

    Apache Kylin™ 令使用者僅需三步,即可實現超大數據集上的亞秒級查詢。

    1. 定義數據集上的一個星形或雪花形模型
    2. 在定義的數據表上構建cube
    3. 使用標準 SQL 通過 ODBC、JDBC 或 RESTFUL API 進行查詢,僅需亞秒級響應時間即可獲得查詢結果

    如果沒有Kylin

    大數據在數據積累后,需要計算,而數據越多,算力越差,內存需求也越高,詢時間與數據量成線性增長,而這些對於Kylin影響不大,大數據中硬盤往往比內存要更便宜,Kylin通過與計算的形式,以空間換時間,亞秒級的響應讓人們愛不釋手。

    注:所謂詢時間與數據量成線性增長:假設查詢 1 億條記錄耗時 1 分鐘,那麼查詢 10 億條記錄就需 10分鐘,100 億條記錄就至少需要 1 小時 40 分鐘。

    http://kylin.apache.org/cn/

    1 Kylin架構

    Kylin 提供與多種數據可視化工具的整合能力,如 Tableau,PowerBI 等,令用戶可以使用 BI 工具對 Hadoop 數據進行分析

    1. REST Server REST Server

    是一套面嚮應用程序開發的入口點,旨在實現針對 Kylin 平台的應用開發 工作。 此類應用程序可以提供查詢、獲取結果、觸發 cube 構建任務、獲取元數據以及獲取 用戶權限等等。另外可以通過 Restful 接口實現 SQL 查詢。

    1. 查詢引擎(Query Engine)

    當 cube 準備就緒后,查詢引擎就能夠獲取並解析用戶查詢。它隨後會與系統中的其它 組件進行交互,從而向用戶返回對應的結果。

    1. 路由器(Routing)

    在最初設計時曾考慮過將 Kylin 不能執行的查詢引導去 Hive 中繼續執行,但在實踐后 發現 Hive 與 Kylin 的速度差異過大,導致用戶無法對查詢的速度有一致的期望,很可能大 多數查詢幾秒內就返回結果了,而有些查詢則要等幾分鐘到幾十分鐘,因此體驗非常糟糕。 最後這個路由功能在發行版中默認關閉。

    1. 元數據管理工具(Metadata)

    Kylin 是一款元數據驅動型應用程序。元數據管理工具是一大關鍵性組件,用於對保存 在 Kylin 當中的所有元數據進行管理,其中包括最為重要的 cube 元數據。其它全部組件的 正常運作都需以元數據管理工具為基礎。 Kylin 的元數據存儲在 hbase 中。

    1. 任務引擎(Cube Build Engine)

    這套引擎的設計目的在於處理所有離線任務,其中包括 shell 腳本、Java API 以及 MapReduce 任務等等。任務引擎對 Kylin 當中的全部任務加以管理與協調,從而確保每一項任務 都能得到切實執行並解決其間出現的故障。

    2 Kylin軟硬件要求

    • 軟件要求
      • Hadoop: 2.7+, 3.1+ (since v2.5)
      • Hive: 0.13 – 1.2.1+
      • HBase: 1.1+, 2.0 (since v2.5)
      • Spark (optional) 2.3.0+
      • Kafka (optional) 1.0.0+ (since v2.5)
      • JDK: 1.8+ (since v2.5)
      • OS: Linux only, CentOS 6.5+ or Ubuntu 16.0.4+
    • 硬件要求
      • 最低配置:4 core CPU, 16 GB memory
      • 高負載場景:24 core CPU, 64 GB memory

    3 Kylin單機安裝

    3.1 修改環境變量

    vim /etc/profile 
    #>>>注意地址指定為自己的
    #kylin
    export KYLIN_HOME=/usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60
    export PATH=$PATH:$KYLIN_HOME/bin
        
    #cdh
    export CDH_HOME=/opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373
    
    #hadoop
    export HADOOP_HOME=${CDH_HOME}/lib/hadoop
    export HADOOP_DIR=${HADOOP_HOME}
    export HADOOP_CLASSPATH=${HADOOP_HOME}
    export PATH=$PATH:$HADOOP_HOME/bin
    export PATH=$PATH:$HADOOP_HOME/sbin
        
    #hbase
    export HBASE_HOME=${CDH_HOME}/lib/hbase
    export PATH=$PATH:$HBASE_HOME/bin
        
     #hive
    export HIVE_HOME=${CDH_HOME}/lib/hive
    export PATH=$PATH:$HIVE_HOME/bin
        
    #spark
    export SPARK_HOME=${CDH_HOME}/lib/spark
    export PATH=$PATH:$SPARK_HOME/bin   
    
    #kafka
    export KAFKA_HOME=${CDH_HOME}/lib/kafka
    export PATH=$PATH:$KAFKA_HOME/bin 
    #<<<
    
    source /etc/profile 
    

    3.2 修改hdfs用戶權限

    usermod -s /bin/bash hdfs
    su hdfs
    hdfs dfs -mkdir /kylin
    hdfs dfs -chmod a+rwx /kylin
    su
    

    3.3 上傳安裝包解壓

    mkdir /usr/local/src/kylin
    cd /usr/local/src/kylin
    tar -zxvf apache-kylin-3.0.1-bin-cdh60.tar.gz
    cd /usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60
    

    3.4 Java兼容hbase

    • hbase 所有節點

    在CLASSPATH=${CLASSPATH}:$JAVA_HOME/lib/tools.jar后添加

    >>---
    :/opt/cloudera/parcels/CDH/lib/hbase/lib/*
    <<---
    
    • Kylin節點添加jar包
    cp /opt/cloudera/cm/common_jars/commons-configuration-1.9.cf57559743f64f0b3a504aba449c9649.jar /usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60/tomcat/lib
    

    這2步不做會引起 Could not find or load main class org.apache.hadoop.hbase.util.GetJavaProperty

    3.5 啟動停止

    ./bin/kylin.sh start
    #停止  ./bin/kylin.sh stop
    

    3.6 web頁面

    訪問端口7070

    賬號密碼:ADMIN / KYLIN

    4 Kylin集群安裝

    4.1 修改環境變量

    vim /etc/profile 
    #>>>注意地址指定為自己的
    #kylin
    export KYLIN_HOME=/usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60
    export PATH=$PATH:$KYLIN_HOME/bin
        
    #cdh
    export CDH_HOME=/opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373
    
    #hadoop
    export HADOOP_HOME=${CDH_HOME}/lib/hadoop
    export HADOOP_DIR=${HADOOP_HOME}
    export HADOOP_CLASSPATH=${HADOOP_HOME}
    export PATH=$PATH:$HADOOP_HOME/bin
    export PATH=$PATH:$HADOOP_HOME/sbin
        
    #hbase
    export HBASE_HOME=${CDH_HOME}/lib/hbase
    export PATH=$PATH:$HBASE_HOME/bin
        
     #hive
    export HIVE_HOME=${CDH_HOME}/lib/hive
    export PATH=$PATH:$HIVE_HOME/bin
        
    #spark
    export SPARK_HOME=${CDH_HOME}/lib/spark
    export PATH=$PATH:$SPARK_HOME/bin   
    
    #kafka
    export KAFKA_HOME=${CDH_HOME}/lib/kafka
    export PATH=$PATH:$KAFKA_HOME/bin 
    #<<<
    
    source /etc/profile 
    

    4.2 修改hdfs用戶權限

    usermod -s /bin/bash hdfs
    su hdfs
    hdfs dfs -mkdir /kylin
    hdfs dfs -chmod a+rwx /kylin
    su
    

    4.3 上傳安裝包解壓

    mkdir /usr/local/src/kylin
    cd /usr/local/src/kylin
    tar -zxvf apache-kylin-3.0.1-bin-cdh60.tar.gz
    cd /usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60
    

    4.4 Java兼容hbase

    • hbase 所有節點

    在CLASSPATH=${CLASSPATH}:$JAVA_HOME/lib/tools.jar后添加

    vim /opt/cloudera/parcels/CDH/lib/hbase/bin/hbase
    >>---
    :/opt/cloudera/parcels/CDH/lib/hbase/lib/*
    <<---
    
    • Kylin節點添加jar包
    cp /opt/cloudera/cm/common_jars/commons-configuration-1.9.cf57559743f64f0b3a504aba449c9649.jar /usr/local/src/kylin/apache-kylin-3.0.1-bin-cdh60/tomcat/lib
    

    這2步不做會引起 Could not find or load main class org.apache.hadoop.hbase.util.GetJavaProperty

    4.5 修改kylin配置文件

    Kylin根據自己的運行職責狀態,可以劃分為以下三大類角色

    • Job節點:僅用於任務調度,不用於查詢
    • Query節點:僅用於查詢,不用於構建任務的調度
    • All節點:模式代表該服務同時用於任務調度和 SQL 查詢
      • 2.0以前同一個集群只能有一個節點(Kylin實例)用於job調度(all或者job模式的只能有一個實例)
      • 2.0開始可以多個job或者all節點實現HA
    vim conf/kylin.properties
    >>----
    #指定元數據庫路徑,默認值為 kylin_metadata@hbase,確保kylin集群使用一致
    kylin.metadata.url=kylin_metadata@hbase
    #指定 Kylin 服務所用的 HDFS 路徑,默認值為 /kylin,請確保啟動 Kylin 實例的用戶有讀寫該目錄的權限
    kylin.env.hdfs-working-dir=/kylin
    kylin.server.mode=all
    kylin.server.cluster-servers=cdh01.cm:7070,cdh02.cm:7070,cdh03.cm:7070
    kylin.storage.url=hbase
    #構建任務失敗后的重試次數,默認值為 0
    kylin.job.retry=2
    #最大構建併發數,默認值為 10
    kylin.job.max-concurrent-jobs=10
    #構建引擎間隔多久檢查 Hadoop 任務的狀態,默認值為 10(s)
    kylin.engine.mr.yarn-check-interval-seconds=10
    #MapReduce 任務啟動前會依據輸入預估 Reducer 接收數據的總量,再除以該參數得出 Reducer 的數目,默認值為 500(MB)
    kylin.engine.mr.reduce-input-mb=500
    #MapReduce 任務中 Reducer 數目的最大值,默認值為 500
    kylin.engine.mr.max-reducer-number=500
    #每個 Mapper 可以處理的行數,默認值為 1000000,如果將這個值調小,會起更多的 Mapper
    kylin.engine.mr.mapper-input-rows=1000000
    #啟用分佈式任務鎖
    kylin.job.scheduler.default=2
    kylin.job.lock=org.apache.kylin.storage.hbase.util.ZookeeperJobLock
    <<----
    

    4.6 啟動停止

    所有Kylin節點

    ./bin/kylin.sh start
    #停止  ./bin/kylin.sh stop
    

    4.7 nginx負載均衡

    yum -y install nginx
    
    vim /etc/nginx/nginx.conf
    >>---http中添加替換內容
    upstream kylin {
            least_conn;
            server 192.168.37.10:7070 weight=8;
            server 192.168.37.11:7070 weight=7;
            server 192.168.37.12:7070 weight=7;
    	}
        server {
            listen       9090;
            server_name  localhost;
    
            location / {
                    proxy_pass http://kylin;
            }
        }
    
    <<---
    
    #重啟 nginx 服務
    systemctl restart nginx  
    

    4.8 訪問web頁面

    訪問任何節點的7070端口都可以進入kylin

    訪問nginx所在機器9090端口/kylin負載均衡進入kylin

    賬號密碼:ADMIN / KYLIN

    4 大規模并行處理@列式存儲

    自從 10 年前 Hadoop 誕生以來,大數據的存儲和批處理問題均得到了妥善解決,而如何高速地分析數據也就成為了下一個挑戰。於是各式各樣的“SQL on Hadoop”技術應運而生,其中以 Hive 為代表,Impala、Presto、Phoenix、Drill、 SparkSQL 等緊隨其後(何以解憂–唯有CV SQL BOY)。它們的主要技術是“大規模并行處理”(Massive Parallel Processing,MPP)和“列式存儲”(Columnar Storage)

    大規模并行處理可以調動多台機器一起進行并行計算,用線性增加的資源來換取計算時間的線性下降

    列式存儲則將記錄按列存放,這樣做不僅可以在訪問時只讀取需要的列,還可以利用存儲設備擅長連續讀取的特點,大大提高讀取的速率。

    這兩項關鍵技術使得 Hadoop 上的 SQL 查詢速度從小時提高到了分鐘。 然而分鐘級別的查詢響應仍然離交互式分析的現實需求還很遠。分析師敲入 查詢指令,按下回車,還需要去倒杯咖啡,靜靜地等待查詢結果。得到結果之後才能根據情況調整查詢,再做下一輪分析。如此反覆,一個具體的場景分析常常需要幾小時甚至幾天才能完成,效率低下。 這是因為大規模并行處理和列式存儲雖然提高了計算和存儲的速度,但並沒有改變查詢問題本身的時間複雜度,也沒有改變查詢時間與數據量成線性增長的關係這一事實。

    假設查詢 1 億條記錄耗時 1 分鐘,那麼查詢 10 億條記錄就需 10分鐘,100 億條記錄就至少需要 1 小時 40 分鐘。 當然,可以用很多的優化技術縮短查詢的時間,比如更快的存儲、更高效的壓縮算法,等等,但總體來說,查詢性能與數據量呈線性相關這一點是無法改變的。雖然大規模并行處理允許十倍或百倍地擴張計算集群,以期望保持分鐘級別的查詢速度,但購買和部署十倍或百倍的計算集群又怎能輕易做到,更何況還有 高昂的硬件運維成本。 另外,對於分析師來說,完備的、經過驗證的數據模型比分析性能更加重要, 直接訪問紛繁複雜的原始數據並進行相關分析其實並不是很友好的體驗,特別是在超大規模的數據集上,分析師將更多的精力花在了等待查詢結果上,而不是在更加重要的建立領域模型上

    5 Kylin如何解決海量數據的查詢問題

    **Apache Kylin 的初衷就是要解決千億條、萬億條記錄的秒級查詢問題,其中的關鍵就是要打破查詢時間隨着數據量成線性增長的這個規律。根據OLAP分析,可以注意到兩個結論: **

    • 大數據查詢要的一般是統計結果,是多條記錄經過聚合函數計算后的統計值。原始的記錄則不是必需的,或者訪問頻率和概率都極低。

    • 聚合是按維度進行的,由於業務範圍和分析需求是有限的,有意義的維度聚合組合也是相對有限的,一般不會隨着數據的膨脹而增長。

    **基於以上兩點,我們可以得到一個新的思路——“預計算”。應盡量多地預先計算聚合結果,在查詢時刻應盡量使用預算的結果得出查詢結果,從而避免直 接掃描可能無限增長的原始記錄。 **

    舉例來說,使用如下的 SQL 來查詢 11月 11日 那天銷量最高的商品:

    select item,sum(sell_amount)
    from sell_details
    where sell_date='2020-11-11'
    group by item
    order by sum(sell_amount) desc
    

    用傳統的方法時需要掃描所有的記錄,再找到 11月 11日 的銷售記錄,然後按商品聚合銷售額,最後排序返回。

    假如 11月 11日 有 1 億條交易,那麼查詢必須讀取並累計至少 1 億條記錄,且這個查詢速度會隨將來銷量的增加而逐步下降。如果日交易量提高一倍到 2 億,那麼查詢執行的時間可能也會增加一倍。

    而使用預 計算的方法則會事先按維度 [sell_date , item] 計 算 sum(sell_amount)並存儲下來,在查詢時找到 11月 11日 的銷售商品就可以直接排序返回了。讀取的記錄數最大不會超過維度[sell_date,item]的組合數。

    顯然這個数字將遠遠小於實際的銷售記錄,比如 11月 11日 的 1 億條交易包含了 100萬條商品,那麼預計算后就只有 100 萬條記錄了,是原來的百分之一。並且這些 記錄已經是按商品聚合的結果,因此又省去了運行時的聚合運算。從未來的發展來看,查詢速度只會隨日期和商品數目(時間,商品維度)的增長而變化,與銷售記錄的總數不再有直接聯繫。假如日交易量提高一倍到 2 億,但只要商品的總數不變,那麼預計算的結果記錄總數就不會變,查詢的速度也不會變。

    預計算就是 Kylin 在“大規模并行處理”和“列式存儲”之外,提供給大數據分析的第三個關鍵技術。

    6 Kylin 入門案例

    6.1 hive數據準備

    --創建數據庫kylin_hive
    create database kylin_hive; 
    
    --創建表部門表dept
    create external table if not exists kylin_hive.dept(
    deptno int,
    dname string,
    loc int )
    row format delimited fields terminated by '\t';
    --添加數據
    INSERT INTO TABLE kylin_hive.dept VALUES(10,"ACCOUNTING",1700),(20,"RESEARCH",1800),(30,"SALES",1900),(40,"OPERATIONS",1700)
    --查看數據
    SELECT * FROM kylin_hive.dept
    
    --創建員工表emp
    create external table if not exists kylin_hive.emp(
    empno int,
    ename string,
    job string,
    mgr int,
    hiredate string, 
    sal double, 
    comm double,
    deptno int)
    row format delimited fields terminated by '\t';
    
    --添加數據
    INSERT INTO TABLE kylin_hive.emp VALUES(7369,"SMITHC","LERK",7902,"1980-12-17",800.00,0.00,20),(7499,"ALLENS","ALESMAN",7698,"1981-2-20",1600.00,300.00,30),(7521,"WARDSA","LESMAN",7698,"1981-2-22",1250.00,500.00,30),(7566,"JONESM","ANAGER",7839,"1981-4-2",2975.00,0.00,20),(7654,"MARTIN","SALESMAN",7698,"1981-9-28",1250.00,1400.00,30),(7698,"BLAKEM","ANAGER",7839,"1981-5-1",2850.00,0.00,30),(7782,"CLARKM","ANAGER",7839,"1981-6-9",2450.00,0.00,10),(7788,"SCOTTA","NALYST",7566,"1987-4-19",3000.00,0.00,20),(7839,"KINGPR","ESIDENT",7533,"1981-11-17",5000.00,0.00,10),(7844,"TURNER","SALESMAN",7698,"1981-9-8",1500.00,0.00,30),(7876,"ADAMSC","LERK",7788,"1987-5-23",1100.00,0.00,20),(7900,"JAMESC","LERK",7698,"1981-12-3",950.00,0.00,30),(7902,"FORDAN","ALYST",7566,"1981-12-3",3000.00,0.00,20),(7934,"MILLER","CLERK",7782,"1982-1-23",1300.00,0.00,10)
    --查看數據
    SELECT * FROM kylin_hive.emp
    

    6.2 創建工程

    • 輸入工程名稱以及工程描述

    6.3 Kylin加載Hive表

    雖然 Kylin 使用 SQL 作為查詢接口並利用 Hive 元數據,Kylin 不會讓用戶查詢所有的 hive 表,因為到目前為止它是一個預構建 OLAP(MOLAP) 系統。為了使表在 Kylin 中可用,使用 “Sync” 方法能夠方便地從 Hive 中同步表。

    • 選擇項目添加hive數據源
    • 添加數據源表–>hive庫名稱.表名稱(以逗號分隔)

    • 這裏只添加了表的Schema元信息,如果需要加載數據,還需要點擊Reload Table

    6.4 Kylin添加Models(模型)

    • 填寫模型名字
    • 選擇事實表,這裏選擇員工EMP表為事實表
    • 添加維度表,這裏選擇部門DEPT表為維度表,並選擇我們的join方式,以及join連接字段

    • 選擇聚合維度信息
    • 選擇度量信息
    • 添加分區信息及過濾條件之後“Save”

    6.5 Kylin構建Cube

    Kylin 的 OLAP Cube 是從星型模式的 Hive 表中獲取的預計算數據集,這是供用戶探索、管理所有 cube 的網頁管理頁面。由菜單欄進入Model 頁面,系統中所有可用的 cube 將被列出。

    • 創建一個new cube
    • 選擇我們的model以及指定cube name
    • 添加我們的自定義維度,這裡是在創建Models模型時指定的事實表和維度表中取
      • LookUpTable可選擇normal或derived(一般列、衍生列)
      • normal緯度作為普通獨立的緯度,而derived 維度不會計算入cube,將由事實表的外鍵推算出

    • 添加統計維度,勾選相應列作為度量,kylin提供8種度量:SUM、MAX、MIN、COUNT、COUNT_DISTINCT、TOP_N、EXTENDED_COLUMN、PERCENTILE
      • DISTINCT_COUNT有兩個實現:
        1. 近似實現 HyperLogLog,選擇可接受的錯誤率,低錯誤率需要更多存儲;
        2. 精確實現 bitmap
      • TopN 度量在每個維度結合時預計算,需要兩個參數:
        1. 一是被用來作為 Top 記錄的度量列,Kylin 將計算它的 SUM 值並做倒序排列,如sum(price)
        2. 二是 literal ID,代表最 Top 的記錄,如seller_id
      • EXTENDED_COLUMN
        • Extended_Column 作為度量比作為維度更節省空間。一列和零一列可以生成新的列
      • PERCENTILE
        • Percentile 代表了百分比。值越大,錯誤就越少。100為最合適的值

    • 設置多個分區cube合併信息

    如果是分區統計,需要關於歷史cube的合併,

    這裡是全量統計,不涉及多個分區cube進行合併,所以不用設置歷史多個cube進行合併

    • Auto Merge Thresholds:

      • 自動合併小的 segments 到中等甚至更大的 segment。如果不想自動合併,刪除默認2個選項
    • Volatile Range:

      • 默認為0,會自動合併所有可能的cube segments,或者用 ‘Auto Merge’ 將不會合併最新的 [Volatile Range] 天的 cube segments
    • Retention Threshold:

      • 默認為0,只會保存 cube 過去幾天的 segment,舊的 segment 將會自動從頭部刪除
    • Partition Start Date:

      • cube 的開始日期
    • 高級設置

    暫時也不做任何設

    置高級設定關係到立方體是否足夠優化,可根據實際情況將維度列定義為強制維度、層級維度、聯合維度

    • Mandatory維度指的是總會存在於group by或where中的維度
    • Hierarchy是一組有層級關係的維度,如國家、省份、城市
    • Joint是將多個維度組合成一個維度

    • 額外的其他的配置屬性

    這裏也暫時不做配置

    Kylin 允許在 Cube 級別覆蓋部分 kylin.properties 中的配置

    • 完成保存配置

    通過Planner計劃者,可以看到4個維度,得到Cuboid Conut=15,為2的4次方-1,因為全部沒有的指標不會使用,所以結果等於15。

    • 構建Cube

    6.6 數據查詢

    • 根據部門查詢,部門工資總和
    SELECT  DEPT.DNAME,SUM(EMP.SAL) 
    FROM EMP 
    LEFT JOIN DEPT 
    ON DEPT.DEPTNO = EMP.DEPTNO  
    GROUP BY DEPT.DNAME
    

    7 入門案例構建流程

    • 動畫演示

    8 Kylin的工作原理

    就是對數據模型做 Cube 預計算,並利用計算的結果加速查詢,具體工作過程如下:

    1. 指定數據模型,定義維度和度量。

    2. 預計算 Cube,計算所有 Cuboid 並保存為物化視圖。

    3. 執行查詢時,讀取 Cuboid,運算,產生查詢結果。

    由於 Kylin 的查詢過程不會掃描原始記錄,而是通過預計算預先完成表的關聯、聚合等複雜運算,並利用預計算的結果來執行查詢,因此相比非預計算的查詢技術,其速度一般要快一到兩個數量級,並且這點在超大的數據集上優勢更明顯。當數據集達到千億乃至萬億級別時,Kylin 的速度甚至可以超越其他非預計算技術 1000 倍以上。

    9 Cube 和 Cuboid

    Cube(或 Data Cube),即數據立方體,是一種常用於數據分析與索引的技術;它可以對原始數據建立多維度索引。通過 Cube 對數據進行分析,可以大大加快數據的查詢效率。

    Cuboid 特指在某一種維度組合下所計算的數據。 給定一個數據模型,我們可以對其上的所有維度進行組合。對於 N 個維度來說,組合的所有可能性共有 2 的 N 次方種。對於每一種維度的組合,將度量做 聚合運算,然後將運算的結果保存為一個物化視圖,稱為 Cuboid。

    所有維度組合的 Cuboid 作為一個整體,被稱為 Cube。所以簡單來說,一個 Cube 就是許多按維度聚合的物化視圖的集合。

    下面來列舉一個具體的例子:

    假定有一個電商的銷售數據集,其中維度包括 時間(Time)、商品(Item)、地點(Location)和供應商(Supplier),度量為銷售額(GMV)。

    • 那麼所有維度的組合就有 2 的 4 次方 =16 種
      • 一維度(1D) 的組合有[Time]、[Item]、[Location]、[Supplier]4 種
      • 二維度(2D)的組合 有[Time,Item]、[Time,Location]、[Time、Supplier]、[Item,Location]、 [Item,Supplier]、[Location,Supplier]6 種
      • 三維度(3D)的組合也有 4 種
      • 零維度(0D)的組合有 1 種
      • 四維度(4D)的組合有 1 種

    10 cube構建算法

    10.1 逐層構建算法

    我們知道,一個N維的Cube,是由1個N維子立方體、N個(N-1)維子立方體、N*(N-1)/2個(N-2)維子立方體、……、N個1維子立方體和1個0維子立方體構成,總共有2^N個子立方體組成。

    在逐層算法中,按維度數逐層減少來計算,每個層級的計算(除了第一層,它是從原始數據聚合而來),是基於它上一層級的結果來計算的。比如,[Group by A, B]的結果,可以基於[Group by A, B, C]的結果,通過去掉C后聚合得來的;這樣可以減少重複計算;當 0維度Cuboid計算出來的時候,整個Cube的計算也就完成了。

    每一輪的計算都是一個MapReduce任務,且串行執行;一個N維的Cube,至少需要N次MapReduce Job。

    算法優點:

    1. 此算法充分利用了MapReduce的優點,處理了中間複雜的排序和shuffle工作,故而算法代碼清晰簡單,易於維護;

    2. 受益於Hadoop的日趨成熟,此算法非常穩定,即便是集群資源緊張時,也能保證最終能夠完成。

    算法缺點:

    1. 當Cube有比較多維度的時候,所需要的MapReduce任務也相應增加;由於Hadoop的任務調度需要耗費額外資源,特別是集群較龐大的時候,反覆遞交任務造成的額外開銷會相當可觀;

    2. 由於Mapper邏輯中並未進行聚合操作,所以每輪MR的shuffle工作量都很大,導致效率低下。

    3. 對HDFS的讀寫操作較多:由於每一層計算的輸出會用做下一層計算的輸入,這些Key-Value需要寫到HDFS上;當所有計算都完成后,Kylin還需要額外的一輪任務將這些文件轉成HBase的HFile格式,以導入到HBase中去;

    總體而言,該算法的效率較低,尤其是當Cube維度數較大的時候。

    10.2 快速構建算法

    也被稱作“逐段”(By Segment) 或“逐塊”(By Split) 算法,從1.5.x開始引入該算法,該算法的主要思想是,每個Mapper將其所分配到的數據塊,計算成一個完整的小Cube 段(包含所有Cuboid)。每個Mapper將計算完的Cube段輸出給Reducer做合併,生成大Cube,也就是最終結果。如圖所示解釋了此流程。

    與舊的逐層構建算法相比,快速算法主要有兩點不同:

    1. Mapper會利用內存做預聚合,算出所有組合;Mapper輸出的每個Key都是不同的,這樣會減少輸出到Hadoop MapReduce的數據量,Combiner也不再需要;

    2. 一輪MapReduce便會完成所有層次的計算,減少Hadoop任務的調配。

    11 備份及恢復

    Kylin將它全部的元數據(包括cube描述和實例、項目、倒排索引描述和實例、任務、表和字典)組織成層級文件系統的形式。然而,Kylin使用hbase來存儲元數據,而不是一個普通的文件系統。如果你查看過Kylin的配置文件(kylin.properties),你會發現這樣一行:

    ## The metadata store in hbase
    kylin.metadata.url=kylin_metadata@hbase
    

    這表明元數據會被保存在一個叫作“kylin_metadata”的htable里。你可以在hbase shell里scan該htbale來獲取它。

    11.1 使用二進制包來備份Metadata Store

    有時你需要將Kylin的Metadata Store從hbase備份到磁盤文件系統。在這種情況下,假設你在部署Kylin的hadoop命令行(或沙盒)里,你可以到KYLIN_HOME並運行:

    ./bin/metastore.sh backup
    

    來將你的元數據導出到本地目錄,這個目錄在KYLIN_HOME/metadata_backps下,它的命名規則使用了當前時間作為參數:KYLIN_HOME/meta_backups/meta_year_month_day_hour_minute_second,如:meta_backups/meta_2020_06_18_19_37_49/

    11.2 使用二進制包來恢復Metatdara Store

    萬一你發現你的元數據被搞得一團糟,想要恢復先前的備份:

    1. 首先,重置Metatdara Store(這個會清理Kylin在hbase的Metadata Store的所有信息,請確保先備份):
    ./bin/metastore.sh reset
    
    1. 然後上傳備份的元數據到Kylin的Metadata Store:
    ./bin/metastore.sh restore $KYLIN_HOME/meta_backups/meta_xxxx_xx_xx_xx_xx_xx
    
    1. 等恢復操作完成,可以在“Web UI”的“System”頁面單擊“Reload Metadata”按鈕對元數據緩存進行刷新,即可看到最新的元數據

    做完備份,刪除一些文件,然後進行恢複測試,完美恢復,叮叮叮!

    12 kylin的垃圾清理

    Kylin在構建cube期間會在HDFS上生成中間文件;除此之外,當清理/刪除/合併cube時,一些HBase表可能被遺留在HBase卻以後再也不會被查詢;雖然Kylin已經開始做自動化的垃圾回收,但不一定能覆蓋到所有的情況;你可以定期做離線的存儲清理:

    1. 檢查哪些資源可以清理,這一步不會刪除任何東西:
    ${KYLIN_HOME}/bin/kylin.sh org.apache.kylin.tool.StorageCleanupJob --delete false
    
    1. 你可以抽查一兩個資源來檢查它們是否已經沒有被引用了;然後加上“–delete true”選項進行清理。
    ${KYLIN_HOME}/bin/kylin.sh org.apache.kylin.tool.StorageCleanupJob --delete true
    

    完成后,中間HDFS上的中間文件和HTable會被移除。

    13 Kylin優化

    13.1 維度優化

    如果不進行任何維度優化,直接將所有的維度放在一個聚集組裡,Kylin就會計算所有的維度組合(cuboid)。

    比如,有12個維度,Kylin就會計算2的12次方即4096個cuboid,實際上查詢可能用到的cuboid不到1000個,甚至更少。 如果對維度不進行優化,會造成集群計算和存儲資源的浪費,也會影響cube的build時間和查詢性能,所以我們需要進行cube的維度優化。

    當你在保存cube時遇到下面的異常信息時,意味1個聚集組的維度組合數已經大於 4096 ,你就必須進行維度優化了。

    或者發現cube的膨脹率過大。

    但在現實情況中,用戶的維度數量一般遠遠大於4個。假設用戶有10 個維度,那麼沒有經過任何優化的Cube就會存在 2的10次方 = 1024個Cuboid;雖然每個Cuboid的大小存在很大的差異,但是單單想到Cuboid的數量就足以讓人想象到這樣的Cube對構建引擎、存儲引擎來說壓力有多麼巨大。因此,在構建維度數量較多的Cube時,尤其要注意Cube的剪枝優化(即減少Cuboid的生成)。

    13.2 使用衍生維度

    • 衍生維度:維表中可以由主鍵推導出值的列可以作為衍⽣維度。

    • 使用場景:以星型模型接入時。例如用戶維表可以從userid推導出用戶的姓名,年齡,性別。

    • 優化效果:維度表的N個維度組合成的cuboid個數會從2的N次方降為2。

    衍生維度用於在有效維度內將維度表上的非主鍵維度排除掉,並使用維度表的主鍵(其實是事實表上相應的外鍵)來替代它們。Kylin會在底層記錄維度表主鍵與維度表其他維度之間的映射關係,以便在查詢時能夠動態地將維度表的主鍵“翻譯”成這些非主鍵維度,並進行實時聚合。

    雖然衍生維度具有非常大的吸引力,但這也並不是說所有維度表上的維度都得變成衍生維度,如果從維度表主鍵到某個維度表維度所需要的聚合工作量非常大,則不建議使用衍生維度。

    13.3 使用聚合組(Aggregation group)

    聚合組(Aggregation Group)是一種強大的剪枝工具。聚合組假設一個Cube的所有維度均可以根據業務需求劃分成若干組(當然也可以是一個組),由於同一個組內的維度更可能同時被同一個查詢用到,因此會表現出更加緊密的內在關聯。每個分組的維度集合均是Cube所有維度的一個子集,不同的分組各自擁有一套維度集合,它們可能與其他分組有相同的維度,也可能沒有相同的維度。每個分組各自獨立地根據自身的規則貢獻出一批需要被物化的Cuboid,所有分組貢獻的Cuboid的並集就成為了當前Cube中所有需要物化的Cuboid的集合。不同的分組有可能會貢獻出相同的Cuboid,構建引擎會察覺到這點,並且保證每一個Cuboid無論在多少個分組中出現,它都只會被物化一次。

    對於每個分組內部的維度,用戶可以使用如下三種可選的方式定義,它們之間的關係,具體如下。

    1. 強制維度(Mandatory)

      • 強制維度:所有cuboid必須包含的維度,不會計算不包含強制維度的cuboid。

      • 適用場景:可以將確定在查詢時一定會使用的維度設為強制維度。例如,時間維度。

      • 優化效果:將一個維度設為強制維度,則cuboid個數直接減半。

    如果一個維度被定義為強制維度,那麼這個分組產生的所有Cuboid中每一個Cuboid都會包含該維度。每個分組中都可以有0個、1個或多個強制維度。如果根據這個分組的業務邏輯,則相關的查詢一定會在過濾條件或分組條件中,因此可以在該分組中把該維度設置為強制維度。

    1. 層級維度(Hierarchy),

      • 層級維度:具有一定層次關係的維度。

      • 使用場景:像年,月,日;國家,省份,城市這類具有層次關係的維度。

      • 優化效果:將N個維度設置為層次維度,則這N個維度組合成的cuboid個數會從2的N次方減少到N+1。

    每個層級包含兩個或更多個維度。假設一個層級中包含D1,D2…Dn這n個維度,那麼在該分組產生的任何Cuboid中, 這n個維度只會以(),(D1),(D1,D2)…(D1,D2…Dn)這n+1種形式中的一種出現。每個分組中可以有0個、1個或多個層級,不同的層級之間不應當有共享的維度。如果根據這個分組的業務邏輯,則多個維度直接存在層級關係,因此可以在該分組中把這些維度設置為層級維度。

    1. 聯合維度(Joint),

      • 聯合維度:將幾個維度視為一個維度。

      • 適用場景:

        1. 可以將確定在查詢時一定會同時使用的幾個維度設為一個聯合維度。
        2. 可以將基數很小的幾個維度設為一個聯合維度。
        3. 可以將查詢時很少使用的幾個維度設為一個聯合維度。
      • 優化效果:將N個維度設置為聯合維度,則這N個維度組合成的cuboid個數會從2的N次方減少到1。

    每個聯合中包含兩個或更多個維度,如果某些列形成一個聯合,那麼在該分組產生的任何Cuboid中,這些聯合維度要麼一起出現,要麼都不出現。每個分組中可以有0個或多個聯合,但是不同的聯合之間不應當有共享的維度(否則它們可以合併成一個聯合)。如果根據這個分組的業務邏輯,多個維度在查詢中總是同時出現,則可以在該分組中把這些維度設置為聯合維度。

    這些操作可以在Cube Designer的Advanced Setting中的Aggregation Groups區域完成,如下圖所示。

    聚合組的設計非常靈活,甚至可以用來描述一些極端的設計。假設我們的業務需求非常單一,只需要某些特定的Cuboid,那麼可以創建多個聚合組,每個聚合組代表一個Cuboid。具體的方法是在聚合組中先包含某個Cuboid所需的所有維度,然後把這些維度都設置為強制維度。這樣當前的聚合組就只能產生我們想要的那一個Cuboid了。

    再比如,有的時候我們的Cube中有一些基數非常大的維度,如果不做特殊處理,它就會和其他的維度進行各種組合,從而產生一大堆包含它的Cuboid。包含高基數維度的Cuboid在行數和體積上往往非常龐大,這會導致整個Cube的膨脹率變大。如果根據業務需求知道這個高基數的維度只會與若干個維度(而不是所有維度)同時被查詢到,那麼就可以通過聚合組對這個高基數維度做一定的“隔離”。我們把這個高基數的維度放入一個單獨的聚合組,再把所有可能會與這個高基數維度一起被查詢到的其他維度也放進來。這樣,這個高基數的維度就被“隔離”在一個聚合組中了,所有不會與它一起被查詢到的維度都沒有和它一起出現在任何一個分組中,因此也就不會有多餘的Cuboid產生。這點也大大減少了包含該高基數維度的Cuboid的數量,可以有效地控制Cube的膨脹率。

    13.4 併發粒度優化

    當Segment中某一個Cuboid的大小超出一定的閾值時,系統會將該Cuboid的數據分片到多個分區中,以實現Cuboid數據讀取的并行化,從而優化Cube的查詢速度。具體的實現方式如下:構建引擎根據Segment估計的大小,以及參數“kylin.hbase.region.cut”的設置決定Segment在存儲引擎中總共需要幾個分區來存儲,如果存儲引擎是HBase,那麼分區的數量就對應於HBase中的Region數量。kylin.hbase.region.cut的默認值是5.0,單位是GB,也就是說對於一個大小估計是50GB的Segment,構建引擎會給它分配10個分區。用戶還可以通過設置kylin.hbase.region.count.min(默認為1)和kylin.hbase.region.count.max(默認為500)兩個配置來決定每個Segment最少或最多被劃分成多少個分區。

    由於每個Cube的併發粒度控制不盡相同,因此建議在Cube Designer 的Configuration Overwrites(上圖所示)中為每個Cube量身定製控制併發粒度的參數。假設將把當前Cube的kylin.hbase.region.count.min設置為2,kylin.hbase.region.count.max設置為100。這樣無論Segment的大小如何變化,它的分區數量最小都不會低於2,最大都不會超過100。相應地,這個Segment背後的存儲引擎(HBase)為了存儲這個Segment,也不會使用小於兩個或超過100個的分區。我們還調整了默認的kylin.hbase.region.cut,這樣50GB的Segment基本上會被分配到50個分區,相比默認設置,我們的Cuboid可能最多會獲得5倍的併發量。

    13.5 Row Key優化

    Kylin會把所有的維度按照順序組合成一個完整的Rowkey,並且按照這個Rowkey升序排列Cuboid中所有的行。

    設計良好的Rowkey將更有效地完成數據的查詢過濾和定位,減少IO次數,提高查詢速度,維度在rowkey中的次序,對查詢性能有顯著的影響。

    Row key的設計原則如下:

    1. 被用作where過濾的維度放在前邊。
    1. 基數大的維度放在基數小的維度前邊。

    13.6 增量cube構建

    構建全量cube,也可以實現增量cube的構建,就是通過分區表的分區時間字段來進行增量構建

    1. 更改model

    1. 更改cube

    14 Kafka 流構建 Cube(Kylin實時案例)

    Kylin v1.6 發布了可擴展的 streaming cubing 功能,它利用 Hadoop 消費 Kafka 數據的方式構建 cube。

    參考:http://kylin.apache.org/blog/2016/10/18/new-nrt-streaming/

    前期準備:kylin v1.6.0 或以上版本 和 可運行的 Kafka(v0.10.0 或以上版本)的 Hadoop 環境

    14.1 Kafka創建Topic

    • 創建樣例名為 “kylin_streaming_topic” 具有一個副本三個分區的 topic
    bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic kylin_streaming_topic
    

    • 將樣例數據放入 topic,Kylin 有一個實用類可以做這項工作;
    cd $KYLIN_HOME
    ./bin/kylin.sh org.apache.kylin.source.kafka.util.KafkaSampleProducer --topic kylin_streaming_topic --broker cdh01.cm:9092,cdh02.cm:9092,cdh03.cm:9092
    

    工具每一秒會向 Kafka 發送 100 條記錄。直至本案例結束請讓其一直運行。

    14.2 用streaming定義一張表

    登陸 Kylin Web GUI,選擇一個已存在的 project 或創建一個新的 project;點擊 “Model” -> “Data Source”,點擊 “Add Streaming Table” 圖標

    • 在彈出的對話框中,輸入您從 kafka-console-consumer 中獲得的樣例記錄,點擊 “»” 按鈕,Kylin 會解析 JSON 消息並列出所有的消息
    {"country":"CHINA","amount":41.53789973661185,"qty":6,"currency":"USD","order_time":1592485535129,"category":"TOY","device":"iOS","user":{"gender":"Male","id":"12d127ab-707e-592f-2e4c-69ad654afa48","first_name":"unknown","age":25}}
    
    • 您需要為這個 streaming 數據源起一個邏輯表名;該名字會在後續用於 SQL 查詢;這裡是在 “Table Name” 字段輸入 “STREAMING_SALES_TABLE” 作為樣例。

    • 您需要選擇一個時間戳字段用來標識消息的時間;Kylin 可以從這列值中獲得其他時間值,如 “year_start”,”quarter_start”,這為您構建和查詢 cube 提供了更高的靈活性。這裏可以查看 “order_time”。您可以取消選擇那些 cube 不需要的屬性。這裏我們保留了所有字段。

    • 注意 Kylin 從 1.6 版本開始支持結構化 (或稱為 “嵌入”) 消息,會將其轉換成一個 flat table structure。默認使用 “_” 作為結構化屬性的分隔符。

    • 點擊 “Next”。在這個頁面,提供了 Kafka 集群信息;輸入 “kylin_streaming_topic” 作為 “Topic” 名;集群有 3 個 broker,其主機名為”cdh01.cm,cdh02.cm,cdh03.cm“,端口為 “9092”,點擊 “Save”。
    • 在 “Advanced setting” 部分,”timeout” 和 “buffer size” 是和 Kafka 進行連接的配置,保留它們。

    • 在 “Parser Setting”,Kylin 默認您的消息為 JSON 格式,每一個記錄的時間戳列 (由 “tsColName” 指定) 是 bigint (新紀元時間) 類型值;在這個例子中,您只需設置 “tsColumn” 為 “order_time”;

    • 在現實情況中如果時間戳值為 string 如 “Jul 20,2016 9:59:17 AM”,您需要用 “tsParser” 指定解析類和時間模式例如:
    • 點擊 “Submit” 保存設置。現在 “Streaming” 表就創建好了。

    14.3 定義數據模型

    • 有了上一步創建的表,現在我們可以創建數據模型了。步驟和您創建普通數據模型是一樣的,但有兩個要求:

      • Streaming Cube 不支持與 lookup 表進行 join;當定義數據模型時,只選擇 fact 表,不選 lookup 表;
      • Streaming Cube 必須進行分區;如果您想要在分鐘級別增量的構建 Cube,選擇 “MINUTE_START” 作為 cube 的分區日期列。如果是在小時級別,選擇 “HOUR_START”。
    • 這裏我們選擇 13 個 dimension 和 2 個 measure 列:

    保存數據模型。

    14.4 創建 Cube

    Streaming Cube 和普通的 cube 大致上一樣. 有以下幾點需要您注意:

    • 分區時間列應該是 Cube 的一個 dimension。在 Streaming OLAP 中時間總是一個查詢條件,Kylin 利用它來縮小掃描分區的範圍。
    • 不要使用 “order_time” 作為 dimension 因為它非常的精細;建議使用 “mintue_start”,”hour_start” 或其他,取決於您如何檢查數據。
    • 定義 “year_start”,”quarter_start”,”month_start”,”day_start”,”hour_start”,”minute_start” 作為層級以減少組合計算。
    • 在 “refersh setting” 這一步,創建更多合併的範圍,如 0.5 小時,4 小時,1 天,然後是 7 天;這將會幫助您控制 cube segment 的數量。
    • 在 “rowkeys” 部分,拖拽 “minute_start” 到最上面的位置,對於 streaming 查詢,時間條件會一直显示;將其放到前面將會幫助您縮小掃描範圍。

    保存 cube。

    14.5 運行Cube

    可以在 web GUI 觸發 build,通過點擊 “Actions” -> “Build”,或用 ‘curl’ 命令發送一個請求到 Kylin RESTful API:

    curl -X PUT --user ADMIN:KYLIN -H "Content-Type: application/json;charset=utf-8" -d '{ "sourceOffsetStart": 0, "sourceOffsetEnd": 9223372036854775807, "buildType": "BUILD"}' http://localhost:7070/kylin/api/cubes/{your_cube_name}/build2
    

    請注意 API 終端和普通 cube 不一樣 (這個 URL 以 “build2” 結尾)。

    這裏的 0 表示從最後一個位置開始,9223372036854775807 (Long 類型的最大值) 表示到 Kafka topic 的結束位置。如果這是第一次 build (沒有以前的 segment),Kylin 將會尋找 topics 的開頭作為開始位置。

    在 “Monitor” 頁面,一個新的 job 生成了;等待其直到 100% 完成。

    14.6 查看結果

    點擊 “Insight” 標籤,編寫 SQL 運行,例如:

    select minute_start, count(*), sum(amount), sum(qty) from streaming_sales_table group by minute_start order by minute_start
    

    14.7 自動 build

    一旦第一個 build 和查詢成功了,您可以按照一定的頻率調度增量 build。Kylin 將會記錄每一個 build 的 offsets;當收到一個 build 請求,它將會從上一個結束的位置開始,然後從 Kafka 獲取最新的 offsets。有了 REST API 您可以使用任何像 Linux cron 調度工具觸發它:

    crontab -e
    */5 * * * * curl -X PUT --user ADMIN:KYLIN -H "Content-Type: application/json;charset=utf-8" -d '{ "sourceOffsetStart": 0, "sourceOffsetEnd": 9223372036854775807, "buildType": "BUILD"}' http://localhost:7070/kylin/api/cubes/{your_cube_name}/build2
    

    現在您可以觀看 cube 從 streaming 中自動 built。當 cube segments 累積到更大的時間範圍,Kylin 將會自動的將其合併到一個更大的 segment 中。

    15 JDBC查詢kylin

    • maven依賴
        <dependencies>
            <dependency>
                <groupId>org.apache.kylin</groupId>
                <artifactId>kylin-jdbc</artifactId>
                <version>3.0.1</version>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <!-- 限制jdk版本插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.0</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    • java類
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    
    public class KylinJdbc {
        public static void main(String[] args) throws Exception {
            //Kylin_JDBC 驅動
            String KYLIN_DRIVER = "org.apache.kylin.jdbc.Driver";
            //Kylin_URL
            String KYLIN_URL = "jdbc:kylin://localhost:9090/kylin_hive";
            //Kylin的用戶名
            String KYLIN_USER = "ADMIN";
            //Kylin的密碼
            String KYLIN_PASSWD = "KYLIN";
            //添加驅動信息
            Class.forName(KYLIN_DRIVER);
            //獲取連接
            Connection connection = DriverManager.getConnection(KYLIN_URL, KYLIN_USER, KYLIN_PASSWD);
            //預編譯SQL
            PreparedStatement ps = connection.prepareStatement("SELECT sum(sal) FROM emp group by deptno");
            //執行查詢
            ResultSet resultSet = ps.executeQuery();
            //遍歷打印
            while (resultSet.next()) {
                        System.out.println(resultSet.getInt(1));
            }
        }
    }
    

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

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

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

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

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

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    聚甘新