Pixel 的雷達黑科技傳將被用在新世代 Google 智慧顯示器上_台中搬家

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

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

曾在 Google Pixel 旗艦機上作為新進手勢感應功能的關鍵感應器 Soli Motion 雷達感測器,來到了最新的 Pixel 5 上,卻疑似因為產品定位而沒有提供,更引起 Google 可能已經放棄這方面發展的猜測。不過現在又有了它可能會透過更多 Nest Home 裝置復活的跡象。繼續閱讀 Pixel 的雷達黑科技傳將被用在新世代 Google 智慧顯示器上報導內文。

▲圖片來源:Google

Pixel 的雷達黑科技傳將被用在新世代 Google 智慧顯示器上

其實在外媒去年訪談 Google 的時候,官方就有表示並沒有放棄 Soli 雷達技術。至於可能的應用方向,當時就有猜測很可能會被用在物聯產品方面 — 後續也真的先被 Nest 溫控器所採納。而隨著支援超音波偵測點亮的 Nest Home mini 推出之後,Google 似乎也打算要將更進階的手勢辨識功能給應用在更多的 Nest 物聯產品之上。

▲圖片來源:Google

相對於將 Soli Motion 感測器用在手機上,進行隔空用手勢切換音樂與關閉通知等用途 — 其實現在的智慧顯示器也已經可以透過攝影機來提供類似的手勢功能了。根據外媒 9to5Google 的消息,Google 2021 年 Smart Display 智慧顯示器,將很可能融入 Soli Motion 的雷達感應帶來更進階的機能,會將其應用在睡眠追蹤記錄功能。

誒… 雖然大家應該心裡第一反應就是「那我睡覺的資料不就讓OOO知道了」。不過與其擔心穿戴裝置是否會在睡眠過程中沒電,還有一定得要持續穿戴的麻煩。說真的,對在意這方面監測的使用者而言,透過隨時對著床並且不用擔心充電問題的 Google Home 物聯裝置,也算是解決了不少的麻煩 — 而且還能放放照片、播播音樂跟詢問問題(比讚)。

▲圖片來源:Google

是說,這也不是 Nest 首度採用 Soli 感應器了。其實在他們的自動溫控產品上,就已經有導入來做為更精確感應居家動靜的用途。講起來,這樣的技術好像在物聯產品上反而更發光發熱,創造出不少不錯的應用呢。感覺可以期待一下新世代 Smart Display 可以因此獲得什麼樣的有趣創新功能。

引用來源

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

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

延伸閱讀:

蘋果 AirTags 防丟追蹤器還沒出,但已有皮革鑰匙圈與眼鏡環三方配件照流出

沒錯 Facebook 又改版了,差在哪你看得出來嗎?

您也許會喜歡:

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

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

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

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

汽機車「聲音照相」科技執法懶人包_台中搬家公司

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

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

為改善高噪音車輛擾寧問題,環保署推動「聲音照相」科技執法來抓汽、機車噪音車,35套系統預計已經在2021年元旦於18個縣市正式上路。環保署「聲音照相」科技執法首日,各縣市都有斬獲,其中又以新北市首日就告發221台車輛噪音超標戰果輝煌。

環保署「聲音照相」科技執法是什麼? 目前有哪些路段開始實施「聲音照相」科技執法? 以下作一整理與說明:

掌握最新電信資費訊息,請加入小丰子3C俱樂部粉絲頁!

小丰子3C俱樂部

 

針對高噪音車輛擾寧問題,目前採搭配攔查檢測或通知車主到指定地點進行原地噪音量測,但偶有碰到在路上很吵但原地噪音卻合格的狀況加上攔查檢測非常耗人力物力,有鑑於此,環保署特參考超速照相執法模式,推動聲音照相直接開罰措施,並完成「機動車輛行駛噪音量測方法-影像輔助法」、「機動車輛噪音管制標準」及「使用中機動車輛噪音管制辦法」等三項法規修訂工作,在聲音照相都已符合目前檢測標準在精密度和準確度的相關規定下,「聲音照相」科技執法已經在2021年1月1日公告正式上路,違規可處新臺幣1800元至3600元。

 

以下針對環保署「聲音照相」執法的方式與全台執法熱點地段做一整理:

1.環保署「聲音照相」科技執法是什麼?
「聲音照相」是透過噪音照相系統(設備包含噪音計、風速計、魚眼攝影機、車牌辨識攝影機、控制箱體…),來紀錄行經車輛產生的噪音是否超標的系統。只要行經檢測地點產生的噪音超標準,照相機就會「聽音」”喀擦”一聲拍下車牌,改裝機車噪音也能「聽音」拍下車牌。

 

為避免環境噪音影響「聲音照相」執法結果,針對由聲音照相系統紀錄到的噪音超標行為,經背景音修正、比對前後三秒確認排除環境干擾、且氣候為無雨、風速小於5m/s者,作為噪音是否超標的判定標準。

 

若行車產生的噪音超標,各地環保局就會根據噪音管制法第26條以不當操駕進行開罰;若從照片中辨認使用非原廠或認證排氣管等違法改裝者,地方環保局將再要求到檢與限期改善。

 

環保署表示:「聲音照相」執法是從受體保護(民眾)角度出發,以「抓吵」為原則,車輛只要沒有過度不當改裝或刻意製造噪音的操駕行為,不會有超標的疑慮,駕駛人只要配合「順順騎、順順開,油門輕、不怕拍」,並不用擔憂受罰!

 

2.開罰標準:

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

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

開罰的標準為:速限50km/h路段,不得超過86分貝;速限50~70km/h路段,不得超過90分貝。至於速限70km/h以上路段,大多離人口密集處較遠,噪音影響相對小,目前未納入「聲音拍照」科技執法範圍。只要噪音超標,不當操駕的超標車輛將以《噪音管制法》開罰1800至3600元罰鍰,不當改造車輛也將限期改

 

不過, 執行公務車輛、非機動車輛自身設備產生的聲音及多車同時經過會被排除。

 

3.「聲音照相」執法有哪些路段?

環保署表示目前35套「聲音照相」系統已經在2021年元旦於18個縣市正式上路。「聲音照相」系統分成「固定式高噪音車輛偵測攝影系統」與「移動式聲音照相系統」,前者主要固定架設在民眾陳情熱區之特定路段設置以長期監視取締;後者則採非定時、非定點,於民眾常陳情噪音車輛出沒之熱區,執行機動性聲音照相取締工作。

 

在「聲音照相」執法路段,在架設點前方100公尺至300公尺間,會設置「前有聲音照相 請降低音量行駛」的告示牌,提醒汽機車駕駛。

 

以下是已知各縣市重點取締汽機車噪音的路段與取締熱點,請習慣故意發出吵雜聲音的汽機車主,看好自己的荷包!

Note: 以上資訊純供參考,實際熱點以各地方環保局公告為準

 

整體而言,環保署推動的「聲音照相」科技執法主要是針對一些違法改裝的車主及故意不當操駕的跑車或大型重型機車車主而來,對於正常行駛的汽、機車車主基本上是沒有影響的。有了這項可以取締故意製造噪音的科技,對於經常身受車輛排氣管高分貝的音量打擾的民眾將是一大福音。

 

您也許會喜歡:

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

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

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

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

ClickHouse源碼筆記1:聚合函數的實現_網頁設計公司

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

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

由於工作的需求,後續筆者工作需要和開源的OLAP數據庫ClickHouse打交道。ClickHouse是Yandex在2016年6月15日開源了一個分析型數據庫,以強悍的單機處理能力被稱道。
筆者在實際測試ClickHouse和閱讀ClickHouse的源碼過程之中,對”戰鬥民族”開發的數據庫十分欣賞。ClickHouse不僅是一個很好的數據庫學習材料,而且同時應用了大量的CPP17的新特性進行開發,也是一個大型的Modern CPP的教導資料。
筆者接下來會陸續將閱讀ClickHouse的部分心得體會與通過源碼閱讀筆記的方式和大家分享,坦白說,這種源碼閱讀筆記很難寫啊。(多一分繁瑣,少一分就模糊了~~)
第一篇文章,我們就從聚合函數的實現開始聊起~~ 上車!

1.基礎知識的梳理

什麼是聚合函數?

聚合函數: 顧名思義就是對一組數據執行聚合計算並返回結果的函數。
這類函數在數據庫之中很常見,如:count, max, min, sum等等。

