從事業單位跳去大廠
程序员Carl 代码随想录 从事业单位跳去大厂! : https://mp.weixin.qq.com/s/dHD6K5ng_F2lPQ8cXjf-8w
Go 相關(這個不用多說吧,把 Go 官方的包和別人的書籍看幾遍,你就會印象深刻了)
1. new 和 make 的區別
"new" 函數:
- "new" 函數用於動態分配並初始化一個新的零值對象的指針。
- 它接受一個類型作為參數,並返回一個指向該類型的零值對象的指針。
- "new" 函數返回的是對象的指針,並且對象的內容被初始化為其零值。
- 它在Heap上分配內存空間。 通常用於實例化結構體(struct)、數組(array)、切片(slice)、字典(map)等。
type Person struct {
Name string
Age int
}
func main() {
p := new(Person)
fmt.Println(p) // Output: &{ 0}
}
"make" 函數:
- "make" 函數用於創建和初始化內建的引用類型(slice、map 和 channel)。
- 它接受一個類型和相關的參數,返回初始化後的對象。
- "make" 函數返回的是對象本身,而不是指向對象的指針。
- 它在堆上分配內存空間(對於 slice 和 map)或在運行時進行相應的初始化(對於 channel)。
- 通常用於實例化切片(slice)、字典(map)和通道(channel)。
func main() {
s := make([]int, 0, 5)
fmt.Println(s) // Output: []
}
總結來說,在 Golang 中,"new" 函數用於動態分配並初始化對象的指針,而 "make" 函數用於創建和初始化引用類型的對象本身。
其他
Immutable(不變性) Go objects:
不可變意味著一旦創建了一個對象或變量,它的值就不能被更改。在 Go 中,大多數內建類型(如整數、浮點數、布爾值、字串等)是不可變的。這意味著一旦它們的值被設置,就無法更改它們的內容。當我們對不可變對象進行操作時,實際上是創建了一個新的對象,而不是修改原始對象
package main
import "fmt"
func main() {
s := "Hello"
s = s + " World" // 創建了一個新的字串對象,而不是修改原始對象
fmt.Println(s) // Output: Hello World
num := 10
num = num + 5 // 創建了一個新的整數對象,而不是修改原始對象
fmt.Println(num) // Output: 15
}
- interfaces
- booleans, numeric values (including values of type int)
- strings
- pointers
- function pointers, and closures which can be reduced to function pointers
- 函數指針(function pointers)和可以簡化為函數指針的閉包(closures)都是可以傳遞和操作函數的方式。
- https://go.dev/play/p/RCgCAzU3643
package main
import "fmt"
// 定義一個接受兩個整數參數並返回它們之和的函數類型
type AddFunc func(int, int) int
// 函數: 將兩個整數相加
func add(a, b int) int {
return a + b
}
func main() {
// 使用函數指針
var addFunc AddFunc
addFunc = add
// 使用函數指針調用函數
result := addFunc(3, 4)
fmt.Println("Result (function pointer):", result) // 7
// 使用閉包
addClosure := func(a, b int) int {
return a + b
}
// 使用閉包調用函數
result = addClosure(3, 4)
fmt.Println("Result (closure):", result) // 7
// 將閉包轉換為函數指針
addFunc = addClosure
// 使用函數指針調用函數
result = addFunc(3, 4)
fmt.Println("Result (function pointer from closure):", result) // 7
}
// Result (function pointer): 7
// Result (closure): 7
// Result (function pointer from closure): 7
- structs having a single field
- Go 中,結構體(struct)通常被用於表示一個包含多個字段的數據結構。然而,如果一個結構體只有單一字段,且該字段是不可變的,那麼這樣的結構體可以被認為是immutable(不可變)的。
package main
import "fmt"
type SingleFieldStruct struct {
Field int
}
func main() {
s := SingleFieldStruct{Field: 10}
fmt.Println(s) // Output: {10}
// 修改字段值需要創建一個新的結構體實例
s = SingleFieldStruct{Field: 20}
fmt.Println(s) // Output: {20}
}
- 具有併發安全
Mutable(變性) Go objects: (可以被make 都是)
可變意味著一旦創建了一個對象或變量,它的值可以被修改。在 Go 中,一些內建類型(如切片、字典、指針、自定義結構體等)是可變的。這意味著我們可以直接修改這些對象的內容,而不需要創建新的對象。
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
numbers[0] = 10 // 直接修改切片內的元素值
fmt.Println(numbers) // Output: [10 2 3 4 5]
person := struct {
Name string
Age int
}{
Name: "John",
Age: 30,
}
person.Age = 31 // 直接修改結構體內的字段值
fmt.Println(person) // Output: {John 31}
}
- arrays and slices
- maps
- channels
closures which are capturing at least 1 variable from the outer scope
當一個閉包(closure)捕獲了至少一個外部作用域的變量時,這意味著閉包在其內部引用了來自外部作用域的變量。
package main import "fmt" func main() { outer := 10 // 創建一個閉包 closure := func() { fmt.Println("Outer variable:", outer) } // 調用閉包 closure() }
不具有併發安全
2. slice 和 array 的區別以及各自的實現是怎麼樣的?如何比較兩個 slice?
區別:
- 大小固定 vs. 大小可變:數組的大小在創建時就已經確定,並且無法改變;而切片的大小可以根據需要動態調整。
- 值語義 vs. 引用語義:數組是值類型,直接存儲元素的實際值;切片是引用類型,存儲的是底層數組的引用。
實現:
- 數組的實現:數組是一個固定大小的元素序列,使用連續的內存塊來存儲元素。在 Go 中,數組的長度是類型的一部分,所以具有不同長度的數組是不同的類型。
- 切片的實現:切片是對數組的一部分連續片段的引用。切片內部包含了一個指向底層數組的指針、長度和容量信息。切片本身並不存儲實際的元素,它只是引用底層數組中的一部分數據。
比較兩個切片:
在 Go 中,切片不能直接進行比較操作,即不能使用 == 運算符來比較兩個切片是否相等。如果要比較兩個切片是否相等,可以使用循環遍歷切片的元素,並逐個比較, 或者用reflect.DeepEqual。
https://go.dev/play/p/w02WsFOEXgY
package main
import (
"fmt"
"reflect"
)
func slicesEqual(s1, s2 []int) bool {
if len(s1) != len(s2) {
return false
}
for i := 0; i < len(s1); i++ {
if s1[i] != s2[i] {
return false
}
}
return true
}
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{1, 2, 3, 4, 5}
slice3 := []int{1, 2, 3, 4}
fmt.Println(slicesEqual(slice1, slice2)) // 输出: true
fmt.Println(slicesEqual(slice1, slice3)) // 输出: false
fmt.Println(reflect.DeepEqual(slice1, slice2)) // 输出: true
fmt.Println(reflect.DeepEqual(slice1, slice3)) // 输出: false
}
3. fmt 中不同輸出函數的區別是什麼?
- Print 和 Println 函數直接將參數打印到標準輸出,一個是在同一行,一個是在新的一行。
- Printf 函數使用格式化字符串將參數格式化並打印到標準輸出。
- Sprintf 函數將格式化的字符串作為返回值返回,而不是打印到標準輸出。
4. Go 中的引用類型有哪些?如何初始化?
Slices, maps, channels, pointers, functions Don't worry about pointers with these

