吃到飽變吃不飽?電動機車商用資費為何如此難算?

近日 Gogoro 電池月租吃到飽方案引發爭議,對於如何定義商用,以及如何舉證開罰,各界都有不同看法,但 Gogoro 先是強調不再寬待,昨夜又臨時發表聲明政策轉彎,然而品牌形象已經產生傷害。究竟 Gogoro 為何如此堅持,而電動機車的資費又應該怎麼設計會更合理呢?

近日由於網友貼出了一張 Gogoro 寄送的「違規使用通知信」,而讓吃到飽方案成為爭論焦點。我們快速整理一下目前的重點。

  1. Gogoro 月租 899 吃到飽方案,禁止商業使用。
  2. 連續兩個月里程超過 1,600 公里,將被視為商業使用而開罰。
  3. 用戶收到通知後,可以回寄照片證明是用於出遊或長程通勤,即可免罰。
  4. 有路人開始檢舉外送員騎 gogoro 送餐。
  5. Gogoro 發公開信,5/10 起若被檢舉,無論里程長短,將直接變更為商用方案。
  6. Gogoro 修改標準,需連續兩個月里程超過 1,600 公里且被檢舉商業使用才開罰。

且不談這次資費爭議,我們此時可以想的一件事情是,如果燃油車終將被淘汰,電動車需要怎樣的能源費用標準才合理?

假設以每月 1,600 公里為使用里程來計算,目前各種能源方案以 Gogoro 商業型最貴,七期燃油車最便宜,充電式機車在光陽調降月租費用之後,如果採用兩顆電池方案,再加上全部在家充電,費用也相當便宜。

每月騎 1,600 公里,機車能源費用比較。(圖片來源:科技新報製)

不過 IONEX 方案並未說明是否可作為商業使用,而且月租費 398 方案限定綁約兩年,期滿後回到原價 598 元,這個方案還提供 2,000 公里里程,算是相當優惠,如果能夠在家充電的話,是一個不錯的選項。(充電時間約 4 小時)

而燃油車在油價狂降的此刻,商用優勢更為明顯,即使九五汽油價格回升到 30 元,每月費用仍然不到一千元,當然前提是要騎乘七期燃油車,才有每公升 50 公里的低油耗表現。

Gogoro 商業方案的天價,讓人望之卻步,為什麼會訂出這麼高的金額呢?雖然 Gogoro 官方並未明說,但顯然換電站建置與電池成本,如果在頻繁換電情況下,確實讓 Gogoro 電網不堪負荷,而原本換電的優勢也因為電池來不及充飽而打折,因此官方才祭出強硬手腕。

Gogoro 第二次政策轉彎,重新定義吃到飽違約標準。(Source:)

但 Gogoro 滿街跑對於官方來說又是最佳宣傳,所以之前才會容許模糊地帶存在,但是當其他車主開始檢舉之後,官方也不得不有所回應。經過兩次轉彎,最新的定調是,連續兩個月里程超過 1,600 公里且經檢舉才會視為商業使用。換句話說,如果偶爾兼差外送,並不會被追討違約金。

按照 Gogoro 官方說法,為了 99% 的用戶著想,他們願意放寬認定標準,但也看得出來,換電站與電池流通量不足,才是這次爭議真正的核心。否則何必為了 0.3% 的極少數用戶,而鬧出滿城風雨。

而充電式機車像是 e-moving 推出的商用版 ie PICKUP,則看準 Gogoro 在這個領域的不足,期望能夠搶佔商用電動機車市場,電池租賃方案分別為 399 元/月基礎型(家充不限里程)、599 元/月輕量型提供 100 分鐘超級充電時數、799 元/月進階型提供 400 分鐘,合約皆為 2 年一簽,車輛定價則為 83,800 元。

光陽 IONEX 的電池租用方案費用較低,但需要用戶自行在家充電,或是找快充站付費充電。(圖片來源:)

那麼充電式機車會是商用機車的新未來嗎?這仍要取決於未來充電式機車的性能是否有充足進步,以 IONEX 為例,定價 66,800 元新台幣,極速在 60 km/h 以下,在理想狀態下的滿電續航里程為 60 km,而快充到滿需要一個小時(額外付費),要作為商業使用,恐怕還有所不足。更何況當前資費方案,其實是因為用戶量極少,才推出的短期優惠,未來如果用戶增加,會否漲價,或是加入禁止商用條款也未可知。

電動車要商用化的另一項挑戰,來自於維修保養體系,對於商業用戶來說,時間就是金錢,而據點少、難預約的電動機車服務站,在這一點就輸給發展許久的油車一大截了。

以目前兩種電動機車的型態來看,換電系統對於使用者來說比較符合商用需求,但營運商成本較高;充電系統雖然有價格優勢,卻輸在車輛性能與時間彈性上。在可見的將來,全面禁用燃油車幾乎已是定局,若要讓商用機車能夠全面電動化,勢必需要更多的基礎建設(充電站、換電站、保修據點)才能拉低成本與里程焦慮,在那之前,恐怕難有比現在更好的作法。

最終我們建議,Gogoro 不該繼續在模糊地帶打轉,而是仔細估算商用方案的定價,相信如果能夠將方案價格調降到 1,500 元以下,或是與外送平台、快遞業者合作推優惠方案,讓商用族群可以正正當當的「吃到飽」,而不是每個月精算里程才是正途。試想,如果滿街的外送員都騎電動車,不正是電動車的一大勝利嗎?

(合作媒體:。首圖來源:)

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

「居家避疫」未解封,特斯拉宣布延期復工至少一週

美國加州尚未解除居家避疫(Shelter in Place)令,電動車大廠特斯拉(Tesla)位於加州費利蒙(Fremont)的組裝廠生產因此陷入停頓。5 月 1 日,特斯拉以電子郵件通知正在休無薪假的美國員工,將延期復工至少一週,具體時間待定。

CNBC、路透社報導,由於舊金山灣區六郡(包括舊金山、聖克拉拉、聖馬刁、馬林、康特拉哥斯,以及佛利蒙廠所在的阿拉米達郡)下達居家避疫令,特斯拉佛利蒙廠已於 3 月 24 日暫停生產。因疫情未完全降溫,當地政府 4 月 27 日宣布,將居家避疫令期限由 5 月 4 日延長至 5 月底,特斯拉隨後也取消原定復工計畫。

特斯拉北美人力資源主管 Valerie Capers Workman 在內部電子郵件表示,主管通知復工日期之前,休無薪假中的員工將保持休假狀態,預計需至少再等一週。在休無薪假期間,員工保有領取失業救濟金的資格,而在家工作或維持工廠基本營運的員工也將維持現有作業方式,直到另行通知。

受疫情因素影響,4 月 7 日,特斯拉宣布啟動無薪假機制,所有時薪制員工休假至 5 月 4 日,可在家工作或任職必要職務的月薪制員工,依據職等不同,暫時減薪 10%~30% 不等,其餘無法在家工作,且未分配到必要工作的月薪制員工,也必須休無薪假。

在此之前,特斯拉已通知人力派遣公司,針對加州費利蒙廠及內華達州的電動車電池廠實施裁員,影響約數百名約聘員工。

1 日特斯拉股價重挫 10.30% 收 701.32 美元,原因是特斯拉執行長(Elon Musk)在 Twitter 發文稱,他認為特斯拉的股價過高。今年以來,特斯拉股價累計飆漲 67.65%,遠優於大盤標普 500 指數同期間挫跌 12.38%。

(本文內文由  授權使用;首圖來源:Windell Oskay from Sunnyvale, CA, USA [], )

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

談反應式編程在服務端中的應用,數據庫操作優化,提速 Upsert

反應式編程在客戶端編程當中的應用相當廣泛,而當前在服務端中的應用相對被提及較少。本篇將介紹如何在服務端編程中應用響應時編程來改進數據庫操作的性能。

開篇就是結論

接續上一篇《談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒》之後,這次,我們帶來了關於利用反應式編程進行 upsert 優化的案例說明。建議讀者可以先閱讀一下前一篇,這樣更容易理解本篇介紹的方法。

同樣還是利用批量化的思路,將單個 upsert 操作批量進行合併。已達到減少數據庫鏈接消耗從而大幅提升性能的目的。

業務場景

在最近的一篇文章《十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗》中。我們通過激活多個常駐於內存當中的 Claptrap 來實現快速驗證 JWT 正確性的目的。

但,當時有一個技術問題沒有得到解決:

Newbe.Claptrap 框架設計了一個特性:當 Claptrap Deactive 時,可以選擇將快照立即保存到數據庫。因此,當嘗試從集群中關閉一個節點時,如果節點上存在大量的 Claptrap ,那麼將產生大量的數據庫 upsert 操作。瞬間推高數據庫消耗,甚至導致部分錯誤而保存失敗。

一點點代碼

有了前篇的 IBatchOperator,那麼留給這篇的代碼內容就非常少了。

首先,按照使用上一篇的 IBatchOperator 編寫一個支持操作的 Repository,形如以下代碼:

public class BatchUpsert : IUpsertRepository
{
private readonly IDatabase _database;
private readonly IBatchOperator<(int, int), int> _batchOperator;

public BatchUpsert(IDatabase database)
{
_database = database;
var options = new BatchOperatorOptions<(int, int), int>
{
BufferCount = 100,
BufferTime = TimeSpan.FromMilliseconds(50),
DoManyFunc = DoManyFunc
};
_batchOperator = new BatchOperator<(int, int), int>(options);
}

private Task<int> DoManyFunc(IEnumerable<(int, int)> arg)
{
return _database.UpsertMany(arg.ToDictionary(x => x.Item1, x => x.Item2));
}

public Task UpsertAsync(int key, int value)
{
return _batchOperator.CreateTask((key, value));
}
}

然後,只要實現對應數據庫的 UpsertMany 方法,便可以很好地完成這項優化。

各種數據庫的操作

結合 Newbe.Claptrap 現在項目的實際。目前,被支持的數據庫分別有 SQLite、PostgreSQL、MySql 和 MongoDB。以下,分別對不同類型的數據庫的批量 Upsert 操作進行說明。

由於在 Newbe.Claptrap 項目中的 Upsert 需求都是以主鍵作為對比鍵,因此以下也只討論這種情況。

SQLite

根據官方文檔,使用 INSERT OR REPLACE INTO 便可以實現主鍵衝突時替換數據的需求。

具體的語句格式形如以下:

INSERT OR REPLACE INTO TestTable (id, value)
VALUES
(@id0,@value0),
...
(@idn,@valuen);

因此只要直接拼接語句和參數調用即可。需要注意的是,SQLite 的可傳入參數默認為 999,因此拼接的變量也不應大於該數量。

官方文檔:INSERT

PostgreSQL

眾所周知,PostgreSQL 在進行批量寫入時,可以使用高效的 COPY 語句來完成數據的高速導入,這遠遠快於 INSERT 語句。但可惜的是 COPY 並不能支持 ON CONFLICT DO UPDATE 子句。因此,無法使用 COPY 來完成 upsert 需求。

因此,我們還是回歸使用 INSERT 配合 ON CONFLICT DO UPDATE 子句,以及 unnest 函數來完成批量 upsert 的需求。

具體的語句格式形如以下:

INSERT INTO TestTable (id, value)
VALUES (unnest(@ids), unnest(@values))
ON CONFLICT ON CONSTRAINT TestTable_pkey
DO UPDATE SET value=excluded.value;

其中的 ids 和 values 分別為兩個等長的數組對象,unnest 函數可以將數組對象轉換為行數據的形式。

注意,可能會出現 ON CONFLICT DO UPDATE command cannot affect row a second time 錯誤。

因此如果嘗試使用上述方案,需要在傳入數據庫之前,先在程序中去重一遍。而且,通常來說,在程序中進行一次去重可以減少向數據庫中傳入的數據,這本身也很有意義。

官方文檔:unnest 函數
官方文檔:Insert 語句

MySql

MySql 與 SQLite 類似,支持 REPLACE 語法。具體語句形式如下:

REPLACE INTO TestTable (id, value)
VALUES
(@id0,@value0),
...
(@idn,@valuen);

官方文檔:REPLACE 語句

MongoDB

MongoDB 原生支持 bulkWrite 的批量傳輸模式,也支持 replace 的 upsert 語法。因此操作非常簡單。

那麼這裏展示一下 C# 操作方法:

private async Task SaveManyCoreMany(
IDbFactory dbFactory,
IEnumerable<StateEntity> entities)
{
var array = entities as StateEntity[] ?? entities.ToArray();
var items = array
.Select(x => new MongoStateEntity
{
claptrap_id = x.ClaptrapId,
claptrap_type_code = x.ClaptrapTypeCode,
version = x.Version,
state_data = x.StateData,
updated_time = x.UpdatedTime,
})
.ToArray();

var client = dbFactory.GetConnection(_connectionName);
var db = client.GetDatabase(_databaseName);
var collection = db.GetCollection<MongoStateEntity>(_stateCollectionName);

var upsertModels = items.Select(x =>
{
var filter = new ExpressionFilterDefinition<MongoStateEntity>(entity =>
entity.claptrap_id == x.claptrap_id && entity.claptrap_type_code == x.claptrap_type_code);
return new ReplaceOneModel<MongoStateEntity>(filter, x)
{
IsUpsert = true
};
});
await collection.BulkWriteAsync(upsertModels);
}

這是從 Newbe.Claptrap 項目業務場景中給出的代碼,讀者可以結合自身需求進行修改。

官方文檔:db.collection.bulkWrite ()

通用型解法

優化的本質是減少數據庫鏈接的使用,盡可能在一個鏈接內完成更多的工作。因此如果特定的數據庫不支持以上數據庫類似的操作。那麼還是存在一種通用型的解法:

  1. 以盡可能快地方式將數據寫入一臨時表
  2. 將臨時表的數據已連表 update 的方式更新的目標表
  3. 刪除臨時表

UPDATE with a join

性能測試

以 SQLite 為例,嘗試對 12345 條數據進行 2 次 upsert 操作。

單條併發:1 分 6 秒

批量處理:2.9 秒

可以在該鏈接找到測試的代碼。

樣例中不包含有 MySql、PostgreSQL 和 MongoDB 的樣例,因為沒有優化之前,在不提高連接池的情況下,一併發基本就爆炸了。所有優化的結果是直接解決了可用性的問題。

所有的示例代碼均可以在代碼庫中找到。如果 Github Clone 存在困難,也可以點擊此處從 Gitee 進行 Clone

常見問題解答

此處對一些常見的問題進行解答。

客戶端是等待批量操作的結果嗎?

這是一個很多網友提出的問題。答案是:是的。

假設我們公開了一個 WebApi 作為接口,由瀏覽器調用。如果同時有 100 個瀏覽器同時發出請求。

那麼這 100 個請求會被合併,然後寫入數據庫。而在寫入數據庫之前,這些客戶端都不會得到服務端的響應,會一直等待。

這也是該合併方案區別於普通的 “寫隊列,后寫庫” 方案的地方。

原理上講,這種和 bulkcopy 有啥不一樣?

兩者是不相關,必須同時才有作用的功能。
首先,代碼中的 database.InsertMany 就是你提到的 bulkcopy。

這個代碼的關鍵不是 InsertMany ,而是如何將單次的插入請求合併。
試想一下,你可以在 webapi 上公開一個 bulkcopy 的 API。
但是,你無法將來自不同客戶端的請求合併在同一個 API 裏面來調用 bulkcopy。
例如,有一萬個客戶端都在調用你的 API,那怎麼合併這些 API 請求呢?

如果如果通過上面這種方式,雖然你只是對外公開了一個單次插入的 API。你卻實現了來自不同客戶端請求的合併,變得可以使用 bulkcopy 了。這在高併發下很有意義。

另外,這符合開閉的原理,因為你沒有修改 Repository 的 InsertOne 接口,卻實現了 bulkcopy
的效果。

如果批量操作中一個操作異常失敗是否會導致被合併的其他操作全部失敗?

如果業務場景是合併會有影響,那當然不應該合併。

批量操作一個失敗,當然是一起失敗,因為底層的數據庫事務肯定也是一起失敗。

除非批量接口也支持對每個傳入的 ID 做區別對待。典型的,比如 mongodb 的 bulkcopy 可以返回哪些成功哪些失敗,那麼我們就有能力設置不同的 Tcs 狀態。

哪些該合併,哪些不該合併,完全取決於業務。樣例給出的是如果要合併,應該怎麼合併。不會要求所有都要合併。

Insert 和 Upsert 都說了,那 Delete 和 Select 呢?

筆者籠統地將該模式稱為 “反應式批量處理”。要確認業務場景是否應用該模式,需要具備以下這兩個基本的要求:

  • 業務下游的批量處理是否會比累積的單條處理要快,如果會,那可以用
  • 業務上游是否會出現短時間的突增頻率的請求,如果會,那可以用

當然,還需要考量,比如:下游的批量操作能否卻分每個請求的結果等等問題。但以上兩點是一定需要考量的。

那麼以 Delete 為例:

  • Delete Where In 的速度會比 Delete = 的速度快嗎?試一下
  • 會有突增的 Delete 需求嗎?想一下

小小工具 Zeal

筆者是一個完整存儲過程都寫不出來的人。能夠查閱到這些數據庫的文檔,全靠一款名為 Zeal 的離線文檔查看免費軟件。推薦給您,您也值得擁有。

