[C#.NET 拾遺補漏]05:操作符的幾個騷操作

閱讀本文大概需要 1.5 分鐘。

大家好,這是極客精神【C#.NET 拾遺補漏】專輯的第 5 篇文章,今天要講的內容是操作符。

操作符的英文是 Operator,在數值計算中習慣性的被叫作運算符,所以在中文的概念中,運算符是操作符的一個子集。

操作符是很基礎的知識了,基礎歸基礎,我們來回顧一下 C# 操作符那些比較騷的操作,能想到的不多,請大家補充。

操作符的重載

操作符重載大部分語言都沒有,而 C# 有。C# 允許用戶定義類型對操作符進行重載,方式是使用 operate 關鍵字把操作符寫成公開靜態函數。下面來演示一下重載 + 這個操作符。

我們創建一個 Complex 結構類型來代表一個複數,我們知道複數有實數和虛數組成,於是可以這樣定義:

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }
}

現在我們想實現複數的相加操作,即:

Complex a = new Complex() { Real = 1, Imaginary = 2 };
Complex b = new Complex() { Real = 4, Imaginary = 8 };
Complex c = a + b;

默認情況,自定義類是不能進行算術運算的,以上 a + b 會編譯報錯,我們需要對 + 進行操作符重載:

public static Complex operator +(Complex c1, Complex c2)
{
    return new Complex
    {
        Real = c1.Real + c2.Real,
        Imaginary = c1.Imaginary + c2.Imaginary
    };
}

C# 中像加減乘除等這類操作符都可以重載,也有些操作符是不能重載的,具體請查看文末參考鏈接。

隱式和顯式轉換操作符

我們知道子類可以隱式轉換為父類,在某種情況下(如父類由子類賦值而來)父類可以顯式轉換為子類。

在 C# 中,對於沒有子父類關係的用戶定義類型,也是可以實現顯式和隱式轉換的。C# 允許用戶定義類型通過使用 implicitexplicit 關鍵字來控制對象的賦值和對象的類型轉換。它的定義形式如下:

public static <implicit/explicit> operator <結果類型>(<源類型> myType)

這裏以結果類型為方法名,源類型對象作為參數,只能是這一個參數,不能定義第二個參數,但可以通過該參數對象訪問其類的私有成員。下面是一個既有顯式又有隱式轉換操作符的例子:

public class BinaryImage
{
    private readonly bool[] _pixels;

    // 隱式轉換操作符示例
    public static implicit operator ColorImage(BinaryImage bm)
    {
        return new ColorImage(bm);
    }

    // 顯式轉換操作符示例
    public static explicit operator bool[](BinaryImage bm)
    {
        return bm._pixels;
    }
}

public class ColorImage
{
    public ColorImage(BinaryImage bm) { }
}

這樣,我們就可以把 BinaryImage 對象隱式轉換為 ColorImage 對象,把 BinaryImage 對象顯式轉換為 bool 數組對象:

var binaryImage = new BinaryImage();
ColorImage colorImage = binaryImage; // 隱式轉換
bool[] pixels = (bool[])binaryImage; // 顯式轉換

而且轉換操作符可以定義為雙向显示和隱式轉換。既可從你的類型而來,亦可到你的類型而去:

public class BinaryImage
{
    public BinaryImage(ColorImage cm) { }

    public static implicit operator ColorImage(BinaryImage bm)
    {
        return new ColorImage(bm);
    }

    public static explicit operator BinaryImage(ColorImage cm)
    {
        return new BinaryImage(cm);
    }
}

我們知道 as 操作符也是一種顯式轉換操作符,那它適用於上面的這種情況嗎,即:

ColorImage cm = myBinaryImage as ColorImage;

你覺得這樣寫有問題嗎?請在評論區告訴我答案。

空條件和空聯合操作符

空條件(Null Conditional)操作符 ?. 和空聯合(Null Coalescing)操作符 ??,都是 C# 6.0 的語法,大多數人都很熟悉了,使用也很簡單。

?. 操作符會在對象為 null 時立即返回 null,不為 null 時才會調用後面的代碼。其中的符號 ? 代表對象本身,符號 . 代表調用,後面不僅可以是對象的屬性也可以是索引器或方法。以該操作符為分隔的每一截類型相同時可以接龍。示例:

var bar = foo?.Value; // 相當於 foo == null ? null : foo.Value
var bar = foo?.StringValue?.ToString(); // 每一截類型相同支持接龍
var bar = foo?.IntValue?.ToString(); // 每一截類型不同,不能接龍,因為結果類型無法確定

如果是調用索引器,則不需要符號 .,比如:

var foo = new[] { 1, 2, 3 };
var bar = foo?[1]; // 相當於 foo == null ? null : foo[1]

空聯合操作符 ??,當左邊為空時則返回右邊的值,否則返回左邊的值。同樣,每一截的類型相同時支持接龍。

var fizz = foo.GetBar() ?? bar;
var buzz = foo ?? bar ?? fizz;

=> Lambda 操作符

Lambda 操作符,即 =>,它用來定義 Lambda 表達式,也被廣泛用於 LINQ 查詢。它的一般定義形式如下:

(input parameters) => expression

示例:

string[] words = { "cherry", "apple", "blueberry" };
int minLength = words.Min((string w) => w.Length);

實際應用中我們一般省略參數的類型聲明:

int minLength = words.Min(w => w.Length);

Lambda 操作符的後面可以是表達式,可以是語句,也可以是語句塊,比如:

// 表達式
(int x, int y) => x + y

// 語句
(string x) => Console.WriteLine(x)

// 語句塊
(string x) => {
    x += " says Hello!";
    Console.WriteLine(x);
}

這個操作符也可以很方便的用來定義委託方法(其實 Lambda 操作符就是由委託演變而來)。

單獨定義委託方法:

void MyMethod(string s)
{
    Console.WriteLine(s + " World");
}
delegate void TestDelegate(string s);
TestDelegate myDelegate = MyMethod;
myDelegate("Hello");

使用 Lambda 操作符:

delegate void TestDelegate(string s);
TestDelegate myDelegate = s => Console.WriteLine(s + " World");
myDelegate("Hello");

在一個類中,當實現體只有一句代碼時,也可以用 Lambda 操作符對方法和 Setter / Getter 進行簡寫:

public class Test
{
    public int MyProp { get => 123; }
    public void MyMethod() => Console.WriteLine("Hello!");
}

以上是幾種比較有代表性的操作符的“騷”操作,當然還有,但大多都過於基礎,大家都知道,就不總結了。

C# 雖然目前不是最受歡迎的語言,但確實是一門優美的語言,其中少不了這些操作符語法糖帶來的功勞。

參考:https://bit.ly/3h5yKNr

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

【其他文章推薦】

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

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

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

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

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

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

Uber基於Apache Hudi構建PB級數據湖實踐

1. 引言

從確保準確預計到達時間到預測最佳交通路線,在Uber平台上提供安全、無縫的運輸和交付體驗需要可靠、高性能的大規模數據存儲和分析。2016年,Uber開發了增量處理框架Apache Hudi,以低延遲和高效率為關鍵業務數據管道賦能。一年後,我們開源了該解決方案,以使得其他有需要的組織也可以利用Hudi的優勢。接着在2019年,我們履行承諾,進一步將其捐贈給了Apache Software Foundation,差不多一年半之後,Apache Hudi畢業成為Apache Software Foundation頂級項目。為紀念這一里程碑,我們想分享Apache Hudi的構建、發布、優化和畢業之旅,以使更大的大數據社區受益。

2. 什麼是Apache Hudi

Apache Hudi是一個存儲抽象框架,可幫助組織構建和管理PB級數據湖,通過使用upsert增量拉取等原語,Hudi將流式處理帶到了類似批處理的大數據中。這些功能通過統一的服務層(幾分鐘左右即可實現數據延遲),幫助我們更快,更新鮮地獲取服務數據,從而避免了維護多個系統的額外開銷。更靈活地,Apache Hudi還可以在Hadoop分佈式文件系統(HDFS)或雲存儲上運行。

Hudi在數據湖上啟用原子性、一致性、隔離性和持久性(ACID)語義。 Hudi的兩個最廣泛使用的功能是upserts增量拉取,它使用戶能夠捕獲變更數據並將其應用於數據湖,為了實現這一點,Hudi提供了可插拔索引機制,以及自定義索引實現。Hudi具有控制和管理數據湖中文件布局的能力,這不僅能克服HDFS NameNode節點和其他雲存儲限制,而且對於通過提高可靠性和查詢性能來維護健康的數據生態系統也非常重要。另外Hudi支持多種查詢引擎,例如Presto,Apache Hive,Apache Spark和Apache Impala。

圖1. Apache Hudi通過在表上提供不同的視圖來攝取變更日誌、事件和增量流,以服務於不同的應用場景

從總體上講,Hudi在概念上分為3個主要組成部分:需要存儲的原始數據;用於提供upsert功能的索引數據以及用於管理數據集的元數據。內核方面,Hudi維護在不同時間點在表上執行的所有動作的時間軸,在Hudi中稱為即時,這提供了表格的即時視圖,同時還有效地支持了按序到達的數據檢索,Hudi保證時間軸上的操作是原子性的,並且基於即時時間,與數據庫中進行更改的時間是一致的。利用這些信息,Hudi提供了同一Hudi表的不同視圖,包括用於快速列式文件性能的讀優化視圖,用於快速數據攝取的實時視圖以及用於將Hudi表作為變更日誌流讀取的增量視圖,如上圖1所示。

Hudi將數據表組織到分佈式文件系統上基本路徑(basepath)下的目錄結構中。 表分為多個分區,在每個分區內,文件被組織成文件組,由文件ID唯一標識。 每個文件組包含幾個文件切片,其中每個切片包含在某個特定提交/壓縮(commit/compaction)瞬間生成的基本數據文件(*.parquet),以及包含對基本數據文件進行插入/更新的一組日誌文件(*.log)。Hudi採用了Multiversion Concurrency Control(MVCC),其中壓縮操作將日誌和基本文件合併以生成新的文件片,而清理操作則將未使用的/較舊的文件片去除,以回收文件系統上的空間。

Hudi支持兩種表類型:寫時複製和讀時合併。 寫時複製表類型僅使用列文件格式(例如,Apache Parquet)存儲數據。通過寫時複製,可以通過在寫過程中執行同步合併來簡單地更新版本並重寫文件。