ClickHouse的實現接口
  • IAggregateFunction接口
    在ClickHouse之中,定義了一個統一的聚合函數接口:IAggregateFunction.(在ClickHouse之中,所有的接口類都是以大寫的I開頭的。) 上文筆者提到的聚合函數,則都是作為抽象類IAggregateFunction的子類實現的。其中該接口最為核心的方法是下面這5個方法:
    • add函數:最為核心的調用接口,將對應AggregateDataPtr指針之中數據取出,與列columns中的第row_num的數據進行對應的聚合計算。(這裏可以看到ClickHouse是一個純粹的列式存儲數據庫,所有的操作都是基於列的數據結構。)
    • merge函數:將兩個聚合結果進行合併的函數,通常用在併發執行聚合函數的過程之中,需要將對應的聚合結果進行合併。
    • serialize函數與deserialize函數:序列化與反序列化的函數,通常用於spill to disk或分佈式場景需要保存或傳輸中間結果的。
    • addBatch函數:這是函數也是非常重要的,雖然它僅僅實現了一個for循環調用add函數。它通過這樣的方式來減少虛函數的調用次數,並且增加了編譯器內聯的概率。(虛函數的調用需要一次訪存指令,一次查表,最終才能定位到需要調用的函數上,這在傳統的火山模型的實現上會帶來極大的CPU開銷。)
  /** Adds a value into aggregation data on which place points to.
     *  columns points to columns containing arguments of aggregation function.
     *  row_num is number of row which should be added.
     *  Additional parameter arena should be used instead of standard memory allocator if the addition requires memory allocation.
     */
    virtual void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena) const = 0;

    /// Merges state (on which place points to) with other state of current aggregation function.
    virtual void merge(AggregateDataPtr place, ConstAggregateDataPtr rhs, Arena * arena) const = 0;

    /// Serializes state (to transmit it over the network, for example).
    virtual void serialize(ConstAggregateDataPtr place, WriteBuffer & buf) const = 0;

    /// Deserializes state. This function is called only for empty (just created) states.
    virtual void deserialize(AggregateDataPtr place, ReadBuffer & buf, Arena * arena) const = 0;
    // /** Contains a loop with calls to "add" function. You can collect arguments into array "places"
      *  and do a single call to "addBatch" for devirtualization and inlining.
      */
    virtual void addBatch(size_t batch_size, AggregateDataPtr * places, size_t place_offset, const IColumn ** columns, Arena * arena) const = 0;

  • 抽象類IColumn
    上面的接口IAggregateFunction的函數使用到了ClickHouse的核心接口IColumn類,這裏也進行簡要的介紹。 IColumn 接口表達了所有數據在ClickHouse之中的用內存表達的數據結構,其他帶有具體數據類型的如ColumnUInt8、ColumnArray 等, 都實現了對應的列接口,並且在子類之中具象實現了不同的內存布局。
    IColumn的子類實現細節很瑣碎,筆者這裏就暫時不展開講了,筆者這裏就簡單講講涉及到聚合函數調用部分的IColumn接口的對應方法:
    這裏columns是一個二維數組,通過columns[0]可以取到第一列。(這裏只有涉及到一列,為什麼columns是二維數組呢?因為處理array等列的時候,也是通過對應的接口,而array就需要應用二維數組了. )
    注意這裡有一個強制的類型轉換,column已經轉換為ColVecType類型了,這是模板派生出IColumn的子類。
    然後通過IColumn子類實現的getData方法獲取對應row_num行的數據進行add函數調用就完成了一次聚合函數的計算了。
    void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena *) const override
    {
        const auto & column = static_cast<const ColVecType &>(*columns[0]);
        this->data(place).add(column.getData()[row_num]);
    }
  • IAggregateFunctionHelper接口
    這個接口是上面提到 IAggregateFunction的輔助子類接口,它很巧妙的通過模板的類型派生,將虛函數的調用轉換為函數指針的調用,這個在實際聚合函數的實現過程之中能夠大大提高計算的效率。
    函數addFree就實現了我上述所說的過程,但是它是一個private的函數,所以通常我們都是通過getAddressOfAddFunction獲取對應的函數地址。這在聚合查詢的過程之中能夠提高20%左右的執行效率。
template <typename Derived>
class IAggregateFunctionHelper : public IAggregateFunction
{
private:
    static void addFree(const IAggregateFunction * that, AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena)
    {
        static_cast<const Derived &>(*that).add(place, columns, row_num, arena);
    }

public:
    IAggregateFunctionHelper(const DataTypes & argument_types_, const Array & parameters_)
        : IAggregateFunction(argument_types_, parameters_) {}

    AddFunc getAddressOfAddFunction() const override { return &addFree; }
  • AggregateFunctionFactory類
    顧名思義,這個是一個生成聚合函數的工廠類。它的邏輯很簡單,所有ClickHouse之中所相關的聚合函數都是通過這個工廠類註冊並且獲取,然後進行調用的。
class AggregateFunctionFactory final : private boost::noncopyable, public IFactoryWithAliases<AggregateFunctionCreator>
{
public:

    static AggregateFunctionFactory & instance();

    /// Register a function by its name.
    /// No locking, you must register all functions before usage of get.
    void registerFunction(
        const String & name,
        Creator creator,
        CaseSensitiveness case_sensitiveness = CaseSensitive);

    /// Throws an exception if not found.
    AggregateFunctionPtr get(
        const String & name,
        const DataTypes & argument_types,
        const Array & parameters = {},
        int recursion_level = 0) const;

2.聚合函數的註冊流程

有了上述的背景知識,我們接下來舉個栗子。來看看一個聚合函數的實現細節,以及它是如何被使用的。

AggregateFunctionSum

筆者這裏選取了一個很簡單的聚合算子Sum,我們來看看它實現的代碼細節。
這裏我們可以看到AggregateFunctionSum是個final類,無法被繼承了。而它繼承了上面提到的IAggregateFunctionHelp類的子類IAggregateFunctionDataHelper類。

這裏我們就重點看,這個類override了getName方法,返回了對應的名字sum。並且實現了我們上文提到的四個核心的方法。

  • add
  • merge
  • seriable
  • deserialize
template <typename T, typename TResult, typename Data>
class AggregateFunctionSum final : public IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>
{
public:
    using ResultDataType = std::conditional_t<IsDecimalNumber<T>, DataTypeDecimal<TResult>, DataTypeNumber<TResult>>;
    using ColVecType = std::conditional_t<IsDecimalNumber<T>, ColumnDecimal<T>, ColumnVector<T>>;
    using ColVecResult = std::conditional_t<IsDecimalNumber<T>, ColumnDecimal<TResult>, ColumnVector<TResult>>;

    String getName() const override { return "sum"; }

    AggregateFunctionSum(const DataTypes & argument_types_)
        : IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>(argument_types_, {})
        , scale(0)
    {}

    AggregateFunctionSum(const IDataType & data_type, const DataTypes & argument_types_)
        : IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>(argument_types_, {})
        , scale(getDecimalScale(data_type))
    {}

    DataTypePtr getReturnType() const override
    {
        if constexpr (IsDecimalNumber<T>)
            return std::make_shared<ResultDataType>(ResultDataType::maxPrecision(), scale);
        else
            return std::make_shared<ResultDataType>();
    }

    void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena *) const override
    {
        const auto & column = static_cast<const ColVecType &>(*columns[0]);
        this->data(place).add(column.getData()[row_num]);
    }

    void merge(AggregateDataPtr place, ConstAggregateDataPtr rhs, Arena *) const override
    {
        this->data(place).merge(this->data(rhs));
    }

    void serialize(ConstAggregateDataPtr place, WriteBuffer & buf) const override
    {
        this->data(place).write(buf);
    }

    void deserialize(AggregateDataPtr place, ReadBuffer & buf, Arena *) const override
    {
        this->data(place).read(buf);
    }

    void insertResultInto(ConstAggregateDataPtr place, IColumn & to) const override
    {
        auto & column = static_cast<ColVecResult &>(to);
        column.getData().push_back(this->data(place).get());
    }

private:
    UInt32 scale;
};

接下來,ClickHouse實現了兩種聚合計算:AggregateFunctionSumData和AggregateFunctionSumKahanData。後者是用Kahan算法避免float類型精度損失的,我們可以暫時不細看。直接看SumData的實現。這是個模板類,之前我們講到AggregateFunction的函數就是通過AggregateDataPtr指針來獲取AggregateFunctionSumData的地址,來調用add實現聚合算子的。我們可以看到AggregateFunctionSumData實現了前文提到的add, merge, write,read四大方法,正好和接口一一對應上了。

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

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

template <typename T>
struct AggregateFunctionSumData
{
    T sum{};

    void add(T value)
    {
        sum += value;
    }

    void merge(const AggregateFunctionSumData & rhs)
    {
        sum += rhs.sum;
    }

    void write(WriteBuffer & buf) const
    {
        writeBinary(sum, buf);
    }

    void read(ReadBuffer & buf)
    {
        readBinary(sum, buf);
    }

