從事業單位跳去大廠

程序员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.DeepEqualhttps://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 時,常見的模式有兩種:

  1. 互斥模式(Exclusive Mode):一次只有一個 Goroutine 能夠獲得 Mutex 的鎖,其他 Goroutine 需要等待鎖的釋放。
  2. 讀寫模式(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 間通信的重要機制之一。它具有以下特性:

  1. 通信機制:通道提供了一種同步的通信機制,允許 Goroutine 之間安全地發送和接收數據。通過通道,發送方和接收方可以進行阻塞等待,直到另一方准備好進行通信。
  2. 有類型:通道是具有特定類型的數據傳輸通道,只能傳輸該類型的數據。例如,可以創建整數類型的通道、字符串類型的通道等。
  3. 先進先出:通道遵循先進先出(FIFO)的原則,保證發送的數據和接收的數據按照發送的順序進行傳遞。
  4. 阻塞和非阻塞:通道提供了阻塞和非阻塞的操作。當通道為空時,接收操作會阻塞,直到有數據可接收;當通道已滿時,發送操作會阻塞,直到有空間可用。非阻塞操作則會立即返回,並通過返回值指示操作是否成功。

通道的實現原理是基於通道數據結構和 Goroutine 調度器的協同工作:

  1. 通道數據結構:通道的底層數據結構由一個隊列(queue)和一些元數據組成。隊列用於存儲發送的元素,元數據則包含有關通道狀態和操作的信息。
  2. 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 的實現原理和相關問題的回答:

  1. 線程安全性:
    • 在 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 類型
  2. 擴容:
    • map 中的元素數量超過一定閾值時,Go 語言會自動對 map 進行擴容,以提高性能。
    • 擴容過程中,會創建一個新的更大的哈希表,並將原有的鍵值對重新哈希到新的桶中。
    • 擴容過程是在後台進行的,不會阻塞其他 Goroutine 對 map 的訪問。
    • 擴容過程中,會暫停對 map 的寫入操作,但讀取操作仍然可以繼續進行。

需要注意的是,由於 map 的擴容會重新哈希所有的鍵值對,因此擴容可能導致一些性能開銷。在實際應用中,如果對性能有更高的要求,可以在初始化時指定初始容量,以減少擴容次數。

8. 接口如何實現的?接口可以做什麼?

在 Go 語言中,接口(Interface)是一種定義行為的類型。接口定義了一組方法的集合,但不包含具體的實現。通過實現接口的方法,類型可以滿足接口的要求,並被視為該接口的實現類型。

接口的實現是隱式的,即類型只需要實現了接口中定義的所有方法,就被視為實現了該接口,無需顯式聲明。這種方式稱為結構化類型。

接口的定義格式如下:

type 接口名 interface {
    方法1()
    方法2()
    // ...
}

通過接口,我們可以實現以下功能:

  1. 多態性(Polymorphism):接口使得我們可以通過統一的方式操作不同類型的對象。我們可以將實現了同一接口的不同類型的對象存儲在同一個容器中,以及使用相同的方法對它們進行操作,從而實現多態性。
  2. 代碼復用:通過接口,我們可以定義一組共同的行為,並讓不同的類型去實現這些行為。這樣可以提高代碼的可重用性,避免了重複編寫相似的代碼。
  3. 解耦和抽象:接口將抽象與具體實現分離,通過接口定義對外提供的方法,而無需關心具體的實現細節。這樣可以降低代碼的耦合度,提高代碼的可維護性和靈活性。
  4. 接口組合:可以通過接口嵌套和接口組合的方式,定義更複雜的接口,以滿足更多的行為要求。
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 接口,並讓 DogCat 類型分別實現了 Speak 方法。然後,我們創建了一個接口類型的變量 animal,分別將其賦值為 DogCat 的實例。通過調用 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")
    }
}

在上面的示例中,我們創建了兩個通道 ch1ch2,並在兩個 Goroutine 中分別向這兩個通道發送數據。 然後,我們使用 select 語句在這兩個通道上進行非阻塞的接收操作。其中,通過 <-ch1<-ch2 分別接收 ch1ch2 的數據,並打印相應的消息。 此外,我們還使用 time.After 創建了一個定時器,當超過指定的時間後,會觸發一個超時的分支。

通過 select 語句,我們可以在多個通道之間進行非阻塞的操作,根據實際情況選擇執行對應的代碼分支。這種機制使得我們可以實現靈活的並發通信和同步邏輯。

10. 說說對 GMP 的理解以及什麼是 Work stealing 和 Hand off 機制

GMP 是 Go 語言調度器的關鍵組成部分,用於管理 Goroutine 的創建、調度和執行。 GMP 代表以下概念:

  1. G(Goroutine):Goroutine 是 Go 語言中並發執行的基本單位。每個 Goroutine 都有自己的調用棧和上下文信息。 Goroutine 的創建和銷毀由調度器(Scheduler)管理。
  2. M(Machine):Machine 代表著操作系統線程(OS Thread),它負責執行 Goroutine。一個 M 可以關聯多個 Goroutine,但同一時刻只能執行一個 Goroutine。
  3. P(Processor):Processor 用於調度 Goroutine 到 Machine 執行。它是調度器與 M 之間的中介。每個 M 關聯一個 P,而 P 可以關聯多個 M。