讀時合併表類型使用列式(例如Apache Parquet)和基於行(例如Apache Avro)文件格式的組合來存儲數據。 更新記錄到增量文件中,然後以同步或異步壓縮方式生成列文件的新版本。

Hudi還支持兩種查詢類型:快照查詢和增量查詢。 快照查詢是從給定的提交或壓縮操作開始對錶進行”快照”的請求。利用快照查詢時,寫時複製表類型僅暴露最新文件片中的基本/列文件,並且與非Hudi表相比,可保證相同的列查詢性能。寫入時複製提供了現有Parquet表的替代品,同時提供了upsert/delete和其他功能。對於讀時合併表,快照查詢通過動態合併最新文件切片的基本文件和增量文件來提供近乎實時的數據(分鐘級)。對於寫時複製表,自給定提交或壓縮以來,增量查詢將提供寫入表的新數據,並提供更改流以啟用增量數據管道。

3. Apache Hudi在Uber的使用

在Uber,我們在各種場景中都使用到了Hudi,從在Uber平台上提供有關行程的快速、準確的數據,從檢測欺詐到在我們的UberEats平台上提供餐廳和美食推薦。為了演示Hudi的工作原理,讓我們逐步了解如何確保Uber Marketplace中的行程數據在數據湖上是最新的,從而改善Uber平台上的騎手和駕駛員的用戶體驗。行程的典型生命周期始於騎手提出的行程,然後隨着行程的進行而繼續,直到行程結束且騎手到達最終目的地時才結束。 Uber的核心行程數據以表格形式存儲在Uber的可擴展數據存儲Schemaless中。行程表中的單個行程條目在行程的生命周期中可能會經歷許多更新。在Uber使用Hudi之前,大型Apache Spark作業會定期將整個數據集重新寫入HDFS,以獲取上游在線表的插入、更新和刪除,從而反映出行程狀態的變化。就背景而言,在2016年初(在構建Hudi之前),一些最大的任務是使用1000個executors並處理超過20TB的數據,此過程不僅效率低下,而且難以擴展。公司的各個團隊都依靠快速、準確的數據分析來提供高質量的用戶體驗,為滿足這些要求,我們當前的解決方案無法擴展進行數據湖上的增量處理。使用快照和重新加載解決方案將數據移至HDFS時,這些低效率的處理正在寫到到所有數據管道,包括使用此原始數據的下游ETL,我們可以看到這些問題只會隨着規模的擴大而加劇。

在沒有其他可行的開源解決方案可供使用的情況下,我們於2016年末為Uber構建並啟動了Hudi,以構建可促進大規模快速,可靠數據更新的事務性數據湖。Uber的第一代Hudi利用了寫時複製表類型,該表類型每30分鐘將作業處理速度提高到20GB,I/O和寫入放大減少了100倍。到2017年底,Uber的所有原始數據表都採用了Hudi格式,運行着地球上最大的事務數據湖之一。

圖2. Hudi的寫時複製功能使我們能夠執行文件級更新,從而大大提高數據的新鮮度

4. 改進Apache Hudi

隨着Uber數據處理和存儲需求的增長,我們開始遇到Hudi的寫時複製功能的局限性,主要是需要繼續提高數據的處理速度和新鮮度,即使使用Hudi”寫時複製”功能,我們的某些表收到的更新也分散在90%的文件中,從而導致需要重寫數據湖中任何給定的大型表的數據,重寫數據量大約為100TB。由於寫時複製甚至為單個修改的記錄重寫整個文件,因此寫複製功能導致較高的寫放大和損害的新鮮度,從而導致HDFS群集上不必要的I/O以及更快地消耗磁盤空間,此外,更多的數據表更新意味着更多的文件版本,以及HDFS文件數量激增,反過來,這些需求導致HDFS Namenode節點不穩定和較高的計算成本。

為了解決這些日益增長的擔憂,我們實現了第二種表類型,即”讀時合併”。由於讀時合併通過動態合併數據來使用近實時的數據,為避免查詢端的計算成本,我們需要合理使用此模式。”讀時合併”部署模型包括三個獨立的作業,其中包括一個攝取作業,包括由插入、更新和刪除組成的新數據,一個次要的壓縮作業,以異步方式主動地壓縮少量最新分區的更新/刪除內容,以及一個主要的壓縮作業,該作業會緩慢穩定地壓縮大量舊分區中的更新/刪除。這些作業中的每一個作業都以不同的頻率運行,次要作業和提取作業的運行頻率比主要作業要高,以確保其最新分區中的數據以列格式快速可用。通過這樣的部署模型,我們能夠以列式為數千個查詢提供新鮮數據,並將我們的查詢側合併成本限制在最近的分區上。使用讀時合併,我們能夠解決上面提到的所有三個問題,並且Hudi表幾乎不受任何對數據湖的更新或刪除的影響。現在,在Uber,我們會根據不同場景同時使用Apache Hudi的寫時複製和讀時合併功能。

圖3. Uber的Apache Hudi團隊開發了一種數據壓縮策略,用於讀時合併表,以便頻繁將最近的分區轉化為列式存儲,從而減少了查詢端的計算成本

有了Hudi,Uber每天向超過150PB數據湖中插入超過5,000億條記錄,每天使用30,000多個core,超過10,000多個表和數千個數據管道,Hudi每周在我們的各種服務中提供超過100萬個查詢。

5. Apache Hudi經驗總結

Uber在2017年開源了Hudi,為其他人帶來了該解決方案的好處,該解決方案可大規模提取和管理數據存儲,從而將流處理引入大數據。當Hudi畢業於Apache軟件基金會下的頂級項目時,Uber的大數據團隊總結了促使我們構建Hudi的各種考慮因素,包括:

  • 如何提高數據存儲和處理效率?
  • 如何確保數據湖包含高質量的表?
  • 隨着業務的增長,如何繼續大規模有效地提供低延遲的數據?
  • 在分鐘級別的場景中,我們如何統一服務層?

如果沒有良好的標準化和原語,數據湖將很快成為無法使用的”數據沼澤”。這樣的沼澤不僅需要花費大量時間和資源來協調、清理和修復表,而且還迫使各個服務所有者構建複雜的算法來進行調整、改組和交易,從而給技術棧帶來不必要的複雜性。

如上所述,Hudi通過無縫地攝取和管理分佈式文件系統上的大型分析數據集來幫助用戶控制其數據湖,從而彌補了這些差距。建立數據湖是一個多方面的問題,需要在數據標準化、存儲技術、文件管理實踐,數據攝取與數據查詢之間折衷性能等方面進行取捨。在我們建立Hudi時與大數據社區的其他成員交談時,我們了解到這些問題在許多工程組織中普遍存在。我們希望在過去的幾年中,開源和與Apache社區的合作,在Hudi基礎上發展可以使其他人在不同行業對大數據運營有更深入的了解。 在Uber之外,Apache Hudi已在多家公司用於生產,其中包括阿里雲,騰訊雲,AWS、Udemy等。

6. 未來計劃

圖4. Apache Hudi場景包括數據分析和基礎架構運行狀況監視

Hudi通過對數據集強制schema,幫助用戶構建更強大、更新鮮的數據湖,從而提供高質量的見解。

在Uber,擁有全球最大的事務數據湖之一為我們提供了各種Apache Hudi用例場景的機會,由於以這種規模解決問題並提高效率可能會產生重大影響,因此有直接的動機促使我們更加深入。在Uber,我們已經使用了先進的Hudi原語,如增量拉取來幫助建立鏈式增量流水線,從而減少了作業的計算空間,而這些作業本來會執行大型掃描和寫入。我們根據特定的用例場景和要求調整讀時合併表的壓縮策略。 自從我們將Hudi捐贈給Apache基金會以來,最近幾個月,Uber貢獻了一些功能,例如嵌入式時間軸服務以實現高效的文件系統訪問,刪除重命名以支持雲友好的部署並提高增量拉取性能。

在接下來的幾個月中,Uber計劃為Apache Hudi社區貢獻更多新功能。其中一些功能可通過優化計算使用量以及改善數據應用程序的性能來幫助降低成本,我們還將更深入地研究如何根據訪問模式和數據應用程序需求來改善存儲管理和查詢性能。

有關我們如何計劃實現這些目標的更多信息,您可以閱讀一些RFC,包括支持列索引和O(1)查詢計劃的智能元數據,將Parquet表高效引導到Hudi,記錄級別索引支持更快速插入,這些RFC由Uber的Hudi團隊向Apache社區提出。

隨着Apache Hudi畢業成為Apache頂級項目,我們很高興為該項目雄心勃勃的路線圖做出貢獻。Hudi使Uber和其他公司可以使用開放源文件格式,在未來證明其數據湖的速度,可靠性和交易能力,從而消除了許多大數據挑戰,並構建了豐富而可移植的數據應用程序。

Apache Hudi是一個成長中的社區,具有令人興奮且不斷髮展的發展路線圖。 如果您有興趣為這個項目做貢獻,可點擊這裏。

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

【其他文章推薦】

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

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

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

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

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

性能調優必備利器之 JMH

if 快還是 switch 快?HashMap 的初始化 size 要不要指定,指定之後性能可以提高多少?各種序列化方法哪個耗時更短?

無論出自何種原因需要進行性能評估,量化指標總是必要的。

在大部分場合,簡單地回答誰快誰慢是遠遠不夠的,如何將程序性能量化呢?

這就需要我們的主角 JMH 登場了!

JMH 簡介

JMH(Java Microbenchmark Harness)是用於代碼微基準測試的工具套件,主要是基於方法層面的基準測試,精度可以達到納秒級。該工具是由 Oracle 內部實現 JIT 的大牛們編寫的,他們應該比任何人都了解 JIT 以及 JVM 對於基準測試的影響。

當你定位到熱點方法,希望進一步優化方法性能的時候,就可以使用 JMH 對優化的結果進行量化的分析。

JMH 比較典型的應用場景如下:

  1. 想準確地知道某個方法需要執行多長時間,以及執行時間和輸入之間的相關性
  2. 對比接口不同實現在給定條件下的吞吐量
  3. 查看多少百分比的請求在多長時間內完成

下面我們以字符串拼接的兩種方法為例子使用 JMH 做基準測試。

