巴西東北岸海灘 再出現原油污染

摘錄自2019年12月31日中央社報導

巴西海軍今天(31日)表示,東北部塞阿拉州(Ceara)部分海灘發現原油油污,快兩個月前,這個地區也曾被另一波浮油侵襲。

那次的污染是原油大規模外洩的一部分,在9月到11月污染了巴西東北岸數百個海灘,威脅海洋生物、觀光業和漁業,源頭至今仍是個謎。

塞阿拉聯邦大學(Federal University of Ceara)海洋研究人員卡瓦坎特(Rivelino Cavalcante)告訴新聞網站G1,和這次污染相同,於今年稍早出現在海灘上的大量油污仍沈積在海床,因為洋流的關係才跑到岸邊。

巴西政府官員曾表示,檢測顯示外洩原油的源頭是委內瑞拉,但委國否認。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

湄公河開發不止 洞里薩湖水量大減 柬埔寨淡水漁業恐崩盤

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

澳洲熱浪恐惡化野火危機 數千觀光客準備撤離

摘錄自2020年1月2日中央社雪梨報導

澳洲4日恐遭一波熱浪侵襲,預期會引發致命火災,數千名觀光客必須在不到48小時內撤離已被野火肆虐的沿海社區。

法新社報導,在2019年到2020年的跨年夜,災難性野火把澳洲大陸南岸燒出遍地火場,沿著海岸形成一條火帶,造成至少8人死亡,海邊小鎮被火焰包圍,度假遊客陷入困境。

澳洲新南威爾斯省(NSW)鄉村消防局公告「遊客禁停區」,從度假勝地巴特曼灣(Batemans Bay)沿著向來風景優美的東南海岸延伸約200公里,到相鄰的維多利亞省(Victoria),當局也呼籲這裡的人遠離火災威脅的地區。

當局告誡遊客在4日前離開,天氣預報預測這天會有狂風,氣溫將飆升到攝氏40度以上。

新南威爾斯省運輸部長康史坦士(Andrew Constance)接受澳洲廣播公司(ABC)訪問時說,這可能是這個地區「歷來最大規模的人員撤離」,數千人準備疏散,以避開4日可能的新一波惡火。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

搶入特斯拉自駕電動車供應鏈 英特磊營收看漲

英特磊 8 日公布 6 月營收 7,693 萬元,月增 8.18% 而年增 15.2%,第 2 季營收 2.24 億元,季增 8.21% 年增 46.84%,月、季營收同步創高,公司並將在 9 月洽談車用防撞系統新單,可望打入 Tesla 發展自駕(ADAS)電動車需求,再創營運高峰。    據了解,英特磊收購 Soitec 後,取得全球最大車用防撞 IC 廠 UMS 供應認證,UMS 打入多數歐系高級車,包括 BMW、Audi、賓士等,同時間,UMS 也是與 Tesla 計畫合作自駕車系統 Mobileye 的主要客戶,在此緊密的車用供應鏈關係中,英特磊持續獲得熱賣車款點火。   英特磊看好第 3 季旺季需求,將獲得新布局的車用防撞雷達、Skyworks 物聯網帶動,因此公司先在 5 月進行歲修調整機台,將對全年營收衝刺動能有關鍵性助益。 ]

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

Elon Musk:Model X 會讓特斯拉銷量倍增

美國豪華電動車製造商特斯拉 (Tesla) 執行長 Elon Musk 仍舊對公司業務信心滿滿,據彭博社報導,Musk 7 日表示,特斯拉預定本季問世的電動運動休旅車「Model X」有望讓公司的電動車銷售量倍增。   特斯拉希望讓 2015 年的電動車銷售量達到 55,000 台,該公司目前只販售 Model S 車款,而其上半年的交車量達到 21,552 台、全年度目標達成度約有 40%。   特斯拉曾於 7 月 2 日宣布,旗下高檔電動車 Model S 第二季交車超出預期,第二季交車量來到 11,507 輛,高於原先預估的 10,000 至 11,000 輛。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

次世代倫敦經典!紅色雙層公車改採比亞迪電動車系統

提到英國倫敦,不少人就會想起經典的紅色雙層公車。倫敦交通局為響應環保,宣布與比亞迪簽約,將部分紅色雙層公車打造為搭載電池的次世代電動公車,且即將開始在倫敦市展開服務。

在英國舉辦的第40屆清潔巴士高峰會(Clean Bus Summit)上,倫敦交通局與比亞迪聯合宣布了這項消息。倫敦市場Boris Johnson指出:「過去不少人都認為雙層公車因所需的電池體積問題而無法電動化,但比亞迪辦到了,領先全球。」從今年十月開始,比亞迪所生產的紅色雙層巴士將成為倫敦第16路公車所採用的車種。

比亞迪歐洲營運經理Isbrand Ho傳達公司與倫敦交通局的愉快合作,且表示希望還能把無汙染的電動公車推廣到更多國家、城市。比亞迪電動公車採用許多領先技術,包含12年保固的碳酸鋰鐵電池技術、專利的內輪引擎、反饋制動系統等。比亞迪電動公車具有壽命較長、環保無汙染、安靜、平穩等優點,每次充電後可在市區內行駛約155英里。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

支撐馬蜂窩「雙11」營銷大戰背後的技術架構

(馬蜂窩技術原創內容,公眾號 ID: mfwtech)

引言

消費者的狂歡節「雙 11」剛剛過去。在電商競爭環境日益激烈的今天,為了抓住流量紅利,雙 11 打響的已經不僅僅是「促銷戰」,也是「營銷戰」,這對平台的技術支撐能力提出新的要求。

從 2014 年的「318 大促」,到正在進行的 「馬蜂窩雙 11 全球旅行蜂搶節」,馬蜂窩旅遊電商業務的大促已經走過 5 年時間,僅僅是雙 11、暑期、十一黃金周、年終這些關鍵節點的 S 級促銷就張羅了 50 多場,每年上線活動達幾百個。

圖:馬蜂窩11.11全球旅行蜂搶節

 

在這個過程中,馬蜂窩營銷平台也在經歷着優化和改進,不斷探索靈活高效的營銷活動運營開發方式,更好地支撐業務營銷活動的模式創新和投放需求,努力實現平台商家與馬蜂窩旅遊消費者的高效匹配,目前已經形成了一套較為完整的技術體系。

本文將以馬蜂窩營銷活動後台的技術實踐為重點,分享馬蜂窩營銷平台的架構設計思路,希望能讓遇到類似問題的同學有所收穫。

 

一、馬蜂窩營銷平台架構

1. 營銷中心體系

談到大促,大家可能首先會想到的海量數據、高併發、高流量等關鍵詞。其實除了這些,營銷活動數量多、周期短、功能複雜多變等,都是營銷活動運營開發需要應對的挑戰。並且由於我們的很多活動會涉及到一些獎勵或權益的下發,對於安全性的要求也很高。

針對以上問題,馬蜂窩營銷系統的技術架構要具備的核心能力包括:

  • 打造靈活、高效的活動開發模式

  • 提供高可靠、高可用的活動運營支撐

  • 保證營銷活動業務的安全運行

因此,我們本着「平台化、組件化、模塊化」的方法,將營銷體系的架構設計如下:

 

馬蜂窩整體營銷體系分為 B 端和 C 端兩部分。B 端主要面向商家,幫助商家在馬蜂窩招商平台進行大促活動的提報以及商品選取;C 端主要是面向馬蜂窩用戶,平台用戶可以在業務營銷頁面完成活動商品的購買、秒殺、大促紅包贏取等具體的營銷活動參与互動。

2. C 端營銷平台

C 端營銷平台的系統架構主要分為主要分為營銷應用層、中間層、投放平台、搭建平台四個部分。

  • 活動開發平台:營銷平台最核心的部分,也是本文重點。包括前端頁面搭建層「魔方」、業務邏輯層「蜂玩樂園」、獎勵規則控制層「獎池」三部分

  • 投放平台:是指營銷活動頁的投放,包括投放策略、運營策略和機制等

  • 中間件:負責併發處理、分佈式緩存和容災限流等等

  • 營銷應用:包括馬蜂窩大促營銷、業務營銷、新人禮包等

下面,我們重點介紹營銷搭建平台的核心部分——活動開發平台,是如何實現高效、靈活的營銷活動開發模式的。

 

二、活動開發平台的實現

2.1 靈活高效的開發模式