    T get() const
    {
        return sum;
    }
};

ClickHouse在Server啟動時。main函數之中會調用registerAggregateFunction的初始化函數註冊所有的聚合函數。
然後調用到下面的函數:

void registerAggregateFunctionSum(AggregateFunctionFactory & factory)
{
    factory.registerFunction("sum", createAggregateFunctionSum<AggregateFunctionSumSimple>, AggregateFunctionFactory::CaseInsensitive);
    factory.registerFunction("sumWithOverflow", createAggregateFunctionSum<AggregateFunctionSumWithOverflow>);
    factory.registerFunction("sumKahan", createAggregateFunctionSum<AggregateFunctionSumKahan>);
}

這裏又調用了 factory.registerFunction("sum", createAggregateFunctionSum<AggregateFunctionSumSimple>, AggregateFunctionFactory::CaseInsensitive);來進行上述我們看到的聚合函數的註冊。這裡有一點很噁心的模板代碼,筆者這裏簡化了一下,把註冊的部分函數拉出來:

createAggregateFunctionSum(const std::string & name, const DataTypes & argument_types, const Array & parameters)
{
    AggregateFunctionPtr res;
    DataTypePtr data_type = argument_types[0];
    if (isDecimal(data_type))
        res.reset(createWithDecimalType<Function>(*data_type, *data_type, argument_types));
    else
        res.reset(createWithNumericType<Function>(*data_type, argument_types));
    return res;

這裏的Function模板就是上面的AggregateFunctionSumSimple, 而它又是下面的模板類型:

template <typename T> using AggregateFunctionSumSimple = typename SumSimple<T>::Function;

template <typename T>
struct SumSimple
{
    /// @note It uses slow Decimal128 (cause we need such a variant). sumWithOverflow is faster for Decimal32/64
    using ResultType = std::conditional_t<IsDecimalNumber<T>, Decimal128, NearestFieldType<T>>;
    using AggregateDataType = AggregateFunctionSumData<ResultType>;
    using Function = AggregateFunctionSum<T, ResultType, AggregateDataType>;
};

不知道讀者被繞暈了沒,最終繞回來還是new出來這個AggregateFunctionSum<T, ResultType, AggregateDataType>
也就是完成了這個求和算子的註冊,後續我們get出來就可以愉快的調用啦。(這裏這部分的模板變化比較複雜,如果看不明白可以回到源碼梳理一下~~~)

3. 小結

好了,關於聚合函數的基礎信息,和它是如何實現並且通過工廠方法註冊獲取的流程算是搞明白了。
關於其他的聚合算子,也是大同小異的方式。筆者就不再贅述了,感興趣的可以回到源碼之中繼續一探究竟。講完了聚合函數的實現,下一篇筆者就要繼續給探究聚合函數究竟在ClickHouse之中是如何和列存結合使用,並實現向量化的~~。
筆者是一個ClickHouse的初學者,對ClickHouse有興趣的同學,也歡迎和筆者多多指教,交流。

4. 參考資料

官方文檔
ClickHouse源代碼

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

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

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

最佳實踐之“全身而退”_如何寫文案

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

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

全身而退,是一種境界,更是一種智慧!編程,特別是框架設計,需要的就是哲學思維。“全身而退”這四個字,還在當年的內存泄露的踩坑過程中就悟出來了,有圖有真相:

    

有人會問,內存泄露和這個“全身而退”好像搭不上邊?從某些角度看也是,要不我不會單獨放在這裏講。但是也有一點關係,內存泄露不就是內存有分配沒有釋放,何時釋放,一般是程序退出之時。當年作為踩坑專業戶,踩過的坑遠遠不止前面有關“踩坑系列”系列文章提到的,可謂五花八門,也許可以套用一句話:“你們看到過的官場小說遠沒有我經歷過的精彩”:)大家可以看前面一系列踩坑經歷的總結。 

當時踩過很多崩潰的坑,時不時的崩潰,我形容我當時是救火隊長。而當時經過不少的踩坑分析有一個共同的現象,竟然都是退出的時候,就莫名其妙崩潰了。然後這個問題其實也容易重現,你不停的啟動和退出就行了,至少百分之五十的概率。只是因為運行的時間不同,退出崩潰的堆棧還不一樣。

正是這樣子,我們總結開玩笑說,就是經不起折騰。也造就了後面,做項目時,寫完一個類,一定會測試退出。整個系統分不同的時間點反覆重啟、反覆插拔網線。基本達到着魔的程度,不是?原來我控制欲這麼強:)事實也證明了,經過這樣反覆重啟的折騰,新產品落地投入市場,基本沒有這些亂七八糟的問題了!踩坑經驗(二)有截圖作證:

     

很久才會出現一個Bug,而且也可以輕鬆搞定。一般都是新加的功能,或者很特別的情況沒考慮到。

所以,也通過真實的項目驗證了,全身而退的重要性。當然現在和當年的心境也完全不一樣了!事實上,後面經過分佈式洗禮,我的這種方法是正確的。分佈式就是解決這種單點故障。當然分佈式單點故障和單機故障,是不一樣的概念或者說屬性。分佈式中是考慮不可抗拒的原因,集群中單節點的穩定性肯定是集群穩定性的基礎。只是這種測試方法可以用來測試分佈式的故障切換,甚至是故障恢復!集群考慮的是集群的全身而退,事實上,我們在測試過程中也出現過集群整體失效的時候。

那麼我們可以從哪些方面去做到全身而退呢?下面我從實踐經驗中總結以下幾點。 

一、單實例的使用

1.方式一

    

說到單實例,估計絕大部分人都說很簡單。如上的例子,一般的人會說沒問題。從邏輯來說確實沒問題,很簡單,不是嗎?實現類更簡單:

    

就一句話,靜態變量再申明一下。你是不是這樣實現的呢? 這是我們重構的新平台上,依然有這樣使用的。以前的平台那就更多了,基本上是全部。導致的結果就是上面描述的崩潰就是家常便飯。當時在部門範圍內做培訓的時候,我把這個作為考題,結果能指出問題點的少之又少。

2.方式二

後面的修改:

    

這是定義,我們再看看具體實現:

     

平時我對代碼是追求簡潔的,但是在這裏我反而增加了函數,是不是看上去複雜了?No!簡潔必須建立在遵循原理的基礎之上。那為什麼要這麼改,遵循的原理是什麼?

最基本的原理就是單實例首先是一個對象,而且是類的對象,不是簡單變量。那麼類就可能涉及深拷貝,涉及到new和delete,比基礎變量要複雜得多。如果像第一種方式,那麼就可能出現編譯期間開始進行內存分配等操作,而退出的時候也在等着編譯器系統的自動回收。在這種啟動和退出之時非常容易操作已經不存在的對象和內存,而出現了崩潰。

其實,第一種方式使用的是靜態變量,可返回的是引用。從內存的布局而言,屬於系統(內核)管理,無法做到應用層的控制,面對複雜情況的內存使用很難做到全身而退。關於程序的內存布局和分配可以查閱虛擬技術和內存泄露兩篇文檔。

所以基於以上兩點,將單實例定義為類的私有變量,指針形式。那麼只有在程序啟動后要獲取的時候才開始構建類對象,完全可以掌控在自己(程序的用戶態)的手中,而且可以非常靈活的加入調試代碼,比如捕獲異常等。在程序退出之時,亦可以很方便很合理的選擇何時釋放和銷毀對象。

當然,一般編碼規範的公司就會直接用第二種方式,其他人使用的時候直接copy改改就行。同樣的如果是“先輩們”使用了第一種,錯誤也會傳承下去。。。而且大多時候錯誤的東西還很難糾正過來,就說第一種方式竟然被發現在代碼重構后!

所以,編程的時候我們需要有懷疑的精神、反思的精神和專業的引導!

二、架構設計

1.模塊與接口

模塊和層級清晰,接口簡單易用。都有利於全身而退。我們新版的存儲系統中,核心業務有三塊,IO+rewrite,而僅僅只有一個寫模塊I和rewrite模塊之間的接口!讀模塊O和其他都是完全分開。

2.類的五大函數

你聽說的都是類的三大函數吧,我這裏咋變成了五大函數,而且我的五大不是包括原來的三大。

2.1 構造函數和析構函數

只是單純的對成員變量進行賦值。

2.2 初始化函數和反初始化函數

例如內存的分配和釋放,多線程的創建啟動和停止。都應該放在這裏面,而不是上面提到的構造和析構中。在內存泄露中也提到過。其實只有一個目的,好控制!

2.3 複位函數

有異常情況下,不是被動退出,而是能夠主動控制,最壞的情況是複位。將各種變量、狀態、內存和多線程恢復到初始狀態。有的人可能會說,那怎麼不直接用初始化函數呢?這兩種功能還是不一樣的。初始化是系統啟動的時候用的。而複位可能承載更多的判斷,例如打印一些重要變量的信息,以利於開發人員跟蹤異常情況。

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

這五大函數是我建立一個類最基本的模板,也是做到全身而退基本的保障。

一個類要盡量做到職責單一,其實一個函數也是類似的。例如說初始化函數不能省,很多人將初始化和構造放一起,或者複位和初始化放一起。

析構函數只是簡單的初始化變量而已,而例如內存的分配、線程和定時器的啟動最好都到初始化函數中;

假如採用單例模式,則只需用到構造函數,像上面提到的單實例模式用的不好,如果構造函數中包括太多內存和多線程的使用,對退出將是一個災難。

而從另一個角度,虛擬內存的原理,內存只有分配和使用的時候才開始產生缺頁中斷即才開始尋找物理內存。所以何時初始化,何時使用內存可以掌握在開發人員的手中,而不是依賴系統來控制。

所以我認為這也是C++相比JAVA來說更好的優勢,可能有的人認為C++就是很難控制,像內存、指針、繼承,而且還沒有GC機制等等。而我認為剛剛相反,例如指針就是一把雙刃劍,你懂它,它就威力無窮!你看很多開源庫,比如經典的MySQL、高效的Redis、一直很火的Nginx,是吧,指針發揮了太大太大的作用,你能說這些庫不穩定嗎?雖然說我也一直關注和學習Java和Spring,主要學習它們的抽象思想!

3.鎖的設計

細心的你可以發現,上面單實例使用的第二種方式,加了一把鎖。我一般的類設計中,鎖的數量是2~3,大多情況下是2。單實例模式使用一把鎖,狀態機用一把鎖。在死鎖的踩坑經驗總結中曾經提到過,鎖的使用個數少,範圍少。所以你可以回頭看看狀態機和單實例模式鎖的使用。該加的時候一定加鎖,該不要加的時候一定不能加,不能模糊。

鎖或者說多線程同步使用不當,是很容易出問題的,不僅是運行過程中可能出現死鎖,退出的時候也可能出現,可能的情況就是退出失敗,同樣需要手動進行殺進程。

4.時序

前面的函數、接口、類、內存、多線程與鎖都設計好了,對於全身而退,或許已經成功了一半。還有一半在哪裡,時序。就是將這些對象和接口如何串起來,如果做到有序。最常見的是退出和啟動的順序即時序是相反的,這是基本的要求,而對於複雜的情況需要分兩方面來討論。

一方面,每個類、每個模塊都要做到全身而退,即每個類和模塊都需要有序的退出。所以上面類的設計,五大函數最終是為了全身而退,為了有序的釋放內存、停止線程等。我們上文提到的有限狀態機最終也是為了更好的控制線程、為了線程的退出,鎖的設計同樣可以減少干擾提高效率。所以系統的全身而退主要依賴於每個類、每個模塊的全身而退。

另一方面,程序最後的退出其實也是要封裝和集中在一兩個函數中,不能太散。即層級關係要非常明確,類似於樹的結構,最後集中到根節點,也就是一個函數。所以層級設計的目的也應該是,越到外層越好控制!

以上提到的幾點,包括時序,包括單實例模式,也包括最近一系列踩坑經歷,我最後都會歸納到框架或設計,所以也可以說這一切都是為了最終的全身而退。

總之,能隨時隨地的全身而退的代碼是一種境界,經得起各種折騰的代碼才是好代碼,經得起各種折騰的設計才是好的設計!

  

