本文首发于 泊浮目的简书: https://www.jianshu.com/u/204...
版本 | 日期 | 备注 |
---|---|---|
1.0 | 2020.6.14 | 文章首发 |
对于一个分布式集群来讲,保证数据写入一致性最简单的方式就是依靠一个节点来调度和管理其余节点。在分布式中咱们通常称其为Leader。html
为何是最简单的方式呢?咱们想象一下,当咱们写数据到Leader时,Leader写入本身的一份数据后,可能会作副本到Follower,那么拷贝的数量、及所在的位置都由该Leader来控制。但若是是多Leader调度,就要涉及到数据分区,请求负载均衡等问题了。
今天,笔者就和你们一块儿来看看ZK的选举流程。java
这是一种典型的多数派算法,听名字就知道是为ZK而生了(Zookeeper Atomic Broadcast)。其Leader的选举主要关心节点的ID和数据ID,这两个属性越大,则表示数据越新,优先成为主。算法
常见由两种场景触发选举,不管如何,至少得有两台ZK机器。apache
咱们知道,每台zk都须要配置不一样的myid
,而当刚开始时,zxid一定都为0。这便意味着会挑选myid
最大的zk节点做为leader。服务器
zk节点每通过一次事务处理,都会更新zxid。那便意味着数据越新,zxid会越大。在这个选举过程当中,会挑选出zxid的节点做为leader。网络
核心方法为org.apache.zookeeper.server.quorum.QuorumPeer.startLeaderElection
和org.apache.zookeeper.server.quorum.QuorumPeer.run
,咱们的源码分析也基于此展开。并发
咱们得从QuorumPeerMain
来看,由于这是启动的入口:app
/** * * <h2>Configuration file</h2> * * When the main() method of this class is used to start the program, the first * argument is used as a path to the config file, which will be used to obtain * configuration information. This file is a Properties file, so keys and * values are separated by equals (=) and the key/value pairs are separated * by new lines. The following is a general summary of keys used in the * configuration file. For full details on this see the documentation in * docs/index.html * <ol> * <li>dataDir - The directory where the ZooKeeper data is stored.</li> * <li>dataLogDir - The directory where the ZooKeeper transaction log is stored.</li> * <li>clientPort - The port used to communicate with clients.</li> * <li>tickTime - The duration of a tick in milliseconds. This is the basic * unit of time in ZooKeeper.</li> * <li>initLimit - The maximum number of ticks that a follower will wait to * initially synchronize with a leader.</li> * <li>syncLimit - The maximum number of ticks that a follower will wait for a * message (including heartbeats) from the leader.</li> * <li>server.<i>id</i> - This is the host:port[:port] that the server with the * given id will use for the quorum protocol.</li> * </ol> * In addition to the config file. There is a file in the data directory called * "myid" that contains the server id as an ASCII decimal value. * */ @InterfaceAudience.Public public class QuorumPeerMain { private static final Logger LOG = LoggerFactory.getLogger(QuorumPeerMain.class); private static final String USAGE = "Usage: QuorumPeerMain configfile"; protected QuorumPeer quorumPeer; /** * To start the replicated server specify the configuration file name on * the command line. * @param args path to the configfile */ public static void main(String[] args) { QuorumPeerMain main = new QuorumPeerMain(); try { main.initializeAndRun(args); } catch (IllegalArgumentException e) { LOG.error("Invalid arguments, exiting abnormally", e); LOG.info(USAGE); System.err.println(USAGE); System.exit(2); } catch (ConfigException e) { LOG.error("Invalid config, exiting abnormally", e); System.err.println("Invalid config, exiting abnormally"); System.exit(2); } catch (DatadirException e) { LOG.error("Unable to access datadir, exiting abnormally", e); System.err.println("Unable to access datadir, exiting abnormally"); System.exit(3); } catch (AdminServerException e) { LOG.error("Unable to start AdminServer, exiting abnormally", e); System.err.println("Unable to start AdminServer, exiting abnormally"); System.exit(4); } catch (Exception e) { LOG.error("Unexpected exception, exiting abnormally", e); System.exit(1); } LOG.info("Exiting normally"); System.exit(0); } protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException { QuorumPeerConfig config = new QuorumPeerConfig(); if (args.length == 1) { config.parse(args[0]); } // Start and schedule the the purge task DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config .getDataDir(), config.getDataLogDir(), config .getSnapRetainCount(), config.getPurgeInterval()); purgeMgr.start(); if (args.length == 1 && config.isDistributed()) { runFromConfig(config); } else { LOG.warn("Either no config or no quorum defined in config, running " + " in standalone mode"); // there is only server in the quorum -- run as standalone ZooKeeperServerMain.main(args); } } public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException { try { ManagedUtil.registerLog4jMBeans(); } catch (JMException e) { LOG.warn("Unable to register log4j JMX control", e); } LOG.info("Starting quorum peer"); try { ServerCnxnFactory cnxnFactory = null; ServerCnxnFactory secureCnxnFactory = null; if (config.getClientPortAddress() != null) { cnxnFactory = ServerCnxnFactory.createFactory(); cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false); } if (config.getSecureClientPortAddress() != null) { secureCnxnFactory = ServerCnxnFactory.createFactory(); secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), true); } quorumPeer = getQuorumPeer(); quorumPeer.setTxnFactory(new FileTxnSnapLog( config.getDataLogDir(), config.getDataDir())); quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled()); quorumPeer.enableLocalSessionsUpgrading( config.isLocalSessionsUpgradingEnabled()); //quorumPeer.setQuorumPeers(config.getAllMembers()); quorumPeer.setElectionType(config.getElectionAlg()); quorumPeer.setMyid(config.getServerId()); quorumPeer.setTickTime(config.getTickTime()); quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout()); quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout()); quorumPeer.setInitLimit(config.getInitLimit()); quorumPeer.setSyncLimit(config.getSyncLimit()); quorumPeer.setConfigFileName(config.getConfigFilename()); quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory())); quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false); if (config.getLastSeenQuorumVerifier()!=null) { quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false); } quorumPeer.initConfigInZKDatabase(); quorumPeer.setCnxnFactory(cnxnFactory); quorumPeer.setSecureCnxnFactory(secureCnxnFactory); quorumPeer.setSslQuorum(config.isSslQuorum()); quorumPeer.setUsePortUnification(config.shouldUsePortUnification()); quorumPeer.setLearnerType(config.getPeerType()); quorumPeer.setSyncEnabled(config.getSyncEnabled()); quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs()); if (config.sslQuorumReloadCertFiles) { quorumPeer.getX509Util().enableCertFileReloading(); } // sets quorum sasl authentication configurations quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl); if(quorumPeer.isQuorumSaslAuthEnabled()){ quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl); quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl); quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal); quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext); quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext); } quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize); quorumPeer.initialize(); quorumPeer.start(); quorumPeer.join(); } catch (InterruptedException e) { // warn, but generally this is ok LOG.warn("Quorum Peer interrupted", e); } } // @VisibleForTesting protected QuorumPeer getQuorumPeer() throws SaslException { return new QuorumPeer(); } }
咱们从QuorumPeerMain.main()
-> main.initializeAndRun(args)
-> runFromConfig
-> quorumPeer.start()
,继续往下看QuorumPeer.java(这个类用于管理选举相关的逻辑):负载均衡
@Override public synchronized void start() { if (!getView().containsKey(myid)) { throw new RuntimeException("My id " + myid + " not in the peer list"); } loadDataBase(); startServerCnxnFactory(); try { adminServer.start(); } catch (AdminServerException e) { LOG.warn("Problem starting AdminServer", e); System.out.println(e); } startLeaderElection(); super.start(); }
如今,咱们来到核心代码startLeaderElection
:less
synchronized public void startLeaderElection() { try { if (getPeerState() == ServerState.LOOKING) { currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); } } catch(IOException e) { RuntimeException re = new RuntimeException(e.getMessage()); re.setStackTrace(e.getStackTrace()); throw re; } // if (!getView().containsKey(myid)) { // throw new RuntimeException("My id " + myid + " not in the peer list"); //} if (electionType == 0) { try { udpSocket = new DatagramSocket(getQuorumAddress().getPort()); responder = new ResponderThread(); responder.start(); } catch (SocketException e) { throw new RuntimeException(e); } } this.electionAlg = createElectionAlgorithm(electionType); }
逻辑很是的简单,若是处于Looking状态(服务器刚启动时默认为Looking),那么就发起选举的投票,并确认选举算法(从3.4.0开始,只有FastLeaderElection选举算法了),并将其发送出去。因为代码篇幅较大,这里再也不粘出,感兴趣的读者能够自行阅读FastLeaderElection.Messenger.WorkerReceiver.run
。其本质上就是一个线程,从存储vote的队列中取出vote,并发送。
在这里普及一下服务器状态:
接下来看QuorumPeer
的相关核心代码:
@Override public void run() { updateThreadName(); LOG.debug("Starting quorum peer"); try { jmxQuorumBean = new QuorumBean(this); MBeanRegistry.getInstance().register(jmxQuorumBean, null); for(QuorumServer s: getView().values()){ ZKMBeanInfo p; if (getId() == s.id) { p = jmxLocalPeerBean = new LocalPeerBean(this); try { MBeanRegistry.getInstance().register(p, jmxQuorumBean); } catch (Exception e) { LOG.warn("Failed to register with JMX", e); jmxLocalPeerBean = null; } } else { RemotePeerBean rBean = new RemotePeerBean(this, s); try { MBeanRegistry.getInstance().register(rBean, jmxQuorumBean); jmxRemotePeerBean.put(s.id, rBean); } catch (Exception e) { LOG.warn("Failed to register with JMX", e); } } } } catch (Exception e) { LOG.warn("Failed to register with JMX", e); jmxQuorumBean = null; } try { /* * Main loop */ while (running) { switch (getPeerState()) { case LOOKING: LOG.info("LOOKING"); if (Boolean.getBoolean("readonlymode.enabled")) { LOG.info("Attempting to start ReadOnlyZooKeeperServer"); // Create read-only server but don't start it immediately final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb); // Instead of starting roZk immediately, wait some grace // period before we decide we're partitioned. // // Thread is used here because otherwise it would require // changes in each of election strategy classes which is // unnecessary code coupling. Thread roZkMgr = new Thread() { public void run() { try { // lower-bound grace period to 2 secs sleep(Math.max(2000, tickTime)); if (ServerState.LOOKING.equals(getPeerState())) { roZk.startup(); } } catch (InterruptedException e) { LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started"); } catch (Exception e) { LOG.error("FAILED to start ReadOnlyZooKeeperServer", e); } } }; try { roZkMgr.start(); reconfigFlagClear(); if (shuttingDownLE) { shuttingDownLE = false; startLeaderElection(); } setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { LOG.warn("Unexpected exception", e); setPeerState(ServerState.LOOKING); } finally { // If the thread is in the the grace period, interrupt // to come out of waiting. roZkMgr.interrupt(); roZk.shutdown(); } } else { try { reconfigFlagClear(); if (shuttingDownLE) { shuttingDownLE = false; startLeaderElection(); } setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { LOG.warn("Unexpected exception", e); setPeerState(ServerState.LOOKING); } } break;
在这里仅仅截取了Looking
的相关逻辑,上半段的if主要处理只读服务——其用于handle只读client。else逻辑则是常见的状况,可是从代码块:
reconfigFlagClear(); if (shuttingDownLE) { shuttingDownLE = false; startLeaderElection(); } setCurrentVote(makeLEStrategy().lookForLeader());
其实区别不大。接着来看lookForLeader
,为了篇幅,咱们只截取Looking相关的代码:
/** * Starts a new round of leader election. Whenever our QuorumPeer * changes its state to LOOKING, this method is invoked, and it * sends notifications to all other peers. */ public Vote lookForLeader() throws InterruptedException { try { self.jmxLeaderElectionBean = new LeaderElectionBean(); MBeanRegistry.getInstance().register( self.jmxLeaderElectionBean, self.jmxLocalPeerBean); } catch (Exception e) { LOG.warn("Failed to register with JMX", e); self.jmxLeaderElectionBean = null; } if (self.start_fle == 0) { self.start_fle = Time.currentElapsedTime(); } try { HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>(); int notTimeout = finalizeWait; synchronized(this){ logicalclock.incrementAndGet(); updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } LOG.info("New election. My id = " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid)); sendNotifications(); /* * Loop in which we exchange notifications until we find a leader */ while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ /* * Remove next notification from queue, times out after 2 times * the termination time */ Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
注释说的很清楚,这个方法会开启新的一轮选举:当咱们的服务器状态变为Looking,这个方法会被调用,被通知集群其余须要参与选举的服务器。那么在这段逻辑中,recvqueue
会存放着相关的选举通知信息,取出一个。接下来有两个逻辑分支:
咱们来看totalOrderPredicate
这个方法:
/** * Check if a pair (server id, zxid) succeeds our * current vote. * * @param id Server identifier * @param zxid Last zxid observed by the issuer of this vote */ protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) { LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" + Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid)); if(self.getQuorumVerifier().getWeight(newId) == 0){ return false; } /* * We return true if one of the following three cases hold: * 1- New epoch is higher * 2- New epoch is the same as current epoch, but new zxid is higher * 3- New epoch is the same as current epoch, new zxid is the same * as current zxid, but server id is higher. */ return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId))))); }
理一下逻辑:
通过这个逻辑,即可以肯定外部投票优于内部投票——即更适合成为Leader。这时便会把外部选票信息来覆盖内部投票,并发送出去:
case LOOKING: // If notification > current, replace and send messages out if (n.electionEpoch > logicalclock.get()) { logicalclock.set(n.electionEpoch); recvset.clear(); if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { updateProposal(n.leader, n.zxid, n.peerEpoch); } else { updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } sendNotifications();
接下来就会判断集群中是否有过半的服务器承认该投票。
/** * Termination predicate. Given a set of votes, determines if have * sufficient to declare the end of the election round. * * @param votes * Set of votes * @param vote * Identifier of the vote received last */ protected boolean termPredicate(Map<Long, Vote> votes, Vote vote) { SyncedLearnerTracker voteSet = new SyncedLearnerTracker(); voteSet.addQuorumVerifier(self.getQuorumVerifier()); if (self.getLastSeenQuorumVerifier() != null && self.getLastSeenQuorumVerifier().getVersion() > self .getQuorumVerifier().getVersion()) { voteSet.addQuorumVerifier(self.getLastSeenQuorumVerifier()); } /* * First make the views consistent. Sometimes peers will have different * zxids for a server depending on timing. */ for (Map.Entry<Long, Vote> entry : votes.entrySet()) { if (vote.equals(entry.getValue())) { voteSet.addAck(entry.getKey()); } } return voteSet.hasAllQuorums(); //是否超过一半 }
不然的话会继续收集选票。
接下来即是更新服务器状态。
/* * This predicate is true once we don't read any new * relevant message from the reception queue */ if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch); leaveInstance(endVote); return endVote; }
上文咱们提到了QuorumPeer.java
,里面有个main loop
,不一样的角色会在这个loop下作本身的事。直到退出。在这里,咱们以Follower为例,进行分析:
case FOLLOWING: try { LOG.info("FOLLOWING"); setFollower(makeFollower(logFactory)); follower.followLeader(); } catch (Exception e) { LOG.warn("Unexpected exception",e); } finally { follower.shutdown(); setFollower(null); updateServerState(); } break;
跳follower.followLeader()
:
/** * the main method called by the follower to follow the leader * * @throws InterruptedException */ void followLeader() throws InterruptedException { self.end_fle = Time.currentElapsedTime(); long electionTimeTaken = self.end_fle - self.start_fle; self.setElectionTimeTaken(electionTimeTaken); LOG.info("FOLLOWING - LEADER ELECTION TOOK - {} {}", electionTimeTaken, QuorumPeer.FLE_TIME_UNIT); self.start_fle = 0; self.end_fle = 0; fzk.registerJMX(new FollowerBean(this, zk), self.jmxLocalPeerBean); try { QuorumServer leaderServer = findLeader(); try { connectToLeader(leaderServer.addr, leaderServer.hostname); long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO); if (self.isReconfigStateChange()) throw new Exception("learned about role change"); //check to see if the leader zxid is lower than ours //this should never happen but is just a safety check long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid); if (newEpoch < self.getAcceptedEpoch()) { LOG.error("Proposed leader epoch " + ZxidUtils.zxidToString(newEpochZxid) + " is less than our accepted epoch " + ZxidUtils.zxidToString(self.getAcceptedEpoch())); throw new IOException("Error: Epoch of leader is lower"); } syncWithLeader(newEpochZxid); QuorumPacket qp = new QuorumPacket(); while (this.isRunning()) { readPacket(qp); processPacket(qp); } } catch (Exception e) { LOG.warn("Exception when following the leader", e); try { sock.close(); } catch (IOException e1) { e1.printStackTrace(); } // clear pending revalidations pendingRevalidations.clear(); } } finally { zk.unregisterJMX((Learner)this); } }
跳往核心方法processPacket
:
/** * Examine the packet received in qp and dispatch based on its contents. * @param qp * @throws IOException */ protected void processPacket(QuorumPacket qp) throws Exception{ switch (qp.getType()) { case Leader.PING: ping(qp); break; case Leader.PROPOSAL: TxnHeader hdr = new TxnHeader(); Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr); if (hdr.getZxid() != lastQueued + 1) { LOG.warn("Got zxid 0x" + Long.toHexString(hdr.getZxid()) + " expected 0x" + Long.toHexString(lastQueued + 1)); } lastQueued = hdr.getZxid(); if (hdr.getType() == OpCode.reconfig){ SetDataTxn setDataTxn = (SetDataTxn) txn; QuorumVerifier qv = self.configFromString(new String(setDataTxn.getData())); self.setLastSeenQuorumVerifier(qv, true); } fzk.logRequest(hdr, txn); break; case Leader.COMMIT: fzk.commit(qp.getZxid()); break; case Leader.COMMITANDACTIVATE: // get the new configuration from the request Request request = fzk.pendingTxns.element(); SetDataTxn setDataTxn = (SetDataTxn) request.getTxn(); QuorumVerifier qv = self.configFromString(new String(setDataTxn.getData())); // get new designated leader from (current) leader's message ByteBuffer buffer = ByteBuffer.wrap(qp.getData()); long suggestedLeaderId = buffer.getLong(); boolean majorChange = self.processReconfig(qv, suggestedLeaderId, qp.getZxid(), true); // commit (writes the new config to ZK tree (/zookeeper/config) fzk.commit(qp.getZxid()); if (majorChange) { throw new Exception("changes proposed in reconfig"); } break; case Leader.UPTODATE: LOG.error("Received an UPTODATE message after Follower started"); break; case Leader.REVALIDATE: revalidate(qp); break; case Leader.SYNC: fzk.sync(); break; default: LOG.warn("Unknown packet type: {}", LearnerHandler.packetToString(qp)); break; } }
在case COMMITANDACTIVATE
中,咱们能够看到当其收到leader改变相关的消息时,就会抛出异常。接下来它本身就会变成LOOKING
状态,开始选举。
那么如何肯定leader不可用呢?答案是经过心跳指令。在必定时间内若是leader的心跳没有过来,那么则认为其已经不可用。
见LeanerHandler.run
里的case Leader.PING
:
case Leader.PING: // Process the touches ByteArrayInputStream bis = new ByteArrayInputStream(qp .getData()); DataInputStream dis = new DataInputStream(bis); while (dis.available() > 0) { long sess = dis.readLong(); int to = dis.readInt(); leader.zk.touch(sess, to); } break;
首先,咱们要知道。选举算法的本质是共识算法,而绝大多数共识算法就是为了解决分布式环境下数据一致性而诞生的。而zk里所谓leader、follower之类的,无非也是个状态,基于zk这个语义下(上下文里)你们都认为一个leader是leader,才是有效的共识。
常见的共识算法都有哪些呢?现阶段的共识算法主要能够分红三大类:公链,联盟链和私链。下面描述这三种类别的特征:
copy from https://zhuanlan.zhihu.com/p/...;做者:美图技术团队
基于篇幅,接下来简单介绍下两个较为典型的共识算法。
Raft 算法是典型的多数派投票选举算法,其选举机制与咱们平常生活中的民主投票机制相似,核心思想是“少数服从多数”。也就是说,Raft 算法中,得到投票最多的节点成为主。
采用 Raft 算法选举,集群节点的角色有 3 种:
Raft 选举的流程,能够分为如下几步:
这个算法比起ZAB,较易实现,但因为消息通讯量大,相比于ZAB,更适用于中小的场景。
PoW 算法,是以每一个节点或服务器的计算能力(即“算力”)来竞争记帐权的机制,所以是一种使用工做量证实机制的共识算法。也就是说,谁的算力强(解题快),谁得到记帐权的可能性就越大。
好比发生一次交易,同时有三个节点(A、B、C)都收到了这个记帐请求。A节点已经算出来了,那么就会通知BC节点进行验证——这是一种椭圆曲线加密算法,解题的速度会比验证的速度慢不少。当全部节点验证后,这个记帐就记下来了。
听起来很公平。但PoW 机制每次达成共识须要全网共同参与运算,增长了每一个节点的计算量,而且若是题目过难,会致使计算时间长、资源消耗多 ;而若是题目过于简单,会致使大量节点同时得到记帐权,冲突多。这些问题,都会增长达成共识的时间。
在本文,咱们先提到了zookeeper的leader选举,大体流程以下:
和服务器启动时选举
很是的像,无非就是多了一个状态变动——当Leader挂了,余下的Follower都会将本身的服务器状态变动为LOOKING,而后进入选举流程。
咱们还提到了一致性算法和共识算法的概念,那么一致性与共识的区别是什么呢?在日常使用中,咱们一般会混淆一致性和共识这两个概念,不妨在这儿说清:
即:一致性强调的是结果,共识强调的是达成一致的过程,共识算法是保障系统知足不一样程度一致性的核心技术。