租約-代碼實踐

租約

本文主要根據租約的基本原理,採用go語言實踐一下,租約的基本流程。

租約設計概述

用例模型

租約的主要機制就是爲了保證在分佈式環境下使得各個客戶端使用的數據保持強一致性,每個客戶端在查詢服務器數據的時候,都在服務端存在一個租約信息,如果服務端還有租約沒有到期,則客戶端提交的數據修改阻塞到所有的租約過期菜可進行操作。基本的用例場景描述分爲如下幾種。

客戶端查詢數據用例
用例名稱 客戶端查詢數據
主要參與者 客戶端,服務端
涉及關注點 客戶端:客戶端需要知道的數據在客戶端本地緩存沒有找到,需要向服務端請求查詢數據
前置條件 服務器正常運行
後置條件
基本流程 情況1:當前沒有阻塞的客戶端需要修改數據,此時就直接返回數據;情況2:當前服務端已經存在客戶端需要修改數據的請求,此時需要阻塞等待該客戶端完成修改之後,將修改之後的數據進行返回
客戶端修改數據
用例名稱 客戶端修改數據
主要參與者 客戶端,服務端
涉及關注點 客戶端:客戶端需要將要需要的數據發送到服務端
前置條件 服務器正常運行
後置條件
基本流程 情況1:當前客戶端修改數據的請求發送到服務端的時候,此時服務端沒有租約則直接阻塞其他讀請求並修改數據,修改完成之後返回數據;情況2:當前服務端還有租約未到期,此時阻塞所有的讀請求並阻塞所有後續的修改請求,等到所有租約到期之後,此時修改完成數據,完成之後再返回數據,其他則阻塞的請求繼續執行
時序圖
根據用例模型查詢數據與修改數據的時序圖
服務端客戶端A客戶端B客戶端C連接服務端返回連接成功連接服務端返回連接成功連接服務端返回連接成功查詢數據設置A的租約並返回數據查詢數據設置B的租約並返回數據查詢數據設置C的租約並返回數據修改數據因爲此時A B C三個租約未到期等到租約到期待到租約全部到期並修改數據後返回數據服務端客戶端A客戶端B客戶端C
代碼設計

代碼設計過程中,由於水平限制,代碼的設計過程會有瑕疵。儘量按照有關面向對象的設計原則來。主要包含了三個文件,分別爲server.go(服務端代碼主要邏輯)、lease_library.go(客戶端調用內容的實現)、protocol.go(傳輸內容的序列號與反序列化)、get_op.go(查詢的測試代碼)、set_op(修改值的測試代碼)。代碼的目的主要是爲了實踐一下流程,所以協議的編寫與支持的命令都相對簡單,當前對go語言的編寫不熟練導致代碼中異常的處理會不規範,後續再深入學習並修改。

服務端代碼
package main

import (
	"bytes"
	"fmt"
	"log"
	"net"
	"sync"
	"time"
)

func init() {
	log.SetFlags(log.Ltime|log.Lshortfile)
}


var StoreValues map[string]string



type ServerLeases struct {
	mutex sync.Mutex
	cond *sync.Cond
	waitChangeFlag bool
	clientNums int
	modfiyChan chan bool
}

func(self *ServerLeases) getWaitStatus()bool{
	return self.waitChangeFlag
}

func(self *ServerLeases) updateWaitStatus(status bool){
	self.waitChangeFlag = status
}

func(self *ServerLeases) checkLeases(){
	self.mutex.Lock()
	self.clientNums -= 1
	log.Println("check leases  ", self.clientNums)
	if self.waitChangeFlag == true && self.clientNums == 0 {
		log.Println("send chan to block chan modfiyChan")
		self.modfiyChan <- true
	}
	self.mutex.Unlock()
}

func(self *ServerLeases) addLeasesTimer(){
	self.mutex.Lock()
	self.clientNums += 1
	time.AfterFunc(12*time.Second, self.checkLeases)
	log.Println("current clientNums ", self.clientNums)
	self.mutex.Unlock()
}