5. 說一說你對 Mutex 的理解以及如何使用,有幾種模式
Mutex 的理解:
- Mutex 是一個二進制信號量,它有兩個狀態:鎖定(locked)和未鎖定(unlocked)。
- 一次只能有一個 Goroutine 獲得 Mutex 的鎖,其他 Goroutine 需要等待該鎖的釋放才能繼續執行。
- 當一個 Goroutine 獲得 Mutex 的鎖時,其他 Goroutine 會被阻塞,直到該 Goroutine 釋放鎖。
- Mutex 確保在任何時候只有一個 Goroutine 可以訪問共享資源,從而避免了競態條件(race condition)和數據競爭(data race)的問題。
如何使用 Mutex:
- 首先,需要創建一個 Mutex 對象。在 Go 中,可以使用 sync 包提供的 Mutex 類型來創建 Mutex 對象。
- 在需要保護共享資源的臨界區代碼塊中,使用 Lock 方法獲取 Mutex 的鎖,以防止其他 Goroutine 訪問。
- 在臨界區代碼塊執行完畢後,使用 Unlock 方法釋放 Mutex 的鎖,允許其他 Goroutine 訪問。
- 當一個 Goroutine獲得 Mutex 的鎖時,其他 Goroutine 將被阻塞,直到該 Goroutine 釋放鎖。
package main
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex
func increment() {
mutex.Lock()
counter++
mutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
increment()
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
Mutex 的模式:
在使用 Mutex 時,常見的模式有兩種:
- 互斥模式(Exclusive Mode):一次只有一個 Goroutine 能夠獲得 Mutex 的鎖,其他 Goroutine 需要等待鎖的釋放。
- 讀寫模式(Read-Write Mode):允許多個 Goroutine 並發地獲得 Mutex 的讀鎖,但只允許一個 Goroutine 獲得 Mutex 的寫鎖。這種模式適用於多個 Goroutine 並發地讀取共享資源,但只能有一個 Goroutine 寫入共享資源的情況。
讀寫模式的實現可以使用 sync.RWMutex 類型,它提供了 RLock 和 RUnlock 方法用於讀鎖定,以及 Lock 和 Unlock 方法用於寫鎖定。通過使用讀寫鎖,我們可以在讀取共享資源時允許並發訪問,提高並發性能。
package main
import (
"fmt"
"sync"
"time"
)
var (
data = make(map[string]string)
mutex sync.RWMutex
)
func readData(key string) {
mutex.RLock()
defer mutex.RUnlock()
value := data[key]
fmt.Printf("Read: key=%s, value=%s\n", key, value)
}
func writeData(key string, value string) {
mutex.Lock()
defer mutex.Unlock()
data[key] = value
fmt.Printf("Write: key=%s, value=%s\n", key, value)
}
func main() {
go readData("key1")
go readData("key2")
go writeData("key1", "value1")
time.Sleep(1 * time.Second)
go readData("key1")
go readData("key2")
time.Sleep(2 * time.Second)
}
6. channel 的特性以及實現原理
通道(Channel)是 Go 語言中用於 Goroutine 間通信的重要機制之一。它具有以下特性:
- 通信機制:通道提供了一種同步的通信機制,允許 Goroutine 之間安全地發送和接收數據。通過通道,發送方和接收方可以進行阻塞等待,直到另一方准備好進行通信。
- 有類型:通道是具有特定類型的數據傳輸通道,只能傳輸該類型的數據。例如,可以創建整數類型的通道、字符串類型的通道等。
- 先進先出:通道遵循先進先出(FIFO)的原則,保證發送的數據和接收的數據按照發送的順序進行傳遞。
- 阻塞和非阻塞:通道提供了阻塞和非阻塞的操作。當通道為空時,接收操作會阻塞,直到有數據可接收;當通道已滿時,發送操作會阻塞,直到有空間可用。非阻塞操作則會立即返回,並通過返回值指示操作是否成功。
通道的實現原理是基於通道數據結構和 Goroutine 調度器的協同工作:
- 通道數據結構:通道的底層數據結構由一個隊列(queue)和一些元數據組成。隊列用於存儲發送的元素,元數據則包含有關通道狀態和操作的信息。
- Goroutine 調度器:通道的發送和接收操作會涉及 Goroutine 的調度和切換。發送操作將元素放入通道的隊列中,並阻塞當前 Goroutine,直到有接收方接收數據;接收操作則從隊列中取出元素,並阻塞當前 Goroutine,直到有發送方發送數據。
通過使用 Goroutine 的調度和切換,通道實現了高效的同步和通信機制,使得多個 Goroutine 可以在並發的情況下安全地進行數據傳遞。
在實踐中,可以使用 make 函數創建一個通道,並使用 <- 運算符進行數據的發送和接收。例如:
https://go.dev/play/p/0mJAFcDhYJh
ch := make(chan int) // 創建一個整數類型的通道
go func() {
ch <- 42 // 向通道發送數據
}()
result := <-ch // 從通道接收數據
fmt.Println(result) // 輸出: 42
在上面的示例中,我們創建了一個整數類型的通道 ch,然後在一個匿名的 Goroutine 中將數據 42 發送到通道。最後,我們通過 <-ch 的方式從通道中接收數據,並將結果賦給變量 result,最終打印出 42。
通過通道的特性和實現原理,我們可以實現 Goroutine 之間的同步和通信,確保數據的安全傳遞和協
7. map 如何實現的?是否為線程安全?如何擴容?
在 Go 語言中,map 是一種無序的鍵值對集合。它的實現是基於哈希表(Hash Table)。
map 的實現使用了哈希表來存儲鍵值對,其中每個鍵經過哈希函數的映射,得到一個桶(Bucket)的索引。每個桶中存儲了一個鍊錶或紅黑樹,用於解決哈希衝突的情況。
下面是 map 的實現原理和相關問題的回答:
- 線程安全性:
- 在 Go 語言中,
map不是並發安全的,即不支持多個 Goroutine 並發地讀取和修改同一個map。 - 如果需要在多個 Goroutine 中使用
map,需要使用額外的同步機制,比如使用互斥鎖(sync.Mutex)或讀寫鎖(sync.RWMutex)進行保護, 或者使用 sync.Map(Go1.9 及以後)。 - 一口氣搞懂 Go sync-map 所有知識點 : https://www.readfog.com/a/1636088860119240704
- sync.Map 類型:
- 在讀和刪場景上的性能是最佳的,領先一倍有多。
- 在寫入場景上的性能非常差,落後原生 map + 鎖整整有一倍之多。
- 因此在實際的業務場景中。假設是讀多寫少的場景,會更建議使用 sync.Map 類型
- sync.Map 類型:
- 在 Go 語言中,
- 擴容:
- 當
map中的元素數量超過一定閾值時,Go 語言會自動對map進行擴容,以提高性能。 - 擴容過程中,會創建一個新的更大的哈希表,並將原有的鍵值對重新哈希到新的桶中。
- 擴容過程是在後台進行的,不會阻塞其他 Goroutine 對
map的訪問。 - 擴容過程中,會暫停對
map的寫入操作,但讀取操作仍然可以繼續進行。
- 當
需要注意的是,由於 map 的擴容會重新哈希所有的鍵值對,因此擴容可能導致一些性能開銷。在實際應用中,如果對性能有更高的要求,可以在初始化時指定初始容量,以減少擴容次數。
8. 接口如何實現的?接口可以做什麼?
在 Go 語言中,接口(Interface)是一種定義行為的類型。接口定義了一組方法的集合,但不包含具體的實現。通過實現接口的方法,類型可以滿足接口的要求,並被視為該接口的實現類型。
接口的實現是隱式的,即類型只需要實現了接口中定義的所有方法,就被視為實現了該接口,無需顯式聲明。這種方式稱為結構化類型。
接口的定義格式如下:
type 接口名 interface {
方法1()
方法2()
// ...
}
通過接口,我們可以實現以下功能:
- 多態性(Polymorphism):接口使得我們可以通過統一的方式操作不同類型的對象。我們可以將實現了同一接口的不同類型的對象存儲在同一個容器中,以及使用相同的方法對它們進行操作,從而實現多態性。
- 代碼復用:通過接口,我們可以定義一組共同的行為,並讓不同的類型去實現這些行為。這樣可以提高代碼的可重用性,避免了重複編寫相似的代碼。
- 解耦和抽象:接口將抽象與具體實現分離,通過接口定義對外提供的方法,而無需關心具體的實現細節。這樣可以降低代碼的耦合度,提高代碼的可維護性和靈活性。
- 接口組合:可以通過接口嵌套和接口組合的方式,定義更複雜的接口,以滿足更多的行為要求。
package main
import "fmt"
// 接口定義
type Animal interface {
Speak()
}
// 實現接口的類型1
type Dog struct{}
func (d Dog) Speak() {
fmt.Println("Woof!")
}
// 實現接口的類型2
type Cat struct{}
func (c Cat) Speak() {
fmt.Println("Meow!")
}
func main() {
// 創建接口類型的變量,並分別賦值為不同類型的實例
var animal Animal
animal = Dog{}
animal.Speak() // 輸出: Woof!
animal = Cat{}
animal.Speak() // 輸出: Meow!
}
在上述示例中,我們定義了一個 Animal 接口,並讓 Dog 和 Cat 類型分別實現了 Speak 方法。然後,我們創建了一個接口類型的變量 animal,分別將其賦值為 Dog 和 Cat 的實例。通過調用 Speak 方法,我們可以看到不同類型的實例表現出了不同的行為。
接口的應用非常廣泛,它可以用於定義通用的行為規範,促進代碼的重用和擴展。通過接口,我們可以實現面向接口編程的思想,使代碼更加靈活和
9. 說一下 select 機制,如何使用
select 是 Go 語言中用於處理多個通道操作的控制結構。它可以用於在多個通道上進行非阻塞的讀取或寫入操作,以便實現並發的通信和同步。
select 語句的語法如下:
select {
case <-通道1:
// 執行通道1的讀取操作
case 數據 := <-通道2:
// 執行通道2的讀取操作,並將結果賦給變量數據
case 通道3 <- 數據:
// 執行通道3的寫入操作,將數據發送到通道3
default:
// 當沒有任何通道操作準備就緒時執行的邏輯
}
select 語句中可以包含多個 case 分支,每個 case 分支都是一個通道操作(讀取或寫入)。當其中任意一個通道操作準備就緒時,即可執行對應的分支代碼。如果多個通道操作同時準備就緒,會隨機選擇一個執行。
以下是 select 的使用示例:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(3 * time.Second)
ch2 <- 2
}()
select {
case <-ch1:
fmt.Println("Received from ch1")
case <-ch2:
fmt.Println("Received from ch2")
case <-time.After(4 * time.Second):
fmt.Println("Timeout")
}
}
在上面的示例中,我們創建了兩個通道 ch1 和 ch2,並在兩個 Goroutine 中分別向這兩個通道發送數據。
然後,我們使用 select 語句在這兩個通道上進行非阻塞的接收操作。其中,通過 <-ch1 和 <-ch2 分別接收 ch1 和 ch2 的數據,並打印相應的消息。
此外,我們還使用 time.After 創建了一個定時器,當超過指定的時間後,會觸發一個超時的分支。
通過 select 語句,我們可以在多個通道之間進行非阻塞的操作,根據實際情況選擇執行對應的代碼分支。這種機制使得我們可以實現靈活的並發通信和同步邏輯。
10. 說說對 GMP 的理解以及什麼是 Work stealing 和 Hand off 機制
GMP 是 Go 語言調度器的關鍵組成部分,用於管理 Goroutine 的創建、調度和執行。 GMP 代表以下概念:
- G(Goroutine):Goroutine 是 Go 語言中並發執行的基本單位。每個 Goroutine 都有自己的調用棧和上下文信息。 Goroutine 的創建和銷毀由調度器(Scheduler)管理。
- M(Machine):Machine 代表著操作系統線程(OS Thread),它負責執行 Goroutine。一個 M 可以關聯多個 Goroutine,但同一時刻只能執行一個 Goroutine。
- P(Processor):Processor 用於調度 Goroutine 到 Machine 執行。它是調度器與 M 之間的中介。每個 M 關聯一個 P,而 P 可以關聯多個 M。
GMP 模型的工作原理如下:
- 初始時,調度器創建了一些 M,並與一些 P 綁定。這些 M 等待接收 Goroutine 的調度。
- 當創建一個新的 Goroutine 時,調度器會選擇一個空閒的 M,並將 Goroutine 綁定到該 M 上。
- 當 M 執行 Goroutine 時,如果遇到阻塞操作(如 I/O),M 會與關聯的 P 斷開連接,讓 P 繼續調度其他 Goroutine。
- 斷開連接的 M 稱為閒置 M。閒置 M 會等待一段時間,如果在這段時間內沒有可執行的 Goroutine,它會將自己歸還給調度器,然後繼續等待新的任務。
- 調度器會根據需要創建新的 M,並與閒置 M 進行綁定,以處理更多的 Goroutine。
Work stealing(工作竊取)
是一種調度算法,用於在負載不均衡的情況下,實現 Goroutine 的平衡調度。當一個 M 執行完自己的 Goroutine 後,如果與其關聯的 P 上沒有更多的 Goroutine 可供調度,它會從其他 M 關聯的 P 中竊取 Goroutine 進行執行。這樣可以充分利用系統資源,提高並發執行效率。
Hand off(移交)
是一種調度機制,在某些情況下可以將 Goroutine 直接從一個 M 移交給另一個 M,而無需經過 P。這種機制可以減少調度器的開銷,提高 Goroutine 的執行效率。
總結起來,GMP 模型是 Go 語言調度器的核心機制,用於管理 Goroutine 的創建和調度。它通過 M、P 和 G 的協同工作,實現了高效的並發執行。 Work stealing 和 Hand off 是調度器的兩種調度機制,用於實現負載均衡和減少調度開銷。
11. 說說 Go 中的 GC 機制和屏障相關的技術
TODO: 寫到另外一個地方 Go面试题(六):一文弄懂 Golang GC、三色标记、混合写屏障机制【图文解析GC】 : https://blog.csdn.net/xiaodaoge_it/article/details/121890145
- Golang v1.3之前採用傳統採取標記-清除法,需要STW,暫停整個程序的運行。
- 在v1.5版本中,引入了三色標記法和插入寫屏障機制,其中插入寫屏障機制只在堆內存中生效。但在標記過程中,最後需要對棧進行STW。
- 在v1.8版本中結合刪除寫屏障機制,推出了混合屏障機制,屏障限制只在堆內存中生效。避免了最後節點對棧進行STW的問題,提升了GC效率
屏障(Barrier)是與垃圾回收器相關的技術之一,用於在程序運行過程中追踪對象的引用修改。 Go 語言的垃圾回收器使用了寫屏障(Write Barrier)和讀屏障(Read Barrier)來捕獲對對象引用的修改。 屏障技術在並發標記-清除算法中起到重要的作用,它們確保了垃圾回收器能夠準確地追踪和回收不再使用的對象,同時不干擾程序的正常執行。
寫屏障(Write Barrier)
寫屏障用於在修改指針時,通知垃圾回收器進行必要的標記和追踪操作。當程序執行寫操作時,寫屏障會檢查指針的修改,並將相關的對象標記為灰色,以確保這些對象能夠被垃圾回收器正確地處理。
插入寫屏障
規則:當一個對象引用另外一個對象時,將另外一個對象標記為灰色。 滿足:強三色不變式。不會存在黑色對象引用白色對象 插入寫屏障最大的弊端就是,在一次正常的三色標記流程結束後,需要對棧上重新進行一次stw,然後再rescan一次
刪除寫屏障
規則:在刪除引用時,如果被刪除引用的對象自身為灰色或者白色,那麼被標記為灰色。 滿足: 弱三色不變式。灰色對像到白色對象的路徑不會斷
對比插入寫屏障和刪除寫屏障:
- 插入寫屏障:
- 插入寫屏障哪裡都好,就是棧上的操作管不到,所以最後需要對棧空間進行stw保護,然後rescan保證引用的白色對象存活。
- 刪除寫屏障:
- 在GC開始時,會掃描記錄整個棧做快照,從而在刪除操作時,可以攔截操作,將白色對象置為灰色對象。
- 回收精度低。
讀屏障(Read Barrier)
讀屏障用於在讀取指針時,確保所讀取的指針引用的對像不會被回收。當程序執行讀操作時,讀屏障會將相關對像從白色標記為灰色,以防止垃圾回收器錯誤地回收這些對象。
12. 說一說 Go 中的內存分配和逃逸分析
在 Go 語言中,內存分配是自動管理的,開發人員無需顯式地進行內存分配和釋放。 Go 編譯器在編譯時會對代碼進行逃逸分析,以確定變量的生命週期和是否逃逸到堆上進行分配。
逃逸分析是一種靜態分析技術,用於確定變量在函數內部是否逃逸到函數外部。如果一個變量逃逸到函數外部,意味著它的生命週期超出了函數的範圍,需要在堆上分配內存。相反,如果變量不逃逸,它可以在棧上分配內存,由編譯器自動管理。
逃逸分析對於優化內存分配和減少垃圾回收的影響非常大。以下是逃逸分析的一些關鍵點:
- 棧(Stack)分配:當一個變量的生命週期僅限於函數內部且沒有逃逸時,它可以在棧(Stack)上分配內存。棧(Stack)上的內存分配和釋放速度快,適合短暫的對象。
- 堆(Heap)分配:當一個變量逃逸到函數外部時,它需要在堆上分配內存。堆上的內存分配和釋放相對較慢,但對於長時間存在或需要跨函數訪問的對像是必需的。
逃逸分析的好處是可以減少垃圾回收的壓力和內存分配的開銷。當變量逃逸到堆(Heap)上時,它們的內存由垃圾回收器管理,當它們不再被引用時會被自動回收。而對於棧(Stack)上分配的對象,它們的生命週期與函數的調用關係直接相關,不需要垃圾回收的介入。
逃逸分析在編譯階段進行,因此不會對運行時性能產生額外的開銷。 編譯器會根據逃逸分析的結果,優化變量的分配方式,盡量減少內存分配和垃圾回收的開銷,提高程序的執行效率。
總結起來,Go 語言的內存分配是自動管理的,並通過逃逸分析確定變量的生命週期和內存分配位置。逃逸分析有助於減少垃圾回收的壓力和內存分配的開銷,提高程序的性能。
13. 說一下進程、線程、協程的區別,這個要整花活
concurrency/01_Introduction/02_Process_vs_Threads.md
進程(Process)
進程是操作系統中的一個執行實體,它擁有獨立的內存空間和系統資源。每個進程都是獨立運行的,相互之間不會干擾。進程之間通過進程間通信(Inter-Process Communication, IPC)機制進行數據交換和協作。進程是操作系統進行資源分配和調度的基本單位。
線程(Thread)
線程是進程內的一個執行單元,它與其他線程共享同一個進程的內存空間和系統資源。多個線程可以在同一進程內並發執行,共享進程的上下文環境。線程之間通過共享內存進行數據交換,但需要注意同步和互斥的問題。線程的創建、銷毀和調度由操作系統負責。
協程(Coroutine)
協程是一種輕量級的線程,也稱為用戶級線程。協程是由程序員在應用程序中自行管理的,不依賴於操作系統的線程調度機制。 協程擁有自己的棧空間(Stack),可以在一個或多個線程上並發執行。協程之間通過協作式調度進行切換,需要程序員自行控制協程的切換點。協程可以提高程序的並發性和性能,適用於處理大量的 I/O 操作或密集的計算任務。
區別
- 調度機制:進程和線程由操作系統的調度器進行管理和調度,調度器決定哪個進程或線程可以執行。而協程由程序員自行控制調度,程序員決定在什麼時候切換協程的執行。
- 資源開銷:進程是獨立的執行實體,每個進程都有自己的內存空間和系統資源,因此進程間的切換需要較大的資源開銷。線程共享進程的資源,線程間的切換較為快速,但仍然需要較大的開銷。協程是輕量級的,切換開銷非常小,因為協程的調度是由程序員自行控制。
- 編程模型:進程和線程屬於內核級調度,由操作系統提供的系統調用接口進行管理。協程屬於用戶級調度,由應用程序自行控制。協程的編程模型更加靈活,可以根據具體的應用場景進行優化和控制。
總結起來,進程是操作系統進行資源分配和調度的基本單位,線程是進程內的執行單元,而協程是由程序員自行管理和調度的輕量級線程。它們在調度機制、資源開銷和編程模型上存在明顯的區別,適用於不同的應用場景
14. 手寫 Go 中某個包的大致實現,比如 context 包如何實現的,可以寫一下嗎?
package context
import (
"context"
"sync"
"time"
)
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
type cancelCtx struct {
parent context.Context
mu sync.Mutex
done chan struct{}
err error
}
func (c *cancelCtx) Deadline() (deadline time.Time, ok bool) {
return c.parent.Deadline()
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) Value(key interface{}) interface{} {
return c.parent.Value(key)
}
func (c *cancelCtx) cancel(err error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.err = err
close(c.done)
}
}
func WithCancel(parent context.Context) (context.Context, context.CancelFunc) {
ctx := &cancelCtx{
parent: parent,
done: make(chan struct{}),
}
cancel := func() {
ctx.cancel(context.Canceled)
}
return ctx, cancel
}
func Background() context.Context {
return context.Background()
}
func TODO() context.Context {
return context.TODO()
}
項目(結合了組件八股來問)
1. 列舉一些你在項目中做的比較有挑戰的事情或者業務,比如具體的技術細節體現,如何攻堅某個難點,怎麼做技術選型的(幾乎每個組件都要問一下,問什麼要用這個組件,而不是其他的)?
性能優化:在項目中,性能問題通常是一個重要的挑戰。這可能涉及到優化關鍵代碼段的執行速度、減少資源消耗或提高系統的可擴展性。在解決性能問題時,可以通過使用更高效的數據結構、並發編程、緩存技術或者分佈式系統架構等方法來改善性能。
大規模數據處理:處理大規模數據集時,可能需要考慮如何優化數據讀取、存儲和處理。這可能涉及到選擇合適的數據存儲引擎、使用分佈式計算框架、並行化處理等。技術選型時,需要評估各種方案的性能、可擴展性、易用性和維護成本等因素。
安全性和隱私保護:保護用戶數據的安全和隱私是一個重要的挑戰。在設計和實現系統時,需要考慮身份驗證、數據加密、訪問控制等安全機制,並遵守隱私保護法規和最佳實踐。
架構設計和技術選型:在選擇組件和技術時,需要綜合考慮多個因素,包括功能需求、性能要求、團隊熟悉度、社區支持和未來擴展性等。常見的技術選型包括數據庫選擇、消息隊列系統、緩存方案、框架選型等。在做技術選型時,可以評估各種方案的優缺點,並結合實際需求和團隊條件做出決策。
複雜業務邏輯處理:某些業務場景可能涉及復雜的業務邏輯,例如交易系統、工作流引擎等。在處理複雜業務邏輯時,可以使用規則引擎、狀態機、領域驅動設計(DDD)等技術手段來管理和執行複雜的業務流程。
攻克這些挑戰的一般方法包括:深入分析問題、進行合適的技術調研和評估、利用現有的工具和框架、與團隊成員合作、進行測試和性能優化、不斷迭代和改進。同時,團隊成員之間的良好溝通和合作也是成功解決難題的關鍵
2. 你們項目的架構是什麼樣的,可以說一下數據流向和請求流向嗎?
3. 你對 Jaeger 和 OpenTracing 怎麼理解的?那你怎麼理解 TraceID 和 SpanID 的定義的? Jaeger 的實現原理是怎麼樣的?
4. Prometheus 是怎麼做監控告警的?有哪些組件,實現流程是怎麼樣的?
Prometheus是一套開源的監控和警報系統,它通過收集時間序列數據並提供靈活的查詢語言來實現監控功能。 Prometheus的監控告警通常涉及以下組件和流程:
Exporters:Prometheus通過Exporter組件來收集應用程序和系統的指標數據。 Exporter是一個獨立的進程或庫,它可以將應用程序或系統的指標暴露給Prometheus。常見的Exporter包括Node Exporter(用於收集主機級別的指標)、Blackbox Exporter(用於網絡探活和監控)、MySQL Exporter(用於MySQL數據庫監控)等。
Prometheus Server:Prometheus Server負責從Exporter或其他數據源中獲取指標數據,並存儲在本地的時間序列數據庫中。它定期地拉取指標數據,可以進行數據的聚合和存儲,並提供查詢和警報功能。 Prometheus Server還負責自動發現Exporter,並維護與Exporter的連接。
數據存儲:Prometheus使用本地的時間序列數據庫存儲指標數據。默認情況下,Prometheus使用一種稱為TSDB(Time Series Database)的格式來存儲數據。 TSDB使用分塊存儲策略,可以有效地壓縮和存儲大量的時間序列數據。
查詢和可視化:Prometheus提供了PromQL查詢語言,可以用於從存儲的時間序列數據中提取和分析指標。用戶可以使用PromQL查詢數據並生成圖表或儀表板來進行可視化。
告警規則:Prometheus允許用戶定義告警規則,以便在指標達到預定義的閾值時觸發告警。告警規則通常使用PromQL表達式來描述告警條件。當觸發告警時,Prometheus可以發送通知到配置的接收器,如電子郵件、PagerDuty、Slack等。
整個流程可以概括為以下步驟:
- 配置和啟動Prometheus Server,並指定要監控的目標和Exporter的地址。
- Exporter將指標數據暴露給Prometheus Server。
- Prometheus Server定期拉取指標數據,並存儲在本地的時間序列數據庫中。
- 用戶可以使用PromQL查詢數據,並進行數據分析和可視化。
- 用戶可以定義告警規則,並配置接收告警通知的方式。
- 當指標達到預定義的告警條件時,Prometheus觸發告警並發送通知。
通過這些組件和流程,Prometheus實現了靈活的監控和告警功能,可以幫助用戶監控和分析應用程序、系統和基礎設施的性能和狀態。
5. etcd 是用來幹嘛的?怎麼實現的?為什麼選用 etcd 而不是 Redis?
etcd是一個分佈式鍵值存儲系統,用於可靠地存儲和檢索數據。 它主要用於構建分佈式系統和服務發現,是一種高度可用、一致性的數據存儲解決方案。
etcd的實現基於Raft一致性算法,它將數據分佈在一個或多個節點上,每個節點都有完整的數據副本。節點之間通過Raft協議進行通信和數據複製,保證了數據的一致性和可靠性。
etcd的設計目標包括:
- 一致性:etcd保證數據的一致性,即每個節點的數據副本都是一致的。
- 可靠性:etcd通過複製機制和選主算法來確保數據的可靠性。即使其中一個節點出現故障,系統仍然能夠繼續正常運行。
- 高可用性:etcd使用Raft算法實現領導者選舉和故障轉移,確保系統在節點故障時能夠繼續提供服務。
為什麼選擇etcd而不是Redis?
etcd和Redis都是鍵值存儲系統,但它們在設計目標和功能方面有一些區別。
一致性模型:etcd使用Raft算法實現強一致性,而Redis默認使用主從復制實現弱一致性。對於需要強一致性的場景,如分佈式系統的配置管理、服務發現等,etcd更適合。
高可用性:etcd通過Raft算法提供了高可用性機制,支持故障轉移和自動重新選舉。 Redis需要手動配置主從復制和哨兵來實現高可用性。
功能重點:Redis是一個功能豐富的數據結構服務器,支持各種數據類型和豐富的操作。它在緩存、隊列、發布訂閱等方面表現出色。而etcd的設計更專注於分佈式一致性和服務發現,提供了強大的分佈式存儲和同步功能。
綜上所述,選擇etcd還是Redis取決於具體的需求。如果需要構建分佈式系統、服務發現或強一致性的數據存儲,etcd是更合適的選擇。而如果需要一個多功能的數據結構服務器,同時對一致性要求較低,Redis可能更適合。
Redis哨兵模式
Redis哨兵模式是一種用於提高Redis高可用性的解決方案。它通過引入哨兵節點來監控和管理Redis主節點和從節點,實現自動故障檢測和故障轉移。
在Redis哨兵模式中,有以下幾個角色:
哨兵節點(Sentinel):哨兵節點是一個獨立的進程,負責監控Redis主節點和從節點的狀態。它會定期向節點發送PING命令來檢測節點的健康狀態,並通過SENTINEL is-master-down-by-addr命令來判斷主節點是否宕機。如果哨兵節點檢測到主節點宕機,它會發起一次故障轉移操作。
Redis主節點(Master):Redis主節點是提供讀寫服務的節點。在哨兵模式中,主節點的狀態由哨兵節點進行監控,並在主節點宕機時選擇一個從節點升級為新的主節點。
Redis從節點(Slave):Redis從節點是主節點的複製品,用於提供讀取服務和實現數據冗餘。從節點會復制主節點的數據,並在主節點宕機時接替成為新的主節點。
哨兵模式的工作流程如下:
- 哨兵節點啟動並配置監控主節點和從節點的信息。
- 哨兵節點定期向主節點發送PING命令檢測其健康狀態。
- 如果哨兵節點檢測到主節點宕機,它會與其他哨兵節點進行協商,選舉出一個哨兵節點來執行故障轉移操作。
- 選舉出的哨兵節點會向其他哨兵節點發送通知,然後進行故障轉移操作。
- 故障轉移過程包括選擇一個從節點作為新的主節點,並將其他從節點切換為新的主節點的從節點。
- 客戶端可以通過與哨兵節點交互來獲取新的主節點的信息,以便繼續進行讀寫操作。
通過哨兵模式,Redis可以在主節點宕機時自動進行故障轉移,保證服務的可用性。它提供了一種簡單而有效的方式來實現Redis的高可用性,並且可以在運行時動態地調整主節點和從節點的配置。
6. Redis 的數據結構以及源碼深究,為何高性能和快速?數據一致性方案是怎麼做的?如何做持久化? AOF 重寫機制怎麼做的?過期策略是怎麼樣的?主從同步的流程是啥樣的,什麼情況下會觸發全量和增量同步?如何解決?如何利用 Redis 的數據結構設計一個符合業務需求的數據模型?哨兵機制介紹一下? I/O 模型是啥樣的? Redis 是單線程還是多線程?如何解決大 Key、冷 Key、熱 Key 的問題? etc.
Redis的數據結構:
- 字符串(String)
- 哈希表(Hash)
- 列表(List)
- 集合(Set)
- 有序集合(Sorted Set)等。
如果 你是Redis中高級用戶,還需要加上下面幾種數據結構
- HyperLogLog
- Geo
- Pub/Sub
如果你說還玩過Redis Module,像
- BloomFilter
- RedisSearch
- Redis-ML 面試官得眼睛就開始發亮了。