最後,萬事萬物的發展都離不開一個簡單的道理,例如讀一本書: 

從簡單,到複雜,最後到簡單。但這兩個簡單的心境完全不一樣,前者我認為更多的是無知者無畏,後者則是一切瞭然於心。 

 

計算機最終歸結於二進制的運算,最終就是01的組合,組合成各種數據結構和協議,組合成各種調度和算法,等等。

而對應於中國智慧,就是道,抓住事物的本質,以不變應萬變,陰陽論,太極八卦論;對應於中國的萬事開頭難,和全身而退!

也可以對應軟件兩大難點,就是原理的把握和框架的設計。

有了對原理的理解,能抓住事物的本質,就會下筆如有神助,也能最終全身而退!

當然你要達到這一點,得不停的”悟”!

悟的速度依賴”實踐+思考”!

 

肯定不能只依賴CRUD,也不能僅僅依賴改Bug的多少、僅僅依賴參与項目的多少。

有的改10個Bug比不上別人一個Bug,有的參与了10個項目不如比別人開發一個項目。

還是那句話,如果不讀書,行萬里路,也只是一個郵差。

所以,我認為多去“實踐+思考”。

 

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

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

如何在Spring Boot應用啟動之後立刻執行一段邏輯_網頁設計公司

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

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

1. 前言

不知道你有沒有接到這種需求,項目啟動后立馬執行一些邏輯。比如簡單的緩存預熱,或者上線后的廣播之類等等。如果你使用 Spring Boot 框架的話就可以藉助其提供的接口CommandLineRunner和 ApplicationRunner來實現。

2. CommandLineRunner

org.springframework.boot.CommandLineRunner 是Spring Boot提供的一個接口,當你實現該接口並將之注入Spring IoC容器后,Spring Boot應用啟動后就會執行其run方法。一個Spring Boot可以存在多個CommandLineRunner的實現,當存在多個時,你可以實現Ordered接口控制這些實現的執行順序(Order 數值越大優先級越低)。接下來我們來聲明兩個實現並指定順序:

優先執行:

package cn.felord;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * 優先級最高
 * 該類期望在springboot 啟動后第一順位執行
 * @author felord.cn
 * @since 12:57
 **/
@Slf4j
@Component
public class HighOrderCommandLineRunner implements CommandLineRunner, Ordered {
    @Override
    public void run(String... args) throws Exception {
        for (String arg : args) {
            log.info("arg = " + arg);
        }
        log.info("i am highOrderRunner");
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE+1;
    }
}

第二順序執行:

package cn.felord;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * 優先級低於{@code HighOrderCommandLineRunner}
 * @author felord.cn
 * @since 12:59
 **/
@Slf4j
@Component
public class LowOrderCommandLineRunner implements CommandLineRunner, Ordered {

    @Override
    public void run(String... args) throws Exception {
        log.info("i am lowOrderRunner");
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE+1;
    }
}

然後啟動Spring Boot應用后,控制台按照預定的順序打印出了結果:

2020-05-30 23:11:03.685  INFO 11976 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-30 23:11:03.701  INFO 11976 --- [           main] c.f.Application  : Started SpringBootApplication in 4.272 seconds (JVM running for 6.316)
2020-05-30 23:11:03.706  INFO 11976 --- [           main] c.f.HighOrderCommandLineRunner   : i am highOrderRunner
2020-05-30 23:11:03.706  INFO 11976 --- [           main] c.f.LowOrderCommandLineRunner   : i am lowOrderRunner

3. ApplicationRunner

在Spring Boot 1.3.0又引入了一個和CommandLineRunner功能一樣的接口ApplicationRunner。CommandLineRunner接收可變參數String... args,而ApplicationRunner 接收一個封裝好的對象參數ApplicationArguments。除此之外它們功能完全一樣,甚至連方法名都一樣。 聲明一個ApplicationRunner並讓它優先級最低:

package cn.felord;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * 優先級最低
 * @author felord.cn
 * @since 13:00
 **/
@Slf4j
@Component
public class DefaultApplicationRunner implements ApplicationRunner, Ordered {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("i am applicationRunner");
        Set<String> optionNames = args.getOptionNames();
        log.info("optionNames = " + optionNames);
        String[] sourceArgs = args.getSourceArgs();
        log.info("sourceArgs = " + Arrays.toString(sourceArgs));
        List<String> nonOptionArgs = args.getNonOptionArgs();
        log.info("nonOptionArgs = " + nonOptionArgs);
        List<String> optionValues = args.getOptionValues("foo");
        log.info("optionValues = " + optionValues);
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE+2;
    }
}

按照順序打印了三個類的執行結果:

2020-06-01 13:02:39.420  INFO 19032 --- [           main] c.f.MybatisResultmapApplication  : Started MybatisResultmapApplication in 1.801 seconds (JVM running for 2.266)
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.HighOrderCommandLineRunner   : i am highOrderRunner
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.LowOrderCommandLineRunner    : i am lowOrderRunner
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.DefaultApplicationRunner     : i am applicationRunner
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.DefaultApplicationRunner     : optionNames = []
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.DefaultApplicationRunner     : sourceArgs = []
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.DefaultApplicationRunner     : nonOptionArgs = []
2020-06-01 13:02:39.423  INFO 19032 --- [           main] c.f.DefaultApplicationRunner     : optionValues = null

Ordered接口並不能被 @Order註解所代替。

4. 傳遞參數

相信很多同學看到這裏都開始對這兩個run方法的入參感興趣了。Spring Boot應用啟動時是可以接受參數的,換句話說也就是Spring Boot的main方法是可以接受參數的。這些參數通過命令行 java -jar yourapp.jar 來傳遞。CommandLineRunner會原封不動照單全收這些接口,這些參數也可以封裝到ApplicationArguments對象中供ApplicationRunner調用。 我們來認識一下ApplicationArguments的相關方法:

  • getSourceArgs() 被傳遞給應用程序的原始參數,返回這些參數的字符串數組。

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

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

  • getOptionNames() 獲取選項名稱的Set字符串集合。如 --spring.profiles.active=dev --debug 將返回["spring.profiles.active","debug"] 。

  • getOptionValues(String name) 通過名稱來獲取該名稱對應的選項值。如--foo=bar --foo=baz 將返回["bar","baz"]。

  • containsOption(String name) 用來判斷是否包含某個選項的名稱。

  • getNonOptionArgs() 用來獲取所有的無選項參數。

    接下來我們試驗一波,你可以通過下面的命令運行一個 Spring Boot應用 Jar

java -jar yourapp.jar --foo=bar --foo=baz --dev.name=碼農小胖哥 java felordcn

或者在IDEA開發工具中打開Spring Boot應用main方法的配置項,進行如下配置,其他IDE工具同理。

運行Spring Boot應用,將會打印出:

2020-06-01 15:04:31.490  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : arg = --foo=bar
2020-06-01 15:04:31.490  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : arg = --foo=baz
2020-06-01 15:04:31.490  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : arg = --dev.name=碼農小胖哥
2020-06-01 15:04:31.490  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : arg = java
2020-06-01 15:04:31.490  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : arg = felordcn
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.HighOrderCommandLineRunner   : i am highOrderRunner
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.LowOrderCommandLineRunner    : i am lowOrderRunner
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.DefaultApplicationRunner     : i am applicationRunner
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.DefaultApplicationRunner     : optionNames = [dev.name, foo]
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.DefaultApplicationRunner     : sourceArgs = [--foo=bar, --foo=baz, --dev.name=碼農小胖哥, java, felordcn]
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.DefaultApplicationRunner     : nonOptionArgs = [java, felordcn]
2020-06-01 15:04:31.491  INFO 13208 --- [           main] c.f.DefaultApplicationRunner     : optionValues = [bar, baz]

然後你就可以根據實際需要動態地執行一些邏輯。

5. 總結

今天我們對CommandLineRunner和ApplicationRunner進行了講解,從用法到順序執行,又對Spring Boot傳遞參數進行了介紹和演示,希望對你有所幫助。多多關注:碼農小胖哥,更多編程乾貨分享給你。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

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

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

搭建Prometheus平台,你必須考慮的6個因素_網頁設計

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

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

作者簡介

Loris Degioanni,Sysdig的創始人和CTO,同時還是容器安全工具Falco的創建者。

原文鏈接
https://thenewstack.io/6-things-to-consider-in-a-prometheus-monitoring-platform/

本文轉自Rancher Labs

當前,Prometheus被許多企業和組織廣泛使用,以監控其容器和微服務。但是在這一過程中,大型公司通常會陷入困境:當應用程序數量越來越多的時候,擴展監控指標則是一個十分重大的挑戰。

不斷增長的容器使情況複雜化

相對來說,監控單體環境常常更簡單,因為靜態物理服務器和虛擬機數量是確定的,並且監控指標的數量也是有限的。但是,如今由於容器以及需要向微服務架構遷移,要跟蹤監控的實例程序數量激增。

如果說位於數據中心的服務器是寵物,需要我們不斷關注的話,那麼雲實例則更像牛(因為有很多,你不必關心單個實例),而容器則更像小蜜蜂。它們數量很多,有時每台機器有數百個容器,並且新的容器一直不斷出現,當與諸如Kubernetes的容器編排引擎一起使用時,它們的壽命可能非常短。這使得跟蹤監控它們變得更加困難,而且如果你不小心誤操作的話,它們可能會造成很多損害。

隨着複雜性和分佈式環境的增加,你需要監控的實體數量也在增加。此外,你可能希望監控更多屬性以確保你對正在發生的事情有準確的了解,或者在進行故障排除或事件響應的情況下,可以了解正在發生的事情。在短暫的環境中,後者尤其成問題,因為當你想了解問題的根本原因時,通常相關的資源已經停用,這意味着監控解決方案必須提供一種能夠存儲足夠的歷史記錄以進行取證的方法。