通過上圖可以看到,由 MySQL、ElasticSearch、Redis 組成的數據池在底層為活動開發平台提供支撐。其中 MySQL 為最主要的存儲方案,用於會場搭建配置數據、蜂玩樂園的用戶運行數據、獎池配置數據等的存放。ElasticSearch 是搜索引擎,支持活動頁面商家活動報名與篩選過程。Redis 有 2 種用途:1)活動任務併發鎖;2)獎池的獎品數據存放;3)限流和削峰。

之前我們提到,活動開發的挑戰包括數量多、周期短、功能複雜多變。為了降低開發同學的工作量,提升研發效率,我們將前端和後端組件進行了整合,並封裝成功能模塊對提供服務,形成了目前的魔方、蜂玩樂園、獎池三個子系統,使整體結構更加清晰。每個部分解決的問題和主要功能模塊示意如下:

2.1.1 系統分層

魔方

「魔方」系統希望通過組件、工具的方式完成營銷頁面的搭建,實現統一維護和復用,從而減少前端團隊在活動開發中承載的重複性開發工作。目前為止我們已經在「魔方」中開發了 80 多個組件模塊,例如秒殺模塊、貨架模塊、店鋪模塊、導航模塊、領券模塊、遊戲互動模塊等。

現在,小型活動完全可以不用開發人員支持,只需要業務同學操作即可搭建促銷會場上線活動,提升了活動運營效率,也大大解放了前端開發人員。關於「魔方」更多的細節我們會在後續文章單獨介紹,本文不過多展開。

蜂玩樂園

(1) 邏輯功能抽象

營銷活動的核心是創新和吸引力。每次活動開始前,運營同學都會在創意策劃上絞盡腦汁,盡可能創造出與眾不同的新玩法。這些新穎有趣的遊戲玩法,可以在微信,App 等渠道引起用戶的好奇心和興趣,為賣場拉新,進而創造更多的交易。

隨着「花樣」的不斷翻新,活動開發的複雜度也在增加,有時甚至讓技術同學應接不暇,也促使我們探索更加高效的開發方式。

我們開始思考在複雜多變的活動玩法下,是否潛藏着一些不變的模式和規則?通過對不同業務活動模式的分析和抽象,我們將活動的流程和用戶的行為進行了一個有趣的類比:

  • 首先,開發活動就創建了一個「樂園」

  • 我們會根據不同的「規則」去設計每一個「活動」,激發潛在「參与者」的興趣,或建立他們希望贏得獎勵的期待。

  • 進入活動后,我們會驗證參与者「身份」,和需要滿足這次活動的「條件」,來確定他是否可以開始。

  • 活動開始時,參与者參与一次活動需要發生的行為,就是在完成「任務」

  • 完成「任務」后,為參与者發放相應的「權益」或「獎勵」。

這個類比模型在歷屆促銷活動中進行了推演,結果显示基本是通用的,但完成任務可能伴隨獎勵服務,也可能沒有,由具體業務需求決定。舉個例子,在一場紅包裂變的營銷活動中有一個需求是下紅包雨,用戶可以點擊掉下來的紅包領取相應的紅包獎勵。那麼「領取」這個動作就可以視為活動中的一個任務;另一個需求是每當用戶成功邀請一位好友后就可以在任務中心領取一個邀請紅包獎勵,那麼我們可以把在任務中心領取邀請紅包也看成一個任務。

這兩個任務有一個共同的特點就是觸發后都有紅包獎勵,只是在第二個場景中的任務,本質上是用戶發起了一個請求。

經過進一步的梳理、規整,我們抽象出了「參与者」、「活動」、「任務」、「獎品」等業務邏輯功能。

(2) 技術實現

蜂玩樂園將每一個業務邏輯功能收歸到一個唯一的入口和統一的體系中,形成獨立的功能組件模塊,如數據請求模塊、自定義數據配置模塊、驗證器模塊 、執行器模塊、獎勵服務模塊等。每個活動的任務開發都可以選擇模塊配置,模塊配置信息以 yaml. 的格式進行統一管理,這樣的配置具有靈活性、擴展性和可復用性。

在使用的時候解析配置數據,並向組件註冊中心註冊該任務所需要的組件模塊,再按照定義好的順序執行即可。流程如下圖所示:

為大家介紹幾個關鍵模塊的實現。

  • 數據請求模塊

數據請求模塊定義了客戶端與服務端約定好的請求參數規則:

request:
       -
        field:  deviceId
        rule: required  #必填項校驗
        method: post
        message:  deviceId參數錯誤
       -
        field:  sex
        rule: in:[0,1] #範圍校驗
        method: post
        message: 性別範圍錯誤
       -
        field:  phone
        rule:   regex:/^1[3456789]\d{9}$/ #正則校驗
        method: post
        message: 手機號格式錯誤

(i) field – 傳入參數的 key 
(ii) rule – 校驗該參數的規則,目前我們已經實現了一些常用的規則:
(iii) required – 必傳參數

  • in:驗證所傳參數必須在指定範圍內

  • regex:正則表達式校驗 

  • min,max:自定義規則最小和最大長度

  • integer:必須是数字

  • method:定義 GET、POST 請求方式,

(iv) message – 規則驗證失敗返回的錯誤信息。這一層會讀取配置模塊中的請求參數模塊配置內容,將內容解析出來,按照所配置的字段規則做響應的校驗,如校驗通過繼續向下執行,沒有通過則直接返回規則提示。

  • 參數配置模塊

參數配置模塊定義了該任務執行中所需配置的所有靜態數據配置項。營銷活動的特點是多樣性、創新性,所以很難去窮舉各種場景建立一個有針對性的配置中心,因此這裏就為每一個任務單獨開闢了一個沒有結構化的小空間,可根據具體場景的特定需求為任務自由配置,使程序代碼里基本不用再寫各種不合理的硬編碼。

params:
    stockRedPacket:
     amount: 1
     stock: 3
     stockKey:  limit_key
     stockField: limit_key_90
     timeWindow:
       beginTime: "2019-11-06 00:00:00"
       endTime: "2019-11-10 23:59:

以一個用戶開啟紅包的配置信息為例:

(i) stockRedPacket 配置了活動設定的固定庫存與固定金額紅包的業務邏輯

  • amount 金額

  • stock 庫存

  • stockKey、stockField 用來加鎖的字段

(ii) timeWindow 定義了該任務的活動開始和結束時間

 

  • 驗證器模塊

驗證器模塊的功能主要是是對業務或者規則的校驗。它定義了該任務要執行的業務驗證規則,特點是具有單一性、普適性,能提供一種適用於大多數場景的方法。這些驗證規則可以拆解得足夠細,越細則越靈活,得以在不同任務中靈活組裝使用。

validator:
   - MCommon_Validator_TimeWindowValidator
   - MCommon_Validator_AuthValidator
   - MCommon_Validator_LockValidator

  • 這裏使用了活動時間驗證 TimeWindowValidator,不在活動時間內則返回錯誤提示

  • 登陸驗證 AuthValidator,參加活動必須要登錄,前端通過判斷錯誤狀態碼統一跳轉到登陸頁面

  • 併發鎖 LockValidator,避免一個用戶同樣的操作多次提交

  • 取出所有的驗證器,然後通過反射依次按照順序調用,如果其中一個驗證器失敗,則直接返回錯誤信息,終止向下執行。

 

  • 執行器模塊

執行器模塊定義了該任務要執行的一些業務邏輯,比如要執行一段寫日誌的邏輯,要執行一個異步調用的邏輯等,都可以使用此模塊定義一個執行器去執行。

command: MSign_Command
afterCommand: MSign_Command_After

執行器又分為前置 command 和後置 afterComman:

 

  • 如果需要執行獎勵模塊,則先執行前置 command,再執行獎勵邏輯,最後執行後置 afterCommand,最終返回結果

  • 如果沒有獎勵,則先執行前置 command,接着執行後置 afterCommand

 

  • 獎勵服務模塊

獎勵服務模塊決定該任務是否需要執行獎勵發放,如果配置了獎勵,任務在執行時會根據獎勵的配置規則下發獎勵。在我們的實際場景中,主要涉及到的獎勵類型包括獎勵機會、紅包、抽獎、優惠券等:

  • 獎勵機會:有 2 種規則,分別是按固定頻次給用戶初始化機會數,和獎勵增量機會數。

  • 發送紅包:設定固定紅包和隨機紅包,隨機紅包按需求設置發放的概率與用戶群。

  • 抽獎:對接獎池系統,下文詳細介紹。

  • 優惠券:與馬蜂窩優惠中心直接打通,只需要配置優惠券 SN 和渠道號,即可把優惠券發送到用戶卡券。

 