加入依賴

因為 JMH 是 JDK9 自帶的,如果是 JDK9 之前的版本需要加入如下依賴(目前 JMH 的最新版本為 1.23):

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
</dependency>

編寫基準測試

接下來,創建一個 JMH 測試類,用來判斷 +StringBuilder.append() 兩種字符串拼接哪個耗時更短,具體代碼如下所示:

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {

    @Param(value = {"10", "50", "100"})
    private int length;

    @Benchmark
    public void testStringAdd(Blackhole blackhole) {
        String a = "";
        for (int i = 0; i < length; i++) {
            a += i;
        }
        blackhole.consume(a);
    }

    @Benchmark
    public void testStringBuilderAdd(Blackhole blackhole) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(i);
        }
        blackhole.consume(sb.toString());
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StringConnectTest.class.getSimpleName())
                .result("result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

其中需要測試的方法用 @Benchmark 註解標識,這些註解的具體含義將在下面介紹。

在 main() 函數中,首先對測試用例進行配置,使用 Builder 模式配置測試,將配置參數存入 Options 對象,並使用 Options 對象構造 Runner 啟動測試。

另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

執行基準測試

準備工作做好了,接下來,運行代碼,等待片刻,測試結果就出來了,下面對結果做下簡單說明:

# JMH version: 1.23
# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
# Parameters: (length = 100)

該部分為測試的基本信息,比如使用的 Java 路徑,預熱代碼的迭代次數,測量代碼的迭代次數,使用的線程數量,測試的統計單位等。

# Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
# Warmup Iteration   2: 864.685 ±(99.9%) 174.120 ns/op
# Warmup Iteration   3: 798.310 ±(99.9%) 121.161 ns/op

該部分為每一次熱身中的性能指標,預熱測試不會作為最終的統計結果。預熱的目的是讓 JVM 對被測代碼進行足夠多的優化,比如,在預熱后,被測代碼應該得到了充分的 JIT 編譯和優化。

Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
Iteration   2: 807.861 ±(99.9%) 13.163 ns/op
Iteration   3: 851.421 ±(99.9%) 33.564 ns/op
Iteration   4: 805.675 ±(99.9%) 33.038 ns/op
Iteration   5: 821.020 ±(99.9%) 66.943 ns/op

Result "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":
  819.329 ±(99.9%) 72.698 ns/op [Average]
  (min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
  CI (99.9%): [746.631, 892.027] (assumes normal distribution)

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

該部分显示測量迭代的情況,每一次迭代都显示了當前的執行速率,即一個操作所花費的時間。在進行 5 次迭代后,進行統計,在本例中,length 為 100 的情況下 testStringBuilderAdd 方法的平均執行花費時間為 819.329 ns,誤差為 72.698 ns

最後的測試結果如下所示:

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringAdd               10  avgt    5   161.496 ±  17.097  ns/op
StringConnectTest.testStringAdd               50  avgt    5  1854.657 ± 227.902  ns/op
StringConnectTest.testStringAdd              100  avgt    5  6490.062 ± 327.626  ns/op
StringConnectTest.testStringBuilderAdd        10  avgt    5    68.769 ±   4.460  ns/op
StringConnectTest.testStringBuilderAdd        50  avgt    5   413.021 ±  30.950  ns/op
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

結果表明,在拼接字符次數越多的情況下,StringBuilder.append() 的性能就更好。

生成 jar 包執行

對於一些小測試,直接用上面的方式寫一個 main 函數手動執行就好了。

對於大型的測試,需要測試的時間比較久、線程數比較多,加上測試的服務器需要,一般要放在 Linux 服務器里去執行。

JMH 官方提供了生成 jar 包的方式來執行,我們需要在 maven 里增加一個 plugin,具體配置如下:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.4.1</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <finalName>jmh-demo</finalName>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.openjdk.jmh.Main</mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

接着執行 maven 的命令生成可執行 jar 包並執行:

mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 基礎

為了能夠更好地使用 JMH 的各項功能,下面對 JMH 的基本概念進行講解:

@BenchmarkMode

用來配置 Mode 選項,可用於類或者方法上,這個註解的 value 是一個數組,可以把幾種 Mode 集合在一起執行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),還可以設置為 Mode.All,即全部執行一遍。

  1. Throughput:整體吞吐量,每秒執行了多少次調用,單位為 ops/time
  2. AverageTime:用的平均時間,每次操作的平均時間,單位為 time/op
  3. SampleTime:隨機取樣,最後輸出取樣結果的分佈
  4. SingleShotTime:只運行一次,往往同時把 Warmup 次數設為 0,用於測試冷啟動時的性能
  5. All:上面的所有模式都執行一次

@State

通過 State 可以指定一個對象的作用範圍,JMH 根據 scope 來進行實例化和共享操作。@State 可以被繼承使用,如果父類定義了該註解,子類則無需定義。由於 JMH 允許多線程同時執行測試,不同的選項含義如下:

  1. Scope.Benchmark:所有測試線程共享一個實例,測試有狀態實例在多線程共享下的性能
  2. Scope.Group:同一個線程在同一個 group 里共享實例
  3. Scope.Thread:默認的 State,每個測試線程分配一個實例

@OutputTimeUnit

為統計結果的時間單位,可用於類或者方法註解

@Warmup

預熱所需要配置的一些基本測試參數,可用於類或者方法上。一般前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。參數如下所示:

  1. iterations:預熱的次數
  2. time:每次預熱的時間
  3. timeUnit:時間的單位,默認秒
  4. batchSize:批處理大小,每次操作調用幾次方法

為什麼需要預熱?

因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯為機器碼,從而提高執行速度,所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。

@Measurement

實際調用方法所需要配置的一些基本測試參數,可用於類或者方法上,參數和 @Warmup 相同。

@Threads

每個進程中的測試線程,可用於類或者方法上。

@Fork

進行 fork 的次數,可用於類或者方法上。如果 fork 數是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。

@Param

指定某項參數的多種情況,特別適合用來測試一個函數在不同的參數輸入的情況下的性能,只能作用在字段上,使用該註解必須定義 @State 註解。

在介紹完常用的註解后,讓我們來看下 JMH 有哪些陷阱。

JMH 陷阱

在使用 JMH 的過程中,一定要避免一些陷阱。

比如 JIT 優化中的死碼消除,比如以下代碼:

@Benchmark
public void testStringAdd(Blackhole blackhole) {
    String a = "";
    for (int i = 0; i < length; i++) {
        a += i;
    }
}

JVM 可能會認為變量 a 從來沒有使用過,從而進行優化把整個方法內部代碼移除掉,這就會影響測試結果。

JMH 提供了兩種方式避免這種問題,一種是將這個變量作為方法返回值 return a,一種是通過 Blackhole 的 consume 來避免 JIT 的優化消除。

其他陷阱還有常量摺疊與常量傳播、永遠不要在測試中寫循環、使用 Fork 隔離多個測試方法、方法內聯、偽共享與緩存行、分支預測、多線程測試等,感興趣的可以閱讀 https://github.com/lexburner/JMH-samples 了解全部的陷阱。

JMH 插件

大家還可以通過 IDEA 安裝 JMH 插件使 JMH 更容易實現基準測試,在 IDEA 中點擊 File->Settings...->Plugins,然後搜索 jmh,選擇安裝 JMH plugin:

這個插件可以讓我們能夠以 JUnit 相同的方式使用 JMH,主要功能如下:

  1. 自動生成帶有 @Benchmark 的方法
  2. 像 JUnit 一樣,運行單獨的 Benchmark 方法
  3. 運行類中所有的 Benchmark 方法

比如可以通過右鍵點擊 Generate...,選擇操作 Generate JMH benchmark 就可以生成一個帶有 @Benchmark 的方法。

還有將光標移動到方法聲明並調用 Run 操作就運行一個單獨的 Benchmark 方法。

將光標移到類名所在行,右鍵點擊 Run 運行,該類下的所有被 @Benchmark 註解的方法都會被執行。

JMH 可視化

除此以外,如果你想將測試結果以圖表的形式可視化,可以試下這些網站:

  • JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
  • JMH Visualizer:https://jmh.morethan.io/

比如將上面測試例子結果的 json 文件導入,就可以實現可視化:

總結

本文主要介紹了性能基準測試工具 JMH,它可以通過一些功能來規避由 JVM 中的 JIT 或者其他優化對性能測試造成的影響。

只需要將待測的業務邏輯用 @Benchmark 註解標識,就可以讓 JMH 的註解處理器自動生成真正的性能測試代碼,以及相應的性能測試配置文件。

最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

參考

http://openjdk.java.net/projects/code-tools/jmh/

深入拆解Java虛擬機

《實戰Java高併發程序設計》

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

【其他文章推薦】

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

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

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

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

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

阻塞隊列一——java中的阻塞隊列

目錄

  • 阻塞隊列簡介:介紹阻塞隊列的特性與應用場景
  • java中的阻塞隊列:介紹java中實現的供開發者使用的阻塞隊列
  • BlockQueue中方法:介紹阻塞隊列的API接口
  • 阻塞隊列的實現原理:具體的例子說明阻塞隊列的實現原理
  • 總結

阻塞隊列簡介

阻塞隊列(BlockingQueue)首先是一個支持先進先出的隊列,與普通的隊列完全相同;
其次是一個支持阻塞操作的隊列,即:

  • 當隊列滿時,會阻塞執行插入操作的線程,直到隊列不滿。
  • 當隊列為空時,會阻塞執行獲取操作的線程,直到隊列不為空。

阻塞隊列用在多線程的場景下,因此阻塞隊列使用了鎖機制來保證同步,這裏使用的可重入鎖;
而對於阻塞與喚醒機制則有與鎖綁定的Condition實現

應用場景:生產者消費者模式

java中的阻塞隊列

java中的阻塞隊列根據容量可以分為有界隊列和無界隊列:

  • 有界隊列:隊列中只能存儲有限個元素,超出后存放元素線程會被阻塞或者失敗。
  • 無界隊列:隊列中可以存儲無限個元素。

java8中提供了7種阻塞隊列阻塞隊列供開發者使用,如下錶:

類名 描述
ArrayBlockingQueue 一個由數組結構組成的有界阻塞隊列
LinkedBlockingQueue 由鏈表結構組成的有界阻塞隊列(默認大小Integer.MAX_VALUE)
PriorityBlockingQueue 支持優先級排序的無界阻塞隊列
DelayQueue 使用優先級隊列實現的延遲無界阻塞隊列
SynchronousQueue 不存儲元素的阻塞隊列,即單個元素的隊列
LinkedTransferQueue 由鏈表結構組成的無界阻塞隊列
LinkedBlockingDeque 由鏈表結構組成的雙向阻塞隊列

另外還有一個在ScheduledThreadPoolExecutor中實現的DelayedWorkQueue阻塞隊列,
但這個阻塞隊列開發者不能使用。它們之間的UML類圖如下圖:

BlockingQueue接口是阻塞隊列對外的訪問接口,所有的阻塞隊列都實現了BlockQueue中的方法

BlockQueue中方法

作為一個隊列的核心方法就是入隊和出隊。由於存在阻塞策略,BlockQueue將出隊入隊的情況分為了四組,每組提供不同的方法:

  • 拋出異常:當隊列滿時,如果再往隊列中插入元素,則拋出IllegalStateException異常;
    當隊列為空時,從隊列中獲取元素則拋出NoSuchElementException異常。

  • 返回特定值(布爾值):當隊列滿時,如果再往隊列中插入元素,則返回false;當隊列為空時,從隊列中獲取元素則返回null。

  • 一直阻塞:當隊列滿時,如果再往隊列中插入元素,阻塞當前線程直到隊列中至少一個被移除或者響應中斷退出;
    當隊列為空時,則阻塞當前線程直到至少一個元素元素入隊或者響應中斷退出。

  • 超時退出:當隊列滿時,如果再往隊列中插入元素,阻塞當前線程直到隊列中至少一個被移除或者達到指定的等待時間退出或者響應中斷退出;
    當隊列為空時,則阻塞當前線程直到至少一個元素元素入隊或者達到指定的等待時間退出或者響應中斷退出。

對於每種情況BlockingQueue提供的方法如下錶:

方法\處理方式 拋出異常 返回特定值(布爾值) 一直阻塞 超時退出
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time.unit)
檢查 element() peek() 不可用 不可用