Zeal 官網地址:https://zealdocs.org/

最後但是最重要!

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分佈式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

如果你對該項目感興趣,你可以通過 github issues 提交您的看法。

如果您無法正常訪問 github issue,您也可以發送郵件到 newbe-claptrap@googlegroups.com 來參与我們的討論。

點擊鏈接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5。

您還可以查閱本系列的其他選文:

  • Newbe.Claptrap – 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架
  • 十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗
  • 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
  • 談反應式編程在服務端中的應用,數據庫操作優化,提速 Upsert
  • Newbe.Claptrap 項目周報 1 – 還沒輪影,先用輪跑

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

 

  • 本文作者: newbe36524
  • 本文鏈接: https://www.newbe.pro/Newbe.Claptrap/Reactive-In-Server-2/
  • 版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

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

【其他文章推薦】

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

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

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

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

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

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

SpringBoot2.x的依賴管理

前提

這篇文章是《SpringBoot2.x入門》專輯的第1篇文章,使用的SpringBoot版本為2.3.1.RELEASEJDK版本為1.8

主要梳理一下SpringBoot2.x的依賴關係和依賴的版本管理,依賴版本管理是開發和管理一個SpringBoot項目的前提。

SpringBoot其實是通過starter的形式,對spring-framework進行裝箱,消除了(但是兼容和保留)原來的XML配置,目的是更加便捷地集成其他框架,打造一個完整高效的開發生態。

SpringBoot依賴關係

因為個人不太喜歡Gradle,所以下文都以Maven舉例。

SpringCloud的版本(SpringCloud的正式版是用倫敦地鐵站或者說倫敦某地名的英文名稱作為版本號,例如比較常用的F版本Finchley就是位於倫敦北部芬奇利)管理不同,SpringBoot的依賴組件發布版本格式是:X.Y.Z.RELEASE。因為SpringBoot組件一般會裝箱為starter,所以組件的依賴GAV一般為:org.springframework.boot:spring-boot-starter-${組件名}:X.Y.Z.RELEASE,其中X是主版本,不同的主版本意味着可以放棄兼容性,也就是SpringBoot1.xSpringBoot2.x不保證兼容性,而組件名一般是代表一類中間件或者一類功能,如data-redisspring-boot-starter-data-redis,提供Redis訪問功能)、jdbcspring-boot-starter-jdbc,提供基於JDBC驅動訪問數據庫功能)等等。以SpringBoot當前最新的發布版本2.3.1.RELEASEorg.springframework.boot:spring-boot-starter:jar:2.3.1.RELEASE為例,用mvn dependency:tree分析它的依賴關係如下:

這個依賴樹也印證了starter是基於Spring項目裝箱和擴展的。

SpringBoot依賴管理

如果使用Spring Initializr創建一個SpringBoot項目的話,那麼會發現項目的POM文件中會加入了一個parent元素:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

其實spring-boot-starter-parent相當於作為了當前項目的父模塊,在父模塊裏面管理了當前指定的SpringBoot版本2.3.1.RELEASE所有依賴的第三方庫的統一版本管理,通過spring-boot-starter-parent上溯到最頂層的項目,會找到一個properties元素,裏面統一管理Spring框架和所有依賴到的第三方組件的統一版本號,這樣就能確保對於一個確定的SpringBoot版本,它引入的其他starter不再需要指定版本,同時所有的第三方依賴的版本也是固定的。如項目的POM文件如下:

<!-- 暫時省略其他的配置屬性 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

這樣只需要修改parent元素中的版本號,就能全局更變所有starter的版本號。這種做法其實本質上是把當前項目作為spring-boot-starter-parent的子項目,其實在一定程度上並不靈活。這裏推薦使用另一種方式:通過dependencyManagement元素全局管理SpringBoot版本,適用於單模塊或者多模塊的Maven項目。項目的(父)POM文件如下:

<!-- spring-boot-guide 父POM -->
<properties>
    <spring.boot.version>2.3.1.RELEASE</spring.boot.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

然後需要用到其他starter的時候,只需要在dependencies直接引入即可,不再需要指定版本號,版本號由dependencyManagement中定義的版本號統一管理。

<!-- spring-boot-guide/ch0-dependency 子POM -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>

SpringBoot依賴覆蓋

有些特殊的情況,可能項目中大部分的starter使用的是相對低的版本,但是由於部分新的功能需要使用到更高版本的個別starter,則需要強制引入該高版本的starter。這裏舉一個例子,項目用到的SpringBoot組件的版本是2.1.5.RELEASE,使用的中間件服務Elasticsearch的版本是7.x,而spring-boot-starter-data-elasticsearch支持的版本如下:

理論上可以一下子升級SpringBoot2.3.1.RELEASE,其實也可以直接指定spring-boot-starter-data-elasticsearch的版本覆蓋掉全局的SpringBoot組件版本,這裏應用了Maven依賴調解原則

<!-- 父POM或者全局POM -->
<properties>
    <spring.boot.version>2.1.5.RELEASE</spring.boot.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        <version>2.3.1.RELEASE</version>
    </dependency>
</dependencies>

這樣就能單獨提升spring-boot-starter-data-elasticsearch的版本為2.3.1.RELEASE,其他組件的版本依然保持為2.1.5.RELEASE

小結

目前有兩種常用的方式管理SpringBoot組件的版本(兩種方式二選一):

  1. 配置parent元素,通過項目繼承的方式指定SpringBoot組件的版本號,這是Spring Initializr生成的項目中默認的配置方式。
  2. 配置dependencyManagement元素(推薦此方式),通過(父)POM文件統一指定SpringBoot組件的版本號。

另外,SpringBoot1.x2.x之間有兼容性問題(最明顯的一點是2.x中刪除了1.x中大量的內建類,如果用到了這些SpringBoot中的內建類,容易出現ClassNotFoundException),降級或者升級都有比較大的風險。一般情況下,建議使用同一個大版本進行項目開發,如果確定需要進行大版本切換,請務必做完畢的功能測試。

(本文完 c-1-d e-a-20200628)

技術公眾號(《Throwable文摘》,id:throwable-doge),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

pythonic context manager知多少

Context Managers 是我最喜歡的 python feature 之一,在恰當的時機使用 context manager 使代碼更加簡潔、清晰,更加安全,復用性更好,更加 pythonic。本文簡單介紹一下其使用方法以及常見使用場景。

本文地址:https://www.cnblogs.com/xybaby/p/13202496.html

with statement and context manager

Python’s with statement supports the concept of a runtime context defined by a context manager

new statement “with” to the Python language to make it possible to factor out standard uses of try/finally statements.

在 pep0343 中,通過引入 context manager protocol 來支持 With statement , context manager 是用來管理 context(上下文)的,即保證程序要保持一種特定的狀態 — 無論是否發生異常。可以說,context manager 簡化了對 try-finally 的使用,而且更加安全,更加便於使用。

Transforming Code into Beautiful, Idiomatic Python 中,指出了 context manager 的最顯著的優點:

  • Helps separate business logic from administrative logic
  • Clean, beautiful tools for factoring code and improving code reuse

最廣為人知的例子,就是通過 with statement 來讀寫文件,代碼如下:

with open('test.txt') as f:
    contect = f.read()
    handle_content(content)

上面的代碼幾乎等價於

f = open('test.txt') 
try:
    contect = f.read()
    handle_content(content)
finally:
    f.close()

注意,上面的finally的作用就是保證file.close一定會被調用,也就是資源一定會釋放。不過,很多時候,都會忘了去寫這個finally,而 with statement 就徹底避免了這個問題。

從上述兩段代碼也可以看出,with statement 更加簡潔,而且將核心的業務邏輯(從文件中讀取、處理數據)與其他邏輯(打開、關係文件)相分離,可讀性更強。

實現context manager protocol

一個類只要定義了__enter____exit__方法就實現了context manager 協議

object.__enter__(self)
Enter the runtime context related to this object. The with statement will bind this method’s return value to the target(s) specified in the as clause of the statement, if any.

object.__exit__(self, exc_type, exc_value, traceback)
Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.

__enter__方法在進入這個 context 的時候調用,返回值賦值給 with as X 中的 X

__exit__方法在退出 context 的時候調用,如果沒有異常,后三個參數為 None。如果返回值為 True,則Suppress Exception,所以除非特殊情況都應返回 False。另外注意, __exit__方法本身不應該拋出異常。

例子:BlockGuard

在看c++代碼(如mongodb源碼)的時候,經常看見其用 RAII 實現BlockGuard, 用以保證在離開 Block 的時候執行某些動作,同時,也提供手段來取消執行。

下面用python實現一下:

class BlockGuard(object):
	def __init__(self, fn, *args, **kwargs):
		self._fn = fn
		self._args = args
		self._kwargs = kwargs
		self._canceled = False

	def __enter__(self):
		return self

	def __exit__(self, exc_type, exc_value, traceback):
		if not self._canceled:
			self._fn(*self._args, **self._kwargs)
		self._fn = None
		self._args = None
		self._kwargs = None
		return False

	def cancel(self):
		self._canceled = True


def foo():
	print 'sth should be called'


def test_BlockGuard(cancel_guard):
	print 'test_BlockGuard'
	with BlockGuard(foo) as guard:
		if cancel_guard:
			guard.cancel()
	print 'test_BlockGuard  finish'

用yield實現context manager

標準庫 contextlib 中提供了一些方法,能夠簡化我們使用 context manager,如 contextlib.contextmanager(func) 使我們
無需再去實現一個包含__enter__ __exit__方法的類。

The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any.

例子如下:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

需要注意的是:

  • 一定要寫 try finally,才能保證release_resource邏輯一定被調用
  • 除非特殊情況,不再 catch exception,這就跟 __exit__ 一般不返回True一樣

例子: no_throw

這是業務開發中的一個需求, 比如觀察者模式,不希望因為其中一個觀察者出了 trace 就影響後續的觀察者,就可以這樣做:

from contextlib import contextmanager

@contextmanager
def no_throw(*exceptions):
	try:
		yield
	except exceptions:
		pass

def notify_observers(seq):
	for fn in [sum, len, max, min]:
		with no_throw(Exception):
			print "%s result %s" % (fn.__name__, fn(seq))

if __name__ == '__main__':
	notify_observers([])

在python 3.x 的 contexlib 中,就提供了一個contextlib.suppress(*exceptions), 實現了同樣的效果。

context manager 應用場景

context manager 誕生的初衷就在於簡化 try-finally,因此就適合應用於在需要 finally 的地方,也就是需要清理的地方,比如

  • 保證資源的安全釋放,如 file、lock、semaphore、network connection 等
  • 臨時操作的復原,如果一段邏輯有 setup、prepare,那麼就會對應 cleanup、teardown。

對於第一種情況,網絡連接釋放的例子,後面會結合 pymongo 的代碼展示。

在這裏先來看看第二種用途:保證代碼在一個臨時的、特殊的上下文(context)中執行,且在執行結束之後恢復到之前的上下文環境。

改變工作目錄

from contextlib import contextmanager
import os

@contextmanager
def working_directory(path):
    current_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(current_dir)

with working_directory("data/stuff"):
    pass

臨時文件、文件夾

很多時候會產生一堆臨時文件,比如build的中間狀態,這些臨時文件都需要在結束之後清除。

from tempfile import mkdtemp
from shutil import rmtree

@contextmanager
def temporary_dir(*args, **kwds):
    name = mkdtemp(*args, **kwds)
    try:
        yield name
    finally:
        shutil.rmtree(name)

with temporary_dir() as dirname:
    pass

重定向標準輸出、標準錯誤

@contextmanager
def redirect_stdout(fileobj):
    oldstdout = sys.stdout
    sys.stdout = fileobj
    try:
        yield fieldobj
    finally:
        sys.stdout = oldstdout

在 python3.x 中,已經提供了 contextlib.redirect_stdout contextlib.redirect_stderr 實現上述功能

調整logging level

這個在查問題的適合非常有用,一般生產環境不會輸出 debug level 的日誌,但如果出了問題,可以臨時對某些制定的函數調用輸出debug 日誌

from contextlib import contextmanager
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(ch)


@contextmanager
def change_log_level(level):
	old_level = logger.getEffectiveLevel()
	try:
		logger.setLevel(level)
		yield
	finally:
		logger.setLevel(old_level)


def test_logging():
	logger.debug("this is a debug message")
	logger.info("this is a info message")
	logger.warn("this is a warning message")

with change_log_level(logging.DEBUG):
	test_logging()

pymongo中的context manager使用

在 pymongo 中,封裝了好幾個 context manager,用以

  • 管理 semaphore
  • 管理 connection
  • 資源清理

而且,在 pymongo 中,給出了嵌套使用 context manager 的好例子,用來保證 socket 在使用完之後一定返回連接池(pool)。

# server.py
@contextlib.contextmanager
def get_socket(self, all_credentials, checkout=False):
    with self.pool.get_socket(all_credentials, checkout) as sock_info:
        yield sock_info
        
# pool.py
@contextlib.contextmanager
def get_socket(self, all_credentials, checkout=False):
    sock_info = self._get_socket_no_auth()
    try:
        sock_info.check_auth(all_credentials)
        yield sock_info
    except:
        # Exception in caller. Decrement semaphore.
        self.return_socket(sock_info)
        raise
    else:
        if not checkout:
            self.return_socket(sock_info)

可以看到,server.get_socket 調用了 pool.get_socket, 使用 server.get_socket 的代碼完全不了解、也完全不用關心 socket 的釋放細節,如果把 try-except-finally-else 的邏輯移到所有使用socket的地方,代碼就會很醜、很臃腫。

比如,在mongo_client 中需要使用到 socket:

with server.get_socket(all_credentials) as sock_info:
    sock_info.authenticate(credentials)

references

With statement

Context Managers

contextlib

what-is-the-python-with-statement-designed-for

Transforming Code into Beautiful, Idiomatic Python

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

【其他文章推薦】

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

如何在 asp.net core 3.x 的 startup.cs 文件中獲取注入的服務

一、前言

從 18 年開始接觸 .NET Core 開始,在私底下、工作中也開始慢慢從傳統的 mvc 前後端一把梭,開始轉向 web api + vue,之前自己有個半成品的 asp.net core 2.2 的項目模板,最近幾個月的時間,私下除了學習 Angular 也在對這個模板基於 asp.net core 3.1 進行慢慢補齊功能

因為涉及到底層框架大版本升級,由於某些 breaking changes 必定會造成之前的某些寫法沒辦法繼續使用,趁着端午節假期,在改造模板時,發現沒辦法通過構造函數注入的形式在 Startup 文件中注入某些我需要的服務了,因此本篇文章主要介紹如何在 asp.net core 3.x 的 startup 文件中獲取注入的服務

二、Step by Step

2.1、問題案例

這個問題的發現源於我需要改造模型驗證失敗時返回的錯誤信息,如果你有嘗試的話,在 3.x 版本中你會發現在 Startup 類中,我們沒辦法通過構造函數注入的方式再注入任何其它的服務了,這裏僅以我的代碼中需要解決的這個問題作為案例

在定義接口時,為了降低後期調整的複雜度,在接收參數時,一般會將參數包裝成一個 dto 對象(data transfer object – 數據傳輸對象),不管是提交數據,還是查詢數據,對於這個 dto 中的某些屬性,都會存在一定的卡控,例如 xxx 字段不能為空了,xxx 字段的長度不能超過 30

而在 asp.net core 中,因為會自動進行模型驗證,當不符合 dto 中的屬性要求時,接口會自動返回錯誤信息,默認的返回信息如下圖所示

可以看到,因為這裏其實是按照 rfc7231這個 RFC 協議返回的錯誤信息,這個並不符合我的要求,因此這裏我需要改寫這個返回的錯誤信息

自定義 asp.net core 的模型驗證錯誤信息方法有很多種,我的實現方法如下,因為我需要記錄請求的標識 Id 和錯誤日誌,所以這裏我需要將 ILoggerIHttpContextAccessor 注入到 Startup 類中

/// <summary>
/// 修改模型驗證錯誤返回信息
/// </summary>
/// <param name="services">服務容器集合</param>
/// <param name="logger">日誌記錄實例</param>
/// <param name="httpContextAccessor"></param>
/// <returns></returns>
public static IServiceCollection AddCustomInvalidModelState(this IServiceCollection services,
    ILogger<Startup> logger, IHttpContextAccessor httpContextAccessor)
{
    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.InvalidModelStateResponseFactory = actionContext =>
        {
            // 獲取驗證不通過的字段信息
            //
            var errors = actionContext.ModelState.Where(e => e.Value.Errors.Count > 0)
                .Select(e => new ApiErrorDto
                {
                    Title = "請求參數不符合字段格式要求",
                    Message = e.Value.Errors.FirstOrDefault()?.ErrorMessage
                }).ToList();

            var result = new ApiReturnDto<object>
            {
                TraceId = httpContextAccessor.HttpContext.TraceIdentifier,
                Status = false,
                Error = errors
            };

            logger.LogError($"接口請求參數格式錯誤: {JsonConvert.SerializeObject(result)}");

            return new BadRequestObjectResult(result);
        };
    });

    return services;
}

在 asp.net core 2.x 版本中,你完全可以像在別的類中採用構造函數注入的方式一樣直接注入使用

