开篇

其实这篇文章我本来想在讲完选举的时候就开始讲线性一致性读的,但是感觉直接讲没头没尾的看起来比比较困难,所以就有了RheaKV的系列,这是RheaKV,终于可以讲一下SOFAJRaft的线性一致性读是怎么做到了的。所谓线性一致性,一个简单的例子是在 T1 的时间写入一个值,那么在 T1 之后读一定能读到这个值,不可能读到 T1 之前的值。

其中部分内容参考SOFAJRaft文档:

SOFAJRaft 线性一致读实现剖析 | SOFAJRaft 实现原理

SOFAJRaft 实现原理 - SOFAJRaft-RheaKV 是如何使用 Raft 的

RheaKV读取数据

RheaKV的读取数据的入口是DefaultRheaKVStore的bGet。

DefaultRheaKVStore#bGet

  1. public byte[] bGet(final String key) {
  2. return FutureHelper.get(get(key), this.futureTimeoutMillis);
  3. }

bGet方法中会一直调用到DefaultRheaKVStore的一个get方法中:

DefaultRheaKVStore#get

  1. private CompletableFuture<byte[]> get(final byte[] key, final boolean readOnlySafe,
  2. final CompletableFuture<byte[]> future, final boolean tryBatching) {
  3. //校验started状态
  4. checkState();
  5. Requires.requireNonNull(key, "key");
  6. if (tryBatching) {
  7. final GetBatching getBatching = readOnlySafe ? this.getBatchingOnlySafe : this.getBatching;
  8. if (getBatching != null && getBatching.apply(key, future)) {
  9. return future;
  10. }
  11. }
  12. internalGet(key, readOnlySafe, future, this.failoverRetries, null, this.onlyLeaderRead);
  13. return future;
  14. }

get方法会根据传入的参数来判断是否采用批处理的方式来读取数据,readOnlySafe表示是否开启线程一致性读,由于我们调用的是get方法,所以readOnlySafe和tryBatching都会返回true。

所以这里会调用getBatchingOnlySafe的apply方法,将key和future传入。

getBatchingOnlySafe是在我们初始化DefaultRheaKVStore的时候初始化的:

DefaultRheaKVStore#init

  1. .....
  2. this.getBatchingOnlySafe = new GetBatching(KeyEvent::new, "get_batching_only_safe",
  3. new GetBatchingHandler("get_only_safe", true));
  4. .....

在初始化getBatchingOnlySafe的时候传入的处理器是GetBatchingHandler。

然后我们回到getBatchingOnlySafe#apply中,看看这个方法做了什么:

  1. public boolean apply(final byte[] message, final CompletableFuture<byte[]> future) {
  2. //GetBatchingHandler
  3. return this.ringBuffer.tryPublishEvent((event, sequence) -> {
  4. event.reset();
  5. event.key = message;
  6. event.future = future;
  7. });
  8. }

apply方法会向Disruptor发送一个事件进行异步处理,并把我们的key封装到event的key中。getBatchingOnlySafe的处理器是GetBatchingHandler。

批量获取数据

GetBatchingHandler#onEvent

  1. public void onEvent(final KeyEvent event, final long sequence, final boolean endOfBatch) throws Exception {
  2. this.events.add(event);
  3. this.cachedBytes += event.key.length;
  4. final int size = this.events.size();
  5. //校验一下数据量,没有达到MaxReadBytes并且不是最后一个event,那么直接返回
  6. if (!endOfBatch && size < batchingOpts.getBatchSize() && this.cachedBytes < batchingOpts.getMaxReadBytes()) {
  7. return;
  8. }
  9. if (size == 1) {
  10. reset();
  11. try {
  12. //如果只是一个get请求,那么不需要进行批量处理
  13. get(event.key, this.readOnlySafe, event.future, false);
  14. } catch (final Throwable t) {
  15. exceptionally(t, event.future);
  16. }
  17. } else {
  18. //初始化一个刚刚好大小的集合
  19. final List<byte[]> keys = Lists.newArrayListWithCapacity(size);
  20. final CompletableFuture<byte[]>[] futures = new CompletableFuture[size];
  21. for (int i = 0; i < size; i++) {
  22. final KeyEvent e = this.events.get(i);
  23. keys.add(e.key);
  24. futures[i] = e.future;
  25. }
  26. //遍历完events数据到entries之后,重置
  27. reset();
  28. try {
  29. multiGet(keys, this.readOnlySafe).whenComplete((result, throwable) -> {
  30. //异步回调处理数据
  31. if (throwable == null) {
  32. for (int i = 0; i < futures.length; i++) {
  33. final ByteArray realKey = ByteArray.wrap(keys.get(i));
  34. futures[i].complete(result.get(realKey));
  35. }
  36. return;
  37. }
  38. exceptionally(throwable, futures);
  39. });
  40. } catch (final Throwable t) {
  41. exceptionally(t, futures);
  42. }
  43. }
  44. }
  45. }

