go微服務系列之三

稀飯下雪發表於2017-10-25

在前兩篇系列博文中,我已經實現了user-srv、web-srv、api-srv,在新的一篇博文中,我要講解的是如何在專案中如何使用redis儲存session。如果想直接查閱原始碼或者通過demo學習的,可以訪問ricoder_demo

如何編寫一個微服務?這裡用的是go的微服務框架go micro,具體的情況可以查閱:btfak.com/%E5%BE%AE%E…

一、構建user-status.srv

1.1 構建UserStatus.proto

定義User的狀態操作函式,部分原始碼如下:

syntax = "proto3";

package pb;

service UserStatus {
//通過uid獲取session
rpc GetSessionByUID(GetSessionByUIDReq) returns (GetSessionByUIDRep) {}
//通過token獲取session
rpc GetSessionByToken(GetSessionByTokenReq) returns (GetSessionByTokenRep) {}
//獲取使用者的長連線地址
rpc GetConnectorAddr(GetConnectorAddrReq) returns (GetConnectorAddrRep) {}
//更新使用者長連線地址(使用者建立長連線時呼叫)
rpc UpdateConnectorAddr(UpdateConnectorAddrReq) returns (UpdateConnectorAddrRep) {}
//構建session使用者登入時呼叫,此介面會清除舊session
rpc NewSession(NewSessionReq) returns (NewSessionRep) {}
//移除session登出時會呼叫
rpc RemoveSession(RemoveSessionReq) returns (RemoveSessionRep) {}
//token續期
rpc RefreshSession(RefreshSessionReq) returns (RefreshSessionRep) {}
//更新使用者長連線地址(使用者建立長連線時呼叫)
rpc UserConnected(UserConnectedReq) returns (UserConnectedRep) {}
//刪除使用者的長連線地址(使用者長連線斷開時呼叫)
rpc UserDisonnected(UserDisonnectedReq) returns (UserDisonnectedRep) {}
//通過uid來移除session
rpc RemoveSessionByUID(RemoveSessionByUIDReq) returns (RemoveSessionByUIDRep) {}
//通過token找uid
rpc GetUserIDByToken(GetUserIDByTokenReq) returns (GetUserIDByTokenRep) {}
}
/*
還有一些定義,完整示例可以檢視原始碼~
*/複製程式碼
1.2 執行指令碼build_proto.sh自動構建userStatus.pb.go
$ bash ./build_proto.sh複製程式碼

這個build_proto.sh是我自己構建的一個指令碼檔案,執行之後會在/src/share/pb/資料夾下面生成一個userStatus.pb.go檔案

1.3 構建handler,實現userStatus中的函式

我在src資料夾下面新增一個user-status-srv資料夾,並在裡邊新增一個handler資料夾和utils資料夾,一個存放handler檔案,一個存放工具類函式,然後實現handler函式,原始碼如下:

package handler

import (
    //多個匯入包,具體請檢視原始碼
)
type UserStatusHandler struct {
    pool               *redis.Pool
    logger             *zap.Logger
    namespace          string
    sessionExpire int
    tokenExpire int
}

func NewUserStatusHandler(pool *redis.Pool) *UserStatusHandler {
    return &UserStatusHandler{
        pool: pool,
        sessionExpire: 15 * 86400,
        tokenExpire:   15 * 86400,
    }
}

//GetUserIDByToken GetUIDByToken
func (s *UserStatusHandler) GetUserIDByToken(ctx context.Context, req *pb.GetUserIDByTokenReq, rsp *pb.GetUserIDByTokenRep) error {
    return nil  
}
/*
還有其他函式的實現,完整示例可以檢視原始碼~
*/複製程式碼

這裡實現的函式全部先採用空實現,在後面會慢慢新增

1.4 實現main函式,啟動service

原始碼如下:

package main

import (
    //多個匯入包,具體檢視完整原始碼
)

func main() {

    // 建立Service,並定義一些引數
    service := micro.NewService(
        micro.Name(config.Namespace+"userStatus"),
        micro.Version("latest"),
    )
    // 定義Service動作操作
    service.Init(
        micro.Action(func(c *cli.Context) {
            log.Println("micro.Action test ...")
            // 註冊redis
            redisPool := share.NewRedisPool(3, 3, 1,300*time.Second,":6379","redis")
            // 先註冊db
            db.Init(config.MysqlDSN)
            pb.RegisterUserStatusHandler(service.Server(), handler.NewUserStatusHandler(redisPool), server.InternalHandler(true))
        }),
        micro.AfterStop(func() error {
            log.Println("micro.AfterStop test ...")
            return nil
        }),
        micro.AfterStart(func() error {
            log.Println("micro.AfterStart test ...")
            return nil
        }),
    )

    log.Println("啟動user-status-srv服務 ...")

    //啟動service
    if err := service.Run(); err != nil {
        log.Panic("user-status-srv服務啟動失敗 ...")
    }
}複製程式碼