GMP 模型的工作原理如下:

  1. 初始時,調度器創建了一些 M,並與一些 P 綁定。這些 M 等待接收 Goroutine 的調度。
  2. 當創建一個新的 Goroutine 時,調度器會選擇一個空閒的 M,並將 Goroutine 綁定到該 M 上。
  3. 當 M 執行 Goroutine 時,如果遇到阻塞操作(如 I/O),M 會與關聯的 P 斷開連接,讓 P 繼續調度其他 Goroutine。
  4. 斷開連接的 M 稱為閒置 M。閒置 M 會等待一段時間,如果在這段時間內沒有可執行的 Goroutine,它會將自己歸還給調度器,然後繼續等待新的任務。
  5. 調度器會根據需要創建新的 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 編譯器在編譯時會對代碼進行逃逸分析,以確定變量的生命週期和是否逃逸到堆上進行分配。

逃逸分析是一種靜態分析技術,用於確定變量在函數內部是否逃逸到函數外部。如果一個變量逃逸到函數外部,意味著它的生命週期超出了函數的範圍,需要在堆上分配內存。相反,如果變量不逃逸,它可以在棧上分配內存,由編譯器自動管理。

逃逸分析對於優化內存分配和減少垃圾回收的影響非常大。以下是逃逸分析的一些關鍵點:

  1. 棧(Stack)分配:當一個變量的生命週期僅限於函數內部且沒有逃逸時,它可以在棧(Stack)上分配內存。棧(Stack)上的內存分配和釋放速度快,適合短暫的對象。
  2. 堆(Heap)分配:當一個變量逃逸到函數外部時,它需要在堆上分配內存。堆上的內存分配和釋放相對較慢,但對於長時間存在或需要跨函數訪問的對像是必需的。

逃逸分析的好處是可以減少垃圾回收的壓力和內存分配的開銷。當變量逃逸到堆(Heap)上時,它們的內存由垃圾回收器管理,當它們不再被引用時會被自動回收。而對於棧(Stack)上分配的對象,它們的生命週期與函數的調用關係直接相關,不需要垃圾回收的介入。

逃逸分析在編譯階段進行,因此不會對運行時性能產生額外的開銷。 編譯器會根據逃逸分析的結果,優化變量的分配方式,盡量減少內存分配和垃圾回收的開銷,提高程序的執行效率。

總結起來,Go 語言的內存分配是自動管理的,並通過逃逸分析確定變量的生命週期和內存分配位置。逃逸分析有助於減少垃圾回收的壓力和內存分配的開銷,提高程序的性能。

13. 說一下進程、線程、協程的區別,這個要整花活

concurrency/01_Introduction/02_Process_vs_Threads.md

進程(Process)

進程是操作系統中的一個執行實體,它擁有獨立的內存空間和系統資源。每個進程都是獨立運行的,相互之間不會干擾。進程之間通過進程間通信(Inter-Process Communication, IPC)機制進行數據交換和協作。進程是操作系統進行資源分配和調度的基本單位。

線程(Thread)

線程是進程內的一個執行單元,它與其他線程共享同一個進程的內存空間和系統資源。多個線程可以在同一進程內並發執行,共享進程的上下文環境。線程之間通過共享內存進行數據交換,但需要注意同步和互斥的問題。線程的創建、銷毀和調度由操作系統負責。

協程(Coroutine)

協程是一種輕量級的線程,也稱為用戶級線程。協程是由程序員在應用程序中自行管理的,不依賴於操作系統的線程調度機制。 協程擁有自己的棧空間(Stack),可以在一個或多個線程上並發執行。協程之間通過協作式調度進行切換,需要程序員自行控制協程的切換點。協程可以提高程序的並發性和性能,適用於處理大量的 I/O 操作或密集的計算任務。

區別

  1. 調度機制:進程和線程由操作系統的調度器進行管理和調度,調度器決定哪個進程或線程可以執行。而協程由程序員自行控制調度,程序員決定在什麼時候切換協程的執行。
  2. 資源開銷:進程是獨立的執行實體,每個進程都有自己的內存空間和系統資源,因此進程間的切換需要較大的資源開銷。線程共享進程的資源,線程間的切換較為快速,但仍然需要較大的開銷。協程是輕量級的,切換開銷非常小,因為協程的調度是由程序員自行控制。
  3. 編程模型:進程和線程屬於內核級調度,由操作系統提供的系統調用接口進行管理。協程屬於用戶級調度,由應用程序自行控制。協程的編程模型更加靈活,可以根據具體的應用場景進行優化和控制。

總結起來,進程是操作系統進行資源分配和調度的基本單位,線程是進程內的執行單元,而協程是由程序員自行管理和調度的輕量級線程。它們在調度機制、資源開銷和編程模型上存在明顯的區別,適用於不同的應用場景

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. 列舉一些你在項目中做的比較有挑戰的事情或者業務,比如具體的技術細節體現,如何攻堅某個難點,怎麼做技術選型的(幾乎每個組件都要問一下,問什麼要用這個組件,而不是其他的)?

  1. 性能優化:在項目中,性能問題通常是一個重要的挑戰。這可能涉及到優化關鍵代碼段的執行速度、減少資源消耗或提高系統的可擴展性。在解決性能問題時,可以通過使用更高效的數據結構、並發編程、緩存技術或者分佈式系統架構等方法來改善性能。

  2. 大規模數據處理:處理大規模數據集時,可能需要考慮如何優化數據讀取、存儲和處理。這可能涉及到選擇合適的數據存儲引擎、使用分佈式計算框架、並行化處理等。技術選型時,需要評估各種方案的性能、可擴展性、易用性和維護成本等因素。

  3. 安全性和隱私保護:保護用戶數據的安全和隱私是一個重要的挑戰。在設計和實現系統時,需要考慮身份驗證、數據加密、訪問控制等安全機制,並遵守隱私保護法規和最佳實踐。

  4. 架構設計和技術選型:在選擇組件和技術時,需要綜合考慮多個因素,包括功能需求、性能要求、團隊熟悉度、社區支持和未來擴展性等。常見的技術選型包括數據庫選擇、消息隊列系統、緩存方案、框架選型等。在做技術選型時,可以評估各種方案的優缺點,並結合實際需求和團隊條件做出決策。

  5. 複雜業務邏輯處理:某些業務場景可能涉及復雜的業務邏輯,例如交易系統、工作流引擎等。在處理複雜業務邏輯時,可以使用規則引擎、狀態機、領域驅動設計(DDD)等技術手段來管理和執行複雜的業務流程。