onEvent方法首先会校验一下当前的event数量有没有达到阈值以及当前的event是不是Disruptor中最后一个event;然后会根据不同的events集合中的数量来走不同的实现,这里做了一个优化,如果是只有一条数据那么不会走批处理;最后将所有的key放入到keys集合中并调用multiGet进行批处理。

multiGet方法会调用internalMultiGet返回一个Future,从而实现异步的返回结果。

DefaultRheaKVStore#internalMultiGet

  1. private FutureGroup<Map<ByteArray, byte[]>> internalMultiGet(final List<byte[]> keys, final boolean readOnlySafe,
  2. final int retriesLeft, final Throwable lastCause) {
  3. //因为不同的key是存放在不同的region中的,所以一个region会对应多个key,封装到map中
  4. final Map<Region, List<byte[]>> regionMap = this.pdClient
  5. .findRegionsByKeys(keys, ApiExceptionHelper.isInvalidEpoch(lastCause));
  6. //返回值
  7. final List<CompletableFuture<Map<ByteArray, byte[]>>> futures =
  8. Lists.newArrayListWithCapacity(regionMap.size());
  9. //lastCause传入为null
  10. final Errors lastError = lastCause == null ? null : Errors.forException(lastCause);
  11. for (final Map.Entry<Region, List<byte[]>> entry : regionMap.entrySet()) {
  12. final Region region = entry.getKey();
  13. final List<byte[]> subKeys = entry.getValue();
  14. //重试次数减1,设置一个重试函数
  15. final RetryCallable<Map<ByteArray, byte[]>> retryCallable = retryCause -> internalMultiGet(subKeys,
  16. readOnlySafe, retriesLeft - 1, retryCause);
  17. final MapFailoverFuture<ByteArray, byte[]> future = new MapFailoverFuture<>(retriesLeft, retryCallable);
  18. //发送MultiGetRequest请求,获取数据
  19. internalRegionMultiGet(region, subKeys, readOnlySafe, future, retriesLeft, lastError, this.onlyLeaderRead);
  20. futures.add(future);
  21. }
  22. return new FutureGroup<>(futures);
  23. }

internalMultiGet里会根据key去组装region,不同的key会对应不同的region,数据时存在region中的,所以要从不同的region中获取数据,region和key是一对多的关系所以这里会封装成一个map。然后会遍历regionMap,每个region所对应的数据作为一个批次调用到internalRegionMultiGet方法中,根据不同的情况获取数据。

DefaultRheaKVStore#internalRegionMultiGet

  1. private void internalRegionMultiGet(final Region region, final List<byte[]> subKeys, final boolean readOnlySafe,
  2. final CompletableFuture<Map<ByteArray, byte[]>> future, final int retriesLeft,
  3. final Errors lastCause, final boolean requireLeader) {
  4. //因为当前的是client,所以这里会是null
  5. final RegionEngine regionEngine = getRegionEngine(region.getId(), requireLeader);
  6. // require leader on retry
  7. //设置重试函数
  8. final RetryRunner retryRunner = retryCause -> internalRegionMultiGet(region, subKeys, readOnlySafe, future,
  9. retriesLeft - 1, retryCause, true);
  10. final FailoverClosure<Map<ByteArray, byte[]>> closure = new FailoverClosureImpl<>(future,
  11. false, retriesLeft, retryRunner);
  12. if (regionEngine != null) {
  13. if (ensureOnValidEpoch(region, regionEngine, closure)) {
  14. //如果不是null,那么会获取rawKVStore,并从中获取数据
  15. final RawKVStore rawKVStore = getRawKVStore(regionEngine);
  16. if (this.kvDispatcher == null) {
  17. rawKVStore.multiGet(subKeys, readOnlySafe, closure);
  18. } else {
  19. //如果是kvDispatcher不为空,那么放入到kvDispatcher中异步执行
  20. this.kvDispatcher.execute(() -> rawKVStore.multiGet(subKeys, readOnlySafe, closure));
  21. }
  22. }
  23. } else {
  24. final MultiGetRequest request = new MultiGetRequest();
  25. request.setKeys(subKeys);
  26. request.setReadOnlySafe(readOnlySafe);
  27. request.setRegionId(region.getId());
  28. request.setRegionEpoch(region.getRegionEpoch());
  29. //调用rpc请求
  30. this.rheaKVRpcService.callAsyncWithRpc(request, closure, lastCause, requireLeader);
  31. }
  32. }