由原始碼可以看出,我在啟動service之前先註冊了redis、db以及繫結handler,再通過Run啟動service。

1.5 檢視consul

在瀏覽器開啟 http://127.0.0.1:8500/ ,如果可以在頁面中看到對應的srv,則說明service啟動成功。如:

Screenshot from 2017-10-17 18-04-06.png
Screenshot from 2017-10-17 18-04-06.png

二、使用redis

在這一章節中,我將採用redis實現資料的存取。

2.1 新建一個redis.Pool

在main.go函式中,我使用 *share.NewRedisPool(3, 3, 1,300time.Second,":6379","redis") 得到了一個redisPool,NewRedisPool原始碼如下:

func NewRedisPool(maxIdle, maxActive , DBNum int, timeout time.Duration, addr , password string) *redis.Pool {

    return &redis.Pool{
        MaxActive:   maxActive,
        MaxIdle:     maxIdle,
        IdleTimeout: timeout,
        Wait:        true,
        Dial: func() (redis.Conn, error) {
            // return redis.DialURL(rawurl)
            // return redis.Dial("tcp", addr, redis.DialPassword(password), redis.DialDatabase(dbNum))
            return redis.Dial("tcp", addr, redis.DialPassword(password), redis.DialDatabase(DBNum))
        },
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            _, err := c.Do("PING")
            return err
        },
    }
}複製程式碼

在這裡我使用的是第三方開源框架,有興趣的可以檢視 github.com/garyburd/re… 瞭解情況。

2.2 使用redis存取資料

在這裡我以NewSession為例,原始碼如下:

func (s *UserStatusHandler) NewSession(ctx context.Context, req *pb.NewSessionReq, rsp *pb.NewSessionRep) error {
    var oldSession *pb.Session
    defer func() {
        utils.SessionFree(oldSession)
    }()
    fieldMap := make(map[string]interface{}, 0)
    fieldMap["Uid"] = req.Id
    fieldMap["Address"] = req.Address
    fieldMap["Phone"] = req.Phone
    fieldMap["Name"] = req.Name
    //生成Token
    token, err := utils.NewToken(req.Id)
    if err != nil {
        log.Println("生成token失敗", zap.Error(err), zap.Int32("uid", req.Id))
        return err
    }

    //刪除所有舊token
    if err = utils.RemoveUserSessions(req.Id, s.pool); err != nil {
        log.Println("刪除所有舊token失敗", zap.Error(err), zap.Int32("uid", req.Id))
        return err
    }
    conn := s.pool.Get()
    //會話資料寫入redis,格式:t:id => map的雜湊值
    if _, err := conn.Do("HMSET", redis.Args{}.Add(utils.KeyOfSession(req.Id)).AddFlat(fieldMap)...); err != nil {
        conn.Close()
        log.Println("會話資料寫入redis失敗", zap.Error(err), zap.String("key", utils.KeyOfSession(req.Id)), zap.Any("引數", fieldMap))
        return err
    }
    //設定t:id的過期時間
    if _, err := conn.Do("EXPIRE", utils.KeyOfSession(req.Id), s.sessionExpire); err != nil {
        conn.Close()
        s.logger.Error("設定session過期時間失敗", zap.Error(err), zap.String("key", utils.KeyOfSession(req.Id)))
        return err
    }

    //使用者token寫入set裡邊,格式:t:uid:set:id => token
    keyOfSet := utils.KeyOfSet(req.Id)
    if _, err = conn.Do("SADD", keyOfSet, token); err != nil {
        conn.Close()
        log.Println("token寫入使用者集合失敗", zap.Error(err), zap.String("key", keyOfSet), zap.String("引數", token))
        return err
    }
    //設定t:uid:set:id的過期時間
    if _, err = conn.Do("EXPIRE", keyOfSet, s.sessionExpire); err != nil {
        conn.Close()
        log.Println("設定使用者token集合過期時間失敗", zap.Error(err), zap.String("key", keyOfSet))
        return err
    }

    //將token和id對應,格式:token => id
    if _, err = conn.Do("SETEX", utils.KeyOfToken(token), s.tokenExpire, req.Id); err != nil {
        conn.Close()
        log.Println("token寫入redis失敗", zap.Error(err), zap.String("key", utils.KeyOfToken(token)), zap.Int32("引數", req.Id))
        return err
    }

    rsp.Token = token
    return nil
}複製程式碼

