Golang?Redis連接池實(shí)現(xiàn)原理及示例探究
引言
用11篇文章實(shí)現(xiàn)一個(gè)可用的Redis服務(wù),姑且叫EasyRedis吧,希望通過文章將Redis掰開撕碎了呈現(xiàn)給大家,而不是僅僅停留在八股文的層面,并且有非常爽的感覺,歡迎持續(xù)關(guān)注學(xué)習(xí)。
項(xiàng)目代碼地址: https://github.com/gofish2020/easyredis
- [x] easyredis之TCP服務(wù)
- [x] easyredis之網(wǎng)絡(luò)請(qǐng)求序列化協(xié)議(RESP)
- [x] easyredis之內(nèi)存數(shù)據(jù)庫
- [x] easyredis之過期時(shí)間 (時(shí)間輪實(shí)現(xiàn))
- [x] easyredis之持久化 (AOF實(shí)現(xiàn))
- [x] easyredis之發(fā)布訂閱功能
- [x] easyredis之有序集合(跳表實(shí)現(xiàn))
- [x] easyredis之 pipeline 客戶端實(shí)現(xiàn)
- [x] easyredis之事務(wù)(原子性/回滾)
- [x] easyredis之連接池
- [ ] easyredis之分布式集群存儲(chǔ)
Redis之連接池
通過本篇可以學(xué)到什么?
通道的應(yīng)用
連接池的封裝
從本篇開始,實(shí)現(xiàn)分布式相關(guān)的代碼。既然是分布式,那么redis key
就會(huì)分布(分散)在不同的集群節(jié)點(diǎn)上。
當(dāng)客戶端發(fā)送set key value
命令給Redis0
服務(wù),通過hash計(jì)算如果該key應(yīng)該保存在Redis2
服務(wù),那么Redis0
就要連接Redis2
服務(wù),并將命令轉(zhuǎn)發(fā)給Redis2
進(jìn)行處理。
在命令的轉(zhuǎn)發(fā)的過程中,需要頻繁的連接分布式節(jié)點(diǎn),所以我們需要先實(shí)現(xiàn)連接池的基本功能,復(fù)用連接。
在第八篇pipeline客戶端我們已經(jīng)實(shí)現(xiàn)了客戶端連接,本篇需要實(shí)現(xiàn)一個(gè)池子的功能將已經(jīng)使用完的連接緩存起來,等到需要使用的時(shí)候,再取出來繼續(xù)使用。
代碼路徑tool/pool/pool.go
,代碼量160行
池子結(jié)構(gòu)體定義
既然是池子,那定義的數(shù)據(jù)結(jié)構(gòu)里面肯定要有個(gè)緩沖的變量,這里就是
idles chan any
一開始池子中肯定是沒有對(duì)象的,所以需要有個(gè)能夠創(chuàng)建對(duì)象的函數(shù)
newObject
配套有個(gè)釋放對(duì)象的函數(shù)
freeObject
池子中的對(duì)象不可能讓他無限的增多,當(dāng)達(dá)到
activeCount
個(gè)對(duì)象的時(shí)候,就不再繼續(xù)用newObject
生成新對(duì)象,需要等之前的對(duì)象回收以后,才能獲取到對(duì)象(這里不理解往下繼續(xù)看)
type Pool struct { Config // 創(chuàng)建對(duì)象 newObject func() (any, error) // 釋放對(duì)象 freeObject func(x any) // 空閑對(duì)象池 idles chan any mu sync.Mutex activeCount int// 已經(jīng)創(chuàng)建的對(duì)象個(gè)數(shù) waiting []chan any // 阻塞等待 closed bool// 是否已關(guān)閉 } func NewPool(new func() (any, error), free func(x any), conf Config) *Pool { ifnew == nil { logger.Error("NewPool argument new func is nil") returnnil } if free == nil { free = func(x any) {} } p := Pool{ Config: conf, newObject: new, freeObject: free, activeCount: 0, closed: false, } p.idles = make(chan any, p.MaxIdles) return &p }
從池子中獲取對(duì)象
p.mu.Lock()
加鎖(race condition)- 從空閑緩沖
idles
中獲取一個(gè)之前緩沖的對(duì)象 - 如果沒有獲取到就調(diào)用
p.getOne()
新創(chuàng)建一個(gè) - 在
func (p *Pool) getOne() (any, error)
函數(shù)中,會(huì)判斷當(dāng)前池子中是否(歷史上)已經(jīng)創(chuàng)建了足夠多的對(duì)象p.activeCount >= p.Config.MaxActive
,那就不創(chuàng)建新對(duì)象,阻塞等待回收;否則調(diào)用newObject
函數(shù)創(chuàng)建新對(duì)象
func (p *Pool) Get() (any, error) { p.mu.Lock() if p.closed { p.mu.Unlock() returnnil, ErrClosed } select { case x := <-p.idles: // 從空閑中獲取 p.mu.Unlock() // 解鎖 return x, nil default: return p.getOne() // 獲取一個(gè)新的 } } func (p *Pool) getOne() (any, error) { // 說明已經(jīng)創(chuàng)建了太多對(duì)象 if p.activeCount >= p.Config.MaxActive { wait := make(chan any, 1) p.waiting = append(p.waiting, wait) p.mu.Unlock() // 阻塞等待 x, ok := <-wait if !ok { returnnil, ErrClosed } return x, nil } p.activeCount++ p.mu.Unlock() // 創(chuàng)建新對(duì)象 x, err := p.newObject() if err != nil { p.mu.Lock() p.activeCount-- p.mu.Unlock() returnnil, err } return x, nil }
池子對(duì)象回收
回收的過程就是對(duì)象緩存的過程,當(dāng)然也要有個(gè)“度”
先加鎖
回收前先判斷是否有阻塞等待回收len(p.waiting) > 0
,這里的邏輯和上面的等待阻塞邏輯對(duì)應(yīng)起來了
如果沒有阻塞等待的,那就直接將對(duì)象保存到緩沖中idles
中
- 這里還有一個(gè)邏輯,緩沖有個(gè)大小限制(不可能無限的緩沖,多余不使用的對(duì)象,我們將它釋放了,占用內(nèi)存也沒啥意義)
func (p *Pool) Put(x any) { p.mu.Lock() if p.closed { p.mu.Unlock() p.freeObject(x) // 直接釋放 return } //1.先判斷等待中 iflen(p.waiting) > 0 { // 彈出一個(gè)(從頭部) wait := p.waiting[0] temp := make([]chan any, len(p.waiting)-1) copy(temp, p.waiting[1:]) p.waiting = temp wait <- x // 取消阻塞 p.mu.Unlock() return } // 2.直接放回空閑緩沖 select { case p.idles <- x: p.mu.Unlock() default: // 說明空閑已滿 p.activeCount-- // 對(duì)象個(gè)數(shù)-1 p.mu.Unlock() p.freeObject(x) // 釋放 } }
再次封裝(socket連接池)
上面的代碼已經(jīng)完全實(shí)現(xiàn)了一個(gè)池子的功能;但是我們?cè)趯?shí)際使用的時(shí)候,每個(gè)ip地址對(duì)應(yīng)一個(gè)連接池,所以這里又增加了一個(gè)結(jié)構(gòu)體RedisConnPool
,結(jié)合上面的池子功能,再配合之前的pipleline客戶端的功能,實(shí)現(xiàn)socket連接池。
代碼路徑:cluster/conn_pool.go
代碼邏輯:
用一個(gè)字典key表示ip地址,value表示上面實(shí)現(xiàn)的池對(duì)象
GetConn
獲取一個(gè)ip地址對(duì)應(yīng)的連接ReturnConn
歸還連接到連接池中
type RedisConnPool struct { connDict *dict.ConcurrentDict // addr -> *pool.Pool } func NewRedisConnPool() *RedisConnPool { return &RedisConnPool{ connDict: dict.NewConcurrentDict(16), } } func (r *RedisConnPool) GetConn(addr string) (*client.RedisClent, error) { var connectionPool *pool.Pool // 對(duì)象池 // 通過不同的地址addr,獲取不同的對(duì)象池 raw, ok := r.connDict.Get(addr) if ok { connectionPool = raw.(*pool.Pool) } else { // 創(chuàng)建對(duì)象函數(shù) newClient := func() (any, error) { // redis的客戶端連接 cli, err := client.NewRedisClient(addr) if err != nil { returnnil, err } // 啟動(dòng) cli.Start() if conf.GlobalConfig.RequirePass != "" { // 說明服務(wù)需要密碼 reply, err := cli.Send(aof.Auth([]byte(conf.GlobalConfig.RequirePass))) if err != nil { returnnil, err } if !protocol.IsOKReply(reply) { returnnil, errors.New("auth failed:" + string(reply.ToBytes())) } return cli, nil } return cli, nil } // 釋放對(duì)象函數(shù) freeClient := func(x any) { cli, ok := x.(*client.RedisClent) if ok { cli.Stop() // 釋放 } } // 針對(duì)addr地址,創(chuàng)建一個(gè)新的對(duì)象池 connectionPool = pool.NewPool(newClient, freeClient, pool.Config{ MaxIdles: 1, MaxActive: 20, }) // addr -> *pool.Pool r.connDict.Put(addr, connectionPool) } // 從對(duì)象池中獲取一個(gè)對(duì)象 raw, err := connectionPool.Get() if err != nil { returnnil, err } conn, ok := raw.(*client.RedisClent) if !ok { returnnil, errors.New("connection pool make wrong type") } return conn, nil } func (r *RedisConnPool) ReturnConn(peer string, cli *client.RedisClent) error { raw, ok := r.connDict.Get(peer) if !ok { return errors.New("connection pool not found") } raw.(*pool.Pool).Put(cli) returnnil }
以上就是Golang實(shí)現(xiàn)Redis之連接池的詳細(xì)內(nèi)容,更多關(guān)于Golang Redis連接池的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go實(shí)現(xiàn)替換(覆蓋)文件某一行內(nèi)容的示例代碼
本文主要介紹了Go實(shí)現(xiàn)替換(覆蓋)文件某一行內(nèi)容的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Go語言for range(按照鍵值循環(huán))遍歷操作
這篇文章主要介紹了Go語言for range(按照鍵值循環(huán))遍歷操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12詳解Go開發(fā)Struct轉(zhuǎn)換成map兩種方式比較
本篇文章主要介紹了詳解Go開發(fā)Struct轉(zhuǎn)換成map兩種方式比較,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過程詳解
這篇文章主要介紹了Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過程詳解,大概思路是在Go的結(jié)構(gòu)體中每個(gè)屬性打上一個(gè)excel標(biāo)簽,利用反射獲取標(biāo)簽中的內(nèi)容,作為表格的Header,需要的朋友可以參考下2022-06-06一文教你如何快速學(xué)會(huì)Go的struct數(shù)據(jù)類型
結(jié)構(gòu)是表示字段集合的用戶定義類型。它可以用于將數(shù)據(jù)分組為單個(gè)單元而不是將每個(gè)數(shù)據(jù)作為單獨(dú)的值的地方。本文就來和大家聊聊Go中struct數(shù)據(jù)類型的使用,需要的可以參考一下2023-03-03