因为我们这里是client端调用internalRegionMultiGet方法的,所以是没有设置regionEngine的,那么会直接向server的当前region所对应的leader节点发送一个MultiGetRequest请求。

因为上面的这些方法基本上和put是一致的,我们已经在5. SOFAJRaft源码分析— RheaKV中如何存放数据?讲过了,所以这里不重复的讲了。

server端处理MultiGetRequest请求

MultiGetRequest请求会被KVCommandProcessor所处理,KVCommandProcessor里会根据请求的magic方法返回值来判断是用什么方式来进行处理。我们这里会调用到DefaultRegionKVService的handleMultiGetRequest方法中处理请求。

  1. public void handleMultiGetRequest(final MultiGetRequest request,
  2. final RequestProcessClosure<BaseRequest, BaseResponse<?>> closure) {
  3. final MultiGetResponse response = new MultiGetResponse();
  4. response.setRegionId(getRegionId());
  5. response.setRegionEpoch(getRegionEpoch());
  6. try {
  7. KVParameterRequires.requireSameEpoch(request, getRegionEpoch());
  8. final List<byte[]> keys = KVParameterRequires.requireNonEmpty(request.getKeys(), "multiGet.keys");
  9. //调用MetricsRawKVStore的multiGet方法
  10. this.rawKVStore.multiGet(keys, request.isReadOnlySafe(), new BaseKVStoreClosure() {
  11. @SuppressWarnings("unchecked")
  12. @Override
  13. public void run(final Status status) {
  14. if (status.isOk()) {
  15. response.setValue((Map<ByteArray, byte[]>) getData());
  16. } else {
  17. setFailure(request, response, status, getError());
  18. }
  19. closure.sendResponse(response);
  20. }
  21. });
  22. } catch (final Throwable t) {
  23. LOG.error("Failed to handle: {}, {}.", request, StackTraceUtil.stackTrace(t));
  24. response.setError(Errors.forException(t));
  25. closure.sendResponse(response);
  26. }
  27. }

handleMultiGetRequest方法会调用MetricsRawKVStore的multiGet方法来批量获取数据。

MetricsRawKVStore#multiGet

  1. public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
  2. //实例化MetricsKVClosureAdapter对象
  3. final KVStoreClosure c = metricsAdapter(closure, MULTI_GET, keys.size(), 0);
  4. //调用RaftRawKVStore的multiGet方法
  5. this.rawKVStore.multiGet(keys, readOnlySafe, c);
  6. }

multiGet方法会传入一个MetricsKVClosureAdapter实例,通过这个实例实现异步回调response。然后调用RaftRawKVStore的multiGet方法。

RaftRawKVStore#multiGet

  1. public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
  2. if (!readOnlySafe) {
  3. this.kvStore.multiGet(keys, false, closure);
  4. return;
  5. }
  6. // KV 存储实现线性一致读
  7. // 调用 readIndex 方法,等待回调执行
  8. this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
  9. @Override
  10. public void run(final Status status, final long index, final byte[] reqCtx) {
  11. //如果状态返回成功,
  12. if (status.isOk()) {
  13. RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
  14. return;
  15. }
  16. //readIndex 读取失败尝试应用键值读操作申请任务于 Leader 节点的状态机 KVStoreStateMachine
  17. RaftRawKVStore.this.readIndexExecutor.execute(() -> {
  18. if (isLeader()) {
  19. LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
  20. status);
  21. // If 'read index' read fails, try to applying to the state machine at the leader node
  22. applyOperation(KVOperation.createMultiGet(keys), closure);
  23. } else {
  24. LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
  25. // Client will retry to leader node
  26. new KVClosureAdapter(closure, null).run(status);
  27. }
  28. });
  29. }
  30. });
  31. }