public class Startup
{
    /// <summary>
    /// 日誌記錄實例
    /// </summary>
    private readonly ILogger<Startup> _logger;

    /// <summary>
    /// Http 請求實例
    /// </summary>
    private readonly IHttpContextAccessor _httpContextAccessor;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="configuration"></param>
    /// <param name="logger"></param>
    /// <param name="httpContextAccessor"></param>
    public Startup(IConfiguration configuration, ILogger<Startup> logger, IHttpContextAccessor httpContextAccessor)
    {
        Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    /// <summary>
    /// 配置實例
    /// </summary>
    public IConfiguration Configuration { get; }

    /// <summary>
    /// This method gets called by the runtime. Use this method to add services to the container.
    /// </summary>
    public void ConfigureServices(IServiceCollection services)
    {
        //注入的其它服務

        // 返回自定義的模型驗證錯誤信息
        services.AddCustomInvalidModelState(_logger, _httpContextAccessor);
    }
}

但是當你直接遷移到 asp.net core 3.x 版本后,你會發現程序會報如下的錯誤,很常見的一個依賴注入的錯誤,源頭直指我們通過構造函數注入的 ILoggerIHttpContextAccessor 接口

2.2、解決方法

根本原因

通過查閱 stackoverflow 發現了這樣的一個問題:How do I write logs from within Startup.cs,在最高贊的回答中提到了在泛型主機(GenericHostBuilder)中,沒辦法注入除 IConfiguration 之外的任何服務到 Startup類中,而泛型主機則是在 asp.net core 3.0 中添加的功能

查了下升級日誌,從中可以看到,在泛型主機中, Startup 類的構造函數注入只支持 IHostEnvironmentIWebHostEnvironmentIConfiguration ,嗯,不好好看別人文檔的鍋

為什麼使用 WebHostBuilder可以,換成 GenericHostBuilder 就不行了呢

按照正常的邏輯來說,對於一個 asp.net core 應用,原則上來說只有有一個根級(root)的依賴注入容器,但是因為我們在 Startup 類中通過構造函數注入的形式注入服務時,告訴程序了我需要這個服務的實例,從而導致在構建 WebHost 時存在了一個單獨的容器,並且這個容器只包含了我們需要使用到的服務信息,之後,因為會創建了一個包含完整服務的依賴注入容器,這裏就會存在一個服務哪怕是單例的也可能會存在註冊兩次的問題,這無疑有些不太合乎規範

在推行泛型主機之後,嚴格控制了只會存在一個依賴注入容器,而所有的服務都是在 Startup.ConfigureServices 方法執行完成后才會註冊到依賴注入容器中,因此沒辦法像之前一樣在根容器註冊完成之前通過構造函數注入的形式使用

解決方案

如果你需要在 Startup.Configure 方法中使用自定義的服務,因為這裏已經完成了各種服務的註冊,和之前一樣,我們直接在方法簽名中包含需要使用到的服務即可

public void Configure(IApplicationBuilder app, IHostEnvironment env, ILogger<Startup> logger)
{
    logger.LogInformation("在 Configure 中使用自定義的服務");
}

如果你需要在 Startup.ConfigureServices 中使用的話,則需要換一種方法

最簡單的方法,直接替換泛型主機為原來的 WebHostBuilder,這樣就可以直接在 Startup 類中注入各種服務接口了,不過,考慮到這一改動其實是在開倒車,所以這裏不推薦採用這種方法

既然沒辦法正向通過依賴注入容器來自動創建我們需要的服務實例,是不是可以通過服務容器,手動去獲取我們需要的服務,也就是被稱為服務定位(Service Locator)的方式來獲取實例

當然,這似乎與依賴注入的思想相左,對於依賴注入來說,我們將所有需要使用的服務定義好,在應用啟動前完成註冊,之後在使用時由依賴注入容器提供服務的實例即可,而服務定位則是我們已經知道存在這個服務了,從容器中獲取出來然後由自己手動的創建實例

雖然服務定位是一種反模式,但是在某些情況下,我們又不得不採用

這裏對於本篇文章開篇中需要解決的問題,我也是採用服務定位的方式,通過構建一個 ServiceProvider 之後,手動的從容器中獲取需要使用的服務實例,調整后的代碼如下

/// <summary>
/// 添加自定義模型驗證失敗時返回的錯誤信息
/// </summary>
/// <param name="services">服務容器集合</param>
/// <returns></returns>
public static IServiceCollection AddCustomInvalidModelState(this IServiceCollection services)
{
    // 構建一個服務的提供程序
    var provider = services.BuildServiceProvider();

    // 獲取需要使用的服務實例
    //
    var logger = provider.GetRequiredService<ILogger<Startup>>();
    var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>();

    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.InvalidModelStateResponseFactory = actionContext =>
        {
            // 獲取失敗信息
            //
            var errors = actionContext.ModelState.Where(e => e.Value.Errors.Count > 0)
                .Select(e => new ApiErrorMessageDto
                {
                    Title = "Request parameters do not meet the field requirements",
                    Message = e.Value.Errors.FirstOrDefault()?.ErrorMessage
                }).ToList();

            var result = new ApiResponseDto<object>
            {
                TraceId = httpContextAccessor.HttpContext.TraceIdentifier,
                Status = false,
                Error = errors
            };

            logger.LogError($"接口請求參數格式錯誤: {JsonSerializer.Serialize(result)}");

            return new BadRequestObjectResult(result);
        };
    });

    return services;
}

對於配置一些需要基於某些服務的服務,這裏也可以通過委託的形式獲取到需要使用的服務實例,示例代碼如下

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMyService>((container) =>
    {
        var logger = container.GetRequiredService<ILogger<MyService>>();
        return new MyService
        {
            Logger = logger
        };
    });
}

三、參考資料

  • ASP.NET Core 3.0 的新增功能

  • Generic Host restricts Startup constructor injection

  • 依賴注入模式

  • Avoiding Startup service injection in ASP.NET Core 3

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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

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

上周熱點回顧(6.22-6.28)

熱點隨筆:

· 程序員敲代碼時耳機里聽的到底是什麼? (風的姿態)
· CPU明明8個核,網卡為啥拚命折騰一號核? (軒轅之風)
· 手把手教你基於SqlSugar4編寫一個可視化代碼生成器(生成實體,以SqlServer為例,文末附源碼) (熊澤-學習中的苦與樂)
· 在運行時生成C# .NET類 (芝麻麻雀)
· 因為我的一個低級錯誤,生產數據庫崩潰了將近半個小時 (鄙人薛某)
· C# 人臉識別庫 (View12138)
· 基於領域驅動設計(DDD)超輕量級快速開發架構 (阿新)
· .Net Core 中GC的工作原理 (她微笑的臉)
· 關於技術文章“標題黨”一事我想說兩句 (精緻碼農)
· 【故障公告】阿里雲 RDS 實例 CPU 100% 故障引發全站無法正常訪問 (博客園團隊)
· 思考:如何保證服務穩定性? (老_張)
· 只看到了別人28歲從字節跳動退休,背後的期權知識你知道嗎? (四猿外)

熱點新聞:

· 瘋王,任正非!
· VSCode彩虹屁插件:釘宮理惠,英雄聯盟版現已生成,你Pick哪一個?
· 全國首創!廣東人坐火車就像坐地鐵一樣方便了:無需提前買票
· 95后快遞小哥獲評“高層次人才”:杭州買房享受百萬元補貼
· 二線手機廠商墜落簡史:鎚子、魅族、金立已成過客
· 外賣員確診背後:年近50 每天接老婆下班 工作14小時
· 歷時26年!中國終於有了自己的全球導航系統
· 看!北斗三號最後一顆組網衛星在太空張開“翅膀” 畫面燃了
· 攻克地獄級難度!川藏鐵路拉林段120座橋樑主體工程全部完工
· 作家王小山控訴攜程欠錢不還:願意下跪懇請梁建章退租
· 知乎熱議:替代Matlab的國產軟件出現 半年內實現Matlab功能的70%
· 這個比QQ空間還古老的網站 是多少女孩的精神家園?

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

Python 圖像處理 OpenCV (12): Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子邊緣檢測技術

前文傳送門:

「Python 圖像處理 OpenCV (1):入門」

「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

「Python 圖像處理 OpenCV (5):圖像的幾何變換」

「Python 圖像處理 OpenCV (6):圖像的閾值處理」

「Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理」

「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

「Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算」

「Python 圖像處理 OpenCV (10):圖像處理形態學之頂帽運算與黑帽運算」

「Python 圖像處理 OpenCV (11):Canny 算子邊緣檢測技術」

引言

前文介紹了 Canny 算子邊緣檢測,本篇繼續介紹 Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子等常用邊緣檢測技術。

Roberts 算子

Roberts 算子,又稱羅伯茨算子,是一種最簡單的算子,是一種利用局部差分算子尋找邊緣的算子。他採用對角線方向相鄰兩象素之差近似梯度幅值檢測邊緣。檢測垂直邊緣的效果好於斜向邊緣,定位精度高,對噪聲敏感,無法抑制噪聲的影響。

1963年, Roberts 提出了這種尋找邊緣的算子。 Roberts 邊緣算子是一個 2×2 的模版,採用的是對角方向相鄰的兩個像素之差。

Roberts 算子的模板分為水平方向和垂直方向,如下所示,從其模板可以看出, Roberts 算子能較好的增強正負 45 度的圖像邊緣。

\[dx = \left[ \begin{matrix} -1 & 0\\ 0 & 1 \\ \end{matrix} \right] \]

\[dy = \left[ \begin{matrix} 0 & -1\\ 1 & 0 \\ \end{matrix} \right] \]

Roberts 算子在水平方向和垂直方向的計算公式如下:

\[d_x(i, j) = f(i + 1, j + 1) – f(i, j) \]

\[d_y(i, j) = f(i, j + 1) – f(i + 1, j) \]

Roberts 算子像素的最終計算公式如下:

\[S = \sqrt{d_x(i, j)^2 + d_y(i, j)^2} \]

今天的公式都是小學生水平,千萬別再說看不懂了。

實現 Roberts 算子,我們主要通過 OpenCV 中的 filter2D() 這個函數,這個函數的主要功能是通過卷積核實現對圖像的卷積運算:

def filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
  • src: 輸入圖像
  • ddepth: 目標圖像所需的深度
  • kernel: 卷積核

接下來開始寫代碼,首先是圖像的讀取,並把這個圖像轉化成灰度圖像,這個沒啥好說的:

# 讀取圖像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化處理圖像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

然後是使用 Numpy 構建卷積核,並對灰度圖像在 x 和 y 的方向上做一次卷積運算:

# Roberts 算子
kernelx = np.array([[-1, 0], [0, 1]], dtype=int)
kernely = np.array([[0, -1], [1, 0]], dtype=int)

x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
y = cv.filter2D(grayImage, cv.CV_16S, kernely)

注意:在進行了 Roberts 算子處理之後,還需要調用convertScaleAbs()函數計算絕對值,並將圖像轉換為8位圖進行显示,然後才能進行圖像融合:

# 轉 uint8 ,圖像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

最後是通過 pyplot 將圖像显示出來:

# 显示圖形
titles = ['原始圖像', 'Roberts算子']
images = [rgb_img, Roberts]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

最終結果如下:

Prewitt 算子

Prewitt 算子是一種一階微分算子的邊緣檢測,利用像素點上下、左右鄰點的灰度差,在邊緣處達到極值檢測邊緣,去掉部分偽邊緣,對噪聲具有平滑作用。

由於 Prewitt 算子採用 3 * 3 模板對區域內的像素值進行計算,而 Robert 算子的模板為 2 * 2 ,故 Prewitt 算子的邊緣檢測結果在水平方向和垂直方向均比 Robert 算子更加明顯。Prewitt算子適合用來識別噪聲較多、灰度漸變的圖像。

Prewitt 算子的模版如下:

\[dx = \left[ \begin{matrix} 1 & 0 & -1\\ 1 & 0 & -1\\ 1 & 0 & -1\\ \end{matrix} \right] \]

\[dy = \left[ \begin{matrix} -1 & -1 & -1\\ 0 & 0 & 0\\ 1 & 1 & 1\\ \end{matrix} \right] \]

在代碼實現上, Prewitt 算子的實現過程與 Roberts 算子比較相似,我就不多介紹,直接貼代碼了:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化處理圖像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Prewitt 算子
kernelx = np.array([[1,1,1],[0,0,0],[-1,-1,-1]],dtype=int)
kernely = np.array([[-1,0,1],[-1,0,1],[-1,0,1]],dtype=int)

x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
y = cv.filter2D(grayImage, cv.CV_16S, kernely)

# 轉 uint8 ,圖像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 用來正常显示中文標籤
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示圖形
titles = ['原始圖像', 'Prewitt 算子']
images = [rgb_img, Prewitt]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

從結果上來看, Prewitt 算子圖像銳化提取的邊緣輪廓,其效果圖的邊緣檢測結果比 Robert 算子更加明顯。

Sobel 算子

Sobel 算子的中文名稱是索貝爾算子,是一種用於邊緣檢測的離散微分算子,它結合了高斯平滑和微分求導。

Sobel 算子在 Prewitt 算子的基礎上增加了權重的概念,認為相鄰點的距離遠近對當前像素點的影響是不同的,距離越近的像素點對應當前像素的影響越大,從而實現圖像銳化並突出邊緣輪廓。

算法模版如下:

\[dx = \left[ \begin{matrix} 1 & 0 & -1\\ 2 & 0 & -2\\ 1 & 0 & -1\\ \end{matrix} \right] \]

\[dy = \left[ \begin{matrix} -1 & -2 & -1\\ 0 & 0 & 0\\ 1 & 2 & 1\\ \end{matrix} \right] \]

Sobel 算子根據像素點上下、左右鄰點灰度加權差,在邊緣處達到極值這一現象檢測邊緣。對噪聲具有平滑作用,提供較為精確的邊緣方向信息。因為 Sobel 算子結合了高斯平滑和微分求導(分化),因此結果會具有更多的抗噪性,當對精度要求不是很高時, Sobel 算子是一種較為常用的邊緣檢測方法。

Sobel 算子近似梯度的大小的計算公式如下:

\[G = \sqrt{d_X^2 + d_y^2} \]

梯度方向的計算公式如下:

\[\theta = \tan^{-1}(\frac {d_x}{d_y}) \]

如果以上的角度 θ 等於零,即代表圖像該處擁有縱向邊緣,左方較右方暗。

在 Python 中,為我們提供了 Sobel() 函數進行運算,整體處理過程和前面的類似,代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化處理圖像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Sobel 算子
x = cv.Sobel(grayImage, cv.CV_16S, 1, 0)
y = cv.Sobel(grayImage, cv.CV_16S, 0, 1)

# 轉 uint8 ,圖像融合
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 用來正常显示中文標籤
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示圖形
titles = ['原始圖像', 'Sobel 算子']
images = [rgb_img, Sobel]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

Laplacian 算子

拉普拉斯( Laplacian )算子是 n 維歐幾里德空間中的一個二階微分算子,常用於圖像增強領域和邊緣提取。

Laplacian 算子的核心思想:判斷圖像中心像素灰度值與它周圍其他像素的灰度值,如果中心像素的灰度更高,則提升中心像素的灰度;反之降低中心像素的灰度,從而實現圖像銳化操作。

在實現過程中, Laplacian 算子通過對鄰域中心像素的四方向或八方向求梯度,再將梯度相加起來判斷中心像素灰度與鄰域內其他像素灰度的關係,最後通過梯度運算的結果對像素灰度進行調整。

Laplacian 算子分為四鄰域和八鄰域,四鄰域是對鄰域中心像素的四方向求梯度,八鄰域是對八方向求梯度。

四鄰域模板如下:

\[H = \left[ \begin{matrix} 0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0\\ \end{matrix} \right] \]

八鄰域模板如下:

\[H = \left[ \begin{matrix} -1 & -1 & -1\\ -1 & 4 & -1\\ -1 & -1 & -1\\ \end{matrix} \right] \]

通過模板可以發現,當鄰域內像素灰度相同時,模板的卷積運算結果為0;當中心像素灰度高於鄰域內其他像素的平均灰度時,模板的卷積運算結果為正數;當中心像素的灰度低於鄰域內其他像素的平均灰度時,模板的卷積為負數。對卷積運算的結果用適當的衰弱因子處理並加在原中心像素上,就可以實現圖像的銳化處理。

在 OpenCV 中, Laplacian 算子被封裝在 Laplacian() 函數中,其主要是利用Sobel算子的運算,通過加上 Sobel 算子運算出的圖像 x 方向和 y 方向上的導數,得到輸入圖像的圖像銳化結果。

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖像
img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化處理圖像
grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Laplacian
dst = cv.Laplacian(grayImage, cv.CV_16S, ksize = 3)
Laplacian = cv.convertScaleAbs(dst)

# 用來正常显示中文標籤
plt.rcParams['font.sans-serif'] = ['SimHei']

# 显示圖形
titles = ['原始圖像', 'Laplacian 算子']
images = [rgb_img, Laplacian]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

最後

邊緣檢測算法主要是基於圖像強度的一階和二階導數,但導數通常對噪聲很敏感,因此需要採用濾波器來過濾噪聲,並調用圖像增強或閾值化算法進行處理,最後再進行邊緣檢測。