流行的監控工具:Prometheus

越來越多需要雲監控的團隊正在轉向Prometheus,這是一個開源的CNCF項目。Prometheus已成為開發人員用來在雲原生環境中收集和理解指標的首選監控工具。它由一個大型社區支持,有來自700多家公司的6300個貢獻者,有13500個代碼提交和7200個拉取請求。

默認情況下,典型的雲原生應用程序堆棧(如Kubernetes、Ngnix、MongoDB、Kafka、golang等)會暴露Prometheus指標。Prometheus是一個可以垂直彈性伸縮的Go程序,為單個容器或單個主機部署它時十分容易。換言之,一開始使用Prometheus極為容易,你可以輕鬆監控你的第一個Kubernetes集群,但是這也意味着隨着基礎架構的增長,監控會越來越複雜。

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

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

應用程序增長帶來的擴展問題

隨着環境規模增長,你需要跟蹤監控飛速增長的時間序列數據,並且在數據量達到某個點之後,單個Prometheus實例無法繼續跟蹤監控。這一情況下,最直接的選擇是在整個企業中運行一組Prometheus服務器,但這帶來了一些挑戰。例如,跨數十甚至數百台Prometheus服務器管理和合併數據並不容易。同樣,了解企業工作流程、單點登錄、基於角色的訪問控制以及遵守SLA或合規性也不是容易的問題。隨着應用程序的增長,在不中斷開發人員工作的情況下運行一個全方位的監控解決方案,這將成為一個可管理性和可靠性的問題。

為了解決這一問題,企業採用了許多方法。

簡單的方法是為每個命名空間或每個集群都準備一個單獨的Prometheus服務器。這種方法到一定規模就會難以為繼,此外,它還有一個缺點,那就是會造成大量的斷開的數據孤島。這會使故障排查變得很麻煩,因為大多數問題會跨越多個服務/團隊/集群。不但在每個環境中很難找到相同的指標,你還需要把數據拼接在一起,以試圖了解發生了什麼。

另一個常見方法是使用類似Cortex或Thanos的開源工具來集合多個Prometheus服務器。這些高效的工具可以讓你集中查詢服務器、收集數據然後在統一的dashboard中共享。然而,與任何數據密集型分佈式系統一樣,它們需要大量的技能和資源才能運行。

需要考慮的6個因素

對於那些以Prometheus為起點,然後尋求商業化解決方案以獲得全局監控的公司來說,重要的是,不丟失Prometheus上完成的所有標準化開發工作——dashboard、告警、exporter等。然而,這不是需要考慮的唯一事情,如果你繼續使用Prometheus,需要堅持以下標準:

1、 兼容性,以支持所有Prometheus功能

你的供應商/所使用的工具/SaaS解決方案需要能夠使用任何可產生Prometheus指標的實體程序中消耗數據,無論是本地Kubernetes還是雲服務。相對來說,消耗Prometheus指標微不足道,但是也不要忽略一些小事情,例如將指標提取到存儲中或增加數據時能夠重新標註指標,這樣對你的環境更有意義。這些小事加起來,能夠收集到的數據將會堆積如山、大不相同。

2、 PromQL兼容性

Prometheus查詢語言由Prometheus創建者發明,用於提取存儲在Prometheus中的信息。PromQL能讓你查詢指定服務或指定用戶的指標,它還能匯總或細分數據。例如,你可以使用它显示所有容器中每個應用的CPU使用率。或者僅显示Cassandra容器的數據,並將其显示為每個集群的單個值。可以說,PromQL釋放了Prometheus的真正價值,因此如果將Prometheus的指標集成到一個不完全支持PromQL的產品中,就完全違背了使用Prometheus的初衷。

3、 支持熱插拔

要真正與Prometheus兼容,該解決方案必須能夠支持熱插拔,以便能夠與你現有的dashboard、告警和腳本一起使用。例如,許多使用Prometheus的企業都將Grafana用於dashboard。這個開源工具能夠與Prometheus很好地集成在一起,包括在查詢級別,並且可以用於生成一系列有用的圖表和dashboard。因此,聲稱與Prometheus兼容的商業產品應與Grafana等工具兼容。僅僅說解決方案可以讓你在Grafana中查看数字是遠遠不夠的,你需要能夠按照原樣提取現有的Grafana dashboard,並將它們重新應用於商業解決方案中已安裝的數據。

4、 訪問控制

在評估工具時,訪問控制是另一個你需要考慮的安全問題。能夠使用行業標準協議(包括LDAP、Google Oauth、SAML和OpenID)保護用戶身份驗證,使公司能夠通過基於服務的訪問控制來隔離和保護資源。

5、 故障排查

Kubernetes簡化了部署、彈性伸縮和管理容器化應用程序和微服務。這有助於保持服務的正常運行,但是要識別和解決諸如性能降低、部署失敗和連接錯誤之類的根本問題,你需要能夠從整個環境中收集和可視化基礎架構、應用程序和性能數據。由於無法同時訪問實時信息和上下文數據,因此幾乎不可能關聯環境中的指標,所以你可以更快地解決問題。

6、 與現有告警兼容

最後,如果你正在尋找商業解決方案來幫助解決Prometheus可擴展性問題,請確保它支持所有級別的告警。能夠實現這一目標的關鍵是全面支持Alert Manager功能,而Alert Manager還要求100%的集成和 PromQL兼容性。

如果你找到一個能夠滿足以上標準的商業化工具,你應該能夠輕鬆將其集成到現有的Prometheus中,並且能夠避免公司遇到的可擴展性問題。開發人員有充分的理由喜愛Prometheus,因此在採用商業化方案之前進行全面、盡職的調查將確保他們仍然可以使用自己喜歡的指標。

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

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

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

六一兒童節,程序員寫給女兒的一封信_貨運

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

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

今天是六一兒童節,不想寫技術了,就寫點別的吧——給四歲的女兒寫封信。我的讀者群體里大部分都是大學生或者初入職場的新人,一時半會是體會不到做父母的辛酸和樂趣。

但我希望我的故事能給讀者朋友們枯燥的生活增添一丟丟樂趣,這就足夠了,對吧?這也是我一直以來的心愿。

親愛的女兒,你好呀。

我是你的爸爸馬偉青(有些讀者叫我二哥、有些叫王老師、有些叫馬老師、有些叫青哥,我統統接受,一個也不挑),等你能看懂這封信的時候,你已經可以自由地閱讀了。大量的閱讀,對人的一生至關重要,所以咱們家最多的就是書本了——這也是我一直引以為傲的。

媽媽也喜歡給你買書,我覺得她給你買的書都質量蠻高的,比如說甜心英語、長頸鹿卡密爾、馬努和米娜、巧虎、窗邊的小豆豆等等,這些書都是成套的,爸爸讀起來都覺得挺有意思,我想你也一定非常喜歡。

因為你讀書的樣子真的非常專註,非常可愛。

記得你剛從媽媽肚子里剖出來的時候,外婆抱着你,對爸爸說了一句夠驕傲一輩子的話:“長得真像你啊。”不是爸爸要和你媽媽爭寵,而是你的確是爸爸媽媽愛情的結晶,你的到來,豐富了我們的生活和情感,更重要的是,賦予了爸爸媽媽神聖的職責。

如果沒有你,爸爸在業餘時間里,可能還沉浸在 Dota、火炬之光、三國群英傳 7 的遊戲中。想想都覺得無比的可怕,打遊戲沒有錯,但爸爸應該去做一些更积極向上、更富有意義的事情,比如說讀書和寫作。

我比你姑姑大 11 歲,所以她小的時候,爸爸帶的非常多。就連上了小學,爸爸去哪裡她都要跟到哪裡,包括和你媽媽約會的時候。

也許是這樣一份特殊的經歷,爸爸在帶你的過程中顯然比你媽媽更專業一些,就連拍飽嗝(防止你奶吃多了吐)這種高級別的手法爸爸都會。由於媽媽工作上的限制,爸爸總體上帶你的時間確實要比媽媽多一些,陪你去上金寶貝早教課、上兔加熊體能課、上樂高積木、上愛貝英語,包括在小區裏面和小朋友玩,以及日常護理,洗澡洗頭髮,爸爸都能得心應手。

一開始,爸爸也是放不開的,畢竟帶孩子這件事,女性的佔比要比男性的佔比多得多,站在女人堆里,有時候真的是舉足無措啊。

但爸爸硬是挺了過來。關鍵是,還兼顧了家務、工作和寫作,想想都覺得自己挺牛逼的,我想你也這麼覺得吧?

爸爸和媽媽畢竟有一些不同,有的時候會嚴厲一些,所以你從來不鬧爸爸,你的這份克制多多少少會讓爸爸覺得內疚,但我想,對你的性格,對你以後的為人處事會有一些幫助,畢竟社會有美好的一面,也有殘忍的一面。

你長這麼大,只有一件事,讓爸爸至今懷恨在心,就是在你滿月之前,剪手指甲的時候把你手指頭上的肉剪掉了一小塊。鮮紅的血液流出來了你才後知后覺地哭了起來,可把爸爸嚇壞了。從此以後,再沒敢替你剪過指甲。

好了,過往就先煽情到這裏。接下來,爸爸寫一下對你的期許,或許不叫期許,更應該叫做祝福。無論你成為怎樣的你,爸爸永遠都是你背後最強有力的支柱。

第一,爸爸媽媽會努力給你力所能及最好的教育條件。但是,爸爸不希望學校過早的壓榨你的潛力,如果作業真的多到沒有時間去玩,爸爸寧願你不寫,家長會挨批的話,爸爸不嫌丟臉。

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

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

在爸爸看來,學習固然重要,但如果是通過寫不完的作業來達成的話,我認為是有問題的。爸爸來自於農村,所以讀書上學是唯一改變命運的出路,但你不同,你可以放心大膽地去做更多你喜歡做的事情,不用去承擔後果,以及背負巨大的壓力。