multiGet调用node的readIndex方法进行一致性读操作,并设置回调,如果返回成功那么就直接调用RocksRawKVStore读取数据,如果返回不是成功那么申请任务于 Leader 节点的状态机 KVStoreStateMachine。

线性一致性读readIndex

所谓线性一致读,一个简单的例子是在 t1 的时刻我们写入了一个值,那么在 t1 之后,我们一定能读到这个值,不可能读到 t1 之前的旧值(想想 Java 中的 volatile 关键字,即线性一致读就是在分布式系统中实现 Java volatile 语义)。简而言之是需要在分布式环境中实现 Java volatile 语义效果,即当 Client 向集群发起写操作的请求并且获得成功响应之后,该写操作的结果要对所有后来的读请求可见。和 volatile 的区别在于 volatile 是实现线程之间的可见,而 SOFAJRaft 需要实现 Server 之间的可见。

SOFAJRaft提供的线性一致读是基于 Raft 协议的 ReadIndex 实现用 ;Node#readIndex(byte [] requestContext, ReadIndexClosure done) 发起线性一致读请求,当安全读取时传入的 Closure 将被调用,正常情况从状态机中读取数据返回给客户端。

Node#readIndex

  1. public void readIndex(final byte[] requestContext, final ReadIndexClosure done) {
  2. if (this.shutdownLatch != null) {
  3. //异步执行回调
  4. Utils.runClosureInThread(done, new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
  5. throw new IllegalStateException("Node is shutting down");
  6. }
  7. Requires.requireNonNull(done, "Null closure");
  8. //EMPTY_BYTES
  9. this.readOnlyService.addRequest(requestContext, done);
  10. }

readIndex会调用ReadOnlyServiceImpl#addRequest将requestContext和回调方法done传入,requestContext传入的是BytesUtil.EMPTY_BYTES

接着往下看

ReadOnlyServiceImpl#addRequest

  1. public void addRequest(final byte[] reqCtx, final ReadIndexClosure closure) {
  2. if (this.shutdownLatch != null) {
  3. Utils.runClosureInThread(closure, new Status(RaftError.EHOSTDOWN, "Was stopped"));
  4. throw new IllegalStateException("Service already shutdown.");
  5. }
  6. try {
  7. EventTranslator<ReadIndexEvent> translator = (event, sequence) -> {
  8. event.done = closure;
  9. //EMPTY_BYTES
  10. event.requestContext = new Bytes(reqCtx);
  11. event.startTime = Utils.monotonicMs();
  12. };
  13. int retryTimes = 0;
  14. while (true) {
  15. //ReadIndexEventHandler
  16. if (this.readIndexQueue.tryPublishEvent(translator)) {
  17. break;
  18. } else {
  19. retryTimes++;
  20. if (retryTimes > MAX_ADD_REQUEST_RETRY_TIMES) {
  21. Utils.runClosureInThread(closure,
  22. new Status(RaftError.EBUSY, "Node is busy, has too many read-only requests."));
  23. this.nodeMetrics.recordTimes("read-index-overload-times", 1);
  24. LOG.warn("Node {} ReadOnlyServiceImpl readIndexQueue is overload.", this.node.getNodeId());
  25. return;
  26. }
  27. ThreadHelper.onSpinWait();
  28. }
  29. }
  30. } catch (final Exception e) {
  31. Utils.runClosureInThread(closure, new Status(RaftError.EPERM, "Node is down."));
  32. }
  33. }

addRequest方法里会将传入的reqCtx和closure封装成一个时间,传入到readIndexQueue队列中,事件发布成功后会交由ReadIndexEventHandler处理器处理,发布失败会进行重试,最多重试3次。

ReadIndexEventHandler

  1. private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
  2. // task list for batch
  3. private final List<ReadIndexEvent> events = new ArrayList<>(
  4. ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());
  5. @Override
  6. public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
  7. throws Exception {
  8. if (newEvent.shutdownLatch != null) {
  9. executeReadIndexEvents(this.events);
  10. this.events.clear();
  11. newEvent.shutdownLatch.countDown();
  12. return;
  13. }
  14. this.events.add(newEvent);
  15. //批量执行
  16. if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
  17. executeReadIndexEvents(this.events);
  18. this.events.clear();
  19. }
  20. }
  21. }