攻克這些挑戰的一般方法包括:深入分析問題、進行合適的技術調研和評估、利用現有的工具和框架、與團隊成員合作、進行測試和性能優化、不斷迭代和改進。同時,團隊成員之間的良好溝通和合作也是成功解決難題的關鍵

2. 你們項目的架構是什麼樣的,可以說一下數據流向和請求流向嗎?

3. 你對 Jaeger 和 OpenTracing 怎麼理解的?那你怎麼理解 TraceID 和 SpanID 的定義的? Jaeger 的實現原理是怎麼樣的?

4. Prometheus 是怎麼做監控告警的?有哪些組件,實現流程是怎麼樣的?

Prometheus是一套開源的監控和警報系統,它通過收集時間序列數據並提供靈活的查詢語言來實現監控功能。 Prometheus的監控告警通常涉及以下組件和流程:

  1. Exporters:Prometheus通過Exporter組件來收集應用程序和系統的指標數據。 Exporter是一個獨立的進程或庫,它可以將應用程序或系統的指標暴露給Prometheus。常見的Exporter包括Node Exporter(用於收集主機級別的指標)、Blackbox Exporter(用於網絡探活和監控)、MySQL Exporter(用於MySQL數據庫監控)等。

  2. Prometheus Server:Prometheus Server負責從Exporter或其他數據源中獲取指標數據,並存儲在本地的時間序列數據庫中。它定期地拉取指標數據,可以進行數據的聚合和存儲,並提供查詢和警報功能。 Prometheus Server還負責自動發現Exporter,並維護與Exporter的連接。

  3. 數據存儲:Prometheus使用本地的時間序列數據庫存儲指標數據。默認情況下,Prometheus使用一種稱為TSDB(Time Series Database)的格式來存儲數據。 TSDB使用分塊存儲策略,可以有效地壓縮和存儲大量的時間序列數據。

  4. 查詢和可視化:Prometheus提供了PromQL查詢語言,可以用於從存儲的時間序列數據中提取和分析指標。用戶可以使用PromQL查詢數據並生成圖表或儀表板來進行可視化。

  5. 告警規則: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的設計目標包括:

  1. 一致性:etcd保證數據的一致性,即每個節點的數據副本都是一致的。
  2. 可靠性:etcd通過複製機制和選主算法來確保數據的可靠性。即使其中一個節點出現故障,系統仍然能夠繼續正常運行。
  3. 高可用性:etcd使用Raft算法實現領導者選舉和故障轉移,確保系統在節點故障時能夠繼續提供服務。

為什麼選擇etcd而不是Redis?

etcd和Redis都是鍵值存儲系統,但它們在設計目標和功能方面有一些區別。

  1. 一致性模型:etcd使用Raft算法實現強一致性,而Redis默認使用主從復制實現弱一致性。對於需要強一致性的場景,如分佈式系統的配置管理、服務發現等,etcd更適合。

  2. 高可用性:etcd通過Raft算法提供了高可用性機制,支持故障轉移和自動重新選舉。 Redis需要手動配置主從復制和哨兵來實現高可用性。

  3. 功能重點:Redis是一個功能豐富的數據結構服務器,支持各種數據類型和豐富的操作。它在緩存、隊列、發布訂閱等方面表現出色。而etcd的設計更專注於分佈式一致性和服務發現,提供了強大的分佈式存儲和同步功能

綜上所述,選擇etcd還是Redis取決於具體的需求。如果需要構建分佈式系統、服務發現或強一致性的數據存儲,etcd是更合適的選擇。而如果需要一個多功能的數據結構服務器,同時對一致性要求較低,Redis可能更適合。

Redis哨兵模式

Redis哨兵模式是一種用於提高Redis高可用性的解決方案。它通過引入哨兵節點來監控和管理Redis主節點和從節點,實現自動故障檢測和故障轉移。

在Redis哨兵模式中,有以下幾個角色:

  1. 哨兵節點(Sentinel):哨兵節點是一個獨立的進程,負責監控Redis主節點和從節點的狀態。它會定期向節點發送PING命令來檢測節點的健康狀態,並通過SENTINEL is-master-down-by-addr命令來判斷主節點是否宕機。如果哨兵節點檢測到主節點宕機,它會發起一次故障轉移操作。

  2. Redis主節點(Master):Redis主節點是提供讀寫服務的節點。在哨兵模式中,主節點的狀態由哨兵節點進行監控,並在主節點宕機時選擇一個從節點升級為新的主節點。

  3. Redis從節點(Slave):Redis從節點是主節點的複製品,用於提供讀取服務和實現數據冗餘。從節點會復制主節點的數據,並在主節點宕機時接替成為新的主節點。