最後我先使用高斯濾波去噪之後,再進行邊緣檢測:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖像
img = cv.imread('maliao.jpg')
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 灰度化處理圖像
gray_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 高斯濾波
gaussian_blur = cv.GaussianBlur(gray_image, (3, 3), 0)

# Roberts 算子
kernelx = np.array([[-1, 0], [0, 1]], dtype = int)
kernely = np.array([[0, -1], [1, 0]], dtype = int)
x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# Prewitt 算子
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# Sobel 算子
x = cv.Sobel(gaussian_blur, cv.CV_16S, 1, 0)
y = cv.Sobel(gaussian_blur, cv.CV_16S, 0, 1)
absX = cv.convertScaleAbs(x)
absY = cv.convertScaleAbs(y)
Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)

# 拉普拉斯算法
dst = cv.Laplacian(gaussian_blur, cv.CV_16S, ksize = 3)
Laplacian = cv.convertScaleAbs(dst)

# 展示圖像
titles = ['Source Image', 'Gaussian Image', 'Roberts Image',
          'Prewitt Image','Sobel Image', 'Laplacian Image']
images = [rgb_img, gaussian_blur, Roberts, Prewitt, Sobel, Laplacian]
for i in np.arange(6):
   plt.subplot(2, 3, i+1), plt.imshow(images[i], 'gray')
   plt.title(titles[i])
   plt.xticks([]), plt.yticks([])
plt.show()

示例代碼

如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

參考

https://blog.csdn.net/Eastmount/article/details/89001702

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

4W字的後端面試知識點總結(持續更新)

點贊再看,養成習慣,微信搜索【三太子敖丙】關注這個互聯網苟且偷生的工具人。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

前言

前段時間敖丙不是在複習嘛,很多小夥伴也想要我的複習路線,以及我自己筆記裏面的一些知識點,好了,丙丙花了一個月的時間,整整一個月啊,給大家整理出來了。

一上來我就放個大招好吧,我的複習腦圖,可以說是全得不行,為了防止被盜圖,我加了水印哈。

這期看下去你會發現很硬核,而且我會持續更新,啥也不說了,看在我熬夜一個月滿臉痘痘的份上,你可以點贊了哈哈。

注:如果圖被壓縮了,可以去公眾號【三太子敖丙】回復【複習】獲取原圖

Spring

Spring框架的七大模塊

Spring Core:框架的最基礎部分,提供 IoC 容器,對 bean 進行管理。

Spring Context:繼承BeanFactory,提供上下文信息,擴展出JNDI、EJB、电子郵件、國際化等功能。

Spring DAO:提供了JDBC的抽象層,還提供了聲明性事務管理方法。

Spring ORM:提供了JPA、JDO、Hibernate、MyBatis 等ORM映射層.

Spring AOP:集成了所有AOP功能

Spring Web:提供了基礎的 Web 開發的上下文信息,現有的Web框架,如JSF、Tapestry、Structs等,提供了集成

Spring Web MVC:提供了 Web 應用的 Model-View-Controller 全功能實現。

Bean定義5種作用域

singleton(單例) prototype(原型) request session global session

spring ioc初始化流程?

resource定位 即尋找用戶定義的bean資源,由 ResourceLoader通過統一的接口Resource接口來完成 beanDefinition載入 BeanDefinitionReader讀取、解析Resource定位的資源 成BeanDefinition 載入到ioc中(通過HashMap進行維護BD) BeanDefinition註冊 即向IOC容器註冊這些BeanDefinition, 通過BeanDefinitionRegistery實現

BeanDefinition加載流程?

定義BeanDefinitionReader解析xml的document BeanDefinitionDocumentReader解析document成beanDefinition

DI依賴注入流程? (實例化,處理Bean之間的依賴關係)

過程在Ioc初始化后,依賴注入的過程是用戶第一次向IoC容器索要Bean時觸發

  • 如果設置lazy-init=true,會在第一次getBean的時候才初始化bean, lazy-init=false,會容器啟動的時候直接初始化(singleton bean);

  • 調用BeanFactory.getBean()生成bean的;

  • 生成bean過程運用裝飾器模式產生的bean都是beanWrapper(bean的增強);

    依賴注入怎麼處理bean之間的依賴關係?

    其實就是通過在beanDefinition載入時,如果bean有依賴關係,通過佔位符來代替,在調用getbean時候,如果遇到佔位符,從ioc里獲取bean注入到本實例來

Bean的生命周期?

  • 實例化Bean: Ioc容器通過獲取BeanDefinition對象中的信息進行實例化,實例化對象被包裝在BeanWrapper對象中
  • 設置對象屬性(DI):通過BeanWrapper提供的設置屬性的接口完成屬性依賴注入;
  • 注入Aware接口(BeanFactoryAware, 可以用這個方式來獲取其它 Bean,ApplicationContextAware):Spring會檢測該對象是否實現了xxxAware接口,並將相關的xxxAware實例注入給bean
  • BeanPostProcessor:自定義的處理(分前置處理和後置處理)
  • InitializingBean和init-method:執行我們自己定義的初始化方法
  • 使用
  • destroy:bean的銷毀

IOC:控制反轉:將對象的創建權,由Spring管理. DI(依賴注入):在Spring創建對象的過程中,把對象依賴的屬性注入到類中。

Spring的IOC注入方式

構造器注入 setter方法注入 註解注入 接口注入

怎麼檢測是否存在循環依賴?

Bean在創建的時候可以給該Bean打標,如果遞歸調用回來發現正在創建中的話,即說明了循環依賴了。

Spring如解決Bean循環依賴問題?

Spring中循環依賴場景有:

  • 構造器的循環依賴
  • 屬性的循環依賴
  • singletonObjects:第一級緩存,裏面放置的是實例化好的單例對象; earlySingletonObjects:第二級緩存,裏面存放的是提前曝光的單例對象; singletonFactories:第三級緩存,裏面存放的是要被實例化的對象的對象工廠
  • 創建bean的時候Spring首先從一級緩存singletonObjects中獲取。如果獲取不到,並且對象正在創建中,就再從二級緩存earlySingletonObjects中獲取,如果還是獲取不到就從三級緩存singletonFactories中取(Bean調用構造函數進行實例化后,即使屬性還未填充,就可以通過三級緩存向外提前暴露依賴的引用值(提前曝光),根據對象引用能定位到堆中的對象,其原理是基於Java的引用傳遞),取到后從三級緩存移動到了二級緩存完全初始化之後將自己放入到一級緩存中供其他使用,
  • 因為加入singletonFactories三級緩存的前提是執行了構造器,所以構造器的循環依賴沒法解決。
  • 構造器循環依賴解決辦法:在構造函數中使用@Lazy註解延遲加載。在注入依賴時,先注入代理對象,當首次使用時再創建對象說明:一種互斥的關係而非層次遞進的關係,故稱為三個Map而非三級緩存的緣由 完成注入;

Spring 中使用了哪些設計模式?

  • 工廠模式: spring中的BeanFactory就是簡單工廠模式的體現,根據傳入唯一的標識來獲得bean對象;
  • 單例模式: 提供了全局的訪問點BeanFactory;
  • 代理模式: AOP功能的原理就使用代理模式(1、JDK動態代理。2、CGLib字節碼生成技術代理。)
  • 裝飾器模式: 依賴注入就需要使用BeanWrapper;
  • 觀察者模式: spring中Observer模式常用的地方是listener的實現。如ApplicationListener。
  • 策略模式: Bean的實例化的時候決定採用何種方式初始化bean實例(反射或者CGLIB動態字節碼生成)

AOP 核心概念

1、切面(aspect):類是對物體特徵的抽象,切面就是對橫切關注點的抽象

2、橫切關注點:對哪些方法進行攔截,攔截后怎麼處理,這些關注點稱之為橫切關注點。

3、連接點(joinpoint):被攔截到的點,因為 Spring 只支持方法類型的連接點,所以在Spring 中連接點指的就是被攔截到的方法,實際上連接點還可以是字段或者構造器。

4、切入點(pointcut):對連接點進行攔截的定義

5、通知(advice):所謂通知指的就是指攔截到連接點之後要執行的代碼,通知分為前置、後置、異常、最終、環繞通知五類。

6、目標對象:代理的目標對象

7、織入(weave):將切面應用到目標對象並導致代理對象創建的過程

8、引入(introduction):在不修改代碼的前提下,引入可以在運行期為類動態地添加方法或字段。

解釋一下AOP

傳統oop開發代碼邏輯自上而下的,這個過程中會產生一些橫切性問題,這些問題與我們主業務邏輯關係不大,會散落在代碼的各個地方,造成難以維護,aop思想就是把業務邏輯與橫切的問題進行分離,達到解耦的目的,提高代碼重用性和開發效率;

AOP 主要應用場景有:

  • 記錄日誌
  • 監控性能
  • 權限控制
  • 事務管理

AOP源碼分析

  • @EnableAspectJAutoProxy給容器(beanFactory)中註冊一個AnnotationAwareAspectJAutoProxyCreator對象;

  • AnnotationAwareAspectJAutoProxyCreator對目標對象進行代理對象的創建,對象內部,是封裝JDK和CGlib兩個技術,實現動態代理對象創建的(創建代理對象過程中,會先創建一個代理工廠,獲取到所有的增強器(通知方法),將這些增強器和目標類注入代理工廠,再用代理工廠創建對象);

  • 代理對象執行目標方法,得到目標方法的攔截器鏈,利用攔截器的鏈式機制,依次進入每一個攔截器進行執行

    AOP應用場景

    • 日誌記錄
    • 事務管理
    • 線程池關閉等

AOP使用哪種動態代理?

  • 當bean的是實現中存在接口或者是Proxy的子類,—jdk動態代理;不存在接口,spring會採用CGLIB來生成代理對象;
  • JDK 動態代理主要涉及到 java.lang.reflect 包中的兩個類:Proxy 和 InvocationHandler。
  • Proxy 利用 InvocationHandler(定義橫切邏輯) 接口動態創建 目標類的代理對象。

jdk動態代理

  • 通過bind方法建立代理與真實對象關係,通過Proxy.newProxyInstance(target)生成代理對象
  • 代理對象通過反射invoke方法實現調用真實對象的方法

動態代理與靜態代理區別

  • 靜態代理,程序運行前代理類的.class文件就存在了;
  • 動態代理:在程序運行時利用反射動態創建代理對象<復用性,易用性,更加集中都調用invoke>

CGLIB與JDK動態代理區別

  • Jdk必須提供接口才能使用;
  • C不需要,只要一個非抽象類就能實現動態代理

SpringMVC

springMVC流程:

(1):用戶請求發送給DispatcherServlet,DispatcherServlet調用HandlerMapping處理器映射器;

(2):HandlerMapping根據xml或註解找到對應的處理器,生成處理器對象返回給DispatcherServlet;

(3):DispatcherServlet會調用相應的HandlerAdapter;

(4):HandlerAdapter經過適配調用具體的處理器去處理請求,生成ModelAndView返回給DispatcherServlet

(5):DispatcherServlet將ModelAndView傳給ViewReslover解析生成View返回給DispatcherServlet;

(6):DispatcherServlet根據View進行渲染視圖;

->DispatcherServlet->HandlerMapping->Handler ->DispatcherServlet->HandlerAdapter處理handler->ModelAndView ->DispatcherServlet->ModelAndView->ViewReslover->View ->DispatcherServlet->返回給客戶

Mybatis

Mybatis原理

  • sqlsessionFactoryBuilder生成sqlsessionFactory(單例)
  • 工廠模式生成sqlsession執行sql以及控制事務
  • Mybatis通過動態代理使Mapper(sql映射器)接口能運行起來即為接口生成代理對象將sql查詢到結果映射成pojo

sqlSessionFactory構建過程

  • 解析並讀取配置中的xml創建Configuration對象 (單例)
  • 使用Configruation類去創建sqlSessionFactory(builder模式)

Mybatis一級緩存與二級緩存

默認情況下一級緩存是開啟的,而且是不能關閉的。

  • 一級緩存是指 SqlSession 級別的緩存 原理:使用的數據結構是一個 map,如果兩次中間出現 commit 操作 (修改、添加、刪除),本 sqlsession 中的一級緩存區域全部清空
  • 二級緩存是指可以跨 SqlSession 的緩存。是 mapper 級別的緩存; 原理: 是通過 CacheExecutor 實現的。CacheExecutor其實是 Executor 的代理對象

Zookeeper+eureka+springcloud

SpringBoot啟動流程

  • new springApplication對象,利用spi機制加載applicationContextInitializer, applicationLister接口實例(META-INF/spring.factories);

  • 調run方法準備Environment,加載應用上下文(applicationContext),發布事件 很多通過lister實現

  • 創建spring容器, refreshContext() ,實現starter自動化配置,spring.factories文件加載, bean實例化

    SpringBoot自動配置的原理

    • @EnableAutoConfiguration找到META-INF/spring.factories(需要創建的bean在裏面)配置文件
    • 讀取每個starter中的spring.factories文件

Spring Boot 的核心註解

核心註解是@SpringBootApplication 由以下三種組成

  • @SpringBootConfiguration:組合了 @Configuration 註解,實現配置文件的功能。
  • @EnableAutoConfiguration:打開自動配置的功能。
  • @ComponentScan:Spring組件掃描。

SpringBoot常用starter都有哪些

spring-boot-starter-web – Web 和 RESTful 應用程序; spring-boot-starter-test – 單元測試和集成測試; spring-boot-starter-jdbc – 傳統的 JDBC; spring-boot-starter-security – 使用 SpringSecurity 進行身份驗證和授權; spring-boot-starter-data-jpa – 帶有 Hibernate 的 Spring Data JPA; spring-boot-starter-data-rest – 使用 Spring Data REST 公布簡單的 REST 服務

Spring Boot 的核心配置文件

(1):Application.yml 一般用來定義單個應用級別的,如果搭配 spring-cloud-config 使用

(2).Bootstrap.yml(先加載) 系統級別的一些參數配置,這些參數一般是不變的

Zuul與Gateway區別

(1):zuul則是netflix公司的項目集成在spring-cloud中使用而已, Gateway是spring-cloud的 一個子項目;

(2):zuul不提供異步支持流控等均由hystrix支持, gateway提供了異步支持,提供了抽象負載均衡,提供了抽象流控; 理論上gateway則更適合於提高系統吞吐量(但不一定能有更好的性能),最終性能還需要通過嚴密的壓測來決定

(3):兩者底層實現都是servlet,但是gateway多嵌套了一層webflux框架

(4): zuul可用至其他微服務框架中,內部沒有實現限流、負載均衡;gateway只能用在springcloud中;

Zuul原理分析

(1):請求給zuulservlet處理(HttpServlet子類) zuulservlet中有一個zuulRunner對象,該對象中初始化了RequestContext(存儲請求的數據),RequestContext被所有的zuulfilter共享;

(2): zuulRunner中有 FilterProcessor(zuulfilter的管理器),其從filterloader 中獲取zuulfilter;

(3):有了這些filter之後, zuulservelet執行的Pre-> route-> post 類型的過濾器,如果在執行這些過濾器有錯誤的時候則會執行error類型的過濾器,執行完后把結果返回給客戶端.

Gateway原理分析

(1):請求到達DispatcherHandler, DispatchHandler在IOC容器初始化時會在容器中實例化HandlerMapping接口

(2):用handlerMapping根據請求URL匹配到對應的Route,然後有對應的filter做對應的請求轉發最終response返回去

Zookeeper 工作原理(待查)

Zookeeper 的核心是原子廣播,這個機制保證了各個 server 之間的同步。實現這個機制的協議叫做 Zab 協議。Zab 協議有兩種模式,它們分別是恢復模式和廣播模式。

zoo與eur區別

  • zookeeper保證cp(一致性)
  • eureka保證ap(可用性)
  • zoo在選舉期間註冊服務癱瘓,期間不可用
  • eur各個節點平等關係,只要有一台就可保證服務可用,而查詢到的數據可能不是最新的,可以很好應對網絡故障導致部分節點失聯情況
  • zoo有leader和follower角色,eur各個節點平等
  • zoo採用半數存活原則(避免腦裂),eur採用自我保護機制來解決分區問題
  • eur本質是個工程,zoo只是一個進程 ZooKeeper基於CP,不保證高可用,如果zookeeper正在選主,或者Zookeeper集群中半數以上機器不可用,那麼將無法獲得數據。 Eureka基於AP,能保證高可用,即使所有機器都掛了,也能拿到本地緩存的數據。作為註冊中心,其實配置是不經常變動的,只有發版(發布新的版本)和機器出故障時會變。對於不經常變動的配置來說,CP是不合適的,而AP在遇到問題時可以用犧牲一致性來保證可用性,既返回舊數據,緩存數據。 所以理論上Eureka是更適合做註冊中心。而現實環境中大部分項目可能會使用ZooKeeper,那是因為集群不夠大,並且基本不會遇到用做註冊中心的機器一半以上都掛了的情況。所以實際上也沒什麼大問題。

Hystrix原理(待查)

通過維護一個自己的線程池,當線程池達到閾值的時候,就啟動服務降級,返回fallback默認值

為什麼需要hystrix熔斷

防止雪崩,及時釋放資源,防止系統發生更多的額級聯故障,需要對故障和延遲進行隔離,防止單個依賴關係的失敗影響整個應用程序;

