ETCD 分布式锁实现逻辑

https://github.com/coreos/etcd/blob/master/Documentation/api.md

 

Atomic Compare-and-Swap

etcd can be used as a centralized coordination service in a cluster, and CompareAndSwap (CAS) is the most basic operation used to build a distributed lock service.

This command will set the value of a key only if the client-provided conditions are equal to the current conditions.

The current comparable conditions are:

  1. prevValue - checks the previous value of the key.

  2. prevIndex - checks the previous modifiedIndex of the key.

  3. prevExist - checks existence of the key: if prevExist is true, it is an update request; if prevExist is false, it is acreate request.

Here is a simple example. Let's create a key-value pair first: foo=one.

curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one

Now let's try some invalid CompareAndSwap commands.

Trying to set this existing key with prevExist=false fails as expected:

curl http://127.0.0.1:2379/v2/keys/foo?prevExist=false -XPUT -d value=three

The error code explains the problem:

{
    "cause": "/foo",
    "errorCode": 105,
    "index": 39776,
    "message": "Key already exists"
}

Now let's provide a prevValue parameter:

curl http://127.0.0.1:2379/v2/keys/foo?prevValue=two -XPUT -d value=three

This will try to compare the previous value of the key and the previous value we provided. If they are equal, the value of the key will change to three.

{
    "cause": "[two != one]",
    "errorCode": 101,
    "index": 8,
    "message": "Compare failed"
}

which means CompareAndSwap failed. cause explains why the test failed. Note: the condition prevIndex=0 always passes.

Let's try a valid condition:

curl http://127.0.0.1:2379/v2/keys/foo?prevValue=one -XPUT -d value=two

The response should be:

{
    "action": "compareAndSwap",
    "node": {
        "createdIndex": 8,
        "key": "/foo",
        "modifiedIndex": 9,
        "value": "two"
    },
    "prevNode": {
        "createdIndex": 8,
        "key": "/foo",
        "modifiedIndex": 8,
        "value": "one"
    }
}

We successfully changed the value from "one" to "two" since we gave the correct previous value.

上面这个API主要用来在分布式环境中做集中式的协商,CAS操作的基本用途就是创建分布式的锁服务,即选主。
 
此命令的基本作用在于仅当客户端提供的条件等于当前etcd的条件时,才会修改一个key的值。当前提供的可以比较的条件有:
 
1- prevValue 检查key以前的值
2- prevIndex 检查key以前的modifiedIndex
3- prevExist - 检查key的存在性,如果prevExist为true, 则这是一个更新请求,如果prevExist的值是false, 这是一个创建请求
 
下面是一个例子,首先创建一个key-value对:foo=one
 
# curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one
{"action":"set","node":{"key":"/foo","value":"one","modifiedIndex":27,"createdIndex":27}}
 
接下来,试一些非法的CompareAndSwap命令,首先是当prevExist=false的情况下设置已存在的key,命令如下:
 
# curl http://127.0.0.1:2379/v2/keys/foo?prevExist=false -XPUT -d value=three
{"errorCode":105,"message":"Key already exists","cause":"/foo","index":27}
 
此时可以看到,返回了错误代码,告诉我们这个key已经存在。
 
接下来,我们使用prevValue参数,命令如下:
 
# curl http://127.0.0.1:2379/v2/keys/foo?prevValue=two -XPUT -d value=three
{"errorCode":101,"message":"Compare failed","cause":"[two != one]","index":27}
 
上面命令的意思是如果指定的key以前的值等于two,则会将此key的值改成我们提供的three
 
最后,试一下合法的条件,命令如下:
 
curl http://127.0.0.1:2379/v2/keys/foo?prevValue=one -XPUT -d value=two
{"action":"compareAndSwap","node":{"key":"/foo","value":"two","modifiedIndex":28,"createdIndex":27},"prevNode":{"key":"/foo","value":"one","modifiedIndex":27,"createdIndex":27}}
 
这里可以看到,我们成功地将foo这个key的值改成了two,因为提供了正确的prevValue
 
这样,如果要实现分布式锁,则我们为每一个锁提供一个唯一的key,这样,大家都会来竞争,通过CompareAndSwap的操作,设置prevExist,这样当多个节点尝试去创建一个目录时,只有一个能够成功,而创建成功的用户即认为是获得了锁。
 
整个方案要解决几个问题:
 
1- 为每个锁设置共同的名称或者目录,以便通过后面的SDK可以迅速地在Etcd集群中定位
2- 需要判断某个锁是否已经存在,当存在的话,则不再处理(通过curl来验证,后面再通过Etcd Java API来验证)
3- 如果某个节点失效,则其他节点需要得到通知,以便能够迅速地接管
 
第一个问题很容易解决,假如我们创建一个/locks的目录,每个不同的锁都是/locks目录下的节点。例如,我们要为SCEC的订单报表服务需要做选主,则创建的节点是:/locks/scec/report/order。
 
