1. 写入
1.1. etcd一致性
etcd 提供了强一致性(Strong Consistency)。所有写操作都必须通过当前的领导者节点(Leader),并在多数追随者节点(Followers)确认后才会被提交。
1、工作流程
- 客户端将写请求发送到领导者节点。
- 领导者节点将写操作记录到自己的日志,然后将日志条目复制到所有追随者节点。
- 当多数节点确认收到日志条目后,领导者节点将其标记为已提交,并应用到状态机。
- 领导者节点向客户端返回成功确认。
Raft 算法通过以下机制来处理这些异常:
- 领导者故障:如果领导者节点失效,集群中的其他节点会发起新的领导者选举,选出新的领导者节点。新的领导者节点会从已提交的日志条目中恢复一致性状态。
- 追随者故障:如果某个追随者节点失效,领导者节点仍然可以在大多数节点确认写操作后进行提交。一旦追随者节点恢复,它会从领导者节点同步最新的日志条目,恢复一致状态。
因此,当客户端收到写入成功的消息时,表示leader节点及大多数节点都已成功写入,且提交到状态机了。就算还有其他少量节点还没有写入,etcd后续也会保证它们会被同步到。
2、优缺点
优点:提供强一致性保证,即所有节点最终会收敛到相同的状态。适用于对数据一致性要求高的场景。
缺点:在高负载或网络分区情况下,可能会出现写操作延迟。
1.2. etcd写入顺序
由于所有写请求都必须经过 Leader 节点,并且 Leader 节点会将日志条目按顺序添加到自己的日志中,这就保证了写请求的顺序性。
虽然这些写请求在 Leader 节点上的处理和复制过程是并行的,但各个 Follower 节点也是按照 Leader 节点写入的顺序提交日志条目。这是 Raft 共识算法的一部分,确保了整个集群中的所有节点都保持相同的一致性和操作顺序。
Raft 算法中的日志复制和提交流程:
Leader 写入日志:
- Leader 节点接收到写请求后,会将请求转换为日志条目,并将其添加到本地的 Raft 日志中。
- 这些日志条目在 Leader 节点上是按顺序写入的,形成一个有序的日志条目列表。
日志条目的复制:
- Leader 节点将新日志条目并行地复制到所有 Follower 节点。
- Follower 节点接收到这些日志条目后,会将它们添加到本地日志中,保持与 Leader 节点相同的顺序。
日志条目的提交:
- 当 Leader 节点和多数 Follower 节点都确认接收到并持久化了某个日志条目后,Leader 节点会将这个日志条目标记为已提交。
- 提交是按顺序进行的,即 Leader 节点只有在之前的日志条目提交后,才能提交后续的日志条目。
应用到状态机:
- 提交日志条目后,Leader 和 Follower 节点会按照日志中的顺序将这些操作应用到各自的状态机中。
因此,可以说etcd的写入是严格按照顺序写入的。
1.3. 对比eureka
Eureka 主要采用以高可用性和最终一致性为目标的设计,而不是强一致性。
1、工作流程
写请求处理:
- 客户端将写请求(如服务注册或续约)发送到一个 Eureka 服务器节点。
- 该节点立即处理请求并将信息更新到本地注册表,然后立即向客户端返回成功确认。
- 随后,该节点以异步的方式将更新传播给其他 Eureka 服务器节点。
异步复制机制:
- Eureka 采用异步复制机制,即写操作先在收到请求的节点上完成,然后通过后台线程将更新同步到集群中的其他节点。
- 这种设计确保了高可用性和快速响应,但在网络分区或节点故障的情况下,可能会导致短暂的数据不一致。
2、优点
对于服务发现和注册场景,这种最终一致性通常是可接受的,因为服务实例的变化(如上线或下线)不需要立即全局可见。
由于没有主节点,任何 Eureka 服务器节点都可以处理写请求,提高了系统的可用性。如果一个节点失效,其他节点仍然可以处理请求,服务注册和发现不会中断。
3、缺点
由于异步复制机制,Eureka 提供的是最终一致性。这意味着系统在一段时间后会达到一致状态,但在短时间内,可能会存在数据不一致的情况。即客户端收到写入成功的返回时,实际上可能只在一个节点上写入成功,如果此时查询的请求打入其他节点,就查询不到。
另外,因为每个节点都可以先写入,而且没有多数节点都确认。当多个节点同时修改某个配置时,各自节点都写入成功了,但在节点之间日志同步时,就会出现冲突。
4、写入顺序
由于eureka每个节点都可以先写入,然后再异步同步给其他节点,所以没办法保证顺序。
2. 读取
etcd 提供了多种读取方式,主要包括线性一致性读取、顺序一致性读取和最终一致性读取。下面将详细讲解这几种读取方式的读取流程。
2.1. 线性一致性读取(Linearizable Read)
1、流程
- 发送请求到 Leader 节点:客户端将读取请求发送到集群中的 Leader 节点。
Leader 处理读取请求:
- Leader 节点将读取请求作为一个空写操作(no-op)写入 Raft 日志,确保读取操作在所有先前的写操作之后。
- Leader 节点等待这个空写操作被多数节点(包括自己)确认和提交。
- 读取最新数据:当空写操作被提交后,Leader 节点执行读取操作,确保返回的数据是最新的。
- 返回结果给客户端:Leader 节点将读取结果返回给客户端。
2、特点
- 强一致性:确保读取的数据是最新的,反映了所有先前的写操作。
- 较高的延迟:需要与多数节点通信并确认空写操作,因此延迟较高。
2.2. 顺序一致性读取(Sequential Read)
1、流程
- 发送请求到 Leader 节点:客户端将读取请求发送到集群中的 Leader 节点。
Leader 处理读取请求:
- Leader 节点直接从本地状态读取数据,并返回给客户端。
- 由于 Leader 节点已经确保了操作的顺序性,该读取操作是顺序一致的。
2、特点
- 强一致性:读取操作遵循操作的顺序性,但不一定是最新的。
- 较高的延迟:不需要与多数节点通信,性能较好。
2.3. 最终一致性读取(Eventually Consistent Read)
1、流程
- 发送请求到 Leader 节点:客户端可以将读取请求发送到集群中的任意节点(包括 Leader 和 Follower)。
节点处理读取请求
- 接受请求的节点直接从本地状态读取数据,并返回给客户端。
- 由于读取的数据可能不同步,因此数据一致性较弱。
2、特点
- 一致性较弱:返回的数据可能不是最新的,但在一段时间后会达到一致性。
- 性能最佳:可以从任意节点读取数据,延迟最低,适用于对一致性要求不高的场景。
2.4. 代码示例
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.ReadOption;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class EtcdReadExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
String endpoint = "http://localhost:2379"; // etcd 服务器地址
String key = "your-key"; // 键名
// 创建 etcd 客户端实例
Client client = Client.builder().endpoints(endpoint).build();
try {
// 线性一致性读取(默认)
System.out.println("Linearizable Read (default):");
linearizableRead(client, key);
// 顺序一致性读取
System.out.println("\nSequential Read:");
sequentialRead(client, key);
// 最终一致性读取
System.out.println("\nEventually Consistent Read:");
eventuallyConsistentRead(client, key);
} finally {
// 关闭客户端连接
client.close();
}
}
// 线性一致性读取(默认方式)
private static void linearizableRead(Client client, String key) throws ExecutionException, InterruptedException {
// 调用通用读取方法,不传递任何特殊选项
performRead(client, key);
}
// 顺序一致性读取
private static void sequentialRead(Client client, String key) throws ExecutionException, InterruptedException {
// 设置读取选项为顺序一致性
ReadOption readOption = ReadOption.newBuilder().withConsistency(ReadOption.Consistency.SERIALIZABLE).build();
performRead(client, key, readOption);
}
// 最终一致性读取
private static void eventuallyConsistentRead(Client client, String key) throws ExecutionException, InterruptedException {
// 设置读取选项为最终一致性
GetOption getOption = GetOption.newBuilder().withConsistency(GetOption.Consistency.KUS).build();
performRead(client, key, getOption);
}
// 执行读取操作并输出结果(不传递选项,使用默认的线性一致性读取)
private static void performRead(Client client, String key) throws ExecutionException, InterruptedException {
// 执行读取操作(默认的线性一致性读取)
CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, "UTF-8"));
// 获取读取结果
GetResponse response = responseFuture.get();
// 输出读取到的值
if (response.getKvs().size() > 0) {
System.out.println("Key: " + response.getKvs().get(0).getKey().toStringUtf8());
System.out.println("Value: " + response.getKvs().get(0).getValue().toStringUtf8());
} else {
System.out.println("Key not found");
}
}
// 执行读取操作并输出结果(传递读取选项)
private static void performRead(Client client, String key, GetOption getOption) throws ExecutionException, InterruptedException {
// 执行读取操作
CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, "UTF-8"), getOption);
// 获取读取结果
GetResponse response = responseFuture.get();
// 输出读取到的值
if (response.getKvs().size() > 0) {
System.out.println("Key: " + response.getKvs().get(0).getKey().toStringUtf8());
System.out.println("Value: " + response.getKvs().get(0).getValue().toStringUtf8());
} else {
System.out.println("Key not found");
}
}
// 重载方法,适配 ReadOption
private static void performRead(Client client, String key, ReadOption readOption) throws ExecutionException, InterruptedException {
// 执行读取操作
CompletableFuture<GetResponse> responseFuture = client.getKVClient().get(ByteSequence.from(key, "UTF-8"), readOption);
// 获取读取结果
GetResponse response = responseFuture.get();
// 输出读取到的值
if (response.getKvs().size() > 0) {
System.out.println("Key: " + response.getKvs().get(0).getKey().toStringUtf8());
System.out.println("Value: " + response.getKvs().get(0).getValue().toStringUtf8());
} else {
System.out.println("Key not found");
}
}
}
2.5. 总结
通过代码可以看出,etcd默认的读取方式是线行一致性读取,说明etcd更侧重强一致性的场景。
三种读取方式一致性由弱到强分别为:
1、最终一致性读取
客户端可以将读取请求发送到集群中的任意节点(包括 Leader 和 Follower)。
如果读取请求到了leader节点,或者是同步到leader最新日志的follower节点,其实和“顺序一致性读取”一样。
但如果读取请求到了一个落后leader日志的follower节点(因为网络暂时隔离等情况),那么读取到的数据就是过时的。基于 raft算法,要等到后续网络等恢复之后,该节点才会同步到最新日志。
该节点的好处在于“读写分离”,当高并发读取时,如果都请求leader节点会扛不住。
2、顺序一致性读取
客户端的请求都发送在leader节点,所以相比较前者而言,能保障读到的是当前已提交的最新的数据。
但是etcd 内部使用读写锁机制,因此读操作不会与正在进行的写操作冲突,当有一批非事务的读写命令请求时,没办法严格保障执行顺序。
3、线性一致性读取
读取操作被伪装成空写操作,也会有大多数follower节点的确认。在写入的章节说过,etcd实现了所有节点写入的顺序一致性,因此该读取方式,也实现了严格的顺序。
但缺点就是,所有请求都交给leader处理,读取请求都需要大多数follower节点确认,所以网络消耗较高,高并发时有瓶颈。
2.6. zookeeper的读取
zookeeper和etcd不同,zookeeper 的读取操作默认是从 follower 节点进行的。
- Leader 节点:负责处理所有写操作,并将写操作通过 Zab 协议(ZooKeeper Atomic Broadcast)广播给所有 follower 节点。
- Follower 节点:负责处理读取操作和转发写操作请求给 leader 节点。
读取操作的默认行为是从 follower 节点进行,但这也意味着读取操作可能会有一定的延迟,因为 follower 节点需要从 leader 节点同步最新的数据。为了确保读取的一致性,ZooKeeper 提供了 sync 方法,可以强制客户端从 leader 节点同步最新的更新。
sync 方法
sync 方法可以确保在读取操作之前,所有之前的写操作都已经被应用到 follower 节点。使用 sync 可以强制客户端在读取数据前,同步最新的状态,从而实现强一致性的读取。
3. 监听
etcd 的 watch 功能用于监听键或键的前缀,以便在这些键发生变化时通知客户端。watch 是 etcd 中非常强大和常用的功能,可以用来实现服务发现、配置管理和分布式锁等功能。
实际应用场景
- 配置管理:可以通过 watch 机制监听配置变化,当配置发生变化时,及时通知相应的服务进行更新。
- 服务发现:通过 watch 机制监听服务注册信息的变化,当有新服务注册或服务下线时,及时通知客户端进行相应处理。
- 分布式锁:通过 watch 机制监听锁的状态变化,实现分布式锁的抢占和释放。
3.1. 内部实现
1、修订版本(Revision)
在 etcd 中,每次写操作都会生成一个新的修订版本(Revision)。这个修订版本是一个递增的数字,用于标识每次事务提交的顺序。watch 机制利用这个修订版本来跟踪键值的变化。
2、事件存储和转发
当客户端创建 watch 时,etcd 会将 watch 请求转发到当前的 leader 节点。leader 节点会将 watch 请求添加到其内部的监听列表中。
当有新的写操作提交时,leader 节点会检查是否有 watch 监听这个键或键前缀。如果有,leader 节点会将相应的事件通知给客户端。
3、异步通知
etcd 的 watch 机制是异步的。当有新的事件生成时,etcd 会异步地将事件推送给客户端。这种异步通知机制可以提高系统的响应速度和并发处理能力。
3.2. 断点续传
etcd 提供了断点续传功能,确保在客户端连接中断后重新连接时,不会丢失变更事件。具体实现通过以下几个步骤:
- 指定修订版本:客户端在发起 watch 请求时,可以指定从哪个修订版本开始监听。
- 事件缓存:leader 节点会缓存一段时间内的事件,以便在客户端重新连接时提供这些事件。
- 重新连接:客户端在重新连接时,可以通过指定上次接收到的最后一个事件的修订版本,继续接收从该修订版本后的所有事件。
缓存事件时间
etcd 的事件缓存机制主要是为了确保在客户端重新连接时,不会丢失在断连期间的事件。缓存的时间和事件数量由 etcd 的内部实现和配置决定
- 事件保留时间:默认情况下,etcd 会保留最近一段时间内的事件。这段时间通常是配置的 etcd 副本保留时间(compaction interval)。
- 事件数量限制:缓存的事件数量也受到内存和其他资源的限制,以防止内存占用过多。
etcd 提供了一些配置项来调节事件的保留时间和缓存策略。
retention configuration(保留配置)
--auto-compaction-retention:这个参数用于指定自动压缩的保留时间间隔。压缩会删除超过这个时间间隔的历史修订版本和事件。
如果设置为 1 小时(--auto-compaction-retention=1h),那么 etcd 会保留过去 1 小时内的所有事件。
Compaction(压缩)
etcd 提供了手动和自动的压缩机制,通过删除旧的修订版本和事件来节省空间。
自动压缩可以通过配置 --auto-compaction-mode 和 --auto-compaction-retention 参数来实现。
3.3. 代码示例
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.Watch;
import io.etcd.jetcd.Watch.Watcher;
import io.etcd.jetcd.watch.WatchEvent;
import io.etcd.jetcd.watch.WatchResponse;
import io.etcd.jetcd.options.WatchOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class EtcdWatchService {
private static final Logger LOGGER = LoggerFactory.getLogger(EtcdWatchService.class);
@Value("${etcd.endpoints}")
private String etcdEndpoints;
private Client client;
private Watcher watcher;
private AtomicLong lastRevision = new AtomicLong(0);
private volatile boolean shouldReconnect = true;
private static final int MAX_RETRIES = 5; // 最大重试次数
private int retryCount = 0;
private long baseDelay = 1000; // 基础延迟时间(毫秒)
@PostConstruct
public void init() {
client = Client.builder().endpoints(etcdEndpoints.split(",")).build();
startWatching();
}
@PreDestroy
public void cleanup() {
shouldReconnect = false;
if (watcher != null) {
watcher.close();
}
if (client != null) {
client.close();
}
}
private void startWatching() {
ByteSequence key = ByteSequence.from("my-key", StandardCharsets.UTF_8);
WatchOption watchOption = WatchOption.newBuilder().withRevision(lastRevision.get()).build();
watcher = client.getWatchClient().watch(key, watchOption, new Watch.Listener() {
@Override
public void onNext(WatchResponse response) {
response.getEvents().forEach(event -> {
switch (event.getEventType()) {
case PUT:
LOGGER.info("PUT event: {} -> {}", event.getKeyValue().getKey(),
event.getKeyValue().getValue().toString(StandardCharsets.UTF_8));
break;
case DELETE:
LOGGER.info("DELETE event: {}", event.getKeyValue().getKey());
break;
}
// 更新最后的修订版本号
lastRevision.set(event.getKeyValue().getModRevision());
});
}
@Override
public void onError(Throwable throwable) {
LOGGER.error("Watch error: {}", throwable.getMessage());
throwable.printStackTrace();
if (shouldReconnect && retryCount < MAX_RETRIES) {
reconnect();
} else {
LOGGER.error("Max retries reached, will not attempt to reconnect.");
}
}
@Override
public void onCompleted() {
LOGGER.info("Watch completed");
}
});
}
private void reconnect() {
try {
long delay = (long) (baseDelay * Math.pow(2, retryCount)); // 指数退避
LOGGER.info("Reconnecting watch in {} ms...", delay);
cleanupWatcher();
Thread.sleep(delay);
startWatching();
retryCount++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOGGER.error("Reconnect interrupted", e);
}
}
private void cleanupWatcher() {
if (watcher != null) {
watcher.close();
}
}
}
lastRevision
在每次监听到写入消息后,都需要记录最新的revision,这样当下次重连时传入lastRevision,可以继续基于上次的版本继续消费缓存的事件。
Watch.Listener 接口三个方法
- onNext(WatchResponse response): 当被监听的键发生变化时,etcd 会调用此方法。
- onError(Throwable throwable): 当发生错误时,etcd 会调用此方法。
- onCompleted(): 当 watch 完成时,etcd 会调用此方法
onError的场景
etcd 的 Watch.Listener 中的 onError 方法会在各种异常和错误情况下被触发,这些情况包括但不限于网络问题、连接断开、权限问题等。包括:
网络问题
- 网络中断:如果客户端与 etcd 服务器之间的网络连接中断或不稳定,会触发 onError 方法。
- 超时:如果网络请求超时,导致无法接收到 etcd 服务器的响应,也会触发 onError。
服务端问题
- etcd 服务器重启:如果 etcd 服务器重启,现有的 watch 连接会中断,触发 onError 方法。
- etcd Leader 变更:在 etcd 集群中,如果 leader 节点发生变更,现有的 watch 连接可能会中断,触发 onError 方法。
客户端问题
- 客户端主动关闭:如果客户端主动关闭连接,也会触发 onError 方法。
- 客户端资源耗尽:如果客户端资源(如线程、文件描述符等)耗尽,可能导致 watch 连接失败,触发 onError 方法。
权限问题
- 权限不足:如果客户端对所 watch 的键或键前缀没有足够的权限,也会导致 watch 失败,触发 onError 方法。
3.4. 对比zookeeper
zookeeper 的 watch 机制是一种一次性触发的通知机制,允许客户端在指定的节点(znode)上设置 watch,当 znode 的数据或子节点状态发生变化时,客户端会收到通知。
1、基本特性
- 一次性触发:ZooKeeper 的 watch 是一次性的,即当 watch 被触发后,它就会失效。如果客户端需要继续监视该 znode 的变化,需要重新设置 watch。
- 没有事件重放机制:ZooKeeper 不像 Etcd,没有事件重放机制,这意味着如果因为连接中断或程序错误等,错过了某个消息事件,重新监听后也不会再收到原事件。
- 注册的位置:watch 可以注册在数据读取操作(如 getData 或 getChildren)上,当这些操作返回结果时,watch 会被注册到指定的 znode 上。
2、没有断点续传
ZooKeeper 的 watch 机制本身并不直接支持断点续传(即断连后重新连接时能够继续监听断连期间的事件)。
这是因为 ZooKeeper 的 watch 是一次性触发的,当 watch 被触发后,它就会失效。如果客户端需要继续监视该 znode 的变化,需要重新设置 watch。而且 ZooKeeper 的 watch是客户端实现的,并不像etcd,会在服务端将事件缓存一段时间。
ZooKeeper 的设计哲学是一次性触发 watch,它更像是一种通知机制,而不是持续的订阅。
代码示例
zookeeper watch代码:
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Service
public class ZooKeeperWatchService implements Watcher {
private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperWatchService.class);
@Autowired
private ZooKeeper zooKeeper;
private final String watchedNodePath = "/my-node";
@PostConstruct
public void init() {
try {
watchNode(watchedNodePath);
} catch (KeeperException | InterruptedException e) {
LOGGER.error("Error initializing ZooKeeper watch", e);
}
}
@PreDestroy
public void cleanup() throws InterruptedException {
if (zooKeeper != null) {
zooKeeper.close();
}
}
private void watchNode(String path) throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists(path, this);
if (stat != null) {
byte[] data = zooKeeper.getData(path, this, stat);
LOGGER.info("Current data at {}: {}", path, new String(data));
} else {
LOGGER.warn("Node {} does not exist", path);
}
}
@Override
public void process(WatchedEvent event) {
LOGGER.info("Received event: {}", event);
try {
if (event.getType() == Event.EventType.NodeDataChanged) {
// Data has changed, watch the node again to keep watching for future changes
watchNode(event.getPath());
} else if (event.getType() == Event.EventType.NodeCreated || event.getType() == Event.EventType.NodeDeleted) {
// For NodeCreated and NodeDeleted events, re-watch the parent node
watchNode(watchedNodePath);
} else if (event.getState() == Event.KeeperState.SyncConnected) {
// Re-connect to ZooKeeper and re-watch the node
watchNode(watchedNodePath);
}
} catch (KeeperException | InterruptedException e) {
LOGGER.error("Error watching node", e);
}
}
}
process
方法是 Watcher 接口的实现方法,用于处理来自 ZooKeeper 的 WatchedEvent- 在每次监听到消息后,都需要重新监听节点。调用
zooKeeper.getData(path, this, stat);
获取节点数据,同时设置数据变化的 watch。this
再次表示当前实现 Watcher 接口的实例。
3. Etcd 更优势的方面
- 高并发时更好:Etcd提供了一个事件流接口,允许客户端连续接收变化通知。这种设计在处理大量连续变化时非常高效,因为它避免了 ZooKeeper 频繁的连接和断开连接操作,减少了网络和服务器端的负担。
- 消息事件不容易丢失:Etcd支持事件重放,在客户端重新连接后,还可以断点续传。另外 ZooKeeper 的监听是一次性触发,然后再重新注册,如果在这时间窗口之间有变更事件,同样也会丢失。
4. 二者擅长场景
(1)ZooKeeper 的 Watch 机制适用场景
- 事件触发简单且可预测的场景:ZooKeeper的watch是一次性触发的,这适合于事件变化不频繁,且每次变化后客户端可以简单地重新注册watch的场景。例如,在分布式锁或领导者选举中,某个关键节点的变化通常是较少且重要的,watch的简化触发机制已足够。
(2)Etcd 的 Watch 机制适用场景:
- 高频次事件通知的场景:持续更新和自动重新设置,etcd的watch支持持久连接和事件流,这适合于频繁变化的环境,如动态服务发现和配置管理,客户端无需手动重新注册watch。
- 需要事件历史和重放能力的场景:事件可靠性:,etcd允许客户端在重新连接后获取自上次断开以来的所有事件,这对于需要确保事件不丢失的系统至关重要,例如在分布式缓存更新中。
4. 租约
租约的核心概念是为键值对设定一个生存时间(TTL),当租约到期时,绑定到该租约的所有键值对都会自动删除。
Etcd 租约的基本操作
- 创建租约:为键值对分配一个租约,并设置 TTL。
- 绑定键值对到租约:将键值对绑定到租约上,以便在租约到期时自动删除这些键值对。一个租约可以绑定多个键,当租约到期后都会被删除。
- 续期租约:在租约到期之前,通过续期操作延长租约的生存时间。注意etcd并不能续期指定时间的租约,而是将租约重置回原来的时间。
- 释放租约:显式释放租约。
代码示例
import com.google.protobuf.ByteString;
import com.ibm.etcd.api.KeyValue;
import com.ibm.etcd.api.RangeResponse;
import com.ibm.etcd.client.EtcdClient;
import com.ibm.etcd.client.lease.LeaseClient;
import com.ibm.etcd.client.kv.KvClient;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.Lease;
import io.etcd.jetcd.common.exception.EtcdException;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.PutOption;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
public class EtcdLeaseExample {
public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
// 创建 Etcd 客户端
Client client = Client.builder()
.endpoints("http://localhost:2379")
.build();
// 获取 KV 和 Lease 客户端
KvClient kvClient = client.getKvClient();
LeaseClient leaseClient = client.getLeaseClient();
// 创建租约,TTL 为 10 秒
long leaseId = leaseClient.grant(10).get().getID();
// 将键值对绑定到租约
kvClient.put(ByteSequence.from("key1", "UTF-8"), ByteString.copyFromUtf8("value1"))
.withLease(leaseId)
.get();
kvClient.put(ByteSequence.from("key2", "UTF-8"), ByteString.copyFromUtf8("value2"))
.withLease(leaseId)
.get();
// 续期租约
leaseClient.keepAlive(leaseId)
.thenAccept(response -> System.out.println("Lease kept alive"));
// 等待租约过期
Thread.sleep(15000);
// 检查键值对是否已删除
RangeResponse response1 = kvClient.get(ByteSequence.from("key1", "UTF-8")).get();
RangeResponse response2 = kvClient.get(ByteSequence.from("key2", "UTF-8")).get();
if (response1.getKvs().isEmpty()) {
System.out.println("Key1 is deleted");
} else {
System.out.println("Key1 still exists: " + response1.getKvs().get(0).getValue().toStringUtf8());
}
if (response2.getKvs().isEmpty()) {
System.out.println("Key2 is deleted");
} else {
System.out.println("Key2 still exists: " + response2.getKvs().get(0).getValue().toStringUtf8());
}
// 释放租约
leaseClient.revoke(leaseId).get();
// 关闭 Etcd 客户端
client.close();
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。