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,這樣你不用手動指定合約文件了。

總結

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

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

【其他文章推薦】

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

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

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

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

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

聚甘新

Linux nohup命令詳解,終端關閉程序依然可以在執行!

大家好,我是良許。

在工作中,我們很經常跑一個很重要的程序,有時候這個程序需要跑好幾個小時,甚至需要幾天,這個時候如果我們退出終端,或者網絡不好連接中斷,那麼程序就會被中止。而這個情況肯定不是我們想看到的,我們希望即使終端關閉,程序依然可以在跑。

這時我們就可以使用 nohup 這個命令。

nohup 命令是英語詞組 no hangup 的縮寫,意思是不掛斷,也就是指程序不退出。這個命令會使程序忽略 HUP 信號,保證程序能夠正常進行。HUP 信號有些人可能比較陌生,它是在終端被中止的時候向它所關聯的進程所發出的信號,進程收到這個信號后就會中止運行。所以如果你不希望進程被這個信號幹掉的話,就可以忽略這個信號。而 nohup 命令做的就是這個事情。

本文我們將詳細介紹 nohup 命令的具體用法。

nohup命令基本語法

nohup 命令的基本語法如下:

$ nohup command arguments

或者:

$ nohup options

如果你想要得到更多關於 nohup 的用法介紹,可以查看它的幫助頁面:

$ nohup --help

如果你需要查看它的版本號,可以使用 --version 選項。

$ nohup --version

使用nohup命令啟動一個程序

如果你需要運行一個程序,即使對應的 Shell 被退出后依然保持運行,可以這樣使用 nohup 運行這個程序:

$ nohup command

當這個程序進行起來之後,這個程序對應的 log 輸出及其錯誤日誌都將被記錄在 nohup.out 文件里,這個文件一般位於家目錄或者當前目錄。

重定向程序的輸出

如果我不想把程序的輸出保存在家目錄或者當前目錄,我想保存在我指定的路徑,並且自定義文件名,要怎麼操作?這時我們就可以使用重定向操作 >

比如,我現在有個腳本 myScript.sh 我想把它的輸出保存在家目錄下的 output 目錄下,文件名為 myOutput.txt ,可以這樣運行:

$ nohup ./myScript.sh > ~/output/myOutput.txt

使用nohup命令後台啟動一個程序

如果想讓程序在後台運行,可以加上 & 符號。但這樣運行之後,程序就無影無蹤了。想要讓程序重新回到終端,可以使用 fg 命令。

這個命令的輸出 log 將保存在 nohup.out 文件里,你可以使用 cat 或其它命令查看。第二行里 8699 這個数字代表這個命令對應的進程號,也就是 pid 。我們可以使用 ps 命令來找到這個進程。

使用nohup同時運行多個程序

如果你需要同時跑多個程序,沒必要一個個運行,直接使用 && 符號即可。比如,你想同時跑 mkdir ,ping,ls 三個命令,可以這樣運行:

$ nohup bash -c 'mkdir files &&
ping -c 1 baidu.com && ls'> output.txt

終止跑在後台的進程

上面有提到,nohup 命令結合 & 符號可以使進程在後台運行,即使關閉了終端依然不受影響。這時,如果想要終止這個進程,要怎麼操作呢?

最簡單的當屬 kill 命令,相信大家用過很多次了。

$ kill -9 PID

那要如何找到進程對應的 pid 呢?我們可以使用 ps 命令。

$ ps aux | grep myScript.sh

或者你使用 pgrep 命令也行。

接下來,再使用 kill 命令就可以終止該進程了。

$ kill -9 14942

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

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

【其他文章推薦】

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

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

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

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新

SpringBoot + Mybatis + Redis 整合入門項目

這篇文章我決定一改以往的風格,以幽默風趣的故事博文來介紹如何整合 SpringBoot、Mybatis、Redis。

很久很久以前,森林里有一隻可愛的小青蛙,他邁着沉重的步伐走向了找工作的道路,結果發現許多的招聘要求都要會 Redis。

小青蛙就想啥是 Redis 呢,為什麼要用 Redis 呢?難道是因為 Mysql 的幣格不夠高嗎,小青蛙點開了收藏已久的網站:十萬個為什麼

發現原來隨着使用網站的用戶越來越多,表中的數據也越來越多,查詢速度越來越慢。

MySql 的性能遇到了瓶頸,所以許多網站都用 Redis 作緩存。

 

然而能作緩存的不僅只有 Redis,還有 Memcache,那為什麼要用 Redis 呢?

1、性能方面:它們都是將數據存放在內存中,所以性能基本相似。

2、數據類型方面:Redis 支持五種數據數據類型:字符串、散列、列表、集合、有序集合,而 Memcache 僅僅支持簡單的 key-value。

3、數據持久化方面:Redis 可以通過 RDB快照、AOF日誌 等方式進行數據持久化,但是 Memcache 不可以。

4、數據備份方面:Redis  支持 master-slave 主從模式的數據備份。

 

在了解到許多 Redis 的好處后,小青蛙已經迫不及待的想了解它了。

為了更好的使用 Redis,了解 Redis 的五種數據類型適應場景是很有必要的。

1、String 類型:一個 key 對應一個 value,而 value 不僅僅是 String,也可以是数字、甚至是一個序列化對象。

2、Hash 類型:一個 key 對應 多個 field,一個 field 對應 yige value,實際上該類型最適合存儲序列化對象。

           key 相當於數據庫表名字,field  相當於主鍵,value 也就是序列化對象。

3、List 類型:簡單的字符串列表,按照插入順序排序,該結構類似於數據結構中的雙向鏈表,可以從頭部插也可以從尾部插。

4、Set 類型:它是字符串集合,只不過它是無序的且不存在重複的字符串,但是它可以實現 交集、並集、差集。

5、ZSet 類型:它也是字符串集合,它和 Set 的區別是該集合的元素存在 score 屬性,按照 score 屬性的高低排序。可以應用在排行榜上。

值得注意的是,不要習慣性的認為 Redis 字符串只能存字符串,實際上,它可以存儲任何序列化后的對象,當然也可以讀出來。

 

小青蛙知道了 Redis 的五種數據類型應用場景后,迫不及待的想要實踐它了。

為了知道如何讓它作為緩存,以及如何操作數據,小青蛙打開了珍藏已久的視頻網站來學習:青蛙愛學習

在該視頻網站上,小青蛙沉迷其中無法自拔,額,呸呸。緩過神來,發現了一個很好的視頻。小青蛙幽默的說:快進我的收藏夾吃灰去吧。

小青蛙向來都不是一個收藏從未停止,學習從未開始的青蛙。它模仿着視頻建了一個 SpringBoot 項目。

此時,小青蛙想為什麼要勾選 Lombok 呢,因為它可以簡化類,並且提供了 log 等功能。

之後小青蛙為了連上 Mysql 和 Redis 就開始配置 application.properties 文件:

# 數據源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=lemon@mango

# Redis 配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100

配置好該文件后,需要 redisTemplate 模板 Bean,因為自動配置的 redisTemplate Bean 沒有提供序列化操作:(因為是入門版的,所以這樣最好理解)

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
                template.setConnectionFactory(factory);
                GenericJackson2JsonRedisSerializer genericJsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
                template.setKeySerializer(new StringRedisSerializer());
                template.setValueSerializer(genericJsonRedisSerializer);
                template.setHashKeySerializer(genericJsonRedisSerializer);
                template.setHashValueSerializer(genericJsonRedisSerializer);
                template.afterPropertiesSet();
                return template;
    }
}

