阿里校招面試,我來瞅瞅怎么回事
Go里有哪些數據結構是并發安全的?
并發安全就是程序在并發的情況下執行的結果都是正確的;
Go中數據類型分為兩大類:
- 基本數據類型:字節型、整型、布爾型、浮點型、復數型、字符串
- 復合數據類型:數組、切片、指針、結構體、字典、、函數、接口
字節型、布爾型、整型、浮點型取決于操作系統指令值,在64位的指令集架構中可以由一條機器指令完 成,不存在被細分為更小的操作單位,所以這些類型的并發賦值是安全的,但是這個也跟操作系統的位 數有關,比如int64在32位操作系統中,它的高32位和低32位是分開賦值的,此時是非并發安全的。
復數類型、字符串、結構體、數組,切片,字典,通道,接口, 這些底層都是struct,不同成員的賦值 都不是一起的,所以都不是并發安全的。
Go如何實現一個單例模式?
單例模式的作用是確保無論對象被實例化多少次,全局都只有一個實例存在。根據這一特性,我們可以將其應用到全局唯一性配置、數據庫連接對象、文件訪問對象等。
餓漢式
餓漢式實現單例模式非常簡單,直接看代碼:
package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
return instance
}
singleton 包在被導入時會自動初始化 instance 實例,使用時通過調用 singleton.GetSingleton() 函數即可獲得 singleton 這個結構體的單例對象。
這種方式的單例對象是在包加載時立即被創建,所以這個方式叫作餓漢式。與之對應的另一種實現方式叫作懶漢式,懶漢式模式下實例會在第一次被使用時被創建。
需要注意的是,盡管餓漢式實現單例模式的方式簡單,但大多數情況下并不推薦。因為如果單例實例化時初始化內容過多,會造成程序加載用時較長。
懶漢式
接下來我們再來看下如何通過懶漢式實現單例模式:
package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
相較于餓漢式的實現,懶漢式將實例化 singleton 結構體部分的代碼移到了 GetSingleton() 函數內部。這樣能夠將對象實例化的步驟延遲到 GetSingleton() 第一次被調用時。
不過通過 instance == nil 的判斷來實現單例并不十分可靠,如果有多個 goroutine 同時調用 GetSingleton() 就無法保證并發安全。
- sync.map的底層實現
什么是sync.Map
Go 的內建 map 是不支持并發寫操作的,原因是 map 寫操作不是并發安全的,當你嘗試多個 Goroutine 操作同一個 map,會產生報錯:fatal error: concurrent map writes。
因此官方另外引入了 sync.Map 來滿足并發編程中的應用。
sync.Map 的實現原理可概括為:
- 通過 read 和 dirty 兩個字段將讀寫分離,讀的數據存在只讀字段 read 上,將最新寫入的數據則存在 dirty 字段上
- 讀取時會先查詢 read,不存在再查詢 dirty,寫入時則只寫入 dirty
- 讀取 read 并不需要加鎖,而讀或寫 dirty 都需要加鎖
- 另外有 misses 字段來統計 read 被穿透的次數(被穿透指需要讀 dirty 的情況),超過一定次數則將 dirty 數據同步到 read 上
- 對于刪除數據則直接通過標記來延遲刪除
數據結構
type Map struct {
// 加鎖作用,保護 dirty 字段
mu Mutex
// 只讀的數據,實際數據類型為 readOnly
read atomic.Value
// 最新寫入的數據
dirty map[interface{}]*entry
// 計數器,每次需要讀 dirty 則 +1
misses int
}
其中 readOnly 的數據結構為:
type readOnly struct {
// 內建 map
m map[interface{}]*entry
// 表示 dirty 里存在 read 里沒有的 key,通過該字段決定是否加鎖讀 dirty
amended bool
}
entry 數據結構則用于存儲值的指針:
type entry struct {
p unsafe.Pointer // 等同于 *interface{}
}
屬性 p 有三種狀態:
- p == nil: 鍵值已經被刪除,且 m.dirty == nil
- p == expunged: 鍵值已經被刪除,但 m.dirty!=nil 且 m.dirty 不存在該鍵值(expunged 實際是空接口指針)
- 除以上情況,則鍵值對存在,存在于 m.read.m 中,如果 m.dirty!=nil 則也存在于 m.dirty
Map 常用的有以下方法:
- Load:讀取指定 key 返回 value
- Store:存儲(增或改)key-value
- Delete:刪除指定 key
channel在什么情況下會panic?
- 關閉為nil的channel
- 關閉一個已經關閉的通道
- 向一個已經關閉的通道寫數據
- 關閉通道導致發送阻塞的協程panic
redis有哪些數據結構,分別常用于哪些場合?
redis的基本數據結構有: 1、String(字符串);2、Hash(哈希);3、List(列表);4、Set(集合);5、zset(有序集合)。
String(字符串)
String 類型是 Redis 中最基本、最常用的數據類型,甚至被很多用戶當成 Redis 少數的數據類型去使用。String 類型在 Redis 中是二進制安全(binary safe)的,這意味著 String 值關心二進制的字符串,不關心具體格式,你可以用它存儲 json 格式或 JPEG 圖片格式的字符串。
應用:
- 存儲一些配置數據:在前后分離式開發中,有些數據雖然存儲在數據庫,但是更改特別少。比如有個全國地區表。當前端發起請求后,后臺如果每次都從關系型數據庫讀取,會影響網站整體性能。我們可以在名列前茅次訪問的時候,將所有地區信息存儲到redis字符串中,再次請求,直接從數據庫中讀取地區的json字符串,返回給前端。
- 緩存對象:將對象轉為json存儲,比如商品信息,用戶信息。
- 數據統計:redis整型可以用來記錄網站訪問量,某個文件的下載量,簽到人數、視頻訪問量等等。(自增自減)
- 時間內限制請求次數:比如已登錄用戶請求短信驗證碼,驗證碼在5分鐘內有效的場景。當用戶首次請求了短信接口,將用戶id存儲到redis 已經發送短信的字符串中,并且設置過期時間為5分鐘。當該用戶再次請求短信接口,發現已經存在該用戶發送短信記錄,則不再發送短信。
- 訂單號(全局少數):有時候你需要去生成一個全局少數值的時候可以通過redis生成。關鍵命令:incrby(原子自增)。
- 分布式session:當我們用nginx做負載均衡的時候,如果我們每個從服務器上都各自存儲自己的session,那么當切換了服務器后,session信息會由于不共享而會丟失,我們不得不考慮第三應用來存儲session。
Hash(哈希)
Hash的數據結構我們可以簡單理解為java中的 Map,這種結構就特別適合存儲對象,上面的String的類型確實也可以存儲對象,但每次修改對象中的某一個屬性,都要拿出整個json字符串在修改這個屬性,之后在重新插入,而hash的接口特點讓我們可以只修改該對象的某一個屬性。
hash數據類型在存儲上述類型的數據時具有比 String 類型更靈活、更快的優勢,具體的說,使用 String 類型存儲,必然需要轉換和解析 json 格式的字符串,即便不需要轉換,在內存開銷方面,還是 hash 占優勢。
應用:
- Redisson分布式鎖:Redisson在實現分布式鎖的時候,內部的用的數據就是hash而不是String。因為Redisson為了實現可重入加鎖機制。所以在hash中存入了當前線程ID。
- 購物車列表:以用戶id為key,商品id為field,商品數量為value,恰好構成了購物車的3個要素。
- 緩存對象:hash類型的 (key, field, value) 的結構與對象的(對象id, 屬性, 值)的結構相似,也可以用來存儲對象。
List(列表)
List類型是按照插入順序排序的字符串鏈表,一個列表非常多可以存儲2^32-1個元素。我們可以簡單理解為就相當于java中的LinkesdList。和數據結構中的普通鏈表一樣,我們可以在其頭部(left)和尾部(right)添加新的元素。在插入時,如果該鍵并不存在,Redis將為該鍵創建一個新的鏈表。與此相反,如果鏈表中所有的元素均被移除,那么該鍵也將會被從數據庫中刪除。
應用:
- 消息隊列:lpop和rpush(或者反過來,lpush和rpop)能實現隊列的功能。
Set(集合)
Redis 中的 set和Java中的HashSet 有些類似,它內部的鍵值對是無序的、少數的。它的內部實現相當于一個特殊的字典,字典中所有的value都是一個值 NULL。當集合中最后一個元素被移除之后,數據結構被自動刪除,內存被回收。
應用:
- 抽獎活動:存儲某活動中中獎的用戶ID ,因為有去重功能,可以保證同一個用戶不會中獎兩次。
zset(有序集合)
Sorted-Sets中的每一個成員都會有一個分數(score)與之關聯,Redis正是通過分數來為集合中的成員進行從小到大的排序。成員是少數的,但是分數(score)卻是可以重復的。
應用: 作為有序的,不可重復的列表,可以做一些排行榜相關的場景:
- 排行榜(商品銷量,視頻評分,用戶游戲分數)
- 新聞熱搜
說下緩存擊穿,緩存穿透,緩存雪崩有什么區別?
緩存擊穿
當大量緩存數據在同一時間過期(失效)或者 Redis 故障宕機時,如果此時有大量的用戶請求,都無法在 Redis 中處理,于是全部請求都直接訪問數據庫,從而導致數據庫的壓力驟增,嚴重的會造成數據庫宕機,從而形成一系列連鎖反應,造成整個系統崩潰,這就是緩存雪崩
緩存擊穿
我們的業務通常會有幾個數據會被頻繁地訪問,比如秒殺活動,這類被頻地訪問的數據被稱為熱點數據。
如果緩存中的某個熱點數據過期了,此時大量的請求訪問了該熱點數據,就無法從緩存中讀取,直接訪問數據庫,數據庫很容易就被高并發的請求沖垮,這就是緩存擊穿。
緩存穿透
當發生緩存雪崩或擊穿時,數據庫中還是保存了應用要訪問的數據,一旦緩存恢復相對應的數據,就可以減輕數據庫的壓力,而緩存穿透就不一樣了。
當用戶訪問的數據,既不在緩存中,也不在數據庫中,導致請求在訪問緩存時,發現緩存缺失,再去訪問數據庫時,發現數據庫中也沒有要訪問的數據,沒辦法構建緩存數據,來服務后續的請求。那么當有大量這樣的請求到來時,數據庫的壓力驟增,這就是緩存穿透的問題。
主鍵索引和唯一索引的區別
- 主鍵是一種約束,唯一索引是一種索引,兩者在本質上是不同的。
- 主鍵創建后一定包含一個唯一性索引,唯一性索引并不一定就是主鍵。
- 唯一性索引列允許空值,而主鍵列不允許為空值。
- 主鍵可以被其他表引用為外鍵,而唯一索引不能。
- 一個表最多只能創建一個主鍵,但可以創建多個唯一索引。
- 主鍵更適合那些不容易更改的唯一標識,如自動遞增列、身份證號等。
- 在RBO模式下,主鍵的執行計劃優先級要高于唯一索引。兩者可以提高查詢的速度。
約束主要有:主鍵約束、外鍵約束、非空約束、檢査約束(bentwen and ,大于、小于、等于、不等于)、唯一約束。
索引為什么使用B+樹,而不使用跳表?
B+樹是多叉樹結構,每個結點都是一個16k的數據頁,能存放較多索引信息,所以扇出很高。三層左右就可以存儲2kw左右的數據(知道結論就行,想知道原因可以看之前的文章)。也就是說查詢一次數據,如果這些數據頁都在磁盤里,那么最多需要查詢三次磁盤IO。
跳表是鏈表結構,一條數據一個結點,如果最底層要存放2kw數據,且每次查詢都要能達到二分查找的效果,2kw大概在2的24次方左右,所以,跳表大概高度在24層左右。最壞情況下,這24層數據會分散在不同的數據頁里,也即是查一次數據會經歷24次磁盤IO。
因此存放同樣量級的數據,B+樹的高度比跳表的要少,如果放在mysql數據庫上來說,就是磁盤IO次數更少,因此B+樹查詢更快。
而針對寫操作,B+樹需要拆分合并索引數據頁,跳表則獨立插入,并根據隨機函數確定層數,沒有旋轉和維持平衡的開銷,因此跳表的寫入性能會比B+樹要好。
計算機網絡的多層模型簡要介紹
- 應用層(Application):為用戶的應用程序提供網絡服務
- 表示層(Presentation):將信息表示為一定形式和格式的數據流
- 會話層(Session):負責通信主機之間會話的建立、管理和拆除,協調通信雙方的會話
- 傳輸層(Transport):負責通信主機間端到端的連接
- 網絡層(Network):負責將分組從源機送到目的機,包括尋址和最優路徑選擇等
- 數據鏈路層(Data Link):提供可靠的幀傳遞,實現差錯控制、流控等等
- 物理層(Physical):提供透明的比特流(01流)傳遞
http2.0相比與http1.1的優化
HTTP2.0(Hypenext TransferProtocol version2)是超文本傳輸協議的第二版,HTTP2.0相比于HTTP1x,大幅度的提升了web性能,同時向下兼容HTTP1.X協議版 本。
主要核心優勢有
1、采用二進制格式傳輸數據,而非htp1.1文本格式,二進制格式在協議的解析和優化擴展上帶來了跟多的優勢和可能
2、對消息頭采用Hpack進行壓縮傳輸,能夠節省消息頭占用的網絡流量,htp1.1每次請求,都會攜帶大量冗余的頭信息,浪費了很多寬帶資源,
3、異步連接多路復用
4、Server Push,服務器端能夠更快的把資源推送到客戶端。
5、保持與HTTP 1.1語義的向后兼容性也是該版本的一個關鍵
本文轉載自微信公眾號「王中陽Go」,作者「王中陽Go」,可以通過以下二維碼關注。
轉載本文請聯系「王中陽Go」公眾號。