func(self *ServerLeases) waitLeasesTimeout(){
	self.cond.L.Lock()
	self.cond.Wait()
	self.cond.L.Unlock()
}

func(self *ServerLeases) notifyWaitLeasesClients(){
	self.cond.L.Lock()
	self.cond.Broadcast()
	self.cond.L.Unlock()
}

var serverLeases ServerLeases

type ClientServer struct {
	conn net.Conn
	recvData string
	sendData []byte
	writeChan chan bool
}


func(self *ClientServer) init(c net.Conn){
	self.conn = c
	self.recvData = ""
	self.sendData = []byte("")
	self.writeChan = make(chan bool)

	// 開啓協程啓動執行 一個讀 一個寫
	go self.HandleReadEvent()
	go self.SendData()
}

func(self *ClientServer) Dispatch(params map[string]interface{}){
	if params == nil {
		return
	}

	// 發送回客戶端
	var sendMap map[string]interface{}
	sendMap = make(map[string]interface{})

	method, ok := params["method"]
	if ok != true {
		return
	}

	// 檢查服務端狀態 加鎖

	if method == "query" {
		serverLeases.mutex.Lock()
		wait_status := serverLeases.getWaitStatus()
		log.Println("get query wait status ", wait_status)
		// 添加到租約列表中
		// 如果當前有修改操作則阻塞當前查詢操作
		if wait_status == true {
			log.Println("wait .... and release lock")
			serverLeases.mutex.Unlock()
			serverLeases.waitLeasesTimeout()
			log.Println("notify from and  continue")
		} else {
			serverLeases.mutex.Unlock()
		}
		serverLeases.addLeasesTimer()

		sendMap["data"] = StoreValues
	} else if method == "modfiy" {
		// 修改操作則檢查當前是否有客戶端有租約
		serverLeases.mutex.Lock()
		serverLeases.updateWaitStatus(true)
		serverLeases.mutex.Unlock()
		log.Println("wait all leases expire  ", serverLeases)
		if serverLeases.clientNums > 0 {
			log.Println("wait modfiy chan")
			<- serverLeases.modfiyChan
		}
		log.Println("modfiy chan start  ")

		// 修改數據
		serverLeases.mutex.Lock()
		log.Println("-----------------------")
		log.Println("modfiy data ", params)
		datas := params["data"]
		var modfiyData map[string]interface{}
		modfiyData = make(map[string]interface{})
		modfiyData = datas.(map[string]interface{})
		for k, v := range modfiyData{
			StoreValues[k] = v.(string)
		}
		log.Println("modify after ", StoreValues)
		sendMap["data"] = StoreValues
		serverLeases.updateWaitStatus(false)
		serverLeases.mutex.Unlock()

		// 喚醒 其他等待協程
		serverLeases.notifyWaitLeasesClients()
	}


	sendData := SerializeProtocol(sendMap)

	// 更新發送緩衝區
	var buffer bytes.Buffer
	buffer.Write(self.sendData)
	buffer.Write(sendData)
	self.sendData = buffer.Bytes()

	self.writeChan <- true
}

func(self *ClientServer) HandleRecv(){
	// 清空接受緩衝區
	var result map[string]interface{}
	self.recvData, result = DeserializeProtocol(self.recvData)

	// 處理業務邏輯
	if result == nil {
		return
	}
	self.Dispatch(result)
}

func(self *ClientServer) SendData(){
	for {
		_, ok := <-self.writeChan
		if ok == false{
			log.Println("writeChan error ", ok)
			return
		}
		for len(self.sendData) > 0 {
			n, err := self.conn.Write(self.sendData)
			if err != nil {
				fmt.Println("client write error ", err)
				self.Close()
				return
			}
			self.sendData = self.sendData[n:]
		}
	}
}