如程式碼所示,操作redis的步驟是 conn := s.pool.Get() 先開啟一個連線,再通過conn.Do("EXPIRE", keyOfSet, s.sessionExpire) 的一種方式操作redis中的資料,具體的可以檢視redis的api,這裡有個函式 utils.SessionFree(oldSession) ,這是我在utils包下自定義的一個函式,這個知識點再接下來的知識點中會有涉及。

三、額外講解sync.Pool

我在專案中使用了sync.pool儲存session物件,目的是為了儲存和複用session這個臨時物件,以減少記憶體分配,減低gc壓力,那麼sync.Pool是什麼呢?以下是官方給出的解釋(自己翻譯的):

  • Pool是一個可以存取臨時物件的集合。
  • Pool中儲存的item都可能在沒有任何通知的情況下被自動釋放掉,即如果Pool持有該物件的唯一引用,這個item就可能被回收。
  • Pool在被多個執行緒使用的情況下是安全的。
  • Pool的目的是快取分配了但是未使用的item用於之後的重用,以減輕GC的壓力。也就是說,pool讓建立高效的並且執行緒安全的空閒列表更加容易,不過Pool並不適用於所有空閒列表。
  • Pool的合理用法是用於管理一組被多個獨立併發執行緒共享並可能重用的臨時item。Pool提供了讓多個執行緒分攤記憶體申請消耗的方法。
  • Pool比較經典的一個例子在fmt包裡,該Pool維護一個動態大小的臨時輸出快取倉庫,該倉庫會在過載(許多執行緒活躍的列印時)增大,在沉寂時縮小。
  • 另一方面,管理著短壽命物件的空閒列表不適合使用Pool,因為這種情況下記憶體申請消耗不能很好的分配。這時應該由這些物件自己實現空閒列表。

以下是Pool的資料型別:

type Pool struct {
    noCopy noCopy

    local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
    localSize uintptr        // size of the local array

    // New optionally specifies a function to generate
    // a value when Get would otherwise return nil.
    // It may not be changed concurrently with calls to Get.
    New func() interface{}
}

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

type poolLocal struct {
    poolLocalInternal

    // Prevents false sharing on widespread platforms with
    // 128 mod (cache line size) = 0 .
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}複製程式碼

由註釋我們也可以看出,其中的local成員的真實型別是一個poolLocal陣列,而localSize是陣列長度,poolLocal是真正儲存資料的地方。private儲存了一個臨時物件,shared是儲存臨時物件的陣列,而從private和shared的註釋我們也可以看出,一個是屬於特定的P私有的,一個是屬於所有的P,至於這個P是什麼,可以自行參考golang的排程模型,後期我也會專門寫一篇相關的部落格。其次,Pool是給每個執行緒分配了一個poolLocal物件,就是說local陣列的長度,就是工作執行緒的數量(size := runtime.GOMAXPROCS(0))。當多執行緒在併發讀寫的時候,通常情況下都是在自己執行緒的poolLocal中存取資料,而只有當自己執行緒的poolLocal中沒有資料時,才會嘗試加鎖去其他執行緒的poolLocal中“偷”資料。

我們可以看看Get函式,原始碼如下:

func (p *Pool) Get() interface{} {
    if race.Enabled {
        race.Disable()
    }
    l := p.pin()
    x := l.private
    l.private = nil
    runtime_procUnpin()
    if x == nil {
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
        }
        l.Unlock()
        if x == nil {
            x = p.getSlow()
        }
    }
    if race.Enabled {
        race.Enable()
        if x != nil {
            race.Acquire(poolRaceAddr(x))
        }
    }
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}複製程式碼

這個函式的原始碼並不難讀,在呼叫Get的時候首先會先在local陣列中獲取當前執行緒對應的poolLocal物件,然後再從poolLocal物件中獲取private中的資料,如果private中有資料,則取出來直接返回。如果沒有則先鎖住shared,然後從shared中取出資料後直接返回,如果還是沒有則呼叫getSlow函式。那麼為什麼這裡要鎖住shared呢?答案我們可以在getSlow中找到,因為當shared中沒有資料的時候,會嘗試去其他的poolLocal的shared中偷資料。

    // See the comment in pin regarding ordering of the loads.
    size := atomic.LoadUintptr(&p.localSize) // load-acquire
    local := p.local                         // load-consume
    // Try to steal one element from other procs.
    pid := runtime_procPin()
    runtime_procUnpin()
    for i := 0; i < int(size); i++ {
        l := indexLocal(local, (pid+i+1)%int(size))
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
            l.Unlock()
            break
        }
        l.Unlock()
    }
    return x複製程式碼

tip:該專案的原始碼(包含資料庫的增刪查改的demo)可以檢視 原始碼

有興趣的可以關注我的個人公眾號 ~

qrcode_for_gh_04e57fbebd02_258.jpg
qrcode_for_gh_04e57fbebd02_258.jpg

相關文章