獎池

在營銷活動中,許多場景都涉及用戶抽獎或獎品發放。營銷技術平台因此對獎品發放的整個生命周期進行了抽象和封裝,創建了「獎池」。

(1) 主要功能

獎池的主要功能點包括:

  • 創建獎品池:為每次活動創建一個或多個獎品池

  • 設置獎品:在單一獎品池中均勻放置獎品

  • 用戶抽獎:用戶在單一獎池中抽獎,支持按概率抽獎,支持獎品的發放和領取

  • 中獎統計:包括獎品已發放數量,已領取數量,剩餘數量

如下圖所示,只需創建好獎池,配置好獎品信息,把對應的獎池 ID 填寫到任務,即可實現抽獎功能:

(2) 方案設計

獎池早期的設計非常簡單,獎品實體僅定義「余量」的概念,利用關係型數據庫中單行記錄存儲一次活動的獎品、總量、發放量、余量等數據。在用戶流量較小且均勻的情況下,發放過程平穩正常。每次進行獎品發放時,在單行記錄上進行 update 操作,扣減余量並增加發放量即可。

然而隨着公司業務的發展,一次營銷活動帶來的效果讓我們不得不立刻改進獎池方案。這次營銷活動帶來的流量遠超預期,但獎品數量的配置卻一如往常。當活動開啟后,獎品消耗很快,並在一段時間后被提前抽光。為了不影響用戶體驗,營銷運營同學不得不持續向獎池中補充獎品。

經歷這次問題開發同學發現,獎池提前抽光的原因在於設計中忽略了時間分佈的因素,使獎品抽光的速度只與訪問量相關。因此,大家開始思考如何讓獎品固定且均勻分佈在活動周期內。

通過學習與比較,最終選擇了業界比較通用的方案,使用 Redis 的有序集合(Sorted Set)創建獎池和設置獎品,從而使獎品在活動時間段內均勻分佈,防止提前抽光的情況出現。

(3) 實現算法

 

1. 時間戳:根據獎品的數量和活動時長,為每 1 份獎品設置一個出獎時間戳,這份獎品僅能在這一時間點及之後被抽出。這一步使出獎時間戳盡量均勻分佈在活動時間範圍內。

2. 創建獎品池:為每一組獎品設置一個獎池,在 Redis 創建一個 zset 數據結構,將其中的每 1 份獎品作為 1 個成員(Member),將時間戳作為分值(score)。
3. 放置獎品:使用ZADD 獎池 出獎時間戳 1 份獎品 語法,在 Redis 中布置一個獎品。

4. 抽獎:使用 Sorted Set 的排序方法,每次排序后查看排名第一的獎品,比較當前時間戳與獎品時間戳的大小。如果當前時間晚於或等於出獎時間,則使用 ZREM 指令出獎,否則不出。

示意圖如下:

2.1.2  體系統一

為了讓開發同學只專註於任務的設計開發,我們抽象出「賬戶」的概念,每個任務產生的數據資源會存儲在所在的「賬戶」體系下,使其支撐多個類似的活動。這種設計的好處在於:

(1)同一用戶在參与不同的活動時得到的獎勵都是相互獨立的,不會出現混淆的情況。

(2)之前每次活動都需要單獨創建數據表,活動下線后表不能復用。時間長了造成系統佔用許多無用的數據表。而把數據庫表以抽象的任務形態創建,不針對具體的某一業務類型,就可以使數據表實現復用。這樣我們只專註任務的設計開發,不用再關心數據表的設計。

在營銷大促的活動中,我們也接入了風控中心、併發鎖和限流服務,以保障整個活動的安全和穩定。

2.2 可用性和可靠性

秒殺模塊是大促流量的最高峰。結合業務實際,我們針對這種場景也做了限流和削峰處理。

限流採用的方案是限制時間窗內最大請求數據,用戶再搶會員權益時,第一步會讀取限流配置 key 和 value,判斷單位時間內是否超過限制的最大請求數 value,如果超過則返回信息提示結束請求;如果沒有超過閾值,則進入下一步操作。目前的限流系統只是在應用層面的實現,為了更好地支撐業務發展,後續我們也會接入網關服務,通過 Sentinel 和 Hystrix 做限流熔斷,避免流量進入應用層,提高服務的高可用性。

削峰部分結合實例說明。

以秒殺金卡會員的場景為例,我們會先用 RabbitMQ 承接瞬時的流量洪峰,再平滑地消費所有請求,並提前把庫存數量對應的 Token 寫入 Redis 緩存(這裏我們針對性的對不同的用戶引入了 Token 機制,以保證我們的秒殺商品在時效和庫存上得以保障)。用戶在秒殺時會順序地從 Redis 中 rPop 對應的 Token,避免庫存超賣的情況;用戶拿到 Token 之後再去收銀台下單開通金卡會員,就可以避免流量同一時刻去下單。

隨着業務和技術的發展,系統的不確定性以及流量的預估都更為困難。我們也在不斷學習業界的先進經驗,來更好地提升系統的可用性和可靠性。目前我們正在調研基於 Noah 的「自適應」限流技術並积極推進,以期針對不同的服務器性能以及當前的流量情況進行針對性的限流控制,相信我們會在後續的優化中會做得更好。

2.3 風險控制

目前是接入公司統一的風控中心。在營銷活動需求確定好后,我們會向風控服務中心提供需要風控的業務場景邏輯。風控中心根據業務配置不同策略,給出不同的場景 key。我們只需要在營銷活動任務中的自定義參數配置模塊配置好風控場景 key,就可在獎勵服務模塊自動調用風控接口去校驗用戶,如果識別出是風險用戶則會被攔截,終止活動參与。

可用性和可靠性、風險控制的實現流程如下圖所示:

 

三、近期規劃

1. 完善監控體系

目前對於活動運行中的數據監控,主要依賴數據組的統計與輸出。線上活動的運行情況並不能通過「蜂玩樂園」與「獎池」系統實時並綜合表現出來。

未來會補齊運行時的活動監控功能,通過活動、任務、獎品的運行時數據指標,指導運營同學第一時間調整活動參數,取得最佳運營效果。

2. 服務化改造

營銷基礎平台依舊搭建在單體架構上,部分功能的邊界與職責並不完全清晰。接下來營銷技術平台會進行技術架構的升級與改造,從單體架構轉向微服務架構,使服務能力與開發能效同步提升。

小結

隨着營銷的逐年發展,活動的趣味性和複雜度會一起上升,這需要我們不斷更新對營銷活動的認識。在這過程中也要反覆嘗試新的抽象和重構,通過不斷改進現有系統,支持更多和更好玩的營銷活動,讓馬蜂窩用戶玩兒得更省心,玩兒得更省錢。

本文作者:馬蜂窩電商研發營銷中心團隊劉遠勻、任浩、唐溶波。

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

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

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

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

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

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

go中的數據結構-字典map

1. map的使用

  golang中的map是一種數據類型,將鍵與值綁定到一起,底層是用哈希表實現的,可以快速的通過鍵找到對應的值。

  類型表示:map[keyType][valueType] key一定要是可比較的類型(可以理解為支持==的操作),value可以是任意類型。

  初始化:map只能使用make來初始化,聲明的時候默認為一個為nil的map,此時進行取值,返回的是對應類型的零值(不存在也是返回零值)。添加元素無任何意義,還會導致運行時錯誤。向未初始化的map賦值引起 panic: assign to entry in nil map。

 1 package main
 2 
 3 import (  
 4     "fmt"
 5 )
 6 
 7 // bool 的零值是false
 8 var m map[int]bool 
 9 a, ok := m[1]