哨兵模式的工作流程如下:

  1. 哨兵節點啟動並配置監控主節點和從節點的信息。
  2. 哨兵節點定期向主節點發送PING命令檢測其健康狀態。
  3. 如果哨兵節點檢測到主節點宕機,它會與其他哨兵節點進行協商,選舉出一個哨兵節點來執行故障轉移操作。
  4. 選舉出的哨兵節點會向其他哨兵節點發送通知,然後進行故障轉移操作。
  5. 故障轉移過程包括選擇一個從節點作為新的主節點,並將其他從節點切換為新的主節點的從節點。
  6. 客戶端可以通過與哨兵節點交互來獲取新的主節點的信息,以便繼續進行讀寫操作。

通過哨兵模式,Redis可以在主節點宕機時自動進行故障轉移,保證服務的可用性。它提供了一種簡單而有效的方式來實現Redis的高可用性,並且可以在運行時動態地調整主節點和從節點的配置。

6. Redis 的數據結構以及源碼深究,為何高性能和快速?數據一致性方案是怎麼做的?如何做持久化? AOF 重寫機制怎麼做的?過期策略是怎麼樣的?主從同步的流程是啥樣的,什麼情況下會觸發全量和增量同步?如何解決?如何利用 Redis 的數據結構設計一個符合業務需求的數據模型?哨兵機制介紹一下? I/O 模型是啥樣的? Redis 是單線程還是多線程?如何解決大 Key、冷 Key、熱 Key 的問題? etc.

Redis的數據結構:

Redis 面試問題- Redis有哪些數據結構?

  1. 字符串(String)
  2. 哈希表(Hash)
  3. 列表(List)
  4. 集合(Set)
  5. 有序集合(Sorted Set)等。

如果 你是Redis中高級用戶,還需要加上下面幾種數據結構

  1. HyperLogLog
  2. Geo
  3. Pub/Sub

如果你說還玩過Redis Module,像

  1. BloomFilter
  2. RedisSearch
  3. Redis-ML 面試官得眼睛就開始發亮了。

Redis的高性能和快速主要體現在以下幾個方面:

  1. 內存存儲:Redis將數據存儲在內存中,讀寫操作都是在內存中完成,相比於磁盤I/O操作,速度更快。

  2. 單線程模型:Redis採用單線程模型,通過事件驅動的方式處理客戶端請求。這樣可以避免線程切換和鎖競爭的開銷,提高了性能。

  3. 高效的網絡模型:Redis使用自己開發的網絡模型,採用非阻塞I/O和事件通知機制,充分利用操作系統的多路復用特性,提高網絡通信效率。

  4. 精細的數據結構設計: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提供了兩種過期策略 定時刪除和惰性刪除。

  1. 定時刪除:Redis會使用一個定時器來檢查設置了過期時間的鍵,當鍵過期時,會立即刪除。
  2. 惰性刪除:Redis在訪問一個鍵時,會先檢查該鍵是否過期,如果過期則刪除。這樣可以避免在定時刪除過程中的大量鍵刪除操作。

主從同步流程:

Redis的主從復制分為全量同步和增量同步兩個階段。

  1. 全量同步(full synchronization) SYNC:當從節點連接到主節點時,它會發送SYNC命令請求全量同步。主節點執行BGSAVE命令生成RDB文件,並將RDB文件發送給從節點。從節點加載RDB文件,將主節點的數據完全複製到自己的數據庫中。

  2. 增量同步(incremental synchronization)PSYNC:全量同步完成後,主節點會將後續的寫操作以命令的形式發送給從節點,從節點執行相同的寫操作,實現主從數據的同步。

Redis 2.8以前採用的複製都為全量複製。 Redis在2.8及以上版本使用PSYNC命令完成主從數據同步,PSYNC同步過程分為全量複製和部分複制,完善了SYNC存在的缺陷。

Redis主从同步原理、及SYNC和PSYNC同步区别

什麼情況下會觸發全量和增量同步

在 Redis 主從復制中,會出現以下情況觸發全量同步和增量同步:

  1. 全量同步(Full synchronization):

    • 主節點(Master)啟動或重新啟動時,會觸發全量同步。
    • 新添加的從節點(Slave)首次連接到主節點時,會觸發全量同步。
    • 從節點斷線重連後,如果復制偏移量(replication offset)無效,會觸發全量同步。
  2. 增量同步(Incremental synchronization):

    • 從節點與主節點成功建立連接後,會進行增量同步以保持數據一致。
    • 當主節點接收到新的寫命令時,會將寫命令的複制操作發送給所有從節點,從節點會接收並執行這些複製操作來進行增量同步。
    • 增量同步會持續進行,直到從節點與主節點的數據完全一致。

需要注意的是,在 Redis 的複製過程中,全量同步只在特定情況下觸發,而增量同步是持續進行的。 全量同步會將主節點的數據完整復製到從節點,而增量同步只傳輸從斷開連接後的數據更新部分,以減少網絡傳輸和提高同步效率。

此外,Redis 還支持部分同步(Partial Resynchronization)的方式來進行快速恢復和增量同步。部分同步是在全量同步的基礎上,通過傳輸複製偏移量和主節點的運行 ID 進行增量同步,以減少複製的數據量和時間。 全量同步和增量同步是在 Redis 主從復制中的不同階段和情況下觸發的機制,用於確保主節點和從節點之間的數據一致性和持續同步。