ReadIndexEventHandler是ReadOnlyServiceImpl里面的内部类,里面有一个全局的events集合用来做事件的批处理,如果当前的event已经达到了32个或是整个Disruptor队列里最后一个那么会调用ReadOnlyServiceImpl的executeReadIndexEvents方法进行事件的批处理。

ReadOnlyServiceImpl#executeReadIndexEvents

  1. private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
  2. if (events.isEmpty()) {
  3. return;
  4. }
  5. //初始化ReadIndexRequest
  6. final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
  7. .setGroupId(this.node.getGroupId()) //
  8. .setServerId(this.node.getServerId().toString());
  9. final List<ReadIndexState> states = new ArrayList<>(events.size());
  10. for (final ReadIndexEvent event : events) {
  11. rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
  12. states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
  13. }
  14. final ReadIndexRequest request = rb.build();
  15. this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
  16. }

executeReadIndexEvents封装好ReadIndexRequest请求和将ReadIndexState集合封装到ReadIndexResponseClosure中,为后续的操作做装备

NodeImpl#handleReadIndexRequest

  1. public void handleReadIndexRequest(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> done) {
  2. final long startMs = Utils.monotonicMs();
  3. this.readLock.lock();
  4. try {
  5. switch (this.state) {
  6. case STATE_LEADER:
  7. readLeader(request, ReadIndexResponse.newBuilder(), done);
  8. break;
  9. case STATE_FOLLOWER:
  10. readFollower(request, done);
  11. break;
  12. case STATE_TRANSFERRING:
  13. done.run(new Status(RaftError.EBUSY, "Is transferring leadership."));
  14. break;
  15. default:
  16. done.run(new Status(RaftError.EPERM, "Invalid state for readIndex: %s.", this.state));
  17. break;
  18. }
  19. } finally {
  20. this.readLock.unlock();
  21. this.metrics.recordLatency("handle-read-index", Utils.monotonicMs() - startMs);
  22. this.metrics.recordSize("handle-read-index-entries", request.getEntriesCount());
  23. }
  24. }

因为线性一致读在任何集群内的节点发起,并不需要强制要求放到 Leader 节点上,允许在 Follower 节点执行,因此大大降低 Leader 的读取压力。

当在Follower节点执行一致性读的时候实际上Follower 节点调用 RpcService#readIndex(leaderId.getEndpoint(), newRequest, -1, closure) 方法向 Leader 发送 ReadIndex 请求,交由Leader节点实现一致性读。所以我这里主要介绍Leader的一致性读。

继续往下走调用NodeImpl的readLeader方法