10 fmt.Println(a, ok) // false  false
11 
12 // int 的零值是0
13 var m map[int]int 
14 a, ok := m[1]
15 fmt.Println(a, ok) // 0  false
16 
17 
18 func main() {  
19     var agemap[string]int
20     if age== nil {
21         fmt.Println("map is nil.")
22         age= make(map[string]int)
23     }
24 }

  清空map:對於一個有一定數據的集合 exp,清空的辦法就是再次初始化: exp = make(map[string]int),如果後期不再使用該map,則可以直接:exp= nil 即可,但是如果還需要重複使用,則必須進行make初始化,否則無法為nil的map添加任何內容。

  屬性:與切片一樣,map 是引用類型。當一個 map 賦值給一個新的變量,它們都指向同一個內部數據結構。因此改變其中一個也會反映到另一個。作為形參或返回參數的時候,傳遞的是地址的拷貝,擴容時也不會改變這個地址。

 1 func main() {
 2     exp := map[string]int{
 3         "steve": 20,
 4         "jamie": 80,
 5     }
 6     fmt.Println("Ori exp", age)
 7     newexp:= exp
 8     newexp["steve"] = 18
 9     fmt.Println("exp changed", exp)
10 }
11 
12 //Ori age map[steve:20 jamie:80]
13 //age changed map[steve:18 jamie:80]

  遍歷map:map本身是無序的,在遍歷的時候並不會按照你傳入的順序,進行傳出。

 1 //正常遍歷:
 2 for k, v := range exp { 
 3     fmt.Println(k, v)
 4 }
 5 
 6 //有序遍歷
 7 import "sort"
 8 var keys []string
 9 // 把key單獨抽取出來,放在數組中
10 for k, _ := range exp {
11     keys = append(keys, k)
12 }
13 // 進行數組的排序
14 sort.Strings(keys)
15 // 遍曆數組就是有序的了
16 for _, k := range keys {
17     fmt.Println(k, m[k])
18 }

2. map的結構

   Go中的map在可以在 $GOROOT/src/runtime/map.go找到它的實現。哈希表的數據結構中一些關鍵的域如下所示:

 1 type hmap struct {
 2     count        int  //元素個數
 3     flags        uint8   
 4     B            uint8 //擴容常量
 5     noverflow    uint16 //溢出 bucket 個數
 6     hash0        uint32 //hash 種子
 7     buckets      unsafe.Pointer //bucket 數組指針
 8     oldbuckets   unsafe.Pointer //擴容時舊的buckets 數組指針
 9     nevacuate    uintptr  //擴容搬遷進度
10     extra        *mapextra //記錄溢出相關
11 }
12 
13 type bmap struct {
14     tophash        [bucketCnt]uint8  
15     // Followed by bucketCnt keys 
16     //and then bucketan Cnt values  
17     // Followed by overflow pointer.
18 } 

  說明:每個map的底層都是hmap結構體,它是由若干個描述hmap結構體的元素、數組指針、extra等組成,buckets數組指針指向由若干個bucket組成的數組,其每個bucket里存放的是key-value數據(通常是8個)和overflow字段(指向下一個bmap),每個key插入時會根據hash算法歸到同一個bucket中,當一個bucket中的元素超過8個的時候,hmap會使用extra中的overflow來擴展存儲key。

  圖中len 就是當前map的元素個數,也就是len()返回的值。也是結構體中hmap.count的值。bucket array是指數組指針,指向bucket數組。hash seed 哈希種子。overflow指向下一個bucket。

map的底層主要是由三個結構構成:

  1. hmap — map的最外層的數據結構,包括了map的各種基礎信息、如大小、bucket,一個大的結構體。
  2. mapextra — 記錄map的額外信息,hmap結構體里的extra指針指向的結構,例如overflow bucket
  3. bmap — 代表bucket,每一個bucket最多放8個kv,最後由一個overflow字段指向下一個bmap,注意key、value、overflow字段都不显示定義,而是通過maptype計算偏移獲取的。

  mapextra的結構如下

 1 // mapextra holds fields that are not present on all maps.
 2 type mapextra struct {
 3     // If both key and value do not contain pointers and are inline, then we mark bucket
 4     // type as containing no pointers. This avoids scanning such maps.
 5     // However, bmap.overflow is a pointer. In order to keep overflow buckets
 6     // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
 7     // overflow and oldoverflow are only used if key and value do not contain pointers.
 8     // overflow contains overflow buckets for hmap.buckets.
 9     // oldoverflow contains overflow buckets for hmap.oldbuckets.
10     // The indirection allows to store a pointer to the slice in hiter.
11     overflow    *[]*bmap
12     oldoverflow *[]*bmap
13 
14     // nextOverflow holds a pointer to a free overflow bucket.
15     nextOverflow *bmap
16 }

  其中hmap.extra.nextOverflow指向的是預分配的overflow bucket,預分配的用完了那麼值就變成nil。

  bmap的詳細結構如下

  在map中出現哈希衝突時,首先
以bmap為最小粒度掛載,一個bmap累積8個kv之後,就會申請一個新的bmap(overflow bucket)掛在這個bmap的後面形成鏈表,優先用預分配的overflow bucket,如果預分配的用完了,那麼就malloc一個掛上去。這樣減少對象數量,減輕管理內存的負擔,利於gc。
注意golang的map不會shrink,內存只會越用越多,overflow bucket中的key全刪了也不會釋放。

  bmap中所有key存在一塊,所有value存在一塊,這樣做方便內存對齊。當key大於128字節時,bucket的key字段存儲的會是指針,指向key的實際內容;value也是一樣。

  hash值的高8位存儲在bucket中的tophash字段。每個桶最多放8個kv對,所以tophash類型是數組[8]uint8。把高八位存儲起來,這樣不用完整比較key就能過濾掉不符合的key,加快查詢速度。實際上當hash值的高八位小於常量minTopHash時,會加上minTopHash,區間[0, minTophash)的值用於特殊標記。查找key時,計算hash值,用hash值的高八位在tophash中查找,有tophash相等的,再去比較key值是否相同。

 1 type typeAlg struct {
 2     // function for hashing objects of this type
 3     // (ptr to object, seed) -> hash
 4     hash func(unsafe.Pointer, uintptr) uintptr
 5     // function for comparing objects of this type
 6     // (ptr to object A, ptr to object B) -> ==?
 7     equal func(unsafe.Pointer, unsafe.Pointer) bool
 8 
 9 // tophash calculates the tophash value for hash.
10 func tophash(hash uintptr) uint8 {
11     top := uint8(hash >> (sys.PtrSize*8 - 8))
12     if top < minTopHash {
13         top += minTopHash
14     }
15     return top
16 }

  golang為每個類型定義了類型描述器_type,並實現了hashable類型的_type.alg.hash和_type.alg.equal,以支持map的范型,定義了這類key用什麼hash函數、bucket的大小、怎麼比較之類的,通過這個變量來實現范型。

3. map的基本操作

3.1 map的創建

 1 //makemap為make(map [k] v,hint)實現Go map創建。
 2 //如果編譯器已確定映射或第一個存儲桶,可以在堆棧上創建,hmap或bucket可以為非nil。
 3 //如果h!= nil,則可以直接在h中創建map。
 4 //如果h.buckets!= nil,則指向的存儲桶可以用作第一個存儲桶。
 5 func makemap(t *maptype, hint int, h *hmap) *hmap {
 6     if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
 7         hint = 0
 8     }
 9 
10     // 初始化Hmap
11     if h == nil {
12         h = new(hmap)
13     }
14     h.hash0 = fastrand()
15 
16     // 查找將保存請求的元素數的size參數
17     B := uint8(0)
18     for overLoadFactor(hint, B) {
19         B++
20     }
21     h.B = B
22 
23     // 分配初始哈希表
24     // if B == 0, 稍後會延遲分配buckets字段(在mapassign中)
25     //如果提示很大,則將內存清零可能需要一段時間。
26     if h.B != 0 {
27         var nextOverflow *bmap
28         h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
29         if nextOverflow != nil {
30             h.extra = new(mapextra)
31             h.extra.nextOverflow = nextOverflow
32         }
33     }
34 
35     return h
36 }

  hint是一個啟發值,啟發初建map時創建多少個bucket,如果hint是0那麼就先不分配bucket,lazy分配。大概流程就是初始化hmap結構體、設置一下hash seed、bucket數量、實際申請bucket、申請mapextra結構體之類的。   申請buckets的過程:

 1 // makeBucketArray初始化地圖存儲區的後備數組。
 2 // 1 << b是要分配的最小存儲桶數。
 3 // dirtyalloc之前應該為nil或bucket數組
 4 //由makeBucketArray使用相同的t和b參數分配。
 5 //如果dirtyalloc為零,則將分配一個新的支持數組,dirtyalloc將被清除並作為後備數組重用。
 6 func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
 7     base := bucketShift(b)
 8     nbuckets := base
 9     // 對於小b,溢出桶不太可能出現。