如何利用 Redis 的數據結構設計一個符合業務需求的數據模型?

1. Cache : string
2. Session : string Hash

在使用 Redis 實現共享 session 時,可以使用以下數據結構:

  1. 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 數據
    }
    
  2. 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

  1. 散列(Hash)數據結構:你可以使用Redis的Hash數據結構來存儲city id和對應的城市信息。 (Hash key=cityid2city, field= $city.CityId, value=$value)
  2. 有序集合(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
  1. 每篇文章使用 Hash 存儲, 例如每篇文章有3個屬性 title、timestamp、content
  2. 向用户文章列表添加文章 user {id} articles 作為用户文章列表的鍵
  3. 分頁 取用户文章列表 例如下面偽代碼 取用户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
  1. 用戶喜好的tag, 可以找出共同喜好
  2. 用戶和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. 社交網路
  1. 讚, 粉絲, 共投好友/喜愛, 推送, 下拉刷新
  2. 由於社交網站的訪問量通常比較大, 傳統的關聯數據不太適合

Redis 可以使用以下數據結構來實現這些功能:

  1. 字符串(String):可以用來存儲點贊數量、粉絲數量等簡單的計數器數據。
  2. 集合(Set):可以用來存儲用戶的粉絲列表、好友列表等。通過集合的交、並、差等操作,可以方便地進行共同喜歡的用戶、共同好友的計算。
  3. 有序集合(Sorted Set):可以用來存儲用戶的點贊記錄,成員為被點讚的對象,分數為點讚的時間戳,可以通過分數範圍查詢、按分數排序等操作來實現推送和下拉刷新功能。
  4. 列表(List):可以用來存儲推送消息的隊列,新的消息插入列表的頭部,用戶拉取消息時從列表的尾部取出。
12. 紀錄總數 bitmaps, HyperLogLog
  • 如果不需要個別內容, 且接受誤差 使用 HyperLogLog. 參考4.計數器
  • bitmaps. 每個獨立用戶是否訪問過網站存放到bitmaps中, 將訪問的用戶記做1; 沒有的記做0, 用偏移量作為用戶的id

在 Redis 中,可以使用位圖(Bitmap)來實現計算總數的功能。 Bitmap是一種緊湊的數據結構,用於存儲大量的二進制位。在計算總數的場景中,我們可以使用Bitmap來表示某個特定事件的發生情況,例如用戶簽到、用戶訪問等。

以下是使用Bitmap計算總數的基本步驟:

  1. 選擇一個適當的鍵名,用於存儲Bitmap數據。
  2. 初始化Bitmap,可以使用SETBIT命令將Bitmap的初始狀態設置為0,例如:SETBIT key offset 0
  3. 根據實際情況,使用SETBIT命令或BITOP命令對Bitmap進行更新。對於每個發生了特定事件的用戶或對象,可以使用SETBIT命令將對應位置為1,例如:SETBIT key offset 1
  4. 使用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 支持多種隔離級別來控制事務的並發操作:

  1. 讀未提交(Read Uncommitted):最低的隔離級別,允許一個事務讀取另一個事務尚未提交的數據。此級別可能導致臟讀(Dirty Read),即讀取到未提交的數據。
  2. 讀已提交(Read Committed):保證一個事務只能讀取已經提交的數據。這個級別避免了臟讀,但仍可能發生不可重複讀(Non-repeatable Read),即在同一事務中,多次讀取同一數據可能得到不同的結果。
  3. 可重複讀(Repeatable Read):確保一個事務在執行期間多次讀取同一數據時,結果保持一致。這個級別避免了臟讀和不可重複讀,但仍可能出現幻讀(Phantom Read),即在同一事務中,多次查詢時發現新增了新的數據行。
  4. 可串行化(Serializable):最高的隔離級別,通過強制事務串行執行來避免臟讀、不可重複讀和幻讀。事務串行執行可能導致並發性能下降,因此在實際應用中需要慎重選擇。

MySQL 通過鎖機制來實現事務的隔離性。不同的隔離級別會使用不同的鎖策略來控制並發操作。 例如,可重複讀隔離級別使用了多版本並發控制(MVCC)機制,通過保存數據在某個時間點的快照來實現一致性讀取,避免了讀取過程中的鎖衝突。

開啟事務可以使用 START TRANSACTIONBEGIN 語句,提交事務使用 COMMIT 語句,回滾事務使用 ROLLBACK 語句。在事務中,可以使用 SET TRANSACTION 來設置隔離級別。

需要注意的是,事務的隔離級別和並發控制可能會對性能和並發性產生影響,因此在選擇隔離級別時需要根據具體應用的需求權衡。

MVCC 你說一下怎麼實現的,如何解決幻讀?

MVCC(Multi-Version Concurrency Control)是一種並發控制機制,用於解決數據庫中的並發訪問問題,包括解決幻讀的問題。下面是MVCC的基本實現原理和解決幻讀的方法:

  1. 版本號:每個數據行都有一個版本號,用於標識該數據行的版本。在每次數據更新時,會生成一個新的版本,並將新版本的數據寫入數據庫。

  2. 讀操作:在讀取數據時,事務會根據自己的啟動時間戳(Start Timestamp)和數據行的版本號來確定可見性。只有數據行的版本號早於事務的啟動時間戳時,才能讀取到該數據行。

  3. 寫操作:在寫入數據時,會生成一個新的版本,並將新版本的數據行寫入數據庫。寫操作不會對已有的數據行進行直接覆蓋,而是將舊版本的數據標記為無效。這樣舊版本的數據對於之前啟動的事務仍然可見,而新版本的數據對於之後啟動的事務可見。

通過MVCC,可以實現數據的多版本並發控制,避免了讀操作的阻塞和寫操作的衝突。對於解決幻讀的問題,MVCC採取了以下措施:

  1. 快照讀:讀操作只讀取早於事務啟動時間戳的數據版本,避免了讀取到其他事務正在修改的數據行。

  2. 版本鏈:每個數據行的多個版本形成了一個版本鏈,事務根據自己的啟動時間戳在版本鏈中選擇可見的數據版本。這樣,在讀操作期間,即使有其他事務對數據進行了修改,也不會影響當前事務的讀取結果。

通過MVCC的機制,可以有效解決幻讀的問題。 當一個事務啟動後,它只能看到在它啟動之前已經提交的數據版本,而無法看到其他事務尚未提交的數據。 這樣,即使其他事務在事務執行期間插入了新的數據行,當前事務也不會受到幻讀的影響。

如何設計索引,索引的實現有哪幾種方式,為什麼要用 B+ 樹?

  1. 索引設計:

    • 選擇適當的列作為索引列:索引應該選擇在查詢中頻繁用作過濾條件的列,以提高查詢效率。
    • 考慮索引的選擇性:選擇性指的是索引列中不同值的數量與總行數的比率,選擇性越高,索引的效果越好。
    • 考慮多列索引:可以使用多列索引來滿足複合條件查詢的需求,但需要權衡索引的大小和性能。
    • 考慮索引的大小和維護成本:索引會佔用額外的存儲空間,並在數據修改時增加維護成本,需要根據實際情況進行權衡。
  2. 索引的實現方式:

    • B+樹索引:B+樹是最常見的索引結構,它以平衡樹的形式存儲索引數據,可以高效地支持範圍查詢和順序訪問。 B+樹索引適用於磁盤存儲,能夠減少磁盤的隨機I/O操作。
    • 哈希索引:哈希索引使用哈希表存儲索引數據,適用於等值查詢,具有快速的查找速度。然而,哈希索引不支持範圍查詢和排序操作,並且對於數據的插入和刪除較為敏感。
    • 全文索引:全文索引適用於對文本內容進行高效的全文搜索,如文章內容、博客等。它使用特定的算法和數據結構,如倒排索引等,以支持關鍵字的快速搜索。
  3. B+樹的優勢:

    • 支持範圍查詢:B+樹的葉子節點形成有序鍊錶,可以方便地進行範圍查詢,如區間查詢、排序等。
    • 順序訪問效率高:B+樹的葉子節點有序,支持順序訪問,對於範圍查詢、分頁查詢等操作效率較高。
    • 磁盤IO優化:B+樹的內部節點通常存儲關鍵字和指針,減少磁盤的隨機I/O操作,提高查詢性能。

綜合考慮索引的設計和實現方式時,B+樹索引被廣泛應用於關係型數據庫系統,因為它能夠支持高效的範圍查詢和順序訪問,適應了大部分的查詢場景

說一說你項目中的反範式的設計,為什麼要用反範式?

在我的項目中,我們採用了一定程度的反規範化設計。反規範化是指將關係型數據庫中的數據冗餘和重複存儲,以提高查詢性能和減少數據庫操作次數的技術。

以下是我們採用反規範化設計的原因和優勢:

  1. 提高查詢性能:通過將相關數據冗餘存儲在一個文檔或記錄中,可以減少聯接操作和復雜查詢,從而提高查詢性能。在查詢需要多個表或多個字段的情況下,反規範化可以避免多次查詢和聯接操作,加快數據檢索速度。

  2. 減少數據庫操作次數:反規範化可以減少與數據庫的交互次數,減輕數據庫的負載。通過將相關數據存儲在一個文檔或記錄中,可以一次性從數據庫中獲取所需數據,而不需要多次查詢。

  3. 簡化數據模型和查詢邏輯:反規範化可以簡化數據模型和查詢邏輯。通過將關聯數據冗餘存儲,可以消除複雜的關聯關係和查詢操作,使數據模型更加扁平化和直觀,簡化了開發和維護的工作。

  4. 支持高性能的讀取操作:如果應用程序中讀取操作頻繁且對數據一致性要求相對較低,反規範化可以在一定程度上提高讀取操作的性能。通過將多個關聯表的數據冗餘存儲在一個文檔或記錄中,可以避免聯接操作和復雜查詢,從而提高讀取性能。

然而,反規範化也存在一些潛在的問題和挑戰,包括數據冗餘、數據一致性維護和更新操作的複雜性。因此,在採用反規範化設計時,需要仔細權衡利弊,並確保數據的一致性和正確性。

說一說你在使用 MySQL 過程中遇到的坑?

在使用數據庫時,"hook"通常指的是數據庫提供的鉤子函數或回調函數,用於在特定事件發生時執行自定義操作。這些鉤子函數可以用於處理數據的插入、更新、刪除等操作。

在我的開發經驗中,我確實遇到過一些與數據庫鉤子相關的問題。以下是一些可能遇到的常見問題和解決方法:

  1. 鉤子函數執行順序問題:有時候需要在不同的鉤子函數中執行操作,並希望它們按特定順序執行。但是,不同數據庫或不同的ORM框架可能對鉤子函數的執行順序有不同的實現方式。解決這個問題的方法是仔細閱讀文檔或源代碼,確保了解鉤子函數的執行順序,並根據需要進行適當的調整。
  2. 鉤子函數觸發條件問題:有些數據庫鉤子函數可能有特定的觸發條件,例如在滿足某些條件時才執行。在配置和使用鉤子函數時,需要確保了解這些觸發條件,並根據業務需求進行正確的配置。否則,可能會導致鉤子函數無法觸發或不按預期執行。
  3. 鉤子函數性能問題:過於復雜或耗時的鉤子函數可能會對數據庫性能產生負面影響。在編寫鉤子函數時,需要謹慎考慮其執行時間和資源消耗。如果遇到性能問題,可以嘗試優化鉤子函數的邏輯或限制其執行頻率,以提高數據庫的響應性能。
  4. 鉤子函數異常處理問題:在鉤子函數中執行的自定義操作可能會出現異常。為了確保數據的一致性和完整性,需要適當處理鉤子函數中的異常。這包括錯誤處理、回滾事務或進行適當的日誌記錄。處理異常的方式取決於具體的應用程序和框架,需要根據情況進行調整和實施。

使用鉤子(hook)的優點和缺點如下:

優點:

  1. 模塊化和可複用性:使用鉤子可以將特定的操作和邏輯與主要代碼解耦,使代碼更具模塊化和可複用性。通過在適當的時機觸發鉤子函數,可以方便地插入自定義邏輯。
  2. 擴展性:鉤子提供了一種擴展現有功能的簡單方式。通過添加新的鉤子函數,可以在不修改主要代碼的情況下,為系統添加新的行為或功能。
  3. 靈活性:鉤子使系統更加靈活,可以根據需求定制特定的操作和邏輯。不同的鉤子函數可以根據具體情況進行不同的處理,從而滿足不同的需求。

缺點:

  1. 複雜性:過多或複雜的鉤子函數可能會增加代碼的複雜性和維護成本。在設計和實現鉤子時,需要仔細考慮它們的使用方式和限制,以避免過度使用或混亂的鉤子函數。
  2. 可能引入不確定性:過度使用鉤子函數可能會引入不確定性和難以追踪的行為。如果鉤子函數的執行順序和相互之間的影響不加控制,可能會導致系統行為的不一致或不可預測性。
  3. 性能影響:鉤子函數的執行時間和資源消耗可能會對系統性能產生負面影響。特別是在高頻率或大規模使用鉤子的情況下,需要仔細評估和測試性能,以確保不會成為性能瓶頸。

合理使用鉤子函數可以提高代碼的靈活性和可擴展性,但過度使用或不恰當使用可能會增加複雜性和引入不確定性。

8. 說一下 raft 的基本原理,有什麼作用?了解 zk 的 zab 協議嗎? paxos 之類的了解嗎?

TODO: 看一下鳳凰架構

Raft

Raft 是一種共識算法,用於在分佈式系統中實現一致性。它的設計目標是使分佈式系統的設計和實現更加可理解和可靠。 Raft 算法通過選舉一個領導者(Leader)來協調節點間的一致性操作,包括日誌複製、狀態機更新等。 Raft 算法具有以下特點:

  1. 強一致性:Raft 算法保證系統在任何時間點都具有一致的狀態,即使在節點故障或網絡分區的情況下也能保持一致性。
  2. 領導者選舉:Raft 中的節點通過互相通信來選舉一個領導者,領導者負責協調和處理客戶端的請求。
  3. 日誌複製:Raft 通過日誌複製機制來保證各個節點之間的數據一致性。領導者將接收到的客戶端請求追加到自己的日誌中,並將這些日誌複製給其他節點。
  4. 安全性: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 框架涉及多個方面的設計和實現,以下是一些常見的關鍵組件和步驟:

  1. 定義接口:RPC 框架需要支持定義接口和方法,這些接口和方法可以在不同的節點之間進行調用。通常使用接口描述語言(IDL)來定義接口,並根據定義生成對應的代碼。
  2. 序列化和反序列化:RPC 框架需要支持將數據在不同節點之間進行序列化和反序列化,以便在網絡上進行傳輸。常見的序列化協議包括 JSON、Protocol Buffers、Thrift 等。
  3. 傳輸協議:RPC 框架需要選擇一種傳輸協議來在網絡上進行數據傳輸。常見的選擇有基於 TCP 的協議如 gRPC、Apache Thrift,以及基於 HTTP 的協議如 JSON-RPC。
  4. 通信方式:RPC 框架可以支持不同的通信方式,包括同步調用、異步調用和回調機制等。這取決於具體的應用場景和需求。
  5. 服務註冊與發現:在分佈式系統中,RPC 框架需要提供服務註冊與發現的能力,以便客戶端能夠找到可用的服務。這可以通過使用服務註冊中心(如 ZooKeeper、Consul)或者使用分佈式配置中心(如 Etcd、Nacos)來實現。
  6. 負載均衡:在有多個服務提供者的情況下,RPC 框架可以支持負載均衡算法,將請求分發到不同的服務提供者上,以實現負載均衡和高可用性。
  7. 容錯和重試:RPC 框架應該具備容錯和重試機制,能夠處理網絡故障、服務不可用等異常情況,並儘可能地保證調用的可靠性。
  8. 監控和日誌:RPC 框架應該提供監控和日誌功能,可以記錄調用的各種指標和信息,方便故障排查和性能優化。

如果我設計一個 RPC 框架,我會考慮以下幾點:

  1. 簡單易用:提供簡潔的 API 和清晰的文檔,使開發者能夠方便地定義和調用遠程服務。
  2. 高性能:採用高效的序列化和傳輸協議,以及優化的網絡通信和線程模型,提供高性能的遠程調用能力。
  3. 可擴展性:支持水平擴展和集群部署,能夠處理大規模的並發請求,並具備良好的容錯和負載均衡能力。
  4. 可監控性:提供監控和日誌功能,記錄調用指標和異常信息,支持可視化監控和告警。
  5. 兼容性:支持多種編程語言和平台,使得不同技術棧的應用能夠方便地進行跨語言的遠程調用。

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和功能。
  • 流程:

    1. 啟動ES集群,節點加入集群,進行節點發現與協調。
    2. 創建索引,定義索引的映射和設置。
    3. 將文檔數據通過API或其他工具插入到索引中。
    4. 執行搜索和聚合操作,ES會將查詢請求分發給相應的分片進行處理。
    5. 返回搜索結果,可以根據需求進行排序、過濾和聚合操作。

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期望的狀態,由控制平面組件進行狀態調整。
    • 自愈能力:通過監控和控制器的機制,自動修復故障和維護集群的期望狀態。
  • 流程:

    1. 配置Kubernetes集群,啟動控制平面組件和工作節點組件。
    2. 使用kubectl或其他方式創建資源對象的定義文件(如Deployment、Service等)。
    3. 控制平面組件接收到定義文件,驗證和解析後存儲在etcd中。
    4. 調度器將Pod調度到可用的節點上。
    5. Kubelet在節點上創建和管理Pod,包括容器的拉取、運行和監控。
    6. Kube-proxy負責為Pod提供網絡代理和負載均衡。
    7. 控制平面組件和控制器不斷監控集群狀態,根據定義文件的期望狀態進行調整和修復。

Kafka:

  • 組件:

    • Broker:Kafka的消息服務器,負責接收、存儲和傳遞消息。
    • Topic:消息的分類和邏輯容器。
    • Partition:Topic的分區,用於實現消息的水平擴展和負載均衡。
    • Producer:生產者,負責發布消息到Topic。
    • Consumer:消費者,負責訂閱Topic並消費消息。
    • Consumer Group:消費者組,將多個消費者組織在一起,實現消息的並行消費。
    • ZooKeeper:用於協調Kafka集群的分佈式服務。
  • 實現:

    • 分佈式架構:Kafka採用分佈式架構,允許消息以分區的方式存儲和傳遞。
    • 持久化存儲:消息被持久化存儲在磁盤上,以保證消息的持久性和可靠性。
    • 基於日誌的存儲模型:Kafka使用基於日誌的存儲模型,通過追加寫入和順序讀取的方式實現高性能和低延遲的消息處理。
    • 副本機制:Kafka通過副本機制實現數據的冗餘和容錯性,確保消息的可用性。
    • 分佈式提交消費位移:Kafka使用ZooKeeper或自身內置的位移管理器(offset manager)來管理消費者的位移,實現消息的順序消費和消費的容錯。
  • 流程:

    1. 配置Kafka集群,啟動Broker和ZooKeeper。
    2. 創建Topic,指定分區和副本數。
    3. Producer將消息發送到指定的Topic。
    4. Consumer訂閱Topic,並從Broker中拉取消息。
    5. Consumer Group中的消費者協調消費位移,實現消息的並行消費。
    6. 消息持久化存儲在Broker的磁盤中,可供後續的消費者重新消費或進行消息回溯。

Zookeeper(ZK)的ZAB協議:

  • ZAB(Zookeeper Atomic Broadcast)協議是ZooKeeper內部使用的一種原子廣播協議,用於實現ZooKeeper的一致性。

  • ZAB協議的基本原理:

    1. 基於原子廣播:ZAB協議確保了消息在ZooKeeper集群中的原子廣播,即消息要么被所有節點接收,要么都不接收。
    2. 基於領導者選舉:ZAB協議通過領導者選舉機制選舉出一個節點作為Leader,負責處理客戶端請求和協調集群狀態。
    3. 基於多數派投票:ZAB協議要求Leader的寫操作得到多數派節點的確認,保證了數據的一致性。
    4. 基於持久化日誌:ZAB協議使用持久化日誌來記錄和復制數據變更操作,確保了數據的可靠性。
  • ZAB協議的流程:

    1. 集群初始化階段:
      • 選舉Leader節點。
      • Leader同步狀態到Follower節點。
    2. 正常運行階段:
      • 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,以此類推。 基於這個特性,可以使用以下的位運算操作進行判斷:

  1. 對於大於0的數n,如果n是2的冪次方,那麼n & (n-1) 的結果應該為0。因為n的二進製表示中只有一個1,而n-1的二進製表示中除了最高位的1,其他位都是1,所以n與n-1進行與運算結果為0。
  2. 對於大於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可能稍微慢一些的原因主要是因為:

  1. 位運算是直接對二進制位進行操作,而n % 2涉及到除法運算,除法運算的計算複雜度較高。除法運算通常比位運算需要更多的計算步驟和時間。
  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) 的查找操作,保證了較高的性能。

© Kimi Tsai all right reserved.            Updated : 2023-07-12 09:04:53

results matching ""

    No results matching ""

    results matching ""

      No results matching ""