func(self *ClientServer) HandleReadEvent(){
	var recvByte = make([]byte, 20)
	for {
		// 優先發送數據返回

		n, err := self.conn.Read(recvByte)
		if err != nil {
			fmt.Println("client recv error ", err)
			self.Close()
			return
		}
		self.recvData += string(recvByte[:n])
		self.HandleRecv()

	}
}

func(self *ClientServer) Close(){
	self.conn.Close()
	close(self.writeChan)
}


func main() {
	fmt.Println("start")
	StoreValues = make(map[string]string)
	StoreValues["store1"] = "stor1_value"
	StoreValues["store2"] = "stor2_value"

	var new_mutex sync.Mutex
	serverLeases = ServerLeases{}
	serverLeases.mutex = new_mutex
	serverLeases.cond = sync.NewCond(&sync.Mutex{})
	serverLeases.modfiyChan = make(chan bool)
	serverLeases.clientNums = 0
	serverLeases.waitChangeFlag = false

	server, err := net.Listen("tcp", "127.0.0.1:7070")
	if err != nil{
		fmt.Println("listen error :", err)
		return
	}
	for {
		conn, err := server.Accept()
		if err != nil{
			fmt.Println("accept error: ", err)
			continue
		}
		c := ClientServer{}
		c.init(conn)
	}
}

服務端代碼量不多,其主要的實現思想,就是將每一個新加入的conn包裝成一個結構體,該結構體ClientServer通過該結構體來實現數據的查詢與修改,接着在服務端實現了一個全局的管理租約的ServerLeases結構體,所有加入的連接都通過該結構體的條件變量來保證在阻塞之後能夠被喚醒,並通過加鎖來實現數據的修改的原子性,併爲每個已經發送的租約設置定時回調,保證租約過期後通過阻塞的需要修改數據的協程依次完成事件通知。

協議代碼
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
)

/*

protocol

len\r\n{"method":"test","data":"val"}

通過長度與json來進行數據的傳入與業務邏輯的處理

 */

var PREFIX = "\r\n"


func SerializeProtocol(data map[string]interface{})[]byte{
	// 序列化
	result, err := json.Marshal(data)
	if err != nil {
		fmt.Println("error encoding json ", err)
		return []byte("")
	}
	length := len(result)
	var buffer bytes.Buffer
	buffer.Write([]byte(strconv.Itoa(length)+PREFIX))
	buffer.Write(result)

	return buffer.Bytes()
}

func DeserializeProtocol(val string)(left string, result map[string]interface{}){
	// 解析  各種異常需要待處理
	left = val

	if len(val) == 0{
		return
	}
	pos := strings.Index(val, PREFIX)
	if pos == -1 {
		fmt.Println("not found")
		return
	}

	len_str := val[:pos]
	length, err := strconv.Atoi(len_str)
	if err != nil {
		fmt.Println("length con error ", err)
		return
	}

	data := val[(pos + len(PREFIX)):]
	if data == "" {
		fmt.Println("string have not enougth length")
		return
	}
	if len(data) >= length {
		cur_recv := []byte(data[:length])
		left = data[length:]
		result = make(map[string]interface{})
		if ok := json.Valid(cur_recv); ok{
			err = json.Unmarshal([]byte(data), &result)
			if err != nil{
				fmt.Println("json ummarshal error", err)
				return
			}
		}
	}
	return
}


協議的設計相對簡單,主要的設計格式都是通過頭部保存數據長度通過\r\n來做分割符,數據內容就是json的字符串內容,所有保證了通過json可以保證較大的靈活度。當前支持的命令就只有query和modfiy兩個處理過程。

客戶端代碼
package main

import (
	"bytes"
	"errors"
	"fmt"
	"log"
	"net"
	"sync"
	"time"
)

func init() {
	log.SetFlags(log.Ltime|log.Lshortfile)
}

type LeaseClient struct {
	conn net.Conn
	msgChan chan bool
	cacheData map[string]interface{}
	mutex sync.Mutex

	recvData string
	sendData []byte
}