至此就整合完成了,小青蛙心想這就完事了?!!!,不信?那就來演示一下:(先建一個簡單的類測試一下)

package com.test.serviceImpl;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class JustForTest {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    public void test(String username) {
        
        if(redisTemplate.hasKey(username)) {
            log.info((String)redisTemplate.opsForValue().get(username));
            log.info("get value from redis");
        }else {
            String password = "password";
            log.info(password);
            log.info("get value from mysql");
            log.info("set value to redis");
            redisTemplate.opsForValue().set(username, password);
        }
        
    }

}
package com.test;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import com.test.serviceImpl.JustForTest;

@SpringBootTest
class TestApplicationTests {
    
    @Autowired
    private JustForTest justFortest;

    @Test
    void contextLoads() {
        justFortest.test("username");
    }
}

哦嚯,報錯了,原來是 pom.xml 中少了 commons.pool 依賴,咱給它加上:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

再來一次:

再再來一次:

可以看到確實存入 Redis 了,小青蛙便去 Redis 數據庫中看看有沒有:

事實證明確實整合完畢了,小青蛙仍然表示不解,說好的是SpringBoot、Mybatis、Redis的整合呢,怎麼只看到 Redis 的?

小青蛙剛這麼想,然後視頻里就說了,心急吃不了熱豆腐,需要慢慢來。緊接着,小青蛙就看到了完整的項目結構:

為了讓青蛙們只關注有關整合的部分,視頻里僅僅只給出 serviceImpl 中的代碼:

package com.demo.serviceImpl;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import com.demo.pojo.SimpleUser;
import com.demo.dao.SimpleUserDao;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@SuppressWarnings({"rawtypes","unchecked"})
public class SimpleUserServiceImpl implements UserDetailsService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SimpleUserDao userDao;
    @Override 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        if(redisTemplate.opsForHash().hasKey("user",username)) {
            SimpleUser user = (SimpleUser)redisTemplate.opsForHash().get("user",username);
            return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
        }else {
            SimpleUser user = userDao.findUserByUsername(username);
            if(user != null) {
                redisTemplate.opsForHash().put("user", "username", user);
                return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
            }else {
                throw new UsernameNotFoundException("Username or Password is not correct");
            }
        }
    }
    
    public int addSimpleUser(SimpleUser user) {
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        return userDao.addSimpleUser(user);
    }
}

和上面測試的簡單小例子相似,僅僅是把 String 換成了對象。小青蛙對 Mysql 的表結構和Redis中存入的數據比較感心趣:

由於對 String 類型帶有 ” 符號,所以需要對其進行轉義。

小青蛙是一個有分享精神的蛙,每次它覺得有價值的東西,它都會分享給它的朋友們,項目地址為:GitHub

小青娃逐漸的弄懂了怎麼去進行整合,但是它還是不太明白為什麼這樣就能整合,也就是 know how but don’t konw why!

小青蛙知道:萬事開頭難,但它不知道的是,後面也很難…從此,小青蛙踏上了探尋源碼的道路!呱呱呱……

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

聚甘新

基於NACOS和JAVA反射機制動態更新JAVA靜態常量非@Value註解

1.前言

項目中都會使用常量類文件, 這些值如果需要變動需要重新提交代碼,或者基於@Value註解實現動態刷新, 如果常量太多也是很麻煩; 那麼 能不能有更加簡便的實現方式呢?

本文講述的方式是, 一個JAVA類對應NACOS中的一個配置文件,優先使用nacos中的配置,不配置則使用程序中的默認值;

2.正文

nacos的配置如下圖所示,為了滿足大多數情況,配置了 namespace命名空間和group;

 

 

 新建個測試工程 cloud-sm.

bootstrap.yml 中添加nacos相關配置;

為了支持多配置文件需要注意ext-config節點,group對應nacos的添加的配置文件的group; data-id 對應nacos上配置的data-id

配置如下:

server:
  port: 9010
  servlet:
    context-path: /sm
spring:
  application:
    name: cloud-sm
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.100.101:8848 #Nacos服務註冊中心地址
        namespace: 1
      config:
        server-addr: 192.168.100.101:8848 #Nacos作為配置中心地址
        namespace: 1
        ext-config:
          - group: TEST_GROUP
            data-id: cloud-sm.yaml
            refresh: true
          - group: TEST_GROUP
            data-id: cloud-sm-constant.properties
            refresh: true

接下來是本文重點:

1)新建註解ConfigModule,用於在配置類上;一個value屬性;

2)新建個監聽類,用於獲取最新配置,並更新常量值

實現流程:

1)項目初始化時獲取所有nacos的配置

2)遍歷這些配置文件,從nacos上獲取配置

3)遍歷nacos配置文件,獲取MODULE_NAME的值

4)尋找配置文件對應的常量類,從spring容器中尋找 常量類 有註解ConfigModule 且值是 MODULE_NAME對應的

5)使用JAVA反射更改常量類的值

6)增加監聽,用於動態刷新

 

import org.springframework.stereotype.Component;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Component
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ConfigModule {
    /**
     *  對應配置文件裏面key為( MODULE_NAME ) 的值
     * @return
     */
    String value();
}
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.utils.LogUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * nacos 自定義監聽
 *
 * @author zch
 */
@Component
public class NacosConfigListener {
    private Logger LOGGER = LogUtils.logger(NacosConfigListener.class);
    @Autowired
    private NacosConfigProperties configs;
    @Value("${spring.cloud.nacos.config.server-addr:}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace:}")
    private String namespace;
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 目前只考慮properties 文件
     */
    private String fileType = "properties";
    /**
     * 需要在配置文件中增加一條 MODULE_NAME 的配置,用於找到對應的 常量類
     */
    private String MODULE_NAME = "MODULE_NAME";

    /**
     * NACOS監聽方法
     *
     * @throws NacosException
     */
    public void listener() throws NacosException {
        if (StringUtils.isBlank(serverAddr)) {
            LOGGER.info("未找到 spring.cloud.nacos.config.server-addr");
            return;
        }
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr.split(":")[0]);
        if (StringUtils.isNotBlank(namespace)) {
            properties.put(PropertyKeyConst.NAMESPACE, namespace);
        }

        ConfigService configService = NacosFactory.createConfigService(properties);
        // 處理每個配置文件
        for (NacosConfigProperties.Config config : configs.getExtConfig()) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //目前只考慮properties 文件
            if (!dataId.endsWith(fileType)) continue;