上述方法一般用於生產者-消費者模型中,是其中的生產和消費操作隊列的核心方法。
除了這些方法,BlockingQueue還提供了一些其他的方法如下錶:

方法名稱 描述
remove(Object o) 從隊列中移除一個指定值
size() 獲取隊列中元素的個數
contains(Object o) 判斷隊列是否包含指定的元素,但是這個元素在這次判斷完可能就會被消費
drainTo(Collection<? super E> c) 將隊列中元素放在給定的集合中,並返回添加的元素個數
drainTo(Collection<? super E> c, int maxElements) 將隊列中元素取maxElements(不超過隊列中元素個數)個放在給定的集合中,並返回添加的元素個數
remainingCapacity() 計算隊列中還可以存放的元素個數
toArray() 以objetc數組的形式獲取隊列中所有的元素
toArray(T[] a) 以給定類型數組的方式獲取隊列中所有的元素
clear() 清空隊列,危險的操作

阻塞隊列的實現原理

阻塞隊列的實現依靠通知模式實現:當生產者向滿了的隊列中添加元素時,會阻塞住生產者,
直到消費者消費了一個隊列中的元素後會通知消費者隊列可用,此時再由生產者向隊列中添加元素。反之亦然。

阻塞隊列的阻塞喚醒依靠Condition——條件隊列來實現。

ArrayBlockingQueue為例說明:

ArrayBlockingQueue的定義:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
   
    /** The queued items */
    //以數組的結構存儲隊列的元素,採用的是循環數組
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    //隊列的隊頭索引
    int takeIndex;

    /** items index for next put, offer, or add */
    //隊列的隊尾索引
    int putIndex;

    /** Number of elements in the queue */
    //隊列中元素的個數
    int count;

    /** Main lock guarding all access */
    //對於ArrayBlockingQueue所有的操作都需要加鎖,
    final ReentrantLock lock;

    /** Condition for waiting takes */
    //條件隊列,當隊列為空時阻塞消費者並在生產者生產後喚醒消費者
    private final Condition notEmpty;

    /** Condition for waiting puts */
    //條件隊列,當隊列滿時阻塞生產者,並在消費者消費隊列后喚醒生產者
    private final Condition notFull;
}

根據類的定義字段可以看到,有兩個Condition條件隊列,猜測以下過程

  • 當隊列為空,消費者試圖消費時應該調用notEmpty.await()方法阻塞,並在生產者生產後調用notEmpty.single()方法
  • 當隊列已滿,生產者試圖放入元素應調用notFull.await()方法阻塞,並在消費者消費隊列后調用notFull.single()方法

向隊列中添加元素put()方法的添加過程。

    /**
    * 向隊列中添加元素
    * 當隊列已滿時需要阻塞當前線程
    * 放入元素后喚醒因隊列為空阻塞的消費者
    */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //當隊列已滿時需要notFull.await()阻塞當前線程
            //offer(e,time,unit)方法就是阻塞的時候加了超時設定
            while (count == items.length)
                notFull.await();
            //放入元素的過程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    /**enqueue實際添加元素的方法*/
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //如果條件隊列中存在等待的線程
        //喚醒
        notEmpty.signal();
    }

從隊列中獲取元素take()方法的獲取過程。

    /**
    * 從隊列中獲取元素
    * 當隊列已空時阻塞當前線程
    * 從隊列中消費元素后喚醒等待的生產線程
    */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //隊列為空需要阻塞當前線程
            while (count == 0)
                notEmpty.await();
            //獲取元素的過程
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    /**dequeue實際消費元素的方法*/
    private E dequeue() {
       // assert lock.getHoldCount() == 1;
       // assert items[takeIndex] != null;
       final Object[] items = this.items;
       @SuppressWarnings("unchecked")
       E x = (E) items[takeIndex];
       items[takeIndex] = null;
       if (++takeIndex == items.length)
           takeIndex = 0;
       count--;
       if (itrs != null)
           itrs.elementDequeued();
       //消費元素后從喚醒阻塞的生產者線程
       notFull.signal();
       return x;
    }

總結

阻塞隊列提供了不同於普通隊列的增加、刪除元素的方法,核心在與隊列滿時阻塞生產者和隊列空時阻塞消費者。
這一阻塞過程依靠與鎖綁定的Condition對象實現。Condition接口的實現在AQS中實現,具體的實現類是
ConditionObject

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

程序員過關斬將–作為一個架構師,我是不是應該有很多職責?

每一個程序員都有一個架構夢。

上面其實本質上是一句富有事實哲理的廢話,要不然也不會有這麼多人關注你的公眾號。這些年隨着“企業数字化”轉型的口號,一大批企業奔跑在轉型的路上,希望領先一步對手將企業IT部門從單純的成本中心轉變為業務驅動者,而這個過程中,企業的架構師起着舉足輕重的作用。架構師的工作在很多擼碼的開發者眼中是很一項很神聖的工作,而且富有挑戰性。

但是事物都有兩面性,很多管理者和技術人員都認為架構師的薪酬不符合實際,有很多架構師確實只會用PPT和大幅海報來應付了事,而且會依仗着在公司地位把自己的一些想法強加給公司其他同事,有的架構師甚至會追求一些無關緊要的概念,在高層和底層灌輸一些錯誤的思想,從而導致做出一些不可逆轉的糟糕決策,使公司陷入危險逆境。

很多時候,公司給予架構師這個角色太多的責任,管理者希望他們能在突發性能問題時能快速解決問題,還能推動企業快速轉型,甚至能幫助企業文化的快速建立,作為一個架構師是不是要抗下這些職責呢?

我不是項目經理

架構師的日常工作經常會面臨并行處理多個不同維度的問題,這些問題可能是不同的主題,甚至在做決策的時候也需要考慮人員的分配,項目時間表的排期,需要用的核心技術以及組件等。有很多高層領導喜歡直接在架構師這裏獲取項目的詳細信息以及技術方案,雖然架構師角色涉及這些信息並且很了解這些信息,但是這並不是架構師的職責所在,甚至很多情況下令架構師處於項目經理的尷尬角色。

我不是開發人員

我想很多人看過那篇文章:作為架構師該不該寫代碼?很多架構師是出身於開發人員,這也難怪會出現這樣的疑問。但是,架構師其實和資深開發是兩條不同的職業路線,我認為兩者沒有高低之分。出色的開發人員需要很深的開發功力,需要最終交付出可運行的軟件。而架構師則需要更廣闊的知識面,更好的組織戰略思想,更好的溝通能力。在一個產品的開發流水線上,架構師可能會負責一部分核心代碼的編寫,但是最主要的工作還是保證這條流水線的正常運轉。

我不是救火員

由於架構師這個角色在公司的地位,很多管理者認為架構師要隨時隨地的能分析並解決任何突發的問題,不瞞各位,這種現象在很多大廠依然存在,包括我司(雖然只是一個四線小廠)。如果一個架構師每天都忙着“救火”這種工作,根本沒有時間去做真正的架構工作,真正的架構設計需要思考,是不可能在短短時間內完成的。但是架構師必須接受出現的產品問題,因為這些問題的產生有可能和架構有着直接關係,在很大程度上能反應架構的缺陷或者問題。

寫在最後

架構師作為企業中很重要的一環,在很多重大技術問題中都作為決策者而存在。很難用代碼的多少或者質量來衡量一個架構師的好壞,如果一個系統在正常運行5年後依然能良好運行並且可以承受一定的變更能力,說明這個系統的架構師的工作是很出色的。如果非要給架構師定義一個KPI標準的話,以下這些工作也許能成為一個參考

  1. 定義IT戰略。小到一個系統的組件列表可行性的確定,大到公司技術的發展方向,乃至未來10年公司技術的預測與大膽嘗試。這些技術戰略都需要架構師根據自身經驗來制定。

  2. 落實對IT藍圖的管控,以實現協調一致,降低複雜度,保證公司所有系統有條不紊的正常工作,架構師的工作之一就是要把複雜度降低,化繁為簡,這需要架構師很強的抽象能力。

  3. 關注項目的實際落地情況,並根據項目實施中反饋的問題進行戰略的適當調整。一個合格的架構師從來不會忽略來自實際項目中的問題反饋。