Redis的高性能和快速主要體現在以下幾個方面:
內存存儲:Redis將數據存儲在內存中,讀寫操作都是在內存中完成,相比於磁盤I/O操作,速度更快。
單線程模型:Redis採用單線程模型,通過事件驅動的方式處理客戶端請求。這樣可以避免線程切換和鎖競爭的開銷,提高了性能。
高效的網絡模型:Redis使用自己開發的網絡模型,採用非阻塞I/O和事件通知機制,充分利用操作系統的多路復用特性,提高網絡通信效率。
精細的數據結構設計:Redis針對不同的數據結構,採用高效的底層實現方式。例如,使用壓縮列表來存儲較小的列表對象,使用跳躍表來實現有序集合等。
數據一致性方案:
Redis的數據一致性是通過複製機制實現的。它採用主從復制(最終一制性)的方式將主節點上的數據複製到從節點上,從而實現數據的冗餘和高可用性。
Redis的持久化:
Redis提供了兩種持久化方式:RDB(Redis Database)和AOF(Append-Only File)。
RDB持久化:RDB是一種快照的方式,將Redis在內存中的數據以二進制格式保存到磁盤上。可以根據配置的策略定期或手動執行持久化操作。 RDB持久化適合用於備份和恢復數據。
AOF持久化:AOF持久化以日誌的形式記錄每個寫操作,將操作追加到AOF文件中。通過重放AOF文件中的操作,可以恢復數據。 AOF持久化支持不同的同步策略,包括每個寫操作、每秒同步、不同步等。 AOF持久化適合用於實現數據的持久性和故障恢復。
AOF重寫機制:
為了避免AOF文件過大,Redis引入了AOF重寫機制。 AOF重寫是通過生成新的AOF文件來替換原有的AOF文件,去除了舊文件中的冗餘操作,減小了AOF文件的大小。 AOF重寫是在後台進行的,不會阻塞主進程的正常操作。
過期策略:
Redis提供了兩種過期策略 定時刪除和惰性刪除。
- 定時刪除:Redis會使用一個定時器來檢查設置了過期時間的鍵,當鍵過期時,會立即刪除。
- 惰性刪除:Redis在訪問一個鍵時,會先檢查該鍵是否過期,如果過期則刪除。這樣可以避免在定時刪除過程中的大量鍵刪除操作。
主從同步流程:
Redis的主從復制分為全量同步和增量同步兩個階段。
全量同步(full synchronization)
SYNC:當從節點連接到主節點時,它會發送SYNC命令請求全量同步。主節點執行BGSAVE命令生成RDB文件,並將RDB文件發送給從節點。從節點加載RDB文件,將主節點的數據完全複製到自己的數據庫中。增量同步(incremental synchronization)
PSYNC:全量同步完成後,主節點會將後續的寫操作以命令的形式發送給從節點,從節點執行相同的寫操作,實現主從數據的同步。
Redis 2.8以前採用的複製都為全量複製。 Redis在2.8及以上版本使用PSYNC命令完成主從數據同步,PSYNC同步過程分為全量複製和部分複制,完善了SYNC存在的缺陷。
什麼情況下會觸發全量和增量同步
在 Redis 主從復制中,會出現以下情況觸發全量同步和增量同步:
全量同步(Full synchronization):
- 主節點(Master)啟動或重新啟動時,會觸發全量同步。
- 新添加的從節點(Slave)首次連接到主節點時,會觸發全量同步。
- 從節點斷線重連後,如果復制偏移量(replication offset)無效,會觸發全量同步。
增量同步(Incremental synchronization):
- 從節點與主節點成功建立連接後,會進行增量同步以保持數據一致。
- 當主節點接收到新的寫命令時,會將寫命令的複制操作發送給所有從節點,從節點會接收並執行這些複製操作來進行增量同步。
- 增量同步會持續進行,直到從節點與主節點的數據完全一致。
需要注意的是,在 Redis 的複製過程中,全量同步只在特定情況下觸發,而增量同步是持續進行的。 全量同步會將主節點的數據完整復製到從節點,而增量同步只傳輸從斷開連接後的數據更新部分,以減少網絡傳輸和提高同步效率。
此外,Redis 還支持部分同步(Partial Resynchronization)的方式來進行快速恢復和增量同步。部分同步是在全量同步的基礎上,通過傳輸複製偏移量和主節點的運行 ID 進行增量同步,以減少複製的數據量和時間。 全量同步和增量同步是在 Redis 主從復制中的不同階段和情況下觸發的機制,用於確保主節點和從節點之間的數據一致性和持續同步。
如何利用 Redis 的數據結構設計一個符合業務需求的數據模型?