10     // 避免計算的開銷。
11     if b >= 4 {
12         //加上估計的溢出桶數
13         //插入元素的中位數
14         //與此值b一起使用。
15         nbuckets += bucketShift(b - 4)
16         sz := t.bucket.size * nbuckets
17         up := roundupsize(sz)
18         if up != sz {
19             nbuckets = up / t.bucket.size
20         }
21     }
22     if dirtyalloc == nil {
23         buckets = newarray(t.bucket, int(nbuckets))
24     } else {
25        // dirtyalloc先前是由上面的newarray(t.bucket,int(nbuckets)),但不能為空。
26         buckets = dirtyalloc
27         size := t.bucket.size * nbuckets
28         if t.bucket.kind&kindNoPointers == 0 {
29             memclrHasPointers(buckets, size)
30         } else {
31             memclrNoHeapPointers(buckets, size)
32         }
33     }
34 
35     if base != nbuckets {
36         //我們預先分配了一些溢出桶。
37         //為了將跟蹤這些溢出桶的開銷降至最低,我們使用的約定是,如果預分配的溢出存儲桶發生了溢出指針為零,則通過碰撞指針還有更多可用空間。
38         //對於最後一個溢出存儲區,我們需要一個安全的非nil指針;只是用bucket。
39         nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
40         last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
41         last.setoverflow(t, (*bmap)(buckets))
42     }
43     return buckets, nextOverflow
44 }

  默認創建2
b個bucket,如果
b大於等於4,那麼就預先額外創建一些overflow bucket。除了最後一個overflow bucket,其餘overflow bucket的overflow指針都是nil,最後一個overflow bucket的overflow指針指向bucket數組第一個元素,作為哨兵,說明到了到結尾了。

3.2 查詢操作

 1 // mapaccess1返回指向h [key]的指針。從不返回nil,而是 如果值類型為零,它將返回對零對象的引用,該鍵不在map中。
 2   //注意:返回的指針可能會使整個map保持活動狀態,因此請不要堅持很長時間。
 3   func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
 4       if raceenabled && h != nil {  //raceenabled是否啟用數據競爭檢測。
 5         callerpc := getcallerpc()
 6         pc := funcPC(mapaccess1)
 7         racereadpc(unsafe.Pointer(h), callerpc, pc)
 8         raceReadObjectPC(t.key, key, callerpc, pc)
 9     }
10     if msanenabled && h != nil {
11         msanread(key, t.key.size)
12     }
13     if h == nil || h.count == 0 {
14         return unsafe.Pointer(&zeroVal[0])
15     }    
16     // 併發訪問檢查
17     if h.flags&hashWriting != 0 {
18         throw("concurrent map read and map write")
19     }
20     
21     // 計算key的hash值
22     alg := t.key.alg
23     hash := alg.hash(key, uintptr(h.hash0)) // alg.hash
24 
25     // hash值對m取餘數得到對應的bucket
26     m := uintptr(1)<<h.B - 1
27     b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
28 
29     // 如果老的bucket還沒有遷移,則在老的bucket裏面找
30     if c := h.oldbuckets; c != nil {
31         if !h.sameSizeGrow() {
32             m >>= 1
33         }
34         oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
35         if !evacuated(oldb) {
36             b = oldb
37         }
38     }
39     
40     // 計算tophash,取高8位
41     top := uint8(hash >> (sys.PtrSize*8 - 8))
42     
43     for {
44         for i := uintptr(0); i < bucketCnt; i++ {
45             // 檢查top值,如高8位不一樣就找下一個
46             if b.tophash[i] != top {
47                 continue
48             }
49             
50             // 取key的地址
51             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
52             
53             if alg.equal(key, k) { // alg.equal
54                 // 取value得地址
55                 v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
56             }
57         }
58        
59         // 如果當前bucket沒有找到,則找bucket鏈的下一個bucket
60         b = b.overflow(t)
61         if b == nil {
62             // 返回零值
63             return unsafe.Pointer(&zeroVal[0])
64         }
65     }
66 }
  1. 先定位出bucket,如果正在擴容,並且這個bucket還沒搬到新的hash表中,那麼就從老的hash表中查找。

  2. 在bucket中進行順序查找,使用高八位進行快速過濾,高八位相等,再比較key是否相等,找到就返回value。如果當前bucket找不到,就往下找overflow bucket,都沒有就返回零值。

  訪問的時候,並不進行擴容的數據搬遷。並且併發有寫操作時拋異常

  注意,t.bucketsize並不是bmap的size,而是bmap加上存儲key、value、overflow指針,所以查找bucket的時候時候用的不是bmap的szie。

3.3 更新/插入過程

 1 // 與mapaccess類似,但是如果map中不存在密鑰,則為該密鑰分配一個插槽
 2 func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
 3     ...
 4     //設置hashWriting調用alg.hash,因為alg.hash可能出現緊急情況后,在這種情況下,我們實際上並沒有進行寫操作.
 5     h.flags |= hashWriting
 6 
 7     if h.buckets == nil {
 8         h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
 9     }
10 
11 again:
12     bucket := hash & bucketMask(h.B)
13     if h.growing() {
14         growWork(t, h, bucket)
15     }
16     b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
17     top := tophash(hash)
18 
19     var inserti *uint8
20     var insertk unsafe.Pointer
21     var val unsafe.Pointer
22     for {
23         for i := uintptr(0); i < bucketCnt; i++ {
24             if b.tophash[i] != top {
25                 if b.tophash[i] == empty && inserti == nil {
26                     inserti = &b.tophash[i]
27                     insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
28                     val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
29                 }
30                 continue
31             }
32             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
33             if t.indirectkey {
34                 k = *((*unsafe.Pointer)(k))
35             }
36             if !alg.equal(key, k) {
37                 continue
38             }
39             // 已經有一個 mapping for key. 更新它.
40             if t.needkeyupdate {
41                 typedmemmove(t.key, k, key)
42             }
43             val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
44             goto done
45         }
46         ovf := b.overflow(t)
47         if ovf == nil {
48             break
49         }
50         b = ovf
51     }
52     //// 如果已經達到了load factor的最大值,就繼續擴容。
53     //找不到鍵的映射。分配新單元格並添加條目。
54     //如果達到最大負載係數或溢出桶過多,並且我們還沒有處於成長的中間,就開始擴容。
55     if !h.growing() && (overLoadFactor(h.count+1, h.B) ||     
56         tooManyOverflowBuckets(h.noverflow, h.B)) {
57         hashGrow(t, h)
58         goto again // //擴大表格會使所有內容無效, so try again
59     }
60     if inserti == nil {
61         // 當前所有存儲桶已滿,請分配一個新的存儲桶
62         newb := h.newoverflow(t, b)
63         inserti = &newb.tophash[0]
64         insertk = add(unsafe.Pointer(newb), dataOffset)
65         val = add(insertk, bucketCnt*uintptr(t.keysize))
66     }
67 
68     // 在插入的位置,存儲鍵值
69     if t.indirectkey {
70         kmem := newobject(t.key)
71         *(*unsafe.Pointer)(insertk) = kmem
72         insertk = kmem
73     }
74     if t.indirectvalue {
75         vmem := newobject(t.elem)
76         *(*unsafe.Pointer)(val) = vmem
77     }
78     typedmemmove(t.key, insertk, key)
79     *inserti = top
80     h.count++
81 
82 done:
83     if h.flags&hashWriting == 0 {
84         throw("concurrent map writes")
85     }
86     h.flags &^= hashWriting
87     if t.indirectvalue {
88         val = *((*unsafe.Pointer)(val))
89     }
90     return val
91 }    
  • hash表如果正在擴容,並且這次要操作的bucket還沒搬到新hash表中,那麼先進行搬遷(擴容細節下面細說)。

  • 在buck中尋找key,同時記錄下第一個空位置,如果找不到,那麼就在空位置中插入數據;如果找到了,那麼就更新對應的value;

  • 找不到key就看下需不需要擴容,需要擴容並且沒有正在擴容,那麼就進行擴容,然後回到第一步。

  • 找不到key,不需要擴容,但是沒有空slot,那麼就分配一個overflow bucket掛在鏈表結尾,用新bucket的第一個slot放存放數據。