要判断某个锁是否存在,比较简单,通过prevExist就可以达成。然而,如果服务当机,则没有这么简单,因为他自己需要知道当前是谁,决定如何修改其值。
 
如何判断是否有改变?能否主动通知?
 
使用etcdctl创建一个时间的节点,然后查看节点的内容
 
1- 创建/locks目录(如果目录已存在,如何处理?)
2- 创建/locks/scec/report目录,如何级联创建(需要写工具类,即createDirRecursive)
3- 仅当不存在order节点的时候,去创建order节点,内容就是创建的时间和自身的IP,指定TTL为30秒
4- 定期更新TTL的值
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl mkdir /locks/scec/report

[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl ls / --recursive
/message
/workers
/workers/00000000000000000021
/workers/00000000000000000022
/workers/00000000000000000023
/foo
/locks
/locks/scec
/locks/scec/report
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report
/locks/scec/report: is a directory
 
上面创建了/locks/scec/repor这个节点,接下来就是创建相应的节点了,我们要创建一个order节点,表示是订单报表的任务,TTL为30秒,而其值为一个JSON,分别是IP,端口,和创建的时间。
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl set /locks/scec/report/order "192.168.1.10" --ttl 10
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
Error:  100: Key not found (/locks/scec/report/order) [34]
 
可以看到,当修改后,这个TTL是会失效的,接着我们来测试,如果没有失效的时候,一直去更新,结果会如何?
 
测试发现,一旦这样设置后,节点会一直存在,即生命周期不断地延长。
 
# ./etcdctl set /locks/scec/report/order "192.168.1.10" --ttl 10
192.168.1.10
 
首先设置一下有TTL值的情况,然后,要监测其改变,需要监测上层目录的改变,这样才能够进行协调,所以每个监听器都是一个单独的线程,否则无法进行回调操作。
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl get /locks/scec/report/order
Error:  100: Key not found (/locks/scec/report/order) [52]
 
此时,原来watch的节点会有变化,会退出,并且显示下面的内容:
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl watch --recursive /locks/scec/report
[expire] /locks/scec/report/order

除此之外,只要下层节点有变化的,都会watch退出,例如:
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl watch --recursive /locks/scec/report
[set] /locks/scec/report/order
192.168.1.10
 
如上所示,一旦有节点的变化,则会退出,因为有事件产生。所以,在watch的时候,如果节点会变化,需要做的事情是设置上级目录的watch。
 
如果节点是永久性的节点,或者说不是节点的删除事件,则了会触发节点上的事件。例如:
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl set /locks/scec/report/order "192.168.1.10"
192.168.1.10
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl set /locks/scec/report/order "192.168.1.11"
192.168.1.11
 
如上所示,中间我们修改了节点的值,此时,我们的节点上的watch会生效:
 
[root@ansible01 etcd-v2.2.0-linux-amd64]# ./etcdctl watch --recursive /locks/scec/report/order
[set] /locks/scec/report/order
192.168.1.11
整个的原理验证没有问题后,就可以使用boon的etcd java client library来完成SDK的封装了。先从用户角度进行分析。
 
首先,对于Java程序员来说,使用spring IoC来注入功能是最常见的,而且,而在分布式锁情况下,watch是最常用的功能,所以,一旦watch后,就触发一个线程,来进行控制,因为API自行完成了这个功能,所以,
 
虽然boon的etcd java client library是非常强大的,但是对于使用者而言,是需要屏蔽这些细节的,以后改成使用zookeeper或者其他的分布式协调器才能够成为可能,因此我们需要首先抽象中相应的接口。
 
对于分布式协调器来说,假如我们这个叫LeaderElection,即选主算法。主要行为有两个,一个是试图成为master,即try,即试图成为Master, 提供的就是目录和节点的名称,其中要实现分级式处理,即如果上级目录不存在,则需要依次创建。已存在的,则忽略继续。这样,如果当前没有主存在,则会注册自己,要提供的有IP和端口,因此需要提供IP和端口信息,这也是接口的部分,参考InetAddress的构造函数。其实现上,需要知道,自己是某一个的master,所以要提供一个方法,isMaster,表示自己是否是某个的主,参数就是节点全路径。当完成后,是需要通知做一些事情的,这是一个触发器,即回调Listener, 所以需要提供一个回调的方法,因此try这里面需要指定接口的实现,这样可以将SDK与外部分离开来。回调的接口实例就毫无疑问地是由外部提供的。并且,定期地,当成为master后,需要周期性地发心跳的信号,这显然需要有一个线程来做这样的事情。要注意调度的不准确性,例如TTL是30秒,则心跳的时间就需要设置为10秒,这样就可以避免心跳调度不及时,导致出现问题。
原文地址:https://www.cnblogs.com/lishijia/p/ETCD.html