微服務優缺點

  • 每個服務高內聚,松耦合,面向接口編程;
  • 服務間通信成本,數據一致性,多服務運維難度增加,http傳輸效率不如rpc

eureka自我保護機制

  • eur不移除長時間沒收到心跳而應該過期的服務
  • 仍然接受新服務註冊和查詢請求,但是不會同步到其它節點(高可用)
  • 當網絡穩定后,當前實例新註冊信息會同步到其它節點(最終一致性)

MQ對比

ActiveMQ:Apache出品,最早使用的消息隊列產品,時間比較長了,最近版本更新比較緩慢。 RabbitMQ:erlang語言開發,支持很多的協議,非常重量級,更適合於企業級的開發。性能較好,但是不利於做二次開發和維護。 RocketMQ:阿里開源的消息中間件,純Java開發,具有高吞吐量、高可用性、適合大規模分佈式系統應用的特點,分佈式事務。 ZeroMQ:號稱最快的消息隊列系統,尤其針對大吞吐量的需求場景,採用 C 語言實現。 消息隊列的選型需要根據具體應用需求而定,ZeroMQ 小而美,RabbitMQ 大而穩,Kakfa 和 RocketMQ 快而強勁

JAVA基礎

AVL樹與紅黑樹(R-B樹)的區別與聯繫

  • AVL是嚴格的平衡樹,因此在增加或者刪除節點的時候,根據不同情況,旋轉的次數比紅黑樹要多;
  • 紅黑樹是用非嚴格的平衡來換取增刪節點時候旋轉次數的降低開銷;
  • 所以簡單說,查詢多選擇AVL樹,查詢更新次數差不多選紅黑樹
  • AVL樹順序插入和刪除時有20%左右的性能優勢,紅黑樹隨機操作15%左右優勢,現實應用當然一般都是隨機情況,所以紅黑樹得到了更廣泛的應用 索引為B+樹 Hashmap為紅黑樹

為啥redis zset使用跳躍鏈表而不用紅黑樹實現

  • skiplist的複雜度和紅黑樹一樣,而且實現起來更簡單。
  • 在併發環境下紅黑樹在插入和刪除時需要rebalance,性能不如跳錶。

JAVA基本數據類型

(1個字節是8個bit) 整數型:byte(1字節)、short(2字節)、int(4字節)、long(8字節) 浮點型:float(4字節)、double(8字節) 布爾型:boolean(1字節) 字符型:char(2字節)

IO與NIO

包括 類File,outputStream,inputStream,writer,readerseralizable(5類1接口)

NIO三大核心內容 selector(選擇器,用於監聽channel),channel(通道),buffer(緩衝區)

NIO與IO區別,IO面向流,NIO面向緩衝區;io阻塞,nio非阻塞

異常類

throwable為父類,子為error跟exception,exception分runtime(空指針,越界等)跟checkexception(sql,io,找不到類等異常)

LVS(4層與7層)原理

  • 由前端虛擬負載均衡器和後端真實服務器群組成;
  • 請求發送給虛擬服務器后其根據包轉發策略以及負載均衡調度算法轉發給真實服務器
  • 所謂四層(lvs,f5)就是基於IP+端口的負載均衡;七層(nginx)就是基於URL等應用層信息的負載均衡

StringBuilder與StringBuffer

  • StringBuilder 更快;
  • StringBuffer是線程安全的

interrupt/isInterrupted/interrupt區別

  • interrupt() 調用該方法的線程的狀態為將被置為”中斷”狀態(set操作)
  • isinterrupted() 是作用於調用該方法的線程對象所對應的線程的中斷信號是true還是false(get操作)。例如我們可以在A線程中去調用B線程對象的isInterrupted方法,查看的是A
  • interrupted()是靜態方法:內部實現是調用的當前線程的isInterrupted(),並且會重置當前線程的中斷狀態(getandset)

sleep與wait區別

sleep屬於線程類,wait屬於object類;sleep不釋放鎖

CountDownLatch和CyclicBarrier區別

  • con用於主線程等待其他子線程任務都執行完畢后再執行,cyc用於一組線程相互等待大家都達到某個狀態后,再同時執行;
  • CountDownLatch是不可重用的,CyclicBarrier可重用

終止線程方法

  • 使用退出標誌,說線程正常退出;
  • 通過判斷this.interrupted() throw new InterruptedException()來停止 使用String常量池作為鎖對象會導致兩個線程持有相同的鎖,另一個線程不執行,改用其他如new Object()

ThreadLocal的原理和應用

原理:

線程中創建副本,訪問自己內部的副本變量,內部實現是其內部類名叫ThreadLocalMap的成員變量threadLocals,key為本身,value為實際存值的變量副本

應用:

  • 用來解決數據庫連接,存放connection對象,不同線程存放各自session;
  • 解決simpleDateFormat線程安全問題;
  • 會出現內存泄漏,顯式remove..不要與線程池配合,因為worker往往是不會退出的;

threadLocal 內存泄漏問題

如果是強引用,設置tl=null,但是key的引用依然指向ThreadLocal對象,所以會有內存泄漏,而使用弱引用則不會; 但是還是會有內存泄漏存在,ThreadLocal被回收,key的值變成null,導致整個value再也無法被訪問到; 解決辦法:在使用結束時,調用ThreadLocal.remove來釋放其value的引用;

如果我們要獲取父線程的ThreadLocal值呢

ThreadLocal是不具備繼承性的,所以是無法獲取到的,但是我們可以用InteritableThreadLocal來實現這個功能。InteritableThreadLocal繼承來ThreadLocal,重寫了createdMap方法,已經對應的get和set方法,不是在利用了threadLocals,而是interitableThreadLocals變量。

這個變量會在線程初始化的時候(調用init方法),會判斷父線程的interitableThreadLocals變量是否為空,如果不為空,則把放入子線程中,但是其實這玩意沒啥鳥用,當父線程創建完子線程后,如果改變父線程內容是同步不到子線程的。。。同樣,如果在子線程創建完后,再去賦值,也是沒啥鳥用的

線程狀態

線程池有5種狀態:running,showdown,stop,Tidying,TERMINATED。

  • running:線程池處於運行狀態,可以接受任務,執行任務,創建線程默認就是這個狀態了

  • showdown:調用showdown()函數,不會接受新任務,但是會慢慢處理完堆積的任務。

  • stop:調用showdownnow()函數,不會接受新任務,不處理已有的任務,會中斷現有的任務。

  • Tidying:當線程池狀態為showdown或者stop,任務數量為0,就會變為tidying。這個時候會調用鈎子函數terminated()。

  • TERMINATED:terminated()執行完成。

在線程池中,用了一個原子類來記錄線程池的信息,用了int的高3位表示狀態,後面的29位表示線程池中線程的個數。

Java中的線程池是如何實現的?

  • 線程中線程被抽象為靜態內部類Worker,是基於AQS實現的存放在HashSet中;
  • 要被執行的線程存放在BlockingQueue中;
  • 基本思想就是從workQueue中取出要執行的任務,放在worker中處理;

如果線程池中的一個線程運行時出現了異常,會發生什麼

如果提交任務的時候使用了submit,則返回的feature里會存有異常信息,但是如果數execute則會打印出異常棧。但是不會給其他線程造成影響。之後線程池會刪除該線程,會新增加一個worker。

線程池原理

  • 提交一個任務,線程池裡存活的核心線程數小於corePoolSize時,線程池會創建一個核心線程去處理提交的任務
  • 如果線程池核心線程數已滿,即線程數已經等於corePoolSize,一個新提交的任務,會被放進任務隊列workQueue排隊等待執行。
  • 當線程池裡面存活的線程數已經等於corePoolSize了,並且任務隊列workQueue也滿,判斷線程數是否達到maximumPoolSize,即最大線程數是否已滿,如果沒到達,創建非核心線程執行提交的任務。
  • 如果當前的線程數達到了maximumPoolSize,還有新的任務過來的話,直接採用拒絕策略處理。

拒絕策略

  • AbortPolicy直接拋出異常阻止線程運行;
  • CallerRunsPolicy如果被丟棄的線程任務未關閉,則執行該線程;
  • DiscardOldestPolicy移除隊列最早線程嘗試提交當前任務
  • DiscardPolicy丟棄當前任務,不做處理

newFixedThreadPool (固定數目線程的線程池)

  • 阻塞隊列為無界隊列LinkedBlockingQueue
  • 適用於處理CPU密集型的任務,適用執行長期的任務

newCachedThreadPool(可緩存線程的線程池)

  • 阻塞隊列是SynchronousQueue
  • 適用於併發執行大量短期的小任務

newSingleThreadExecutor(單線程的線程池)

  • 阻塞隊列是LinkedBlockingQueue
  • 適用於串行執行任務的場景,一個任務一個任務地執行

newScheduledThreadPool(定時及周期執行的線程池)

  • 阻塞隊列是DelayedWorkQueue
  • 周期性執行任務的場景,需要限制線程數量的場景

java鎖相關

synchronized實現原理

contentionList(請求鎖線程隊列) entryList(有資格的候選者隊列) waitSet(wait方法后阻塞隊列) onDeck(競爭候選者) ower(競爭到鎖線程) !ower(執行成功釋放鎖后狀態); Synchronized 是非公平鎖。

Synchronized 在線程進入 ContentionList 時,等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶佔 OnDeck 線程的鎖資源。

底層是由一對monitorenter和monitorexit指令實現的(監視器鎖)

每個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程:

  • 如果monitor的進入數為0,則該線程進入monitor,然後將進入數設置為1,該線程即為monitor的所有者。
  • 如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.
  • 如果其他線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

ReentrantLock 是如何實現可重入性的 ?

內部自定義了同步器 Sync,加鎖的時候通過CAS 算法 ,將線程對象放到一個雙向鏈表 中,每次獲取鎖的時候 ,看下當前維 護的那個線程ID和當前請求的線程ID是否一樣,一樣就可重入了;

ReentrantLock如何避免死鎖?

  • 響應中斷lockInterruptibly()
  • 可輪詢鎖tryLock()
  • 定時鎖tryLock(long time)

tryLock 和 lock 和 lockInterruptibly 的區別

(1):tryLock 能獲得鎖就返回 true,不能就立即返回 false,

(2):tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false

(3):lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖

(4):lock 和 lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程, lock 不會拋出異常,而 lockInterruptibly 會拋出異常。

CountDownLatch和CyclicBarrier的區別是什麼

CountDownLatch是等待其他線程執行到某一個點的時候,在繼續執行邏輯(子線程不會被阻塞,會繼續執行),只能被使用一次。最常見的就是join形式,主線程等待子線程執行完任務,在用主線程去獲取結果的方式(當然不一定),內部是用計數器相減實現的(沒錯,又特么是AQS),AQS的state承擔了計數器的作用,初始化的時候,使用CAS賦值,主線程調用await()則被加入共享線程等待隊列裏面,子線程調用countDown的時候,使用自旋的方式,減1,知道為0,就觸發喚醒。

CyclicBarrier迴環屏障,主要是等待一組線程到底同一個狀態的時候,放閘。CyclicBarrier還可以傳遞一個Runnable對象,可以到放閘的時候,執行這個任務。CyclicBarrier是可循環的,當調用await的時候如果count變成0了則會重置狀態,如何重置呢,CyclicBarrier新增了一個字段parties,用來保存初始值,當count變為0的時候,就重新賦值。還有一個不同點,CyclicBarrier不是基於AQS的,而是基於RentrantLock實現的。存放的等待隊列是用了條件變量的方式。

synchronized與ReentrantLock區別

  • 都是可重入鎖; R是显示獲取和釋放鎖,s是隱式;
  • R更靈活可以知道有沒有成功獲取鎖,可以定義讀寫鎖,是api級別,s是JVM級別;
  • R可以定義公平鎖;Lock是接口,s是java中的關鍵字

什麼是信號量Semaphore

信號量是一種固定資源的限制的一種併發工具包,基於AQS實現的,在構造的時候會設置一個值,代表着資源數量。信號量主要是應用於是用於多個共享資源的互斥使用,和用於併發線程數的控制(druid的數據庫連接數,就是用這個實現的),信號量也分公平和非公平的情況,基本方式和reentrantLock差不多,在請求資源調用task時,會用自旋的方式減1,如果成功,則獲取成功了,如果失敗,導致資源數變為了0,就會加入隊列裏面去等待。調用release的時候會加一,補充資源,並喚醒等待隊列。

Semaphore 應用

  • acquire() release() 可用於對象池,資源池的構建,比如靜態全局對象池,數據庫連接池;
  • 可創建計數為1的S,作為互斥鎖(二元信號量)

可重入鎖概念

(1):可重入鎖是指同一個線程可以多次獲取同一把鎖,不會因為之前已經獲取過還沒釋放而阻塞;

(2):reentrantLock和synchronized都是可重入鎖

(3):可重入鎖的一個優點是可一定程度避免死鎖

ReentrantLock原理(CAS+AQS)

CAS+AQS隊列來實現

(1):先通過CAS嘗試獲取鎖, 如果此時已經有線程佔據了鎖,那就加入AQS隊列並且被掛起;

(2): 當鎖被釋放之後, 排在隊首的線程會被喚醒CAS再次嘗試獲取鎖,

(3):如果是非公平鎖, 同時還有另一個線程進來嘗試獲取可能會讓這個線程搶到鎖;

(4):如果是公平鎖, 會排到隊尾,由隊首的線程獲取到鎖。

AQS 原理

Node內部類構成的一個雙向鏈表結構的同步隊列,通過控制(volatile的int類型)state狀態來判斷鎖的狀態,對於非可重入鎖狀態不是0則去阻塞;

對於可重入鎖如果是0則執行,非0則判斷當前線程是否是獲取到這個鎖的線程,是的話把state狀態+1,比如重入5次,那麼state=5。 而在釋放鎖的時候,同樣需要釋放5次直到state=0其他線程才有資格獲得鎖

AQS兩種資源共享方式

  • Exclusive:獨佔,只有一個線程能執行,如ReentrantLock
  • Share:共享,多個線程可以同時執行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

CAS原理

內存值V,舊的預期值A,要修改的新值B,當A=V時,將內存值修改為B,否則什麼都不做;

CAS的缺點:

(1):ABA問題; (2):如果CAS失敗,自旋會給CPU帶來壓力; (3):只能保證對一個變量的原子性操作,i++這種是不能保證的

CAS在java中的應用:

(1):Atomic系列

公平鎖與分公平鎖

(1):公平鎖指在分配鎖前檢查是否有線程在排隊等待獲取該鎖,優先分配排隊時間最長的線程,非公平直接嘗試獲取鎖 (2):公平鎖需多維護一個鎖線程隊列,效率低;默認非公平

獨佔鎖與共享鎖

(1):ReentrantLock為獨佔鎖(悲觀加鎖策略) (2):ReentrantReadWriteLock中讀鎖為共享鎖 (3): JDK1.8 郵戳鎖(StampedLock), 不可重入鎖 讀的過程中也允許獲取寫鎖后寫入!這樣一來,我們讀的數據就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖, 樂觀鎖的併發效率更高,但一旦有小概率的寫入導致讀取的數據不一致,需要能檢測出來,再讀一遍就行

4種鎖狀態

  • 無鎖

  • 偏向鎖 會偏向第一個訪問鎖的線程,當一個線程訪問同步代碼塊獲得鎖時,會在對象頭和棧幀記錄里存儲鎖偏向的線程ID,當這個線程再次進入同步代碼塊時,就不需要CAS操作來加鎖了,只要測試一下對象頭裡是否存儲着指向當前線程的偏向鎖 如果偏向鎖未啟動,new出的對象是普通對象(即無鎖,有稍微競爭會成輕量級鎖),如果啟動,new出的對象是匿名偏向(偏向鎖) 對象頭主要包括兩部分數據:Mark Word(標記字段, 存儲對象自身的運行時數據)、class Pointer(類型指針, 是對象指向它的類元數據的指針)

  • 輕量級鎖(自旋鎖) (1):在把線程進行阻塞操作之前先讓線程自旋等待一段時間,可能在等待期間其他線程已經 解鎖,這時就無需再讓線程執行阻塞操作,避免了用戶態到內核態的切換。(自適應自旋時間為一個線程上下文切換的時間)

  • (2):在用自旋鎖時有可能造成死鎖,當遞歸調用時有可能造成死鎖

  • (3):自旋鎖底層是通過指向線程棧中Lock Record的指針來實現的

  • 重量級鎖

輕量級鎖與偏向鎖的區別

(1):輕量級鎖是通過CAS來避免進入開銷較大的互斥操作

(2):偏向鎖是在無競爭場景下完全消除同步,連CAS也不執行

自旋鎖升級到重量級鎖條件

(1):某線程自旋次數超過10次;

(2):等待的自旋線程超過了系統core數的一半;

讀寫鎖了解嘛,知道讀寫鎖的實現方式嘛

常用的讀寫鎖ReentrantReanWritelock,這個其實和reentrantLock相似,也是基於AQS的,但是這個是基於共享資源的,不是互斥,關鍵在於state的處理,讀寫鎖把高16為記為讀狀態,低16位記為寫狀態,就分開了,讀讀情況其實就是讀鎖重入,讀寫/寫讀/寫寫都是互斥的,只要判斷低16位就好了。