3.5 刪除的過程

 1 func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
 2     ...
 3     // Set hashWriting after calling alg.hash, since alg.hash may panic,
 4     // in which case we have not actually done a write (delete).
 5     h.flags |= hashWriting
 6 
 7     bucket := hash & bucketMask(h.B)
 8     if h.growing() {
 9         growWork(t, h, bucket)
10     }
11     b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
12     top := tophash(hash)
13 search:
14     for ; b != nil; b = b.overflow(t) {
15         for i := uintptr(0); i < bucketCnt; i++ {
16             if b.tophash[i] != top {
17                 continue
18             }
19             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
20             k2 := k
21             if t.indirectkey {
22                 k2 = *((*unsafe.Pointer)(k2))
23             }
24             if !alg.equal(key, k2) {
25                 continue
26             }
27             // 如果其中有指針,則僅清除鍵。
28             if t.indirectkey {
29                 *(*unsafe.Pointer)(k) = nil
30             } else if t.key.kind&kindNoPointers == 0 {
31                 memclrHasPointers(k, t.key.size)
32             }
33             v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
34             if t.indirectvalue {
35                 *(*unsafe.Pointer)(v) = nil
36             } else if t.elem.kind&kindNoPointers == 0 {
37                 memclrHasPointers(v, t.elem.size)
38             } else {
39                 memclrNoHeapPointers(v, t.elem.size)
40             }
41         // 若找到把對應的tophash裏面的打上空的標記
42             b.tophash[i] = empty
43             h.count--
44             break search
45         }
46     }
47 
48     if h.flags&hashWriting == 0 {
49         throw("concurrent map writes")
50     }
51     h.flags &^= hashWriting
52 }    
  1. 如果正在擴容,並且操作的bucket還沒搬遷完,那麼搬遷bucket。

  2. 找出對應的key,如果key、value是包含指針的那麼會清理指針指向的內存,否則不會回收內存。

3.6 map的擴容

  通過上面的過程我們知道了,插入、刪除過程都會觸發擴容,判斷擴容的函數如下:

 1 // overLoadFactor 判斷放置在1 << B個存儲桶中的計數項目是否超過loadFactor。
 2 func overLoadFactor(count int, B uint8) bool {
 3     return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)  
 4     //return 元素個數>8 && count>bucket數量*6.5,其中loadFactorNum是常量13,loadFactorDen是常量2,所以是6.5,bucket數量不算overflow bucket.
 5 }
 6 
 7 // tooManyOverflowBuckets 判斷noverflow存儲桶對於1 << B存儲桶的map是否過多。
 8 // 請注意,大多數這些溢出桶必須稀疏使用。如果使用密集,則我們已經觸發了常規map擴容。
 9 func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
10     // 如果閾值太低,我們會做多餘的工作。如果閾值太高,則增大和縮小的映射可能會保留大量未使用的內存。
11     //“太多”意味着(大約)溢出桶與常規桶一樣多。有關更多詳細信息,請參見incrnoverflow。
12     if B > 15 {
13         B = 15
14     }
15     // 譯器在這裏看不到B <16;掩碼B生成較短的移位碼。
16     return noverflow >= uint16(1)<<(B&15)
17 }
18 
19 {
20     ....
21     // 如果我們達到最大負載率或溢流桶過多,並且我們還沒有處於成長的中間,就開始成長。
22     if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
23         hashGrow(t, h)
24         goto again // 擴大表格會使所有內容失效,so try again
25     }
26     //if (不是正在擴容 && (元素個數/bucket數超過某個值 || 太多overflow bucket)) {
27     進行擴容
28     //}
29     ....
30 }

  每次map進行更新或者新增的時候,會先通過以上函數判斷一下load factor。來決定是否擴容。如果需要擴容,那麼第一步需要做的,就是對hash表進行擴容:

 1 //僅對hash表進行擴容,這裏不進行搬遷
 2 func hashGrow(t *maptype, h *hmap) {
 3     // 如果達到負載係數,則增大尺寸。否則,溢出bucket過多,因此,保持相同數量的存儲桶並橫向“增長”。
 4     bigger := uint8(1)
 5     if !overLoadFactor(h.count+1, h.B) {
 6         bigger = 0
 7         h.flags |= sameSizeGrow
 8     }
 9     oldbuckets := h.buckets
10     newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
11 
12     flags := h.flags &^ (iterator | oldIterator)
13     if h.flags&iterator != 0 {
14         flags |= oldIterator
15     }
16     // 提交增長(atomic wrt gc)
17     h.B += bigger
18     h.flags = flags
19     h.oldbuckets = oldbuckets
20     h.buckets = newbuckets
21     h.nevacuate = 0
22     h.noverflow = 0
23 
24     if h.extra != nil && h.extra.overflow != nil {
25         // 將當前的溢出bucket提升到老一代。
26         if h.extra.oldoverflow != nil {
27             throw("oldoverflow is not nil")
28         }
29         h.extra.oldoverflow = h.extra.overflow
30         h.extra.overflow = nil
31     }
32     if nextOverflow != nil {
33         if h.extra == nil {
34             h.extra = new(mapextra)
35         }
36         h.extra.nextOverflow = nextOverflow
37     }
38 
39     //哈希表數據的實際複製是增量完成的,通過growWork()和evacuate()。
40 }

  如果之前為2^n ,那麼下一次擴容是2^(n+1),每次擴容都是之前的兩倍。擴容后需要重新計算每一項在hash中的位置,新表為老的兩倍,此時前文的oldbacket用上了,用來存同時存在的兩個新舊map,等數據遷移完畢就可以釋放oldbacket了。擴容的函數hashGrow其實僅僅是進行一些空間分配,字段的初始化,實際的搬遷操作是在growWork函數中:

1 func growWork(t *maptype, h *hmap, bucket uintptr) {
2     //確保我們遷移了了對應的oldbucket,到我們將要使用的存儲桶。
3     evacuate(t, h, bucket&h.oldbucketmask())
4 
5     // 疏散一箇舊桶以在生長上取得進展
6     if h.growing() {
7         evacuate(t, h, h.nevacuate)
8     }
9 }

  evacuate是進行具體搬遷某個bucket的函數,可以看出
growWork會搬遷兩個bucket,一個是入參bucket;另一個是h.nevacuate。這個nevacuate是一個順序累加的值。可以想想如果每次僅僅搬遷進行寫操作(賦值/刪除)的bucket,那麼有可能某些bucket就是一直沒有機會訪問到,那麼擴容就一直沒法完成,總是在擴容中的狀態,因此會額外進行一次順序遷移,理論上,有N個old bucket,最多N次寫操作,那麼必定會搬遷完。在advanceEvacuationMark中進行nevacuate的累加,遇到已經遷移的bucket會繼續累加,一次最多加1024。

  優點:均攤擴容時間,一定程度上縮短了擴容時間(和gc的引用計數法類似,都是均攤)overLoadFactor函數中有一個常量6.5(loadFactorNum/loadFactorDen)來進行影響擴容時機。這個值的來源是測試取中的結果。

4. map的併發安全性

  map的併發操作不是安全的。併發起兩個goroutine,分別對map進行數據的增加:

 1 func main() {
 2     test := map[int]int {1:1}
 3     go func() {
 4         i := 0
 5         for i < 10000 {
 6             test[1]=1
 7             i++
 8         }
 9     }()
10 
11     go func() {
12         i := 0
13         for i < 10000 {
14             test[1]=1
15             i++
16         }
17     }()
18 
19     time.Sleep(2*time.Second)
20     fmt.Println(test)
21 }
22 
23 //fatal error: concurrent map read and map write

  併發讀寫map結構的數據引起了錯誤。

  解決方案1:加鎖

 1 func main() {
 2     test := map[int]int {1:1}
 3     var s sync.RWMutex
 4     go func() {
 5         i := 0
 6         for i < 10000 {
 7             s.Lock()
 8             test[1]=1
 9             s.Unlock()
10             i++
11         }
12     }()
13 
14     go func() {
15         i := 0
16         for i < 10000 {
17             s.Lock()
18             test[1]=1
19             s.Unlock()
20             i++
21         }
22     }()
23 
24     time.Sleep(2*time.Second)
25     fmt.Println(test)
26 }

  特點:實現簡單粗暴,好理解。但是鎖的粒度為整個map,存在優化空間。適用場景:all。

  解決方案2:sync.Map

 1 func main() {
 2     test := sync.Map{}
 3     test.Store(1, 1)
 4     go func() {
 5         i := 0
 6         for i < 10000 {
 7             test.Store(1, 1)
 8             i++
 9         }
10     }()
11 
12     go func() {
13         i := 0
14         for i < 10000 {
15             test.Store(1, 1)
16             i++
17         }
18     }()
19 
20     time.Sleep(time.Second)
21     fmt.Println(test.Load(1))
22 }

  sync.Map的原理:sync.Map裡頭有兩個map一個是專門用於讀的read map,另一個是才是提供讀寫的dirty map;優先讀read map,若不存在則加鎖穿透讀dirty map,同時記錄一個未從read map讀到的計數,當計數到達一定值,就將read map用dirty map進行覆蓋。