NodeImpl#readLeader

  1. private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
  2. final RpcResponseClosure<ReadIndexResponse> closure) {
  3. //1. 获取集群节点中多数选票数是多少
  4. final int quorum = getQuorum();
  5. if (quorum <= 1) {
  6. // Only one peer, fast path.
  7. //如果集群中只有一个节点,那么直接调用回调函数,返回成功
  8. respBuilder.setSuccess(true) //
  9. .setIndex(this.ballotBox.getLastCommittedIndex());
  10. closure.setResponse(respBuilder.build());
  11. closure.run(Status.OK());
  12. return;
  13. }
  14. final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
  15. //2. 任期必须相等
  16. //日志管理器 LogManager 基于投票箱 BallotBox 的 lastCommittedIndex 获取任期检查是否等于当前任期
  17. // 如果不等于当前任期表示此 Leader 节点未在其任期内提交任何日志,需要拒绝只读请求;
  18. if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
  19. // Reject read only request when this leader has not committed any log entry at its term
  20. closure
  21. .run(new Status(
  22. RaftError.EAGAIN,
  23. "ReadIndex request rejected because leader has not committed any log entry at its term, " +
  24. "logIndex=%d, currTerm=%d.",
  25. lastCommittedIndex, this.currTerm));
  26. return;
  27. }
  28. respBuilder.setIndex(lastCommittedIndex);
  29. if (request.getPeerId() != null) {
  30. // request from follower, check if the follower is in current conf.
  31. final PeerId peer = new PeerId();
  32. peer.parse(request.getServerId());
  33. //3. 来自 Follower 的请求需要检查 Follower 是否在当前配置
  34. if (!this.conf.contains(peer)) {
  35. closure
  36. .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer,
  37. this.conf));
  38. return;
  39. }
  40. }
  41. ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
  42. //4. 如果使用的是ReadOnlyLeaseBased,确认leader是否是在在租约有效时间内
  43. if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
  44. // If leader lease timeout, we must change option to ReadOnlySafe
  45. readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
  46. }
  47. switch (readOnlyOpt) {
  48. //5
  49. case ReadOnlySafe:
  50. final List<PeerId> peers = this.conf.getConf().getPeers();
  51. Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
  52. //设置心跳的响应回调函数
  53. final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
  54. respBuilder, quorum, peers.size());
  55. // Send heartbeat requests to followers
  56. //向 Followers 节点发起一轮 Heartbeat,如果半数以上节点返回对应的
  57. // Heartbeat Response,那么 Leader就能够确定现在自己仍然是 Leader
  58. for (final PeerId peer : peers) {
  59. if (peer.equals(this.serverId)) {
  60. continue;
  61. }
  62. this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
  63. }
  64. break;
  65. //6. 因为在租约期内不会发生选举,确保 Leader 不会变化
  66. //所以直接返回回调结果
  67. case ReadOnlyLeaseBased:
  68. // Responses to followers and local node.
  69. respBuilder.setSuccess(true);
  70. closure.setResponse(respBuilder.build());
  71. closure.run(Status.OK());
  72. break;
  73. }
  74. }
  1. 获取集群节点中多数选票数是多少,即集群节点的1/2+1,如果当前的集群里只有一个节点,那么直接返回成功,并调用回调方法
  2. 校验 Raft 集群节点数量以及 lastCommittedIndex 所属任期符合预期,那么响应构造器设置其索引为投票箱 BallotBox 的 lastCommittedIndex
  3. 来自 Follower 的请求需要检查 Follower 是否在当前配置,如果不在当前配置中直接调用回调方法设置异常
  4. 获取 ReadIndex 请求级别 ReadOnlyOption 配置,ReadOnlyOption 参数默认值为 ReadOnlySafe。如果设置的是ReadOnlyLeaseBased,那么会调用isLeaderLeaseValid检查leader是否是在在租约有效时间内
  5. 配置为ReadOnlySafe 调用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 节点发送 Heartbeat 心跳请求,发送心跳成功执行 ReadIndexHeartbeatResponseClosure 心跳响应回调;ReadIndex 心跳响应回调检查是否超过半数节点包括 Leader 节点自身投票赞成,半数以上节点返回客户端Heartbeat 请求成功响应,即 applyIndex 超过 ReadIndex 说明已经同步到 ReadIndex 对应的 Log 能够提供 Linearizable Read
  6. 配置为ReadOnlyLeaseBased,因为Leader 租约有效期间认为当前 Leader 是 Raft Group 内的唯一有效 Leader,所以忽略 ReadIndex 发送 Heartbeat 确认身份步骤,直接返回 Follower 节点和本地节点 Read 请求成功响应。Leader 节点继续等待状态机执行,直到 applyIndex 超过 ReadIndex 安全提供 Linearizable Read

无论是ReadOnlySafe还是ReadOnlyLeaseBased,最后发送成功响应都会调用ReadIndexResponseClosure的run方法。

ReadIndexResponseClosure#run

  1. public void run(final Status status) {
  2. //fail
  3. //传入的状态不是ok,响应失败
  4. if (!status.isOk()) {
  5. notifyFail(status);
  6. return;
  7. }
  8. final ReadIndexResponse readIndexResponse = getResponse();
  9. //Fail
  10. //response没有响应成功,响应失败
  11. if (!readIndexResponse.getSuccess()) {
  12. notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
  13. return;
  14. }
  15. // Success
  16. //一致性读成功
  17. final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
  18. readIndexResponse.getIndex());
  19. for (final ReadIndexState state : this.states) {
  20. // Records current commit log index.
  21. //设置当前提交的index
  22. state.setIndex(readIndexResponse.getIndex());
  23. }
  24. boolean doUnlock = true;
  25. ReadOnlyServiceImpl.this.lock.lock();
  26. try {
  27. //校验applyIndex 是否超过 ReadIndex
  28. if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
  29. // Already applied, notify readIndex request.
  30. ReadOnlyServiceImpl.this.lock.unlock();
  31. doUnlock = false;
  32. //已经同步到 ReadIndex 对应的 Log 能够提供 Linearizable Read
  33. notifySuccess(readIndexStatus);
  34. } else {
  35. // Not applied, add it to pending-notify cache.
  36. ReadOnlyServiceImpl.this.pendingNotifyStatus
  37. .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)) //
  38. .add(readIndexStatus);
  39. }
  40. } finally {
  41. if (doUnlock) {
  42. ReadOnlyServiceImpl.this.lock.unlock();
  43. }
  44. }
  45. }