func(self *LeaseClient) HandleReadEvent(){
	var recvByte = make([]byte, 20)
	for {
		// 優先發送數據返回

		n, err := self.conn.Read(recvByte)
		if err != nil {
			fmt.Println("client recv error ", err)
			return
		}
		self.recvData += string(recvByte[:n])
		self.HandleRecv()

	}
}

func(self *LeaseClient) HandleRecv(){
	// 清空接受緩衝區
	var result map[string]interface{}
	self.recvData, result = DeserializeProtocol(self.recvData)

	// 處理業務邏輯
	if result == nil {
		return
	}
	self.cacheData = result["data"].(map[string]interface{})
	self.msgChan <- true
}

func(self *LeaseClient) sendDataToServer(sendBuffer []byte){
	var buffer bytes.Buffer
	buffer.Write(self.sendData)
	buffer.Write(sendBuffer)
	self.sendData = buffer.Bytes()

	for len(self.sendData) > 0 {
		n, err := self.conn.Write(self.sendData)
		if err != nil {
			fmt.Println("client write error ", err)
			return
		}
		self.sendData = self.sendData[n:]
	}
}

func(self *LeaseClient) queryData(){
	var sendData map[string]interface{}
	sendData = make(map[string]interface{})
	sendData["method"] = "query"
	sendByte := SerializeProtocol(sendData)

	self.sendDataToServer(sendByte)
}

func(self *LeaseClient) get(key string)(value interface{}, err error){
	self.mutex.Lock()
	defer self.mutex.Unlock()

	var ok bool

	if len(self.cacheData) == 0 {
		// 請求遠端服務端數據
		self.queryData()
		log.Println("wait from server data")
		<- self.msgChan
		// 設置本地緩存過期時間
		self.addExpireTimer()
		log.Println("get from server ", self.cacheData)
	}

	value, ok = self.cacheData[key]
	if ok == true {
		return
	}
	err = errors.New("not found")
	return
}

func(self *LeaseClient) set(key string, value string)error{
	// 先過期本地緩存數據
	self.expireCacheData()

	// 想遠端發送修改數據
	var sendData map[string]interface{}
	sendData = make(map[string]interface{})
	sendData["method"] = "modfiy"

	params := make(map[string]string)
	params[key] = value
	sendData["data"] = params

	sendByte := SerializeProtocol(sendData)
	self.sendDataToServer(sendByte)

	<- self.msgChan

	return nil
}

func(self *LeaseClient) expireCacheData(){
	self.mutex.Lock()
	defer self.mutex.Unlock()
	log.Println("expire cache data ", self.cacheData)
	self.cacheData = make(map[string]interface{})
}

func(self *LeaseClient) addExpireTimer(){
	time.AfterFunc(10*time.Second, self.expireCacheData)
}


func GetNewLeaseClient(address string)(leaseClient *LeaseClient,err error){
	if address == "" || len(address) == 0 {
		err = errors.New("address not valid")
		return
	}
	leaseClient = &LeaseClient{}
	conn, err := net.DialTimeout("tcp", address, 2*time.Second)
	if err != nil {
		fmt.Println("connect to server error ", err)
		return
	}
	leaseClient.conn = conn
	leaseClient.msgChan = make(chan bool)
	leaseClient.cacheData = make(map[string]interface{})
	leaseClient.recvData = ""
	leaseClient.sendData = []byte("")

	go leaseClient.HandleReadEvent()
	return
}

客戶端的代碼設計,主要是生成一個LeaseClient結構體並連接遠端的服務器,然後就一直監聽服務端的數據,並更新到保存的cacheData中,從而然調用方能夠在其中獲取數據,如果該cacheData沒有數據則需要向服務端請求數據,等待服務端將數據。如果在get的過程中,檢測到本地cacheData爲空則阻塞等待從服務端請求完成之後再返回。在獲取服務端的數據返回之後,也會設置一個租約時間,如果租約時間到了則清空本地緩存數據重新向服務端查詢數據。