特點:官方出品,通過空間換時間的方式,讀寫分離;不適用於大量寫的場景,會導致read map讀不到數據而進一步加鎖讀取,同時dirty map也會一直晉陞為read map,整體性能較差。適用場景:大量讀,少量寫。

  解決方案3:分段鎖

  這也是數據庫常用的方法,分段鎖每一個讀寫鎖保護一段區間。sync.Map其實也是相當於表級鎖,只不過多讀寫分了兩個map,本質還是一樣的。

  優化方向:將鎖的粒度盡可能降低來提高運行速度。思路:對一個大map進行hash,其內部是n個小map,根據key來來hash確定在具體的那個小map中,這樣加鎖的粒度就變成1/n了。

5. map的GC內存回收

  golang里的map是只增不減的一種數組結構,他只會在刪除的時候進行打標記說明該內存空間已經empty了,不會回收。

 1 var intMap map[int]int
 2 
 3 func main() {
 4     printMemStats("初始化")
 5 
 6     // 添加1w個map值
 7     intMap = make(map[int]int, 10000)
 8     for i := 0; i < 10000; i++ {
 9         intMap[i] = i
10     }
11 
12     // 手動進行gc操作
13     runtime.GC()
14     // 再次查看數據
15     printMemStats("增加map數據后")
16 
17     log.Println("刪除前數組長度:", len(intMap))
18     for i := 0; i < 10000; i++ {
19         delete(intMap, i)
20     }
21     log.Println("刪除后數組長度:", len(intMap))
22 
23     // 再次進行手動GC回收
24     runtime.GC()
25     printMemStats("刪除map數據后")
26 
27     // 設置為nil進行回收
28     intMap = nil
29     runtime.GC()
30     printMemStats("設置為nil后")
31 }
32 
33 func printMemStats(mag string) {
34     var m runtime.MemStats
35     runtime.ReadMemStats(&m)
36     log.Printf("%v:分配的內存 = %vKB, GC的次數 = %v\n", mag, m.Alloc/1024, m.NumGC)
37 }
38 
39 //初始化:分配的內存 = 65KB, GC的次數 = 0
40 //增加map數據后:分配的內存 = 381KB, GC的次數 = 1
41 //刪除前數組長度: 10000
42 //刪除后數組長度: 0
43 //刪除map數據后:分配的內存 = 381KB, GC的次數 = 2
44 //設置為nil后:分配的內存 = 68KB, GC的次數 = 3

  可以看到delete是不會真正的把map釋放的,所以要回收map還是需要設為nil

sync.Map的原理詳解: 

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

數據結構之Array、ArrayList、List、LinkedList對比分析

一、前言:

  在c#數據結構中,集合的應用非常廣泛,無論是做BS架構還是CS架構開發,都離不開集合的使用,比如我們常見的集合包括:Array、ArrayList、List、LinkedList等。這一些數據集合,在功能上都能夠實現集合的存取,但是他們內部有什麼區別,在使用時需要注意一些什麼呢?下面根據個人的經驗,對這一些集合數據的使用做一個簡單的小結,如果說的不對的地方,歡迎指出,多多交流改進。