Run方法首先会校验一下是否需要响应失败,如果响应成功,那么会将所有封装的ReadIndexState更新一下index,然后校验一下applyIndex 是否超过 ReadIndex,超过了ReadIndex代表所有已经复制到多数派上的 Log(可视为写操作)被视为安全的 Log,该 Log 所体现的数据就能对客户端 Client 可见。

ReadOnlyServiceImpl#notifySuccess

  1. private void notifySuccess(final ReadIndexStatus status) {
  2. final long nowMs = Utils.monotonicMs();
  3. final List<ReadIndexState> states = status.getStates();
  4. final int taskCount = states.size();
  5. for (int i = 0; i < taskCount; i++) {
  6. final ReadIndexState task = states.get(i);
  7. final ReadIndexClosure done = task.getDone(); // stack copy
  8. if (done != null) {
  9. this.nodeMetrics.recordLatency("read-index", nowMs - task.getStartTimeMs());
  10. done.setResult(task.getIndex(), task.getRequestContext().get());
  11. done.run(Status.OK());
  12. }
  13. }
  14. }

如果是响应成功,那么会调用notifySuccess方法,会将status里封装的ReadIndexState集合遍历一遍,调用当中的run方法。

这个run方法会调用到我们在multiGet中设置的run方法中

RaftRawKVStore#multiGet

  1. public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
  2. if (!readOnlySafe) {
  3. this.kvStore.multiGet(keys, false, closure);
  4. return;
  5. }
  6. // KV 存储实现线性一致读
  7. // 调用 readIndex 方法,等待回调执行
  8. this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
  9. @Override
  10. public void run(final Status status, final long index, final byte[] reqCtx) {
  11. //如果状态返回成功,
  12. if (status.isOk()) {
  13. RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
  14. return;
  15. }
  16. //readIndex 读取失败尝试应用键值读操作申请任务于 Leader 节点的状态机 KVStoreStateMachine
  17. RaftRawKVStore.this.readIndexExecutor.execute(() -> {
  18. if (isLeader()) {
  19. LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
  20. status);
  21. // If 'read index' read fails, try to applying to the state machine at the leader node
  22. applyOperation(KVOperation.createMultiGet(keys), closure);
  23. } else {
  24. LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
  25. // Client will retry to leader node
  26. new KVClosureAdapter(closure, null).run(status);
  27. }
  28. });
  29. }
  30. });

这个run方法会调用RaftRawKVStore的multiGet从RocksDB中直接获取数据。

总结

我们这篇文章从RheaKVStore的客户端get方法一直讲到,RheaKVStore服务端使用JRaft实现线性一致性读,并讲解了线性一致性读是怎么实现的,通过这个例子大家应该对线性一致性读有了一个相对不错的理解了。

