引言
目前,可实现分布式锁的开源软件还是比较多的,其中应用最广泛、大家最熟悉的应该就是 ZooKeeper,此外还有数据库、Redis、Chubby 等。但若从读写性能、可靠性、可用性、安全性和复杂度等方面综合考量,作为后起之秀的 Etcd 无疑是其中的 “佼佼者” 。它完全媲美业界“名宿” ZooKeeper,在有些方面,Etcd 甚至超越了 ZooKeeper。本场 Chat 将继续“分布式锁”这一主题,介绍基于 Etcd 分布式锁方案。
本场主要内容:
- Raft 算法解读;
- Etcd 介绍;
- Etcd 实现分布式锁的原理;
- 从原理出发:基于 Etcd 实现分布式锁,全方位细节展示;
- 从接口出发:基于 Etcd 的 Lock 接口实现分布式锁。
本场分为两大部分:
一、分布式一致性算法 Raft 原理 及 Etcd 介绍;
二、基于 Etcd 的分布式锁实现原理及方案。
一、分布式一致性算法 Raft 原理 及 Etcd 介绍
1. 分布式一致性算法 Raft 概述
1.1 Raft 背景
在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利 · 兰伯特(Leslie Lamport)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。
Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、libpaxos。此外 Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多的改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。
由于 Paxos 算法过于复杂、实现困难,极大的制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。
Raft 算法由斯坦福的 Diego Ongaro 和 John Ousterhout 于 2013 年发表:《In Search of an Understandable Consensus Algorithm》。相较于 Paxos,Raft 通过逻辑分离使其更容易理解和实现,目前,已经有十多种语言的 Raft 算法实现框架,较为出名的有 etcd、Consul 。
1.2 Raft 角色
一个 Raft 集群包含若干节点,Raft 把这些节点分为三种状态:Leader、 Follower 、Candidate,每种状态负责的任务也是不一样的,正常情况下,集群中的节点只存在 Leader 与 Follower 两种状态。
- Leader(领导者) :负责日志的同步管理,处理来自客户端的请求,与 Follower 保持 heartBeat 的联系;
- Follower(追随者) :响应 Leader 的日志同步请求,响应 Candidate 的邀票请求,以及把客户端请求到 Follower 的事务转发(重定向)给 Leader;
- Candidate(候选者) :负责选举投票,集群刚启动或者 Leader 宕机时,状态为 Follower 的节点将转为 Candidate 并发起选举,选举胜出(获得超过半数节点的投票)后,从 Candidate 转为 Leader 状态;
1.3 Raft 概述
通常,Raft 集群中只有一个 Leader,其它节点都是 Follower 。Follower 都是被动的:它们不会发送任何请求,只是简单的响应来自 Leader 或者 Candidate 的请求。Leader 负责处理所有的客户端请求(如果一个客户端和 Follower 联系,那么 Follower 会把请求重定向给 Leader)。为简化逻辑和实现,Raft 将一致性问题分解成了三个相对独立的子问题:
- 选举(Leader election) :当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来;
- 日志复制(Log replication) :Leader 接收来自客户端的请求并将其以日志条目的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致。
- 安全性(Safety) :如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务器节点不能在同一个日志索引位置应用一个不同的指令。
2. Raft 算法之 Leader election 原理
根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点的状态都是 Follower,由于没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),因此,Followers 会认为 Leader 已经 down,进而转为 Candidate 状态。然后,Candidate 将向集群中其它节点请求投票,同意自己升级为 Leader,如果 Candidate 收到超过半数节点的投票(N/2 + 1),它将获胜成为 Leader。
第一阶段:所有节点都是 Follower
根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时(或者 Leader 宕机),所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100-500 毫秒之间且并不一致(避免同时发起选举)。
第二阶段:Follower 转为 Candidate 并发起投票
由于没有 Leader,Followers 无法与 Leader 保持心跳(Heart Beat),节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转为候选者 candidate 状态、Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。
注意:由于每个节点的选举定时器超时时间都在 100-500 毫秒之间,且不一致,因此,可以避免所有的 Follower 同时转为 Candidate 并发起投票请求。换言之,最先转为 Candidate 并发起投票请求的节点将具有成为 Leader 的 “先发优势”。
第三阶段:投票策略
节点收到投票请求后会根据以下情况决定是否接受投票请求:
- 请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给它;
- 请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己。
第四阶段:Candidate 转为 Leader
一轮选举过后,正常情况下,会有一个 Candidate 收到超过半数节点(n/2 + 1)的投票,那么它将胜出并升级为 Leader,然后定时发送心跳给其它的节点,其它节点会转为 Follower 并与 Leader 保持同步,如此,本轮选举结束。
注意:有可能一轮选举中,没有 Candidate 收到超过半数节点投票,那么将进行下一轮选举。
二、 Etcd 介绍
Etcd 是一个高可用、强一致的分布式键值(key-value)数据库,主要用途是共享配置和服务发现,其内部采用 Raft 算法作为分布式一致性协议,因此,Etcd 集群作为一个分布式系统 “天然” 就是强一致性的。而副本机制(一个 Leader,多个 Follower)又保证了其高可用性。
关于 Etcd 命名的由来
在 Unix 系统中,/etc 目录用于存放系统管理和配置文件;分布式系统(Distributed system)第一个字母是“d”。两者看上去并没有直接联系,但它们加在一起就有点意思了:分布式的关键数据(系统管理和配置文件)存储系统,这便是 etcd 命名的灵感之源。
1.Etcd的架构
- HTTP Server: 用于处理客户端发送的 API 请求以及其它 Etcd 节点的同步与心跳信息请求。
- Store:用于处理 Etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 Etcd 对用户提供的大多数 API 功能的具体实现。
- Raft:Raft 强一致性算法的具体实现,是Etcd 的核心。
- WAL:Write Ahead Log(预写式日志) ,是 Etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引外,Etcd 就通过 WAL 进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照;Entry 表示存储的具体日志内容。
通常,一个用户的请求发送过来,会经由 HTTP Server 转发给 Store 进行具体的事务处理;如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的 Etcd 节点以确认数据提交;最后进行数据的提交,再次同步。
2.Etcd 的基本概念词
- Raft:Etcd 的核心,保证分布式系统强一致性的算法。
- Node:一个 Raft 状态机实例。
- Member: 一个 Etcd 实例,它管理着一个 Node,并且可以为客户端请求提供服务。
- Cluster:由多个 Member 构成可以协同工作的 Etcd 集群。
- Peer:对同一个 Etcd 集群中另外一个 Member 的称呼。
- Client: 向 Etcd 集群发送 HTTP 请求的客户端。
- WAL:预写式日志,Etcd 用于持久化存储的日志格式。
- Snapshot:Etcd 防止 WAL 文件过多而设置的快照,存储 Etcd 数据状态。
- Leader:Raft 算法中通过竞选而产生的处理所有数据提交的节点。
- Follower:竞选失败的节点作为 Raft 中的从属节点,为算法提供强一致性保证。
- Candidate:当 Follower 超过一定时间接收不到 Leader 的心跳时转变为 Candidate 开始竞选。
- Term:某个节点成为 Leader 到下一次竞选期间,称为一个 Term(任期)。
- Index:数据项编号。Raft 中通过 Term 和 Index 来定位数据。
3.Etcd能做什么
在分布式系统中,有一个最基本的需求——如何保证分布式部署的多个节点之间的数据共享。如同团队协作,成员可以分头干活,但总是需要共享一些必须的信息,比如谁是 leader、团队成员列表、关联任务之间的顺序协调等。所以分布式系统要么自己实现一个可靠的共享存储来同步信息,要么依赖一个可靠的共享存储服务,而 Etcd 就是这样一个服务。
Etcd 官方介绍:
A distributed, reliable key-value store for the most critical data of a distributed system.
简言之,一个可用于存储分布式系统关键数据的可靠的键值数据库。关于可靠性自不必多说,Raft 协议已经阐明,但事实上,Etcd 作为 key-value 型数据库还有其它特点:Watch 机制、租约机制、Revision 机制等,正是这些机制赋予了 Etcd 强大的能力。
4.Etcd的主要应用场景
应用场景 1:服务发现
服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。服务发现的实现原理如下:
- 存在一个高可靠、高可用的中心配置节点:基于 Raft 算法的 Etcd 天然支持,不必多解释。
- 服务提供方会持续的向配置节点注册服务:用户可以在 Etcd 中注册服务,并且对注册的服务配置租约,定时续约以达到维持服务的目的(一旦停止续约,对应的服务就会失效)。
- 服务的调用方会持续的读取中心配置节点的配置并修改本机配置,然后 reload 服务:服务提供方在 Etcd 指定的目录(前缀机制支持)下注册的服务,服务调用方在对应的目录下查服务。通过 Watch 机制,服务调用方还可以监测服务的变化。
应用场景 2: 消息发布和订阅
在分布式系统中,组件间通信常用的方式是消息发布-订阅机制。具体而言,即配置一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦有关主题有消息发布,就会实时通知订阅者。通过这种方式可以实现分布式系统配置的集中式管理和实时动态更新。显然,通过 Watch 机制可以实现。
应用在启动时,主动从 Etcd 获取一次配置信息,同时,在 Etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新,Etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。
应用场景 3: 分布式锁
前面已经提及,Etcd 支持 Revision 机制,那么对于同一个 lock,即便有多个客户端争夺(本质上就是 put(lockName, value) 操作),Revision 机制可以保证它们的 Revision 编号有序且唯一,那么,客户端只要根据 Revision 的大小顺序就可以确定获得锁的先后顺序,从而很容易实现公平锁。
应用场景 4: 集群监控与 Leader 竞选
- 集群监控:通过 Etcd 的 Watch 机制,当某个 key 消失或变动时,Watcher 会第一时间发现并告知用户。节点可以为 key 设置租约 (TTL),比如每隔 30s 向 Etcd 发送一次心跳续约,使代表该节点的 key 保持存活,一旦节点故障,续约停止,对应的 key 将失效删除。如此,通过 Watch 机制就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。
- Leader 竞选:使用分布式锁,可以很好的实现 Leader 竞选(抢锁成功的成为 Leader)。Leader 应用的经典场景是在搜索系统中建立全量索引。如果每个机器分别进行索引的建立,不仅耗时,而且不能保证索引的一致性。通过在 Etcd 实现的锁机制竞选 Leader,由 Leader 进行索引计算,再将计算结果分发到其它节点。
5.应用实践-Etcd实现分布式锁
5.1实现的基础
Etcd 的高可用性、强一致性不必多说,前面章节中已经阐明,本节主要介绍 Etcd 支持的以下机制:Watch 机制、Lease 机制、Revision 机制和 Prefix 机制,正是这些机制赋予了 Etcd 实现分布式锁的能力。
- Lease机制:即租约机制(TTL,Time To Live),Etcd 可以为存储的 key-value 对设置租约,当租约到期,key-value 将失效删除;同时也支持续约,通过客户端可以在租约到期之前续约,以避免 key-value 对过期失效。Lease 机制可以保证分布式锁的安全性,为锁对应的 key 配置租约,即使锁的持有者因故障而不能主动释放锁,锁也会因租约到期而自动释放。
- Revision机制:每个 key 带有一个 Revision 号,每进行一次事务加一,因此它是全局唯一的,如初始值为 0,进行一次
put(key, value),key 的 Revision 变为 1;同样的操作,再进行一次,Revision 变为 2;换成 key1 进行put(key1, value)操作,Revision 将变为 3。这种机制有一个作用:通过 Revision 的大小就可以知道进行写操作的顺序。在实现分布式锁时,多个客户端同时抢锁,根据 Revision 号大小依次获得锁,可以避免 “羊群效应” (也称 “惊群效应”),实现公平锁。
- Prefix机制:即前缀机制,也称目录机制。例如,一个名为
/mylock的锁,两个争抢它的客户端进行写操作,实际写入的 key 分别为:key1="/mylock/UUID1",key2="/mylock/UUID2",其中,UUID 表示全局唯一的 ID,确保两个 key 的唯一性。很显然,写操作都会成功,但返回的 Revision 不一样,那么,如何判断谁获得了锁呢?通过前缀/mylock查询,返回包含两个 key-value 对的的 KeyValue 列表,同时也包含它们的 Revision,通过 Revision 大小,客户端可以判断自己是否获得锁,如果抢锁失败,则等待锁释放(对应的 key 被删除或者租约过期),然后再判断自己是否可以获得锁;
- Watch机制:即监听机制,Watch 机制支持 Watch 某个固定的 key,也支持 Watch 一个范围(前缀机制),当被 Watch 的 key 或范围发生变化,客户端将收到通知;在实现分布式锁时,如果抢锁失败,可通过 Prefix 机制返回的 KeyValue 列表获得 Revision 比自己小且相差最小的 key(称为 pre-key),对 pre-key 进行监听,因为只有它释放锁,自己才能获得锁,如果 Watch 到 pre-key 的 DELETE 事件,则说明 pre-key 已经释放,自己已经持有锁。
5.2实现的思路
下面描述使用 Etcd 实现分布式锁的业务流程,假设对某个共享资源设置的锁名为:/lock/mylock
步骤 1: 准备
客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 key,假设第一个客户端对应的 key="/lock/mylock/UUID1",第二个为 key="/lock/mylock/UUID2";客户端分别为自己的 key 创建租约 - Lease,租约的长度根据业务耗时确定,假设为 15s;
步骤 2: 创建定时任务作为租约的“心跳”
当一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,key 将因租约到期而被删除,从而锁释放,避免死锁。
步骤 3: 客户端将自己全局唯一的 key 写入 Etcd
进行 put 操作,将步骤 1 中创建的 key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 put 操作返回的 Revision 分别为 1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。
步骤 4: 客户端判断是否获得锁
客户端以前缀 /lock/mylock 读取 keyValue 列表(keyValue 中带有 key 对应的 Revision),判断自己 key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。
步骤 5: 执行业务
获得锁后,操作共享资源,执行业务代码。
步骤 6: 释放锁
完成业务流程后,删除对应的key释放锁。
5.3图形示例
5.4代码demo
import java.util.List;import java.util.concurrent.ExecutionException;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;import java.util.concurrent.TimeoutException;
import com.coreos.jetcd.Client;import com.coreos.jetcd.KV;import com.coreos.jetcd.Lease;import com.coreos.jetcd.Watch.Watcher;import com.coreos.jetcd.options.GetOption;import com.coreos.jetcd.options.GetOption.SortTarget;import com.coreos.jetcd.options.PutOption;import com.coreos.jetcd.watch.WatchEvent;import com.coreos.jetcd.watch.WatchResponse;import com.coreos.jetcd.data.ByteSequence;import com.coreos.jetcd.data.KeyValue;import com.coreos.jetcd.kv.PutResponse;
import java.util.UUID;
/**
* Etcd 客户端代码,用多个线程“抢锁”模拟分布式系统中,多个进程“抢锁”
*
*/publicclassEtcdClient{
publicstatic void main(String[] args)throwsInterruptedException,ExecutionException,
TimeoutException,ClassNotFoundException
{
// 创建Etcd客户端,Etcd服务端为单机模式
Client client =Client.builder().endpoints("http://localhost:2379").build();
// 对于某共享资源制定的锁名
String lockName ="/lock/mylock";
// 模拟分布式场景下,多个进程“抢锁”
for(int i =0; i <3; i++)
{
newMyThread(lockName, client).start();
}
}
/**
* 加锁方法,返回值为加锁操作中实际存储于Etcd中的key,即:lockName+UUID,
* 根据返回的key,可删除存储于Etcd中的键值对,达到释放锁的目的。
*
* @param lockName
* @param client
* @param leaseId
* @return
*/
publicstaticStringlock(String lockName,Client client, long leaseId)
{
// lockName作为实际存储在Etcd的中的key的前缀,后缀是一个全局唯一的ID,从而确保:对于同一个锁,不同进程存储的key具有相同的前缀,不同的后缀
StringBuffer strBufOfRealKey =newStringBuffer();
strBufOfRealKey.append(lockName);
strBufOfRealKey.append("/");
strBufOfRealKey.append(UUID.randomUUID().toString());
// 加锁操作实际上是一个put操作,每一次put操作都会使revision增加1,因此,对于任何一次操作,这都是唯一的。(get,delete也一样)
// 可以通过revision的大小确定进行抢锁操作的时序,先进行抢锁的,revision较小,后面依次增加。
// 用于记录自己“抢锁”的Revision,初始值为0L
long revisionOfMyself = 0L;
KV kvClient = client.getKVClient();
// lock,尝试加锁,加锁只关注key,value不为空即可。
// 注意:这里没有考虑可靠性和重试机制,实际应用中应考虑put操作而重试
try
{
PutResponse putResponse = kvClient
.put(ByteSequence.fromString(strBufOfRealKey.toString()),
ByteSequence.fromString("value"),
PutOption.newBuilder().withLeaseId(leaseId).build())
.get(10,TimeUnit.SECONDS);
// 获取自己加锁操作的Revision号
revisionOfMyself = putResponse.getHeader().getRevision();
}
catch(InterruptedException|ExecutionException|TimeoutException e1)
{
System.out.println("[error]: lock operation failed:"+ e1);
}
try
{
// lockName作为前缀,取出所有键值对,并且根据Revision进行升序排列,版本号小的在前
List<KeyValue> kvList = kvClient.get(ByteSequence.fromString(lockName),
GetOption.newBuilder().withPrefix(ByteSequence.fromString(lockName))
.withSortField(SortTarget.MOD).build())
.get().getKvs();
// 如果自己的版本号最小,则表明自己持有锁成功,否则进入监听流程,等待锁释放
if(revisionOfMyself == kvList.get(0).getModRevision())
{
System.out.println("[lock]: lock successfully. [revision]:"+ revisionOfMyself);
// 加锁成功,返回实际存储于Etcd中的key
return strBufOfRealKey.toString();
}
else
{
// 记录自己加锁操作的前一个加锁操作的索引,因为只有前一个加锁操作完成并释放,自己才能获得锁
int preIndex =0;
for(int index =0; index < kvList.size(); index++)
{
if(kvList.get(index).getModRevision()== revisionOfMyself)
{
preIndex = index -1;// 前一个加锁操作,故比自己的索引小1
}
}
// 根据索引,获得前一个加锁操作对应的key
ByteSequence preKeyBS = kvList.get(preIndex).getKey();
// 创建一个Watcher,用于监听前一个key
Watcher watcher = client.getWatchClient().watch(preKeyBS);
WatchResponse res = null;
// 监听前一个key,将处于阻塞状态,直到前一个key发生delete事件
// 需要注意的是,一个key对应的事件不只有delete,不过,对于分布式锁来说,除了加锁就是释放锁
// 因此,这里只要监听到事件,必然是delete事件或者key因租约过期而失效删除,结果都是锁被释放
try
{
System.out.println("[lock]: keep waiting until the lock is released.");
res = watcher.listen();
}
catch(InterruptedException e)
{
System.out.println("[error]: failed to listen key.");
}
// 为了便于读者理解,此处写一点冗余代码,判断监听事件是否为DELETE,即释放锁
List<WatchEvent> eventlist = res.getEvents();
for(WatchEvent event : eventlist)
{
// 如果监听到DELETE事件,说明前一个加锁操作完成并已经释放,自己获得锁,返回
if(event.getEventType().toString().equals("DELETE"))
{
System.out.println("[lock]: lock successfully. [revision]:"+ revisionOfMyself);
return strBufOfRealKey.toString();
}
}
}
}
catch(InterruptedException|ExecutionException e)
{
System.out.println("[error]: lock operation failed:"+ e);
}
return strBufOfRealKey.toString();
}
/**
* 释放锁方法,本质上就是删除实际存储于Etcd中的key
*
* @param lockName
* @param client
*/
publicstatic void unLock(String realLockName,Client client)
{
try
{
client.getKVClient().delete(ByteSequence.fromString(realLockName)).get(10,
TimeUnit.SECONDS);
System.out.println("[unLock]: unlock successfully.[lockName]:"+ realLockName);
}
catch(InterruptedException|ExecutionException|TimeoutException e)
{
System.out.println("[error]: unlock failed:"+ e);
}
}
/**
* 自定义一个线程类,模拟分布式场景下多个进程 "抢锁"
*/
publicstaticclassMyThread extends Thread
{
privateString lockName;
privateClient client;
MyThread(String lockName,Client client)
{
this.client = client;
this.lockName = lockName;
}
@Override
public void run()
{
// 创建一个租约,有效期15s
Lease leaseClient = client.getLeaseClient();
Long leaseId = null;
try
{
leaseId = leaseClient.grant(15).get(10,TimeUnit.SECONDS).getID();
}
catch(InterruptedException|ExecutionException|TimeoutException e1)
{
System.out.println("[error]: create lease failed:"+ e1);
return;
}
// 创建一个定时任务作为“心跳”,保证等待锁释放期间,租约不失效;
// 同时,一旦客户端发生故障,心跳便会中断,锁也会应租约过期而被动释放,避免死锁
ScheduledExecutorService service =Executors.newSingleThreadScheduledExecutor();
// 续约心跳为12s,仅作为举例
service.scheduleAtFixedRate(newKeepAliveTask(leaseClient, leaseId),1,12,TimeUnit.SECONDS);
// 1. try to lock
String realLoclName =lock(lockName, client, leaseId);
// 2. to do something
try
{
Thread.sleep(6000);
}
catch(InterruptedException e2)
{
System.out.println("[error]:"+ e2);
}
// 3. unlock
service.shutdown();// 关闭续约的定时任务
unLock(realLoclName, client);
}
}
/**
* 在等待其它客户端释放锁期间,通过心跳续约,保证自己的key-value不会失效
*
*/
publicstaticclassKeepAliveTask implements Runnable
{
privateLease leaseClient;
private long leaseId;
KeepAliveTask(Lease leaseClient, long leaseId)
{
this.leaseClient = leaseClient;
this.leaseId = leaseId;
}
@Override
public void run()
{
leaseClient.keepAliveOnce(leaseId);
}
}}