            changeValue(configService.getConfig(dataId, group, 5000));

            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    changeValue(configInfo);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        }
    }

    /**
     * 改變 常量類的 值
     *
     * @param configInfo
     */
    private void changeValue(String configInfo) {
        if(StringUtils.isBlank(configInfo)) return;
        Properties proper = new Properties();
        try {
            proper.load(new StringReader(configInfo)); //把字符串轉為reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        String moduleName = "";
        Enumeration enumeration = proper.propertyNames();
        //尋找MODULE_NAME的值
        while (enumeration.hasMoreElements()) {
            String strKey = (String) enumeration.nextElement();
            if (MODULE_NAME.equals(strKey)) {
                moduleName = proper.getProperty(strKey);
                break;
            }
        }
        if (StringUtils.isBlank(moduleName)) return;
        Class curClazz = null;
        // 尋找配置文件對應的常量類
        // 從spring容器中 尋找類的註解有ConfigModule 且值是 MODULE_NAME對應的
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            Class clazz = applicationContext.getBean(beanName).getClass();
            ConfigModule configModule = (ConfigModule) clazz.getAnnotation(ConfigModule.class);
            if (configModule != null && moduleName.equals(configModule.value())) {
                curClazz = clazz;
                break;
            }
        }
        if (curClazz == null) return;
        // 使用JAVA反射機制 更改常量
        enumeration = proper.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            String value = proper.getProperty(key);
            if (MODULE_NAME.equals(key)) continue;
            try {
                Field field = curClazz.getDeclaredField(key);
                //忽略屬性的訪問權限
                field.setAccessible(true);
                Class<?> curFieldType = field.getType();
                //其他類型自行拓展
                if (curFieldType.equals(String.class)) {
                    field.set(null, value);
                } else if (curFieldType.equals(List.class)) { // 集合List元素
                    field.set(null, JSONUtils.parse(value));
                } else if (curFieldType.equals(Map.class)) { //Map
                    field.set(null, JSONUtils.parse(value));
                }
            } catch (NoSuchFieldException | IllegalAccessException e) {
                LOGGER.info("設置屬性失敗:{} {} = {} ", curClazz.toString(), key, value);
            }
        }
    }

    @PostConstruct
    public void init() throws NacosException {
        listener();
    }
}

 3.測試

1)新建常量類Constant,增加註解@ConfigModule(“sm”),盡量測試全面, 添加常量類型有 String, List,Map

@ConfigModule("sm")
public class Constant {

    public static volatile String TEST = new String("test");

    public static volatile List<String> TEST_LIST = new ArrayList<>();
    static {
        TEST_LIST.add("默認值");
    }
    public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
    static {
        TEST_MAP.put("KEY","初始化默認值");
    }
    public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
    static {
        TEST_LIST_INT.add(1);
    }
}

2)新建個Controller用於測試這些值

@RestController
public class TestController {

    @GetMapping("/t1")
    public Map<String, Object> test1() {
        Map<String, Object> result = new HashMap<>();

        result.put("string" , Constant.TEST);
        result.put("list" , Constant.TEST_LIST);
        result.put("map" , Constant.TEST_MAP);
        result.put("list_int" , Constant.TEST_LIST_INT);
        result.put("code" , 1);
        return result;
    }
}

3)當前nacos的配置文件cloud-sm-constant.properties為空

 4)訪問測試路徑localhost:9010/sm/t1,返回為默認值

{
    "code": 1,
    "string": "test",
    "list_int": [
        1
    ],
    "list": [
        "默認值"
    ],
    "map": {
        "KEY": "初始化默認值"
    }
}

5)然後更改nacos的配置文件cloud-sm-constant.properties;

 6)再次訪問測試路徑localhost:9010/sm/t1,返回為nacos中的值

{
    "code": 1,
    "string": "12351",
    "list_int": [
        1,
        23,
        4
    ],
    "list": [
        "123",
        "sss"
    ],
    "map": {
        "A": 12,
        "B": 432
    }
}

4.結語

這種實現方式優點如下:

1)動態刷新配置,不需要重啟即可改變程序中的靜態常量值

2)使用簡單,只需在常量類上添加一個註解

3)避免在程序中大量使用@Value,@RefreshScope註解

 不足:

此代碼是個人業餘時間的想法,未經過生產驗證,實現的數據類型暫時只寫幾個,其餘的需要自行拓展

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聚甘新

學習ASP.NET Core(11)-解決跨域問題與程序部署

上一篇我們介紹了系統日誌與測試相關的內容並添加了相關的功能;本章我們將介紹跨域與程序部署相關的內容

一、跨域

1、跨域的概念

1、什麼是跨域?

一個請求的URL由協議,域名,端口號組成,以百度的https://www.baidu.com為例,協議為https,域名由子域名www和主域名baidu組成,端口號若為80會自動隱藏(也可以配置為其它端口,通過代理服務器將80端口請求轉發給實際的端口號)。而當請求的URL的協議,域名,端口號任意一個於當前頁面的URL不同即為跨域

2、什麼是同源策略?

瀏覽器存在一個同源策略,即為了防範跨站腳本的攻擊,出現跨域請求時瀏覽器會限制自身不能執行其它網站的腳本(如JavaScript)。所以說當我們把項目部署到Web服務器后,通過瀏覽器進行請求時就會出現同源策略問題;而像PostMan軟件因其是客戶端形式的,所以不存在此類問題

3、跨域會導致什麼問題?

同源策略會限制以下行為:

  • Cookie、LocalStorage和IndexDb的讀取
  • DOM和JS對象的獲取
  • Ajax請求的發送

2、常用的解決方法

這裏我們將簡單介紹針對跨域問題常用的幾種解決辦法,並就其中的Cors方法進行配置,若對其它方式感興趣,可參照老張的哲學的文章,⅖ 種方法實現完美跨域

2.1、JsonP

1、原理

上面有提到瀏覽器基於其同源策略會限制部分行為,但對於Script標籤是沒有限制的,而JsonP就是基於這一點,它會在頁面種動態的插入Script標籤,其Src屬性對應的就是api接口的地址,前端會以Get方式將處理函數以回調的形式傳遞給後端,後端響應後會再以回調的方式傳遞給前端,最終頁面得以显示

2、優缺點

JsonP出現時間較早,所以對舊版本瀏覽器支持性較好;但自身只支持Get請求,無法確認請求是否成功

2.2、 CORS

1、原理

CORS的全稱是Corss Origin Resource Sharing,即跨域資源共享,它允許將當前域下的資源被其它域的腳本請求訪問。其實現原理就是在響應的head中添加Access-Control-Allow-Origin,只要有該字段就支持跨域請求

2、優缺點

Cors支持所有Http方法,不用考慮接口規則,使用簡單;但是對一些舊版本的瀏覽器支持性欠佳

3、使用

其使用非常簡單,以我們的項目為例,在BlogSystem.Core項目的Startup類的ConfigureServices方法中進行如下配置

同時需要開啟使用中間件,如下:

2.3、Nginx

1、原理

跨域問題是指在一個地址中發起另一個地址的請求,而Nginx可以利用其反向代理的功能,接受請求后直接請求該地址,類似打開了一個新的頁面,所以可以避開跨域的問題

2、優缺點

配置簡單,可以降低開發成本,方便配置負載均衡;靈活性差,每個環境都需要進行不同的配置

二、程序部署

1、部署模式

在.NET Core中,有兩種部署模式,分別為FDD(Framework-dependent)框架依賴發布模式和SCD(Self-contained)自包含獨立發布模式

  • FDD:此類部署需要服務器安裝.NET Core SDK環境,部署的包容量會比較小,但可能因SDK版本存在兼容性問題;
  • SCD:此類部署自包含.NET Core SDK的環境,不同.NET Core版本可以共存,其部署包容量會較大,且需要對服務器進行相關配置

2、常用部署方式

以下內容均參考老張的哲學的文章最全的部署方案 & 最豐富的錯誤分析,有興趣的朋友可以參考原文

2.1、Windows平台

  • 直接運行:發布目標選擇windows時會在文件夾中生成一個exe文件,我們可以直接執行exe文件或使用CLI命令調用dll運行;這種方式雖然方便,卻存在一些弊端,比如說部署多個的情況下會存在很多控制台窗口,如誤操作會導致窗口關閉等;
  • 部署服務:除了上述直接運行的方式外,我們還可以將程序發布為服務,發布后我們可以像控制系統服務一樣控製程序的啟動和關閉

