騰訊互娛面經(jīng)詳解
先來嘮嘮
圖片
今天刷脈脈的時(shí)候, 發(fā)現(xiàn)百度副總裁璩靜一個(gè)人竟然占了前三的兩個(gè)熱榜, 對(duì)于她的離職你怎么看?
言歸正傳, 本文的重點(diǎn)還是分享面經(jīng)干貨
今天分享的是一位朋友在騰訊互娛的面經(jīng), 他本人目前已經(jīng)是收到offer了, 讓我們來看看這個(gè)難度如何:
圖片
面試題詳解
Go接口
接口在Golang中扮演著連接不同類型之間的橋梁,它定義了一組方法的集合,而不關(guān)心具體的實(shí)現(xiàn)。接口的作用主要體現(xiàn)在以下幾個(gè)方面:
多態(tài)性:
接口允許不同的類型實(shí)現(xiàn)相同的方法,從而實(shí)現(xiàn)多態(tài)性。這意味著我們可以使用接口類型來處理不同的對(duì)象,而不需要關(guān)心具體的類型。
package main
import "fmt"
type Animal interface {
Sound() string
}
type Dog struct{}
func (d Dog) Sound() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Sound() string {
return "Meow!"
}
func main() {
animals := []Animal{Dog{}, Cat{}}
for _, animal := range animals {
fmt.Println(animal.Sound())
}
}
在上面的示例中,我們定義了一個(gè)Animal接口,它包含了一個(gè)Sound()方法。然后,我們實(shí)現(xiàn)了Dog和Cat兩個(gè)結(jié)構(gòu)體,分別實(shí)現(xiàn)了Sound()方法。通過將Dog和Cat類型賦值給Animal接口類型,我們可以在循環(huán)中調(diào)用Sound()方法,而不需要關(guān)心具體的類型。這就體現(xiàn)了接口的多態(tài)性,不同的類型可以實(shí)現(xiàn)相同的接口方法。
解耦合:
接口可以將抽象與實(shí)現(xiàn)分離,降低代碼之間的耦合度。通過定義接口,我們可以將實(shí)現(xiàn)細(xì)節(jié)隱藏起來,只暴露必要的方法,從而提高代碼的可維護(hù)性和可讀性。
package main
import "fmt"
type Printer interface {
Print(string)
}
type ConsolePrinter struct{}
func (cp ConsolePrinter) Print(message string) {
fmt.Println(message)
}
type FilePrinter struct{}
func (fp FilePrinter) Print(message string) {
// 將消息寫入文件
fmt.Println("Writing message to file:", message)
}
func main() {
printer := ConsolePrinter{}
printer.Print("Hello, World!")
printer = FilePrinter{}
printer.Print("Hello, World!")
}
在上面的示例中,我們定義了一個(gè)Printer接口,它包含了一個(gè)Print()方法。然后,我們實(shí)現(xiàn)了ConsolePrinter和FilePrinter兩個(gè)結(jié)構(gòu)體,分別實(shí)現(xiàn)了Print()方法。通過將不同的結(jié)構(gòu)體賦值給Printer接口類型的變量,我們可以在主函數(shù)中調(diào)用Print()方法,而不需要關(guān)心具體的實(shí)現(xiàn)。這樣,我們可以根據(jù)需要輕松地切換不同的打印方式,實(shí)現(xiàn)了解耦合。
可擴(kuò)展性:
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
shapes := []Shape{Rectangle{Width: 5, Height: 10}, Circle{Radius: 3}}
for _, shape := range shapes {
fmt.Println("Area:", shape.Area())
}
}
在上面的示例中,我們定義了一個(gè)Shape接口,它包含了一個(gè)Area()方法。然后,我們實(shí)現(xiàn)了Rectangle和Circle兩個(gè)結(jié)構(gòu)體,分別實(shí)現(xiàn)了Area()方法。通過將不同的結(jié)構(gòu)體賦值給Shape接口類型的切片,我們可以在循環(huán)中調(diào)用Area()方法,而不需要關(guān)心具體的類型。這樣,當(dāng)我們需要添加新的形狀時(shí),只需要實(shí)現(xiàn)Shape接口的Area()方法即可,而不需要修改已有的代碼。這就實(shí)現(xiàn)了代碼的可擴(kuò)展性。
接口的應(yīng)用場(chǎng)景
- API設(shè)計(jì):接口在API設(shè)計(jì)中起到了至關(guān)重要的作用。通過定義接口,我們可以規(guī)范API的輸入和輸出,提高代碼的可讀性和可維護(hù)性。
- 單元測(cè)試:接口在單元測(cè)試中也扮演著重要的角色。通過使用接口,我們可以輕松地替換被測(cè)試對(duì)象的實(shí)現(xiàn),從而實(shí)現(xiàn)對(duì)被測(cè)代碼的獨(dú)立測(cè)試。
- 插件系統(tǒng):接口可以用于實(shí)現(xiàn)插件系統(tǒng),通過定義一組接口,不同的插件可以實(shí)現(xiàn)這些接口,并在程序運(yùn)行時(shí)動(dòng)態(tài)加載和使用插件。
- 依賴注入:接口在依賴注入中也有廣泛的應(yīng)用。通過定義接口,我們可以將依賴對(duì)象的創(chuàng)建和管理交給外部容器,從而實(shí)現(xiàn)松耦合的代碼結(jié)構(gòu)。
空結(jié)構(gòu)體的用途
不包含任何字段的結(jié)構(gòu)體,就叫做空結(jié)構(gòu)體。
空結(jié)構(gòu)體的特點(diǎn):
- 零內(nèi)存占用
- 地址都相同
- 無狀態(tài)
空結(jié)構(gòu)體的使用場(chǎng)景
- 實(shí)現(xiàn)set集合
在 Go 語言中,雖然沒有內(nèi)置 Set 集合類型,但是我們可以利用 map 類型來實(shí)現(xiàn)一個(gè) Set 集合。由于 map 的 key 具有唯一性,我們可以將元素存儲(chǔ)為 key,而 value 沒有實(shí)際作用,為了節(jié)省內(nèi)存,我們可以使用空結(jié)構(gòu)體作為 value 的值。
package main
import "fmt"
type Set[K comparable] map[K]struct{}
func (s Set[K]) Add(val K) {
s[val] = struct{}{}
}
func (s Set[K]) Remove(val K) {
delete(s, val)
}
func (s Set[K]) Contains(val K) bool {
_, ok := s[val]
return ok
}
func main() {
set := Set[string]{}
set.Add("程序員")
fmt.Println(set.Contains("程序員")) // true
set.Remove("程序員")
fmt.Println(set.Contains("程序員")) // false
}
- 用于通道信號(hào)
空結(jié)構(gòu)體常用于 Goroutine 之間的信號(hào)傳遞,尤其是不關(guān)心通道中傳遞的具體數(shù)據(jù),只需要一個(gè)觸發(fā)信號(hào)時(shí)。例如,我們可以使用空結(jié)構(gòu)體通道來通知一個(gè) Goroutine 停止工作:
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan struct{})
go func() {
// 模擬工作
fmt.Println("工作中...")
time.Sleep(3 * time.Second)
// 關(guān)閉退出信號(hào)
close(quit)
}()
// 阻塞,等待退出信號(hào)被關(guān)閉
<-quit
fmt.Println("已收到退出信號(hào),退出中...")
}
- 作為方法接收器
有時(shí)候我們需要?jiǎng)?chuàng)建一組方法集的實(shí)現(xiàn)(一般來說是實(shí)現(xiàn)一個(gè)接口),但并不需要在這個(gè)實(shí)現(xiàn)中存儲(chǔ)任何數(shù)據(jù),這種情況下,我們可以使用空結(jié)構(gòu)體來實(shí)現(xiàn):
type Person interface {
SayHello()
Sleep()
}
type CMY struct{}
func (c CMY) SayHello() {
fmt.Println("你好,世界。")
}
func (c CMY) Sleep() {
fmt.Println("晚安,世界...")
}
Go原生支持默認(rèn)參數(shù)或可選參數(shù)嗎,如何實(shí)現(xiàn)
什么是默認(rèn)參數(shù)
默認(rèn)參數(shù)是指在函數(shù)調(diào)用時(shí),如果沒有提供某個(gè)參數(shù)的值,那么使用函數(shù)定義中指定的默認(rèn)值。這種語言特性可以減少代碼量,簡(jiǎn)化函數(shù)的使用。
在Go語言中,函數(shù)不支持默認(rèn)參數(shù)。這意味著如果我們想要設(shè)置默認(rèn)值,那么就需要手動(dòng)在函數(shù)內(nèi)部進(jìn)行處理。
例如,下面是一個(gè)函數(shù)用于計(jì)算兩個(gè)整數(shù)的和:
func Add(a int, b int) int {
return a + b
}
如果我們希望b參數(shù)有一個(gè)默認(rèn)值,例如為0,那么可以在函數(shù)內(nèi)部進(jìn)行處理:
func AddWithDefault(a int, b int) int {
if b == 0 {
b = 0
}
return a + b
}
上面的代碼中,如果b參數(shù)沒有提供值,那么默認(rèn)為0。通過這種方式,我們就實(shí)現(xiàn)了函數(shù)的默認(rèn)參數(shù)功能。
需要注意的是,這種處理方式雖然可以實(shí)現(xiàn)默認(rèn)參數(shù)的效果,但會(huì)增加代碼復(fù)雜度和維護(hù)難度,因此在Go語言中不被推薦使用。
什么是可選參數(shù)
可選參數(shù)是指在函數(shù)調(diào)用時(shí),可以省略一些參數(shù)的值,從而讓函數(shù)更加靈活。這種語言特性可以讓函數(shù)更加易用,提高代碼的可讀性。
在Go語言中,函數(shù)同樣不支持可選參數(shù)。但是,我們可以使用可變參數(shù)來模擬可選參數(shù)的效果。
下面是一個(gè)函數(shù)用于計(jì)算任意個(gè)整數(shù)的和:
func Add(nums ...int) int {
sum := 0
for _, num := range nums {
sum += num
}
return sum
}
上面的代碼中,我們使用...int類型的可變參數(shù)來接收任意個(gè)整數(shù),并在函數(shù)內(nèi)部進(jìn)行求和處理。
如果我們希望b和c參數(shù)為可選參數(shù),那么可以將它們放到nums可變參數(shù)之后:
func AddWithOptional(a int, nums ...int) int {
sum := a
for _, num := range nums {
sum += num
}
return sum
}
上面的代碼中,我們首先將a參數(shù)賦值給sum變量,然后對(duì)可變參數(shù)進(jìn)行求和處理。如果函數(shù)調(diào)用時(shí)省略了nums參數(shù),則sum等于a的值。
需要注意的是,使用可變參數(shù)模擬可選參數(shù)的效果雖然能夠?qū)崿F(xiàn)函數(shù)的靈活性,但也會(huì)降低代碼的可讀性和規(guī)范性。因此在Go語言中不被推薦使用。
defer執(zhí)行順序
在 Go 中,defer 語句用于延遲(defer)函數(shù)的執(zhí)行,通常用于在函數(shù)執(zhí)行結(jié)束前執(zhí)行一些清理或收尾工作。當(dāng)函數(shù)中存在多個(gè) defer 語句時(shí),它們的執(zhí)行順序是“后進(jìn)先出”(Last In First Out,LIFO)的,即最后一個(gè)被延遲的函數(shù)最先執(zhí)行,倒數(shù)第二個(gè)被延遲的函數(shù)次之,以此類推。
在 Go 中,defer 語句中的函數(shù)在執(zhí)行時(shí)會(huì)被壓入一個(gè)棧中,當(dāng)函數(shù)執(zhí)行結(jié)束時(shí),這些被延遲的函數(shù)會(huì)按照后進(jìn)先出的順序執(zhí)行。這意味著在函數(shù)中的 defer 語句中的函數(shù)會(huì)在函數(shù)執(zhí)行結(jié)束前執(zhí)行,包括在 return 語句之前執(zhí)行。
協(xié)程之間信息如何同步
協(xié)程(Goroutine)之間的信息同步通常通過通道(Channel)來實(shí)現(xiàn)。通道是 Go 語言中用于協(xié)程之間通信的重要機(jī)制,可以安全地在不同協(xié)程之間傳遞數(shù)據(jù),實(shí)現(xiàn)協(xié)程之間的信息同步。
一些常見的方法來實(shí)現(xiàn)協(xié)程之間的信息同步:
- 使用無緩沖通道:無緩沖通道是一種同步通道,發(fā)送和接收操作會(huì)在數(shù)據(jù)準(zhǔn)備好之前被阻塞。通過無緩沖通道,可以實(shí)現(xiàn)協(xié)程之間的同步通信,確保數(shù)據(jù)的正確傳遞。
- 使用帶緩沖通道:帶緩沖通道允許在通道中存儲(chǔ)一定數(shù)量的元素,發(fā)送操作只有在通道已滿時(shí)才會(huì)被阻塞。通過帶緩沖通道,可以實(shí)現(xiàn)異步通信。
- 使用同步原語:Go 語言中的 sync 包提供了一些同步原語,如互斥鎖(Mutex)、讀寫鎖(RWMutex)等,可以用于協(xié)程之間的同步訪問共享資源。
- 使用select語句:select 語句可以用于在多個(gè)通道操作中選擇一個(gè)執(zhí)行,可以實(shí)現(xiàn)協(xié)程之間的多路復(fù)用和超時(shí)控制。
- 使用context包:context 包提供了一種在協(xié)程之間傳遞取消信號(hào)和截止時(shí)間的機(jī)制,可以用于協(xié)程之間的協(xié)調(diào)和同步。
GMP模型
GM模型開銷大的原因?
最開始的是GM模型沒有 P 的,是M:N的兩級(jí)線程模型,但是會(huì)出現(xiàn)一些性能問題:
- 全局隊(duì)列的鎖競(jìng)爭(zhēng)。M從全局隊(duì)列中添加或獲取 G 的時(shí)候,都是需要上鎖的(下圖執(zhí)行步驟要加鎖),這樣就會(huì)導(dǎo)致鎖競(jìng)爭(zhēng),雖然達(dá)到了并發(fā)安全,但是性能是非常差的。
- M 轉(zhuǎn)移 G 會(huì)有額外開銷。M 在執(zhí)行 G 的時(shí)候,假設(shè) M1 執(zhí)行的 G1 創(chuàng)建了 G2,新創(chuàng)建的就要放到全局隊(duì)列中去,但是這時(shí)有一個(gè)空閑的 M2 獲取到了 G2,那么這樣 G1、G2 會(huì)被不同的 M 執(zhí)行,但是 M1 中本來就有 G2 的信息,M2 在 G1 上執(zhí)行是更好的,而且取和放到全局隊(duì)列也會(huì)來回加鎖,這樣都會(huì)有一部分開銷。
- 線程的使用效率不能最大化。M 拿不到的時(shí)候就會(huì)一直空閑,阻塞的時(shí)候也不會(huì)切換。也就是沒有 workStealing 機(jī)制和 handOff 機(jī)制。
圖片
GMP
圖片
go生成一個(gè)協(xié)程,此時(shí)放在P中還是M中
如果在本地的隊(duì)列中有足夠的空間,則會(huì)直接進(jìn)入本地隊(duì)列等待M的執(zhí)行;如果本地隊(duì)列已經(jīng)滿了,則進(jìn)入全局隊(duì)列(在GMP模型中,所有的M都可以從全局隊(duì)列中獲取協(xié)程并執(zhí)行)
G阻塞,M、P如何
當(dāng)G因系統(tǒng)調(diào)用(syscall)阻塞時(shí)會(huì)阻塞M,此時(shí)P會(huì)和M解綁即hand off,并尋找新的idle的M,若沒有idle的M就會(huì)新建一個(gè)M
Redis與MySQL數(shù)據(jù)如何同步
這里提供幾種方案:
- 定時(shí)任務(wù)同步:編寫定時(shí)任務(wù)或腳本,定期從 MySQL 中讀取數(shù)據(jù),然后將數(shù)據(jù)同步到 Redis 中。這種方案簡(jiǎn)單直接,適用于數(shù)據(jù)量不大且同步頻率不高的場(chǎng)景。
- 使用消息隊(duì)列:將 MySQL 中的數(shù)據(jù)變更操作(如新增、更新、刪除)通過消息隊(duì)列(如 RabbitMQ、Kafka)發(fā)送到消息隊(duì)列中,然后消費(fèi)者從消息隊(duì)列中讀取消息,將數(shù)據(jù)同步到 Redis 中。這種方案實(shí)現(xiàn)了異步同步,降低了對(duì) MySQL 的壓力。
- 使用數(shù)據(jù)庫(kù)觸發(fā)器:在 MySQL 中設(shè)置觸發(fā)器,當(dāng)數(shù)據(jù)發(fā)生變更時(shí)觸發(fā)觸發(fā)器,將變更信息發(fā)送到消息隊(duì)列或直接同步到 Redis 中。這種方案可以實(shí)現(xiàn)實(shí)時(shí)同步,但需要謹(jǐn)慎設(shè)計(jì)觸發(fā)器邏輯,避免影響數(shù)據(jù)庫(kù)性能。
- 使用數(shù)據(jù)同步工具:可以使用一些數(shù)據(jù)同步工具(如 Maxwell、Debezium)來實(shí)現(xiàn) MySQL 和 Redis 數(shù)據(jù)的實(shí)時(shí)同步。這些工具可以監(jiān)控 MySQL 數(shù)據(jù)庫(kù)的變更,并將變更數(shù)據(jù)同步到 Redis 中。
- 使用緩存庫(kù):一些緩存庫(kù)(如 Redisson、Lettuce)提供了與 MySQL 數(shù)據(jù)庫(kù)的集成,可以通過配置簡(jiǎn)單地實(shí)現(xiàn) MySQL 數(shù)據(jù)到 Redis 的同步。
Explain的字段
explain的用法:
explain select * from gateway_apps;
返回結(jié)果:
圖片
下面對(duì)上面截圖中的字段一一解釋:
1、id:select 查詢序列號(hào)。id相同,執(zhí)行順序由上至下;id不同,id值越大優(yōu)先級(jí)越高,越先被執(zhí)行。
2、select_type:查詢數(shù)據(jù)的操作類型,其值如下:
- simple:簡(jiǎn)單查詢,不包含子查詢或 union
- primary:包含復(fù)雜的子查詢,最外層查詢標(biāo)記為該值
- subquery:在 select 或 where 包含子查詢,被標(biāo)記為該值
- derived:在 from 列表中包含的子查詢被標(biāo)記為該值,MySQL 會(huì)遞歸執(zhí)行這些子查詢,把結(jié)果放在臨時(shí)表
- union:若第二個(gè) select 出現(xiàn)在 union 之后,則被標(biāo)記為該值。若 union 包含在 from 的子查詢中,外層 select 被標(biāo)記為 derived
- union result:從 union 表獲取結(jié)果的 select
3、table:顯示該行數(shù)據(jù)是關(guān)于哪張表
4、partitions:匹配的分區(qū)
5、type:表的連接類型,其值,性能由高到底排列如下:
- system:表只有一行記錄,相當(dāng)于系統(tǒng)表
- const:通過索引一次就找到,只匹配一行數(shù)據(jù)
- eq_ref:唯一性索引掃描,對(duì)于每個(gè)索引鍵,表中只有一條記錄與之匹配。常用于主鍵或唯一索引掃描
- ref:非唯一性索引掃描,返回匹配某個(gè)單獨(dú)值的所有行。用于=、< 或 > 操作符帶索引的列
- range:只檢索給定范圍的行,使用一個(gè)索引來選擇行。一般使用between、>、<情況
- index:只遍歷索引樹
- ALL:全表掃描,性能最差
6、 possible_keys:顯示 MySQL 理論上使用的索引,查詢涉及到的字段上若存在索引,則該索引將被列出,但不一定被查詢實(shí)際使用。如果該值為 NULL,說明沒有使用索引,可以建立索引提高性能
7、key:顯示 MySQL 實(shí)際使用的索引。如果為 NULL,則沒有使用索引查詢
8、key_len:表示索引中使用的字節(jié)數(shù),通過該列計(jì)算查詢中使用的索引的長(zhǎng)度。在不損失精確性的情況下,長(zhǎng)度越短越好 顯示的是索引字段的最大長(zhǎng)度,并非實(shí)際使用長(zhǎng)度
9、ref:顯示該表的索引字段關(guān)聯(lián)了哪張表的哪個(gè)字段
10、 rows:根據(jù)表統(tǒng)計(jì)信息及選用情況,大致估算出找到所需的記錄或所需讀取的行數(shù),數(shù)值越小越好
11、filtered:返回結(jié)果的行數(shù)占讀取行數(shù)的百分比,值越大越好
12、extra:包含不合適在其他列中顯示但十分重要的額外信息,常見的值如下:
- using filesort:說明 MySQL 會(huì)對(duì)數(shù)據(jù)使用一個(gè)外部的索引排序,而不是按照表內(nèi)的索引順序進(jìn)行讀取。出現(xiàn)該值,應(yīng)該優(yōu)化 SQL
- using temporary:使用了臨時(shí)表保存中間結(jié)果,MySQL 在對(duì)查詢結(jié)果排序時(shí)使用臨時(shí)表。常見于排序 order by 和分組查詢 group by。出現(xiàn)該值,應(yīng)該優(yōu)化 SQL
- using index:表示相應(yīng)的 select 操作使用了覆蓋索引,避免了訪問表的數(shù)據(jù)行,效率不錯(cuò)
- using where:where 子句用于限制哪一行
- using join buffer:使用連接緩存
- distinct:發(fā)現(xiàn)第一個(gè)匹配后,停止為當(dāng)前的行組合搜索更多的行
Redis過期刪除策略
Redis 中提供了三種過期刪除的策略
定時(shí)刪除
在設(shè)置某個(gè) key 的過期時(shí)間同時(shí),我們創(chuàng)建一個(gè)定時(shí)器,讓定時(shí)器在該過期時(shí)間到來時(shí),立即執(zhí)行對(duì)其進(jìn)行刪除的操作。
優(yōu)點(diǎn):
對(duì) CPU 是友好的,只有在取出鍵值對(duì)的時(shí)候才會(huì)進(jìn)行過期檢查,這樣就不會(huì)把 CPU 資源花費(fèi)在其他無關(guān)緊要的鍵值對(duì)的過期刪除上。
缺點(diǎn):
如果一些鍵值對(duì)永遠(yuǎn)不會(huì)被再次用到,那么將不會(huì)被刪除,最終會(huì)造成內(nèi)存泄漏,無用的垃圾數(shù)據(jù)占用了大量的資源,但是服務(wù)器卻不能去刪除。
惰性刪除
惰性刪除,當(dāng)一個(gè)鍵值對(duì)過期的時(shí)候,只有再次用到這個(gè)鍵值對(duì)的時(shí)候才去檢查刪除這個(gè)鍵值對(duì),也就是如果用不著,這個(gè)鍵值對(duì)就會(huì)一直存在。
優(yōu)點(diǎn):
對(duì) CPU 是友好的,只有在取出鍵值對(duì)的時(shí)候才會(huì)進(jìn)行過期檢查,這樣就不會(huì)把 CPU 資源花費(fèi)在其他無關(guān)緊要的鍵值對(duì)的過期刪除上。
缺點(diǎn):
如果一些鍵值對(duì)永遠(yuǎn)不會(huì)被再次用到,那么將不會(huì)被刪除,最終會(huì)造成內(nèi)存泄漏,無用的垃圾數(shù)據(jù)占用了大量的資源,但是服務(wù)器卻不能去刪除。
定期刪除
定期刪除是對(duì)上面兩種刪除策略的一種整合和折中
每個(gè)一段時(shí)間就對(duì)一些 key 進(jìn)行采樣檢查,檢查是否過期,如果過期就進(jìn)行刪除
1、采樣一定個(gè)數(shù)的key,采樣的個(gè)數(shù)可以進(jìn)行配置,并將其中過期的 key 全部刪除;
2、如果過期 key 的占比超過可接受的過期 key 的百分比,則重復(fù)刪除的過程,直到過期key的比例降至可接受的過期 key 的百分比以下。
優(yōu)點(diǎn):
定期刪除,通過控制定期刪除執(zhí)行的時(shí)長(zhǎng)和頻率,可以減少刪除操作對(duì) CPU 的影響,同時(shí)也能較少因過期鍵帶來的內(nèi)存的浪費(fèi)。
缺點(diǎn):
執(zhí)行的頻率不太好控制
頻率過快對(duì) CPU 不友好,如果過慢了就會(huì)對(duì)內(nèi)存不太友好,過期的鍵值對(duì)不能及時(shí)的被刪除掉
同時(shí)如果一個(gè)鍵值對(duì)過期了,但是沒有被刪除,這時(shí)候業(yè)務(wù)再次獲取到這個(gè)鍵值對(duì),那么就會(huì)獲取到被刪除的數(shù)據(jù)了,這肯定是不合理的。
Redis 中過期刪除策略
上面討論的三種策略,都有或多或少的問題。Redis 中實(shí)際采用的策略是惰性刪除加定期刪除的組合方式。
定期刪除,獲取 CPU 和 內(nèi)存的使用平衡,針對(duì)過期的 KEY 可能得不到及時(shí)的刪除,當(dāng) KEY 被再次獲取的時(shí)候,通過惰性刪除再做一次過期檢查,來避免業(yè)務(wù)獲取到過期內(nèi)容。
Redis常用數(shù)據(jù)結(jié)構(gòu)
Redis 共有 5 種基本數(shù)據(jù)類型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
Zset使用場(chǎng)景, 具體實(shí)現(xiàn)
Zset的兩種實(shí)現(xiàn)方式:
- ziplist:滿足以下兩個(gè)條件的時(shí)候
- 元素?cái)?shù)量少于128的時(shí)候
- 每個(gè)元素的長(zhǎng)度小于64字節(jié)
- skiplist:不滿足上述兩個(gè)條件就會(huì)使用跳表,具體來說是組合了map和skiplist
- map用來存儲(chǔ)member到score的映射,這樣就可以在O(1)時(shí)間內(nèi)找到member對(duì)應(yīng)的分?jǐn)?shù)
- skiplist按從小到大的順序存儲(chǔ)分?jǐn)?shù)
- skiplist每個(gè)元素的值都是[score,value]對(duì)
skiplist優(yōu)勢(shì)
skiplist本質(zhì)上是并行的有序鏈表,但它克服了有序鏈表插入和查找性能不高的問題。它的插入和查詢的時(shí)間復(fù)雜度都是O(logN)
skiplist原理
普通有序鏈表的插入需要一個(gè)一個(gè)向前查找是否可以插入,所以時(shí)間復(fù)雜度為O(N),比如下面這個(gè)鏈表插入23,就需要一直查找到22和26之間。
圖片
如果節(jié)點(diǎn)能夠跳過一些節(jié)點(diǎn),連接到更靠后的節(jié)點(diǎn)就可以優(yōu)化插入速度:
圖片
在上面這個(gè)結(jié)構(gòu)中,插入23的過程是
- 先使用第2層鏈接head->7->19->26,發(fā)現(xiàn)26比23大,就回到19
- 再用第1層連接19->22->26,發(fā)現(xiàn)比23大,那么就插入到26之前,22之后
上面這張圖就是跳表的初步原理,但一個(gè)元素插入鏈表后,應(yīng)該擁有幾層連接呢?跳表在這塊的實(shí)現(xiàn)方式是隨機(jī)的,也就是23這個(gè)元素插入后,隨機(jī)出一個(gè)數(shù),比如這個(gè)數(shù)是3,那么23就會(huì)有如下連接:
- 第3層head->23->end
- 第2層19->23->26
- 第1層22->23->26
下面這張圖展示了如何形成一個(gè)跳表
圖片
在上述跳表中查找/插入23的過程為:
圖片
總結(jié)一下跳表原理:
- 每個(gè)跳表都必須設(shè)定一個(gè)最大的連接層數(shù)MaxLevel
- 第一層連接會(huì)連接到表中的每個(gè)元素
- 插入一個(gè)元素會(huì)隨機(jī)生成一個(gè)連接層數(shù)值[1, MaxLevel]之間,根據(jù)這個(gè)值跳表會(huì)給這元素建立N個(gè)連接
- 插入某個(gè)元素的時(shí)候先從最高層開始,當(dāng)跳到比目標(biāo)值大的元素后,回退到上一個(gè)元素,用該元素的下一層連接進(jìn)行遍歷,周而復(fù)始直到第一層連接,最終在第一層連接中找到合適的位置
使用場(chǎng)景:
- 對(duì)有序數(shù)據(jù)進(jìn)行排序,例如新聞排行榜或游戲排行榜。
- 對(duì)數(shù)據(jù)進(jìn)行分組,例如將所有評(píng)分在3.0 到4.0 之間的電影分為一組。
- 對(duì)數(shù)據(jù)進(jìn)行去重,例如將所有重復(fù)的單詞從文本中刪除。
本文轉(zhuǎn)載自微信公眾號(hào)「王中陽Go」,作者「王中陽Go」,可以通過以下二維碼關(guān)注。
轉(zhuǎn)載本文請(qǐng)聯(lián)系「王中陽Go」公眾號(hào)。