Go RPC完成一个远程锁机制

337 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

上一节 juejin.cn/post/717310… 简单讲了Go RPC的概念、核心组件、远程调用向量加法器实例 这一节尝试利用Go RPC完成锁机制,具体实验要求如下:

锁服务包括两个模块:锁客户,锁服务器,两者通过RPC通信。需要实现两个功能:(1)客户发acquire请求,从锁服务器请求一个特定的锁,用release释放锁,锁服务器一次将锁授予一个客户。(2) 扩充RPC库,实现at-most-once执行语义,即消除重复的RPC请求。

上述问题可以假设为,银行账户上有10单元的公共余额,客户可以申请借款(acquire),同一时间只能一个人借款(Acquire),这里要使用到锁机制。最后at-most-once要求,同一用户在借款未还的情况下不能再次借款(可能点击借款时重复点击,而只能有一个有效)

有两种机制实现锁(下面用伪代码描述) 1.

Acquire:
mux.Lock()
while(资源数<=0) {空循环}
资源数--
mux.Unlock()

Release:
mux.Lock()
资源数++
mux.Unlock()

当客户端在请求资源时,先把资源锁住,再判断资源是否充足(此处必须先lock,否则其它用户能申请资源并改变资源数,从而影响判断)。现在假设有10个资源,用户一lock资源后发现资源数=0,此时他拿到了锁但没有解锁,此时用户二也要请求资源,由于1拿了lock,所以2就会阻塞在lock语句上,也就是2必须等1申请完才能申请。但是我们希望,用户1没有申请到资源的时候,其余用户和它的优先级一样,也能抢占到锁,用户1长时间在while中持有锁是不合理的。所以引入下一种方法

Acquire:
for i := 0; i < 512; i++ {
	mux.Lock()
	if(资源数>0) {
		资源--
		mux.Unlock()
		return nil
	}
	mux.Unlock()
}

这种情况,用户1在资源未满足的情况下,会一直lock ->unlock,当用户1 unlock后,其他用户便能够和他竞争锁了。值得注意的是,这里设置了512次循环,当用户等了512此lock->unlock的执行时间时,他便不再lock了,不等了,这次请求失效,提醒用户下次再请求。

common.go

package common_server

import (
	"errors"
	"fmt"
	"sync"
)

type Count struct {
	Cash int
	mux  sync.Mutex
}

type LockSer interface {
	Acquire()
	Release()
}

type Countserver struct {
	count Count
	ids   []int
}

type Request struct {
	Id     int
	Action string
}
type Response struct{}

func NewCounter(initCash int) *Countserver {
	return &Countserver{count: Count{Cash: initCash}}
}

func (c *Countserver) Acquire(req Request, res *Response) error {

	var flag bool = true
	for _, id := range c.ids {
		if id == req.Id {
			flag = false
			break
		}
	}

	if flag {
		c.ids = append(c.ids, req.Id)
	}

	for i := 0; i < 512; i++ {
		c.count.mux.Lock()
		if c.count.Cash > 0 && flag {
			c.count.Cash--
			fmt.Printf("Acquire lock %d\n", req.Id)
			c.count.mux.Unlock()
			return nil
		}
		c.count.mux.Unlock()
	}
	return errors.New("acquire at most once")
}

func (c *Countserver) Release(req Request, res *Response) error {
	c.count.mux.Lock()
	c.count.Cash++

	for i, id := range c.ids {
		if id == req.Id {
			c.ids = append(c.ids[:i], c.ids[i+1:]...)
			fmt.Printf("Release lock %d\n", req.Id)
			c.count.mux.Unlock()
			return nil
		}
	}
	c.count.mux.Unlock()
	return errors.New("release at most once")
}

server.go

package main

import (
	"fmt"
	"net/http"
	"net/rpc"
	"server/common_server"
)

func main() {

	var ms = common_server.NewCounter(10)

	// 注册 RPC 服务
	err := rpc.Register(ms)
	if err != nil {
		fmt.Printf("rpc server register faild, err:%s", err)
	}
	// 将 RPC 服务绑定到 HTTP 服务中去
	rpc.HandleHTTP()

	fmt.Printf("server start ....\n")
	err = http.ListenAndServe(":9090", nil)

	if err != nil {
		fmt.Printf("listen and server is failed, err:%v\n", err)
	}

	fmt.Printf("server stop ....")
}

client.go

package main

import (
	"fmt"
	"net/rpc"
)

type Argsignal struct {
	Id     int
	Action string
}
type Repsignal struct{}

func main() {
	var argsignal = Argsignal{}
	var repsignal Repsignal

	var client, err = rpc.DialHTTP("tcp", "127.0.0.1:9090")

	for {
		fmt.Println("Enter the signal id")
		fmt.Scanf("%s %d\n", &argsignal.Action, &argsignal.Id)
		if argsignal.Action == "acquire" {
			err = client.Call("Countserver.Acquire", &argsignal, &repsignal)
			if err != nil {
				fmt.Printf("acquire failed, err:%v\n", err)
			} else {
				fmt.Println("Acquire success")
			}
		} else if argsignal.Action == "release" {
			err = client.Call("Countserver.Release", &argsignal, &repsignal)
			if err != nil {
				fmt.Printf("release failed, err:%v\n", err)
			} else {
				fmt.Println("Release success")
			}
		}

	}

}

实现at-most-once执行语义,即消除重复的RPC请求,可以使用请求ID,这个ID作为参数会传到server上,server本地维护一个ID切片IDs,当新的ID来时,IDs会将其添加。在Release时会在IDs中去掉该ID。当有重复ID来,也就是重复请求,common有判断机制,如果来的ID在IDs中已经存在,会返回errors.New("acquire at most once")。

同样的,分别在server和client上go run一下 代码结果 server:

Enter the signal id
acquire 1          //这是输入
Acquire success    //这是返回的提示
Enter the signal id
acquire 2
Acquire success    
Enter the signal id
acquire 1
acquire failed, err:acquire at most once //由于id=1已经申请过了,再申请触发错误
Enter the signal id
release 1          //先释放ID=1的请求
Release success
Enter the signal id
acquire 1         //此时可以进行ID=1的请求
Acquire success
Enter the signal id
release 1         //释放ID=1请求的资源,注意资源没有一一对应的关系,ID=1并不是请求 
                  //一号资源
Release success
Enter the signal id
release 2         //释放ID=2请求的资源
Release success

重复Acquire请求和重复Release请求都是通过用户发送的ID和server本地维持的IDs实现的。