成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

徒手用 Go 寫個 Redis 服務器(Godis)

開發 前端 Redis
今天給大家帶來的開源項目是 Godis:一個用 Go 語言實現的 Redis 服務器。

 [[406191]]

今天給大家帶來的開源項目是 Godis:一個用 Go 語言實現的 Redis 服務器。支持:

  • 5 種數據結構(string、list、hash、set、sortedset)
  • 自動過期(TTL)
  • 發布訂閱、地理位置、持久化等功能

你或許不需要自己實現 Redis 服務,但你是否厭煩了每天都是寫增刪改查的業務代碼,想提高編程水平試圖從零寫個項目打開 IDE 卻發現無從下手?

動手造輪子一定是提高編程能力的好辦法,下面就帶大家用 Go 從頭開始寫一個 Redis 服務器(Godis),從中你將學到:

  • 如何編寫 Go 語言 TCP 服務器
  • 設計并實現安全可靠的通信協議(redis 協議)
  • 如何使用 Go 語言開發高并發程序
  • 設計和實現分布式集群以及分布式事務
  • 熟悉鏈表、哈希表、跳表以及時間輪等常用數據結構

千萬不要擔心內容太難,學不會或者沒有 Go 語言基礎!!雖然示例代碼是 Go 但不會影響你理解 Redis 的原理和底層協議以及高性能的秘密。而且作者為了照顧到廣大讀者,對技術的講解做了優化。示例代碼在原項目基礎上做了簡化,并逐行地加了注釋。如果是高級玩家,請直接訪問項目閱讀源碼:

https://github.com/HDT3213/godis

下面正文開始,讓我們一起撥開 Redis 的迷霧。

一、寫個 TCP 服務器