zookeeper實現分佈式鎖

(1):利用節點名稱唯一性來實現,加鎖時所有客戶端一起創建節點,只有一個創建成功者獲得鎖,解鎖時刪除節點。

(2):利用臨時順序節點實現,加鎖時所有客戶端都創建臨時順序節點,創建節點序列號最小的獲得鎖,否則監視比自己序列號次小的節點進行等待

(3):方案2比1好處是當zookeeper宕機后,臨時順序節點會自動刪除釋放鎖,不會造成鎖等待;

(4):方案1會產生驚群效應(當有很多進程在等待鎖的時候,在釋放鎖的時候會有很多進程就過來爭奪鎖)。

(5):由於需要頻繁創建和刪除節點,性能上不如redis鎖

volatile變量

(1):變量可見性

(2):防止指令重排序

(3):保障變量單次讀,寫操作的原子性,但不能保證i++這種操作的原子性,因為本質是讀,寫兩次操作

volatile如何保證線程間可見和避免指令重排

volatile可見性是有指令原子性保證的,在jmm中定義了8類原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成為一個原子性操作,這樣子可以確保在讀取的時候都是從主內存讀入,寫入的時候會同步到主內存中(準確來說也是內存屏障),指令重排則是由內存屏障來保證的,由兩個內存屏障:

  • 一個是編譯器屏障:阻止編譯器重排,保證編譯程序時在優化屏障之前的指令不會在優化屏障之後執行。
  • 第二個是cpu屏障:sfence保證寫入,lfence保證讀取,lock類似於鎖的方式。java多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個lock指令,就是增加一個完全的內存屏障指令。

JVM

jre、jdk、jvm的關係:

jdk是最小的開發環境,由jre++java工具組成。

jre是java運行的最小環境,由jvm+核心類庫組成。

jvm是虛擬機,是java字節碼運行的容器,如果只有jvm是無法運行java的,因為缺少了核心類庫。

JVM內存模型

(1):堆<對象,靜態變量,共享

(2):方法區<存放類信息,常量池,共享>(java8移除了永久代(PermGen),替換為元空間(Metaspace))

(3):虛擬機棧<線程執行方法的時候內部存局部變量會存堆中對象的地址等等數據>

(4):本地方法棧<存放各種native方法的局部變量表之類的信息>

(5):程序計數器<記錄當前線程執行到哪一條字節碼指令位置>

對象4種引用

(1):強(內存泄露主因)

(2):軟(只有軟引用的話,空間不足將被回收),適合緩存用

(3):弱(只,GC會回收)

(4):虛引用(用於跟蹤GC狀態)用於管理堆外內存

對象的構成:

一個對象分為3個區域:對象頭、實例數據、對齊填充

對象頭:主要是包括兩部分,1.存儲自身的運行時數據比如hash碼,分代年齡,鎖標記等(但是不是絕對哦,鎖狀態如果是偏向鎖,輕量級鎖,是沒有hash碼的。。。是不固定的)2.指向類的元數據指針。還有可能存在第三部分,那就是數組類型,會多一塊記錄數組的長度(因為數組的長度是jvm判斷不出來的,jvm只有元數據信息)

實例數據:會根據虛擬機分配策略來定,分配策略中,會把相同大小的類型放在一起,並按照定義順序排列(父類的變量也會在哦)

對齊填充:這個意義不是很大,主要在虛擬機規範中對象必須是8字節的整數,所以當對象不滿足這個情況時,就會用佔位符填充

如果判斷一個對象是否存活:

一般判斷對象是否存活有兩種算法,一種是引用計數,另外一種是可達性分析。在java中主要是第二種

java是根據什麼來執行可達性分析的:

根據GC ROOTS。GC ROOTS可以的對象有:虛擬機棧中的引用對象,方法區的類變量的引用,方法區中的常量引用,本地方法棧中的對象引用。

JVM 類加載順序

(1):加載 獲取類的二進制字節流,將其靜態存儲結構轉化為方法區的運行時數據結構

(2):校驗 文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證

(3):準備 在方法區中對類的static變量分配內存並設置類變量數據類型默認的初始值,不包括實例變量,實例變量將會在對象實例化的時候隨着對象一起分配在Java堆中

(4):解析 將常量池內的符號引用替換為直接引用的過程

(5):初始化 為類的靜態變量賦予正確的初始值(Java代碼中被顯式地賦予的值)

JVM三種類加載器

(1):啟動類加載器(home) 加載jvm核心類庫,如java.lang.*等

(2):擴展類加載器(ext), 父加載器為啟動類加載器,從jre/lib/ext下加載類庫

(3):應用程序類加載器(用戶classpath路徑) 父加載器為擴展類加載器,從環境變量中加載類

雙親委派機制

(1):類加載器收到類加載的請求

(2):把這個請求委託給父加載器去完成,一直向上委託,直到啟動類加載器

(3):啟動器加載器檢查能不能加載,能就加載(結束);否則,拋出異常,通知子加載器進行加載

(4):保障類的唯一性和安全性以及保證JDK核心類的優先加載

雙親委派模型有啥作用:

保證java基礎類在不同的環境還是同一個Class對象,避免出現了自定義類覆蓋基礎類的情況,導致出現安全問題。還可以避免類的重複加載。

如何打破雙親委派模型?

(1):自定義類加載器,繼承ClassLoader類重寫loadClass方法;

(2):SPI

tomcat是如何打破雙親委派模型:

tomcat有着特殊性,它需要容納多個應用,需要做到應用級別的隔離,而且需要減少重複性加載,所以劃分為:/common 容器和應用共享的類信息,/server容器本身的類信息,/share應用通用的類信息,/WEB-INF/lib應用級別的類信息。整體可以分為:boostrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加載器)/ShareClassLoader(共享的)->WebAppClassLoader。雖然第一眼是滿足雙親委派模型的,但是不是的,因為雙親委派模型是要先提交給父類裝載,而tomcat是優先判斷是否是自己負責的文件位置,進行加載的。

SPI: (Service Provider interface)

(1):服務提供接口(服務發現機制):

(2):通過加載ClassPath下META_INF/services,自動加載文件里所定義的類

(3):通過ServiceLoader.load/Service.providers方法通過反射拿到實現類的實例

SPI應用?

(1):應用於JDBC獲取數據庫驅動連接過程就是應用這一機制

(2):apache最早提供的common-logging只有接口.沒有實現..發現日誌的提供商通過SPI來具體找到日誌提供商實現類

雙親委派機制缺陷?

(1):雙親委派核心是越基礎的類由越上層的加載器進行加載, 基礎的類總是作為被調用代碼調用的API,無法實現基礎類調用用戶的代碼….

(2):JNDI服務它的代碼由啟動類加載器去加載,但是他需要調獨立廠商實現的應用程序,如何解決? 線程上下文件類加載器(Thread Context ClassLoader), JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI,JDBC

導致fullGC的原因

(1):老年代空間不足

(2):永久代(方法區)空間不足

(3):顯式調用system.gc()

堆外內存的優缺點

Ehcache中的一些版本,各種 NIO 框架,Dubbo,Memcache 等中會用到,NIO包下ByteBuffer來創建堆外內存 堆外內存,其實就是不受JVM控制的內存。

相比於堆內內存有幾個優勢:

減少了垃圾回收的工作,因為垃圾回收會暫停其他的工作。 加快了複製的速度。因為堆內在 flush 到遠程時,會先複製到直接內存(非堆內存),然後在發送;而堆外內存相當於省略掉了複製這項工作。 可以擴展至更大的內存空間。比如超過 1TB 甚至比主存還大的空間。

缺點總結如下:

堆外內存難以控制,如果內存泄漏,那麼很難排查,通過-XX:MaxDirectMemerySize來指定,當達到閾值的時候,調用system.gc來進行一次full gc 堆外內存相對來說,不適合存儲很複雜的對象。一般簡單的對象或者扁平化的比較適合 jstat查看內存回收概況,實時查看各個分區的分配回收情況, jmap查看內存棧,查看內存中對象佔用大小, jstack查看線程棧,死鎖,性能瓶頸

JVM七種垃圾收集器

(1): Serial 收集器 複製算法,單線程,新生代)

(2): ParNew 收集器(複製算法,多線程,新生代)

(3): Parallel Scavenge 收集器(多線程,複製算法,新生代,高吞吐量)

(4):Serial Old 收集器(標記-整理算法,老年代)

(5):Parallel Old 收集器(標記-整理算法,老年代,注重吞吐量的場景下,jdk8默認採用 Parallel Scavenge + Parallel Old 的組合)

(6):CMS 收集器(標記-清除算法,老年代,垃圾回收線程幾乎能做到與用戶線程同時工作,吞吐量低,內存碎片)以犧牲吞吐量為代價來獲得最短回收停頓時間-XX:+UseConcMarkSweepGC jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默認垃圾收集器G1

使用場景:

(1):應用程序對停頓比較敏感

(2):在JVM中,有相對較多存活時間較長的對象(老年代比較大)會更適合使用CMS

cms垃圾回收過程:

(1):初始標識<找到gcroot(stw)>

GC Roots有以下幾種:

1:系統類加載器加載的對象

2:處於激活狀態的線程

3:JNI棧中的對象

4:正在被用於同步的各種鎖對象

5:JVM自身持有的對象,比如系統類加載器等。

(2):併發標記(三色標記算法) 三色標記算法處理併發標記出現對象引用變化情況: 黑:自己+子對象標記完成 灰:自己完成,子對象未完成 白:未標記; 併發標記 黑->灰->白 重新標記 灰->白引用消失,黑引用指向->白,導致白漏標 cms處理辦法是incremental update方案 (增量更新)把黑色變成灰色 多線程下併發標記依舊會產生漏標問題,所以cms必須remark一遍(jdk1.9以後不用cms了)

G1 處理方案:

SATB(snapshot at the begining)把白放入棧中,標記過程是和應用程序併發運行的(不需要Stop-The-World) 這種方式會造成某些是垃圾的對象也被當做是存活的,所以G1會使得佔用的內存被實際需要的內存大。不過下一次就回收了 ZGC 處理方案: 顏色指針(color pointers) 2*42方=4T

(3):重新標記(stw)

(4)併發清理

備註:重新標記是防止標記成垃圾之後,對象被引用

(5):G1 收集器(新生代 + 老年代,在多 CPU 和大內存的場景下有很好的性能) G1在java9 便是默認的垃圾收集器,是cms 的替代者 邏輯分代,用分區(region)的思想(默認分2048份) 還是有stw 為解決CMS算法產生空間碎片HotSpot提供垃圾收集器,通過-XX:+UseG1GC來啟用

G1中提供了三種模式垃圾回收模式

(1):young gc(eden region被耗盡無法申請內存時,就會觸發)

(2):mixed gc(當老年代大小占整個堆大小百分比達到該閾值時,會觸發)

(3):full gc(對象內存分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發)

(8):ZGC和shenandoah (oracle產收費) no stw

arthas 監控工具

(1):dashboard命令查看總體jvm運行情況

(2):jvm显示jvm詳細信息

(3):thread 显示jvm裏面所有線程信息(類似於jstack) 查看死鎖線程命令thread -b

(4):sc * 显示所有類(search class)

(5):trace 跟蹤方法

定位頻繁full GC,堆內存滿 oom

第一步:jps獲取進程號 第二步:jmap -histo pid | head -20 得知有個對象在不斷創建 備註:jmap如果線上服務器堆內存特別大,,會卡死需堆轉存(一般會說在測試環境壓測,導出轉存) -XX:+HeapDumpOnOutOfMemoryError或jmap -dumpLformat=b,file=xxx pid 轉出文件進行分析 (arthas沒有實現jmap命令)heapdump –live /xxx/xx.hprof導出文件

G1垃圾回收器(重點)

回收過程 (1):young gc(年輕代回收)–當年輕代的Eden區用盡時–stw 第一階段,掃描根。 根是指static變量指向的對象,正在執行的方法調用鏈條上的局部變量等 第二階段,更新RS(Remembered Sets)。 處理dirty card queue中的card,更新RS。此階段完成后,RS可以準確的反映老年代對所在的內存分段中對象的引用 第三階段,處理RS。 識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認為是存活的對象。 第四階段,複製對象。 此階段,對象樹被遍歷,Eden區內存段中存活的對象會被複制到Survivor區中空的內存分段 第五階段,處理引用。 處理Soft,Weak,Phantom,Final,JNI Weak 等引用。

(2):concrruent marking(老年代併發標記) 當堆內存使用達到一定值(默認45%)時,不需要Stop-The-World,在併發標記前先進行一次young gc

(3):混合回收(mixed gc) 併發標記過程結束以後,緊跟着就會開始混合回收過程。混合回收的意思是年輕代和老年代會同時被回收

(4):Full GC? Full GC是指上述方式不能正常工作,G1會停止應用程序的執行,使用單線程的內存回收算法進行垃圾回收,性能會非常差,應用程序停頓時間會很長。要避免Full GC的發生,一旦發生需要進行調整。

什麼時候發生Full GC呢?

比如堆內存太小,當G1在複製存活對象的時候沒有空的內存分段可用,則會回退到full gc,這種情況可以通過增大內存解決

儘管G1堆內存仍然是分代的,但是同一個代的內存不再採用連續的內存結構

年輕代分為Eden和Survivor兩個區,老年代分為Old和Humongous兩個區

新分配的對象會被分配到Eden區的內存分段上

Humongous區用於保存大對象,如果一個對象佔用的空間超過內存分段Region的一半;

如果對象的大小超過一個甚至幾個分段的大小,則對象會分配在物理連續的多個Humongous分段上。

Humongous對象因為佔用內存較大並且連續會被優先回收

為了在回收單個內存分段的時候不必對整個堆內存的對象進行掃描(單個內存分段中的對象可能被其他內存分段中的對象引用)引入了RS數據結構。RS使得G1可以在年輕代回收的時候不必去掃描老年代的對象,從而提高了性能。每一個內存分段都對應一個RS,RS保存了來自其他分段內的對象對於此分段的引用

JVM會對應用程序的每一個引用賦值語句object.field=object進行記錄和處理,把引用關係更新到RS中。但是這個RS的更新並不是實時的。G1維護了一個Dirty Card Queue

那為什麼不在引用賦值語句處直接更新RS呢?

這是為了性能的需要,使用隊列性能會好很多。

線程本地分配緩衝區(TLAB: Thread Local Allocation Buffer)?

棧上分配->tlab->堆上分配 由於堆內存是應用程序共享的,應用程序的多個線程在分配內存的時候需要加鎖以進行同步。為了避免加鎖,提高性能每一個應用程序的線程會被分配一個TLAB。TLAB中的內存來自於G1年輕代中的內存分段。當對象不是Humongous對象,TLAB也能裝的下的時候,對象會被優先分配於創建此對象的線程的TLAB中。這樣分配會很快,因為TLAB隸屬於線程,所以不需要加鎖

PLAB: Promotion Thread Local Allocation Buffer

G1會在年輕代回收過程中把Eden區中的對象複製(“提升”)到Survivor區中,Survivor區中的對象複製到Old區中。G1的回收過程是多線程執行的,為了避免多個線程往同一個內存分段進行複製,那麼複製的過程也需要加鎖。為了避免加鎖,G1的每個線程都關聯了一個PLAB,這樣就不需要進行加鎖了

OOM問題定位方法

(1):jmap -heap 10765如上圖,可以查看新生代,老生代堆內存的分配大小以及使用情況;

(2):jstat 查看GC收集情況

(3):jmap -dump:live,format=b,file=到本地

(4):通過MAT工具打開分析

DUBBO

dubbo流程

(1):生產者(Provider)啟動,向註冊中心(Register)註冊

(2):消費者(Consumer)訂閱,而後註冊中心通知消費者

(3):消費者從生產者進行消費

(4):監控中心(Monitor)統計生產者和消費者

Dubbo推薦使用什麼序列化框架,還有哪些?

推薦使用Hessian序列化,還有Duddo、FastJson、Java自帶序列化

Dubbo默認使用的是什麼通信框架,還有哪些?

默認使用 Netty 框架,也是推薦的選擇,另外內容還集成有Mina、Grizzly。

Dubbo有哪幾種負載均衡策略,默認是哪種?

(1):隨機調用<默認>

(2):權重輪詢

(3):最少活躍數

(4):一致性Hash

RPC流程

(1)消費者調用需要消費的服務,

(2):客戶端存根將方法、入參等信息序列化發送給服務端存根

(3):服務端存根反序列化操作根據解碼結果調用本地的服務進行相關處理

(4):本地服務執行具體業務邏輯並將處理結果返回給服務端存根

(5):服務端存根序列化

(6):客戶端存根反序列化

(7):服務消費方得到最終結果

RPC框架的實現目標PC框架的實現目標是把調用、編碼/解碼的過程給封裝起來,讓用戶感覺上像調用本地服務一樣的調用遠程服務

服務暴露、服務引用、服務調用(TODO)

Redis

redis單線程為什麼執行速度這麼快?

(1):純內存操作,避免大量訪問數據庫,減少直接讀取磁盤數據,redis將數據儲存在內存裏面,讀寫數據的時候都不會受到硬盤 I/O 速度的限制,所以速度快

(2):單線程操作,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗

(3):採用了非阻塞I/O多路復用機制