1. Cache : string
2. Session : string Hash
在使用 Redis 實現共享 session 時,可以使用以下數據結構:
Redis Hash:使用 Redis 的 Hash 數據結構存儲 session 數據。可以將每個 session 存儲為一個 Hash,使用一個唯一的鍵來標識每個 session,並使用字段來存儲 session 的屬性和值。
// 存儲 session err := redisClient.HMSet(ctx, "session:sessionId", map[string]interface{}{ "user_id": "123", "username": "john", "last_login": "2022-01-01", }).Err() // 獲取 session result, err := redisClient.HGetAll(ctx, "session:sessionId").Result() if err == nil { // 處理 session 數據 }Redis String:將整個 session 數據以字符串形式存儲在 Redis 中。這種方式適用於 session 數據較小且不需要進行複雜的操作。
示例代碼:
// 存儲 session err := redisClient.Set(ctx, "session:sessionId", "user_id=123&username=john&last_login=2022-01-01", 0).Err() // 獲取 session sessionData, err := redisClient.Get(ctx, "session:sessionId").Result() if err == nil { // 處理 session 數據 }Hash 數據結構可以更好地組織和管理 session 數據,而 String 數據結構簡單且輕量,適合存儲簡單的 session 數據。
3. 限速 string
func main() {
// 創建 Redis 客戶端
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 如果有密碼,則填寫密碼
DB: 0, // 選擇要使用的數據庫,默認為 0
})
// 定義手機號和 Redis 鍵名
phoneNum := "138xxxxx2xx"
key := "shortMsg:limit:" + phoneNum
// 嘗試設置鍵值對,設置過期時間為 60 秒,僅在鍵不存在時設置成功
isExists, err := redisClient.SetNX(context.Background(), key, 1, 60*time.Second).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
// 判斷鍵是否不存在
if isExists {
// 鍵已存在,檢查計數器的值
counter, err := redisClient.Incr(context.Background(), key).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
if counter <= 5 {
fmt.Println("Access granted")
} else {
fmt.Println("Rate limited")
}
} else {
// 第一次設置成功,計數器初始值為 1
_, err := redisClient.Incr(ctx, key).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Access granted")
}
}
4. 計數器 string HyperLogLog
要使用 Redis 的 HyperLogLog 數據結構實現計數器功能,你可以利用 HyperLogLog 的基數統計特性來實現近似的計數器功能。 HyperLogLog 提供了一種高效的基數估計算法,適用於大規模數據的基數統計。
以下是一個使用 HyperLogLog 實現計數器的示例:
package main
import (
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
// 創建 Redis 客戶端連接
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 如果有密碼,則填寫密碼
DB: 0, // 選擇要使用的數據庫,默認為 0
})
// 增加計數器值
err := redisClient.PFAdd(context.Background(), "counter", "item1").Err()
if err != nil {
log.Fatal(err)
}
err = redisClient.PFAdd(context.Background(), "counter", "item2").Err()
if err != nil {
log.Fatal(err)
}
err = redisClient.PFAdd(context.Background(), "counter", "item3").Err()
if err != nil {
log.Fatal(err)
}
// 獲取計數器值的近似基數
count, err := redisClient.PFCount(context.Background(), "counter").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("Counter:", count) // 3
// 關閉 Redis 客戶端連接
err = redisClient.Close()
if err != nil {
log.Fatal(err)
}
}
在上面的示例中,我們使用 PFAdd 方法將不同的元素添加到 HyperLogLog 結構中,模擬增加計數器值。然後,我們使用 PFCount 方法獲取近似的基數估計值,即計數器的值。
需要注意的是,HyperLogLog 的計數器是基於近似統計的,不保證絕對準確。如果需要精確的計數器功能,建議使用 Redis 的普通數據結構,如字符串、哈希表等,並結合原子操作來實現計數器功能。
5. 用戶訊息 Hash
hmset user:1 name kimi age 18 city taipei
6. 消息對列 List Pub/Sub
lpush+brpop
Brpop 命令用於從list中移出並獲取列表的最後一个元素, 如果列表没有元素會阻塞列表直到等待超時或發現可彈出元素為止
它是 RPOP 命令的阻塞版本。
因為會導致客戶端在沒有獲取到元素時一直阻塞。因此,使用時需要根據實際需求合理設置超時時間,以避免無限阻塞的情況發生
LPUSH queue_name message
BRPOP key [key ...] timeout
Pub/Sub
package main
import (
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
// 創建 Redis 客戶端連接
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 如果有密碼,則填寫密碼
DB: 0, // 選擇要使用的數據庫,默認為 0
})
// 創建訂閱者
sub := redisClient.Subscribe("channel")
// 獲取訂閱通道的消息
ch := sub.Channel()
// 啟動一個 goroutine 處理接收到的消息
go func() {
for msg := range ch {
fmt.Println("Received message:", msg.Payload)
}
}()
// 發布消息到通道
err := redisClient.Publish(context.Background(), "channel", "Hello, Redis Pub/Sub").Err()
if err != nil {
log.Fatal(err)
}
// 等待一段時間,觀察是否接收到消息
time.Sleep(time.Second)
// 關閉訂閱者連接
err = sub.Close()
if err != nil {
log.Fatal(err)
}
// 關閉 Redis 客戶端連接
err = redisClient.Close()
if err != nil {
log.Fatal(err)
}
}
7. IP對應城市 Hash Zset
https://github.com/kimi0230/RedisIPCountry
- 散列(Hash)數據結構:你可以使用Redis的Hash數據結構來存儲city id和對應的城市信息。 (Hash key=cityid2city, field= $city.CityId, value=$value)
- 有序集合(Sorted Set)數據結構:你可以使用Redis的Sorted Set數據結構來按照IP地址的範圍進行存儲。將IP地址轉換為整數表示,並作為Sorted Set的score,城市作為對應的member。這樣可以方便地進行IP地址範圍的查詢。 (zset: key = ip2cityid, member = $cityID, score = $resIP)
字符串(String)數據結構:如果你有一個已經處理好的IP地址到城市的映射數據,你可以將整個映射數據以字符串的形式存儲在Redis中。這樣可以簡單地將整個映射數據加載到內存中進行查詢。
func (c *Client) FindCityByIp(ctx context.Context, ip string) string {
ipAddress := strconv.Itoa(int(c.IpToScore(ip)))
// Min:最小分數, Max:"10" 最大分數, Offset:0 類似 sql 的 limit, Count: 一次返回多少數據
res := c.Conn.ZRevRangeByScore(ctx, "ip2cityid:", &redis.ZRangeBy{Max: ipAddress, Min: "0", Offset: 0, Count: 2}).Val()
if len(res) == 0 {
return ""
}
// 從 ip2cityid (zset) 取出 city id 並在 cityid2city(hash)中找城市資訊
cityId := strings.Split(res[0], "_")[0]
var result cityInfo
if err := json.Unmarshal([]byte(c.Conn.HGet(ctx, "cityid2city:", cityId).Val()), &result); err != nil {
log.Fatalln("unmarshal err: ", err)
}
return strings.Join([]string{result.CityId, result.City, result.Country, result.Region}, " ")
}
8. 排行榜 list zset
- ZRANK用於獲取成員在有序集合中的排名(索引位置)。返回一個整數(排名)
- ZSCORE用於獲取成員在有序集合中的分值。返回一個浮點數(分值)
# 添加用戶讚數
zadd user:ranking:2016_03_15 mike 3
zincrby user:ranking:2016_03_15 mike 1
# 取消用戶讚數
zrem user:ranking:2016_03_15 mike
# 展示用户信息以及用戶分數
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike
9. 文章列表 list hash
- 每篇文章使用 Hash 存儲, 例如每篇文章有3個屬性 title、timestamp、content
- 向用户文章列表添加文章 user {id} articles 作為用户文章列表的鍵
- 分頁 取用户文章列表 例如下面偽代碼 取用户id=1的前10篇文章
hmset article:1 title xx timestamp 1476536196 content xxxx
lpush user:1:acticles article:1 article:3
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}
10. tags set
- 用戶喜好的tag, 可以找出共同喜好
- 用戶和tag的關係維護應該在一個事物內執行, 防止部分命令失敗造成數據不一致
使用有序集合(Sorted Set)和集合(Set)結合的方式。
實現用戶的喜好標籤和查找共同喜好:
// 添加用戶的喜好標籤
func addTagsForUser(userID string, tags []string) error {
// 將標籤添加到用戶的集合中
key := "user:" + userID + ":tags"
members := make([]redis.Z, len(tags))
for i, tag := range tags {
members[i] = redis.Z{Score: 0, Member: tag}
}
_, err := redisClient.ZAdd(context.Background(), key, members...).Result()
if err != nil {
return err
}
// 將標籤添加到對應的標籤集合中
for _, tag := range tags {
tagKey := "tag:" + tag + ":users"
_, err := redisClient.SAdd(context.Background(), tagKey, userID).Result()
if err != nil {
return err
}
}
return nil
}
// 查找共同喜好的用戶
func findUsersWithCommonTags(userID string) ([]string, error) {
// 獲取用戶的標籤集合
userTagsKey := "user:" + userID + ":tags"
userTags, err := redisClient.ZRange(context.Background(), userTagsKey, 0, -1).Result()
if err != nil {
return nil, err
}
// 找出共同喜好的用戶
commonUsers := make([]string, 0)
for _, tag := range userTags {
tagKey := "tag:" + tag + ":users"
users, err := redisClient.SMembers(context.Background(), tagKey).Result()
if err != nil {
return nil, err
}
if len(commonUsers) == 0 {
commonUsers = users
} else {
commonUsers = intersect(commonUsers, users)
}
}
return commonUsers, nil
}
// 求兩個字符串切片的交集
func intersect(a, b []string) []string {
m := make(map[string]bool)
for _, item := range a {
m[item] = true
}
res := make([]string, 0)
for _, item := range b {
if m[item] {
res = append(res, item)
}
}
return res
}
上述代碼示例中,addTagsForUser函數用於給用戶添加喜好標籤。它將用戶的標籤存儲在有序集合中,並將每個標籤也添加到對應的標籤集合中。
findUsersWithCommonTags函數用於查找與指定用戶具有共同喜好的其他用戶。
它首先獲取指定用戶的標籤集合,然後依次遍歷每個標籤,找出對應的標籤集合中的用戶,
最後求取所有標籤集合的交集,得到共同喜好的用戶。
11. 社交網路
- 讚, 粉絲, 共投好友/喜愛, 推送, 下拉刷新
- 由於社交網站的訪問量通常比較大, 傳統的關聯數據不太適合
Redis 可以使用以下數據結構來實現這些功能:
- 字符串(String):可以用來存儲點贊數量、粉絲數量等簡單的計數器數據。
- 集合(Set):可以用來存儲用戶的粉絲列表、好友列表等。通過集合的交、並、差等操作,可以方便地進行共同喜歡的用戶、共同好友的計算。
- 有序集合(Sorted Set):可以用來存儲用戶的點贊記錄,成員為被點讚的對象,分數為點讚的時間戳,可以通過分數範圍查詢、按分數排序等操作來實現推送和下拉刷新功能。
- 列表(List):可以用來存儲推送消息的隊列,新的消息插入列表的頭部,用戶拉取消息時從列表的尾部取出。
12. 紀錄總數 bitmaps, HyperLogLog
- 如果不需要個別內容, 且接受誤差 使用 HyperLogLog. 參考4.計數器
- bitmaps. 每個獨立用戶是否訪問過網站存放到bitmaps中, 將訪問的用戶記做1; 沒有的記做0, 用偏移量作為用戶的id
在 Redis 中,可以使用位圖(Bitmap)來實現計算總數的功能。 Bitmap是一種緊湊的數據結構,用於存儲大量的二進制位。在計算總數的場景中,我們可以使用Bitmap來表示某個特定事件的發生情況,例如用戶簽到、用戶訪問等。
以下是使用Bitmap計算總數的基本步驟:
- 選擇一個適當的鍵名,用於存儲Bitmap數據。
- 初始化Bitmap,可以使用
SETBIT命令將Bitmap的初始狀態設置為0,例如:SETBIT key offset 0。 - 根據實際情況,使用
SETBIT命令或BITOP命令對Bitmap進行更新。對於每個發生了特定事件的用戶或對象,可以使用SETBIT命令將對應位置為1,例如:SETBIT key offset 1。 - 使用
BITCOUNT命令計算Bitmap中為1的位數,即總數。例如:BITCOUNT key。
通過以上步驟,我們可以使用Bitmap來快速、高效地計算特定事件發生的總數。 需要注意的是,Bitmap是一種緊湊的數據結構,可以有效地使用內存。 但也需要注意Bitmap的大小和內存消耗,特別是在處理大規模數據時,需要合理控制Bitmap的大小,避免佔用過多的內存資源。
13. 聊天室, 公告, 服務之間的消息傳遞 Pub/Sub
14. 搜尋功能 set, zset
反向索引 (inverted indexes), 反向索引會從每一個被索引的文檔裡面提出一些單詞, 並創建表格來記錄每篇文章都包含哪些單詞