需要注意的是上述兩類方法都需要藉助IIS或者是代理服務器進行服務的轉發,否則只能在本地進行訪問;

2.2、Linux平台

Linux平台常用的部署方式即為程序+代理服務器,但是當我們配置完成后運行程序時,該運行命令會一直佔用操作窗口,所以我們需要使用“守護進程”來解決這個問題,簡單來說就是將程序放到後台運行,不影響我們進行其他操作

綜上,部署模式、部署方式及部署平台有多種組合方式,接下來我們挑選下述3種方法進行演示:

方案 依賴運行時/宿主機 依賴代理服務器 其它配置
Windows程序(SCD)+Nginx
Windows服務(FDD)+IIS 設置為服務
Linux程序(FDD)+Nginx 守護進程

3、程序發布

1、這裏我們右擊BlogSystem.Core項目,選擇發布,選擇文件夾后,點擊高級

2、為了演示後面的發布實例,這裏我們分別選擇3種組合模式,①獨立+win-x64;②框架依賴+win-x64;③框架依賴+linux-x64

3、將發布實例拷貝到單獨的文件夾種,這裏我們使用SCD-Window驗證下程序能否直接運行,運行BlogSystem.Core.exe,報錯:

原來還是老問題,BLL沒有添加到發布文件中,我們到項目的bin文件夾下將BLL和DAL的dll文件分別拷貝至3個文件夾,再次運行,出現404錯誤,經過確認發現,首頁對應的是Swagger文檔頁面,而在配置中間件時我們有添加開發環境才配置swagger的邏輯,所以這裏我們可以根據個人需求決定是否添加。

這裏我為了方便確認發布是否成功,所以將其從判斷邏輯中取出了。重新生成發布文件,拷貝BLL和DAL的dll文件,再次運行,還是報錯。原來時Swagger的XML文件缺失,從bin文件夾下拷貝添加至發布文件,運行后成功显示頁面

4、有的朋友會說了,每次都要拷貝這兩個dll和這兩個xml文件,太麻煩了。其實也是有對應的解決辦法的,我們可以使用dotnet的CLI命令進行發布,選擇引用的發布文件夾為bin文件夾,拷貝至發布文件夾即可,有興趣的朋友可以自行研究

三、服務器發布

這裏我用的是阿里雲服務器,Window系統版本是Window Server2012 R2,Linux系統版本是CentOS 8.0;在操作前記得確認拷貝的發布文件能否在本地正常運行

1、Windows程序(SCD)+Nginx

1、解壓后雙擊exe文件網站可以正常運行,如下:

2、這個時候我們發現了一個問題,服務器上沒有數據庫,所以無法確認功能是否正常,這裏我們先下載安裝一個Microsoft SQL Server 2012 Express數據庫(建項目時沒有考慮到發布后測試的問題,實際上像SQLite數據庫是非常符合這類場景的)

安裝完成后我們新建一個BlogSystem的數據庫,通過Sql文件的形式將數據庫結構和數據導入至服務器數據庫,這時候又發現一個問題,由於我們連接數據庫的邏輯放置在model層的BlogSystemContext文件夾下,所以需要將連接中的DataSource更改為Express數據庫,重新發布后覆蓋舊的發布文件(系統設計有缺陷,可以將EF上下文文件放在應用程序層或單獨一層),再次運行,成功執行查詢,如下:

3、這個時候本地已經可以進行正常的訪問了,但是外部網絡是無法訪問調用接口的,這裏我們藉助Nginx進行服務的轉發。下載Nginx后解壓對conf文件夾下的nginx.conf文件進行如下配置:

4、在nginx.exe文件所在目錄的文件路徑輸入cmd,鍵入nginx啟動服務訪問8081端口,成功显示頁面(確保core程序正常運行)如下:

5、這個時候我們使用其它電腦訪問接口,發現還是無法訪問,經過查詢是阿里雲服務器進行了相關的限制,在阿里雲控制台配置安全組規則后即可正常訪問,如下:

6、配置完成后運行,成功訪問該網站且功能正常。這類方法不需要藉助Core的運行時環境,可以說十分便捷

2、Windows服務(FDD)+IIS

1、首先我們將FDD發布文件壓縮后拷貝至Window Server主機,因FDD的部署方法需要藉助.NET Core運行時環境,所以這裏我們首先到官網https://dotnet.microsoft.com/download/dotnet-core/current/runtime下載安裝.NET Core運行時,這裏我們選擇的是右邊這個,安裝完需要重新啟動

2、上一個方法中桌面显示控制台窗口顯然不是一個較佳的方案,所以這裏我們將其註冊為服務。官方提供了ASP.NET Core服務託管的方法,但使用較為複雜,這裏我們藉助一個名為nssm的工具來達到同樣的目的。我們下載nssm后,在其exe路徑運行cmd命令,執行nssm install,在彈出的窗口中進行如下配置:

3、我們在系統服務中開啟BlogSytem.Core_Server,在控制面版中選擇安裝IIS服務,併發布對應的項目,安裝完成后,添加部署為8082端口,將應用程序池修改為無託管,如下:

4、運行網站,成功显示頁面,但是進行功能試用時發現報錯;經過確認是由於IIS應用程序池的用戶驗證模式和sqlserver的驗證模式不同,解決辦法有三種①修改應用程序池高級設置中的進程模型中的標識②將連接數據庫字符串中的Integrated Security=True去除,並添加數據庫連接對應的賬號密碼③在數據庫的“安全性”>“登錄名”裏面,添加對應IIS程序池的名稱,並在這個用戶的“服務器角色”和“用戶映射”中給他對應的權限

後續嘗試方案一失敗,嘗試方案二成功,方案三由於要安裝SSMS所以沒有嘗試,有遇到相同問題的朋友可以自己試下

3、Linux程序(FDD)+Nginx

1、首先我們使用MobaXterm工具登錄至Linux主機(選擇此工具是由於其)同時支持文件傳送和命令行操作),這裏使用的Linux版本是CentOS 8.0;藉助MobaXterm工具在home文件夾下創建WebSite文件夾,並在其內部創建BlogSystem文件夾,將我們準備好的FDD部署方式的發布文件上傳至此文件夾后,使用命令sudo dnf install dotnet-sdk-3.1安裝.net core sdk,如下圖

2、輸入cd /home/WebSite/BlogSystem切換至項目文件夾后,使用dotnet BlogSystem.Core.dll運行程序,成功執行,但是由於我們沒有數據庫,且未配置代理服務器,所以無法驗證服務是否正常運行;所以這裏我們先參照微軟doc快速入門:在 Red Hat 上安裝 SQL Server 並創建數據庫安裝Sql Server數據庫(阿里雲默認安裝了python3作為解釋器所以無需重複安裝),安裝完成后我們開放在阿里雲實例中開放1433端口,使用可視化工具導入表結構和數據

3、完成上述操作后我們需要配置守護進程,將程序放在後台運行。首先我們在/etc/systemd/system下新建守護進程文件,文件名以.service結尾,這裏我們新建名為BlogSystem.service文件,使用MobaXterm自帶的編輯器打開文件後進行如下配置,注意後面的中文備註需要去除否則會報錯

[Unit]
Description=BlogSystem    #服務描述,隨便填就好