二、Array集合簡介

  Array集合,也就是數組,是最簡單的數據結構,其存儲的數據在內存空間是連續的,數組有一下一些特點

  • 1.數據存儲是連續的
  • 2.數組長度在定義時就必須制定
  • 3.數組存儲的數據類型都是同一類型
  • 4.數組可以直接通過小標訪問

  優缺點:
   優點:
  1、可以根據索引直接訪問,訪問速度快
   2、數據是安全的,由於數據類型一致性,在存儲使用過程中不涉及
  缺點:
  1、由於數據是連續存儲的,導致插入效率變慢
    2、由於數組長度大小固定,那麼對預期非固定長度的数字不好處理

  練習實例代碼:

 

    /// <summary>
    /// 數組練習操作 
    /// </summary>
    public class ArrayTest
    {
        /// 數組 Array 對於大家來說一點都不陌生
        /// 數組是在內存連續分配的存儲空間,這也導致數組有一下一些特點
        /// 1.數據存儲是連續的
        /// 2.數組長度在定義時就必須制定
        /// 3.數組存儲的數據類型都是同一類型
        /// 4.數組可以直接通過小標訪問
        /// 
        /// 優缺點:
        /// 優點:
        ///     1、可以根據索引直接訪問,訪問速度快
        ///     2、數據是安全的,由於數據類型一致性,在存儲使用過程中不涉及到裝箱拆箱操作
        /// 缺點:
        ///     1、由於數據是連續存儲的,導致插入效率變慢
        ///     2、由於數組長度大小固定,那麼對預期非固定長度的数字不好處理

        /// int類型的數組操作 
        public static void IntArrayTest()
        {

            //// 定義一個秒錶,執行獲取執行時間
            Stopwatch st = new Stopwatch();//實例化類
            st.Start();//開始計時

            Console.WriteLine("開始初始化長度為10000000的int數組:");

            //// 定義一個數組
            int[] nums = new int[10000000];
            for (int i = 0; i < 10000000; i++)
            {
                nums[i] = 1 + 1;
            }

            //需要統計時間的代碼段

            st.Stop();//終止計時
            Console.WriteLine(string.Format("初始化長度為10000的int數組完畢!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));
        }
    }

 

三、ArrayList集合簡介

ArrayList 是Array的升級版,能夠解決Array的一些缺點
ArrayList其內部實現也是Array,只是其長度是可以動態,在其內部用一個變量記錄控制長度,ArrayList有如下一些特點

  • 1.長度不固定
  • 2.可以存儲不同的數據類型(object)
  • 3.同樣支持索引查詢(可以直接通過小標訪問)
  • 4.靈活性更強,以犧牲性能為代價

優缺點:
優點:
1、長度不固定,在定義是不必擔長度溢出
2、可以存儲任意數據類型
3、可根據索引查詢,查詢效率快
缺點:
1、由於長度不固定,執行效率低下,因為超出默認長度(10)后,會自動擴容拷貝數據,犧牲性能
2、由於存儲類型是object,所以在存數據時會有裝箱操作,在取數據時會有拆箱操作,影響效率
3、線程不安全,因為其內部實現是用size、array來共同控制,在新增操作時是非原子操作,所以非安全線程

使用技巧:
在實際使用過程中,為了避免自動擴容,可以預估數據長度,初始化一個數據長度,從而提高效率

練習實例代碼:

    /// <summary>
    /// ArrayList數組練習操作 
    /// </summary>
    public class ArrayListTest
    {
        /// ArrayList 是Array的升級版,能夠解決Array的一些缺點
        /// ArrayList其內部實現也是Array,只是其長度是可以動態,在其內部用一個變量記錄控制長度,ArrayList有如下一些特點
        /// 1.長度不固定
        /// 2.可以存儲不同的數據類型(object)
        /// 3.同樣支持索引查詢(可以直接通過小標訪問)
        /// 4.靈活性更強,以犧牲性能為代價

        /// 優缺點:
        /// 優點:
        ///     1、長度不固定,在定義是不必擔長度溢出
        ///     2、可以存儲任意數據類型
        ///     3、可根據索引查詢,查詢效率快
        /// 缺點:
        ///     1、由於長度不固定,執行效率低下,因為超出默認長度(10)后,會自動擴容拷貝數據,犧牲性能
        ///     2、由於存儲類型是object,所以在存數據時會有裝箱操作,在取數據時會有拆箱操作,影響效率
        ///     3、線程不安全,因為其內部實現是用size、array來共同控制,在新增操作時是非原子操作,所以非安全線程
        ///     
        /// 使用技巧:
        ///     在實際使用過程中,為了避免自動擴容,可以預估數據長度,初始化一個數據長度,從而提高效率

        /// ArrayList操作實例
        public static void ArrayListOpert()
        {

            //// 定義一個秒錶,執行獲取執行時間
            Stopwatch st = new Stopwatch();//實例化類

            //// 需要統計時間的代碼段(統計初始化長度時的執行時間)
            st.Start();//開始計時
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("ArryList集合存儲數據量為10000000,初始化一個長度,執行開始:");

            ArrayList arrayList = new ArrayList(10000000);

            //// 定義一個數組
            for (int i = 0; i < 10000000; i++)
            {
                arrayList.Add(1 + 1);
            }

            st.Stop();//終止計時
            Console.WriteLine(string.Format("ArryList集合存儲數據量為10000000,初始化一個長度,執行完畢:!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));

            //// 需要統計時間的代碼段(統計初始化非指定長度時的執行時間)

            st.Restart();
            Console.WriteLine("");
            Console.WriteLine("ArryList集合存儲數據量為10000000,初始化不指定長度,執行開始:");

            arrayList = new ArrayList();

            //// 定義一個數組
            for (int i = 0; i < 10000000; i++)
            {
                arrayList.Add(1 + 1);
            }

            st.Stop();//終止計時
            Console.WriteLine(string.Format("ArryList集合存儲數據量為10000000,初始化不指定長度,執行完畢:!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));
        }
    }

 

四、List集合簡介

 

隨着c#泛型的推出,為了避免ArrayList一些缺點,微軟推出了List集合
List集合內部還是採用的Array實現,同時在定義時需要指定對應的數據類型
這樣級保留了Array集合的優點,同時也避免了ArrayList集合的數據類型不安全和裝箱帶來的性能犧牲
List特點:

  • 1、數據長度不固定,自動增加
  • 2、存儲相同的數據類型
  • 3、可根據索引查詢,查詢效率快

優缺點:
優點:
1、長度不固定,在定義是不必擔長度溢出
2、存儲相同數據類型的數據,避免的數據的裝箱拆箱,提高了數據處理效率
3、支持索引查詢,查詢效率快
缺點:
1、由於長度不固定,執行效率低下,因為超出默認長度(10)后,會自動擴容拷貝數據,犧牲性能
2、線程不安全,因為其內部實現是用size、array來共同控制,在新增操作時是非原子操作,所以非安全線程

 

練習實例代碼:

   /// <summary>
    /// List練習操作
    /// </summary>
    public class ListTest
    {
        /// 隨着c#泛型的推出,為了避免ArrayList一些缺點,微軟推出了List集合
        /// List集合內部還是採用的Array實現,同時在定義時需要指定對應的數據類型
        /// 這樣級保留了Array集合的優點,同時也避免了ArrayList集合的數據類型不安全和裝箱帶來的性能犧牲
        /// List特點:
        /// 1、數據長度不固定,自動增加
        /// 2、存儲相同的數據類型
        /// 3、可根據索引查詢,查詢效率快
        /// 
        /// 優缺點:
        /// 優點:
        ///     1、長度不固定,在定義是不必擔長度溢出
        ///     2、存儲相同數據類型的數據,避免的數據的裝箱拆箱,提高了數據處理效率
        ///     3、支持索引查詢,查詢效率快
        /// 缺點:
        ///     1、由於長度不固定,執行效率低下,因為超出默認長度(10)后,會自動擴容拷貝數據,犧牲性能
        ///     2、線程不安全,因為其內部實現是用size、array來共同控制,在新增操作時是非原子操作,所以非安全線程

        /// ArrayList操作實例
        public static void ListOpert()
        {
            //// 定義一個秒錶,執行獲取執行時間
            Stopwatch st = new Stopwatch();//實例化類
            st.Start();//開始計時
                       //// 需要統計時間的代碼段(統計初始化長度時的執行時間)
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("List集合存儲數據量為10000000,初始化一個長度,執行開始:");

            List<int> list = new List<int>(10000000);

            //// 定義一個數組
            for (int i = 0; i < 10000000; i++)
            {
                list.Add(1 + 1);
            }

            //需要統計時間的代碼段

            st.Stop();//終止計時
            Console.WriteLine(string.Format("List集合存儲數據量為10000000,初始化一個長度,執行完畢:!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));

            //// 需要統計時間的代碼段(統計初始化非指定長度時的執行時間)
            st.Restart();
            Console.WriteLine("");
            Console.WriteLine("List集合存儲數據量為10000000,初始化不指定長度,執行開始:");

            list = new List<int>();

            //// 定義一個數組
            for (int i = 0; i < 10000000; i++)
            {
                list.Add(1 + 1);
            }

            st.Stop();//終止計時
            Console.WriteLine(string.Format("List集合存儲數據量為10000000,初始化不指定長度,執行完畢:!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));
        }
    }

 

五、LinkedList集合簡介

LinkedList鏈表的底層是採用雙向鏈表的方式實現,
在鏈表(Linked List)中,每一個元素都指向下一個元素,以此來形成了一個鏈(chain)
可以從頭部和尾部插入數據,在存儲內存上採用非連續方式存儲,鏈表有如下一些特點

  • 1、內存存儲上是非連續的
  • 2、能夠支持從頭部和底部同時插入
  • 3、長度是非固定的

優缺點:
優點:
1、由於非連續存儲,中部插入和刪除元素效率高
2、長度非固定,在創建時不用考慮其長度
3、可以沖頭部和底部添加元素
4、數據類型是安全的,在創建時需要指定的數據類型
缺點:
1、由於非連續存儲,不能通過小標訪問,查詢效率低

練習實例代碼:

    /// <summary>
    /// LinkedList練習操作
    /// </summary>
    public class LinkedListTest {
        /// LinkedList鏈表的底層是採用雙向鏈表的方式實現,
        /// 在鏈表(Linked List)中,每一個元素都指向下一個元素,以此來形成了一個鏈(chain)
        /// 可以從頭部和尾部插入數據,在存儲內存上採用非連續方式存儲,鏈表有如下一些特點
        /// 1、內存存儲上是非連續的
        /// 2、能夠支持從頭部和底部同時插入
        /// 3、長度是非固定的
        /// 優缺點:
        /// 優點:
        ///     1、由於非連續存儲,中部插入和刪除元素效率高
        ///     2、長度非固定,在創建時不用考慮其長度
        ///     3、可以沖頭部和底部添加元素
        ///     4、數據類型是安全的,在創建時需要指定的數據類型
        ///  缺點:
        ///     1、由於非連續存儲,不能通過小標訪問,查詢效率低

        /// LinkedList操作實例
        public static void LinkedListTestOpert() {
            //// 定義一個秒錶,執行獲取執行時間
            Stopwatch st = new Stopwatch();//實例化類
            st.Start();//開始計時
                       //// 需要統計時間的代碼段(統計初始化長度時的執行時間)
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Linked集合存儲數據量為10000000,執行開始:");

            LinkedList<int> list = new LinkedList<int>();

            //// 定義一個數組
            for (int i = 0; i < 10000000; i++)
            {
                list.AddFirst(1 + 1);
            }

            //需要統計時間的代碼段

            st.Stop();//終止計時
            Console.WriteLine(string.Format("Linked集合存儲數據量為10000000,執行完畢:!總耗時{0}毫秒", st.ElapsedMilliseconds.ToString()));
        }
    }

 

六、每種集合數據執行結果對比分析

 

    class Program
    {
        static void Main(string[] args)
        {
            //// array數組操作測試
            ArrayTest.IntArrayTest();

            //// arrayList集合操測試
            ArrayListTest.ArrayListOpert();

            //// List集合操作測試
            ListTest.ListOpert();

            //// LinkedList集合操作測試
            LinkedListTest.LinkedListTestOpert();

            ///// 通過測試數據
            //通過測試數據大概可以分析得出一些結論
            //1、整體效率上Array效率最高,ArrayList效率最低,List效率介於Array和ArrayList之間
            //2、ArrayList和List集合,在定義時如果知道數據長度,那麼初始化時,指定長度的效率比不指定的長度效率高
            
            
            //總結:
            //在數據集合使用選擇上給出以下一些建議:
            //1、Array:當元素的數量是固定的,並且需要使用下標時
            //2、ArrayList:當存儲的元素類型不同時
            //3、List:當元素的數量是固定的,並且需要使用下標時
            //4、LinkedList:當元素需要能夠在列表的兩端添加時
            Console.ReadLine();
        }
    }

  執行結果數據

 

通過測試數據大概可以分析得出一些結論
1、整體效率上Array效率最高,ArrayList效率最低,List效率介於Array和ArrayList之間
2、ArrayList和List集合,在定義時如果知道數據長度,那麼初始化時,指定長度的效率比不指定的長度效率高

七、總結:

在數據集合使用選擇上給出以下一些建議:
1、Array:當元素的數量是固定的,並且需要使用下標時
2、ArrayList:當存儲的元素類型不同時,初始化時給一個預估的長度
3、List:當元素的數量是固定的,並且需要使用下標時,初始化時給一個預估的長度
4、LinkedList:當元素需要能夠在列表的兩端添加時

附件:

 

關於這一些練習的代碼,上傳到github,有興趣的可以看一下:

 

 

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

回顧2019年極端天氣事件 全球損失超過1000億美元

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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