Redis數據結構底層實現

String:

(1)Simple dynamic string(SDS)的數據結構

struct sdshdr{
 //記錄buf數組中已使用字節的數量
 //等於 SDS 保存字符串的長度
 int len;
 //記錄 buf 數組中未使用字節的數量
 int free
 //字節數組,用於保存字符串
 char buf[];
}

它的優點: (1)不會出現字符串變更造成的內存溢出問題

(2)獲取字符串長度時間複雜度為1

(3)空間預分配, 惰性空間釋放free字段,會默認留夠一定的空間防止多次重分配內存

應用場景: String 緩存結構體用戶信息,計數

Hash:

數組+鏈表的基礎上,進行了一些rehash優化; 1.Reids的Hash採用鏈地址法來處理衝突,然後它沒有使用紅黑樹優化。

2.哈希表節點採用單鏈表結構。

3.rehash優化 (採用分而治之的思想,將龐大的遷移工作量劃分到每一次CURD中,避免了服務繁忙)

應用場景: 保存結構體信息可部分獲取不用序列化所有字段

List:

應用場景: (1):比如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現

(2):list的實現為一個雙向鏈表,即可以支持反向查找和遍歷

Set:

內部實現是一個 value為null的HashMap,實際就是通過計算hash的方式來快速排重的,這也是set能提供判斷一個成員 是否在集合內的原因。 應用場景: 去重的場景,交集(sinter)、並集(sunion)、差集(sdiff),實現如共同關注、共同喜好、二度好友等功能

Zset:

內部使用HashMap和跳躍表(SkipList)來保證數據的存儲和有序,HashMap里放的是成員到score的映射,而跳躍表裡存放的是所有的成員,排序依據是HashMap里存的score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。 跳錶:每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的 應用場景: 實現延時隊列

redis事務

(1):Multi開啟事務

(2):Exec執行事務塊內命令

(3):Discard 取消事務

(4):Watch 監視一個或多個key,如果事務執行前key被改動,事務將打斷

redis事務的實現特徵

(1):所有命令都將會被串行化的順序執行,事務執行期間,Redis不會再為其它客戶端的請求提供任何服務,從而保證了事物中的所有命令被原子的執行

(2):Redis事務中如果有某一條命令執行失敗,其後的命令仍然會被繼續執行

(3):在事務開啟之前,如果客戶端與服務器之間出現通訊故障並導致網絡斷開,其後所有待執行的語句都將不會被服務器執行。然而如果網絡中斷事件是發生在客戶端執行EXEC命令之後,那麼該事務中的所有命令都會被服務器執行

(4):當使用Append-Only模式時,Redis會通過調用系統函數write將該事務內的所有寫操作在本次調用中全部寫入磁盤。

然而如果在寫入的過程中出現系統崩潰,如電源故障導致的宕機,那麼此時也許只有部分數據被寫入到磁盤,而另外一部分數據卻已經丟失。

Redis服務器會在重新啟動時執行一系列必要的一致性檢測,一旦發現類似問題,就會立即退出並給出相應的錯誤提示。此時,我們就要充分利用Redis工具包中提供的redis-check-aof工具,該工具可以幫助我們定位到數據不一致的錯誤,並將已經寫入的部分數據進行回滾。修復之後我們就可以再次重新啟動Redis服務器了

Redis的同步機制?

(1):全量拷貝, 1.slave第一次啟動時,連接Master,發送PSYNC命令,

2.master會執行bgsave命令來生成rdb文件,期間的所有寫命令將被寫入緩衝區。

  1. master bgsave執行完畢,向slave發送rdb文件

  2. slave收到rdb文件,丟棄所有舊數據,開始載入rdb文件

  3. rdb文件同步結束之後,slave執行從master緩衝區發送過來的所以寫命令。

  4. 此後 master 每執行一個寫命令,就向slave發送相同的寫命令。

    (2):增量拷貝 如果出現網絡閃斷或者命令丟失等異常情況,從節點之前保存了自身已複製的偏移量和主節點的運行ID

  5. 主節點根據偏移量把複製積壓緩衝區里的數據發送給從節點,保證主從複製進入正常狀態。

    redis集群模式性能優化

    (1) Master最好不要做任何持久化工作,如RDB內存快照和AOF日誌文件

    (2) 如果數據比較重要,某個Slave開啟AOF備份數據,策略設置為每秒同步一次

    (3) 為了主從複製的速度和連接的穩定性,Master和Slave最好在同一個局域網內

    (4) 盡量避免在壓力很大的主庫上增加從庫

    (5) 主從複製不要用圖狀結構,用單向鏈表結構更為穩定,即:Master <- Slave1 <- Slave2 <- Slave3…這樣的結構方便解決單點故障問題,實現Slave對Master的替換。如果Master掛了,可以立刻啟用Slave1做Master,其他不變。

    Redis集群方案

    (1):官方cluster方案

    (2):twemproxy

    代理方案twemproxy是一個單點,很容易對其造成很大的壓力,所以通常會結合keepalived來實twemproy的高可用

    (3):codis 基於客戶端來進行分片

集群不可用場景

(1):master掛掉,且當前master沒有slave

(2):集群超過半數以上master掛掉,無論是否有slave集群進入fail狀態

redis 最適合的場景

(1):會話緩存session cache

(2):排行榜/計數器ZRANGE

(3):發布/訂閱

緩存淘汰策略

(1):先進先出算法(FIFO)

(2):最近使用最少Least Frequently Used(LFU)

(3):最長時間未被使用的Least Recently Used(LRU)

當存在熱點數據時,LRU的效率很好,但偶發性的、周期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重

redis過期key刪除策略

(1):惰性刪除,cpu友好,但是浪費cpu資源

(2):定時刪除(不常用)

(3):定期刪除,cpu友好,節省空間

緩存雪崩以及處理辦法

同一時刻大量緩存失效;

處理方法:

(1):緩存數據增加過期標記

(2):設置不同的緩存失效時間

(3):雙層緩存策略C1為短期,C2為長期

(4):定時更新策略

緩存擊穿原因以及處理辦法

頻繁請求查詢系統中不存在的數據導致;

處理方法:

(1):cache null策略,查詢反饋結果為null仍然緩存這個null結果,設置不超過5分鐘過期時間

(2):布隆過濾器,所有可能存在的數據映射到足夠大的bitmap中 google布隆過濾器:基於內存,重啟失效不支持大數據量,無法在分佈式場景 redis布隆過濾器:可擴展性,不存在重啟失效問題,需要網絡io,性能低於google

redis阻塞原因

(1):數據結構使用不合理bigkey

(2):CPU飽和

(3):持久化阻塞,rdb fork子線程,aof每秒刷盤等

hot key出現造成集群訪問量傾斜解決辦法

(1):使用本地緩存

(2): 利用分片算法的特性,對key進行打散處理(給hot key加上前綴或者後綴,把一個hotkey 的數量變成 redis 實例個數N的倍數M,從而由訪問一個 redis key 變成訪問 N * M 個redis key)

Redis分佈式鎖

2.6版本以後lua腳本保證setnx跟setex進行原子性(setnx之後,未setex,服務掛了,鎖不釋放) a獲取鎖,超過過期時間,自動釋放鎖,b獲取到鎖執行,a代碼執行完remove鎖,a和b是一樣的key,導致a釋放了b的鎖。 解決辦法:remove之前判斷value(高併發下value可能被修改,應該用lua來保證原子性)

Redis如何做持久化

bgsave做鏡像全量持久化,aof做增量持久化。因為bgsave會耗費較長時間,不夠實時,在停機的時候會導致大量丟失數據 ,所以需要aof來配合使用。在redis實例重啟時,會使用bgsave持久化文件重新構建內存,再使用aof重放近期的操作指令來 實 現完整恢復重啟之前的狀態。

對方追問那如果突然機器掉電會怎樣?

取決於aof日誌sync屬性的配置,如果不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數據。但是在高性能的要求下每次都sync是不現實的,一般都使用定時sync,比如1s1次,這個時候最多就會丟失1s的數據.

redis鎖續租問題?

(1):基於redis的redission分佈式可重入鎖RLock,以及配合java集合中lock;

(2):Redission 內部提供了一個監控鎖的看門狗,不斷延長鎖的有效期,默認檢查鎖的超時時間是30秒

(3):此方案的問題:如果你對某個redis master實例,寫入了myLock這種鎖key的value,此時會異步複製給對應的master ,slave實例。但是這個過程中一旦發生redis master宕機,主備切換,redis slave變為了redis master。

接着就會導致,客戶端2來嘗試加鎖的時候,在新的redis master上完成了加鎖,而客戶端1也以為自己成功加了鎖。 此時就會導致多個客戶端對一個分佈式鎖完成了加鎖 解決辦法:只需要將新的redis實例,在一個TTL時間內,對客戶端不可用即可,在這個時間內,所有客戶端鎖將被失效或者自動釋放.

bgsave的原理是什麼?

fork和cow。fork是指redis通過創建子進程來進行bgsave操作,cow指的是copy on write,子進程創建后,父子進程共享數據段,父進程繼續提供讀寫服務,寫進的頁面數據會逐漸和子進程分離開來。

RDB與AOF區別

(1):R文件格式緊湊,方便數據恢復,保存rdb文件時父進程會fork齣子進程由其完成具體持久化工作,最大化redis性能,恢復大數據集速度更快,只有手動提交save命令或關閉命令時才觸發備份操作;

(2):A記錄對服務器的每次寫操作(默認1s寫入一次),保存數據更完整,在redis重啟是會重放這些命令來恢複數據,操作效率高,故障丟失數據更少,但是文件體積更大;

1億個key,其中有10w個key是以某個固定的已知的前綴開頭的,如果將它們全部找出來?

使用keys指令可以掃出指定模式的key列表。 如果這個redis正在給線上的業務提供服務,那使用keys指令會有什麼問題? redis的單線程的。keys指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候可以使用scan指令,scan指令可以無阻塞的提取出指定模式的key列表,但是會有一定的重複概率,在客戶端做一次去重就可以了 ,但是整體所花費的時間會比直接用keys指令長。

如何使用Redis做異步隊列?

一般使用list結構作為隊列,rpush生產消息,lpop消費消息。當lpop沒有消息的時候,要適當sleep一會再重試。

可不可以不用sleep呢?

list還有個指令叫blpop,在沒有消息的時候,它會阻塞住直到消息到來。

能不能生產一次消費多次呢?

使用pub/sub主題訂閱者模式,可以實現1:N的消息隊列。

pub/sub有什麼缺點?

在消費者下線的情況下,生產的消息會丟失,得使用專業的消息隊列如rabbitmq等。

redis如何實現延時隊列?

使用sortedset,想要執行時間的時間戳作為score,消息內容作為key調用zadd來生產消息,消費者用zrangebyscore指令獲取N秒之前的數據輪詢進行處理。

為啥redis zset使用跳躍鏈表而不用紅黑樹實現?

(1):skiplist的複雜度和紅黑樹一樣,而且實現起來更簡單。

(2):在併發環境下紅黑樹在插入和刪除時需要rebalance,性能不如跳錶。

MYSQL

數據庫三範式

一: 確保每列的原子性

二:非主鍵列不存在對主鍵的部分依賴 (要求每個表只描述一件事情)

三: 滿足第二範式,並且表中的列不存在對非主鍵列的傳遞依賴

數據庫主從複製原理

(1):主庫db的更新事件(update、insert、delete)被寫到binlog

(2):主庫創建一個binlog dump thread線程,把binlog的內容發送到從庫

(3):從庫創建一個I/O線程,讀取主庫傳過來的binlog內容並寫入到relay log.

(4):從庫還會創建一個SQL線程,從relay log裏面讀取內容寫入到slave的db.

複製方式分類

(1):異步複製(默認) 主庫寫入binlog日誌后即可成功返回客戶端,無須等待binlog日誌傳遞給從庫的過程,但是一旦主庫宕機,就有可能出現丟失數據的情況。

(2)半同步複製:( 5.5版本之後) (安裝半同步複製插件)確保從庫接收完成主庫傳遞過來的binlog內容已經寫入到自己的relay log(傳送log)后才會通知主庫上面的等待線程。如果等待超時,則關閉半同步複製,並自動轉換為異步複製模式,直到至少有一台從庫通知主庫已經接收到binlog信息為止

存儲引擎

(1):Myiasm是mysql默認的存儲引擎,不支持數據庫事務,行級鎖,外鍵;插入更新需鎖表,效率低,查詢速度快,Myisam使用的是非聚集索引

(2):innodb 支持事務,底層為B+樹實現,適合處理多重併發更新操作,普通select都是快照讀,快照讀不加鎖。InnoDb使用的是聚集索引

聚集索引

(1):聚集索引就是以主鍵創建的索引

(2):每個表只能有一個聚簇索引,因為一個表中的記錄只能以一種物理順序存放,實際的數據頁只能按照一顆 B+ 樹進行排序

(3):表記錄的排列順序和與索引的排列順序一致

(4):聚集索引存儲記錄是物理上連續存在

(5):聚簇索引主鍵的插入速度要比非聚簇索引主鍵的插入速度慢很多

(6):聚簇索引適合排序,非聚簇索引不適合用在排序的場合,因為聚簇索引恭弘=叶 恭弘節點本身就是索引和數據按相同順序放置在一起,索引序即是數據序,數據序即是索引序,所以很快。非聚簇索引恭弘=叶 恭弘節點是保留了一個指向數據的指針,索引本身當然是排序的,但是數據並未排序,數據查詢的時候需要消耗額外更多的I/O,所以較慢

(7):更新聚集索引列的代價很高,因為會強制innodb將每個被更新的行移動到新的位置

非聚集索引

(1):除了主鍵以外的索引

(2):聚集索引的恭弘=叶 恭弘節點就是數據節點,而非聚簇索引的恭弘=叶 恭弘節點仍然是索引節點,並保留一個鏈接指向對應數據塊

(3):聚簇索引適合排序,非聚簇索引不適合用在排序的場合

(4):聚集索引存儲記錄是物理上連續存在,非聚集索引是邏輯上的連續。

使用聚集索引為什麼查詢速度會變快?

使用聚簇索引找到包含第一個值的行后,便可以確保包含後續索引值的行在物理相鄰

建立聚集索引有什麼需要注意的地方嗎?

在聚簇索引中不要包含經常修改的列,因為碼值修改后,數據行必須移動到新的位置,索引此時會重排,會造成很大的資源浪費

InnoDB 表對主鍵生成策略是什麼樣的?

優先使用用戶自定義主鍵作為主鍵,如果用戶沒有定義主鍵,則選取一個Unique鍵作為主鍵,如果表中連Unique鍵都沒有定義的話,則InnoDB會為表默認添加一個名為row_id隱藏列作為主鍵。

非聚集索引最多可以有多少個?

每個表你最多可以建立249個非聚簇索引。非聚簇索引需要大量的硬盤空間和內存

BTree 與 Hash 索引有什麼區別?

(1):BTree索引可能需要多次運用折半查找來找到對應的數據塊 (2):HASH索引是通過HASH函數,計算出HASH值,在表中找出對應的數據 (3):大量不同數據等值精確查詢,HASH索引效率通常比B+TREE高 (4):HASH索引不支持模糊查詢、範圍查詢和聯合索引中的最左匹配規則,而這些Btree索引都支持

數據庫索引優缺點

(1):需要查詢,排序,分組和聯合操作的字段適合建立索引

(2):索引多,數據更新表越慢,盡量使用字段值不重複比例大的字段作為索引,聯合索引比多個獨立索引效率高

(3):對數據進行頻繁查詢進建立索引,如果要頻繁更改數據不建議使用索引

(4):當對表中的數據進行增加、刪除和修改的時候,索引也要動態的維護,降低了數據的維護速度。

索引的底層實現是B+樹,為何不採用紅黑樹,B樹?

(1):B+Tree非恭弘=叶 恭弘子節點只存儲鍵值信息,降低B+Tree的高度,所有恭弘=叶 恭弘子節點之間都有一個鏈指針,數據記錄都存放在恭弘=叶 恭弘子節點中

(2): 紅黑樹這種結構,h明顯要深的多,效率明顯比B-Tree差很多

(3):B+樹也存在劣勢,由於鍵會重複出現,因此會佔用更多的空間。但是與帶來的性能優勢相比,空間劣勢往往可以接受,因此B+樹的在數據庫中的使用比B樹更加廣泛

索引失效條件

(1):條件是or,如果還想讓or條件生效,給or每個字段加個索引

(2):like開頭%

(3):如果列類型是字符串,那一定要在條件中將數據使用引號引用起來,否則不會使用索引

(4):where中索引列使用了函數或有運算

數據庫事務特點

ACID 原子性,一致性,隔離性,永久性

數據庫事務說是如何實現的?

(1):通過預寫日誌方式實現的,redo和undo機制是數據庫實現事務的基礎

(2):redo日誌用來在斷電/數據庫崩潰等狀況發生時重演一次刷數據的過程,把redo日誌里的數據刷到數據庫里,保證了事務 的持久性(Durability)

(3):undo日誌是在事務執行失敗的時候撤銷對數據庫的操作,保證了事務的原子性

數據庫事務隔離級別

(1):讀未提交read-uncommitted– 臟,不可重複讀–幻讀 A讀取了B未提交的事務,B回滾,A 出現臟讀;