第二,在上學的年紀,一定要多多讀書,你喜歡讀什麼樣的書,爸爸都會給你買,沒有任何限制。

爸爸在上初中的時候,讀書的興趣是最濃烈,最純粹的,但那時候學校不允許讀任何課外書,真的是抹殺了爸爸求知的天性。

人最怕的除了窮,就是無知了。“窮”這個難題,爸爸媽媽會努力幫你解決,但“無知”這個難題,就需要你自己去完成了。而書,是解決這個難題最強有力的武器。

第三,英語非常非常重要,了解我們自己的文化固然重要,吸取別人的長處同樣重要。如果你精通英語的話,周遊世界,探索你感興趣的領域就會容易得多,這是不爭的事實。

爸爸就時常後悔沒有把英語學到極致,否則爸爸會在編程領域更上一層樓的。但爸爸沒有放棄,仍然在盡最大的努力改善中。你現在已經認識四五十個單詞了,這在爸爸看來,太棒了。

第四,女孩子一定要學會撒嬌和賣萌,這也是爸爸從小一直教你的,遇到一些問題時,哭鬧不是最好的解決辦法,變通才是最好的。

你可以像爸爸一樣,做一個情緒化的人,該哭的時候哭,該笑的時候笑,不要做女強人,學會與朋友分享你的痛苦和快樂。

第五,女孩子一定要學會化妝和打扮,把自己收拾的美美的,衣着得體,糟糕的情緒也會逃之夭夭。

體能鍛煉也是必不可少的,一方面是為了保持身體健康,塑造完美體形,另一方面,能夠幫助你養成良好的生活習慣,以及堅強的意志力。

第六,做你自己,做好你自己。不要太計較別人的眼光,把自己活得快活一點,樂觀一點、瀟洒一點。

如果以後爸爸媽媽的觀點和你不一致,不要聽我們的,聽從你自己內心最真實的那個聲音。你雖然是我們的女兒,但生命是你自己的,由你負責。但不管怎樣,爸爸和媽媽,都會做你最堅強的後盾,永遠支持你和愛護你。

就寫這麼多吧。Peace。

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻。

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

※回頭車貨運收費標準

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

深入了解ConcurrentHashMap_網頁設計公司

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

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

在上一篇文章【簡單了解系列】從基礎的使用來深挖HashMap里,我從最基礎的使用中介紹了HashMap,大致是JDK1.7和1.8中底層實現的變化,和介紹了為什麼在多線程下可能會造成死循環,擴容機制是什麼樣的。感興趣的可以先看看。

我們知道,HashMap是非線程安全的容器,那麼為什麼ConcurrentHashMap能夠做到線程安全呢?

底層結構

首先看一下ConcurrentHashMap的底層數據結構,在Java8中,其底層的實現方式與HashMap一樣的,同樣是數組、鏈表再加紅黑樹,具體的可以參考上面的HashMap的文章,下面所有的討論都是基於Java 1.8。

transient volatile Node<K,V>[] table;

volatile關鍵字

對比HashMap的底層結構可以發現,table的定義中多了一個volatile關鍵字。這個關鍵字是做什麼的呢?我們知道所有的共享變量都存在主內存中,就像table。

而線程對變量的所有操作都必須在線程自己的工作內存中完成,而不能直接讀取主存中的變量,這是JMM的規定。所以每個線程都會有自己的工作內存,工作內存中存放了共享變量的副本。而正是因為這樣,才造成了可見性的問題。

ABCD四個線程同時在操作一個共享變量X,此時如果A從主存中讀取了X,改變了值,並且寫回了內存。那麼BCD線程所得到的X副本就已經失效了。此時如果沒有被volatile修飾,那麼BCD線程是不知道自己的變量副本已經失效了。繼續使用這個變量就會造成數據不一致的問題。

內存可見性

而如果加上了volatile關鍵字,BCD線程就會立馬看到最新的值,這就是內存可見性。你可能想問,憑什麼加了volatile的關鍵字就可以保證共享變量的內存可見性?

那是因為如果變量被volatile修飾,在線程進行寫操作時,會直接將新的值寫入到主存中,而不是線程的工作內存中;而在讀操作時,會直接從主存中讀取,而不是線程的工作內存。

基礎使用

首先這個使用與HashMap沒有任何區別,只是實現改成了ConcurrentHashMap。

Map<String, String> map = new ConcurrentHashMap<>();
map.put("微信搜索", "SH的全棧筆記");
map.get("微信搜索"); // SH的全棧筆記

取值

首先我們來看一下get方法的使用,源碼如下。

public V get(Object key) {
  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  int h = spread(key.hashCode());
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (e = tabAt(tab, (n - 1) & h)) != null) {
    if ((eh = e.hash) == h) {
      if ((ek = e.key) == key || (ek != null && key.equals(ek)))
        return e.val;
    }
    else if (eh < 0)
      return (p = e.find(h, key)) != null ? p.val : null;
    while ((e = e.next) != null) {
      if (e.hash == h &&
          ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
    }
  }
  return null;
}

大概解釋一下這個過程發生了什麼,首先根據key計算出哈希值,如果找到了就直接返回值。如果是紅黑樹的話,就在紅黑樹中查找值,否則就按照鏈表的查找方式查找。

這與HashMap也差不多的,元素會首先以鏈表的方式進行存儲,如果該桶中的元素數量大於TREEIFY_THRESHOLD的值,就會觸發樹化。將當前的鏈錶轉換為紅黑樹。因為如果數量太多的話,鏈表的查詢效率就會變得非常低,時間複雜度為O(n),而紅黑樹的查詢時間複雜度則為O(logn),這個閾值在Java 1.8中的默認值為8,定義如下。

static final int TREEIFY_THRESHOLD = 8;

賦值

put的源碼就不放出來了,放在這大家估計也不會一行一行的去看。所以我就簡單的解釋一下put的過程發生了什麼事,並貼上關鍵代碼就好了。

整個過程,除開併發的一些細節,大致的流程和1.8中的HashMap是差不多的。

  • 首先會根據傳入的key計算出hashcode,如果是第一次被賦值,那自然需要進行初始化table
  • 如果這個key沒有存在過,直接用CAS在當前槽位的頭節點創建一個Node,會用自旋來保證成功
  • 如果當前的Node的hashcode是否等於-1,如果是則證明有其它的線程正在執行擴容操作,當前線程就加入到擴容的操作中去
  • 且如果該槽位(也就是桶)上的數據結構如果是鏈表,則按照鏈表的插入方式,直接接在當前的鏈表的後面。如果數量大於了樹化的閾值就會轉為紅黑樹。
  • 如果這個key存在,就會直接覆蓋。
  • 判斷是否需要擴容

看到這你可能會有一堆的疑問。

例如在多線程的情況下,幾個線程同時來執行put操作時,怎麼保證只執行一次初始化,或者怎麼保證只執行一次擴容呢?萬一我已經寫入了數據,另一個線程又初始化了一遍,豈不是造成了數據不一致的問題。同樣是多線程的情況下, 怎麼保證put值的時候不會被其他線程覆蓋。CAS又是什麼?

接下來我們就來看一下在多線程的情況下,ConcurrentHashMap是如何保證線程安全的。

初始化的線程安全

首先我們來看初始化的源碼。

private final Node<K,V>[] initTable() {
  Node<K,V>[] tab; int sc;
  while ((tab = table) == null || tab.length == 0) {
    if ((sc = sizeCtl) < 0)
      Thread.yield(); // lost initialization race; just spin
    else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
      try {
        if ((tab = table) == null || tab.length == 0) {
          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
          @SuppressWarnings("unchecked")
          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
          table = tab = nt;
          sc = n - (n >>> 2);
        }
      } finally {
        sizeCtl = sc;
      }
      break;
    }
  }
  return tab;
}

可以看到有一個關鍵的變量,sizeCtl,其定義如下。

private transient volatile int sizeCtl;

sizeCtl使用了關鍵字volatile修飾,說明這是一個多線程的共享變量,可以看到如果是首次初始化,第一個判斷條件if ((sc = sizeCtl) < 0)是不會滿足的,正常初始化的話sizeCtl的值為0,初始化設定了size的話sizeCtl的值會等於傳入的size,而這兩個值始終是大於0的。

CAS

然後就會進入下面的U.compareAndSwapInt(this, SIZECTL, sc, -1)方法,這就是上面提到的CAS,Compare and Swap(Set),比較並交換,Unsafe是位於sun.misc下的一個類,在Java底層用的比較多,它讓Java擁有了類似C語言一樣直接操作內存空間的能力。

例如可以操作內存、CAS、內存屏障、線程調度等等,但是如果Unsafe類不能被正確使用,就會使程序變的不安全,所以不建議程序直接使用它。

compareAndSwapInt的四個參數分別是,實例、偏移地址、預期值、新值。偏移地址可以快速幫我們在實例中定位到我們要修改的字段,此例中便是sizeCtl。如果內存當中的sizeCtl是傳入的預期值,則將其更新為新的值。這個Unsafe類的方法可以保證這個操作的原子性。當你在使用parallelStream進行併發的foreach遍歷時,如果涉及到修改一個整型的共享變量時,你肯定不能直接用i++,因為在多線程下,i++每次操作不能保證原子性。所以你可能會用到如下的方式。

AtomicInteger num = new AtomicInteger();
arr.parallelStream().forEach(item -> num.getAndIncrement());

你可能會好奇,為什麼使用了AtomicInteger就可以保證原子性,跟Unsafe類和CAS又有什麼關係,讓我們接着往下,看getAndIncrement方法的底層實現。

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看到,底層調用的是Unsafe類的方法,這不就聯繫上了嗎,而getAndIncrement的實現又長這樣。

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

