使用consul的http api所提供的四个方法实现分布式锁:
- session create
https://www.consul.io/api/session.html#create-session
建立一个session,可以指定TTL过期时间,并通过session renew刷新TTL。也可以不指定TTL,session会一直保持至被destroy
https://www.consul.io/docs/internals/sessions.html
指定或不指定TTL,是在锁的liveness和safety之间做出权衡 - key acquire
https://www.consul.io/api/kv.html#acquire
取得一个key的锁,为其设置session,session过期时,锁也就失效了
不带有acquire的更新value操作,并不会受已有锁的限制,这点很像linux的文件锁Flock,不是个强制锁 - key release
https://www.consul.io/api/kv.html#release
将key上的session移除 - session destroy
https://www.consul.io/api/session.html#delete-session
删除session
lock delay的概念
lock delay是指在lock释放后,对其再lock需要等待的时间,其文档中说是收到了 http://research.google.com/archive/chubby.html 的启发,目的是尽可能防止 仍旧存活的节点获取锁之后 做出一些可能导致不一致场景出现 的操作
实现
consul的go client提供了lock方法,里面基本上就是对上述方法的封装。而因业务需求,我选择在server端增加lock方法,方便不同的client统一使用
这里用伪代码实现一个server端的lock接口,只体现主流程。写成一个blocking的操作,尝试在一段时间内不断的acquire直到成功或超时
方法返回给client sessionId,之后client可以通过这个sessionId请求renew和unlock等操作来完成整个流程
//LockOnSession basically using the same logic as the "Lock" function in github.com/hashicorp/consul/api/lock.go
func LockOnSession(key, sessionTTL) (*iface.LockOnSessionOutput, error) {
// verify input
{
if _, err := time.ParseDuration(SessionTTL); err != nil {
return &iface.LockOnSessionOutput{}, grpc.Errorf(codes.InvalidArgument, "invalid SessionTTL: %v", err)
}
}
var sessionId string
s := consulClient.Session()
// create session
{
se := consulApi.SessionEntry{
TTL: SessionTTL,
Behavior: consulApi.SessionBehaviorDelete, // Delete或release,按需要选择
}
sessionId, _, err = s.CreateNoChecks(&se, nil)
if nil != err {
return &iface.LockOnSessionOutput{}, grpc.Errorf(codes.Internal, "create session error: %v", err)
}
}
qOpts := &consulApi.QueryOptions{
WaitTime: consulApi.DefaultLockWaitTime,
}
kv := consulClient.KV()
start := time.Now()
attempts := 0
WAIT:
if attempts > 0 {
elapsed := time.Now().Sub(start)
start = time.Now()
if elapsed > qOpts.WaitTime {
s.Destroy(sessionId, nil)
return &iface.LockOnSessionOutput{Locked: false, SessionId: ""}, nil
}
qOpts.WaitTime -= elapsed
}
attempts ++
// Look for an existing lock, blocking until not taken
pair, meta, err := kv.Get(Key, qOpts)
if nil != err {
s.Destroy(sessionId, nil)
return &iface.LockOnSessionOutput{}, grpc.Errorf(codes.Internal, "read lock error: %v", err)
}
if nil != pair && consulApi.LockFlagValue != pair.Flags {
s.Destroy(sessionId, nil)
return &iface.LockOnSessionOutput{}, grpc.Errorf(codes.FailedPrecondition, "%v", consulApi.ErrLockConflict)
}
if nil != pair && "" != pair.Session {
qOpts.WaitIndex = meta.LastIndex
goto WAIT
}
// Try to acquire the lock
lockEntry := &consulApi.KVPair{
Key: Key,
Session: sessionId,
Flags: consulApi.LockFlagValue,
}
if locked, _, err := kv.Acquire(lockEntry, nil); nil != err {
s.Destroy(sessionId, nil)
return &iface.LockOnSessionOutput{}, grpc.Errorf(codes.Internal, "acquire lock error: %v", err)
} else if !locked {
// Determine why the lock failed
if nil != pair && "" != pair.Session {
//If the session is not null, this means that a wait can safely happen
//using a long poll
qOpts.WaitIndex = meta.LastIndex
goto WAIT
} else {
// If the session is empty and the lock failed to acquire, then it means
// a lock-delay is in effect and a timed wait must be used
<-time.After(time.Second)
goto WAIT
}
}
return &iface.LockOnSessionOutput{Locked: true, SessionId: sessionId}, nil
}