(2):不可重複讀read-committed– 不可重複讀–幻讀 A只能讀B已提交的事務,但是A還沒結束,B又更新數據隱式提交,然後A又讀了一次出現不可重複讀;

(3):可重複讀repeatable-read<默認>– 幻讀 事務開啟,不允許其他事務的UPDATE修改操作 A讀取B已提交的事務,然而B在該表插入新的行,之後A在讀取的時候多出一行,出現幻讀;

(4):串行化serializable–

七種事務傳播行為

(1)Propagation.REQUIRED<默認> 如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務。

(2)Propagation.SUPPORTS 如果當前存在事務,則加入該事務;如果當前不存在事務,則以非事務的方式繼續運行。

(3)Propagation.MANDATORY 如果當前存在事務,則加入該事務;如果當前不存在事務,則拋出異常。

(4)Propagation.REQUIRES_NEW 重新創建一個新的事務,如果當前存在事務,延緩當前的事務。

(5)Propagation.NOT_SUPPORTED 以非事務的方式運行,如果當前存在事務,暫停當前的事務。

(6)Propagation.NEVER 以非事務的方式運行,如果當前存在事務,則拋出異常。

(7)Propagation.NESTED 如果沒有,就新建一個事務;如果有,就在當前事務中嵌套其他事務。

產生死鎖的四個必要條件

(1):互斥: 資源x的任意一個時刻只能被一個線程持有 (2):佔有且等待:線程1佔有資源x的同時等待資源y,並不釋放x (3):不可搶佔:資源x一旦被線程1佔有,其他線程不能搶佔x (4):循環等待:線程1持有x,等待y,線程2持有y,等待x 當全部滿足時才會死鎖

@Transaction

底層實現是AOP,動態代理 (1):實現是通過Spring代理來實現的。生成當前類的代理類,調用代理類的invoke()方法,在invoke()方法中調用 TransactionInterceptor攔截器的invoke()方法;

(2):非public方式其事務是失效的;

(3):自調用也會失效,因為動態代理機制導致

(4)多個方法外層加入try…catch,解決辦法是可以在catch里 throw new RuntimeException()來處理

分佈式事務

XA方案

有一個事務管理器的概念,負責協調多個數據庫(資源管理器)的事務 不適合高併發場景,嚴重依賴數據庫層面,同步阻塞問題;協調者故障則所有參与者會阻塞

TCC方案

嚴重依賴代碼補償和回滾,一般銀行用,和錢相關的支付、交易等相關的場景,我們會用TCC Try,對各個服務的資源做檢測,對資源進行鎖定或者預留 Confirm,在各個服務中執行實際的操作 Cancel,如果任何一個服務的業務方法執行出錯,那麼這裏就需要進行補償,即執行已操作成功的業務邏輯的回滾操作

可靠消息最終一致性方案

1):本地消息服務 本地消息表其實是國外的 ebay 搞出來的這麼一套思想。 主動方是認證服務,有個消息異常處理系統,mq,還有消息消費端應用系統,還有採集服務;

  • 在我認證返回數據中如果有發票是已經認證的,在處理認證數據的操作與發送消息在同一個本地事務中,業務執行完,消息數據也同時存在一條待確認的數據;
  • 發送消息給mq,,mq發送消息給消息消費端服務,同時存一份消息數據,然後發送給採集服務,進行抵賬表更新操作;
  • 採集服務邏輯處理完以後反饋給消息消費端服務,其服務刪除消息數據,同時通知認證服務,把消息記錄改為已確認成功費狀態;
  • 對於異常流程,消息異常處理系統會查詢認證服務中過期未確認的消息發送給mq,相當於重試

2):獨立消息最終一致性方案: A 主動方應用系統,B消息服務子系統,C消息狀態確認子系統,C2消息管理子系統 D 消息恢復子系統,mq ,消息消費端E ,被動系統F

 流程:
A預發送消息給B,然後執行A業務邏輯,B存儲預發送消息,A執行完業務邏輯發送業務操作結果給B,B更新預發送消息為確認併發送消息狀態同時發送消息給mq,然後被E監聽然後發送給F消費掉
C:對預發送消息異常的處理,去查詢待確認狀態超時的消息,去A中查詢進行數據處理,如果A中業務處理成功了,那麼C需改消息狀態為確認併發送狀態,然後發送消息給mq;如果A中業務處理失敗了..那麼C直接把消息刪除即可.
C2 : 查詢消息的頁面,對消息的可視化,以及批量處理死亡消息;
D: B給mq放入數據如果失敗,,通過D去重試,多次重試失敗,消息設置為死亡 
E:確保F執行完成,發送消息給B刪除消息
優化建議: 
 (1)數據庫:如果用redis,持久化要配置成appendfsync always,確保每次新添加消息都能持久化進磁盤
 (2)在被動方應用業務冪等性判斷比較麻煩或者比較耗性能情況下,增加消息日誌記錄表.用於判斷之前有無發送過;

最大努力通知性(定期校對)

(1)業務主動方完成業務處理之後,設置時間階梯型通知規則向業務活動的被動方發送消息,允許消息丟失.

(2)被動方根據定時策略,向主動方查詢,恢復丟失的業務消息

(3)被動方的處理結果不影響主動方的處理結果

(4)需增加業務查詢,通知服務,校對系統服務的建設成本

(5)適用於對業務最終一致性的時間敏感度低,跨企業的業務通知活動

(6)比如銀行通知,商戶通知,交易業務平台間商戶通知,多次通知,查詢校對等

Seata(阿里)

應用層基於SQL解析實現了自動補償,從而最大程度的降低業務侵入性; 將分佈式事務中TC(事務協調者)獨立部署,負責事務的註冊、回滾; 通過全局鎖實現了寫隔離與讀隔離。

網絡

TCP和UDP的比較

TCP向上層提供面向連接的可靠服務 ,UDP向上層提供無連接不可靠服務。 雖然 UDP 並沒有 TCP 傳輸來的準確,但是也能在很多實時性要求高的地方有所作為 對數據準確性要求高,速度可以相對較慢的,可以選用TCP

TCP三次握手

TCP四次揮手

(1):客戶端發送終止命令FIN

(2):服務端收到后回復ACK,處於close_wait狀態

(3):服務器將關閉前需要發送信息發送給客戶端后處於last_ack狀態

(4):客戶端收到FIN后發送ack后處於tim-wait而後進入close狀態

為什麼要進行第三次握手

為了防止服務器端開啟一些無用的連接增加服務器開銷以及防止已失效的連接請求報文段突然又傳送到了服務端

JDK1.8新特性

Lambda表達式

java也開始承認了函數式編程, 就是說函數既可以作為參數,也可以作為返回值, 大大的簡化了代碼的開發

default關鍵字

打破接口裡面是只能有抽象方法,不能有任何方法的實現,接口裡面也可以有方法的實現了

新時間日期APILocalDate | LocalTime | LocalDateTime

之前使用的java.util.Date月份從0開始,我們一般會+1使用,很不方便,java.time.LocalDate月份和星期都改成了enum java.util.Date和SimpleDateFormat都不是線程安全的,而LocalDate和LocalTime和最基本的String一樣,是不變類型,不但線程安全,而且不能修改。 新接口更好用的原因是考慮到了日期時間的操作,經常發生往前推或往後推幾天的情況。用java.util.Date配合Calendar要寫好多代碼,而且一般的開發人員還不一定能寫對。

JDK1.7與JDK1.8 ConcurrentHashMap對比

(1):JDK1.7版本的ReentrantLock+Segment+HashEntry(數組)

(2):JDK1.7採用segment的分段鎖機制實現線程安全

(3):JDK1.8版本中synchronized+CAS+HashEntry(數組)+紅黑樹

(4):JDK1.8採用CAS+Synchronized保證線程安全

(5):查詢時間複雜度從原來的遍歷鏈表O(n),變成遍歷紅黑樹O(logN)

1.8 HashMap數組+鏈表+紅黑樹來實現hashmap,當碰撞的元素個數大於8時 & 總容量大於64,會有紅黑樹的引入 除了添加之後,效率都比鏈表高,1.8之後鏈表新進元素加到末尾

JDK1.8使用synchronized來代替重入鎖ReentrantLock?

(1):因為粒度降低了,在相對而言的低粒度加鎖方式,synchronized並不比ReentrantLock差

(2):基於JVM的synchronized優化空間更大

(3):在大數據量下,基於API的ReentrantLock會比基於JVM的內存壓力開銷更多的內存

JDK1.9新特性

模塊系統:

模塊是一個包的容器,Java 9 最大的變化之一是引入了模塊系統(Jigsaw 項目)。

集合工廠方法

通常,您希望在代碼中創建一個集合(例如,List 或 Set ),並直接用一些元素填充它。 實例化集合,幾個 “add” 調用,使得代碼重複。 Java 9,添加了幾種集合工廠方法:

Set<Integer> ints = Set.of(1, 2, 3);
List<String> strings = List.of("first", "second");

改進的 Stream API

Stream 接口中添加了 4 個新的方法:dropWhile, takeWhile, ofNullable。還有個 iterate 方法的新重載方法

改進的 Javadoc:

Javadoc 現在支持在 API 文檔中的進行搜索。另外,Javadoc 的輸出現在符合兼容 HTML5 標準。

redis代理集群模式,spring有哪些註解,b+b 紅黑樹區別,三次握手,valitile重排序底層代碼, cas 事務的4個特性,java8 java11 特性, filter和interceptor的區別 @autowired原理, dispatcherservlet,分佈式事務解決方案spring都有哪些模塊,fork join隊列,排序算法,

集合

java的集合框架有哪幾種:

兩種:collection和map,其中collection分為set和List。

List你使用過哪些

ArrayList和linkedList使用的最多,也最具代表性。

你知道vector和ArrayList和linkedList的區別嘛

ArrayList實現是一個數組,可變數組,默認初始化長度為10,也可以我們設置容量,但是沒有設置的時候是默認的空數組,只有在第一步add的時候會進行擴容至10(重新創建了數組),後續擴容按照3/2的大小進行擴容,是線程不安全的,適用多讀取,少插入的情況

linkedList是基於雙向鏈表的實現,使用了尾插法的方式,內部維護了鏈表的長度,以及頭節點和尾節點,所以獲取長度不需要遍歷。適合一些插入/刪除頻繁的情況。

Vector是線程安全的,實現方式和ArrayList相似,也是基於數組,但是方法上面都有synchronized關鍵詞修飾。其擴容方式是原來的兩倍。

hashMap和hashTable和ConcurrentHashMap的區別

hashMap是map類型的一種最常用的數據結構,其底部實現是數組+鏈表(在1.8版本后變為了數組+鏈表/紅黑樹的方式),其key是可以為null的,默認hash值為0。擴容以2的冪等次(為什麼。。。因為只有是2的冪等次的時候(n-1)&x==x%n,當然不一定只有一個原因)。是線程不安全的

hashTable的實現形式和hashMap差不多,它是線程安全的,是繼承了Dictionary,也是key-value的模式,但是其key不能為null。

ConcurrentHashMap是JUC併發包的一種,在hashMap的基礎上做了修改,因為hashmap其實是線程不安全的,那在併發情況下使用hashTable嘛,但是hashTable是全程加鎖的,性能不好,所以採用分段的思想,把原本的一個數組分成默認16段,就可以最多容納16個線程併發操作,16個段叫做Segment,是基於ReetrantLock來實現的

說說你了解的hashmap吧

hashMap是Map的結構,內部用了數組+鏈表的方式,在1.8后,當鏈表長度達到8的時候,會變成紅黑樹,這樣子就可以把查詢的複雜度變成O(nlogn)了,默認負載因子是0.75,為什麼是0.75呢?

我們知道當負載因子太小,就很容易觸發擴容,如果負載因子太大就容易出現碰撞。所以這個是空間和時間的一個均衡點,在1.8的hashmap介紹中,就有描述了,貌似是0.75的負載因子中,能讓隨機hash更加滿足0.5的泊松分佈。

除此之外,1.7的時候是頭插法,1.8后就變成了尾插法,主要是為了解決rehash出現的死循環問題,而且1.7的時候是先擴容后插入,1.8則是先插入后擴容(為什麼?正常來說,如果先插入,就有可能節點變為樹化,那麼是不是多做一次樹轉化,比1.7要多損耗,個人猜測,因為讀寫問題,因為hashmap並不是線程安全的,如果說是先擴容,后寫入,那麼在擴容期間,是訪問不到新放入的值的,是不是不太合適,所以會先放入值,這樣子在擴容期間,那個值是在的)。

1.7版本的時候用了9次擾動,5次異或,4次位移,減少hash衝突,但是1.8就只用了兩次,覺得就足夠了一次異或,一次位移。

concurrentHashMap呢

concurrentHashMap是線程安全的map結構,它的核心思想是分段鎖。在1.7版本的時候,內部維護了segment數組,默認是16個,segment中有一個table數組(相當於一個segmeng存放着一個hashmap。。。),segment繼承了reentrantlock,使用了互斥鎖,map的size其實就是segment數組的count和。而在1.8的時候做了一個大改版,廢除了segment,採用了cas加synchronize方式來進行分段鎖(還有自旋鎖的保證),而且節點對象改用了Node不是之前的HashEntity。

Node可以支持鏈表和紅黑樹的轉化,比如TreeBin就是繼承了Node,這樣子可以直接用instanceof來區分。1.8的put就很複雜來,會先計算出hash值,然後根據hash值選出Node數組的下標(默認數組是空的,所以一開始put的時候會初始化,指定負載因子是0.75,不可變),判斷是否為空,如果為空,則用cas的操作來賦值首節點,如果失敗,則因為自旋,會進入非空節點的邏輯,這個時候會用synchronize加鎖頭節點(保證整條鏈路鎖定)這個時候還會進行二次判斷,是否是同一個首節點,在分首節點到底是鏈表還是樹結構,進行遍歷判斷。

concurrentHashMap的擴容方式

1.7版本的concurrentHashMap是基於了segment的,segment內部維護了HashEntity數組,所以擴容是在這個基礎上的,類比hashmap的擴容,

1.8版本的concurrentHashMap擴容方式比較複雜,利用了ForwardingNode,先會根據機器內核數來分配每個線程能分到的busket數,(最小是16),這樣子可以做到多線程協助遷移,提升速度。然後根據自己分配的busket數來進行節點轉移,如果為空,就放置ForwardingNode,代表已經遷移完成,如果是非空節點(判斷是不是ForwardingNode,是就結束了),加鎖,鏈路循環,進行遷移。

hashMap的put方法的過程

判斷key是否是null,如果是null對應的hash值就是0,獲得hash值過後則進行擾動,(1.7是9次,5次異或,4次位移,1.8是2次),獲取到的新hash值找出所在的index,(n-1)&hash,根據下標找到對應的Node/entity,然後遍歷鏈表/紅黑樹,如果遇到hash值相同且equals相同,則覆蓋值,如果不是則新增。如果節點數大於8了,則進行樹化(1.8)。完成后,判斷當前的長度是否大於閥值,是就擴容(1.7是先擴容在put)。

為什麼修改hashcode方法要修改equals

都是map惹的禍,我們知道在map中判斷是否是同一個對象的時候,會先判斷hash值,在判斷equals的,如果我們只是重寫了hashcode,沒有順便修改equals,比如Intger,hashcode就是value值,如果我們不改寫equals,而是用了Object的equals,那麼就是判斷兩者指針是否一致了,那就會出現valueOf和new出來的對象會對於map而言是兩個對象,那就是個問題了

TreeMap了解嘛

TreeMap是Map中的一種很特殊的map,我們知道Map基本是無序的,但是TreeMap是會自動進行排序的,也就是一個有序Map(使用了紅黑樹來實現),如果設置了Comparator比較器,則會根據比較器來對比兩者的大小,如果沒有則key需要是Comparable的子類(代碼中沒有事先check,會直接拋出轉化異常,有點坑啊)。

LinkedHashMap了解嘛

LinkedHashMap是HashMap的一種特殊分支,是某種有序的hashMap,和TreeMap是不一樣的概念,是用了HashMap+鏈表的方式來構造的,有兩者有序模式:訪問有序,插入順序,插入順序是一直存在的,因為是調用了hashMap的put方法,並沒有重載,但是重載了newNode方法,在這個方法中,會把節點插入鏈表中,訪問有序默認是關閉的,如果打開,則在每次get的時候都會把鏈表的節點移除掉,放到鏈表的最後面。這樣子就是一個LRU的一種實現方式。

數據結構+算法

TODO(未完待續)

總結

內容過於硬核了,導致很多排版細節,我沒辦法做得像其他期一樣精緻了,大家見諒。

涉及的內容和東西太多了,可能很多都是點到為止,也有很多不全的,也有很多錯誤的點,已經快3W字了,我校驗實在困難,我會放在GitHub上面,大家可以跟我一起更新這個文章,造福後人吧。

搞不好下次我需要看的時候,我都得看着這個複習了。

我是敖丙,一個在互聯網苟且偷生的工具人。

你知道的越多,你不知道的越多人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!

注:如果本篇博客有任何錯誤和建議,歡迎人才們留言,你快說句話啊

文章持續更新,可以微信搜索「 三太子敖丙 」第一時間閱讀,回復【資料】【面試】【簡歷】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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