沒錯,這裏底層調用了compareAndSwapInt方法。可以看到這裏加了while,如果該方法返回false就一直循環,直到成功為止。這個過程有個的名字,叫自旋。特別高端啊,說人話就是無限循環。

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

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

什麼情況會返回false呢?那就是var5變量存儲的值,和現在內存中實際var5的值不同,說明這個變量已經被其他線程修改過了,此時通過自旋來重新獲取,直到成功為止,然後自旋結束。

結論

聊的稍微有點多,這小節的問題是如何保證不重複初始化。那就是執行首次擴容時,會將變量sizeCtl設置為-1,因為其被volatile修飾,所以其值的修改對其他線程可見。

其它線程再調用初始化時,就會發現sizeCtl的值為-1,說明已經有線程正在執行初始化的操作了,就會執行Thread.yield(),然後退出。

yield相信大家都不陌生,和sleep不同,sleep可以讓線程進入阻塞狀態,且可以指定阻塞的時間,同時釋放CPU資源。而yield不會讓線程進入阻塞狀態,而且也不能指定時間,它讓線程重新進入可執行狀態,讓出CPU調度,讓CPU資源被同優先級或者高優先級的線程使用,稍後再進行嘗試,這個時間依賴於當前CPU的時間片劃分。

如何保證值不被覆蓋

我們在上一節舉了在併發下i++的例子,說在併發下i++並不是一個具有原子性的操作,假設此時i=1,線程A和線程B同時取了i的值,同時+1,然後此時又同時的寫回。那麼此時i++的值會是2而不是3,在併發下1+1+1=2是可能出現的。

讓我們來看一下ConcurrentHashMap在目標key已經存在時的賦值操作,因為如果不存在會直接調用Unsafe的方法創建一個Node,所以後續的線程就會進入到下面的邏輯中來,由於太長,我省略了一些代碼。

......
V oldVal = null;
synchronized (f) {
  if (tabAt(tab, i) == f) {
    if (fh >= 0) {
      binCount = 1;
      for (Node<K,V> e = f;; ++binCount) {
        ......
      }
    }
    else if (f instanceof TreeBin) {
      Node<K,V> p;
      binCount = 2;
      if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
        oldVal = p.val;
        if (!onlyIfAbsent)
          p.val = value;
      }
    }
  }
}
if (binCount != 0) {
  if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);
  if (oldVal != null)
    return oldVal;
  break;
}

上述代碼在賦值的邏輯外層包了一個synchronized,這個有什麼用呢?

synchronized關鍵字

這個地方也可以換一個方式來理解,那就是synchronized如何保證線程安全的。線程安全,我認為更多的是描述一種風險。在堆內存中的數據由於可以被任何線程訪問到,在沒有任何限制的情況下存在被意外修改的風險。

而synchronized是通過對共享資源加鎖的方式,使同一時間只能有一個線程能夠訪問到臨界區(也就是共享資源),共享資源包括了方法、鎖代碼塊和對象。

那是不是使用了synchronized就一定能保證線程安全呢?不是的,如果不能正確的使用,很可能就會引發死鎖,所以,保證線程安全的前提是正確的使用synchronized。

自動擴容的線程安全

除了初始化、併發的寫入值,還有一個問題值得關注,那就是在多線程下,ConcurrentHashMap是如何保證自動擴容是線程安全的。

擴容的關鍵方案是transfer,但是由於代碼太多了,貼在這個地方可能會影響大家的理解,感興趣的可以自己的看一下。

還是大概說一下自動擴容的過程,我們以一個線程來舉例子。在putVal的最後一步,會調用addCount方法,然後在方法里判讀是否需要擴容,如果容量超過了實際容量 * 負載因子(也就是sizeCtl的值)就會調用transfer方法。

計算分區的範圍

因為ConcurrentHashMap是支持多線程同時擴容的,所以為了避免每個線程處理的數量不均勻,也為了提高效率,其對當前的所有桶按數量(也就是上面提到的槽位)進行分區,每個線程只處理自己分到的區域內的桶的數據即可。

當前線程計算當前stride的代碼如下。

stride = (NCPU > 1) ? (n >>> 3) / NCPU : n);

如果計算出來的值小於設定的最小範圍,也就是private static final int MIN_TRANSFER_STRIDE = 16;,就把當前分區範圍設置為16。

初始化nextTable

nextTable也是一個共享變量,定義如下,用於存放在正在擴容之後的ConcurrentHashMap的數據,當且僅當正在擴容時才不為空。

private transient volatile Node<K,V>[] nextTable;

如果當前transfer方法傳入的nextTab(這是個局部變量,比上面提到的nextTable少了幾個字母,不要搞混了)是null,說明是當前線程是第一個調用擴容操作的線程,就需要初始化一個size為原來容量2被的nextTable,核心代碼如下。

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 可以看到傳入的初始化容量是n << 1。

初始化成功之後就更新共享變量nextTable的值,並設置transferIndex的值為擴容前的length,這也是一個共享的變量,表示擴容使還未處理的桶的下標。

設置分區邊界

一個新的線程加入擴容操作,在完成上述步驟后,就會開始從現在正在擴容的Map中找到自己的分區。例如,如果是第一個線程,那麼其取到的分區就會如下。

start = nextIndex - 1;
end = nextIndex > stride ? nextIndex - stride : 0;
// 實際上就是當還有足夠的桶可以分的時候,線程分到的分區為 [n-stride, n - 1]

可以看到,分區是從尾到首進行的。而如果是首次進入的線程,nextIndex 的值會被初始化為共享變量transferIndex 的值。

Copy分區內的值

當前線程在自己劃分到的分區內開始遍歷,如果當前桶是null,那麼就生成一個 ForwardingNode,代碼如下。

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

並把當前槽位賦值為fwd,你可以把ForwardingNode理解為一個標誌位,如果有線程遍歷到了這個桶, 發現已經是ForwardingNode了,就代表這個桶已經被處理過了,就會跳過這個桶。

如果這個桶沒有被處理過,就會開始給當前的桶加鎖,我們知道ConcurrentHashMap會在多線程的場景下使用,所以當有線程正在擴容的時候,可能還會有線程正在執行put操作,所以如果當前Map正在執行擴容操作,如果此時再寫入數據,很可能會造成的數據丟失,所以要對桶進行加鎖。

總結

對比在1.7中採用的Segment分段鎖的臃腫設計,1.8中直接使用了CAS和Synchronized來保證併發下的線程安全。總的來說,在1.8中,ConcurrentHashMap和HashMap的底層實現都差不多,都是數組、鏈表和紅黑樹的方式。其主要區別就在於應用場景,非併發的情況可以使用HashMap,而如果要處理併發的情況,就需要使用ConcurrentHashMap。關於ConcurrentHashMap就先聊到這裏。

本文使用 mdnice 排版

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

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

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

啪啪,打臉了!領導說:try-catch必須放在循環體外!_租車

※超省錢租車方案

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

哈嘍,親愛的小夥伴們,技術學磊哥,進步沒得說!歡迎來到新一期的性能解讀系列,我是磊哥。

今天給大家帶來的是關於 try-catch 應該放在循環體外,還是放在循環體內的文章,我們將從性能和業務場景分析這兩個方面來回答此問題。

很多人對 try-catch 有一定的誤解,比如我們經常會把它(try-catch)和“低性能”直接畫上等號,但對 try-catch 的本質(是什麼)卻缺少着最基礎的了解,因此我們也會在本篇中對 try-catch 的本質進行相關的探索。

小貼士:我會盡量用代碼和評測結果來證明問題,但由於本身認知的局限,如有不當之處,請讀者朋友們在評論區指出。

性能評測

話不多說,我們直接來開始今天的測試,本文我們依舊使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基準測試套件)來進行測試。

首先在 pom.xml 文件中添加 JMH 框架,配置如下:

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>{version}</version>
</dependency>