架構師一定要避免和消除那些系統設計中不可逆轉的錯誤決策

來源參考:架構師應該知道的37件事

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

【其他文章推薦】

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

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

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

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

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

Autoware 標定工具 Calibration Tool Kit 聯合標定 Robosense-16 和 ZED 相機!

一、安裝 Autoware & ZED 內參標定 & 外參標定準備

之前的這篇文章:Autoware 進行 Robosense-16 線雷達與 ZED 雙目相機聯合標定! 記錄了我用 Autoware 標定相機和雷達的過程,雖然用的不是 Calibration Tool Kit 工具,但是博客裏面的以下章節也適用本次的 Calibration Tool Kit :

  • 一、編譯安裝 Autoware-1.10.0
  • 二、標定 ZED 相機內參
  • 3.1 聯合標定準備

如果你是第一次看這篇 Calibration Tool Kit 聯合標定的博客,建議先按照之前的博客安裝 Autoware、標定 ZED 內參和做好外參標定的準備(標定板,錄製標定包等),最好用上篇博客的方法標定一次。

這篇博客我就直接開始介紹使用 Calibration Tool Kit 標定雷達和相機外參的過程!

二、Calibration Tool Kit 聯合標定雷達和 ZED 相機

2.1 啟動 Autoware

先啟動 Autoware-1.10.0,啟動過程中可能需要輸入 root 密碼:

# 1. 進入 autoware 的 ros 目錄下
cd autoware-1.10.0/ros

# 2. source 環境,zsh 或 bash
source devel/setup.zsh[.bash]

# 3. 啟動主界面
./run

切換到 Sensing 選項卡:

2.2 回放雷達相機 Bag

這裏回放時需要更改雷達的話題為 /points_raw,因為這個工具訂閱的雷達主題是固定的:

rosbag play --pause xxx.bag /rslidar_points:=/points_raw

我用的 Robosense 雷達,發布的話題是 rslidar_points,這個回放默認暫停,防止跑掉數據,按空格繼續或暫停。

2.3 啟動 Calibration Tool Kit

點擊 Calibration Tool Kit 啟動標定工具:

選擇圖像輸入話題,我只用的 ZED 的左圖像話題,如果沒有相機話題,確保前面你已經回放了 bag,選擇好了點擊 OK 確定:

選擇標定類型為相機到 velodyne 雷達的標定(對 Robosense 雷達也適用,只不過需要更改點雲的發布話題),點擊 OK 確定:

進入標定主界面 MainWindow:

配置標定板棋盤格參數:

  • Pattern Size(m):標定板中每個格子的邊長,單位 m,我的標定板每個格子長 0.025 m
  • Pattern Number:標定板長X寬的單元格數量 – 1,我的標定板是長有 12 個格子,寬有 9 個,所以填 11×8,減一是因為標定檢測的是內部角點

設置好了后,重啟 Calibration Tool Kit,點擊左上角 Load 導入第一步標定的相機內參 YAML 文件,但是這個工具只能導入 YML 格式的文件:

因此需要把前面的內參標定文件拷貝一份,修改格式為 yml 即可,YAML 和 YML 其實是一樣的:

修改好了之後,再點擊 Load 加載 yml 格式的內參文件即可:

選擇不加載相機和雷達的標定數據,因為我是直接回放 Bag 標定:

到這裏都設置好了,下面開始外參標定過程!

2.4 標定過程

打開回放 bag 終端,按空格繼續回放數據,主界面會显示相機圖像:

但是右邊的點雲窗口沒有显示數據,需要我們調整視角才可以,視角的調整方法如下(文末有個 pdf 專門介紹):

簡單解釋下,建議直接操作,很容易:

  • 移動點雲:上下左右方向鍵、PgUp、PgDn
  • 旋轉點雲:a、d、w、s、q、e
  • 切換模式:数字 1 和数字 2
  • 視角縮放:減號縮小、加號放大
  • 點雲大小:o 鍵使用小點雲、p 使用大點雲
  • 改變點雲窗口背景顏色:b

我使用的使用直接按数字 2 切換模式就能看到點雲了,其實這些模式我也不是很懂。。。:

如果需要更換背景,按 b 鍵改變為大致灰色即可:

我這裏就不改背景了,黑色也挺好看出點雲的,然後使用上面的視角操作方法,把點雲中的標定板放大到中心位置:

之後點擊右上角的 Grab 捕獲當前幀的圖像和點雲,使用 -+ 縮放視角:

如果你點擊 grab 沒反應很正常,可能是棋盤格離得太遠或者模糊了,你多試幾個位置應該就能捕獲到,我回放一個 Bag 也就捕獲了 9 張左右。

然後把鼠標放到右下角捕獲的點雲窗口,選擇一個棋盤格的中心位置區域,關於這個區域的選擇,我是參考這個標定工具的文檔例子(文末有鏈接)選擇的,大概就是標定板的中心位置選擇一個圓形的區域,盡量保證向外側的平面法向量垂直於標定板平面:

鼠標左鍵點擊選擇,右鍵點擊取消,我的選擇如下,可以參考:

然後重複以上步驟,不斷回放暫停,Grab 捕獲單幀圖像和點雲(多選一些),選擇點雲區域,直到回放結束,接着就可以點擊右上角的「Calibrate」按鈕計算外參矩陣(左上角显示),然後再點擊「Project」查看標定效果:

切換左下方的單幀圖片和點雲窗口,捕獲的每一幀圖像和點雲都可以看到對齊效果,另外左邊也能看到標定的誤差,當然是越小越好,我目前的標定效果一般般,後續打算再標幾次。

標定好之後,點擊左上角「save」保存外參矩陣即可,文件名建議帶上時間戳方便識別:

最後的外參數文件如下,這個文件包含了相機內參和相機到雷達的外參:

以上就是我的雷達相機聯合標定過程!希望能幫助正在標定雷達和相機的同學 ^_^!

三、標定結果測試

可以直接用之前博客 Autoware 進行 Robosense-16 線雷達與 ZED 雙目相機聯合標定! 中的「四、標定結果測試」一節介紹的步驟來測試融合效果:

前幾天我把 ROS 的點雲和圖像的融合節點也調試好了,所以直接在程序裏面加載了外參矩陣,然後做了個初步的融合,效果如下:

我也錄了個融合視頻,可以看看:B 站:Robosense-16 雷達與 ZED 相機數據融合。

五、標定資源

以下是我標定過程中收集的一些好的資料,這裏也分享給大家:

  • 標定工具的使用文檔在這裏:CalibrationToolkit_Manual.pdf
  • 這裏還有個視頻,有條件的同學可以看看:Yutobe:Autoware 標定相機和雷達

另外 ROS 融合節點的程序我還在完善中,建議關注我的 Github 項目,後續會上傳節點代碼:AI-Notes: lidar_camera_fusion,如果標定遇到問題,可以公眾號後台給我發消息,或者直接在博客平台留言,我看到會儘快回復的,不過公眾號應該回復的快些,哈哈 :)

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

【其他文章推薦】

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

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

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

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

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

SpringBoot 2.3 整合最新版 ShardingJdbc + Druid + MyBatis 實現分庫分表

  今天項目不忙,想搞一下shardingJDBC分庫分表看看,主要想實現以下幾點:

  1. 捨棄xml配置,使用.yml或者.properties文件+java的方式配置spring。
  2. 使用 Druid 作為數據庫連接池,同時開啟監控界面,並支持監控多數據源。
  3. 不依賴 com.dangdangsharding-jdbc-core 包。此包過於古老,最後一次更新在2016年。目測只是封裝了一層,意義不大。感覺如果不是dangdang公司內部開發,沒必要用這個包。(且本人實測不能和最新的Druid包一起用,insert語句報錯)

  折騰了半天,網上找的例子大部分跑不通。直接自己從零開搞,全部組件直接上當前最新版本。

  SpringBoot: 2.3.0

  mybatis: 2.1.3

  druid: 1.1.22

  sharding-jdbc: 4.1.1

  注意:這裏因為是自己邊看源碼邊配置,(sharding官網的例子可能是版本問題基本沒法用,GitHub 我這裏網絡基本打不開),所以數據源和sharding大部分用java代碼配置。了解配置原理后,也可以簡化到 .yml / .properties 文件中。

Sharding-JDBC簡介

  Apache ShardingSphere 是一套開源的分佈式數據庫中間件解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款相互獨立,卻又能夠混合部署配合使用的產品組成。

  Sharding-JDBC定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。

  • 適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  • 支持任意實現JDBC規範的數據庫。目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 標準的數據庫。

Sharding配置示意圖

  簡單的理解如下圖,對sharding-jdbc進行配置,其實就是對所有需要進行分片的表進行配置。對錶的配置,則主要是對分庫的配置和分表的配置。這裏可以只分庫不分表,或者只分表不分庫,或者同時包含分庫和分表邏輯。

 

  先看一下我的項目目錄結構整體如下:

  

一、POM依賴配置

  完整的pom表如下,其中主要是對 mysql-connector-java、mybatis-spring-boot-starter、druid-spring-boot-starter、sharding-jdbc-core 的依賴。

  注意:sharding-jdbc-core 我用的4.0+的版本,因為已經晉陞為 apache 基金會的頂級項目,其 groupId 變為了 org.apache.shardingsphere,之前是io.shardingsphere。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>shardingjdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shardingjdbc</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <!--<sharding.jdbc.version>3.0.0</sharding.jdbc.version>-->
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>4.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

pom.xml

二、application.properties

  這裏配置了兩個數據源,為避免和自動裝配產生衝突,屬性前綴要和自動裝配掃描的前綴區分開,這裏我用 datasource0datasource1

  下面 spring.datasource.druid 開頭的配置,會被 druid 的代碼自動掃描裝配。

#################################### common config : ####################################
spring.application.name=shardingjdbc
# 應用服務web訪問端口
server.port=8080