[Service]
WorkingDirectory=/home/WebSite/BlogSystem/   #工作目錄,填你應用的絕對路徑
ExecStart=/usr/bin/dotnet /home/WebSite/BlogSystem/BlogSystem.Core.dll    #啟動:前半截是你dotnet的位置(一般都在這個位置),後半部分是你程序入口的dll,中間用空格隔開
Restart=always  
RestartSec=25 #如果服務出現問題會在25秒后重啟,數值可自己設置
SyslogIdentifier=BlogSystem  #設置日誌標識,此行可以沒有
User=root   #配置服務用戶,越高越好
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

我們使用cd /etc/systemd/system/切換至BlogSystem.service對應的目錄,使用systemctl enable BlogSystem.service設置為開機運行后,再使用systemctl start BlogSystem.service啟動服務,另外可以使用systemctl status BlogSystem確認服務狀態

4、接下來我們安裝代理Nginx代理默認的5000端口,使用sudo yum install nginx安裝nginx后,我們到\etc\nginx文件夾下打開nginx.conf文件進行如下配置:

配置完成我們進入\etc\nginx文件夾下,使用systemctl enable nginx將nginx設置為開機啟動,並使用systemctl start nginx啟用服務,同樣可以使用systemctl status nginx確認其狀態。確認無誤后在阿里雲中開放8081端口,外網可正常訪問,但功能試用時報錯,原來是數據庫連接錯誤,重新設置后即可正常訪問

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網絡上的視頻內容和文章,僅為學習和交流,地址如下:

老張的哲學,系列一、ASP.NET Core 學習視頻教程

solenovex,ASP.NET Core 3.x 入門視頻

聲明

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

聚甘新

【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  ?

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

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

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

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

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

聚甘新

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

一、文本挖掘  

     文本挖掘則是對文本進行處理,從中挖掘出來文本中有用的信息和關鍵的規則,在文本挖掘領域應用最往廣泛的是對文本進行分類和聚類,其挖掘的方法分為無監督學習和監督學習。文本挖掘還可以劃分為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

 

 

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

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

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

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

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

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

※超省錢租車方案

聚甘新

從一個計算器開始說起——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  ?

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

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

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

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

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

聚甘新

環境篇: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));
        }
    }
}

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

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

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

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

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

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

※超省錢租車方案

聚甘新

xenomai內核解析之雙核系統調用(一)

版權聲明:本文為本文為博主原創文章,轉載請註明出處。如有錯誤,歡迎指正。博客地址:https://www.cnblogs.com/wsg1100/

目錄

  • xenomai 內核系統調用
    • 一、32位Linux系統調用
    • 二、32位實時系統調用
    • 三、 64位系統調用
    • 五、 實時系統調用表cobalt_syscalls
    • 六、實時系統調用權限控制cobalt_sysmodes
    • 參考

xenomai 內核系統調用

解析系統調用是了解內核架構最有力的一把鑰匙,在這之前先搞懂xenomai與linux兩個內核共存后系統調用是如何實現的。

為什麼需要系統調用

linux內核中設置了一組用於實現系統功能的子程序,稱為系統調用。系統調用和普通庫函數調用非常相似,只是系統調用由操作系統核心提供,運行於內核態,而普通的函數調用由函數庫或用戶自己提供,運行於用戶態

一般的,進程是不能訪問內核的。它不能訪問內核所佔內存空間也不能調用內核函數。CPU硬件決定了這些(這就是為什麼它被稱作“保護模式”

為了和用戶空間上運行的進程進行交互,內核提供了一組接口。透過該接口,應用程序可以訪問硬件設備和其他操作系統資源。這組接口在應用程序和內核之間扮演了使者的角色,應用程序發送各種請求,而內核負責滿足這些請求(或者讓應用程序暫時擱置)。實際上提供這組接口主要是為了保證系統穩定可靠,避免應用程序肆意妄行,惹出大麻煩。

系統調用在用戶空間進程和硬件設備之間添加了一个中間層。該層主要作用有三個:

  • 它為用戶空間提供了一種統一的硬件的抽象接口。比如當需要讀些文件的時候,應用程序就可以不去管磁盤類型和介質,甚至不用去管文件所在的文件系統到底是哪種類型。
  • 系統調用保證了系統的穩定和安全。作為硬件設備和應用程序之間的中間人,內核可以基於權限和其他一些規則對需要進行的訪問進行裁決。舉例來說,這樣可以避免應用程序不正確地使用硬件設備,竊取其他進程的資源,或做出其他什麼危害系統的事情。
  • 每個進程都運行在虛擬系統中,而在用戶空間和系統的其餘部分提供這樣一層公共接口,也是出於這種考慮。如果應用程序可以隨意訪問硬件而內核又對此一無所知的話,幾乎就沒法實現多任務和虛擬內存,當然也不可能實現良好的穩定性和安全性。在Linux中,系統調用是用戶空間訪問內核的惟一手段;除異常和中斷外,它們是內核惟一的合法入口。

Linux加上實時系統內核xenomai后,實時任務常調用xenomai系統調用來完成實時的服務,如果實時任務需要用到linux的服務,還會調用linux的系統調用。

一、32位Linux系統調用

linux應用程序除直接系統調用外還會由glibc觸發系統調用,glibc為了提高應用程序的性能,對一些系統調用進行了封裝。
32位系統系統調用使用軟中斷int 0x80指令實現,軟中斷屬於異常的一種,通過它陷入(trap)內核,trap在整理的文檔x86 Linux中斷系統有說明。tarp_init()中設置IDT(Interrupt Descriptor Table 每个中斷處理程序的地址都保存在一個特殊的位置)由關int 0x80的IDT如下:

static const __initconst struct idt_data def_idts[] = {
	......
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_32),
	......
};

當生系統調用時,硬件根據向量號在 IDT 中找到對應的表項,即中斷描述符,進行特權級檢查,發現 DPL = CPL = 3 ,允許調用。然後硬件將切換到內核棧 (tss.ss0 : tss.esp0)。接着根據中斷描述符的 segment selector 在 GDT / LDT 中找到對應的段描述符,從段描述符拿到段的基址,加載到 cs 。將 offset 加載到 eip。最後硬件將 ss / sp / eflags / cs / ip / error code 依次壓到內核棧。於是開始執行entry_INT80_32函數,該函數在entry_32.S定義:

ENTRY(entry_INT80_32)
	ASM_CLAC
	pushl	%eax		/* pt_regs->orig_ax */
	SAVE_ALL pt_regs_ax=$-ENOSYS	/* *存儲當前用戶態寄存器,保存在pt_regs結構里*/
	/*
	 * User mode is traced as though IRQs are on, and the interrupt gate
	 * turned them off.
	 */
	TRACE_IRQS_OFF

	movl	%esp, %eax
	call	do_int80_syscall_32
.Lsyscall_32_done:
	.......
.Lirq_return:
	INTERRUPT_RETURN/*iret 指令將原來用戶態保存的現場恢復回來,包含代碼段、指令指針寄存器等。這時候用戶態
進程恢復執行。*/

在內核棧的最高地址端,存放的是結構 pt_regs,首先通過 push 和 SAVE_ALL 將當前用戶態的寄存器,保存在棧中 pt_regs 結構裏面.保存完畢后,關閉中斷,將當前棧指針保存到 eax,即do_int80_syscall_32的參數1。
調用do_int80_syscall_32=>do_syscall_32_irqs_on。先看看沒有ipipe時Linux實現如下:

__always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
	struct thread_info *ti = pt_regs_to_thread_info(regs);
	unsigned int nr = (unsigned int)regs->orig_ax;

	.....
	if (likely(nr < IA32_NR_syscalls)) {
		nr = array_index_nospec(nr, IA32_NR_syscalls);
		regs->ax = ia32_sys_call_table[nr](	/*根據系統調用號索引直接執行*/
			(unsigned int)regs->bx, (unsigned int)regs->cx,
			(unsigned int)regs->dx, (unsigned int)regs->si,
			(unsigned int)regs->di, (unsigned int)regs->bp);
	}
	syscall_return_slowpath(regs);
}

在這裏,將系統調用號從pt_reges中eax 裏面取出來,然後根據系統調用號,在系統調用表中找到相應的函數進行調用,並將寄存器中保存的參數取出來,作為函數參數。如果仔細比對,就能發現,這些參數所對應的寄存器,和 Linux 的註釋是一樣的。ia32_sys_call_table系統調用表生成後面解析(此圖來源於網絡)。

相關內核調用執行完后,一直返回到 do_syscall_32_irqs_on ,如果系統調用有返回值,會被保存到 regs->ax 中。接着返回 entry_INT80_32 繼續執行,最後執行 INTERRUPT_RETURN 。 INTERRUPT_RETURN 在 arch/x86/include/asm/irqflags.h 中定義為 iret ,iret 指令將原來用戶態保存的現場恢復回來,包含代碼段、指令指針寄存器等。這時候用戶態進程恢復執行。

系統調用執行完畢。

二、32位實時系統調用

Xenomai使用I-pipe 攔截常規Linux系統調用調度程序,並將系統調用定向到實現它們的系統。

實時系統調用,除了直接系統調用外,xenomai還實現了libcoblat實時庫,相當於glibc,通過libcoblat進行xenomai系統調用,以libcoblat庫函數sem_open為例,libcolat庫中C函數實現如下:

COBALT_IMPL(sem_t *, sem_open, (const char *name, int oflags, ...))
{
	......
	err = XENOMAI_SYSCALL5(sc_cobalt_sem_open,
			       &rsem, name, oflags, mode, value);
	if (err == 0) {
		if (rsem != sem)
			free(sem);
		return &rsem->native_sem;
	}
	.......
	return SEM_FAILED;
}

libcolat庫調用系統調用使用宏XENOMAI_SYSCALL5XENOAI_SYSCALL宏在\include\asm\xenomai\syscall.h中聲明,XENOMAI_SYSCALL5中的’5’代表’該系統調用有五個參數:

#define XENOMAI_DO_SYSCALL(nr, op, args...)			\
({								\
	unsigned __resultvar;					\
	asm volatile (						\
		LOADARGS_##nr					\
		"movl %1, %%eax\n\t"				\
		DOSYSCALL					\
		RESTOREARGS_##nr				\
		: "=a" (__resultvar)				\
		: "i" (__xn_syscode(op)) ASMFMT_##nr(args)	\
		: "memory", "cc");				\
	(int) __resultvar;					\
})

#define XENOMAI_SYSCALL0(op)			XENOMAI_DO_SYSCALL(0,op)
#define XENOMAI_SYSCALL1(op,a1)			XENOMAI_DO_SYSCALL(1,op,a1)
#define XENOMAI_SYSCALL2(op,a1,a2)		XENOMAI_DO_SYSCALL(2,op,a1,a2)
#define XENOMAI_SYSCALL3(op,a1,a2,a3)		XENOMAI_DO_SYSCALL(3,op,a1,a2,a3)
#define XENOMAI_SYSCALL4(op,a1,a2,a3,a4)	XENOMAI_DO_SYSCALL(4,op,a1,a2,a3,a4)
#define XENOMAI_SYSCALL5(op,a1,a2,a3,a4,a5)	XENOMAI_DO_SYSCALL(5,op,a1,a2,a3,a4,a5)

每個宏中,內嵌另一個宏DOSYSCALL,即實現系統調用的int指令:int $0x80

#define DOSYSCALL  "int $0x80\n\t"

系統調用過程硬件處理及中斷入口上節一致,從do_syscall_32_irqs_on開始不同,有ipipe后變成下面這樣子:

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
	struct thread_info *ti = current_thread_info();
	unsigned int nr = (unsigned int)regs->orig_ax;/*取出系統調用號*/
	int ret;
	
	ret = pipeline_syscall(ti, nr, regs);/*pipeline 攔截系統調用*/
	......
done:
	syscall_return_slowpath(regs);
}

套路和ipipe接管中斷類似,在關鍵路徑上攔截系統調用,然後調用ipipe_handle_syscall(ti, nr, regs)讓ipipe來接管處理:

int ipipe_handle_syscall(struct thread_info *ti,
			 unsigned long nr, struct pt_regs *regs)
{
	unsigned long local_flags = READ_ONCE(ti->ipipe_flags);
	int ret; 
	if (nr >= NR_syscalls && (local_flags & _TIP_HEAD)) {/*運行在head域且者系統調用號超過linux*/
		ipipe_fastcall_hook(regs);			/*快速系統調用路徑*/
		local_flags = READ_ONCE(ti->ipipe_flags);
		if (local_flags & _TIP_HEAD) {
			if (local_flags &  _TIP_MAYDAY)
				__ipipe_call_mayday(regs);
			return 1; /* don't pass down, no tail work. */
		} else {
			sync_root_irqs();
			return -1; /* don't pass down, do tail work. */
		}
	}

	if ((local_flags & _TIP_NOTIFY) || nr >= NR_syscalls) {
		ret =__ipipe_notify_syscall(regs);
		local_flags = READ_ONCE(ti->ipipe_flags);
		if (local_flags & _TIP_HEAD)
			return 1; /* don't pass down, no tail work. */
		if (ret)
			return -1; /* don't pass down, do tail work. */
	}

	return 0; /* pass syscall down to the host. */
}

這個函數的處理邏輯是這樣,怎樣區分xenomai系統調用和linux系統調用?每個CPU架構不同linux系統調用總數不同,在x86系統中有300多個,用變量NR_syscalls表示,系統調用號與系統調用一一對應。首先獲取到的系統調用號nr >= NR_syscalls,不用多想,那這個系統調用是xenomai內核的系統調用。
另外還有個問題,如果是Linux非實時任務觸發的xenomai系統調用,或者xenomai 實時任務要調用linux的服務,這些交叉服務涉及實時任務與非實時任務在兩個內核之間運行,優先級怎麼處理等問題。這些涉及cobalt_sysmodes[].

首先看怎麼區分一個任務是realtime還是no_realtime。在task_struct結構的頭有一個成員結構體thread_info,存儲着當前線程的信息,ipipe在結構體thread_info中增加了兩個成員變量ipipe_flagsipipe_data,ipipe_flags用來來標示一個線程是實時還是非實時,_TIP_HEAD置位表示已經是實時上下文。對於需要切換到xenomai上下文的系統調用_TIP_NOTIFY置位。

struct thread_info {
	unsigned long		flags;		/* low level flags */
	u32			status;		/* thread synchronous flags */
#ifdef CONFIG_IPIPE
	unsigned long		ipipe_flags;
	struct ipipe_threadinfo ipipe_data;
#endif
};

