基於 golang 的實現簡單的 RPC 版 Watch 功能

本文主要參考《Go 語言高級編程》一書!
在很多系統中都提供了 Watch 監視功能的接口,當系統滿足某種條件時 Watch 方法返回監控的結果。在這裏我們可以嘗試通過 RPC 框架實現一個基本的 Watch 功能。因爲 RPC client.send 方法是線程安全的,我們可以通過在不同的 Goroutine 中同時併發阻塞調用 RPC 方法,通過在一個獨立的 Goroutine 中調用 Watch 函數進行監控。

package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"net"
	"net/rpc"
	"sync"
	"time"
)

// 簡單的內存 KV 數據庫
type KVStoreService struct {
	m      map[string]string           // 存儲數據
	filter map[string]func(key string) // Watch 調用時的過濾器函數列表
	mu     sync.Mutex
}

func NewKVStoreService() *KVStoreService {
	return &KVStoreService{
		m:      make(map[string]string),
		filter: make(map[string]func(key string)),
	}
}

func (p *KVStoreService) Get(key string, value *string) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	if v, ok := p.m[key]; ok {
		*value = v
		return nil
	}
	return errors.New("not found")
}

// 輸入參數是 key 和 value 組成的數組,匿名結構體則表示忽略輸出參數
func (p *KVStoreService) Set(kv [2]string, reply *struct{}) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	key, value := kv[0], kv[1]
	oldValue := p.m[key]
	// 當修改 key 對應的 value 時,調用每一個過濾器函數
	if oldValue != value {
		for _, fn := range p.filter {
			fn(key)
		}
	}
	// 更新
	p.m[key] = value
	return nil
}

func (p *KVStoreService) Watch(timeoutSecond int, keyChanged *string) error {
	id := fmt.Sprintf("watch-%s-%03d", time.Now(), rand.Int())
	ch := make(chan string, 10)
	p.mu.Lock()
	// 註冊過濾器函數
	p.filter[id] = func(key string) {
		ch <- key
	}
	p.mu.Unlock()
	select {
	// 是否超時
	case <-time.After(time.Duration(timeoutSecond) * time.Second):
		return errors.New("timeout")
	case key := <-ch:
		*keyChanged = key
		return nil
	}
	return nil
}

func main() {
	// 將 KVStoreService 對象註冊爲一個 RPC 服務
	// 將對象中所有滿足 RPC 規則的對象方法註冊爲 RPC 函數
	// 所有註冊的方法會放在 “KVStoreService” 服務空間執行
	_ = rpc.RegisterName("KVStoreService", NewKVStoreService())
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}
	conn, err := listener.Accept()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	// 在該 TCP 連接上爲對方提供 RPC 服務
	rpc.ServeConn(conn)
}

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
	quit := make(chan struct{})
	client, err := rpc.Dial("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}
	// 啓動獨立的 Goroutine 監控 key 的變化,同步阻塞,直到有 key 發生變化或者超時
	go func() {
		var keyChanged string
		err := client.Call("KVStoreService.Watch", 30, &keyChanged)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("watch: ", keyChanged)
		quit <- struct{}{}
	}()
	var key string
	// 獲取某個 key 的值
	err = client.Call("KVStoreService.Get", "abc", &key)
	if err != nil {
		log.Println(err)
	} else {
		fmt.Println("get key: ", key)
	}
	// 設置某個 key 的值,因爲原來對應的爲空,調用該方法,會觸發 Watch 返回
	err = client.Call("KVStoreService.Set", [2]string{"abc", "abc-value"}, new(struct{}))
	if err != nil {
		log.Fatal(err)
	}
	// 再次 key 的值
	err = client.Call("KVStoreService.Get", "abc", &key)
	if err != nil {
		log.Println(err)
	} else {
		fmt.Println("get key: ", key)
	}
	<-quit
}

首先啓動一個獨立的 Goroutine 監控 key 的變化。同步的watch調用會阻塞, 直到有 key 發生變化或者超時。然後在通過 Set 方法修改 KV 值時,服務器會將變化的 key 通過 Watch 方法返回。 這樣我們就可以實現對某些狀態的監控。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章