# mybatis配置
mybatis.mapper-locations=classpath:com/example/shardingjdbc/mapper/*.xml
mybatis.type-aliases-package=com.example.shardingjdbc.**.entity

datasource0.url=jdbc:mysql://localhost:3306/test0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
datasource0.driver-class-name=com.mysql.cj.jdbc.Driver
datasource0.type=com.alibaba.druid.pool.DruidDataSource
datasource0.username=root
datasource0.password=852278

datasource1.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
datasource1.driver-class-name=com.mysql.cj.jdbc.Driver
datasource1.type=com.alibaba.druid.pool.DruidDataSource
datasource1.username=root
datasource1.password=852278

#
##### 連接池配置 #######
# 過濾器設置(第一個stat很重要,沒有的話會監控不到SQL)
spring.datasource.druid.filters=stat,wall,log4j2

##### WebStatFilter配置 #######
#啟用StatFilter
spring.datasource.druid.web-stat-filter.enabled=true
#添加過濾規則
spring.datasource.druid.web-stat-filter.url-pattern=/*
#排除一些不必要的url
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
#開啟session統計功能
spring.datasource.druid.web-stat-filter.session-stat-enable=true
#缺省sessionStatMaxCount是1000個
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
#spring.datasource.druid.web-stat-filter.principal-session-name=
#spring.datasource.druid.web-stat-filter.principal-cookie-name=
#spring.datasource.druid.web-stat-filter.profile-enable=

##### StatViewServlet配置 #######
#啟用內置的監控頁面
spring.datasource.druid.stat-view-servlet.enabled=true
#內置監控頁面的地址
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
#關閉 Reset All 功能
spring.datasource.druid.stat-view-servlet.reset-enable=false
#設置登錄用戶名
spring.datasource.druid.stat-view-servlet.login-username=admin
#設置登錄密碼
spring.datasource.druid.stat-view-servlet.login-password=123
#白名單(如果allow沒有配置或者為空,則允許所有訪問)
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
#黑名單(deny優先於allow,如果在deny列表中,就算在allow列表中,也會被拒絕)
spring.datasource.druid.stat-view-servlet.deny=

三、數據源和分片配置

  如下代碼,先從配置文件讀取數據源的所需要的屬性,然後生成 Druid 數據源。注意這裏配置語句中的 setFilters,如果不添加 filters,則 Duird 監控界面無法監控到sql。另外,其他諸如最大連接數之類的屬性這裏沒有配,按需配置即可。數據源創建好后,添加到 dataSourceMap 集合中。

  再往下註釋比較清楚,構造 t_user 表的分片規則(包括分庫規則 + 分表規則),然後將所有表的分片規則組裝成 ShardingRuleConfiguration

  最後,將前兩步配好的 dataSourceMapshardingRuleConfiguration 交給 ShardingDataSourceFactory,用來構造數據源。

  到這裏,sharding 、druid 的配置代碼就都寫好了。剩下基本都是業務代碼了。

package com.example.shardingjdbc.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.example.shardingjdbc.sharding.UserShardingAlgorithm;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.StandardShardingStrategyConfiguration;
import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
public class DataSourceConfig {
    @Value("${datasource0.url}")
    private String url0;
    @Value("${datasource0.username}")
    private String username0;
    @Value("${datasource0.password}")
    private String password0;
    @Value("${datasource0.driver-class-name}")
    private String driverClassName0;

    @Value("${datasource1.url}")
    private String url1;
    @Value("${datasource1.username}")
    private String username1;
    @Value("${datasource1.password}")
    private String password1;
    @Value("${datasource1.driver-class-name}")
    private String driverClassName1;

    @Value(("${spring.datasource.druid.filters}"))
    private String filters;

    @Bean("dataSource")
    public DataSource dataSource() {
        try {
            DruidDataSource dataSource0 = new DruidDataSource();
            dataSource0.setDriverClassName(this.driverClassName0);
            dataSource0.setUrl(this.url0);
            dataSource0.setUsername(this.username0);
            dataSource0.setPassword(this.password0);
            dataSource0.setFilters(this.filters);

            DruidDataSource dataSource1 = new DruidDataSource();
            dataSource1.setDriverClassName(this.driverClassName1);
            dataSource1.setUrl(this.url1);
            dataSource1.setUsername(this.username1);
            dataSource1.setPassword(this.password1);
            dataSource1.setFilters(this.filters);

            //分庫設置
            Map<String, DataSource> dataSourceMap = new HashMap<>(2);
            //添加兩個數據庫database0和database1
            dataSourceMap.put("ds0", dataSource0);
            dataSourceMap.put("ds1", dataSource1);

            // 配置 t_user 表規則
            TableRuleConfiguration userRuleConfiguration = new TableRuleConfiguration("t_user", "ds${0..1}.t_user${0..1}");
            // 配置分表規則
            userRuleConfiguration.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", UserShardingAlgorithm.tableShardingAlgorithm));
            // 配置分庫規則
            userRuleConfiguration.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", UserShardingAlgorithm.databaseShardingAlgorithm));
            // Sharding全局配置
            ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
            shardingRuleConfiguration.getTableRuleConfigs().add(userRuleConfiguration);
            // 創建數據源
            DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, new Properties());
            return dataSource;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

DataSourceConfig.java

  上面構造分片規則的時候,我定義了User表的分片算法類 UserShardingAlgorithm,並定義了兩個內部類分別實現了數據庫分片和表分片的邏輯。代碼如下:

package com.example.shardingjdbc.sharding;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

public class UserShardingAlgorithm {
    public static final DatabaseShardingAlgorithm databaseShardingAlgorithm = new DatabaseShardingAlgorithm();
    public static final TableShardingAlgorithm tableShardingAlgorithm = new TableShardingAlgorithm();

    static class DatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        @Override
        public String doSharding(Collection<String> databaseNames, PreciseShardingValue<Long> shardingValue) {
            for (String database : databaseNames) {
                if (database.endsWith(String.valueOf(shardingValue.getValue() % 2))) {
                    return database;
                }
            }

            return "";
        }
    }

    static class TableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        @Override
        public String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> shardingValue) {
            for (String table : tableNames) {
                if (table.endsWith(String.valueOf(shardingValue.getValue() % 2))) {
                    return table;
                }
            }

            return "";
        }
    }
}

UserShardingAlgorithm.java

  這裏實現分片規則時,實現的接口是 PreciseShardingAlgorithm,即精確分片,將指定的鍵值記錄映射到指定的1張表中(最多1張表)。這個接口基本上能滿足80%的需求了。

  其他的還有 Range、ComplexKey、Hint分片規則,這3種都可以將符合條件的鍵值記錄映射到多張表,即可以將記錄 a 同時插入A、B 或 B、C多張表中。

  其中,

    Range 是範圍篩選分片。我個人理解,比如id尾數1-5插入A表,6-0插入B表,這種情況,使用Range作為篩選條件更方便。也可以根據時間範圍分片。(如有誤請指正)。

    ComplexKey 看名字就是組合鍵分片,可以同時根據多個鍵,制定映射規則。

    Hint 看名字沒看懂,但看源碼其實也是組合鍵分片,但僅支持對組合鍵進行精確篩選。

    而 ComplexKey 支持對組合鍵進行範圍篩選。所以可以理解為 ComplexKey 是 Hint 的高級版本。  

  不管實現哪種分片算法,都要確保算法覆蓋所有可能的鍵值。

四、使用行表達式配置分片策略(對第三步優化,可略過)

    上面第三步,我們通過實現 PreciseShardingValue 接口,來定義分片算法。這樣每有一張表需要分片,都要重新定義一個類,太麻煩。

  Sharding 提供了行表達式配置的方式,對簡單的分片邏輯,直接定義一個行表達式即可。(這種方式其實就是直接在 .yml 文件中配置分片策略的解析方式)

  和上面的代碼類似,這裏之改動了6、8行,直接 new 一個 InlineShardingStrategyConfiguration,省去了定義分片算法類的繁瑣步驟。

 

 1              // .....省略其他代碼
 2  
 3             // 配置 t_user 表規則
 4             TableRuleConfiguration userRuleConfiguration = new TableRuleConfiguration("t_user", "ds${0..1}.t_user${0..1}");
 5             // 行表達式分表規則
 6             userRuleConfiguration.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "t_user${id % 2}"));
 7             // 行表達式分庫規則
 8             userRuleConfiguration.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "ds${id % 2}"));
 9 
10             // Sharding全局配置
11             ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
12             shardingRuleConfiguration.getTableRuleConfigs().add(userRuleConfiguration);
13             // 創建數據源
14             DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, new Properties());
15             return dataSource;

五、分佈式主鍵(雪花算法)

  分庫后,不能再使用 mysql 的自增主鍵,否則會產生重複主鍵。自定義主鍵,主要需要解決兩個問題:

  1. 主鍵唯一(必須)
  2. 主鍵單調遞增(可選)(提升索引效率,減少索引重排產生的空間碎片)

  Sharding 內部提供了2個主鍵生成器,一個使用雪花算法 SnowflakeShardingKeyGenerator,一個使用 UUID(考慮上面第2條,因此不使用 UUID)。

  雪花算法的主要原理:用一個 64 bit 的 long 型数字做主鍵。其中,

    第 1 位,1 bit 作為符號位永遠為 0,表示是正數。

    第 2 – 42 位, 41 個 bit 填充時間戳。

    第 43 – 52 位,10 個 bit 填充機器唯一id。舉個例子,可以用前4位標識機房號,后6位標識機器號。

    第 53 – 64 位,12 個 bit 填充id序號。範圍 0 – 4095,即每台機器每 1 毫秒最多生成 4096 個不同的主鍵id。

  雪花算法的主要實現代碼如下

  1. 先判斷時鐘是否回調。這裏默認容忍回調時間為0,如有回調則會產生異常。可以通過配置 max.tolerate.time.difference.milliseconds 屬性,讓其自旋等待時鐘回到上一次執行時間。
  2. 按當前毫秒數,遞增生成id序號。如果時鐘進入了下一毫秒,則從0開始重新生成id序號,範圍 0 – 4095。
  3. 將 時間戳 + 機器序號 + id序號 拼裝成 主鍵id。這裏機器序號默認為0,可以通過 worker.id 屬性進行配置。不同的服務器需要配置成不同的数字,範圍 0 – 1023。

  其中 EPOCH 是時鐘起點,sharding中設置的是2016年11月1日,那麼41位的時間戳差不多可以用70年,一直到2086年。

    public synchronized Comparable<?> generateKey() {
        long currentMilliseconds = timeService.getCurrentMillis();
        if (this.waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentMillis();
        }

        if (this.lastMilliseconds == currentMilliseconds) {
            if (0L == (this.sequence = this.sequence + 1L & 4095L)) {
                currentMilliseconds = this.waitUntilNextTime(currentMilliseconds);
            }
        } else {
            this.vibrateSequenceOffset();
            this.sequence = (long)this.sequenceOffset;
        }

        this.lastMilliseconds = currentMilliseconds;
        return currentMilliseconds - EPOCH << 22 | this.getWorkerId() << 12 | this.sequence;
    }

六、業務代碼

  使用分佈式的主鍵ID生成器,需要給不同的表注入不同的ID生成器,在config包下加一個KeyIdConfig類,如下:

  (為了保持時鐘的統一,可以專門找一台機器作為時鐘服務,然後給所有主鍵生成器配置統一的時鐘服務。下圖中未配置,如需配置,直接調用setTimeService方法即可)

@Configuration
public class KeyIdConfig {
    @Bean("userKeyGenerator")
    public SnowflakeShardingKeyGenerator userKeyGenerator() {
        return new SnowflakeShardingKeyGenerator();
    }

    @Bean("orderKeyGenerator")
    public SnowflakeShardingKeyGenerator orderKeyGenerator() {
        return new SnowflakeShardingKeyGenerator();
    }
}

  其他業務代碼,整體如下:

package com.example.shardingjdbc.entity;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private String phone;
    private String email;
    private String password;
    private Integer cityId;
    private Date createTime;
    private Integer sex;
}

User.java

package com.example.shardingjdbc.mapper;

import com.example.shardingjdbc.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

public interface UserMapper {
    /**
     * 保存
     */
    void save(User user);

    /**
     * 查詢
     * @param id
     * @return
     */
    User get(Long id);
}