查詢數據代碼示例
package main

import (
	"math/rand"
	"time"
	"log"
)


func main() {
	var value interface{}
	leaseClient, err := GetNewLeaseClient("127.0.0.1:7070")
	if err != nil{
		log.Println("new error ", err)
		return
	}
	for {
		var sleepTime int
		sleepTime = rand.Intn(10)
		log.Println("sleep time ", sleepTime)
		time.Sleep(time.Duration(sleepTime)*time.Second)

		value, err = leaseClient.get("store2")
		if err != nil {
			log.Println("get error ", value, err)
		}
		log.Println("final get value  ", value)
	}
}

主要就是隨機的根據休眠一個時間之後,然後再去查詢客戶端數據。

修改數據代碼示例
package main

import (
	"math/rand"
	"strconv"
	"time"
	"log"
)

func init() {
	log.SetFlags(log.Ltime|log.Lshortfile)
}

func main() {
	//var value interface{}
	leaseClient, err := GetNewLeaseClient("127.0.0.1:7070")
	if err != nil{
		log.Println("new error ", err)
		return
	}
	for {
		var sleepTime int
		sleepTime = rand.Intn(10)
		log.Println("sleep time ", sleepTime)
		time.Sleep(time.Duration(sleepTime)*time.Second)

		value := strconv.Itoa(rand.Int())

		leaseClient.set("client_set_key", "client_set_value_"+value)
		log.Println("current  cacheData ", leaseClient.cacheData)
	}
}

修改也是隨機休眠一個時間之後再想服務端提交數據去修改。

代碼運行演示

此時爲了展示效果,在本地啓動一個服務端,兩個查詢客戶端,一個修改數據客戶端

go run server.go protocol.go
go run lease_library.go protocol.go get_op.go
go run set_op.go protocol.go lease_library.go

此時查看服務端的日誌輸入如下;

16:53:14 server.go:42: send chan to block chan modfiyChan
16:53:14 server.go:132: modfiy chan start  
16:53:14 server.go:136: -----------------------
16:53:14 server.go:137: modfiy data  map[data:map[client_set_key:client_set_value_3510942875414458836] method:modfiy]
16:53:14 server.go:145: modify after  map[client_set_key:client_set_value_3510942875414458836 store1:stor1_value store2:stor2_value]
16:53:15 server.go:108: get query wait status  false
16:53:15 server.go:52: current clientNums  1
16:53:15 server.go:108: get query wait status  false
16:53:15 server.go:52: current clientNums  2
16:53:22 server.go:127: wait all leases expire   {{0 0} 0xc00005c0c0 true 2 0xc0000b6060}
16:53:22 server.go:129: wait modfiy chan
16:53:26 server.go:108: get query wait status  true
16:53:26 server.go:112: wait .... and release lock
16:53:26 server.go:108: get query wait status  true
16:53:26 server.go:112: wait .... and release lock
16:53:27 server.go:40: check leases   1
16:53:27 server.go:40: check leases   0
16:53:27 server.go:42: send chan to block chan modfiyChan
16:53:27 server.go:132: modfiy chan start  
16:53:27 server.go:136: -----------------------
16:53:27 server.go:137: modfiy data  map[data:map[client_set_key:client_set_value_4324745483838182873] method:modfiy]
16:53:27 server.go:145: modify after  map[client_set_key:client_set_value_4324745483838182873 store1:stor1_value store2:stor2_value]
16:53:27 server.go:115: notify from and  continue
16:53:27 server.go:52: current clientNums  1
16:53:27 server.go:115: notify from and  continue
16:53:27 server.go:52: current clientNums  2
16:53:28 server.go:127: wait all leases expire   {{0 0} 0xc00005c0c0 true 2 0xc0000b6060}
16:53:28 server.go:129: wait modfiy chan
16:53:39 server.go:40: check leases   1
16:53:39 server.go:40: check leases   0
16:53:39 server.go:42: send chan to block chan modfiyChan
16:53:39 server.go:132: modfiy chan start  
16:53:39 server.go:136: -----------------------
16:53:39 server.go:137: modfiy data  map[data:map[client_set_key:client_set_value_2703387474910584091] method:modfiy]
16:53:39 server.go:145: modify after  map[client_set_key:client_set_value_2703387474910584091 store1:stor1_value store2:stor2_value]
16:53:41 server.go:108: get query wait status  false
16:53:41 server.go:52: current clientNums  1
16:53:41 server.go:108: get query wait status  false
16:53:41 server.go:52: current clientNums  2
16:53:46 server.go:127: wait all leases expire   {{0 0} 0xc00005c0c0 true 2 0xc0000b6060}
16:53:46 server.go:129: wait modfiy chan
16:53:53 server.go:40: check leases   1
16:53:53 server.go:40: check leases   0
16:53:53 server.go:42: send chan to block chan modfiyChan