ipipe_handle_syscall處理邏輯:
1.對於已經在實時上下文的實時任務發起xenomai的系統調用,使用快速調用路徑函數ipipe_fastcall_hook(regs);
2.需要切換到實時上下文或者非實時調用實時的,使用慢速調用路徑:

__ipipe_notify_syscall(regs)
->ipipe_syscall_hook(caller_domain, regs)

快速調用ipipe_fastcall_hook(regs)內直接handle_head_syscall執行代碼如下:

static int handle_head_syscall(struct ipipe_domain *ipd, struct pt_regs *regs)
{
	....
	code = __xn_syscall(regs);
	nr = code & (__NR_COBALT_SYSCALLS - 1);
	......
	handler = cobalt_syscalls[code];
	sysflags = cobalt_sysmodes[nr];
	........

	ret = handler(__xn_reg_arglist(regs));
	.......

	__xn_status_return(regs, ret);

	.......
}

這個函數很複雜,涉及xenomai與linux之間很多聯繫,代碼是簡化后的,先取出系統調用號,然後從cobalt_syscalls取出系統調用入口handler,然後執行handler(__xn_reg_arglist(regs))執行完成后將執行結果放到寄存器ax,後面的文章會詳細分析ipipe如何處理系統調用。

三、 64位系統調用

我們再來看 64 位的情況,系統調用,不是用中斷了,而是改用 syscall 指令。並且傳遞參數的寄存器也變了。

#define DO_SYSCALL(name, nr, args...)			\
({							\
	unsigned long __resultvar;			\
	LOAD_ARGS_##nr(args)				\
	LOAD_REGS_##nr					\
	asm volatile (					\
		"syscall\n\t"				\
		: "=a" (__resultvar)			\
		: "0" (name) ASM_ARGS_##nr		\
		: "memory", "cc", "r11", "cx");		\
	(int) __resultvar;				\
})

#define XENOMAI_DO_SYSCALL(nr, op, args...) \
	DO_SYSCALL(__xn_syscode(op), nr, args)

#define XENOMAI_SYSBIND(breq) \
	XENOMAI_DO_SYSCALL(1, sc_cobalt_bind, breq)

這裏將系統調用號使用__xn_syscode(op)處理了一下,把最高位置1,表示Cobalt系統調用,然後使用syscall 指令。

#define __COBALT_SYSCALL_BIT	0x10000000
#define __xn_syscode(__nr)	(__COBALT_SYSCALL_BIT | (__nr))

syscall 指令還使用了一種特殊的寄存器,我們叫特殊模塊寄存器(Model Specific Registers,簡稱 MSR)。這種寄存器是 CPU 為了完成某些特殊控制功能為目的的寄存器,其中就有系統調用。在系統初始化的時候,trap_init 除了初始化上面的中斷模式,這裏面還會調用 cpu_init->syscall_init。這裏面有這樣的代碼:

wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

rdmsr 和 wrmsr 是用來讀寫特殊模塊寄存器的。MSR_LSTAR 就是這樣一個特殊的寄存器, 當 syscall 指令調用的時候,會從這個寄存器裏面拿出函數地址來調用,也就是調entry_SYSCALL_64。
該函數在’entry_64.S’定義:

ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
	......
	swapgs
	/*
	 * This path is only taken when PAGE_TABLE_ISOLATION is disabled so it
	 * is not required to switch CR3.
	 */
	movq	%rsp, PER_CPU_VAR(rsp_scratch)
	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp

	/* Construct struct pt_regs on stack */
	pushq	$__USER_DS			/* pt_regs->ss */
	pushq	PER_CPU_VAR(rsp_scratch)	/* pt_regs->sp */
	pushq	%r11				/* pt_regs->flags */
	pushq	$__USER_CS			/* pt_regs->cs */
	pushq	%rcx				/* pt_regs->ip *//*保存用戶太指令指針寄存器*/