哨兵機制:
Redis的哨兵機制用於實現高可用性。哨兵是一個獨立的進程,負責監控主節點和從節點的狀態。當哨兵節點檢測到主節點宕機時,它會自動將其中一個從節點升級為新的主節點,實現故障轉移。 哨兵機制還支持自動發現新的主節點和從節點,並進行配置更新。
I/O模型:
Redis使用了多種I/O模型,包括阻塞I/O、非阻塞I/O、事件驅動I/O和異步I/O。根據不同的操作和配置,Redis會選擇最合適的I/O模型來提高性能和並發能力。
Redis是單線程的還是多線程的:
Redis在主進程中是單線程的,主要用於處理客戶端的請求和執行命令。 但是,Redis在後台會有多個線程執行不同的任務,例如持久化、AOF重寫、主從復制等。
解決大Key、冷Key、熱Key問題:
針對大Key、冷Key、熱Key等問題,可以採取以下策略:
- 大Key:使用Hash數據結構將大Key拆分成多個小Key,減少單個鍵值對的大小。 Big key
- 冷Key:對於很少被訪問的Key,可以考慮設置合適的過期時間,或者通過Redis的LRU機制進行自動淘汰。
- 熱Key:對於頻繁被訪問的Key,可以考慮使用Redis的內存淘汰策略,例如volatile-lru、allkeys-lru等,保留熱門的Key,淘汰不常用的Key。
7. MySQL 中的事務你介紹下,隔離級別都有啥,怎麼實現的? MVCC 你說一下怎麼實現的,如何解決幻讀?你們的數據庫表是如何設計的?如何設計索引,索引的實現有哪幾種方式,為什麼要用 B+ 樹?說一說你項目中的反範式的設計,為什麼要用反範式?說一說你在使用 MySQL 過程中遇到的坑?
MySQL 中的事務你介紹下,隔離級別都有啥,怎麼實現的?
數據庫的隔離級別 MySQL 中的事務是一組數據庫操作的邏輯單元,要么全部執行成功,要麼全部回滾到初始狀態。 事務提供了數據的一致性和隔離性,確保多個並發操作不會相互干擾。
MySQL 支持多種隔離級別來控制事務的並發操作:
- 讀未提交(Read Uncommitted):最低的隔離級別,允許一個事務讀取另一個事務尚未提交的數據。此級別可能導致臟讀(Dirty Read),即讀取到未提交的數據。
- 讀已提交(Read Committed):保證一個事務只能讀取已經提交的數據。這個級別避免了臟讀,但仍可能發生不可重複讀(Non-repeatable Read),即在同一事務中,多次讀取同一數據可能得到不同的結果。
- 可重複讀(Repeatable Read):確保一個事務在執行期間多次讀取同一數據時,結果保持一致。這個級別避免了臟讀和不可重複讀,但仍可能出現幻讀(Phantom Read),即在同一事務中,多次查詢時發現新增了新的數據行。
- 可串行化(Serializable):最高的隔離級別,通過強制事務串行執行來避免臟讀、不可重複讀和幻讀。事務串行執行可能導致並發性能下降,因此在實際應用中需要慎重選擇。
MySQL 通過鎖機制來實現事務的隔離性。不同的隔離級別會使用不同的鎖策略來控制並發操作。 例如,可重複讀隔離級別使用了多版本並發控制(MVCC)機制,通過保存數據在某個時間點的快照來實現一致性讀取,避免了讀取過程中的鎖衝突。
開啟事務可以使用 START TRANSACTION 或 BEGIN 語句,提交事務使用 COMMIT 語句,回滾事務使用 ROLLBACK 語句。在事務中,可以使用 SET TRANSACTION 來設置隔離級別。
需要注意的是,事務的隔離級別和並發控制可能會對性能和並發性產生影響,因此在選擇隔離級別時需要根據具體應用的需求權衡。
MVCC 你說一下怎麼實現的,如何解決幻讀?
MVCC(Multi-Version Concurrency Control)是一種並發控制機制,用於解決數據庫中的並發訪問問題,包括解決幻讀的問題。下面是MVCC的基本實現原理和解決幻讀的方法:
版本號:每個數據行都有一個版本號,用於標識該數據行的版本。在每次數據更新時,會生成一個新的版本,並將新版本的數據寫入數據庫。
讀操作:在讀取數據時,事務會根據自己的啟動時間戳(Start Timestamp)和數據行的版本號來確定可見性。只有數據行的版本號早於事務的啟動時間戳時,才能讀取到該數據行。
寫操作:在寫入數據時,會生成一個新的版本,並將新版本的數據行寫入數據庫。寫操作不會對已有的數據行進行直接覆蓋,而是將舊版本的數據標記為無效。這樣舊版本的數據對於之前啟動的事務仍然可見,而新版本的數據對於之後啟動的事務可見。
通過MVCC,可以實現數據的多版本並發控制,避免了讀操作的阻塞和寫操作的衝突。對於解決幻讀的問題,MVCC採取了以下措施:
快照讀:讀操作只讀取早於事務啟動時間戳的數據版本,避免了讀取到其他事務正在修改的數據行。
版本鏈:每個數據行的多個版本形成了一個版本鏈,事務根據自己的啟動時間戳在版本鏈中選擇可見的數據版本。這樣,在讀操作期間,即使有其他事務對數據進行了修改,也不會影響當前事務的讀取結果。
通過MVCC的機制,可以有效解決幻讀的問題。 當一個事務啟動後,它只能看到在它啟動之前已經提交的數據版本,而無法看到其他事務尚未提交的數據。 這樣,即使其他事務在事務執行期間插入了新的數據行,當前事務也不會受到幻讀的影響。
如何設計索引,索引的實現有哪幾種方式,為什麼要用 B+ 樹?
索引設計:
- 選擇適當的列作為索引列:索引應該選擇在查詢中頻繁用作過濾條件的列,以提高查詢效率。
- 考慮索引的選擇性:選擇性指的是索引列中不同值的數量與總行數的比率,選擇性越高,索引的效果越好。
- 考慮多列索引:可以使用多列索引來滿足複合條件查詢的需求,但需要權衡索引的大小和性能。
- 考慮索引的大小和維護成本:索引會佔用額外的存儲空間,並在數據修改時增加維護成本,需要根據實際情況進行權衡。
索引的實現方式:
- B+樹索引:B+樹是最常見的索引結構,它以平衡樹的形式存儲索引數據,可以高效地支持範圍查詢和順序訪問。 B+樹索引適用於磁盤存儲,能夠減少磁盤的隨機I/O操作。
- 哈希索引:哈希索引使用哈希表存儲索引數據,適用於等值查詢,具有快速的查找速度。然而,哈希索引不支持範圍查詢和排序操作,並且對於數據的插入和刪除較為敏感。
- 全文索引:全文索引適用於對文本內容進行高效的全文搜索,如文章內容、博客等。它使用特定的算法和數據結構,如倒排索引等,以支持關鍵字的快速搜索。
B+樹的優勢:
- 支持範圍查詢:B+樹的葉子節點形成有序鍊錶,可以方便地進行範圍查詢,如區間查詢、排序等。
- 順序訪問效率高:B+樹的葉子節點有序,支持順序訪問,對於範圍查詢、分頁查詢等操作效率較高。
- 磁盤IO優化:B+樹的內部節點通常存儲關鍵字和指針,減少磁盤的隨機I/O操作,提高查詢性能。
綜合考慮索引的設計和實現方式時,B+樹索引被廣泛應用於關係型數據庫系統,因為它能夠支持高效的範圍查詢和順序訪問,適應了大部分的查詢場景
說一說你項目中的反範式的設計,為什麼要用反範式?
在我的項目中,我們採用了一定程度的反規範化設計。反規範化是指將關係型數據庫中的數據冗餘和重複存儲,以提高查詢性能和減少數據庫操作次數的技術。
以下是我們採用反規範化設計的原因和優勢:
提高查詢性能:通過將相關數據冗餘存儲在一個文檔或記錄中,可以減少聯接操作和復雜查詢,從而提高查詢性能。在查詢需要多個表或多個字段的情況下,反規範化可以避免多次查詢和聯接操作,加快數據檢索速度。
減少數據庫操作次數:反規範化可以減少與數據庫的交互次數,減輕數據庫的負載。通過將相關數據存儲在一個文檔或記錄中,可以一次性從數據庫中獲取所需數據,而不需要多次查詢。
簡化數據模型和查詢邏輯:反規範化可以簡化數據模型和查詢邏輯。通過將關聯數據冗餘存儲,可以消除複雜的關聯關係和查詢操作,使數據模型更加扁平化和直觀,簡化了開發和維護的工作。
支持高性能的讀取操作:如果應用程序中讀取操作頻繁且對數據一致性要求相對較低,反規範化可以在一定程度上提高讀取操作的性能。通過將多個關聯表的數據冗餘存儲在一個文檔或記錄中,可以避免聯接操作和復雜查詢,從而提高讀取性能。
然而,反規範化也存在一些潛在的問題和挑戰,包括數據冗餘、數據一致性維護和更新操作的複雜性。因此,在採用反規範化設計時,需要仔細權衡利弊,並確保數據的一致性和正確性。
說一說你在使用 MySQL 過程中遇到的坑?
在使用數據庫時,"hook"通常指的是數據庫提供的鉤子函數或回調函數,用於在特定事件發生時執行自定義操作。這些鉤子函數可以用於處理數據的插入、更新、刪除等操作。
在我的開發經驗中,我確實遇到過一些與數據庫鉤子相關的問題。以下是一些可能遇到的常見問題和解決方法:
- 鉤子函數執行順序問題:有時候需要在不同的鉤子函數中執行操作,並希望它們按特定順序執行。但是,不同數據庫或不同的ORM框架可能對鉤子函數的執行順序有不同的實現方式。解決這個問題的方法是仔細閱讀文檔或源代碼,確保了解鉤子函數的執行順序,並根據需要進行適當的調整。
- 鉤子函數觸發條件問題:有些數據庫鉤子函數可能有特定的觸發條件,例如在滿足某些條件時才執行。在配置和使用鉤子函數時,需要確保了解這些觸發條件,並根據業務需求進行正確的配置。否則,可能會導致鉤子函數無法觸發或不按預期執行。
- 鉤子函數性能問題:過於復雜或耗時的鉤子函數可能會對數據庫性能產生負面影響。在編寫鉤子函數時,需要謹慎考慮其執行時間和資源消耗。如果遇到性能問題,可以嘗試優化鉤子函數的邏輯或限制其執行頻率,以提高數據庫的響應性能。
- 鉤子函數異常處理問題:在鉤子函數中執行的自定義操作可能會出現異常。為了確保數據的一致性和完整性,需要適當處理鉤子函數中的異常。這包括錯誤處理、回滾事務或進行適當的日誌記錄。處理異常的方式取決於具體的應用程序和框架,需要根據情況進行調整和實施。
使用鉤子(hook)的優點和缺點如下:
優點:
- 模塊化和可複用性:使用鉤子可以將特定的操作和邏輯與主要代碼解耦,使代碼更具模塊化和可複用性。通過在適當的時機觸發鉤子函數,可以方便地插入自定義邏輯。
- 擴展性:鉤子提供了一種擴展現有功能的簡單方式。通過添加新的鉤子函數,可以在不修改主要代碼的情況下,為系統添加新的行為或功能。
- 靈活性:鉤子使系統更加靈活,可以根據需求定制特定的操作和邏輯。不同的鉤子函數可以根據具體情況進行不同的處理,從而滿足不同的需求。
缺點:
- 複雜性:過多或複雜的鉤子函數可能會增加代碼的複雜性和維護成本。在設計和實現鉤子時,需要仔細考慮它們的使用方式和限制,以避免過度使用或混亂的鉤子函數。
- 可能引入不確定性:過度使用鉤子函數可能會引入不確定性和難以追踪的行為。如果鉤子函數的執行順序和相互之間的影響不加控制,可能會導致系統行為的不一致或不可預測性。
- 性能影響:鉤子函數的執行時間和資源消耗可能會對系統性能產生負面影響。特別是在高頻率或大規模使用鉤子的情況下,需要仔細評估和測試性能,以確保不會成為性能瓶頸。
合理使用鉤子函數可以提高代碼的靈活性和可擴展性,但過度使用或不恰當使用可能會增加複雜性和引入不確定性。
8. 說一下 raft 的基本原理,有什麼作用?了解 zk 的 zab 協議嗎? paxos 之類的了解嗎?
TODO: 看一下鳳凰架構
Raft
Raft 是一種共識算法,用於在分佈式系統中實現一致性。它的設計目標是使分佈式系統的設計和實現更加可理解和可靠。 Raft 算法通過選舉一個領導者(Leader)來協調節點間的一致性操作,包括日誌複製、狀態機更新等。 Raft 算法具有以下特點:
- 強一致性:Raft 算法保證系統在任何時間點都具有一致的狀態,即使在節點故障或網絡分區的情況下也能保持一致性。
- 領導者選舉:Raft 中的節點通過互相通信來選舉一個領導者,領導者負責協調和處理客戶端的請求。
- 日誌複製:Raft 通過日誌複製機制來保證各個節點之間的數據一致性。領導者將接收到的客戶端請求追加到自己的日誌中,並將這些日誌複製給其他節點。
- 安全性:Raft 算法在保證一致性的同時,還提供了安全性的保證。它通過限制日誌的提交順序來避免數據損壞和衝突。
ZooKeeper(ZK)的 ZAB(ZooKeeper Atomic Broadcast)協議是 ZooKeeper 分佈式系統中用於實現一致性的協議。它與 Paxos 類似,但在一些細節上有所不同。 ZAB 協議包含兩個階段:崩潰恢復階段(ZAB Phase 1)和消息廣播階段(ZAB Phase 2)。
在 ZAB 的崩潰恢復階段,ZooKeeper 中的節點會選舉一個 Leader 來管理系統狀態,並從 Leader 獲取最新的數據。在消息廣播階段,Leader 將接收到的客戶端請求廣播給所有的節點,確保所有節點上的數據一致。
Paxos
Paxos 是另一種共識算法,用於實現分佈式系統中的一致性。它通過多輪投票和提案的方式來達成一致性。 Paxos 算法相對複雜,有多個變種,包括基本的 Paxos 算法和 Fast Paxos 算法等。
9. RPC 框架如何實現的,如果是你的話,你會怎麼設計 RPC 框架?
RPC(Remote Procedure Call)是一種通信協議和編程模型,用於實現分佈式系統中不同節點之間的遠程調用。實現一個 RPC 框架涉及多個方面的設計和實現,以下是一些常見的關鍵組件和步驟:
- 定義接口:RPC 框架需要支持定義接口和方法,這些接口和方法可以在不同的節點之間進行調用。通常使用接口描述語言(IDL)來定義接口,並根據定義生成對應的代碼。
- 序列化和反序列化:RPC 框架需要支持將數據在不同節點之間進行序列化和反序列化,以便在網絡上進行傳輸。常見的序列化協議包括 JSON、Protocol Buffers、Thrift 等。
- 傳輸協議:RPC 框架需要選擇一種傳輸協議來在網絡上進行數據傳輸。常見的選擇有基於 TCP 的協議如 gRPC、Apache Thrift,以及基於 HTTP 的協議如 JSON-RPC。
- 通信方式:RPC 框架可以支持不同的通信方式,包括同步調用、異步調用和回調機制等。這取決於具體的應用場景和需求。
- 服務註冊與發現:在分佈式系統中,RPC 框架需要提供服務註冊與發現的能力,以便客戶端能夠找到可用的服務。這可以通過使用服務註冊中心(如 ZooKeeper、Consul)或者使用分佈式配置中心(如 Etcd、Nacos)來實現。
- 負載均衡:在有多個服務提供者的情況下,RPC 框架可以支持負載均衡算法,將請求分發到不同的服務提供者上,以實現負載均衡和高可用性。
- 容錯和重試:RPC 框架應該具備容錯和重試機制,能夠處理網絡故障、服務不可用等異常情況,並儘可能地保證調用的可靠性。
- 監控和日誌:RPC 框架應該提供監控和日誌功能,可以記錄調用的各種指標和信息,方便故障排查和性能優化。
如果我設計一個 RPC 框架,我會考慮以下幾點:
- 簡單易用:提供簡潔的 API 和清晰的文檔,使開發者能夠方便地定義和調用遠程服務。
- 高性能:採用高效的序列化和傳輸協議,以及優化的網絡通信和線程模型,提供高性能的遠程調用能力。
- 可擴展性:支持水平擴展和集群部署,能夠處理大規模的並發請求,並具備良好的容錯和負載均衡能力。
- 可監控性:提供監控和日誌功能,記錄調用指標和異常信息,支持可視化監控和告警。
- 兼容性:支持多種編程語言和平台,使得不同技術棧的應用能夠方便地進行跨語言的遠程調用。
10. GORM 框架中事務和遷移的實現,Hooks 如何實現?
事務的實現: GORM 提供了 Begin、Commit 和 Rollback 方法來實現事務。你可以通過調用 Begin 方法開始一個事務,然後在事務中執行數據庫操作,最後通過調用 Commit 提交事務或者 Rollback 回滾事務。這樣可以確保一系列數據庫操作要么全部成功提交,要么全部回滾。
遷移的實現: GORM 提供了 AutoMigrate 方法來實現遷移。你可以在定義模型時使用 GORM 的結構標籤來指定數據庫表的名稱、字段名、數據類型等信息,然後調用 AutoMigrate 方法來自動創建或更新數據庫表結構,以便與模型定義保持同步。
Hooks 的實現: GORM 提供了一系列的 Hook 方法,可以在特定的操作或事件發生時執行自定義的邏輯。你可以在模型結構體中定義這些 Hook 方法,它們會在相應的操作或事件發生時被自動調用。例如,你可以定義 BeforeCreate、AfterSave、BeforeDelete 等方法來在對應的操作前後執行額外的處理邏輯。這樣可以在數據庫操作時插入自定義的邏輯,比如數據驗證、關聯操作等。
還有 ES、k8s、kafka 相關的,比如它們的組件都有哪些,如何實現的,流程是怎麼樣的?
ES(Elasticsearch):
組件:
- 節點(Node):運行在集群中的一個實例,可以是主節點或數據節點。
- 索引(Index):存儲和組織數據的邏輯容器,由一個或多個分片組成。
- 分片(Shard):數據的水平分割單元,每個分片是一個獨立的索引。
- 副本(Replica):分片的複製品,用於提高數據的冗餘和可用性。
- 集群(Cluster):由一個或多個節點組成的邏輯組,共同存儲和處理數據。
- 節點發現與協調(Node Discovery and Coordination):負責節點的發現和加入集群。
- 分佈式協調(Distributed Coordination):用於管理集群的狀態、領導選舉和分片分配等。
- 索引和搜索(Indexing and Searching):負責數據的索引和搜索操作。
- 分佈式存儲和檢索(Distributed Storage and Retrieval):將數據分佈在多個節點上進行存儲和檢索。
- 分佈式搜索和聚合(Distributed Search and Aggregation):將搜索和聚合操作分佈在多個節點上進行並行處理。
實現:
- 基於倒排索引(Inverted Index):通過構建倒排索引來加速搜索和過濾操作。
- 分佈式架構:利用分片和副本機制實現數據的水平擴展和容錯性。
- 基於Lucene庫:ES基於Lucene搜索引擎庫構建,提供了更高級別的API和功能。
流程:
- 啟動ES集群,節點加入集群,進行節點發現與協調。
- 創建索引,定義索引的映射和設置。
- 將文檔數據通過API或其他工具插入到索引中。
- 執行搜索和聚合操作,ES會將查詢請求分發給相應的分片進行處理。
- 返回搜索結果,可以根據需求進行排序、過濾和聚合操作。
Kubernetes(K8s):
組件:
- 控制平面組件:
- API Server:提供Kubernetes API接口。
- Scheduler:負責調度Pod到集群中的節點。
- Controller Manager:管理控制器,監控集群狀態並進行調整。
- etcd:分佈式鍵值存儲,存儲集群的狀態信息。
- 工作節點組件:
- Kubelet:與Master節點通信,管理和執行Pod。
- Kube-proxy:負責網絡代理和負載均衡。
- 容器運行時(Container Runtime):如Docker、Containerd等,負責管理容器的創建、運行和銷毀。
- 附加組件:
- Ingress Controller:處理集群內外部的HTTP和HTTPS路由。
- DNS:提供集群內部的DNS服務。
- Dashboard:提供Kubernetes集群的Web界面。
- Logging和Monitoring工具:用於日誌記錄和監控集群。
- 控制平面組件:
實現:
- 分佈式架構:Kubernetes通過分佈式設計實現高可用性和可擴展性。
- 聲明式配置:通過定義資源對象的規範(YAML或JSON),告訴Kubernetes期望的狀態,由控制平面組件進行狀態調整。
- 自愈能力:通過監控和控制器的機制,自動修復故障和維護集群的期望狀態。
流程:
- 配置Kubernetes集群,啟動控制平面組件和工作節點組件。
- 使用kubectl或其他方式創建資源對象的定義文件(如Deployment、Service等)。
- 控制平面組件接收到定義文件,驗證和解析後存儲在etcd中。
- 調度器將Pod調度到可用的節點上。
- Kubelet在節點上創建和管理Pod,包括容器的拉取、運行和監控。
- Kube-proxy負責為Pod提供網絡代理和負載均衡。
- 控制平面組件和控制器不斷監控集群狀態,根據定義文件的期望狀態進行調整和修復。
Kafka:
組件:
- Broker:Kafka的消息服務器,負責接收、存儲和傳遞消息。
- Topic:消息的分類和邏輯容器。
- Partition:Topic的分區,用於實現消息的水平擴展和負載均衡。
- Producer:生產者,負責發布消息到Topic。
- Consumer:消費者,負責訂閱Topic並消費消息。
- Consumer Group:消費者組,將多個消費者組織在一起,實現消息的並行消費。
- ZooKeeper:用於協調Kafka集群的分佈式服務。
實現:
- 分佈式架構:Kafka採用分佈式架構,允許消息以分區的方式存儲和傳遞。
- 持久化存儲:消息被持久化存儲在磁盤上,以保證消息的持久性和可靠性。
- 基於日誌的存儲模型:Kafka使用基於日誌的存儲模型,通過追加寫入和順序讀取的方式實現高性能和低延遲的消息處理。
- 副本機制:Kafka通過副本機制實現數據的冗餘和容錯性,確保消息的可用性。
- 分佈式提交消費位移:Kafka使用ZooKeeper或自身內置的位移管理器(offset manager)來管理消費者的位移,實現消息的順序消費和消費的容錯。
流程:
- 配置Kafka集群,啟動Broker和ZooKeeper。
- 創建Topic,指定分區和副本數。
- Producer將消息發送到指定的Topic。
- Consumer訂閱Topic,並從Broker中拉取消息。
- Consumer Group中的消費者協調消費位移,實現消息的並行消費。
- 消息持久化存儲在Broker的磁盤中,可供後續的消費者重新消費或進行消息回溯。
Zookeeper(ZK)的ZAB協議:
ZAB(Zookeeper Atomic Broadcast)協議是ZooKeeper內部使用的一種原子廣播協議,用於實現ZooKeeper的一致性。
ZAB協議的基本原理:
- 基於原子廣播:ZAB協議確保了消息在ZooKeeper集群中的原子廣播,即消息要么被所有節點接收,要么都不接收。
- 基於領導者選舉:ZAB協議通過領導者選舉機制選舉出一個節點作為Leader,負責處理客戶端請求和協調集群狀態。
- 基於多數派投票:ZAB協議要求Leader的寫操作得到多數派節點的確認,保證了數據的一致性。
- 基於持久化日誌:ZAB協議使用持久化日誌來記錄和復制數據變更操作,確保了數據的可靠性。
ZAB協議的流程:
- 集群初始化階段:
- 選舉Leader節點。
- Leader同步狀態到Follower節點。
- 正常運行階段:
- Leader接收客戶端寫操作並廣播給所有Follower節點。
- Follower節點接收消息並進行確認,然後通知Leader。
- Leader收到多數派節點的確認後,將消息應用到自身狀態機,並廣播給所有Follower節點。
- Follower節點將消息應用到自身狀態機並發送確認給Leader。
- 客戶端讀操作可以直接從Follower節點獲取數據。
- 集群初始化階段:
ZAB協議通過Leader的選舉和數據複製機制實現了ZooKeeper集群的高可用性和數據一致性。它提供了強一致性保證,適用於分佈式系統中的協調和共識問題。
算法
1. 如何快速判斷一個數是否是 2 的冪次方
要快速判斷一個數是否是2的冪次方,可以使用位運算的技巧。
一個數如果是2的冪次方,那麼它的二進製表示中只有一個1,其他位都是0。例如,2的0次方是1,2的1次方是10,2的2次方是100,以此類推。 基於這個特性,可以使用以下的位運算操作進行判斷:
- 對於大於0的數n,如果n是2的冪次方,那麼n & (n-1) 的結果應該為0。因為n的二進製表示中只有一個1,而n-1的二進製表示中除了最高位的1,其他位都是1,所以n與n-1進行與運算結果為0。
- 對於大於0的數n,如果n是2的冪次方,那麼n的二進製表示中只有一個1,其他位都是0。因此,n與n-1進行與運算的結果應該是0,同時n與n-1進行異或運算的結果應該是n本身。
func isPowerOfTwo(n int) bool {
return n > 0 && (n&(n-1)) == 0
}
這個函數會返回true,如果n大於0且是2的冪次方,否則返回false。 使用位運算的方式可以快速判斷一個數是否是2的冪次方,因為它只需要進行幾次位運算即可,時間複雜度為O(1)。
與位運算相比,n % 2可能稍微慢一些的原因主要是因為:
- 位運算是直接對二進制位進行操作,而
n % 2涉及到除法運算,除法運算的計算複雜度較高。除法運算通常比位運算需要更多的計算步驟和時間。 - 除法運算可能涉及到浮點數的處理,因為對於浮點數除法,通常需要進行更複雜的計算。這可能會導致除法運算相對位運算更加耗時。
然而,需要注意的是,這種性能差異在大多數情況下是微不足道的。
對於絕大多數應用場景而言,使用n % 2來判斷一個數是否是2的冪次方是完全可行且足夠高效的。
只有在特別注重性能的極端情況下,才可能需要考慮使用位運算來進一步優化。
在一般情況下,我們可以選擇更簡潔和易讀的方式來判斷2的冪次方。
2. 樹相關的
3. LRU
- https://go.dev/play/p/0opiFZD5Hkz
- container/list : https://pkg.go.dev/container/list
- 是 Go 語言標準庫中的一個包,用於實現雙向鏈表(Doubly Linked List)。鏈表是一種常見的數據結構,由一系列節點組成,每個節點包含數據和指向前一個節點和後一個節點的指針。container/list 提供了一個通用的、可雙向遍歷的鏈表實現,可以在運行時動態添加、刪除和修改節點。
- container/list 的性能通常比原生的切片(slice)或映射(map)要差,因此在大部分情況下,使用切片或映射可能更適合。但在某些特定場景下,使用鏈表可能更加方便和有效,例如需要頻繁地在列表的開頭或結尾進行插入和刪除操作的情況。
package main
import (
"container/list"
"fmt"
)
type LRUCache struct {
capacity int
cache map[int]*list.Element
list *list.List
}
type Entry struct {
key int
value int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (c *LRUCache) Get(key int) int {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
return elem.Value.(*Entry).value
}
return -1
}
func (c *LRUCache) Put(key, value int) {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
elem.Value.(*Entry).value = value
} else {
if len(c.cache) >= c.capacity {
// 移除最久未使用的元素
oldest := c.list.Back()
delete(c.cache, oldest.Value.(*Entry).key)
c.list.Remove(oldest)
}
// 添加新元素到列表頭部
newEntry := &Entry{key, value}
newElem := c.list.PushFront(newEntry)
c.cache[key] = newElem
}
}
func main() {
cache := Constructor(2)
cache.Put(1, 1)
cache.Put(2, 2)
fmt.Println(cache.Get(1)) // 输出 1
cache.Put(3, 3)
fmt.Println(cache.Get(2)) // 输出 -1
cache.Put(4, 4)
fmt.Println(cache.Get(1)) // 输出 -1
fmt.Println(cache.Get(3)) // 输出 3
fmt.Println(cache.Get(4)) // 输出 4
}
LRUCache 結構體包含一個容量字段、一個哈希表 cache 用於快速查找元素、一個雙向鍊錶 list 用於維護元素的訪問順序。
Get 方法通過查找哈希表獲取元素,並將其移動到鍊錶頭部,表示該元素是最近訪問的。如果元素不存在,則返回 -1。
Put 方法先檢查哈希表中是否存在元素,如果存在則更新元素值並將其移動到鍊錶頭部;如果不存在,則先檢查緩存是否已滿,如果滿了則移除最久未使用的元素,再添加新元素到鍊錶頭部。
該實現利用了雙鍊錶的 O(1) 插入和刪除操作,並通過哈希表實現了 O(1) 的查找操作,保證了較高的性能。