完整測試代碼如下:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * try - catch 性能測試
 */
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 1 輪,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Benchmark)
@Threads(100)
public class TryCatchPerformanceTest {
    private static final int forSize = 1000; // 循環次數
    public static void main(String[] args) throws RunnerException {
        // 啟動基準測試
        Options opt = new OptionsBuilder()
                .include(TryCatchPerformanceTest.class.getSimpleName()) // 要導入的測試類
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public int innerForeach() {
        int count = 0;
        for (int i = 0; i < forSize; i++) {
            try {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    @Benchmark
    public int outerForeach() {
        int count = 0;
        try {
            for (int i = 0; i < forSize; i++) {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return count;
    }
}

以上代碼的測試結果為:

從以上結果可以看出,程序在循環 1000 次的情況下,單次平均執行時間為:

  • 循環內包含 try-catch 的平均執行時間是 635 納秒 ±75 納秒,也就是 635 納秒上下誤差是 75 納秒;
  • 循環外包含 try-catch 的平均執行時間是 630 納秒,上下誤差 38 納秒。

也就是說,在沒有發生異常的情況下,除去誤差值,我們得到的結論是:try-catch 無論是在 for 循環內還是 for 循環外,它們的性能相同,幾乎沒有任何差別。

try-catch的本質

要理解 try-catch 的性能問題,必須從它的字節碼開始分析,只有這樣我能才能知道 try-catch 的本質到底是什麼,以及它是如何執行的。

此時我們寫一個最簡單的 try-catch 代碼:

public class AppTest {
    public static void main(String[] args) {
        try {
            int count = 0;
            throw new Exception("new Exception");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然後使用 javac 生成字節碼之後,再使用 javap -c AppTest 的命令來查看字節碼文件:

 javap -c AppTest 
警告: 二進制文件AppTest包含com.example.AppTest
Compiled from "AppTest.java"
public class com.example.AppTest {
  public com.example.AppTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: new           #2                  // class java/lang/Exception
       5: dup
       6: ldc           #3                  // String new Exception
       8: invokespecial #4                  // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
      11: athrow
      12: astore_1
      13: aload_1
      14: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      17: return
    Exception table:
       from    to  target type
           0    12    12   Class java/lang/Exception
}

從以上字節碼中可以看到有一個異常表:

Exception table:
       from    to  target type
          0    12    12   Class java/lang/Exception

參數說明:

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

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

  • from:表示 try-catch 的開始地址;
  • to:表示 try-catch 的結束地址;
  • target:表示異常的處理起始位;
  • type:表示異常類名稱。

從字節碼指令可以看出,當代碼運行時出錯時,會先判斷出錯數據是否在 from 到 to 的範圍內,如果是則從 target 標誌位往下執行,如果沒有出錯,直接 goto 到 return。也就是說,如果代碼不出錯的話,性能幾乎是不受影響的,和正常的代碼的執行邏輯是一樣的。

業務情況分析

雖然 try-catch 在循環體內還是循環體外的性能是類似的,但是它們所代碼的業務含義卻完全不同,例如以下代碼:

public class AppTest {
    public static void main(String[] args) {
        System.out.println("循環內的執行結果:" + innerForeach());
        System.out.println("循環外的執行結果:" + outerForeach());
    }
    
    // 方法一
    public static int innerForeach() {
        int count = 0;
        for (int i = 0; i < 6; i++) {
            try {
                if (i == 3) {
                    throw new Exception("new Exception");
                }
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    // 方法二
    public static int outerForeach() {
        int count = 0;
        try {
            for (int i = 0; i < 6; i++) {
                if (i == 3) {
                    throw new Exception("new Exception");
                }
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return count;
    }
}

以上程序的執行結果為:

java.lang.Exception: new Exception

at com.example.AppTest.innerForeach(AppTest.java:15)

at com.example.AppTest.main(AppTest.java:5)

java.lang.Exception: new Exception

at com.example.AppTest.outerForeach(AppTest.java:31)

at com.example.AppTest.main(AppTest.java:6)

循環內的執行結果:5

循環外的執行結果:3

可以看出在循環體內的 try-catch 在發生異常之後,可以繼續執行循環;而循環外的 try-catch 在發生異常之後會終止循環。

因此我們在決定 try-catch 究竟是應該放在循環內還是循環外,不取決於性能(因為性能幾乎相同),而是應該取決於具體的業務場景。

例如我們需要處理一批數據,而無論這組數據中有哪一個數據有問題,都不能影響其他組的正常執行,此時我們可以把 try-catch 放置在循環體內;而當我們需要計算一組數據的合計值時,只要有一組數據有誤,我們就需要終止執行,並拋出異常,此時我們需要將 try-catch 放置在循環體外來執行。

總結

本文我們測試了 try-catch 放在循環體內和循環體外的性能,發現二者在循環很多次的情況下性能幾乎是一致的。然後我們通過字節碼分析,發現只有當發生異常時,才會對比異常表進行異常處理,而正常情況下則可以忽略 try-catch 的執行。但在循環體內還是循環體外使用 try-catch,對於程序的執行結果來說是完全不同的,因此我們應該從實際的業務出發,來決定到 try-catch 應該存放的位置,而非性能考慮。

關注公眾號「Java中文社群」回復“乾貨”,獲取 50 篇原創乾貨 Top 榜。

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

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

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

【大廠面試01期】高併發場景下,如何保證緩存與數據庫一致性?_包裝設計

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

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

PS:本文已收錄到1.1K Star數開源學習指南——《大廠面試指北》,如果想要了解更多大廠面試相關的內容及獲取《大廠面試指北》離線PDF版,請掃描下方二維碼碼關注公眾號“大廠面試”,謝謝大家了!項目地址:https://github.com/NotFound9/interviewGuide

《大廠面試指北》項目截圖:

獲取《大廠面試指北》離線PDF版,請掃描下方二維碼關注公眾號“大廠面試”

面試題:高併發場景下,如何保證緩存與數據庫一致性?

問題分析

我們日常開發中,對於緩存用的最多的場景就像下圖一樣,可能僅僅是對數據進行緩存,減輕數據庫壓力,縮短接口響應時間。

這種方案在不需要考慮高併發得去寫緩存,高併發得讀寫緩存時,是不會有問題,但是如果是在高併發場景下,要保證緩存和數據庫的一致性,至少需要解決以下問題:

高併發寫時的數據不一致問題

高併發讀寫時,請求執行各步驟的順序是不可控的。假設此時有一個請求A,B都在在執行寫流程,請求A是需要將某個數據改成1,請求B是需要將某個數據改為2,執行操作如下時就會導致數據不一致的問題:

1.請求A執行操作1.1刪除緩存。

2.請求A執行操作1.2更新數據庫,將值改為1。

3.請求B執行操作1.1刪除緩存。

4.請求B執行操作1.2更新數據庫,將值改為2

5.假設說請求B所在服務器網絡延遲比較低,請求B先更新緩存,此時緩存中的key對應的value是2。

6.請求A更新緩存,將緩存中B更新的數據進行覆蓋,將key對應的值改為1。

此時數據庫中是B修改后的數據,值為2,而緩存中的數據是1,這樣在緩存過期錢,用戶讀到的都是臟數據,與數據庫不一致。

高併發讀寫時的數據不一致的問題

高併發讀寫時,請求執行各步驟的順序是不可控的。假設此時有一個請求A在執行寫流程,將原值由1改成2,請求B執行讀流程,執行操作如下時就會導致數據不一致的問題:

1.寫請求A執行1.1操作刪除緩存key,value是原值1。

2.讀請求B執行2.1操作發現緩存中沒有數據,就去執行2.2操作讀數據庫,讀到舊數據,值為1。

3.寫請求A執行1.2操作更新數據庫,將數據由1改為2。

4.寫請求A執行1.3操作更新緩存,此時緩存中的數據key對應的value是2。

5.讀請求B執行2.3操作更新緩存,將之前讀到的舊數據1設置到緩存中,此時緩存中的數據key對應的value是1。

所以如果說讀請求B所在服務器網絡延遲比較高,去執行2.3操作比寫請求A晚,就會導致寫請求A更新完緩存后,讀請求B使用之前讀到的舊數據去更新緩存,此時緩存中數據就與數據庫中的不一致。

解決方案

保證數據一致性,網上有很多種方案,例如:

1.先刪除緩存,再更新數據庫。

2.先更新數據庫,再刪除緩存。

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

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

3.先刪除緩存,再更新數據庫,然後異步延遲一段時間再去刪一次緩存。

但是這些方案都是存在各種各樣的問題,這裏篇幅有限,只給出目前相對正確的三套方案,目前的這些方案也有自己的局限性。

方案1.寫請求串行化

寫請求

1.寫請求更新之前先獲取分佈式鎖,獲得之後才能去數據庫更新這個數據,獲取不到就進行等待,超時后就返回更新失敗。

2.更新完之後去刷新緩存,如果刷新失敗,放到內存隊列中進行重試(重試時取數據庫最新數據更新緩存)。

讀請求

讀請求發現緩存中沒有數據時,直接去讀取數據庫,讀完更新緩存。

總結

這種技術方案通過對寫請求的實現串行化來保證數據一致性,但是會導致吞吐量變低。比較適合銀行相關的業務,因為對於銀行項目來說,保證數據一致性比可用性更加重要,就像是去存款機存錢,取錢時,為了保證賬戶安全,都是會讓用戶執行操作后,等待一段時間才能獲得反饋,這段時間其實取款機是不可用的。

方案2.先更新數據庫,異步刪除緩存,刪除失敗后重試

1.先更新數據庫

2.異步刪除緩存(如果數據庫是讀寫分離的,那麼刪除緩存時需要延遲刪除,否則可能會在刪除緩存時,從庫還沒有收到更新后的數據,其他讀請求就去從庫讀到舊數據然後設置到緩存中。)

3.刪除緩存失敗時,將刪除的key放到內存隊列或者是消息隊列中進行異步重試

發散思考

在更新完數據庫后,我們為什麼不直接更新,而是採用刪除緩存呢?

這是因為直接更新緩存的話,在高併發場景下,有多個更新請求時,難以保證后更新數據庫的請求會後更新緩存,也就是上面的高併發寫問題。如果採用刪除緩存,可以讓下次讀時讀取數據庫,更新緩存,保證一致性。

方案3.業務項目更新數據庫,其他項目訂閱binlog更新

1.業務項目直接更新數據庫。

2.cannal項目會讀取數據庫的binlog,然後解析后發消息到kafka。

3.然後緩存更新項目訂閱topic,從kafka接收到更新數據庫操作的消息后,更新緩存,更新緩存失敗時,新建異步線程去重試或者將操作發到消息隊列,後續再進行處理。

總結:

但是這種方案在更新數據庫后,緩存中還是舊值,必須等緩存更新項目消費消息后,更新緩存,緩存中才是最新值。所以更新操作完成與更新生效之間會有一定的延遲。

最後

大家有了解其他的技術方案,歡迎進群一起討論!

評論裏面有朋友問延時雙刪策略是什麼?

這裏解釋一下:延時雙刪策略就是先刪除緩存,再更新數據庫,再異步過一小段時間后刪除緩存(時間取決於MySQL主從同步的時間)。

是因為MySQL如果是讀寫分離時(寫請求寫主庫,讀請求讀從庫),我們更新主庫后,需要一段時間,從庫才會收到更新。

如果是寫請求更新主庫后,第二次立即刪除緩存,MySQL從庫還沒有收到更新,還是舊數據,那麼讀請求直接從庫讀到舊數據,設置到緩存的數據就是舊數據,就會數據不一致,所以這也是延時雙刪策略提出的初衷。

參考鏈接:

https://www.cnblogs.com/-wenli/p/11474164.html

https://www.cnblogs.com/rjzheng/p/9041659.html

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

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

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