GLOBAL(entry_SYSCALL_64_after_hwframe)
	pushq	%rax				/* pt_regs->orig_ax */

	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

	TRACE_IRQS_OFF

	/* IRQs are off. */
	movq	%rsp, %rdi
	call	do_syscall_64		/* returns with IRQs disabled */

	TRACE_IRQS_IRETQ		/* we're about to change IF */

	/*
	 * Try to use SYSRET instead of IRET if we're returning to
	 * a completely clean 64-bit userspace context.  If we're not,
	 * go to the slow exit path.
	 */
	movq	RCX(%rsp), %rcx
	movq	RIP(%rsp), %r11

	cmpq	%rcx, %r11	/* SYSRET requires RCX == RIP */
	jne	swapgs_restore_regs_and_return_to_usermode
	.......
	testq	$(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
	jnz	swapgs_restore_regs_and_return_to_usermode

	/* nothing to check for RSP */

	cmpq	$__USER_DS, SS(%rsp)		/* SS must match SYSRET */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * We win! This label is here just for ease of understanding
	 * perf profiles. Nothing jumps here.
	 */
syscall_return_via_sysret:
	/* rcx and r11 are already restored (see code above) */
	UNWIND_HINT_EMPTY
	POP_REGS pop_rdi=0 skip_r11rcx=1

	/*
	 * Now all regs are restored except RSP and RDI.
	 * Save old stack pointer and switch to trampoline stack.
	 */
	movq	%rsp, %rdi
	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

	pushq	RSP-RDI(%rdi)	/* RSP */
	pushq	(%rdi)		/* RDI */

	/*
	 * We are on the trampoline stack.  All regs except RDI are live.
	 * We can do future final exit work right here.
	 */
	SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

	popq	%rdi
	popq	%rsp
	USERGS_SYSRET64
END(entry_SYSCALL_64)

這裏先保存了很多寄存器到 pt_regs 結構裏面,例如用戶態的代碼段、數據段、保存參數的寄存器.

然後調用 entry_SYSCALL64_slow_pat->do_syscall_64

__visible void do_syscall_64(struct pt_regs *regs)
{
	struct thread_info *ti = current_thread_info();
	unsigned long nr = regs->orig_ax;	/*取出系統調用號*/
	int ret;

	enter_from_user_mode();
	enable_local_irqs();

	ret = ipipe_handle_syscall(ti, nr & __SYSCALL_MASK, regs);
	if (ret > 0) {
		disable_local_irqs();
		return;
	}
	if (ret < 0)
		goto done;
	......
	if (likely((nr & __SYSCALL_MASK) < NR_syscalls)) {
		nr = array_index_nospec(nr & __SYSCALL_MASK, NR_syscalls);
		regs->ax = sys_call_table[nr](
			regs->di, regs->si, regs->dx,
			regs->r10, regs->r8, regs->r9);
	}
done:
	syscall_return_slowpath(regs);
}

與32位一樣,ipipe攔截了系統調用,後面的處理流程類似所以,無論是 32 位,還是 64 位,都會到linux系統調用表 sys_call_table和xenomai系統調用表cobalt_syscalls[] 這裏來。

五、 實時系統調用表cobalt_syscalls

xenomai每個系統的系統系統調用號在\cobalt\uapi\syscall.h中:


#define sc_cobalt_bind				0
#define sc_cobalt_thread_create			1
#define sc_cobalt_thread_getpid			2
	......
#define sc_cobalt_extend			96

bind()函數在內核代碼中對應的聲明和實現為:

/*聲明*/
#define COBALT_SYSCALL_DECL(__name, __args)	\
	long CoBaLt_ ## __name __args
static COBALT_SYSCALL_DECL(bind, lostage,
		      (struct cobalt_bindreq __user *u_breq));
/*實現*/
#define COBALT_SYSCALL(__name, __mode, __args)	\
	long CoBaLt_ ## __name __args
static COBALT_SYSCALL(bind, lostage,
		      (struct cobalt_bindreq __user *u_breq)){......}

其中__name表示系統調用名對應bind、__mode表示該系統調用模式對應lostage。COBALT_SYSCALL展開定義的bind函數后如下:

long CoBaLt_bind(struct cobalt_bindreq __user *u_breq){......}

怎麼將CoBaLt_bind與系統調用號sc_cobalt_bind聯繫起來後放入cobalt_syscalls[]的呢?
在編譯過程中Makefile使用腳本gen-syscall-entries.sh處理各個.c文件中的COBALT_SYSCALL宏,生成一個頭文件syscall_entries.h,裏面是對每個COBALT_SYSCALL宏處理后后的項,以上面COBALT_SYSCALL(bind,...)為例syscall_entries.h中會生成如下兩項,第一項為系統調用入口,第二項為系統調用的模式:

#define __COBALT_CALL_ENTRIES __COBALT_CALL_ENTRY(bind)
#define __COBALT_CALL_MODES	__COBALT_MODE(lostage)

實時系統調用表cobalt_syscalls[]定義在文件kernel\cobalt\posix\syscall.c中:

#define __syshand__(__name)	((cobalt_syshand)(CoBaLt_ ## __name))

#define __COBALT_NI	__syshand__(ni)

#define __COBALT_CALL_NI				\
	[0 ... __NR_COBALT_SYSCALLS-1] = __COBALT_NI,	\
	__COBALT_CALL32_INITHAND(__COBALT_NI)

#define __COBALT_CALL_NFLAGS				\
	[0 ... __NR_COBALT_SYSCALLS-1] = 0,		\
	__COBALT_CALL32_INITMODE(0)

#define __COBALT_CALL_ENTRY(__name)				\
	[sc_cobalt_ ## __name] = __syshand__(__name),		\
	__COBALT_CALL32_ENTRY(__name, __syshand__(__name))

#define __COBALT_MODE(__name, __mode)	\
	[sc_cobalt_ ## __name] = __xn_exec_##__mode,
	
#include "syscall_entries.h"		/*該頭文件由腳本生成*/

static const cobalt_syshand cobalt_syscalls[] = {
	__COBALT_CALL_NI
	__COBALT_CALL_ENTRIES
};

static const int cobalt_sysmodes[] = {
	__COBALT_CALL_NFLAGS
	__COBALT_CALL_MODES
};

__COBALT_CALL_NI宏表示數組空間大小為__NR_COBALT_SYSCALLS(128),每一項由__COBALT_CALL_ENTRIES定義,即腳本頭文件syscall_entries.h中生成的每一項來填充:

#define __COBALT_CALL_ENTRY(__name)				\
	[sc_cobalt_ ## __name] = __syshand__(__name),		\
	__COBALT_CALL32_ENTRY(__name, __syshand__(__name))

__COBALT_CALL32_ENTRY是定義兼容的系統調用,宏展開如下,相當於在數組的多個位置定義包含了同一項CoBaLt_bind

#define __COBALT_CALL32_ENTRY(__name, __handler)	\
	__COBALT_CALL32x_ENTRY(__name, __handler)	\
	__COBALT_CALL32emu_ENTRY(__name, __handler)

#define __COBALT_CALL32emu_ENTRY(__name, __handler)		\
			[sc_cobalt_ ## __name + 256] = __handler,
#define __COBALT_CALL32x_ENTRY(__name, __handler)		\
		[sc_cobalt_ ## __name + 128] = __handler,

最後bind系統調用在cobalt_syscalls[]中如下

static const cobalt_syshand cobalt_syscalls[] = {
	[sc_cobalt_bind] = CoBaLt_bind,
    [sc_cobalt_bind + 128] = CoBaLt_bind,   /*x32 support */
    [sc_cobalt_bind + 256] = CoBaLt_bind,   /*ia32 emulation support*/
	.....
};

相應的數組cobalt_sysmodes[]中的內容如下:

static const int cobalt_sysmodes[] = {
	[sc_cobalt_bind] = __xn_exec_bind,
    [sc_cobalt_bind + 256] = __xn_exec_lostage, /*x32 support */
    [sc_cobalt_bind + 128] = __xn_exec_lostage, /*ia32 emulation support*/
    ......
};

六、實時系統調用權限控制cobalt_sysmodes

上面說到,ipipe管理應用的系統調用時需要分清該系統調用是否合法,是否需要域切換等等。cobalt_sysmodes[]就是每個系統調用對應的模式,控制着每個系統調用的調用路徑。系統調用號為下標,值為具體模式。每個系統調用的sysmode如何生成見上一節,還是以實時應用的bind系統調用為例:

static const int cobalt_sysmodes[] = {
	[sc_cobalt_bind] = __xn_exec_bind,
    [sc_cobalt_bind + 256] = __xn_exec_lostage, /*x32 support */
    [sc_cobalt_bind + 128] = __xn_exec_lostage, /*ia32 emulation support*/
    ......
};

xenomai中所有的系統調用模式定義如下:

/*xenomai\posix\syscall.c*/
#define __xn_exec_lostage    0x1	/*必須在linux域運行該系統調用*/	
#define __xn_exec_histage    0x2	/*必須在Xenomai域運行該系統調用*/	
#define __xn_exec_shadow     0x4		/*影子系統調用:必須映射調用方*/
#define __xn_exec_switchback 0x8 	/*切換回切換; 調用者必須返回其原始模式*/
#define __xn_exec_current    0x10		/*在不管域直接執行。*/
#define __xn_exec_conforming 0x20  	/*在兼容域(Xenomai或Linux)中執行*/
#define __xn_exec_adaptive   0x40	/* 先直接執行如果返回-ENOSYS,則嘗試在相反的域中重新執行系統調用 */
#define __xn_exec_norestart  0x80  /*收到信號后不要重新啟動syscall*/
 /*Shorthand初始化系統調用的簡寫*/
#define __xn_exec_init       __xn_exec_lostage 
/*Xenomai空間中shadow系統調用的簡寫*/
#define __xn_exec_primary   (__xn_exec_shadow|__xn_exec_histage) 
/*Linux空間中shadow系統調用的簡寫*/
#define __xn_exec_secondary (__xn_exec_shadow|__xn_exec_lostage)
/*Linux空間中syscall的簡寫,如果有shadow則切換回linux*/
#define __xn_exec_downup    (__xn_exec_lostage|__xn_exec_switchback)
/* 主域系統不可重啟調用的簡寫 */
#define __xn_exec_nonrestartable (__xn_exec_primary|__xn_exec_norestart)
/*域探測系統調用簡寫*/
#define __xn_exec_probing   (__xn_exec_conforming|__xn_exec_adaptive)
/*將模式選擇移交給syscall。*/
#define __xn_exec_handover  (__xn_exec_current|__xn_exec_adaptive)

使用一個無符號32 位數的每一位來表示一種模式,各模式註釋已經很清楚,不在解釋,後面文章解析ipipe是如何根據mode來處理的。

參考

英特爾® 64 位和 IA-32 架構軟件開發人員手冊第 3 卷 :系統編程指南
極客時間專欄-趣談Linux操作系統
《linux內核源代碼情景分析》

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

【其他文章推薦】

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

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

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

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

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

聚甘新