6. SOFAJRaft源码分析— 透过RheaKV看线性一致性读的更多相关文章

  1. 4. SOFAJRaft源码分析— RheaKV初始化做了什么?

    前言 由于RheaKV要讲起来篇幅比较长,所以这里分成几个章节来讲,这一章讲一讲RheaKV初始化做了什么? 我们先来给个例子,我们从例子来讲: public static void main(fin ...

  2. 3. SOFAJRaft源码分析— 是如何进行选举的?

    开篇 在上一篇文章当中,我们讲解了NodeImpl在init方法里面会初始化话的动作,选举也是在这个方法里面进行的,这篇文章来从这个方法里详细讲一下选举的过程. 由于我这里介绍的是如何实现的,所以请大 ...

  3. 8. SOFAJRaft源码分析— 如何实现日志复制的pipeline机制?

    前言 前几天和腾讯的大佬一起吃饭聊天,说起我对SOFAJRaft的理解,我自然以为我是很懂了的,但是大佬问起了我那SOFAJRaft集群之间的日志是怎么复制的? 我当时哑口无言,说不出是怎么实现的,所 ...

  4. 9. SOFAJRaft源码分析— Follower如何通过Snapshot快速追上Leader日志?

    前言 引入快照机制主要是为了解决两个问题: JRaft新节点加入后,如何快速追上最新的数据 Raft 节点出现故障重新启动后如何高效恢复到最新的数据 Snapshot 源码分析 生成 Raft 节点的 ...

  5. 2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?

    看完这个实现之后,感觉还是要多看源码,多研究.其实JRaft的定时任务调度器是基于Netty的时间轮来做的,如果没有看过Netty的源码,很可能并不知道时间轮算法,也就很难想到要去使用这么优秀的定时调 ...

  6. 7. SOFAJRaft源码分析—如何实现一个轻量级的对象池?

    前言 我在看SOFAJRaft的源码的时候看到了使用了对象池的技术,看了一下感觉要吃透的话还是要新开一篇文章来讲,内容也比较充实,大家也可以学到之后运用到实际的项目中去. 这里我使用Recyclabl ...

  7. 解析$.grep()源码及透过$.grep()看(两次取反)!!的作用

    先上jquery源码: grep: function( elems, callback, inv ) { var retVal, ret = [], i = 0, length = elems.len ...

  8. 5. SOFAJRaft源码分析— RheaKV中如何存放数据?

    概述 上一篇讲了RheaKV是如何进行初始化的,因为RheaKV主要是用来做KV存储的,RheaKV读写的是相当的复杂,一起写会篇幅太长,所以这一篇主要来讲一下RheaKV中如何存放数据. 我们这里使 ...

  9. 1. SOFAJRaft源码分析— SOFAJRaft启动时做了什么?

    我们这次依然用上次的例子CounterServer来进行讲解: 我这里就不贴整个代码了 public static void main(final String[] args) throws IOEx ...

随机推荐

  1. Django跨域问题(CORS错误)

    Django跨域问题(CORS错误) 一.出现跨域问题(cors错误)的原因 通常情况下,A网页访问B服务器资源时,不满足以下三个条件其一就是跨域访问 协议不同 端口不同 主机不同 二.Django解 ...

  2. Ajax:后台jquery实现ajax无刷新删除数据及demo

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8gAAAFSCAIAAAChUmFZAAAgAElEQVR4nO29z4scWZbn2/+Hb30zi8

  3. [网络流 24 题] luoguP4016 负载平衡问题

    [返回网络流 24 题索引] 题目描述 有成环状的 nnn 堆纸牌,现将一张纸牌移动到其邻堆称为一次操作.求使得所有堆纸牌数相等的最少移动次数. Solution 4016\text{Solution ...

  4. selenium-模块概述(1)

    Selenium是一个用于Web应用程序自动化测试工具.Selenium测试直接运行在浏览器中,就像真正的用户在操作一样. 1.目录结构如下: D:\soft\python36\Lib\site-pa ...

  5. java23种设计模式(三)单例模式

    原文地址:https://zhuanlan.zhihu.com/p/23713957 一.概述 1.什么是单例模式? 百度百科是这样定义的:单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个 ...

  6. Eureka和zookeeper的比较

    什么是CAP? CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性). Availability(可用性).Partition tolerance(分区容错性),三者不 ...

  7. Spark执行流程(转)

       原文地址:http://blog.jobbole.com/102645/     我们使用spark-submit提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程.根据你使 ...

  8. Java基础(三)对象与类

    1.类的概念:类是构造对象的模板或蓝图.由类构造对象的过程称为创建类的实例. 2.封装的概念:封装(有时称为数据隐藏)是与对象有关的一个重要概念.对象中的数据称为实例域,操纵数据的过程称为方法.对于每 ...

  9. Redis(十)集群:Redis Cluster

    一.数据分布 1.数据分布理论 2.Redis数据分区 Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16 ...

  10. NetworkManager网络通讯_问题汇总(四)

    此篇来填坑,有些坑是unet自身问题,而大部分则是理解不准确造成的(或者unity定义太复杂) 问题一: isLocalPlayer 值一直是false 出现场景:NetworkLobbyPlayer ...