從日誌中可以看出,在客戶端需要修改數據的時候wait all leases expire等待所有的租約到期,接着就是等到所有日期到期之後,上次查詢的客戶端都notify from and continue繼續執行讀操作,從而保證了修改的數據是全局一致的。

在客戶端查詢的日誌輸出如下;

16:54:06 lease_library.go:165: expire cache data  map[client_set_key:client_set_value_2015796113853353331 store1:stor1_value store2:stor2_value]
16:54:10 lease_library.go:126: wait from server data
16:54:10 lease_library.go:130: get from server  map[client_set_key:client_set_value_3328451335138149956 store1:stor1_value store2:stor2_value]
16:54:10 get_op.go:27: final get value   stor2_value
16:54:10 get_op.go:20: sleep time  8
16:54:18 get_op.go:27: final get value   stor2_value
16:54:18 get_op.go:20: sleep time  0
16:54:18 get_op.go:27: final get value   stor2_value
16:54:18 get_op.go:20: sleep time  5

從日誌中可知,獲取的本地數據會過期,過期之後會從server端獲取數據,獲取完成之後,如果在有效的租約內,再次查詢數據的時候,就是直接從本地的緩存中獲取並沒有訪問遠端服務器。

修改數據的日誌輸出如下;

16:51:49 set_op.go:24: sleep time  1
16:51:50 lease_library.go:165: expire cache data  map[]
16:51:59 set_op.go:30: current  cacheData  map[client_set_key:client_set_value_8674665223082153551 store1:stor1_value store2:stor2_value]
16:51:59 set_op.go:24: sleep time  7
16:52:06 lease_library.go:165: expire cache data  map[client_set_key:client_set_value_8674665223082153551 store1:stor1_value store2:stor2_value]
16:52:13 set_op.go:30: current  cacheData  map[client_set_key:client_set_value_4037200794235010051 store1:stor1_value store2:stor2_value]
16:52:13 set_op.go:24: sleep time  1
16:52:14 lease_library.go:165: expire cache data  map[client_set_key:client_set_value_4037200794235010051 store1:stor1_value store2:stor2_value]
16:52:25 set_op.go:30: current  cacheData  map[client_set_key:client_set_value_6334824724549167320 store1:stor1_value store2:stor2_value]
16:52:25 set_op.go:24: sleep time  5

修改數據的客戶端日誌輸出主要就是休眠不定時的時間,然後再去將要修改的數據發送到服務端,通過響應的時間可以看到一個修改到響應的總共的耗時每次都不一樣,有的是7秒,有的是11秒,這其中主要的原因就是在服務端有發放的租約還未到期等待服務端所有的租約到期然後再講數據返回到本地。

總結

本文主要就是簡單的實踐了租約的基本流程,主要就是根據租約的原理來代碼實踐一下,在本文的示例代碼中有很多不完善的地方,僅僅是爲了演示租約的基本原理,並基本熟悉一下go語言的編寫過程,後續會繼續學習瞭解。由於本人才疏學淺,如有錯誤請批評指正。

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