开启掘金成长之旅!这是我参与「掘金日新计划 · 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实现的。