UserMapper.java

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.shardingjdbc.mapper.UserMapper">
    <resultMap id="resultMap" type="com.example.shardingjdbc.entity.User">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="phone" property="phone"  />
        <result column="email" property="email"  />
        <result column="password" property="password"  />
        <result column="city_id" property="cityId"  />
        <result column="create_time" property="createTime"  />
        <result column="sex" property="sex"  />
    </resultMap>

    <insert id="save">
        insert into t_user (id, name, phone, email, password, city_id, create_time, sex)
        values (#{id}, #{name}, #{phone}, #{email}, #{password}, #{cityId}, #{createTime}, #{sex})
    </insert>

    <select id="get" resultMap="resultMap">
        select *
        from t_user
        where id = #{id}
    </select>
</mapper>

UserMapper.xml

 1 package com.example.shardingjdbc.controller;
 2 
 3 import com.example.shardingjdbc.entity.User;
 4 import com.example.shardingjdbc.mapper.UserMapper;
 5 import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.stereotype.Controller;
 8 import org.springframework.web.bind.annotation.PathVariable;
 9 import org.springframework.web.bind.annotation.RequestMapping;
10 import org.springframework.web.bind.annotation.ResponseBody;
11 
12 import javax.annotation.Resource;
13 import java.util.Date;
14 
15 @Controller
16 public class UserController {
17     @Autowired
18     private UserMapper userMapper;
19 
20     @Resource
21     SnowflakeShardingKeyGenerator userKeyGenerator;
22 
23     @RequestMapping("/user/save")
24     @ResponseBody
25     public String save() {
26         for (int i = 0; i < 50; i++) {
27             Long id = (Long)userKeyGenerator.generateKey();
28             User user = new User();
29             user.setId(id);
30             user.setName("test" + i);
31             user.setCityId(i);
32             user.setCreateTime(new Date());
33             user.setSex(i % 2 == 0 ? 1 : 2);
34             user.setPhone("11111111" + i);
35             user.setEmail("xxxxx");
36             user.setCreateTime(new Date());
37             user.setPassword("eeeeeeeeeeee");
38             userMapper.save(user);
39         }
40 
41         return "success";
42     }
43 
44     @RequestMapping("/user/get/{id}")
45     @ResponseBody
46     public User get(@PathVariable Long id) {
47         User user = userMapper.get(id);
48         return user;
49     }
50 }

UserController.java

 1 CREATE TABLE `t_user` (
 2   `id` bigint(20) NOT NULL,
 3   `name` varchar(64) DEFAULT NULL COMMENT '名稱',
 4   `city_id` int(12) DEFAULT NULL COMMENT '城市',
 5   `sex` tinyint(1) DEFAULT NULL COMMENT '性別',
 6   `phone` varchar(32) DEFAULT NULL COMMENT '電話',
 7   `email` varchar(32) DEFAULT NULL COMMENT '郵箱',
 8   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',
 9   `password` varchar(32) DEFAULT NULL COMMENT '密碼',
10   PRIMARY KEY (`id`)
11 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

t_user.sql

  啟動類如下:

 1 package com.example.shardingjdbc;
 2 
 3 import org.mybatis.spring.annotation.MapperScan;
 4 import org.springframework.boot.SpringApplication;
 5 import org.springframework.boot.autoconfigure.SpringBootApplication;
 6 
 7 @MapperScan("com.example.shardingjdbc.mapper")
 8 @SpringBootApplication
 9 public class ShardingjdbcApplication {
10     public static void main(String[] args) {
11         SpringApplication.run(ShardingjdbcApplication.class, args);
12     }
13 }

ShardingjdbcApplication .java

  注意,這裏我在啟動類上加了 @MapperScan 註解。可能是因為引用依賴的問題,.properties 配置的 mybatis 包掃描目錄不管用了,後面有時間再研究。

七、其他

  除了基本的分庫分表規則以外,還有一些其他的配置,比如綁定表。這裏先不詳細解釋了,舉個簡單的例子:

  現在有 order, order_detail兩張表,1 : 1的關係。

  在配置的時候,應該將相同 order_id 的 order 記錄 和 order_detail 記錄 映射到相同尾號的表中,方便連接查詢。

  比如 id % 2 = 1的,都插入到  order0, order_detail0 中。

  如果配置了綁定關係,那麼查找 id = 1 的記錄,只會產生一次查詢 select * from order0 as o join order_detail0 as d  on o.order_id = d.order_id where o.oder_id = 1。

  否則會產生笛卡兒積查詢, 

    select * from order0 as o join order_detail0 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order0 as o join order_detail1 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order1 as o join order_detail0 as d  on o.order_id = d.order_id where o.order_id = 1

    select * from order1 as o join order_detail1 as d  on o.order_id = d.order_id where o.order_id = 1

八、總結

  項目啟動前,先創建數據庫 test0, test1, 然後分別建表 t_user0, t_user1。 可以全部在同一台機器。

  項目啟動后,訪問 http://localhost:8080/user/save, id 是 偶數的都插入到了 test0 庫的 t_user0 表中, 奇數的都插入到了 test1 庫中的 t_user1 表中。

  druid 的後台監控頁面地址: http://localhost:8080/druid/。

  項目啟動后,sharding日誌會將配置已 yml 格式的形式打印出來,也可以省去 java 配置,將其優化到 .yml 配置文件中去,如下圖:

  

  本文原文地址:https://www.cnblogs.com/lyosaki88/p/springboot_shardingjdbc_druid_mybatis.html

  源碼下載地址:https://474b.com/file/14960372-448059323

  作者QQ: 116269651

 

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

7.98萬起產品力不輸哈弗H6,這款SUV車主是怎樣評價的?

最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。車主:老兵哥購買車型:北汽幻速S6 2017款 1。5T CVT樂享型裸車購買價:10。

前言

作為國內的造車大戶,北汽奉行着“多生孩兒多掙錢”的政策,有注重一般家用市場的北汽紳寶、有注重硬派SUV市場的北京汽車、還有着北汽幻速以及北汽比速這兩個入門品牌。那麼作為一個比較年輕的品牌,北汽幻速的口碑究竟怎樣呢?今天筆者就搜集了幾位北汽幻速S6車主的意見,這款7.98萬起售的緊湊型SUV有着該價位中不俗的競爭力。全系標配的是1.5T發動機,而且有着CVT變速箱作為自動擋的選擇。

那麼多孩子,打起群架肯定贏

北汽銀翔幻速S6

官方指導價:7.98-11.68萬

編者意見:

性價比較高,動力表現在同價位中表現比較優秀。不過全系沒能標配ESp車身穩定系統以及电子助力轉向比較可惜。

車主:BY2000

購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

裸車購買價11.68 萬元

最滿意的地方:整體都比較滿意,但是最滿意的的是價格,性價比很高。能在這個價格買到這樣配置的緊湊型SUV還是不錯的,而且是渦輪增壓發動機。

最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。

車主:老兵哥

購買車型:北汽幻速S6 2017款 1.5T CVT樂享型

裸車購買價:10.68 萬元

最滿意的地方:乘坐空間,沒有想到這個價格的車還能有着那麼大的空間,滿載的情況下也不是很擁擠。而且後備箱容積也是相當可觀,大天窗還有那麼多的配置,買這款車真的是比較值。

最不滿意的地方:裝配工藝有待加強,有些部分的縫隙是比較大的,就如尾門的縫隙,看着很掉價,而且方向盤塑料感太強了。

車主:smg20900

購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

裸車購買價:10.68 萬元

最滿意的地方:外觀,看着更像是二十多萬的SUV。能給人更多的面子,而且在動力方面感覺還是不錯的,一直都可以維持在較低轉速,120km/h時速下轉速也只是2200rpm左右,這個是最為滿意的,所以綜合油耗上也是9L左右,對於一款SUV來說是滿意了。

最不滿意的地方:儲物空間實在是少得可憐,中間只有一個杯架,不夠用。其次是噪音的問題,不過對於如此便宜的車來說,還是可以接受的。

編者總結:

北汽雖然是個歷史悠長的品牌,但事實基本是為別人“代工”,自身在工藝方面以及控製成本方面還是需要向合資學習,所以在做工以及隔音用料上表現一般。不過在主要的發動機上表現卻是相當不錯,雖然渦輪遲滯現象還是有的,但是油耗表現卻是令人信服的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

我敢說這幾款是2016年最成功的自主車型!你同不同意?

如果要盤點2016年自主品牌中最有設計感的車型,寶駿510必定首當其沖。這款定位僅在6萬左右的SUV,居然擁有如此前衛和新潮的設計,不得不佩服上汽高層的決策。叫獸在後台大家對510的評論中可以預料到,這必將又是一款未上市已成爆款的車型。

又到了辭舊迎新的日子,我們即將迎來嶄新的2017年。回望2016,汽車圈內發生了不少大事,也湧現出了非常多的好車。其中不少中國品牌的表現尤其搶眼,叫獸似乎從它們身上看到了正在崛起的“Made in China”,讓我對2017充滿期待。

如今GS4甚至成了傳祺的代名詞,目前傳祺絕大部分的銷量和產能均來自GS4。廣汽同樣也審時度勢,在“SUV熱”持續不降的情形下推出了GS8。這款以中大型7座SUV身份登場的GS8,憑藉非常符合國人審美的大氣造型迅速俘獲了眾多青睞。

據說現在GS8也是一車難求的狀態,訂單量已經排到了明年開年以後。傳祺現在的當務之急是儘快擴大產量,以保證GS4和GS8的銷量需求。對了,傳祺全新的MpV – GM8也將在明年初上市,2017將是廣汽傳祺非常有挑戰和希望的一年。

SUV市場已經成為兵家必爭之地,幾乎所有中國品牌都在這看到了“彎道超車”的希望。最典型的如哈弗H6成了月銷超7萬的“無敵神車”(叫獸的印象中只有五菱達到過這樣的成績)。

在殺得“頭破血流”的SUV爭奪中,想成為爆款可沒那麼簡單。榮威旗下的最新SUV – RX5,通過全新的面貌以及有馬雲“加持”的首款量產互聯網SUV身份登場,從北京車展亮相便收穫了極大關注。上市兩個月後,成為月銷破2萬的“黑馬”,風頭甚至蓋過了博越。

廣州車展亮相的首款量產互聯網轎車 – 榮威i6和MG 推出的最新互聯網SUV – ZS再次成為焦點。以上3款車型雖然都打着互聯網的旗號,但叫獸相信互聯網只是錦上添花,真正讓它們成為“網紅”的是上汽旗下產品設計以及品質的全面提升。要知道,這些才是一款車成功的根本。

寶駿是諸多中國品牌中比較特殊的存在,它身上流淌着上汽、通用以及五菱三家的血液。自730熱銷以及SUV 560成為“黑馬”后,寶駿也頻頻進入大眾的視線。

這都不算啥,真正讓寶駿走向公眾焦點的是310。這款擁有超高顏值的入門小車,以3.68萬的起售價贏得了無數年輕消費者的關注和喜愛。要知道如今入門級轎車市場已經沒有幾款拿得出手的車型了。而310的推出將成為不少第一次買車的消費者的最新選擇,叫獸甚至認為它還影響到部分二手車的銷量。事實上,310已經連續兩個月月銷破萬,這樣的成績對早已被“邊緣化”的A0級市場無疑是一劑強心針。

如果要盤點2016年自主品牌中最有設計感的車型,寶駿510必定首當其沖。這款定位僅在6萬左右的SUV,居然擁有如此前衛和新潮的設計,不得不佩服上汽高層的決策。叫獸在後台大家對510的評論中可以預料到,這必將又是一款未上市已成爆款的車型。

2016是吉利最“吉利”的一年,去年上市的首款“3.0精品”車型 – 博瑞成了吉利集團成立近20年最重要的分水嶺之一。收購沃爾沃之後,吉利“蟄伏”5年終於放出了“大招”,以一款中高級轎車博瑞拉開了“3.0精品”的序幕。

吉利的選擇很大膽,以高開高走的策略打造全新的精品之路。幸運的是博瑞成功了,甚至成為中國品牌汽車發展多年來首款真正成功的進軍高端的轎車。家族旗艦的成功,也將吉利帶入了一個新的高度,隨後以“3.0精品”身份誕生的博越、帝豪GS、帝豪GL以及僅僅只能算是改款的遠景SUV無一不成為月銷過萬的爆款車型。

銷量說明一切。11月,吉利月銷量首次突破10萬大關,不僅老李笑得合不攏嘴,對整个中國品牌來說都是一件值得驕傲的事情。

10月20號,吉利正“春風得意”的時候,總裁安聰慧在德國召開了一場“舉世奪目”的發布會,推出了全新高端品牌- LYNK & CO並亮相兩款概念車。LYNK & CO的目標對手為大眾、豐田等,從此吉利集團旗下品牌將覆蓋到各個檔次。

我想說老李的野心可真不小。無論如何,吉利為中國品牌起了個好頭,值得我們肯定,加油吧。

以上是2016年裡關注度最高的幾个中國品牌。誠然從銷量上它們已經達到和超越與外國品牌競爭的水平,甚至不少車型在價格上比大眾等一線合資品牌還要“堅挺”。這是好事,一來反映出中國品牌已經逐漸被廣大消費者認可,再也不用靠低價吸引顧客;二來說明中國品牌車型的造車水準和品牌影響有了質的提升,這才是最根本也是最重要的。

誇了這麼多,叫獸也不得不提醒一句,雖然中國品牌看似在這兩年集體爆發,但細心的你一定會發現成功的品牌和車型大部分都靠的是SUV。迎合市場潮流成功固然可取,但全面均衡的發展更重要。偏科可不是優秀生,只有SUV、轎車、MpV多領域的成功才是真正的“三好學生”。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

這輛售價不到10萬的SUV能帶你勇闖雪地

針對行駛方面,外后視鏡電加熱、360°全景影像、9寸大屏、車內氛圍燈、無鑰匙進入+一鍵啟動、天窗、定速巡航、發動機怠速啟停等配置都一應俱全,真正的讓駕駛人員也感受得到森雅R7帶來的那份舒適。穩定可靠的動力系統森雅R7搭載代號為CA4GB16的直列4缸1。

俗話說真金不怕火煉,就是說真正好的東西,是經得起考驗的。那對於汽車來說,“火煉”明顯不是現實的考驗方案,真正想檢驗一輛車的極限性能,-20攝氏度下的冰雪試駕就是最好的方法。

在嚴寒環境中,鋼鐵、橡膠、塑料都會變脆,一些平時看上去不起眼的顛簸、磕碰都可能會造成斷開、破裂。可以說,嚴寒用車環境是對整車品質的全面考驗,直接體現廠家的專業化造車水準。就在12月17日,森雅R7“柒待•玩美之行”冰雪試駕活動就在平均積雪厚度達2米,雪量堪稱中國之最的雪鄉舉行。

這輛售價不到十萬的SUV,憑什麼有信心能在冰天雪地下讓我們檢驗它的實力?

7位一體的智能主動安全系統

大家都知道冰雪路面極其濕滑,即使車子換上了雪地胎,車輛依然很容易發生側滑或者甩尾等現象,如果沒有及時的控制好車輛,事故的發生就在頃刻之間。那麼在這個時候,主動安全配置就發揮了重要的作用,這一次在雪鄉試駕的森雅R7就搭載了比較全面的主動安全系統。其中就包括了ABS防抱死、EBD制動力分配、ESp車身穩定系統、牽引力控制、剎車輔助、上坡輔助、胎壓監測等。

在雪鄉試駕的時候就體現了森雅R7這些主動安全配置的作用,整個車身給的感覺都是穩定可控的,即使在濕滑的路面車子也能按照駕駛者的意願來行駛。而縱觀國內的各大汽車品牌,能夠在售價十萬不到的車型齊全配備這七大主動安全配置的品牌並不多,所以森雅R7在這方面的表現可謂相當厚道。

人性化的設計,讓駕乘人員更加舒適

在寒冷的冰天雪地里行駛的話,溫暖和舒適往往是最能夠打動人心的,而在舒適性的配置方面,森雅R7絲毫沒有吝嗇。森雅R7自動擋車型配有6向調節駕駛座椅並搭配了腰部支撐和座椅加熱功能,即使在冰天雪地也能提供溫暖、舒適的乘坐感受,另外,自動空調、後排足部的採暖出風口等配置都體現了森雅R7的人性化設計思想。

針對行駛方面,外后視鏡電加熱、360°全景影像、9寸大屏、車內氛圍燈、無鑰匙進入+一鍵啟動、天窗、定速巡航、發動機怠速啟停等配置都一應俱全,真正的讓駕駛人員也感受得到森雅R7帶來的那份舒適。

穩定可靠的動力系統

森雅R7搭載代號為CA4GB16的直列4缸1.6L自然吸氣發動機,最大馬力為116ps,最大扭矩為155N•m,由一汽自主研發的這台發動機勝在輸出線性,質量可靠,搭配技術同樣成熟的愛信第三代6AT手自一體變速器,換擋邏輯聰明,動力傳輸平順。這讓駕駛員在駕駛的過程中沒有後顧之憂,能夠盡情的征戰冰雪。

實際駕駛過程中,由於該車的油門調教比較靈敏,所以整車的動力響應不會給人慵懶、遲滯的感覺,而是偏向於輕快的調性。第三代的愛信6AT手自一體變速箱是最新一代產品,動力輸出平順,沒有頓挫,該變速箱的升擋時機比較遲,偏向高轉速的輸出會讓車輛有更加积極的動力響應。

回歸到靜態:高原創度的設計

自主品牌被詬病山寨抄襲已是家常便飯,森雅R7卻沒有同流合污,在外觀的自主設計方面下了不少功夫。外觀由大眾控股的IDG公司設計,在森雅R7身上找不到一絲山寨的痕迹。

前臉的整體感很強,大燈-中網一體式的設計,而且採用了微微上揚的線條,頗像一張笑臉。車頭大燈為鹵素光源,但帶有日間行車燈,配合上流線形的造型,科技感十足。

側面的造型則能看出車身的比例十分協調,通過硬朗的線條和突出的輪拱展現了森雅R7的力量美。

車尾的造型圓潤飽滿,尾燈採用LED光源,點亮效果很好,而且尾燈的造型也是和頭燈前後呼應的,頗有心思。

總結

試駕過森雅R7之後,最大的感觸是這輛車的配置實在豐富,特別是在寒冷的冬天,當你坐進一輛車子之後發現它是有座椅加熱功能的,那種感覺是既驚喜又感動的。而森雅R7就是這麼一輛車,以不到十萬的價格,不僅有全面的主動安全配置,在動力系統方面還搭載了最新一代的愛信6AT手自一體變速箱,可以說森雅R7是一汽給我們帶來的又一款誠意之作。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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