眾所周知 Redis 是 C/S 模型,使用 TCP 協議進行通信。接下來就從實現 TCP 服務端開始。作為廣泛用于服務端的編程語言 Golang 提供了非常簡潔的 TCP 接口,所以實現起來十分方便。示例代碼:

  1. func ListenAndServe(address string) { 
  2.     // 綁定監聽地址 
  3.     listener, err := net.Listen("tcp", address) 
  4.     if err != nil { 
  5.         log.Fatal(fmt.Sprintf("listen err: %v", err)) 
  6.     } 
  7.     defer listener.Close() 
  8.     log.Println(fmt.Sprintf("bind: %s, start listening...", address)) 
  9.  
  10.     for { 
  11.         // Accept 會一直阻塞直到有新的連接建立或者listen中斷才會返回 
  12.         conn, err := listener.Accept() 
  13.         if err != nil { 
  14.             // 通常是由于listener被關閉無法繼續監聽導致的錯誤 
  15.             log.Fatal(fmt.Sprintf("accept err: %v", err)) 
  16.         } 
  17.         // 開啟新的 goroutine 處理該連接 
  18.         go Handle(conn) 
  19.     } 
  20.  
  21. func Handle(conn net.Conn) { 
  22.     reader := bufio.NewReader(conn) 
  23.     for { 
  24.         // ReadString 會一直阻塞直到遇到分隔符 '\n' 
  25.         // 遇到分隔符后 ReadString 會返回上次遇到分隔符到現在收到的所有數據 
  26.         // 若在遇到分隔符之前發生異常, ReadString 會返回已收到的數據和錯誤信息 
  27.         msg, err := reader.ReadString('\n'
  28.         if err != nil { 
  29.             // 通常遇到的錯誤是連接中斷或被關閉,用io.EOF表示 
  30.             if err == io.EOF { 
  31.                 log.Println("connection close"
  32.             } else { 
  33.                 log.Println(err) 
  34.             } 
  35.             return 
  36.         } 
  37.         b := []byte(msg) 
  38.         // 將收到的信息發送給客戶端 
  39.         conn.Write(b) 
  40.     } 
  41.  
  42. func main() { 
  43.     ListenAndServe(":8000"

:ok_hand: 至此只用了 40 行代碼就搞定服務端啦!啟動上面的 TCP 服務后,在終端中輸入 telnet 127.0.0.1 8000 就可以連接到剛寫好的服務器,它會將你發送的消息原樣返回給你(所以請不要罵它):

這個 TCP 服務器的非常簡單,主協程調用 accept 函數來監聽端口,接受新連接后開啟一個 Goroutine 來處理它。這種簡單的阻塞 IO 模型有些類似于早期的 Tomcat/Apache 服務器。

阻塞 IO 模型是使用 一個線程處理一個連接 ,在沒有收到新數據時監聽線程處于阻塞狀態,直到數據就緒后線程被喚醒進行處理。因為阻塞 IO 模型需要開啟大量線程并且頻繁地進行上下文切換,所以它的效率很低。而 Redis 使用的 epoll 技術(IO 多路復用)用 一個線程處理大量連接 ,極大地提高了吞吐量。那么我們的 TCP 服務器會比 Redis 慢很多嗎?

當然不會,Golang 利用 Goroutine 調度開銷遠遠小于線程調度開銷的優勢封裝出 goroutine-per-connection 風格的極簡接口,而且 net/tcp 庫將 epoll 封裝成了阻塞 IO 的樣子,在享受 epoll 高性能的同時避免了原生 epoll 接口所需的復雜異步代碼。

在作者的電腦上 Redis 每秒可以響應 10.6k 個 PING 命令,而 Godis(完整代碼) 的吞吐量為 9.2 kqps 相差并不大。想了解更多 Golang 高性能的:secret:密,可以搜索 go netpoller 或者 go 語言 網絡輪詢器 關鍵字

另外,合格的 TCP 的服務器在關閉的時候不應該一停了之,而需要完成響應已接收的請求、釋放 TCP 連接等必要的清理工作。這個功能我們一般稱為 優雅關閉 或者 graceful shutdown ,優雅關閉步驟:

  • 首先,關閉 listener 停止接受新連接
  • 然后,遍歷所有存活連接逐個關閉

優雅關閉的代碼比較多,這里就不完整貼出了。

二、透視 Redis 協議

在解決完通信后,下一步就是搞清楚 Redis 的協議,其實就是一套序列化協議類似 JSON、Protocol Buffers,你看底層其實也就是一些基礎的知識。

自 Redis 2.0 以后的通信統一為 RESP 協議(REdis Serialization Protocol),該協議易于實現不僅可以高效的被程序解析,還能夠被人類讀懂容易調試。

RESP 是一個二進制安全的文本協議,工作于 TCP 協議上。RESP 以行作為單位,客戶端和服務器發送的命令或數據一律以 \r\n (CRLF)作為換行符。

二進制安全是指允許協議中出現任意字符而不會導致故障。比如 C 語言的字符串以 \0 作為結尾不允許字符串中間出現 \0 ,而 Go 語言的 string 則允許出現 \0 ,我們說 Go 語言的 string 是二進制安全的,而 C 語言字符串不是二進制安全的。

RESP 的二進制安全性允許我們在 key 或者 value 中包含 \r 或者 \n 這樣的特殊字符。在使用 Redis 存儲 protobuf、msgpack 等二進制數據時,二進制安全性尤為重要。

RESP 定義了 5 種格式:

  • 簡單字符串(Simple String): 服務器用來返回簡單的結果,比如 "OK" 非二進制安全,且不允許換行
  • 錯誤信息(Error):服務器用來返回簡單的錯誤信息,比如 "ERR Invalid Synatx" 非二進制安全,且不允許換行
  • 整數(Integer):llen、scard 等命令的返回值,64 位有符號整數
  • 字符串(Bulk String):二進制安全字符串,比如 get 等命令的返回值
  • 數組(Array,又稱 Multi Bulk Strings):Bulk String 數組,客戶端發送指令以及 lrange 等命令響應的格式

RESP 通過第一個字符來表示格式:

下面讓我們通過一些實際例子來理解協議。

2.1 字符串

字符串(Bulk String)有兩行,第一行為 $ +正文長度,第二行為實際內容。如:

  1. $3\r\nSET\r\n 

字符串(Bulk String)是二進制安全的,就是說可以在 Bulk String 內部包含 "\r\n" 字符(行尾的 CRLF 被隱藏):

  1. $4 
  2. a\r\nb 

2.2 空

$-1 表示 nil,比如使用 get 命令查詢一個不存在的 key 時,響應即為 $-1 。

2.3 數組

數組(Array)格式第一行為 "*"+數組長度,其后是相應數量的 字符串(Bulk String)。比如 ["foo", "bar"] 的報文(傳輸時的內容):

  1. *2 
  2. $3 
  3. foo 
  4. $3 
  5. bar 

客戶端也使用 數組(Array)格式向服務端發送指令。命令本身將作為第一個參數,比如 SET key value 指令的 RESP 報文:

  1. *3 
  2. $3 
  3. SET 
  4. $3 
  5. key 
  6. $5 
  7. value 

將換行符打印出來:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

2.4 解析預備

知道常用的 RESP 報文內容后,就可以開始著手解析了。但需要注意的是 RESP 是 二進制安全 的協議,它允許在正文中使用 \r\n 字符。舉例來說 Redis 可以正確接收并執行 SET "a\r\nb" hellogithub 指令,這條指令的正確報文是這樣的:

  1. *3   
  2. $3 
  3. SET 
  4. $4 
  5. a\r\nb  
  6. $11 
  7. hellogithub 

當 ReadBytes 讀取到第五行 "a\r\nb\r\n" 時會將其誤認為兩行:

  1. *3   
  2. $3 
  3. SET 
  4. $4 
  5. a  // 錯誤的分行 
  6. // 錯誤的分行 
  7. $11 
  8. hellogithub 

因此當讀取到第四行 $4 后,不應該繼續使用 ReadBytes('\n') 讀取下一行,應使用 io.ReadFull(reader, msg) 方法來讀取指定長度的內容。

  1. msg = make([]byte4 + 2// 正文長度4 + 換行符長度2 
  2. _, err = io.ReadFull(reader, msg) 

2.5 編寫 RESP 協議解析器

解決完上面內容包含 "\r\n" 的問題,我們就可以開始放手編寫 Redis 協議解析器啦!

  1. type Payload struct { 
  2.     Data redis.Reply 
  3.     Err  error 
  4.  
  5. // ParseStream 通過 io.Reader 讀取數據并將結果通過 channel 將結果返回給調用者 
  6. // 流式處理的接口適合供客戶端/服務端使用 
  7. func ParseStream(reader io.Reader) <-chan *Payload { 
  8.     ch := make(chan *Payload) 
  9.     go parse0(reader, ch) 
  10.     return ch 

由于解析器的代碼比較多,這里只簡單地介紹一下核心流程。

  1. func parse0(reader io.Reader, ch chan<- *Payload) { 
  2.     // 初始化讀取狀態 
  3.     readingMultiLine := false 
  4.     expectedArgsCount := 0 
  5.     var args [][]byte 
  6.     var bulkLen int64 
  7.     for { 
  8.         // 上文中我們提到 RESP 是以行為單位的 
  9.         // 因為行分為簡單字符串和二進制安全的 BulkString,我們需要封裝一個 readLine 函數來兼容 
  10.         line, err = readLine(reader, bulkLen) 
  11.         if err != nil {  
  12.             // 處理錯誤 
  13.             return 
  14.         } 
  15.         // 接下來我們對剛剛讀取的行進行解析 
  16.         // 我們簡單的將 Reply 分為兩類: 
  17.         // 單行: StatusReply, IntReply, ErrorReply 
  18.         // 多行: BulkReply, MultiBulkReply 
  19.  
  20.         if !readingMultiLine { 
  21.             if isMulitBulkHeader(line) { 
  22.                 // 我們收到了 MulitBulkReply 的第一行 
  23.                 // 獲得 MulitBulkReply 中 BulkString 的個數 
  24.                 expectedArgsCount = parseMulitBulkHeader(line) 
  25.                 // 等待 MulitBulkReply 后續行 
  26.                 readingMultiLine = true 
  27.             } else if isBulkHeader(line) { 
  28.                 // 我們收到了 BulkReply 的第一行 
  29.                 // 獲得 BulkReply 第二行的長度, 通過 bulkLen 告訴 readLine 函數下一行 BulkString 的長度 
  30.                 bulkLen = parseBulkHeader() 
  31.                 // 這個 Reply 中一共有 1 個 BulkString 
  32.                 expectedArgsCount = 1  
  33.                 // 等待 BulkReply 后續行 
  34.                 readingMultiLine = true 
  35.             } else { 
  36.                 // 處理 StatusReply, IntReply, ErrorReply 等單行 Reply 
  37.                 reply := parseSingleLineReply(line) 
  38.                 // 通過 ch 返回結果 
  39.                 emitReply(ch) 
  40.             } 
  41.         } else { 
  42.             // 進入此分支說明我們正在等待 MulitBulkReply 或 BulkReply 的后續行 
  43.             // MulitBulkReply 的后續行有兩種,BulkHeader 或者 BulkString 
  44.             if isBulkHeader(line) { 
  45.                 bulkLen = parseBulkHeader() 
  46.             } else { 
  47.                 // 我們正在讀取一個 BulkString, 它可能是 MulitBulkReply 或 BulkReply  
  48.                 args = append(args, line) 
  49.             } 
  50.             if len(args) == expectedArgsCount { // 我們已經讀取了所有后續行 
  51.                 // 通過 ch 返回結果 
  52.                 emitReply(ch) 
  53.                 // 重置狀態, 準備解析下一條 Reply 
  54.                 readingMultiLine = false 
  55.                 expectedArgsCount = 0 
  56.                 args = nil 
  57.                 bulkLen = 0 
  58.             } 
  59.         } 
  60.     } 

三、實現內存數據庫

至此我們已經搞定數據接收和解析的部分了,剩下就是我們應該把數據存在哪里了?

拋開持久化部分,作為基于內存的 KV 數據庫 Redis 的所有數據需要都存儲在內存中的哈希表,而這個哈希表就是我們今天需要編寫的最后一個組件。

與單線程的 Redis 不同我們實現的 Redis(godis)是并行工作的,所以我們必須考慮各種并發安全問題。常見的并發安全哈希表設計有幾種:

  • sync.map :Golang 官方提供的并發哈希表,適合讀多寫少的場景。但是在 m.dirty 剛被提升后會將 m.read 復制到新的 m.dirty 中,在數據量較大的情況下復制操作會阻塞所有協程,存在較大的隱患。

  • juc.ConcurrentHashMap :Java 的并發哈希表采用分段鎖實現。在進行擴容時訪問哈希表線程都將協助進行 rehash 操作,在 rehash 結束前所有的讀寫操作都會阻塞。因為緩存數據庫中鍵值對數量巨大且對讀寫操作響應時間要求較高,使用 juc 的策略是不合適的。

  • memcached hashtable :在后臺線程進行 rehash 操作時,主線程會判斷要訪問的哈希槽是否已被 rehash 從而決定操作 old_hashtable 還是操作 new_hashtable。這種設計被稱為 漸進式 rehash 它的優點是 rehash 操作基本不會阻塞主線程的讀寫,是最理想的的方案。

但漸進式 rehash 的實現非常復雜,所以 godis 采用 Golang 社區廣泛使用的分段鎖策略(非上面的三種),就是將 key 分散到固定數量的 shard 中避免進行整體 rehash 操作。shard 是有鎖保護的 map,當 shard 進行 rehash 時會阻塞 shard 內的讀寫,但不會對其他 shard 造成影響。

代碼如下:

  1. type ConcurrentDict struct { 
  2.     table []*Shard 
  3.     count int32 
  4.  
  5. type Shard struct { 
  6.     m     map[string]interface{} 
  7.     mutex sync.RWMutex 
  8.  
  9. func (dict *ConcurrentDict) spread(hashCode uint32) uint32 { 
  10.     tableSize := uint32(len(dict.table)) 
  11.     return (tableSize - 1) & uint32(hashCode) 
  12.  
  13. func (dict *ConcurrentDict) getShard(index uint32) *Shard { 
  14.     return dict.table[index] 
  15.  
  16. func (dict *ConcurrentDict) Get(key string) (val interface{}, exists bool) { 
  17.     hashCode := fnv32(key) 
  18.     index := dict.spread(hashCode) 
  19.     shard := dict.getShard(index) 
  20.     shard.mutex.RLock() 
  21.     defer shard.mutex.RUnlock() 
  22.     val, exists = shard.m[key] 
  23.     return 
  24.  
  25. func (dict *ConcurrentDict) Put(key string, val interface{}) (result int) { 
  26.     if dict == nil { 
  27.         panic("dict is nil"
  28.     } 
  29.     hashCode := fnv32(key) 
  30.     index := dict.spread(hashCode) 
  31.     shard := dict.getShard(index) 
  32.     shard.mutex.Lock() 
  33.     defer shard.mutex.Unlock() 
  34.  
  35.     if _, ok := shard.m[key]; ok { 
  36.         shard.m[key] = val 
  37.         return 0 
  38.     } else { 
  39.         shard.m[key] = val 
  40.         dict.addCount() 
  41.         return 1 
  42.     } 

ConcurrentDict 可以保證對單個 key 操作的并發安全性,但是仍然無法滿足并發安全的需求,舉例來說:

  1. 讀取 -> 做加法 -> 寫入 

因此我們需要實現 db.Locker 用于鎖定一個或一組 key 直到我們完成所有操作后再釋放。

實現 db.Locker 最直接的想法是使用一個 map[string]*sync.RWMutex

  • 加鎖過程分為兩步:初始化 mutex -> 加鎖
  • 解鎖過程也分為兩步: 解鎖 -> 釋放mutex

那么存在一個無法解決的并發問題:

時間 協程A 協程B
1   locker["a"].Unlock()
2 locker["a"] = &sync.RWMutex{}  
3   delete(locker["a"])
4 locker["a"].Lock()  

由于 t3 時協程 B 釋放了鎖,t4 時協程 A 試圖加鎖會失敗。若協程B在解鎖時不執行 delete(locker["a"]) 就可以避免該異常的發生,但是這樣會造成嚴重的內存泄露。

我們注意到哈希槽的數量遠少于 key 的數量,反過來說多個鍵可以共用一個哈希槽。所以我們不再直接對 key 進行加鎖而是鎖定 key 所在的哈希槽也可以保證安全,另一方面哈希槽數量較少即使不釋放也不會消耗太多內存。

  1. type Locks struct { 
  2.     table []*sync.RWMutex 
  3.  
  4. func Make(tableSize int) *Locks { 
  5.     table := make([]*sync.RWMutex, tableSize) 
  6.     for i := 0; i < tableSize; i++ { 
  7.         table[i] = &sync.RWMutex{} 
  8.     } 
  9.     return &Locks{ 
  10.         table: table, 
  11.     } 
  12.  
  13. func (locks *Locks)Lock(key string) { 
  14.     index := locks.spread(fnv32(key)) 
  15.     mu := locks.table[index] 
  16.     mu.Lock() 
  17.  
  18. func (locks *Locks)UnLock(key string) { 
  19.     index := locks.spread(fnv32(key)) 
  20.     mu := locks.table[index] 
  21.     mu.Unlock() 

在鎖定多個 key 時需要注意,若 協程A 持有 鍵a 的鎖試圖獲得 鍵b 的鎖,此時 協程B 持有 鍵b 的鎖試圖獲得 鍵a 的鎖則會形成死鎖。

解決方法是所有協程都按照相同順序加鎖,若兩個協程都想獲得 鍵a 和 鍵b 的鎖,那么必須先獲取 鍵a 的鎖后獲取 鍵b 的鎖,這樣就可以避免循環等待。

到目前為止構建 Redis 服務器所需的基本組件已經備齊,只需要將 TCP 服務器、協議解析器與哈希表組裝起來我們的 Redis 服務器就可以開始工作啦。

最后,以上代碼均簡化自我寫的開源項目 Godis:一個用 Go 語言實現的 Redis 服務器。期待您的關注和 Star:

項目地址: https://github.com/HDT3213/godis

結語

很多朋友的日常工作主要是編寫業務代碼,對于框架、數據庫、中間件這些“架構”、“底層代碼” 有一些恐懼感。

但本文我們只寫了 3 個組件,共計幾百行代碼就實現了一個基本的 Redis 服務器。所以底層的技術并不難,只要你對技術感興趣由淺入深、從簡到繁,“底層代碼”也并不神秘。

責任編輯:張燕妮 來源: 博客園
相關推薦

2021-06-17 08:22:23

服務器Go Redis

2018-12-11 10:43:09

Go語言 HTTP服務器

2024-02-28 08:47:29

文件系統數據

2020-03-31 20:23:46

C語言TCP服務器

2011-08-05 13:41:46

Go

2017-11-10 08:58:49

Web服務器應用程序

2017-09-13 14:46:42

服務器Go函數

2021-06-23 08:00:00

Redis服務器緩存

2017-09-14 08:43:05

2020-11-24 13:47:56

服務器

2010-05-19 15:00:37

IIS服務器

2012-12-19 10:09:19

微服務器云服務器

2023-03-28 07:46:46

go語言kubernetes

2019-05-08 14:37:49

Web服務器HTTP

2024-01-08 08:36:29

HTTPGo代理服務器

2016-07-04 16:21:54

服務器提速寶德“小超人”

2010-05-21 11:50:54

IIS服務器

2010-06-01 10:25:17

組裝服務器

2011-11-10 13:05:40

刀片服務器機架服務器采購指南

2018-05-18 09:43:37

服務器架構大型網站
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 韩国av影院 | 一区二区三区影院 | 亚洲国产日韩欧美 | 日日夜夜天天 | 日韩成人av在线播放 | 成人免费网视频 | 中文字幕av在线播放 | 久久久久久久久综合 | 欧美精品乱码久久久久久按摩 | 中文字幕 国产精品 | 精品视频一区在线 | 99精品久久久 | 国产精品国产a级 | 精品久久久久久久 | 国产综合网址 | 天天综合网永久 | 午夜在线| 中文字幕1区2区3区 亚洲国产成人精品女人久久久 | 91久操视频 | 午夜视频网站 | 日日干夜夜操天天操 | 国产日产久久高清欧美一区 | 欧美日韩国产在线 | 97天天干| 一级做受毛片免费大片 | 日韩中文一区二区三区 | 成av在线| 亚洲一区二区三区免费 | 欧美男人天堂 | 秋霞国产 | 中文字幕精品一区 | 麻豆国产一区二区三区四区 | 黄免费观看视频 | 日韩在线免费观看视频 | 日韩成人在线播放 | 免费看大片bbbb欧美 | 欧美久久久网站 | 国产午夜av片 | 亚洲欧美激情四射 | www.99热这里只有精品 | 色男人的天堂 |