<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>ZCH NOTES</title>
  
  <subtitle>博学笃志 切问近思</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://yoursite.com/"/>
  <updated>2022-05-03T14:43:22.702Z</updated>
  <id>http://yoursite.com/</id>
  
  <author>
    <name>zch</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>从Paxos到Zookeeper分布式一致性原理与实践</title>
    <link href="http://yoursite.com/2022/05/03/%E5%A4%8D%E4%B9%A0%E6%80%BB%E7%BB%93/ZooKeeper%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5/"/>
    <id>http://yoursite.com/2022/05/03/复习总结/ZooKeeper分布式一致性原理与实践/</id>
    <published>2022-05-02T16:00:00.000Z</published>
    <updated>2022-05-03T14:43:22.702Z</updated>
    
    <content type="html"><![CDATA[<h1 id="一致性协议"><a href="#一致性协议" class="headerlink" title="一致性协议"></a>一致性协议</h1><h2 id="2PC与3PC"><a href="#2PC与3PC" class="headerlink" title="2PC与3PC"></a>2PC与3PC</h2><p>在分布式系统中，每一个机器节点虽然都能够明确地知道自己在进行事务操作过程中的结果是成功或失败，但却无法直接获取到其他分布式节点的操作结果。因此，当一个事务操作需要跨越多个分布式节点的时候，为了保持事务处理的ACID特性，就需要引入一个称为“协调者（Coordinator）”的组件来统一调度所有分布式节点的执行逻辑，这些被调度的分布式节点则被称为“参与者”（Participant）。协调者负责调度参与者的行为，并最终决定这些参与者是否要把事务真正进行提交。基于这个思想，衍生出了二阶段提交和三阶段提交两种协议，在本节中，我们将重点对这两种分布式事务中涉及的一致性协议进行讲解。</p><h3 id="2PC"><a href="#2PC" class="headerlink" title="2PC"></a>2PC</h3><p>2PC，是Two-Phase Commit的缩写，即二阶段提交，是计算机网络尤其是在数据库领域内，为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法。</p><p><strong>协议说明</strong></p><p>顾名思义，二阶段提交协议是将事务的提交过程分成了两个阶段来进行处理，其执行流程如下。</p><h4 id="阶段一：提交事务请求"><a href="#阶段一：提交事务请求" class="headerlink" title="阶段一：提交事务请求"></a>阶段一：提交事务请求</h4><ol><li>事务询问。<br>协调者向所有的参与者发送事务内容，询问是否可以执行事务提交操作，并开始等待各参与者的响应。</li><li>执行事务。<br>各参与者节点执行事务操作，并将Undo和Redo信息记入事务日志中。</li><li>各参与者向协调者反馈事务询问的响应。<br>如果参与者成功执行了事务操作，那么就反馈给协调者 Yes 响应，表示事务可以执行；如果参与者没有成功执行事务，那么就反馈给协调者No响应，表示事务不可以执行。</li></ol><p>由于上面讲述的内容在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程，因此二阶段提交协议的阶段一也被称为“投票阶段”，即各参与者投票表明是否要继续执行接下去的事务提交操作。</p><h4 id="阶段二：执行事务提交"><a href="#阶段二：执行事务提交" class="headerlink" title="阶段二：执行事务提交"></a>阶段二：执行事务提交</h4><p>在阶段二中，协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作，正常情况下，包含以下两种可能。</p><p>执行事务提交</p><p>假如协调者从所有的参与者获得的反馈都是Yes响应，那么就会执行事务提交。</p><ol><li>发送提交请求。<br>协调者向所有参与者节点发出Commit请求。</li><li>事务提交。<br>参与者接收到 Commit 请求后，会正式执行事务提交操作，并在完成提交之后释放在整个事务执行期间占用的事务资源。</li><li>反馈事务提交结果。<br>参与者在完成事务提交之后，向协调者发送Ack消息。</li><li>完成事务。<br>协调者接收到所有参与者反馈的Ack消息后，完成事务。</li></ol><p>中断事务</p><p>假如任何一个参与者向协调者反馈了No响应，或者在等待超时之后，协调者尚无法接收到所有参与者的反馈响应，那么就会中断事务。</p><ol><li>发送回滚请求。<br>协调者向所有参与者节点发出Rollback请求。</li><li>事务回滚。<br>参与者接收到 Rollback 请求后，会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作，并在完成回滚之后释放在整个事务执行期间占用的资源。</li><li>反馈事务回滚结果。<br>参与者在完成事务回滚之后，向协调者发送Ack消息。</li><li>中断事务。</li></ol><p>协调者接收到所有参与者反馈的Ack消息后，完成事务中断。</p><p>以上就是二阶段提交过程中，前后两个阶段分别进行的处理逻辑。简单地讲，二阶段提交将一个事务的处理过程分为了投票和执行两个阶段，其核心是对每个事务都采用先尝试后提交的处理方式，因此也可以将二阶段提交看作一个强一致性的算法，图2-1和图2-2分别展示了二阶段提交过程中“事务提交”和“事务中断”两种场景下的交互流程。</p><p><img src="/images/db60b8dab324865b.jpg" alt="img" style="zoom: 50%;"></p>"<p>图2-1.二阶段提交“事务提交”示意图</p><p><img src="/images/7892417d788675b6.jpg" alt="img" style="zoom: 50%;"></p>"<p>图2-2.二阶段提交“事务中断”示意图</p><p><strong>优缺点</strong></p><p>二阶段提交协议的优点：原理简单，实现方便。<br>二阶段提交协议的缺点：同步阻塞、单点问题、脑裂、太过保守。</p><ul><li>同步阻塞<br>二阶段提交协议存在的最明显也是最大的一个问题就是同步阻塞，这会极大地限制分布式系统的性能。在二阶段提交的执行过程中，所有参与该事务操作的逻辑都处于阻塞状态，也就是说，各个参与者在等待其他参与者响应的过程中，将无法进行其他任何操作。</li><li>单点问题<br>在上面的讲解过程中，相信读者可以看出，协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协调者出现问题，那么整个二阶段提交流程将无法运转，更为严重的是，如果协调者是在阶段二中出现问题的话，那么其他参与者将会一直处于锁定事务资源的状态中，而无法继续完成事务操作。</li><li>数据不一致<br>在二阶段提交协议的阶段二，即执行事务提交的时候，当协调者向所有的参与者发送Commit请求之后，发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃，导致最终只有部分参与者收到了Commit请求。于是，这部分收到了Commit请求的参与者就会进行事务的提交，而其他没有收到Commit请求的参与者则无法进行事务提交，于是整个分布式系统便出现了数据不一致性现象。</li><li>太过保守<br>如果在协调者指示参与者进行事务提交询问的过程中，参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话，这时协调者只能依靠其自身的超时机制来判断是否需要中断事务，这样的策略显得比较保守。换句话说，二阶段提交协议没有设计较为完善的容错机制，任意一个节点的失败都会导致整个事务的失败。</li></ul><h3 id="3PC"><a href="#3PC" class="headerlink" title="3PC"></a>3PC</h3><p>在上文中，我们讲解了二阶段提交协议的设计和实现原理，并明确指出了其在实际运行过程中可能存在的诸如同步阻塞、协调者的单点问题、脑裂和太过保守的容错机制等缺陷，因此研究者在二阶段提交协议的基础上进行了改进，提出了三阶段提交协议。</p><p><strong>协议说明</strong></p><p>3PC，是Three-Phase Commit的缩写，即三阶段提交，是2PC的改进版，其将二阶段提交协议的“提交事务请求”过程一分为二，形成了由CanCommit、PreCommit和do Commit三个阶段组成的事务处理协议，其协议设计如图2-3所示。</p><p><img src="/images/ec6649baa1d31fa9.jpg" alt="img"></p>"<p>图2-3.三阶段提交协议流程示意图注 1</p><h4 id="阶段一：CanCommit"><a href="#阶段一：CanCommit" class="headerlink" title="阶段一：CanCommit"></a>阶段一：CanCommit</h4><ol><li>事务询问。<br>协调者向所有的参与者发送一个包含事务内容的 canCommit请求，询问是否可以执行事务提交操作，并开始等待各参与者的响应。</li><li>各参与者向协调者反馈事务询问的响应。<br>参与者在接收到来自协调者的 canCommit请求后，正常情况下，如果其自身认为可以顺利执行事务，那么会反馈Yes响应，并进入预备状态，否则反馈No响应。</li></ol><h4 id="阶段二：PreCommit"><a href="#阶段二：PreCommit" class="headerlink" title="阶段二：PreCommit"></a>阶段二：PreCommit</h4><p>在阶段二中，协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作，正常情况下，包含两种可能。</p><p>执行事务预提交<br>假如协调者从所有的参与者获得的反馈都是Yes响应，那么就会执行事务预提交。</p><ol><li>发送预提交请求。<br>协调者向所有参与者节点发出preCommit的请求，并进入Prepared阶段。</li><li>事务预提交。<br>参与者接收到preCommit请求后，会执行事务操作，并将Undo和Redo信息记录到事务日志中。</li><li>各参与者向协调者反馈事务执行的响应。<br>如果参与者成功执行了事务操作，那么就会反馈给协调者Ack响应，同时等待最终的指令：提交（commit）或中止（abort）。</li></ol><p>中断事务<br>假如任何一个参与者向协调者反馈了No响应，或者在等待超时之后，协调者尚无法接收到所有参与者的反馈响应，那么就会中断事务。</p><ol><li>发送中断请求。<br>协调者向所有参与者节点发出abort请求。</li><li>中断事务。<br>无论是收到来自协调者的abort请求，或者是在等待协调者请求过程中出现超时，参与者都会中断事务。</li></ol><h4 id="阶段三：doCommit"><a href="#阶段三：doCommit" class="headerlink" title="阶段三：doCommit"></a>阶段三：doCommit</h4><p>该阶段将进行真正的事务提交，会存在以下两种可能的情况。</p><p>执行提交</p><ol><li>发送提交请求。<br>进入这一阶段，假设协调者处于正常工作状态，并且它接收到了来自所有参与者的Ack响应，那么它将从“预提交”状态转换到“提交”状态，并向所有的参与者发送doCommit请求。</li><li>事务提交。<br>参与者接收到 doCommit 请求后，会正式执行事务提交操作，并在完成提交之后释放在整个事务执行期间占用的事务资源。</li><li>反馈事务提交结果。<br>参与者在完成事务提交之后，向协调者发送Ack消息。</li><li>完成事务。<br>协调者接收到所有参与者反馈的Ack消息后，完成事务。</li></ol><p>中断事务<br>进入这一阶段，假设协调者处于正常工作状态，并且有任意一个参与者向协调者反馈了No响应，或者在等待超时之后，协调者尚无法接收到所有参与者的反馈响应，那么就会中断事务。</p><ol><li>发送中断请求。<br>协调者向所有的参与者节点发送abort请求。</li><li>事务回滚。<br>参与者接收到abort请求后，会利用其在阶段二中记录的Undo信息来执行事务回滚操作，并在完成回滚之后释放在整个事务执行期间占用的资源。</li><li>反馈事务回滚结果。<br>参与者在完成事务回滚之后，向协调者发送Ack消息。</li><li>中断事务。</li></ol><p>协调者接收到所有参与者反馈的Ack消息后，中断事务。<br>需要注意的是，一旦进入阶段三，可能会存在以下两种故障。<br>·  协调者出现问题。<br>·  协调者和参与者之间的网络出现故障。</p><p>无论出现哪种情况，最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求，针对这样的异常情况，参与者都会在等待超时之后，继续进行事务提交。</p><p><strong>优缺点</strong><br>三阶段提交协议的优点：相较于二阶段提交协议，三阶段提交协议最大的优点就是降低了参与者的阻塞范围，并且能够在出现单点故障后继续达成一致。<br>三阶段提交协议的缺点：三阶段提交协议在去除阻塞的同时也引入了新的问题，那就是在参与者接收到preCommit消息后，如果网络出现分区，此时协调者所在的节点和参与者无法进行正常的网络通信，在这种情况下，该参与者依然会进行事务的提交，这必然出现数据的不一致性。</p><h2 id="Paxos算法"><a href="#Paxos算法" class="headerlink" title="Paxos算法"></a>Paxos算法</h2><h3 id="Proposer生成提案"><a href="#Proposer生成提案" class="headerlink" title="Proposer生成提案"></a>Proposer生成提案</h3><p>对于一个Proposer来说，获取那些已经被通过的提案远比预测未来可能会被通过的提案来得简单。因此，Proposer在产生一个编号为 Mn的提案时，必须要知道当前某一个将要或已经被半数以上 Acceptor批准的编号小于 Mn但为最大编号的提案。并且，Proposer 会要求所有的 Acceptor 都不要再批准任何编号小于Mn的提案——这就引出了如下的提案生成算法。</p><ol><li>Proposer 选择一个新的提案编号 Mn，然后向某个 Acceptor 集合的成员发送请求，要求该集合中的Acceptor做出如下回应。<br>· 向Proposer承诺，保证不再批准任何编号小于Mn的提案。<br>· 如果 Acceptor 已经批准过任何提案，那么其就向 Proposer 反馈当前该Acceptor已经批准的编号小于Mn但为最大编号的那个提案的值。<br>我们将该请求称为编号为Mn的提案的Prepare请求。</li><li>如果Proposer收到了来自半数以上的Acceptor的响应结果，那么它就可以产生编号为Mn、Value值为Vn的提案，这里的Vn是所有响应中编号最大的提案的Value值。当然还存在另一种情况，就是半数以上的Acceptor都没有批准过任何提案，即响应中不包含任何的提案，那么此时Vn值就可以由Proposer任意选择。<br>在确定提案之后，Proposer就会将该提案再次发送给某个Acceptor集合，并期望获得它们的批准，我们称此请求为Accept请求。需要注意的一点是，此时接受Accept请求的Acceptor 集合不一定是之前响应 Prepare 请求的 Acceptor 集合——这点相信读者也能够明白，任意两个半数以上的Acceptor集合，必定包含至少一个公共Acceptor。</li></ol><h3 id="Acceptor批准提案"><a href="#Acceptor批准提案" class="headerlink" title="Acceptor批准提案"></a>Acceptor批准提案</h3><p>在上文中，我们已经讲解了Paxos算法中Proposer的处理逻辑，下面我们来看看Acceptor是如何批准提案的。<br>根据上面的内容，一个Acceptor可能会收到来自Proposer的两种请求，分别是 Prepare请求和Accept请求，对这两类请求做出响应的条件分别如下。</p><ul><li><strong>Prepare请求：</strong>Acceptor可以在任何时候响应一个Prepare请求。</li><li><strong>Accept请求：</strong>在不违背Accept现有承诺的前提下，可以任意响应Accept请求。</li></ul><p>因此，对Acceptor逻辑处理的约束条件，大体可以定义如下。</p><p>P1a：一个Acceptor只要尚未响应过任何编号大于Mn的Prepare请求，那么它就可以接受这个编号为Mn的提案。<br>从上面这个约束条件中，我们可以看出，P1a 包含了 P1。同时，值得一提的是，Paxos算法允许Acceptor忽略任何请求而不用担心破坏其算法的安全性。</p><h3 id="算法优化"><a href="#算法优化" class="headerlink" title="算法优化"></a>算法优化</h3><p>假设一个 Acceptor收到了一个编号为 Mn的 Prepare请求，但此时该 Acceptor已经对编号大于 Mn的 Prepare 请求做出了响应，因此它肯定不会再批准任何新的编号为 Mn的提案，那么很显然，Acceptor 就没有必要对这个 Prepare 请求做出响应，于是Acceptor可以选择忽略这样的Prepare请求。同时，Acceptor也可以忽略掉那些它已经批准过的提案的Prepare请求。</p><h3 id="算法陈述"><a href="#算法陈述" class="headerlink" title="算法陈述"></a>算法陈述</h3><p>综合前面讲解的内容，我们来对Paxos算法的提案选定过程进行一个陈述。结合Proposer和Acceptor对提案的处理逻辑，就可以得到如下类似于两阶段提交的算法执行过程。</p><h4 id="阶段一"><a href="#阶段一" class="headerlink" title="阶段一"></a>阶段一</h4><ol><li>Proposer选择一个提案编号Mn，然后向Acceptor的某个超过半数的子集成员发送编号为Mn的Prepare请求。</li><li>如果一个Acceptor收到一个编号为Mn的Prepare请求，且编号Mn大于该Acceptor已经响应的所有 Prepare 请求的编号，那么它就会将它已经批准过的最大编号的提案作为响应反馈给Proposer，同时该Acceptor会承诺不会再批准任何编号小于Mn的提案。</li></ol><h4 id="阶段二"><a href="#阶段二" class="headerlink" title="阶段二"></a>阶段二</h4><ol><li>如果Proposer收到来自半数以上的Acceptor对于其发出的编号为Mn的Prepare请求的响应，那么它就会发送一个针对[Mn，Vn]提案的Accept请求给Acceptor。注意，Vn的值就是收到的响应中编号最大的提案的值，如果响应中不包含任何提案，那么它就是任意值。</li><li>如果Acceptor收到这个针对[Mn，Vn]提案的Accept请求，只要该Acceptor尚未对编号大于Mn的Prepare请求做出响应，它就可以通过这个提案。</li></ol><p>当然，在实际运行过程中，每一个 Proposer 都有可能会产生多个提案，但只要每个Proposer都遵循如上所述的算法运行，就一定能够保证算法执行的正确性。值得一提的是，每个Proposer都可以在任意时刻丢弃一个提案，哪怕针对该提案的请求和响应在提案被丢弃后会到达，但根据Paxos算法的一系列规约，依然可以保证其在提案选定上的正确性。事实上，如果某个Proposer已经在试图生成编号更大的提案，那么丢弃一些旧的提案未尝不是一个好的选择。因此，如果一个 Acceptor 因为已经收到过更大编号的Prepare请求而忽略某个编号更小的Prepare或者Accept请求，那么它也应当通知其对应的Proposer，以便该Proposer也能够将该提案进行丢弃——这和上面“算法优化”部分中提到的提案丢弃是一致的。</p><h3 id="提案的获取"><a href="#提案的获取" class="headerlink" title="提案的获取"></a>提案的获取</h3><p>在上文中，我们已经介绍了如何来选定一个提案，下面我们再来看看如何让 Learner 获取提案，大体可以有以下几种方案。</p><p>方案一<br>Learner 获取一个已经被选定的提案的前提是，该提案已经被半数以上的 Acceptor批准。因此，最简单的做法就是一旦Acceptor批准了一个提案，就将该提案发送给所有的Learner。<br>很显然，这种做法虽然可以让Learner尽快地获取被选定的提案，但是却需要让每个Acceptor与所有的Learner逐个进行一次通信，通信的次数至少为二者个数的乘积。</p><p>方案二<br>另一种可行的方案是，我们可以让所有的Acceptor将它们对提案的批准情况，统一发送给一个特定的 Learner（下文中我们将这样的 Learner 称为“主 Learner”），在不考虑拜占庭将军问题的前提下，我们假定Learner之间可以通过消息通信来互相感知提案的选定情况。基于这样的前提，当主Learner被通知一个提案已经被选定时，它会负责通知其他的Learner。<br>在这种方案中，Acceptor首先会将得到批准的提案发送给主 Learner，再由其同步给其他 Learner，因此较方案一而言，方案二虽然需要多一个步骤才能将提案通知到所有的 Learner，但其通信次数却大大减少了，通常只是 Acceptor 和Learner 的个数总和。但同时，该方案引入了一个新的不稳定因素：主 Learner随时可能出现故障。</p><p>方案三<br>在讲解方案二的时候，我们提到，方案二最大的问题在于主 Learner存在单点问题，即主 Learner随时可能出现故障。因此，对方案二进行改进，可以将主 Learner的范围扩大，即 Acceptor 可以将批准的提案发送给一个特定的 Learner 集合，该集合中的每个 Learner 都可以在一个提案被选定后通知所有其他的 Learner。这个 Learner集合中的 Learner个数越多，可靠性就越好，但同时网络通信的复杂度也就越高。</p><p>## </p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;一致性协议&quot;&gt;&lt;a href=&quot;#一致性协议&quot; class=&quot;headerlink&quot; title=&quot;一致性协议&quot;&gt;&lt;/a&gt;一致性协议&lt;/h1&gt;&lt;h2 id=&quot;2PC与3PC&quot;&gt;&lt;a href=&quot;#2PC与3PC&quot; class=&quot;headerlink&quot; title=
      
    
    </summary>
    
      <category term="复习" scheme="http://yoursite.com/categories/%E5%A4%8D%E4%B9%A0/"/>
    
    
      <category term="zookeeper" scheme="http://yoursite.com/tags/zookeeper/"/>
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="paxos" scheme="http://yoursite.com/tags/paxos/"/>
    
  </entry>
  
  <entry>
    <title>分布式--Raft协议</title>
    <link href="http://yoursite.com/2022/04/14/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F--raft%E5%8D%8F%E8%AE%AE/"/>
    <id>http://yoursite.com/2022/04/14/分布式/分布式--raft协议/</id>
    <published>2022-04-14T14:04:39.283Z</published>
    <updated>2022-04-14T14:13:16.097Z</updated>
    
    <content type="html"><![CDATA[<p><strong>正文</strong></p><p>  raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。在这里强调了是在工程上，因为在学术理论界，最耀眼的还是大名鼎鼎的Paxos。但Paxos是：少数真正理解的人觉得简单，尚未理解的人觉得很难，大多数人都是一知半解。本人也花了很多时间、看了很多材料也没有真正理解。直到看到raft的论文，两位研究者也提到，他们也花了很长的时间来理解Paxos，他们也觉得很难理解，于是研究出了raft算法。</p><p>   raft是一个共识算法（consensus algorithm），所谓共识，就是多个节点对某个事情达成一致的看法，即使是在部分节点故障、网络延时、网络分割的情况下。这些年最为火热的加密货币（比特币、区块链）就需要共识算法，而在分布式系统中，共识算法更多用于提高系统的容错性，比如分布式存储中的复制集（replication），在<a href="https://www.cnblogs.com/xybaby/p/7153755.html" target="_blank" rel="noopener">带着问题学习分布式系统之中心化复制集</a>一文中介绍了中心化复制集的相关知识。raft协议就是一种leader-based的共识算法，与之相应的是leaderless的共识算法。</p><p>  本文基于论文<a href="https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14" target="_blank" rel="noopener">In Search of an Understandable Consensus Algorithm</a>对raft协议进行分析，当然，还是建议读者直接看论文。</p><p>  本文地址：<a href="https://www.cnblogs.com/xybaby/p/10124083.html" target="_blank" rel="noopener">https://www.cnblogs.com/xybaby/p/10124083.html</a></p><h1 id="raft算法概览"><a href="#raft算法概览" class="headerlink" title="raft算法概览"></a>raft算法概览</h1><p>  Raft算法的头号目标就是容易理解（UnderStandable），这从论文的标题就可以看出来。当然，Raft增强了可理解性，在性能、可靠性、可用性方面是不输于Paxos的。</p><blockquote><p>Raft more understandable than Paxos and also provides a better foundation for building practical systems</p></blockquote><p>   为了达到易于理解的目标，raft做了很多努力，其中最主要是两件事情：</p><ul><li>问题分解</li><li>状态简化</li></ul><p>   问题分解是将”复制集中节点一致性”这个复杂的问题划分为数个可以被独立解释、理解、解决的子问题。在raft，子问题包括，<em>leader election</em>， <em>log replication</em>，<em>safety</em>，<em>membership changes</em>。而状态简化更好理解，就是对算法做出一些限制，减少需要考虑的状态数，使得算法更加清晰，更少的不确定性（比如，保证新选举出来的leader会包含所有commited log entry）</p><blockquote><p>Raft implements consensus by first electing a distinguished leader, then giving the leader complete responsibility for managing the replicated log. The leader accepts log entries from clients, replicates them on other servers, and tells servers when it is safe to apply log entries to their state machines. A leader can fail or become disconnected from the other servers, in which case a new leader is elected.</p></blockquote><p>   上面的引文对raft协议的工作原理进行了高度的概括：raft会先选举出leader，leader完全负责replicated log的管理。leader负责接受所有客户端更新请求，然后复制到follower节点，并在“安全”的时候执行这些请求。如果leader故障，followes会重新选举出新的leader。</p><p>   这就涉及到raft最新的两个子问题： leader election和log replication</p><h1 id="leader-election"><a href="#leader-election" class="headerlink" title="leader election"></a>leader election</h1><p>   raft协议中，一个节点任一时刻处于以下三个状态之一：</p><ul><li>leader</li><li>follower</li><li>candidate</li></ul><p>   给出状态转移图能很直观的直到这三个状态的区别<br><img src="/images/1089769-20181216202049306-1194425087.png" alt="img"></p>"<p>  可以看出所有节点启动时都是follower状态；在一段时间内如果没有收到来自leader的心跳，从follower切换到candidate，发起选举；如果收到majority的造成票（含自己的一票）则切换到leader状态；如果发现其他节点比自己更新，则主动切换到follower。</p><p>   总之，系统中最多只有一个leader，如果在一段时间里发现没有leader，则大家通过选举-投票选出leader。leader会不停的给follower发心跳消息，表明自己的存活状态。如果leader故障，那么follower会转换成candidate，重新选出leader。</p><h2 id="term"><a href="#term" class="headerlink" title="term"></a>term</h2><p>   从上面可以看出，哪个节点做leader是大家投票选举出来的，每个leader工作一段时间，然后选出新的leader继续负责。这根民主社会的选举很像，每一届新的履职期称之为一届任期，在raft协议中，也是这样的，对应的术语叫<strong><em>term</em></strong>。<br><img src="/images/1089769-20181216202155162-452543292.png" alt="img"></p>"<p>   term（任期）以选举（election）开始，然后就是一段或长或短的稳定工作期（normal Operation）。从上图可以看到，任期是递增的，这就充当了逻辑时钟的作用；另外，term 3展示了一种情况，就是说没有选举出leader就结束了，然后会发起新的选举，后面会解释这种<em>split vote</em>的情况。</p><h2 id="选举过程详解"><a href="#选举过程详解" class="headerlink" title="选举过程详解"></a>选举过程详解</h2><p>   上面已经说过，如果follower在<em>election timeout</em>内没有收到来自leader的心跳，（也许此时还没有选出leader，大家都在等；也许leader挂了；也许只是leader与该follower之间网络故障），则会主动发起选举。步骤如下：</p><ul><li>增加节点本地的 <em>current term</em> ，切换到candidate状态</li><li>投自己一票</li><li>并行给其他节点发送 <em>RequestVote RPCs</em></li><li>等待其他节点的回复</li></ul><p>   在这个过程中，根据来自其他节点的消息，可能出现三种结果</p><ol><li>收到majority的投票（含自己的一票），则赢得选举，成为leader</li><li>被告知别人已当选，那么自行切换到follower</li><li>一段时间内没有收到majority投票，则保持candidate状态，重新发出选举</li></ol><p>   第一种情况，赢得了选举之后，新的leader会立刻给所有节点发消息，广而告之，避免其余节点触发新的选举。在这里，先回到投票者的视角，投票者如何决定是否给一个选举请求投票呢，有以下约束：</p><ul><li>在任一任期内，单个节点最多只能投一票</li><li>候选人知道的信息不能比自己的少（这一部分，后面介绍log replication和safety的时候会详细介绍）</li><li>first-come-first-served 先来先得</li></ul><p>   第二种情况，比如有三个节点A B C。A B同时发起选举，而A的选举消息先到达C，C给A投了一票，当B的消息到达C时，已经不能满足上面提到的第一个约束，即C不会给B投票，而A和B显然都不会给对方投票。A胜出之后，会给B,C发心跳消息，节点B发现节点A的term不低于自己的term，知道有已经有Leader了，于是转换成follower。</p><p>   第三种情况，没有任何节点获得majority投票，比如下图这种情况：<br><img src="/images/1089769-20181216202546810-1327167758.png" alt="img"></p>"<p>   总共有四个节点，Node C、Node D同时成为了candidate，进入了term 4，但Node A投了NodeD一票，NodeB投了Node C一票，这就出现了平票 split vote的情况。这个时候大家都在等啊等，直到超时后重新发起选举。如果出现平票的情况，那么就延长了系统不可用的时间（没有leader是不能处理客户端写请求的），因此raft引入了randomized election timeouts来尽量避免平票情况。同时，leader-based 共识算法中，节点的数目都是奇数个，尽量保证majority的出现。</p><h2 id="pre-vote"><a href="#pre-vote" class="headerlink" title="pre-vote"></a>pre-vote</h2><p>follower 心跳超时后先变为 pre-candidate，类似 2PC，发出 prepare-vote rpc 先取得大多数节点的响应，ok 则升 term 变为 candidate，不 ok 则一直是 pre-candidate，直到网络恢复，接收到 leader 的心跳变回 follower；这样这个搅屎棍就不会扰乱系统的正常执行了</p><h2 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h2><p><a href="https://www.modb.pro/db/70145" target="_blank" rel="noopener">解读Raft协议节点状态与选举</a></p><h1 id="log-replication"><a href="#log-replication" class="headerlink" title="log replication"></a>log replication</h1><p>   当有了leader，系统应该进入对外工作期了。客户端的一切请求来发送到leader，leader来调度这些并发请求的顺序，并且保证leader与followers状态的一致性。raft中的做法是，将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求，保证状态一致。</p><h2 id="Replicated-state-machines"><a href="#Replicated-state-machines" class="headerlink" title="Replicated state machines"></a>Replicated state machines</h2><p>   共识算法的实现一般是基于复制状态机（Replicated state machines），何为复制状态机：</p><blockquote><p>If two identical, <strong>deterministic</strong> processes begin in the same state and get the same inputs in the same order, they will produce the same output and end in the same state.</p></blockquote><p>   简单来说：<strong>相同的初识状态 + 相同的输入 = 相同的结束状态</strong>。引文中有一个很重要的词<code>deterministic</code>，就是说不同节点要以相同且确定性的函数来处理输入，而不要引入一下不确定的值，比如本地时间等。如何保证所有节点 <code>get the same inputs in the same order</code>，使用replicated log是一个很不错的注意，log具有持久化、保序的特点，是大多数分布式系统的基石。</p><p>  因此，可以这么说，在raft中，leader将客户端请求（command）封装到一个个log entry，将这些log entries复制（replicate）到所有follower节点，然后大家按相同顺序应用（apply）log entry中的command，则状态肯定是一致的。</p><p>  下图形象展示了这种log-based replicated state machine<br><img src="/images/1089769-20181216202234422-28123572.png" alt="img"></p>"<h2 id="请求完整流程"><a href="#请求完整流程" class="headerlink" title="请求完整流程"></a>请求完整流程</h2><p>  当系统（leader）收到一个来自客户端的写请求，到返回给客户端，整个过程从leader的视角来看会经历以下步骤：</p><ul><li>leader append log entry</li><li>leader issue AppendEntries RPC in parallel</li><li>leader wait for majority response</li><li>leader apply entry to state machine</li><li>leader reply to client</li><li>leader notify follower apply log</li></ul><p>  可以看到日志的提交过程有点类似两阶段提交(2PC)，不过与2PC的区别在于，leader只需要大多数（majority）节点的回复即可，这样只要超过一半节点处于工作状态则系统就是可用的。</p><p>  那么日志在每个节点上是什么样子的呢<br><img src="/images/1089769-20181216202309906-1698663454.png" alt="img"></p>"<p>  不难看到，logs由顺序编号的log entry组成 ，每个log entry除了包含command，还包含产生该log entry时的leader term。从上图可以看到，五个节点的日志并不完全一致，raft算法为了保证高可用，并不是强一致性，而是最终一致性，leader会不断尝试给follower发log entries，直到所有节点的log entries都相同。</p><p>  在上面的流程中，leader只需要日志被复制到大多数节点即可向客户端返回，一旦向客户端返回成功消息，那么系统就必须保证log（其实是log所包含的command）在任何异常的情况下都不会发生回滚。这里有两个词：commit（committed），apply(applied)，前者是指日志被复制到了大多数节点后日志的状态；而后者则是节点将日志应用到状态机，真正影响到节点状态。</p><blockquote><p>The leader decides when it is safe to apply a log entry to the state machines; such an entry is called committed. Raft guarantees that committed entries are durable and will eventually be executed by all of the available state machines. A log entry is committed once the leader that created the entry has replicated it on a majority of the servers</p></blockquote><h1 id="safety"><a href="#safety" class="headerlink" title="safety"></a>safety</h1><p>  在上面提到只要日志被复制到majority节点，就能保证不会被回滚，即使在各种异常情况下，这根leader election提到的选举约束有关。在这一部分，主要讨论raft协议在各种各样的异常情况下如何工作的。</p><p>  衡量一个分布式算法，有许多属性，如</p><ul><li>safety：nothing bad happens,</li><li>liveness： something good eventually happens.</li></ul><p>  在任何系统模型下，都需要满足safety属性，即在任何情况下，系统都不能出现不可逆的错误，也不能向客户端返回错误的内容。比如，raft保证被复制到大多数节点的日志不会被回滚，那么就是safety属性。而raft最终会让所有节点状态一致，这属于liveness属性。</p><p>  raft协议会保证以下属性<br><img src="/images/1089769-20181216202333639-30919755.png" alt="img"></p>"<h3 id="Election-safety"><a href="#Election-safety" class="headerlink" title="Election safety"></a>Election safety</h3><p>  选举安全性，即任一任期内最多一个leader被选出。这一点非常重要，在一个复制集中任何时刻只能有一个leader。系统中同时有多余一个leader，被称之为脑裂（brain split），这是非常严重的问题，会导致数据的覆盖丢失。在raft中，两点保证了这个属性：</p><ul><li>一个节点某一任期内最多只能投一票；</li><li>只有获得majority投票的节点才会成为leader。</li></ul><p>  因此，<strong>某一任期内一定只有一个leader</strong>。</p><h3 id="log-matching"><a href="#log-matching" class="headerlink" title="log matching"></a>log matching</h3><p>  很有意思，log匹配特性， 就是说如果两个节点上的某个log entry的log index相同且term相同，那么在该index之前的所有log entry应该都是相同的。如何做到的？依赖于以下两点</p><ul><li>If two entries in different logs have the same index and term, then they store the same command.</li><li>If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.</li></ul><p>  首先，leader在某一term的任一位置只会创建一个log entry，且log entry是append-only。其次，consistency check。leader在AppendEntries中包含最新log entry之前的一个log 的term和index，如果follower在对应的term index找不到日志，那么就会告知leader不一致。</p><p>  在没有异常的情况下，log matching是很容易满足的，但如果出现了node crash，情况就会变得负责。比如下图<br><img src="/images/1089769-20181216202408734-1760694063.png" alt="img"></p>"<p>  <strong>注意</strong>：上图的a-f不是6个follower，而是某个follower可能存在的六个状态</p><p>  leader、follower都可能crash，那么follower维护的日志与leader相比可能出现以下情况</p><ul><li>比leader日志少，如上图中的ab</li><li>比leader日志多，如上图中的cd</li><li>某些位置比leader多，某些日志比leader少，如ef（多少是针对某一任期而言）</li></ul><p>  当出现了leader与follower不一致的情况，leader强制follower复制自己的log</p><blockquote><p>To bring a follower’s log into consistency with its own, the leader must find the latest log entry where the two logs agree, delete any entries in the follower’s log after that point, and send the follower all of the leader’s entries after that point.</p></blockquote><p>  leader会维护一个nextIndex[]数组，记录了leader可以发送每一个follower的log index，初始化为eader最后一个log index加1， 前面也提到，leader选举成功之后会立即给所有follower发送AppendEntries RPC（不包含任何log entry， 也充当心跳消息）,那么流程总结为：</p><blockquote><p>s1 leader 初始化nextIndex[x]为 leader最后一个log index + 1<br>s2 AppendEntries里prevLogTerm prevLogIndex来自 logs[nextIndex[x] - 1]<br>s3 如果follower判断prevLogIndex位置的log term不等于prevLogTerm，那么返回 False，否则返回True<br>s4 leader收到follower的回复，如果返回值是False，则nextIndex[x] -= 1, 跳转到s2. 否则<br>s5 同步nextIndex[x]后的所有log entries</p></blockquote><h3 id="leader-completeness-vs-elcetion-restriction"><a href="#leader-completeness-vs-elcetion-restriction" class="headerlink" title="leader completeness vs elcetion restriction"></a>leader completeness vs elcetion restriction</h3><p>  leader完整性：如果一个log entry在某个任期被提交（committed），那么这条日志一定会出现在所有更高term的leader的日志里面。这个跟leader election、log replication都有关。</p><ul><li>一个日志被复制到majority节点才算committed</li><li>一个节点得到majority的投票才能成为leader，而节点A给节点B投票的其中一个前提是，B的日志不能比A的日志旧。下面的引文指处了如何判断日志的新旧</li></ul><blockquote><p>voter denies its vote if its own log is more up-to-date than that of the candidate.</p></blockquote><blockquote><p>If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date.</p></blockquote><p>  上面两点都提到了majority：commit majority and vote majority，根据Quorum，这两个majority一定是有重合的，因此被选举出的leader一定包含了最新的committed的日志。</p><p>  raft与其他协议（Viewstamped Replication、mongodb）不同，raft始终保证leade包含最新的已提交的日志，因此leader不会从follower catchup日志，这也大大简化了系统的复杂度。</p><h1 id="corner-case"><a href="#corner-case" class="headerlink" title="corner case"></a>corner case</h1><h2 id="stale-leader"><a href="#stale-leader" class="headerlink" title="stale leader"></a>stale leader</h2><p>  raft保证Election safety，即一个任期内最多只有一个leader，但在网络分割（network partition）的情况下，<strong>可能会出现两个leader，但两个leader所处的任期是不同的</strong>。如下图所示<br><img src="/images/1089769-20181216202652306-2050900084.png" alt="img"></p>"<p>  系统有5个节点ABCDE组成，在term1，Node B是leader，但Node A、B和Node C、D、E之间出现了网络分割，因此Node C、D、E无法收到来自leader（Node B）的消息，在election time之后，Node C、D、E会分期选举，由于满足majority条件，Node E成为了term 2的leader。因此，在系统中貌似出现了两个leader：term 1的Node B， term 2的Node E, Node B的term更旧，但由于无法与Majority节点通信，NodeB仍然会认为自己是leader。</p><p>  在这样的情况下，我们来考虑读写。</p><p>  首先，如果客户端将请求发送到了NodeB，NodeB无法将log entry 复制到majority节点，因此不会告诉客户端写入成功，这就不会有问题。</p><p>  对于读请求，stale leader可能返回stale data，比如在read-after-write的一致性要求下，客户端写入到了term2任期的leader Node E，但读请求发送到了Node B。如果要保证不返回stale data，leader需要check自己是否过时了，办法就是与大多数节点通信一次，这个可能会出现效率问题。另一种方式是使用lease，但这就会依赖物理时钟。</p><p>  从raft的论文中可以看到，leader转换成follower的条件是收到来自更高term的消息，如果网络分割一直持续，那么stale leader就会一直存在。而在raft的一些实现或者raft-like协议中，leader如果收不到majority节点的消息，那么可以自己step down，自行转换到follower状态。</p><h2 id="State-Machine-Safety"><a href="#State-Machine-Safety" class="headerlink" title="State Machine Safety"></a>State Machine Safety</h2><p>  前面在介绍safety的时候有一条属性没有详细介绍，那就是State Machine Safety：</p><blockquote><p>State Machine Safety: if a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index.</p></blockquote><p>  如果节点将某一位置的log entry应用到了状态机，那么其他节点在同一位置不能应用不同的日志。简单点来说，所有节点在同一位置（index in log entries）应该应用同样的日志。但是似乎有某些情况会违背这个原则：<br><img src="/images/1089769-20181216202438174-260853001.png" alt="img"></p>"<p>  上图是一个较为复杂的情况。在时刻(a), s1是leader，在term2提交的日志只赋值到了s1 s2两个节点就crash了。在时刻（b), s5成为了term 3的leader，日志只赋值到了s5，然后crash。然后在(c)时刻，s1又成为了term 4的leader，开始赋值日志，于是把term2的日志复制到了s3，此刻，可以看出term2对应的日志已经被复制到了majority，因此是committed，可以被状态机应用。不幸的是，接下来（d）时刻，s1又crash了，s5重新当选，然后将term3的日志复制到所有节点，这就出现了一种奇怪的现象：被复制到大多数节点（或者说可能已经应用）的日志被回滚。</p><p>  究其根本，是因为term4时的leader s1在（C）时刻提交了之前term2任期的日志。为了杜绝这种情况的发生：</p><blockquote><p><strong>Raft never commits log entries from previous terms by counting replicas</strong>.<br>Only log entries from the leader’s current term are committed by counting replicas; once an entry from the current term has been committed in this way, then all prior entries are committed indirectly because of the Log Matching Property.</p></blockquote><p>  也就是说，某个leader选举成功之后，不会直接提交前任leader时期的日志，而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了，具体怎么实现了，在log matching部分有详细介绍。那么问题来了，如果leader被选举后没有收到客户端的请求呢，论文中有提到，在任期开始的时候发立即尝试复制、提交一条空的log</p><blockquote><p>Raft handles this by having each leader commit a blank no-op entry into the log at the start of its term.</p></blockquote><p>  因此，在上图中，不会出现（C）时刻的情况，即term4任期的leader s1不会复制term2的日志到s3。而是如同(e)描述的情况，通过复制-提交 term4的日志顺便提交term2的日志。如果term4的日志提交成功，那么term2的日志也一定提交成功，此时即使s1crash，s5也不会重新当选。</p><h2 id="leader-crash"><a href="#leader-crash" class="headerlink" title="leader crash"></a>leader crash</h2><p>  follower的crash处理方式相对简单，leader只要不停的给follower发消息即可。当leader crash的时候，事情就会变得复杂。在<a href="http://www.cnblogs.com/mindwind/p/5231986.html" target="_blank" rel="noopener">这篇文章</a>中，作者就给出了一个更新请求的流程图。<br><img src="/images/815275-20160301175358173-526445555.png" alt="例子"><br>  我们可以分析leader在任意时刻crash的情况，有助于理解raft算法的容错性。</p>"<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>  raft将共识问题分解成两个相对独立的问题，leader election，log replication。流程是先选举出leader，然后leader负责复制、提交log（log中包含command）</p><p>  为了在任何异常情况下系统不出错，即满足safety属性，对leader election，log replication两个子问题有诸多约束</p><p>leader election约束：</p><ul><li>同一任期内最多只能投一票，先来先得</li><li>选举人必须比自己知道的更多（比较term，log index）</li></ul><p>log replication约束：</p><ul><li>一个log被复制到大多数节点，就是committed，保证不会回滚</li><li>leader一定包含最新的committed log，因此leader只会追加日志，不会删除覆盖日志</li><li>不同节点，某个位置上日志相同，那么这个位置之前的所有日志一定是相同的</li><li>Raft never commits log entries from previous terms by counting replicas.</li></ul><p>  本文是在看完raft论文后自己的总结，不一定全面。个人觉得，如果只是相对raft协议有一个简单了解，看这个<a href="http://thesecretlivesofdata.com/raft/" target="_blank" rel="noopener">动画演示</a>就足够了，如果想深入了解，还是要看论文，论文中Figure 2对raft算法进行了概括。最后，还是找一个实现了raft算法的系统来看看更好。</p><h1 id="references"><a href="#references" class="headerlink" title="references"></a>references</h1><p><a href="https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14" target="_blank" rel="noopener">https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14</a><br><a href="https://raft.github.io/" target="_blank" rel="noopener">https://raft.github.io/</a><br><a href="http://thesecretlivesofdata.com/raft/" target="_blank" rel="noopener">http://thesecretlivesofdata.com/raft/</a></p><p>转自：<a href="https://www.cnblogs.com/xybaby/p/10124083.html" target="_blank" rel="noopener">一文搞懂Raft算法 </a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;strong&gt;正文&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。在这里强调了是在工程上，因为在学术理论界，最耀眼的还是大名鼎鼎的Paxos。但Paxos是：少数真正理解的人觉得简单，尚未理解的人觉得很难，大多数
      
    
    </summary>
    
    
      <category term="Raft" scheme="http://yoursite.com/tags/Raft/"/>
    
  </entry>
  
  <entry>
    <title>mysql技术内幕</title>
    <link href="http://yoursite.com/2022/04/09/%E5%A4%8D%E4%B9%A0%E6%80%BB%E7%BB%93/mysql%E6%8A%80%E6%9C%AF%E5%86%85%E5%B9%95/"/>
    <id>http://yoursite.com/2022/04/09/复习总结/mysql技术内幕/</id>
    <published>2022-04-08T16:00:00.000Z</published>
    <updated>2022-04-27T09:57:35.837Z</updated>
    
    <content type="html"><![CDATA[<h1 id="MySQL体系结构和存储引擎"><a href="#MySQL体系结构和存储引擎" class="headerlink" title="MySQL体系结构和存储引擎"></a>MySQL体系结构和存储引擎</h1><p>数据库：物理操作系统文件或其他形式文件类型的集合。<br>实例：MySQL数据库由后台线程以及一个共享内存区组成</p><p>MySQL数据库实例在系统上的表现就是一个进程。</p><h2 id="mysql体系结构"><a href="#mysql体系结构" class="headerlink" title="mysql体系结构"></a>mysql体系结构</h2><p><img src="/images/8e7a9c2641330bbd.jpg" alt="img" style="zoom: 50%;"></p>"<p>需要特别注意的是，存储引擎是基于表的，而不是数据库。</p><h3 id="InnoDB存储引擎"><a href="#InnoDB存储引擎" class="headerlink" title="InnoDB存储引擎"></a>InnoDB存储引擎</h3><p>InnoDB存储引擎支持事务，其设计目标主要面向在线事务处理（OLTP）的应用。</p><p>InnoDB通过使用多版本并发控制（MVCC）来获得高并发性，并且实现了SQL标准的4种隔离级别，默认为REPEATABLE级别。同时，使用一种被称为next-key locking的策略来避免幻读（phantom）现象的产生。除此之外，InnoDB储存引擎还提供了插入缓冲（insert buffer）、二次写（double write）、自适应哈希索引（adaptive hash index）、预读（read ahead）等高性能和高可用的功能。</p><h3 id="MyISAM存储引擎"><a href="#MyISAM存储引擎" class="headerlink" title="MyISAM存储引擎"></a>MyISAM存储引擎</h3><p>MyISAM存储引擎不支持事务、表锁设计，支持全文索引，主要面向一些OLAP数据库应用。</p><h1 id="InnoDB存储引擎-1"><a href="#InnoDB存储引擎-1" class="headerlink" title="InnoDB存储引擎"></a>InnoDB存储引擎</h1><h2 id="InnoDB体系架构"><a href="#InnoDB体系架构" class="headerlink" title="InnoDB体系架构"></a>InnoDB体系架构</h2><p><img src="/images/75053726540336d3.jpg" alt="img" style="zoom: 33%;"></p>"<h3 id="后台线程"><a href="#后台线程" class="headerlink" title="后台线程"></a>后台线程</h3><ol><li>Master Thread<br>Master Thread是一个非常核心的后台线程，主要负责将缓冲池中的数据异步刷新到磁盘，保证数据的一致性，包括脏页的刷新、合并插入缓冲（INSERT BUFFER）、UNDO页的回收等。</li><li>IO Thread<br>IO Thread的工作主要是负责这些IO请求的回调（call back）处理</li><li>Purge Thread<br>事务被提交后，其所使用的undolog可能不再需要，因此需要PurgeThread来回收已经使用并分配的undo页。</li><li>Page Cleaner Thread<br>Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。而其目的是为了减轻原Master Thread的工作及对于用户查询线程的阻塞，进一步提高InnoDB存储引擎的性能。</li></ol><h3 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h3><h4 id="缓冲池"><a href="#缓冲池" class="headerlink" title="缓冲池"></a>缓冲池</h4><ul><li>缓冲池简单来说就是一块内存区域，通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。</li><li>页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发，而是通过一种称为Checkpoint的机制刷新回磁盘</li><li>缓冲池的大小直接影响着数据库的整体性能。</li></ul><p>具体来看，缓冲池中缓存的数据页类型有：索引页、数据页、undo页、插入缓冲（insert buffer）、自适应哈希索引（adaptive hash index）、InnoDB存储的锁信息（lock info）、数据字典信息（data dictionary）等。图很好地显示了InnoDB存储引擎中内存的结构情况。</p><p><img src="/images/772d9add029125fa.jpg" alt="img" style="zoom:50%;"></p>"<h4 id="LRU-List、Free-List和Flush-List（如何管理缓冲池）"><a href="#LRU-List、Free-List和Flush-List（如何管理缓冲池）" class="headerlink" title="LRU List、Free List和Flush List（如何管理缓冲池）"></a>LRU List、Free List和Flush List（如何管理缓冲池）</h4><p>缓冲池中页的大小默认为16KB，同样使用LRU算法对缓冲池进行管理<br>在InnoDB的存储引擎中，LRU列表中还加入了midpoint位置。新读取到的页，虽然是最新访问的页，但并不是直接放入到LRU列表的首部，而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下，该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制</p><p><img src="/images/effe171c422bfffe8443ce98d8c26956.png" alt="effe171c422bfffe8443ce98d8c26956.png" style="zoom: 67%;"></p>"<ul><li>在InnoDB存储引擎中，把midpoint之后的列表称为old列表，之前的列表称为new列表。可以简单地理解为new列表中的页都是最为活跃的热点数据。</li><li>InnoDB存储引擎引入了另一个参数来进一步管理LRU列表，这个参数是innodb_old_blocks_time，用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。（old区存活了足够长的时间会被移到young区）</li><li>LRU列表用来管理已经读取的页，但当数据库刚启动时，LRU列表是空的，即没有任何的页。这时页都存放在Free列表中。当需要从缓冲池中分页时，首先从Free列表中查找是否有可用的空闲页，若有则将该页从Free列表中删除，放入到LRU列表中。否则，根据LRU算法，淘汰LRU列表末尾的页，将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时，称此时发生的操作为page made young，而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。可以通过命令SHOW ENGINE INNODB STATUS来观察LRU列表及Free列表的使用情况和运行状态。</li></ul><p>flush listh与lru list</p><p>在LRU列表中的页被修改后，称该页为脏页（dirty page），即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘，而Flush列表中的页即为脏页列表。需要注意的是，脏页既存在于LRU列表中，也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性，Flush列表用来管理将页刷新回磁盘，二者互不影响。</p><h4 id="重做日志缓冲"><a href="#重做日志缓冲" class="headerlink" title="重做日志缓冲"></a>重做日志缓冲</h4><p>InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区，然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大，因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件</p><p>在通常情况下，8MB的重做日志缓冲池足以满足绝大部分的应用，因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。</p><ul><li>Master Thread每一秒将重做日志缓冲刷新到重做日志文件；</li><li>每个事务提交时会将重做日志缓冲刷新到重做日志文件；</li><li>当重做日志缓冲池剩余空间小于1/2时，重做日志缓冲刷新到重做日志文件。</li></ul><h2 id="Checkpoint技术"><a href="#Checkpoint技术" class="headerlink" title="Checkpoint技术"></a>Checkpoint技术</h2><p>为了避免发生数据丢失的问题，当前事务数据库系统普遍都采用了Write Ahead Log策略，即当事务提交时，先写重做日志，再修改页。当由于发生宕机而导致数据丢失时，通过重做日志来完成数据的恢复。这也是事务ACID中D（Durability持久性）的要求。</p><ul><li>当数据库发生宕机时，数据库不需要重做所有的日志，因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。</li><li>当缓冲池不够用时，根据LRU算法会溢出最近最少使用的页，若此页为脏页，那么需要强制执行Checkpoint，将脏页也就是页的新版本刷回磁盘。</li><li>重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的，并不是让其无限增大的，这从成本及管理上都是比较困难的。重做日志可以被重用的部分是指这些重做日志已经不再需要，即当数据库发生宕机时，数据库恢复操作不需要这部分的重做日志，因此这部分就可以被覆盖重用。若此时重做日志还需要使用，那么必须强制产生Checkpoint，将缓冲池中的页至少刷新到当前重做日志的位置。</li></ul><p>对于InnoDB存储引擎而言，其是通过LSN（Log Sequence Number）来标记版本的。而LSN是8字节的数字，其单位是字节。每个页有LSN，重做日志中也有LSN，Checkpoint也有LSN。</p><p>有两种Checkpoint，分别为：</p><ul><li>Sharp Checkpoint<br>Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘，这是默认的工作方式，即参数innodb_fast_shutdown=1</li><li>Fuzzy Checkpoint</li></ul><p>在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint：</p><ul><li>Master Thread Checkpoint<br>差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘</li><li>FLUSH_LRU_LIST Checkpoint<br>InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用</li><li>Async/Sync Flush Checkpoint<br>重做日志文件不可用的情况，这时需要强制将一些页刷新回磁盘，而此时脏页是从脏页列表中选取的</li><li><p>Dirty Page too much Checkpoint</p><p>脏页的数量太多</p></li></ul><h2 id="Master-Thread工作方式"><a href="#Master-Thread工作方式" class="headerlink" title="Master Thread工作方式"></a>Master Thread工作方式</h2><p>Master Thread具有最高的线程优先级别。其内部由多个循环（loop）组成：主循环（loop）、后台循环（backgroup loop）、刷新循环（flush loop）、暂停循环（suspend loop）。</p><p>每秒一次的操作包括：</p><ul><li>redo日志缓冲刷新到磁盘，即使这个事务还没有提交（总是）；</li><li>合并插入缓冲（可能）；</li><li>至多刷新100个InnoDB的缓冲池中的脏页到磁盘（可能）；</li><li>如果当前没有用户活动，则切换到background loop（可能）。</li></ul><p>每10秒的操作，包括如下内容：</p><ul><li>刷新100个脏页到磁盘（可能的情况下）；</li><li>合并至多5个插入缓冲（总是）；</li><li>将日志缓冲刷新到磁盘（总是）；</li><li>删除无用的Undo页（总是）；</li><li>刷新100个或者10个脏页到磁盘（总是）。</li></ul><p>接着来看background loop，若当前没有用户活动（数据库空闲时）或者数据库关闭（shutdown），就会切换到这个循环。background loop会执行以下操作：</p><ul><li>删除无用的Undo页（总是）；</li><li>合并20个插入缓冲（总是）；</li><li>跳回到主循环（总是）；</li><li>不断刷新100个页直到符合条件（可能，跳转到flush loop中完成）。</li></ul><h2 id="InnoDB-关键特性"><a href="#InnoDB-关键特性" class="headerlink" title="InnoDB 关键特性"></a>InnoDB 关键特性</h2><ul><li>插入缓冲（Insert Buffer）</li><li>两次写（Double Write）</li><li>自适应哈希索引（Adaptive Hash Index）</li><li>异步IO（Async IO）</li><li>刷新邻接页（Flush Neighbor Page）</li></ul><h3 id="插入缓冲"><a href="#插入缓冲" class="headerlink" title="插入缓冲"></a>插入缓冲</h3><h4 id="Insert-Buffer"><a href="#Insert-Buffer" class="headerlink" title="Insert Buffer"></a>Insert Buffer</h4><p>Insert Buffer的使用需要同时满足以下两个条件：</p><ul><li>索引是辅助索引（secondary index）；</li><li>索引不是唯一（unique）的。</li></ul><p>对于非聚集索引的插入或更新操作，不是每一次直接插入到索引页中，而是先判断插入的非聚集索引页是否在缓冲池中，若在，则直接插入；若不在，则先放入到一个Insert Buffer对象中，好似欺骗。数据库这个非聚集的索引已经插到叶子节点，而实际并没有，只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge（合并）操作，这时通常能将多个插入合并到一个操作中（因为在一个索引页中），这就大大提高了对于非聚集索引插入的性能。</p><h4 id="Change-Buffer"><a href="#Change-Buffer" class="headerlink" title="Change Buffer"></a>Change Buffer</h4><p>InnoDB从1.0.x版本开始引入了Change Buffer，可将其视为Insert Buffer的升级。从这个版本开始，InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲，他们分别是：Insert Buffer、Delete Buffer、Purge buffer。</p><p>对一条记录进行UPDATE操作可能分为两个过程：</p><ul><li>将记录标记为已删除；</li><li>真正将记录删除。</li></ul><p>因此Delete Buffer对应UPDATE操作的第一个过程，即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程，即将记录真正的删除</p><h4 id="Merge-Insert-Buffer"><a href="#Merge-Insert-Buffer" class="headerlink" title="Merge Insert Buffer"></a>Merge Insert Buffer</h4><p>Insert/Change Buffer是一棵B+树。若需要实现插入记录的辅助索引页不在缓冲池中，那么需要将辅助索引记录首先插入到这棵B+树中。但是Insert Buffer中的记录何时合并（merge）到真正的辅助索引中呢？这是本小节需要关注的重点。</p><p>概括地说，Merge Insert Buffer的操作可能发生在以下几种情况下：</p><ul><li>辅助索引页被读取到缓冲池时；</li><li>Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时；</li><li>Master Thread。</li></ul><h3 id="两次写"><a href="#两次写" class="headerlink" title="两次写"></a>两次写</h3><p>1.为什么要两次写？</p><p>当发生数据库宕机时，可能InnoDB存储引擎正在写入某个页到表中，而这个页只写了一部分，比如16KB的页，只写了前4KB，之后就发生了宕机，这种情况被称为部分写失效（partial page write）。在InnoDB存储引擎未使用doublewrite技术前，曾经出现过因为部分写失效而导致数据丢失的情况。</p><p>有经验的DBA也许会想，如果发生写失效，可以通过重做日志进行恢复。这是一个办法。但是必须清楚地认识到，重做日志中记录的是对页的物理操作，如偏移量800，写’aaaa’记录。如果这个页本身已经发生了损坏，再对其进行重做是没有意义的。这就是说，在应用（apply）重做日志前，用户需要一个页的副本，当写入失效发生时，先通过页的副本来还原该页，再进行重做，这就是doublewrite</p><p>2.两次写的过程</p><p>doublewrite由两部分组成，一部分是内存中的doublewrite buffer，大小为2MB，另一部分是物理磁盘上共享表空间中连续的128个页，即2个区（extent），大小同样为2MB。在对缓冲池的脏页进行刷新时，并不直接写磁盘，而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer，之后通过doublewrite buffer再分两次，每次1MB顺序地写入共享表空间的物理磁盘上，然后马上调用fsync函数，同步磁盘，避免缓冲写带来的问题。在这个过程中，因为doublewrite页是连续的，因此这个过程是顺序写的，开销并不是很大。在完成doublewrite页的写入后，再将doublewrite buffer中的页写入各个表空间文件中，此时的写入则是离散的。</p><p><img src="/images/1e25655d05221940.jpg" alt="img" style="zoom:50%;"></p>"<h3 id="自适应哈希索引"><a href="#自适应哈希索引" class="headerlink" title="自适应哈希索引"></a>自适应哈希索引</h3><p>InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升，则建立哈希索引，称之为自适应哈希索引（Adaptive Hash Index，AHI）</p><h3 id="异步IO"><a href="#异步IO" class="headerlink" title="异步IO"></a>异步IO</h3><p>为了提高磁盘操作性能，当前的数据库系统都采用异步IO（Asynchronous IO，AIO）的方式来处理磁盘操作。InnoDB存储引擎亦是如此。</p><h3 id="刷新邻接页"><a href="#刷新邻接页" class="headerlink" title="刷新邻接页"></a>刷新邻接页</h3><p>其工作原理为：当刷新一个脏页时，InnoDB存储引擎会检测该页所在区（extent）的所有页，如果是脏页，那么一起进行刷新</p><p>对于传统机械硬盘建议启用该特性，而对于固态硬盘有着超高IOPS性能的磁盘，则建议将该参数设置为0，即关闭此特性。</p><h1 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h1><h2 id="日志文件"><a href="#日志文件" class="headerlink" title="日志文件"></a>日志文件</h2><ul><li>错误日志（error log）</li><li>二进制日志（binlog）</li><li>慢查询日志（slow query log）</li><li>查询日志（log）</li></ul><h3 id="二进制日志"><a href="#二进制日志" class="headerlink" title="二进制日志"></a>二进制日志</h3><p>二进制日志（binary log）记录了对MySQL数据库执行更改的所有操作，但是不包括SELECT和SHOW这类操作，因为这类操作对数据本身并没有修改。</p><p>主要有以下几种作用。</p><ul><li>恢复（recovery）：某些数据的恢复需要二进制日志，例如，在一个数据库全备文件恢复后，用户可以通过二进制日志进行point-in-time的恢复。</li><li>复制（replication）：其原理与恢复类似，通过复制和执行二进制日志使一台远程的MySQL数据库（一般称为slave或standby）与一台MySQL数据库（一般称为master或primary）进行实时同步。</li><li>审计（audit）：用户可以通过二进制日志中的信息来进行审计，判断是否有对数据库进行注入的攻击。</li></ul><p>通过配置参数log-bin[=name]可以启动二进制日志。如果不指定name，则默认二进制日志文件名为主机名，后缀名为二进制日志的序列号，所在路径为数据库所在目录（datadir）</p><h4 id="sync-binlog"><a href="#sync-binlog" class="headerlink" title="sync_binlog"></a>sync_binlog</h4><p>二进制日志文件并不是每次写的时候同步到磁盘。因此当数据库所在操作系统发生宕机时，可能会有最后一部分数据没有写入二进制日志文件中，这给恢复和复制带来了问题。<br>参数<code>sync_binlog=[N]</code>表示每写缓冲多次就同步到磁盘。如果将N设为1，即sync_binlog=1表示采用同步写磁盘的方式来写二进制日志，这时写操作不使用才做系统的缓冲来写二进制日志。（备注：该值默认为0，采用操作系统机制进行缓冲数据同步）。<br>当sync_binlog=1，还会存在另外问题。当使用InnoDB存储引擎时，在一个事务发出commit动作之前，由于sync_binlog设为1，因此会将二进制日志立即写入磁盘。如果这时已经写入了二进制日志，但是提交还没有发生，并且此时发生了宕机，那么在Mysql数据库下次启动时，由于commit操作并没有发生，所以这个事务会被回滚掉。但是二进制日志已经记录了该事务信息，不能被回滚。<br>这个问题，可以将innodb_support_xa设为1来解决，确保二进制日志和InnoDB存储引擎数据文件的同步。<br>从官方解释来看，innodb_support_xa的作用是分两类：<br>第一，支持多实例分布式事务（外部xa事务），这个一般在分布式数据库环境中用得较多。<br>第二，支持内部xa事务，说白了也就是说支持binlog与innodb redo log之间数据一致性。</p><h4 id="binlog-format"><a href="#binlog-format" class="headerlink" title="binlog_format"></a>binlog_format</h4><p>binlog_format参数十分重要，它影响了记录二进制日志的格式。<br>MySQL 5.1开始引入了binlog_format参数，该参数可设的值有STATEMENT、ROW和MIXED。<br>（1）STATEMENT格式和之前的MySQL版本一样，二进制日志文件记录的是日志的逻辑SQL语句。<br>（2）在ROW格式下，二进制日志记录的不再是简单的SQL语句了，而是记录表的行更改情况。基于ROW格式的复制类似于Oracle的物理Standby（当然，还是有些区别）。同时，对上述提及的Statement格式下复制的问题予以解决。从MySQL 5.1版本开始，如果设置了binlog_format为ROW，可以将InnoDB的事务隔离基本设为READ COMMITTED，以获得更好的并发性。<br>（3）在MIXED格式下，MySQL默认采用STATEMENT格式进行二进制日志文件的记录，但是在一些情况下会使用ROW格式。</p><p>在通常情况下，我们将参数binlog_format设置为ROW，这可以为数据库的恢复和复制带来更好的可靠性。但是不能忽略的一点是，这会带来二进制文件大小的增加，有些语句下的ROW格式可能需要更大的容量。</p><h2 id="表结构定义文件"><a href="#表结构定义文件" class="headerlink" title="表结构定义文件"></a>表结构定义文件</h2><p>因为MySQL插件式存储引擎的体系结构的关系，MySQL数据的存储是根据表进行的，每个表都会有与之对应的文件。但不论表采用何种存储引擎，MySQL都有一个以frm为后缀名的文件，这个文件记录了该表的表结构定义。</p><h2 id="InnoDB存储引擎文件"><a href="#InnoDB存储引擎文件" class="headerlink" title="InnoDB存储引擎文件"></a>InnoDB存储引擎文件</h2><h3 id="表空间文件"><a href="#表空间文件" class="headerlink" title="表空间文件"></a>表空间文件</h3><p><img src="/images/23fbde65ca4040db.jpg" alt="img" style="zoom: 50%;"></p>"<h3 id="重做日志文件"><a href="#重做日志文件" class="headerlink" title="重做日志文件"></a>重做日志文件</h3><p>在默认情况下，在InnoDB存储引擎的数据目录下会有两个名为ib_logfile0和ib_logfile1的文件。在MySQL官方手册中将其称为InnoDB存储引擎的日志文件，不过更准确的定义应该是重做日志文件（redo log file）。<br>每个InnoDB存储引擎至少有1个重做日志文件组（group），每个文件组下至少有2个重做日志文件，如默认的ib_logfile0和ib_logfile1。<br>InnoDB存储引擎先写重做日志文件1，当达到文件的最后时，会切换至重做日志文件2，再当重做日志文件2也被写满时，会再切换到重做日志文件1中。</p><p><img src="/images/b79c6e1ca3497498.jpg" alt="img" style="zoom:50%;"></p>"<p>重做日志文件的大小设置对于InnoDB存储引擎的性能有着非常大的影响。一方面重做日志文件不能设置得太大，如果设置得很大，在恢复时可能需要很长的时间；另一方面又不能设置得太小了，否则可能导致一个事务的日志需要多次切换重做日志文件。此外，重做日志文件太小会导致频繁地发生async checkpoint，导致性能的抖动。</p><p>也许有人会问，既然同样是记录事务日志，和之前介绍的二进制日志有什么区别？<br>首先，二进制日志会记录所有与MySQL数据库有关的日志记录，包括InnoDB、MyISAM、Heap等其他存储引擎的日志。而InnoDB存储引擎的重做日志只记录有关该存储引擎本身的事务日志。<br>其次，记录的内容不同，无论用户将二进制日志文件记录的格式设为STATEMENT还是ROW，又或者是MIXED，其记录的都是关于一个事务的具体操作内容，即该日志是逻辑日志。而InnoDB存储引擎的重做日志文件记录的是关于每个页（Page）的更改的物理情况。<br>此外，写入的时间也不同，二进制日志文件仅在事务提交前进行提交，即只写磁盘一次，不论这时该事务多大。而在事务进行的过程中，却不断有重做日志条目（redo entry）被写入到重做日志文件中。</p><p>写入重做日志文件的操作不是直接写，而是先写入一个重做日志缓冲（redo log buffer）中，然后按照一定的条件顺序地写入日志文件。图很好地诠释了重做日志的写入过程。</p><p><img src="/images/f9f2e0c043661375.jpg" alt="img" style="zoom:50%;"></p>"<p>从重做日志缓冲往磁盘写入时，是按512个字节，也就是一个扇区的大小进行写入。因为扇区是写入的最小单位，因此可以保证写入必定是成功的。因此在重做日志的写入过程中不需要有doublewrite。</p><p>前面提到了从日志缓冲写入磁盘上的重做日志文件是按一定条件进行的，那这些条件有哪些呢？</p><p>主线程中每秒会将重做日志缓冲写入磁盘的重做日志文件中，不论事务是否已经提交。另一个触发写磁盘的过程是由参数innodb_flush_log_at_trx_commit控制，表示在提交（commit）操作时，处理重做日志的方式。<br>参数innodb_flush_log_at_trx_commit的有效值有0、1、2。0代表当提交事务时，并不将事务的重做日志写入磁盘上的日志文件，而是等待主线程每秒的刷新。1和2不同的地方在于：1表示在执行commit时将重做日志缓冲同步写到磁盘，即伴有fsync的调用。2表示将重做日志异步写到磁盘，即写到文件系统的缓存中。因此不能完全保证在执行commit时肯定会写入重做日志文件，只是有这个动作发生。<br>因此为了保证事务的ACID中的持久性，必须将innodb_flush_log_at_trx_commit设置为1，也就是每当有事务提交时，就必须确保事务都已经写入重做日志文件。</p><h1 id="表"><a href="#表" class="headerlink" title="表"></a>表</h1><h2 id="索引组织表"><a href="#索引组织表" class="headerlink" title="索引组织表"></a>索引组织表</h2><p>在InnoDB存储引擎中，表都是根据主键顺序组织存放的，这种存储方式的表称为索引组织表（index organized table）</p><h2 id="InnoDB逻辑存储结构"><a href="#InnoDB逻辑存储结构" class="headerlink" title="InnoDB逻辑存储结构"></a>InnoDB逻辑存储结构</h2><p>从InnoDB存储引擎的逻辑存储结构看，所有数据都被逻辑地存放在一个空间中，称之为表空间（tablespace）。表空间又由段（segment）、区（extent）、页（page）组成。页在一些文档中有时也称为块（block）</p><p><img src="/images/004927f314469439.jpg" alt="img" style="zoom:50%;"></p>"<h3 id="表空间"><a href="#表空间" class="headerlink" title="表空间"></a>表空间</h3><p>表空间可以看做是InnoDB存储引擎逻辑结构的最高层，所有的数据都存放在表空间中。在默认情况下InnoDB存储引擎有一个共享表空间ibdata1，即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table，则每张表内的数据可以单独放到一个表空间内。</p><h3 id="段"><a href="#段" class="headerlink" title="段"></a>段</h3><p>表空间是由各个段组成的，常见的段有数据段、索引段、回滚段等。</p><h3 id="区"><a href="#区" class="headerlink" title="区"></a>区</h3><p>区是由连续页组成的空间，在任何情况下每个区的大小都为1MB。为了保证区中页的连续性，InnoDB存储引擎一次从磁盘申请4～5个区。在默认情况下，InnoDB存储引擎页的大小为16KB，即一个区中一共有64个连续的页。</p><h3 id="页"><a href="#页" class="headerlink" title="页"></a>页</h3><p>同大多数数据库一样，InnoDB有页（Page）的概念（也可以称为块），页是InnoDB磁盘管理的最小单位。在InnoDB存储引擎中，默认每个页的大小为16KB。</p><h3 id="行"><a href="#行" class="headerlink" title="行"></a>行</h3><p>InnoDB存储引擎是面向列的（row-oriented），也就说数据是按行进行存放的。每个页存放的行记录也是有硬性定义的，最多允许存放16KB/2-200行的记录，即7992行记录。</p><h2 id="InnoDB行记录格式"><a href="#InnoDB行记录格式" class="headerlink" title="InnoDB行记录格式"></a>InnoDB行记录格式</h2><h3 id="Compact行记录格式"><a href="#Compact行记录格式" class="headerlink" title="Compact行记录格式"></a>Compact行记录格式</h3><p>Compact行记录是在MySQL 5.0中引入的，其设计目标是高效地存储数据。简单来说，一个页中存放的行数据越多，其性能就越高。</p><p><img src="/images/ad35dea952131a0c.jpg" alt="img" style="zoom:50%;"></p>"<p>不管是CHAR类型还是VARCHAR类型，在compact格式下NULL值都不占用任何存储空间。</p><h3 id="Redundant行记录格式"><a href="#Redundant行记录格式" class="headerlink" title="Redundant行记录格式"></a>Redundant行记录格式</h3><p>Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式，MySQL 5.0支持Redundant是为了兼容之前版本的页格式。</p><p><img src="/images/049337b471a53f4c.jpg" alt="img" style="zoom:50%;"></p>"<p>对于VARCHAR类型的NULL值，Redundant行记录格式同样不占用任何存储空间，而CHAR类型的NULL值需要占用空间。</p><h3 id="行溢出数据"><a href="#行溢出数据" class="headerlink" title="行溢出数据"></a>行溢出数据</h3><p>InnoDB存储引擎并不支持65535长度的VARCHAR。这是因为还有别的开销，通过实际测试发现能存放VARCHAR类型的最大长度为65532</p><p>有没有想过，InnoDB存储引擎的页为16KB，即16384字节，怎么能存放65532字节呢？因此，在一般情况下，InnoDB存储引擎的数据都是存放在页类型为B-tree node中。但是当发生行溢出时，数据存放在页类型为Uncompress BLOB页中。</p><p><img src="/images/acc3b6421d0cdb2d.jpg" alt="img" style="zoom:50%;"></p>"<h1 id="锁"><a href="#锁" class="headerlink" title="锁"></a>锁</h1><h2 id="InnoDB存储引擎中的锁"><a href="#InnoDB存储引擎中的锁" class="headerlink" title="InnoDB存储引擎中的锁"></a>InnoDB存储引擎中的锁</h2><h3 id="锁的类型"><a href="#锁的类型" class="headerlink" title="锁的类型"></a>锁的类型</h3><ul><li>共享锁（S Lock），允许事务读一行数据。</li><li>排他锁（X Lock），允许事务删除或更新一行数据。</li></ul><p>InnoDB存储引擎支持多粒度（granular）锁定，这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作，InnoDB存储引擎支持一种额外的锁方式，称之为意向锁（Intention Lock）。意向锁是将锁定的对象分为多个层次，意向锁意味着事务希望在更细粒度（fine granularity）上进行加锁</p><p><img src="/images/2e3ffaa5d46e1df5.jpg" alt="img" style="zoom:33%;"></p>"<p>如果需要对页上的记录r进行上X锁，那么分别需要对数据库A、表、页上意向锁IX，最后对记录r上X锁。若其中任何一个部分导致等待，那么该操作需要等待粗粒度锁的完成。</p><p>InnoDB存储引擎支持意向锁设计比较简练，其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁：</p><p>1）意向共享锁（IS Lock），事务想要获得一张表中某几行的共享锁<br>2）意向排他锁（IX Lock），事务想要获得一张表中某几行的排他锁</p><p><img src="/images/c5502aee21510385.jpg" alt="img" style="zoom:50%;"></p>"<p>在InnoDB 1.0版本之前，用户只能通过命令SHOW FULL PROCESSLIST，SHOW ENGINE INNODB STATUS等来查看当前数据库中锁的请求，然后再判断事务锁的情况。从InnoDB1.0开始，在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。通过这三张表，用户可以更简单地监控当前事务并分析可能存在的锁问题。</p><h3 id="一致性非锁定读"><a href="#一致性非锁定读" class="headerlink" title="一致性非锁定读"></a>一致性非锁定读</h3><p>一致性的非锁定读（consistent nonlocking read）是指InnoDB存储引擎通过行多版本控制（multi versioning）的方式来读取当前执行时间数据库中行的数据。</p><p><img src="/images/3d7990fddfb85716.jpg" alt="img" style="zoom:50%;"></p>"<p>快照数据是指该行的之前版本的数据，该实现是通过undo段来完成。而undo用来在事务中回滚数据，因此快照数据本身是没有额外的开销。此外，读取快照数据是不需要上锁的，因为没有事务需要对历史的数据进行修改操作。</p><p>在rc,rr隔离级别下，快照读是不一样的：<br>在READ COMMITTED事务隔离级别下，对于快照数据，非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下，对于快照数据，非一致性读总是读取事务开始时的行数据版本。</p><h3 id="一致性锁定读"><a href="#一致性锁定读" class="headerlink" title="一致性锁定读"></a>一致性锁定读</h3><p>InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读（locking read）操作：</p><ul><li>SELECT…FOR UPDATE</li><li>SELECT…LOCK IN SHARE MODE</li></ul><h3 id="自增长与锁"><a href="#自增长与锁" class="headerlink" title="自增长与锁"></a>自增长与锁</h3><p>插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制，为了提高插入的性能，锁不是在一个事务完成后才释放，而是在完成对自增长值插入的SQL语句后立即释放。</p><h2 id="锁的算法"><a href="#锁的算法" class="headerlink" title="锁的算法"></a>锁的算法</h2><p>InnoDB存储引擎有3种行锁的算法，其分别是：</p><ul><li>Record Lock：单个行记录上的锁</li><li>Gap Lock：间隙锁，锁定一个范围，但不包含记录本身</li><li>Next-Key Lock∶Gap Lock+Record Lock，锁定一个范围，并且锁定记录本身</li></ul><p>Record Lock总是会去锁住索引记录，如果InnoDB存储引擎表在建立的时候没有设置任何一个索引，那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。</p><p>Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法，在Next-Key Lock算法下，InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10，11，13和20这四个值，那么该索引可能被Next-Key Locking的区间为：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">(-∞,10]</span><br><span class="line">(10,11]</span><br><span class="line">(11,13]</span><br><span class="line">(13，20]</span><br><span class="line">(20,+∞)</span><br></pre></td></tr></table></figure></p><p>采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计的目的是为了解决Phantom Problem</p><p>当查询的索引含有唯一属性时，InnoDB存储引擎会对Next-Key Lock进行优化，将其降级为Record Lock，即仅锁住索引本身，而不是范围。</p><h3 id="解决Phantom-Problem"><a href="#解决Phantom-Problem" class="headerlink" title="解决Phantom Problem"></a>解决Phantom Problem</h3><p>REPEATABLE READ下，InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem（幻像问题）。这点可能不同于与其他的数据库，如Oracle数据库，因为其可能需要在SERIALIZABLE的事务隔离级别下才能解决Phantom Problem。<br>Phantom Problem是指在同一事务下，连续执行两次同样的SQL语句可能导致不同的结果，第二次的SQL语句可能会返回之前不存在的行。</p><h2 id="锁问题"><a href="#锁问题" class="headerlink" title="锁问题"></a>锁问题</h2><h3 id="脏读"><a href="#脏读" class="headerlink" title="脏读"></a>脏读</h3><p>脏读指的就是在不同的事务下，当前事务可以读到另外事务未提交的数据，简单来说就是可以读到脏数据。</p><h3 id="不可重复读"><a href="#不可重复读" class="headerlink" title="不可重复读"></a>不可重复读</h3><p>脏读是读到未提交的数据，而不可重复读读到的却是已经提交的数据，但是其违反了数据库事务一致性的要求。</p><h3 id="丢失更新"><a href="#丢失更新" class="headerlink" title="丢失更新"></a>丢失更新</h3><h2 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h2><p>解决死锁问题最简单的一种方法是超时，即当两个事务互相等待时，当一个等待时间超过设置的某一阈值时，其中一个事务进行回滚，另一个等待的事务就能继续进行</p><p>当前数据库还都普遍采用wait-for graph（等待图）的方式来进行死锁检测。较之超时的解决方案，这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息：</p><ul><li>锁的信息链表</li><li>事务等待链表</li></ul><p>通过图可以发现存在回路（t1，t2），因此存在死锁。通过上述的介绍，可以发现wait-for graph是一种较为主动的死锁检测机制，在每个事务请求锁并发生等待时都会判断是否存在回路，若存在则有死锁，通常来说InnoDB存储引擎选择回滚undo量最小的事务。</p><p><img src="/images/5b4ba07c7c314a94.jpg" alt="img" style="zoom:50%;"></p>"<p>死锁举例：</p><p><img src="/images/86610970738b84c0.jpg" alt="img" style="zoom:50%;"></p>"<h1 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h1><h2 id="事务的实现"><a href="#事务的实现" class="headerlink" title="事务的实现"></a>事务的实现</h2><p>原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志，用来保证事务的原子性和持久性。undo log用来保证事务的一致性。<br>redo和undo的作用都可以视为是一种恢复操作，redo恢复提交事务修改的页操作，而undo回滚行记录到某个特定版本。因此两者记录的内容不同，redo通常是物理日志，记录的是页的物理修改操作。undo是逻辑日志，根据每行记录进行记录。</p><h3 id="redo"><a href="#redo" class="headerlink" title="redo"></a>redo</h3><p>重做日志用来实现事务的持久性，即事务ACID中的D。其由两部分组成：一是内存中的重做日志缓冲（redo log buffer），其是易失的；二是重做日志文件（redo log file），其是持久的。</p><p>InnoDB是事务的存储引擎，其通过Force Log at Commit机制实现事务的持久性，即当事务提交（COMMIT）时，必须先将该事务的所有日志写入到重做日志文件进行持久化，待事务的COMMIT操作完成才算完成。这里的日志是指重做日志，在InnoDB存储引擎中，由两部分组成，即redo log和undo log。</p><p>参数innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数的默认值为1，表示事务提交时必须调用一次fsync操作。还可以设置该参数的值为0和2。0表示事务提交时不进行写入重做日志操作，这个操作仅在master thread中完成，而在master thread中每1秒会进行一次重做日志文件的fsync操作。2表示事务提交时将重做日志写入重做日志文件，但仅写入文件系统的缓存中，不进行fsync操作。</p><h4 id="redo-log-与binlog的区别"><a href="#redo-log-与binlog的区别" class="headerlink" title="redo log 与binlog的区别"></a>redo log 与binlog的区别</h4><p>首先，重做日志是在InnoDB存储引擎层产生，而二进制日志是在MySQL数据库的上层产生的，并且二进制日志不仅仅针对于InnoDB存储引擎，MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。<br>其次，两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志，其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志，其记录的是对于每个页的修改。<br>此外，两种日志记录写入磁盘的时间点不同。二进制日志只在事务提交完成后进行一次写入。而InnoDB存储引擎的重做日志在事务进行中不断地被写入</p><h4 id="log-block"><a href="#log-block" class="headerlink" title="log block"></a>log block</h4><p>在InnoDB存储引擎中，重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块（block）的方式进行保存的，称之为重做日志块（redo log block），每块的大小为512字节。</p><p>若一个页中产生的重做日志数量大于512字节，那么需要分割为多个重做日志块进行存储。此外，由于重做日志块的大小和磁盘扇区大小一样，都是512字节，因此重做日志的写入可以保证原子性，不需要doublewrite技术。</p><h4 id="LSN"><a href="#LSN" class="headerlink" title="LSN"></a>LSN</h4><p>LSN是Log Sequence Number的缩写，其代表的是日志序列号。在InnoDB存储引擎中，LSN占用8字节，并且单调递增。LSN表示的含义有：</p><ul><li>重做日志写入的总量</li><li>checkpoint的位置</li><li>页的版本</li></ul><p>LSN不仅记录在重做日志中，还存在于每个页中。在每个页的头部，有一个值FIL_PAGE_LSN，记录了该页的LSN。在页中，LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志，因此页中的LSN用来判断页是否需要进行恢复操作。</p><h4 id="恢复"><a href="#恢复" class="headerlink" title="恢复"></a>恢复</h4><p>InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭，都会尝试进行恢复操作。因为重做日志记录的是物理日志，因此恢复的速度比逻辑日志，如二进制日志，要快很多。</p><p>由于checkpoint表示已经刷新到磁盘页上的LSN，因此在恢复过程中仅需恢复checkpoint开始的日志部分。对于图中的例子，当数据库在checkpoint的LSN为10 000时发生宕机，恢复操作仅恢复LSN 10 000～13 000范围内的日志。</p><p><img src="/images/4d02913d057812a5.jpg" alt="img" style="zoom:50%;"></p>"<h3 id="undo"><a href="#undo" class="headerlink" title="undo"></a>undo</h3><p>重做日志记录了事务的行为，可以很好地通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作，这时就需要undo。因此在对数据库进行修改时，InnoDB存储引擎不但会产生redo，还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了，又或者用户用一条ROLLBACK语句请求回滚，就可以利用这些undo信息将数据回滚到修改之前的样子。</p><p>redo存放在重做日志文件中，与redo不同，undo存放在数据库内部的一个特殊段（segment）中，这个段称为undo段（undo segment）。undo段位于共享表空间内。</p><p>除了回滚操作，undo的另一个作用是MVCC，即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时，若该记录已经被其他事务占用，当前事务可以通过undo读取之前的行版本信息，以此实现非锁定读取。<br>最后也是最为重要的一点是，undo log会产生redo log，也就是undo log的产生会伴随着redo log的产生，这是因为undo log也需要持久性的保护。</p><h4 id="undo存储管理"><a href="#undo存储管理" class="headerlink" title="undo存储管理"></a>undo存储管理</h4><p>当事务提交时，InnoDB存储引擎会做以下两件事情：</p><ul><li><p>将undo log放入列表中，以供之后的purge操作</p><p>事务提交后并不能马上删除undo log及undo log所在的页。这是因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务提交时将undo log放入一个链表中，是否可以最终删除undo log及undo log所在页由purge线程来判断。</p></li><li><p>判断undo log所在的页是否可以重用，若可以分配给下个事务使用</p><p>在InnoDB存储引擎的设计中对undo页可以进行重用。具体来说，当事务提交时，首先将undo log放入链表中，然后判断undo页的使用空间是否小于3/4，若是则表示该undo页可以被重用</p></li></ul><h3 id="purge"><a href="#purge" class="headerlink" title="purge"></a>purge</h3><p>delete和update操作可能并不直接删除原有的数据，仅是将主键列等于1的记录delete flag设置为1，记录并没有被删除，即记录还是存在于B+树中。<br>purge用于最终完成delete和update操作。这样设计是因为InnoDB存储引擎支持MVCC，所以记录不能在事务提交时立即进行处理。<br>是否可以删除该条记录通过purge来进行判断。若该行记录已不被任何其他事务引用，那么就可以进行真正的delete操作。</p><h3 id="group-commit"><a href="#group-commit" class="headerlink" title="group commit"></a>group commit</h3><h4 id="2pc"><a href="#2pc" class="headerlink" title="2pc"></a>2pc</h4><p>MySQL 使用两阶段提交主要解决 binlog 和 redo log 的数据一致性的问题。<br>redo log 和 binlog 都可以用于表示事务的提交状态，而两阶段提交就是让这两个状态保持逻辑上的一致。下图为 MySQL 二阶段提交简图：</p><p><img src="/images/image-20210516105352324.png" alt="image-20210516105352324" style="zoom: 25%;"></p>"<p>两阶段提交原理描述:</p><ol><li>InnoDB redo log 写盘，InnoDB 事务进入 prepare 状态。</li><li>如果前面 prepare 成功，binlog 写盘，那么再继续将事务日志持久化到 binlog，如果持久化成功，那么 InnoDB 事务则进入 commit 状态(在 redo log 里面写一个 commit 记录;binlog fsync成功确保了事务的提交)</li></ol><p>备注: 每个事务 binlog 的末尾，会记录一个 XID event，标志着事务是否提交成功，也就是说，recovery 过程中，binlog 最后一个 XID event 之后的内容都应该被 purge。</p><h4 id="group-commit-1"><a href="#group-commit-1" class="headerlink" title="group commit"></a>group commit</h4><p>在MySQL数据库上层进行提交时首先按顺序将其放入一个队列中，队列中的第一个事务称为leader，其他事务称为follower，leader控制着follower的行为。BLGC的步骤分为以下三个阶段：</p><ul><li><p>Flush阶段，flush redo log and 将每个事务的二进制日志写入内存中。</p><p>参数binlog_max_flush_queue_time用来控制Flush阶段中等待的时间，即使之前的一组事务完成提交，当前一组的事务也不马上进入Sync阶段，而是至少需要等待一段时间。这样做的好处是group commit的事务数量更多，然而这也可能会导致事务的响应时间变慢。该参数的默认值为0，且推荐设置依然为0。</p></li><li><p>Sync阶段，将内存中的二进制日志刷新到磁盘，若队列中有多个事务，那么仅一次fsync操作就完成了二进制日志的写入，这就是BLGC。</p></li><li><p>Commit阶段，leader根据顺序调用存储引擎层事务的提交，InnoDB存储引擎本就支持group commit，因此修复了原先由于锁prepare_commit_mutex导致group commit失效的问题。</p></li></ul><p>当有一组事务在进行Commit阶段时，其他新事物可以进行Flush阶段，从而使group commit不断生效。当然group commit的效果由队列中事务的数量决定，若每次队列中仅有一个事务，那么可能效果和之前差不多，甚至会更差。但当提交的事务越多时，group commit的效果越明显，数据库性能的提升也就越大。</p><p><img src="/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYW5neWFubWFubWFu,size_16,color_FFFFFF,t_70.jpeg" alt="img" style="zoom:50%;"></p>"<p>详见：<a href="https://blog.csdn.net/cymm_liu/article/details/106030636" target="_blank" rel="noopener">MySQL组提交(group commit)</a></p><h1 id="备份与恢复"><a href="#备份与恢复" class="headerlink" title="备份与恢复"></a>备份与恢复</h1><p>可以根据不同的类型来划分备份的方法。根据备份的方法不同可以将备份分为：</p><ul><li>Hot Backup（热备）</li><li>Cold Backup（冷备）</li><li>Warm Backup（温备）</li></ul><p>Hot Backup是指数据库运行中直接备份，对正在运行的数据库操作没有任何的影响。这种方式在MySQL官方手册中称为Online Backup（在线备份）。Cold Backup是指备份操作是在数据库停止的情况下，这种备份最为简单，一般只需要复制相关的数据库物理文件即可。这种方式在MySQL官方手册中称为Offline Backup（离线备份）。Warm Backup备份同样是在数据库运行中进行的，但是会对当前数据库的操作有所影响，如加一个全局读锁以保证备份数据的一致性。</p><p>按照备份后文件的内容，备份又可以分为：</p><ul><li>逻辑备份</li><li>裸文件备份</li></ul><p>在MySQL数据库中，逻辑备份是指备份出的文件内容是可读的，一般是文本文件。内容一般是由一条条SQL语句，或者是表内实际数据组成。如mysqldump和SELECT*INTO OUTFILE的方法。这类方法的好处是可以观察导出文件的内容，一般适用于数据库的升级、迁移等工作。但其缺点是恢复所需要的时间往往较长。</p><p>裸文件备份是指复制数据库的物理文件，既可以是在数据库运行中的复制（如ibbackup、xtrabackup这类工具），也可以是在数据库停止运行时直接的数据文件复制。这类备份的恢复时间往往较逻辑备份短很多。</p><p>若按照备份数据库的内容来分，备份又可以分为：</p><ul><li>完全备份</li><li>增量备份</li><li>日志备份</li></ul><p>完全备份是指对数据库进行一个完整的备份。增量备份是指在上次完全备份的基础上，对于更改的数据进行备份。日志备份主要是指对MySQL数据库二进制日志的备份，通过对一个完全备份进行二进制日志的重做（replay）来完成数据库的point-in-time的恢复工作。MySQL数据库复制（replication）的原理就是异步实时地将二进制日志重做传送并应用到从（slave/standby）数据库。</p><h3 id="复制"><a href="#复制" class="headerlink" title="复制"></a>复制</h3><p>复制（replication）是MySQL数据库提供的一种高可用高性能的解决方案，一般用来建立大型的应用。总体来说，replication的工作原理分为以下3个步骤：</p><p>1）主服务器（master）把数据更改记录到二进制日志（binlog）中。<br>2）从服务器（slave）把主服务器的二进制日志复制到自己的中继日志（relay log）中。<br>3）从服务器重做中继日志中的日志，把更改应用到自己的数据库上，以达到数据的最终一致性。</p><p>复制的工作原理并不复杂，其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是，复制不是完全实时地进行同步，而是异步实时。这中间存在主从服务器之间的执行延时，如果主服务器的压力很大，则可能导致主从服务器延时较大。</p><p><img src="/images/b1d2985a65dfeb1d.jpg" alt="img" style="zoom:33%;"></p>"<h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><h2 id="SQL语句的执行过程"><a href="#SQL语句的执行过程" class="headerlink" title="SQL语句的执行过程"></a>SQL语句的执行过程</h2><p><img src="/images/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA55Sw5Z-C44CB,size_20,color_FFFFFF,t_70,g_se,x_16.png" style="zoom: 50%;"></p>"<p><img src="/images/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA55Sw5Z-C44CB,size_20,color_FFFFFF,t_70,g_se,x_16-20220427174749957.png" alt="在这里插入图片描述" style="zoom:50%;"></p>"<h2 id="基于2PC的一致性保障"><a href="#基于2PC的一致性保障" class="headerlink" title="基于2PC的一致性保障"></a>基于2PC的一致性保障</h2><p><img src="/images/v2-44027cebeca996419644b7c673652260_1440w.jpg" alt="img" style="zoom: 67%;"></p>"<p><a href="https://blog.csdn.net/weixin_51626435/article/details/123411484" target="_blank" rel="noopener">MySQL中：一条update语句是怎样执行的</a><br><a href="https://blog.csdn.net/mashaokang1314/article/details/113881996#fromHistory" target="_blank" rel="noopener">Mysql工作原理——redo日志文件和恢复操作</a><br><a href="https://zhuanlan.zhihu.com/p/346970015" target="_blank" rel="noopener">基于Redo Log和Undo Log的MySQL崩溃恢复流程</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;MySQL体系结构和存储引擎&quot;&gt;&lt;a href=&quot;#MySQL体系结构和存储引擎&quot; class=&quot;headerlink&quot; title=&quot;MySQL体系结构和存储引擎&quot;&gt;&lt;/a&gt;MySQL体系结构和存储引擎&lt;/h1&gt;&lt;p&gt;数据库：物理操作系统文件或其他形式文件类型的
      
    
    </summary>
    
      <category term="复习" scheme="http://yoursite.com/categories/%E5%A4%8D%E4%B9%A0/"/>
    
    
      <category term="mysql" scheme="http://yoursite.com/tags/mysql/"/>
    
  </entry>
  
  <entry>
    <title>kafka核心设计与实践原理</title>
    <link href="http://yoursite.com/2022/04/09/%E5%A4%8D%E4%B9%A0%E6%80%BB%E7%BB%93/kafka%E6%A0%B8%E5%BF%83%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E8%B7%B5%E5%8E%9F%E7%90%86/"/>
    <id>http://yoursite.com/2022/04/09/复习总结/kafka核心设计与实践原理/</id>
    <published>2022-04-08T16:00:00.000Z</published>
    <updated>2022-05-03T06:35:05.699Z</updated>
    
    <content type="html"><![CDATA[<h1 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h1><p><img src="/images/abd2e7ebfe90a4b0.jpg" alt="img" style="zoom: 87%;"></p>"<p>主题是一个逻辑上的概念，它还可以细分为多个分区，一个分区只属于单个主题，很多时候也会把分区称为主题分区（Topic-Partition）。同一主题下的不同分区包含的消息是不同的，分区在存储层面可以看作一个可追加的日志（Log）文件，消息在被追加到分区日志文件的时候都会分配一个特定的偏移量（offset）。offset是消息在分区中的唯一标识，Kafka通过它来保证消息在分区内的顺序性，不过offset并不跨越分区，也就是说，Kafka保证的是分区有序而不是主题有序。</p><p>Kafka 为分区引入了多副本（Replica）机制，通过增加副本数量可以提升容灾能力</p><p>Kafka 消费端也具备一定的容灾能力。Consumer 使用拉（Pull）模式从服务端拉取消息，并且保存消费的具体位置，当消费者宕机后恢复上线时可以根据之前保存的消费位置重新拉取需要的消息进行消费，这样就不会造成消息丢失。</p><p>分区中的所有副本统称为AR（Assigned Replicas）。所有与leader副本保持一定程度同步的副本（包括leader副本在内）组成ISR（In-Sync Replicas），ISR集合是AR集合中的一个子集。消息会先发送到leader副本，然后follower副本才能从leader副本中拉取消息进行同步，同步期间内follower副本相对于leader副本而言会有一定程度的滞后。前面所说的“一定程度的同步”是指可忍受的滞后范围，这个范围可以通过参数进行配置。与leader副本同步滞后过多的副本（不包括leader副本）组成OSR（Out-of-Sync Replicas），由此可见，AR=ISR+OSR。在正常情况下，所有的 follower 副本都应该与 leader 副本保持一定程度的同步，即 AR=ISR，OSR集合为空。</p><p>leader副本负责维护和跟踪ISR集合中所有follower副本的滞后状态，当follower副本落后太多或失效时，leader副本会把它从ISR集合中剔除。如果OSR集合中有follower副本“追上”了leader副本，那么leader副本会把它从OSR集合转移至ISR集合。默认情况下，当leader副本发生故障时，只有在ISR集合中的副本才有资格被选举为新的leader，而在OSR集合中的副本则没有任何机会（不过这个原则也可以通过修改相应的参数配置来改变）。</p><p>ISR与HW和LEO也有紧密的关系。HW是High Watermark的缩写，俗称高水位，它标识了一个特定的消息偏移量（offset），消费者只能拉取到这个offset之前的消息。</p><p>LEO是Log End Offset的缩写，它标识当前日志文件中下一条待写入消息的offset，图中offset为9的位置即为当前日志文件的LEO，LEO的大小相当于当前日志分区中最后一条消息的offset值加1。分区ISR集合中的每个副本都会维护自身的LEO，而ISR集合中最小的LEO即为分区的HW，对消费者而言只能消费HW之前的消息。</p><p><img src="/images/df26cf44890fd343.jpg" alt="img" style="zoom:67%;"></p>"<h1 id="生产者"><a href="#生产者" class="headerlink" title="生产者"></a>生产者</h1><h2 id="客户端开发"><a href="#客户端开发" class="headerlink" title="客户端开发"></a>客户端开发</h2><p>一个正常的生产逻辑需要具备以下几个步骤：</p><p>（1）配置生产者客户端参数及创建相应的生产者实例。<br>（2）构建待发送的消息。<br>（3）发送消息。<br>（4）关闭生产者实例。</p><h2 id="原理分析"><a href="#原理分析" class="headerlink" title="原理分析"></a>原理分析</h2><h3 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</h3><p><img src="/images/image-20220428115634836.png" alt="image-20220428115634836" style="zoom:50%;"></p>"<p>整个生产者客户端由两个线程协调运行，这两个线程分别为主线程和Sender线程（发送线程）。在主线程中由KafkaProducer创建消息，然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器（RecordAccumulator，也称为消息收集器）中。Sender 线程负责从RecordAccumulator中获取消息并将其发送到Kafka中。</p><p>RecordAccumulator 主要用来缓存消息以便 Sender 线程可以批量发送，进而减少网络传输的资源消耗以提升性能。RecordAccumulator 缓存的大小可以通过生产者客户端参数buffer.memory 配置，默认值为 33554432B，即 32MB。如果生产者发送消息的速度超过发送到服务器的速度，则会导致生产者空间不足，这个时候KafkaProducer的send（）方法调用要么被阻塞，要么抛出异常，这个取决于参数max.block.ms的配置，此参数的默认值为60000，即60秒。</p><p>注意ProducerBatch不是ProducerRecord，ProducerBatch中可以包含一至多个 ProducerRecord。通俗地说，ProducerRecord 是生产者中创建的消息，而ProducerBatch是指一个消息批次，ProducerRecord会被包含在ProducerBatch中，这样可以使字节的使用更加紧凑。与此同时，将较小的ProducerRecord拼凑成一个较大的ProducerBatch，也可以减少网络请求的次数以提升整体的吞吐量。</p><p>消息在网络上都是以字节（Byte）的形式传输的，在发送之前需要创建一块内存区域来保存对应的消息。在Kafka生产者客户端中，通过java.io.ByteBuffer实现消息内存的创建和释放。不过频繁的创建和释放是比较耗费资源的，在RecordAccumulator的内部还有一个BufferPool，它主要用来实现ByteBuffer的复用，以实现缓存的高效利用。</p><p>Sender 从 RecordAccumulator 中获取缓存的消息之后，会进一步将原本＜分区，Deque＜ProducerBatch＞＞的保存形式转变成＜Node，List＜ ProducerBatch＞的形式，其中Node表示Kafka集群的broker节点。<br>在转换成＜Node，List＜ProducerBatch＞＞的形式之后，Sender 还会进一步封装成＜Node，Request＞的形式，这样就可以将Request请求发往各个Node了，这里的Request是指Kafka的各种协议请求。</p><p>请求在从Sender线程发往Kafka之前还会保存到InFlightRequests中，InFlightRequests保存对象的具体形式为 Map＜NodeId，Deque＜Request＞＞，它的主要作用是缓存了已经发出去但还没有收到响应的请求（NodeId 是一个 String 类型，表示节点的 id 编号）。与此同时，InFlightRequests还提供了许多管理类的方法，并且通过配置参数还可以限制每个连接（也就是客户端与Node之间的连接）最多缓存的请求数。这个配置参数为max.in.flight.requests.per.connection，默认值为 5，即每个连接最多只能缓存 5 个未响应的请求，超过该数值之后就不能再向这个连接发送更多的请求了，除非有缓存的请求收到了响应（Response）。</p><h3 id="元数据更新"><a href="#元数据更新" class="headerlink" title="元数据更新"></a>元数据更新</h3><p>InFlightRequests还可以获得leastLoadedNode，即所有Node中负载最小的那一个。这里的负载最小是通过每个Node在InFlightRequests中还未确认的请求决定的，未确认的请求越多则认为负载越大。如图：node1的负载最小。选择leastLoadedNode发送请求可以使它能够尽快发出，避免因网络拥塞等异常而影响整体的进度。leastLoadedNode的概念可以用于多个应用场合，比如元数据请求、消费者组播协议的交互。</p><p><img src="/images/image-20220428135038871.png" alt="image-20220428135038871" style="zoom:33%;"></p>"<p>bootstrap.servers参数只需要配置部分broker节点的地址即可，不需要配置所有broker节点的地址，因为客户端可以自己发现其他broker节点的地址，这一过程也属于元数据相关的更新操作。与此同时，分区数量及leader副本的分布都会动态地变化，客户端也需要动态地捕捉这些变化。</p><p>当客户端中没有需要使用的元数据信息时，比如没有指定的主题信息，或者超过metadata.max.age.ms 时间没有更新元数据都会引起元数据的更新操作。客户端参数metadata.max.age.ms的默认值为300000，即5分钟。当需要更新元数据时，会先挑选出leastLoadedNode，然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。这个更新操作是由Sender线程发起的，在创建完MetadataRequest之后同样会存入InFlightRequests，之后的步骤就和发送消息时的类似。元数据虽然由Sender线程负责更新，但是主线程也需要读取这些信息，这里的数据同步通过synchronized和final关键字来保障。</p><h2 id="重要的生产者参数"><a href="#重要的生产者参数" class="headerlink" title="重要的生产者参数"></a>重要的生产者参数</h2><h3 id="acks"><a href="#acks" class="headerlink" title="acks"></a>acks</h3><p>这个参数用来指定分区中必须要有多少个副本收到这条消息，之后生产者才会认为这条消息是成功写入的。acks 是生产者客户端中一个非常重要的参数，它涉及消息的可靠性和吞吐量之间的权衡。acks参数有3种类型的值（都是字符串类型）。</p><p>· acks=1。默认值即为1。生产者发送消息之后，只要分区的leader副本成功写入消息，那么它就会收到来自服务端的成功响应。如果消息无法写入leader副本，比如在leader 副本崩溃、重新选举新的 leader 副本的过程中，那么生产者就会收到一个错误的响应，为了避免消息丢失，生产者可以选择重发消息。如果消息写入leader副本并返回成功响应给生产者，且在被其他follower副本拉取之前leader副本崩溃，那么此时消息还是会丢失，因为新选举的leader副本中并没有这条对应的消息。acks设置为1，是消息可靠性和吞吐量之间的折中方案。</p><p>· acks=0。生产者发送消息之后不需要等待任何服务端的响应。如果在消息从发送到写入Kafka的过程中出现某些异常，导致Kafka并没有收到这条消息，那么生产者也无从得知，消息也就丢失了。在其他配置环境相同的情况下，acks 设置为 0 可以达到最大的吞吐量。</p><p>· acks=-1或acks=all。生产者在消息发送之后，需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下，acks 设置为-1（all）可以达到最强的可靠性。但这并不意味着消息就一定可靠，因为ISR中可能只有leader副本，这样就退化成了acks=1的情况。要获得更高的消息可靠性需要配合 min.insync.replicas 等参数的联动。</p><h1 id="消费者"><a href="#消费者" class="headerlink" title="消费者"></a>消费者</h1><h2 id="消费者与消费组"><a href="#消费者与消费组" class="headerlink" title="消费者与消费组"></a>消费者与消费组</h2><p>消费者（Consumer）负责订阅Kafka中的主题（Topic），并且从订阅的主题上拉取消息。与其他一些消息中间件不同的是：在Kafka的消费理念中还有一层消费组（Consumer Group）的概念，每个消费者都有一个对应的消费组。当消息发布到主题后，只会被投递给订阅它的每个消费组中的一个消费者。</p><p><img src="/images/ddd125132e569738.jpg" alt="img" style="zoom: 50%;"></p>"<h2 id="客户端开发-1"><a href="#客户端开发-1" class="headerlink" title="客户端开发"></a>客户端开发</h2><p>一个正常的消费逻辑需要具备以下几个步骤：</p><p>（1）配置消费者客户端参数及创建相应的消费者实例。<br>（2）订阅主题。<br>（3）拉取消息并消费。<br>（4）提交消费位移。<br>（5）关闭消费者实例。</p><h3 id="消费消息"><a href="#消费消息" class="headerlink" title="消费消息"></a>消费消息</h3><ul><li>Kafka中的消费是基于拉模式的</li><li>Kafka中的消息消费是一个不断轮询的过程，消费者所要做的就是重复地调用poll（）方法，而poll（）方法返回的是所订阅的主题（分区）上的一组消息。返回值类型是 ConsumerRecords，它用来表示一次拉取操作所获得的消息集。</li><li>poll还涉及消费位移、消费者协调器、组协调器、消费者的选举、分区分配的分发、再均衡的逻辑、心跳等内容。</li></ul><h3 id="位移提交"><a href="#位移提交" class="headerlink" title="位移提交"></a>位移提交</h3><p>对于Kafka中的分区而言，它的每条消息都有唯一的offset，用来表示消息在分区中对应的位置。对于消费者而言，它也有一个offset的概念，消费者使用offset来表示消费到分区中某个消息所在的位置。</p><p>在每次调用poll（）方法时，它返回的是还没有被消费过的消息集，要做到这一点，就需要记录上一次消费时的消费位移。并且这个消费位移必须做持久化保存，而不是单单保存在内存中，否则消费者重启之后就无法知晓之前的消费位移。再考虑一种情况，当有新的消费者加入时，那么必然会有再均衡的动作，对于同一分区而言，它可能在再均衡动作之后分配给新的消费者，如果不持久化保存消费位移，那么这个新的消费者也无法知晓之前的消费位移。</p><p>在旧消费者客户端中，消费位移是存储在ZooKeeper中的。而在新消费者客户端中，消费位移存储在Kafka内部的主题__consumer_offsets中。这里把将消费位移存储起来（持久化）的动作称为“提交”，消费者在消费完消息之后需要执行消费位移的提交。</p><p>在 Kafka 中默认的消费位移的提交方式是自动提交，这个由消费者客户端参数enable.auto.commit 配置，默认值为 true。当然这个默认的自动提交不是每消费一条消息就提交一次，而是定期提交，这个定期的周期时间由客户端参数auto.commit.interval.ms配置，默认值为5秒，此参数生效的前提是enable.auto.commit参数为true。<br>自动位移提交的方式在正常情况下不会发生消息丢失或重复消费的现象，但是在编程的世界里异常无可避免，与此同时，自动位移提交也无法做到精确的位移管理。在Kafka中还提供了手动位移提交的方式，这样可以使得开发人员对消费位移的管理控制更加灵活。手动提交可以细分为同步提交和异步提交.</p><p>如果位移提交失败的情况经常发生，那么说明系统肯定出现了故障，在一般情况下，位移提交失败的情况很少发生，不重试也没有关系，后面的提交也会有成功的。重试会增加代码逻辑的复杂度，不重试会增加重复消费的概率。如果消费者异常退出，那么这个重复消费的问题就很难避免，因为这种情况下无法及时提交消费位移；如果消费者正常退出或发生再均衡的情况，那么可以在退出或再均衡执行之前使用同步提交的方式做最后的把关。</p><h3 id="指定位移消费"><a href="#指定位移消费" class="headerlink" title="指定位移消费"></a>指定位移消费</h3><p>在 Kafka 中每当消费者查找不到所记录的消费位移时，就会根据消费者客户端参数auto.offset.reset的配置来决定从何处开始进行消费，这个参数的默认值为“latest”，表示从分区末尾开始消费消息。</p><p>有些时候，我们需要一种更细粒度的掌控，可以让我们从特定的位移处开始拉取消息，而 KafkaConsumer 中的 seek（）方法正好提供了这个功能，让我们得以追前消费或回溯消费。</p><h3 id="再均衡"><a href="#再均衡" class="headerlink" title="再均衡"></a>再均衡</h3><p>再均衡是指分区的所属权从一个消费者转移到另一消费者的行为，它为消费组具备高可用性和伸缩性提供保障，使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者。不过在再均衡发生期间，消费组内的消费者是无法读取消息的。也就是说，在再均衡发生期间的这一小段时间内，消费组会变得不可用。另外，当一个分区被重新分配给另一个消费者时，消费者当前的状态也会丢失。一般情况下，应尽量避免不必要的再均衡的发生。</p><h1 id="主题与分区"><a href="#主题与分区" class="headerlink" title="主题与分区"></a>主题与分区</h1><h2 id="分区的管理"><a href="#分区的管理" class="headerlink" title="分区的管理"></a>分区的管理</h2><h3 id="优先副本的选举"><a href="#优先副本的选举" class="headerlink" title="优先副本的选举"></a>优先副本的选举</h3><p>分区使用多副本机制来提升可靠性，但只有leader副本对外提供读写服务，而follower副本只负责在内部进行消息的同步。从某种程度上说，broker 节点中 leader 副本个数的多少决定了这个节点负载的高低。</p><p>为了能够有效地治理负载失衡的情况，Kafka引入了优先副本（preferred replica）的概念。所谓的优先副本是指在 AR 集合列表中的第一个副本。所谓的优先副本的选举是指通过一定的方式促使优先副本选举为leader副本，以此来促进集群的负载均衡，这一行为也可以称为“分区平衡”。</p><h1 id="日志存储"><a href="#日志存储" class="headerlink" title="日志存储"></a>日志存储</h1><h2 id="文件目录布局"><a href="#文件目录布局" class="headerlink" title="文件目录布局"></a>文件目录布局</h2><p>一个分区对应一个日志（Log）。为了防止 Log 过大，Kafka又引入了日志分段（LogSegment）的概念，将Log切分为多个LogSegment，相当于一个巨型文件被平均分配为多个相对较小的文件，这样也便于消息的维护和清理。事实上，Log 和LogSegment 也不是纯粹物理意义上的概念，Log 在物理上只以文件夹的形式存储，而每个LogSegment 对应于磁盘上的一个日志文件和两个索引文件，以及可能的其他文件</p><p><img src="/images/ceeff03d775c2ac2.jpg" alt="img" style="zoom: 67%;"></p>"<p>向Log 中追加消息时是顺序写入的，只有最后一个 LogSegment 才能执行写入操作，在此之前所有的 LogSegment 都不能写入数据。为了方便描述，我们将最后一个 LogSegment 称为“activeSegment”，即表示当前活跃的日志分段。随着消息的不断写入，当activeSegment满足一定的条件时，就需要创建新的activeSegment，之后追加的消息将写入新的activeSegment。</p><p>为了便于消息的检索，每个LogSegment中的日志文件（以“.log”为文件后缀）都有对应的两个索引文件：偏移量索引文件（以“.index”为文件后缀）和时间戳索引文件（以“.timeindex”为文件后缀）。每个 LogSegment 都有一个基准偏移量 baseOffset，用来表示当前 LogSegment中第一条消息的offset。偏移量是一个64位的长整型数，日志文件和两个索引文件都是根据基准偏移量（baseOffset）命名的，名称固定为20位数字，没有达到的位数则用0填充。比如第一个LogSegment的基准偏移量为0，对应的日志文件为00000000000000000000.log。</p><p>从更加宏观的视角上看，Kafka 中的文件不只上面提及的这些文件，比如还有一些检查点文件，当一个Kafka服务第一次启动的时候，默认的根目录下就会创建以下5个文件：</p><p><img src="/images/3c42bf07b1caca89.jpg" alt="img"></p>"<h2 id="日志格式的演变"><a href="#日志格式的演变" class="headerlink" title="日志格式的演变"></a>日志格式的演变</h2><p>每个分区由内部的每一条消息组成，如果消息格式设计得不够精炼，那么其功能和性能都会大打折扣。比如有冗余字段，势必会不必要地增加分区的占用空间，进而不仅使存储的开销变大、网络传输的开销变大，也会使Kafka的性能下降。反观如果缺少字段，比如在最初的Kafka消息版本中没有timestamp字段，对内部而言，其影响了日志保存、切分策略，对外部而言，其影响了消息审计、端到端延迟、大数据应用等功能的扩展。虽然可以在消息体内部添加一个时间戳，但解析变长的消息体会带来额外的开销，而存储在消息体（参中的value字段）前面可以通过指针偏移量获取其值而容易解析，进而减少了开销（可以查看v1版本），虽然相比于没有 timestamp 字段的开销会大一点。</p><h3 id="v0版本"><a href="#v0版本" class="headerlink" title="v0版本"></a>v0版本</h3><p><img src="/images/a4bcb9fd0df76e82.jpg" alt="img"></p>"<p>attributes（1B）：消息的属性。总共占1个字节，低3位表示压缩类型：0表示NONE、1表示GZIP、2表示SNAPPY、3表示LZ4（LZ4自Kafka 0.9.x引入），其余位保留。</p><p>v0版本中一个消息的最小长度（RECORD_OVERHEAD_V0）为crc32+magic+attributes+key length+value length=4B+1B+1B+4B+4B=14B。也就是说，v0版本中一条消息的最小长度为14B，如果小于这个值，那么这就是一条破损的消息而不被接收。</p><h3 id="v1版本"><a href="#v1版本" class="headerlink" title="v1版本"></a>v1版本</h3><p>Kafka从0.10.0版本开始到0.11.0版本之前所使用的消息格式版本为v1，比v0版本就多了一个timestamp字段，表示消息的时间戳。</p><p><img src="/images/03f0bbeab3f480d0.jpg" alt="img"></p>"<h3 id="消息压缩"><a href="#消息压缩" class="headerlink" title="消息压缩"></a>消息压缩</h3><p>常见的压缩算法是数据量越大压缩效果越好，一条消息通常不会太大，这就导致压缩效果并不是太好。而Kafka实现的压缩方式是将多条消息一起进行压缩，这样可以保证较好的压缩效果。在一般情况下，生产者发送的压缩数据在broker中也是保持压缩状态进行存储的，消费者从服务端获取的也是压缩的消息，消费者在处理消息之前才会解压消息，这样保持了端到端的压缩。</p><p><img src="/images/4890fd5621c66a51.jpg" alt="img" style="zoom:67%;"></p>"<h3 id="v2版本"><a href="#v2版本" class="headerlink" title="v2版本"></a>v2版本</h3><h2 id="日志索引"><a href="#日志索引" class="headerlink" title="日志索引"></a>日志索引</h2><p>Kafka 中的索引文件以稀疏索引（sparse index）的方式构造消息的索引，它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量（由 broker 端参数 log.index.interval.bytes指定，默认值为4096，即4KB）的消息时，偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项，增大或减小log.index.interval.bytes的值，对应地可以增加或缩小索引项的密度。</p><p>稀疏索引通过MappedByteBuffer将索引文件映射到内存中，以加快索引的查询速度。偏移量索引文件中的偏移量是单调递增的，查询指定偏移量时，使用二分查找法来快速定位偏移量的位置，如果指定的偏移量不在索引文件中，则会返回小于指定偏移量的最大偏移量。稀疏索引的方式是在磁盘空间、内存空间、查找时间等多方面之间的一个折中。</p><h3 id="偏移量索引"><a href="#偏移量索引" class="headerlink" title="偏移量索引"></a>偏移量索引</h3><p><img src="/images/a74fc5d2e7c5a8e4.jpg" alt="img" style="zoom:50%;"></p>"<p>（1）relativeOffset：相对偏移量，表示消息相对于baseOffset 的偏移量，占用4 个字节，当前索引文件的文件名即为baseOffset的值。<br>（2）position：物理地址，也就是消息在日志分段文件中对应的物理位置，占用4个字节。</p><p>消息的偏移量（offset）占用8个字节，也可以称为绝对偏移量。索引项中没有直接使用绝对偏移量而改为只占用4个字节的相对偏移量（relativeOffset=offset-baseOffset），这样可以减小索引文件占用的空间。</p><p>示意图如图：</p><p><img src="/images/00616dea7c78c263.jpg" alt="img" style="zoom:67%;"></p>"<h2 id="日志清理"><a href="#日志清理" class="headerlink" title="日志清理"></a>日志清理</h2><ol><li>日志删除（Log Retention）：按照一定的保留策略直接删除不符合条件的日志分段。</li><li>日志压缩（Log Compaction）：针对每个消息的key进行整合，对于有相同key的不同value值，只保留最后一个版本</li></ol><h2 id="磁盘存储"><a href="#磁盘存储" class="headerlink" title="磁盘存储"></a>磁盘存储</h2><p>Kafka 依赖于文件系统（更底层地来说就是磁盘）来存储和缓存消息。</p><p>有关测试结果表明，一个由6块7200r/min的RAID-5阵列组成的磁盘簇的线性（顺序）写入速度可以达到600MB/s，而随机写入速度只有100KB/s，两者性能相差6000倍。操作系统可以针对线性读写做深层次的优化，比如预读（read-ahead，提前将一个比较大的磁盘块读入内存）和后写（write-behind，将很多小的逻辑写操作合并起来组成一个大的物理写操作）技术。顺序写盘的速度不仅比随机写盘的速度快，而且也比随机写内存的速度快</p><p>Kafka 在设计时采用了文件追加的方式来写入消息，即只能在日志文件的尾部追加新的消息，并且也不允许修改已写入的消息，这种方式属于典型的顺序写盘的操作，所以就算 Kafka使用磁盘作为存储介质，它所能承载的吞吐量也不容小觑。</p><h3 id="页缓存"><a href="#页缓存" class="headerlink" title="页缓存"></a>页缓存</h3><p>页缓存是操作系统实现的一种主要的磁盘缓存，以此用来减少对磁盘 I/O 的操作。</p><p>对一个进程而言，它会在进程内部缓存处理所需的数据，然而这些数据有可能还缓存在操作系统的页缓存中，因此同一份数据有可能被缓存了两次。并且，除非使用Direct I/O的方式，否则页缓存很难被禁止。基于这些因素，使用文件系统并依赖于页缓存的做法明显要优于维护一个进程内缓存或其他结构，至少我们可以省去了一份进程内部的缓存消耗，同时还可以通过结构紧凑的字节码来替代使用对象的方式以节省更多的空间。</p><p>Kafka 中大量使用了页缓存，这是 Kafka 实现高吞吐的重要因素之一。</p><p>Linux系统会使用磁盘的一部分作为swap分区，这样可以进行进程的调度：把当前非活跃的进程调入 swap 分区，以此把内存空出来让给活跃的进程。对大量使用系统页缓存的 Kafka而言，应当尽量避免这种内存的交换，否则会对它各方面的性能产生很大的负面影响。我们可以通过修改vm.swappiness参数（Linux系统参数）来进行调节。vm.swappiness参数的上限为 100，它表示积极地使用 swap 分区，并把内存上的数据及时地搬运到 swap 分区中；vm.swappiness 参数的下限为 0，表示在任何情况下都不要发生交换（vm.swappiness=0的含义在不同版本的 Linux 内核中不太相同，这里采用的是变更后的最新解释），这样一来，当内存耗尽时会根据一定的规则突然中止某些进程。笔者建议将这个参数的值设置为 1，这样保留了swap的机制而又最大限度地限制了它对Kafka性能的影响。</p><h3 id="磁盘I-O流程"><a href="#磁盘I-O流程" class="headerlink" title="磁盘I/O流程"></a>磁盘I/O流程</h3><p>从编程角度而言，一般磁盘I/O的场景有以下四种。</p><p>（1）用户调用标准C库进行I/O操作，数据流为：应用程序buffer→C库标准IObuffer→文件系统页缓存→通过具体文件系统到磁盘。<br>（2）用户调用文件 I/O，数据流为：应用程序 buffer→文件系统页缓存→通过具体文件系统到磁盘。<br>（3）用户打开文件时使用O_DIRECT，绕过页缓存直接读写磁盘。<br>（4）用户使用类似dd工具，并使用direct参数，绕过系统cache与文件系统直接写磁盘。</p><p><img src="/images/6568f67112f15f65.jpg" alt="img"></p>"<p>针对不同的应用场景，I/O调度策略也会影响I/O的读写性能，目前Linux系统中的I/O调度策略有4种，分别为NOOP、CFQ、DEADLINE和ANTICIPATORY，默认为CFQ。</p><h4 id="NOOP"><a href="#NOOP" class="headerlink" title="NOOP"></a>NOOP</h4><p>NOOP算法的全写为No Operation。该算法实现了最简单的FIFO队列，所有I/O请求大致按照先来后到的顺序进行操作。之所以说“大致”，原因是NOOP在FIFO的基础上还做了相邻I/O请求的合并，并不是完全按照先进先出的规则满足I/O请求。</p><h4 id="CFQ"><a href="#CFQ" class="headerlink" title="CFQ"></a>CFQ</h4><p>CFQ算法的全写为Completely Fair Queuing。该算法的特点是按照I/O请求的地址进行排序，而不是按照先来后到的顺序进行响应。</p><p>CFQ是默认的磁盘调度算法，对于通用服务器来说是最好的选择。它试图均匀地分布对/IO带宽的访问。CFQ为每个进程单独创建一个队列来管理该进程所产生的请求，也就是说，每个进程一个队列，各队列之间的调度使用时间片进行调度，以此来保证每个进程都能被很好地分配到I/O带宽。I/O调度器每次执行一个进程的4次请求。缺点是，先来的I/O请求并不一定能被满足，可能会出现“饿死”的情况。</p><h4 id="DEADLINE"><a href="#DEADLINE" class="headerlink" title="DEADLINE"></a>DEADLINE</h4><p>DEADLINE在CFQ的基础上，解决了I/O请求“饿死”的极端情况。除了CFQ本身具有的I/O排序队列，DEADLINE额外分别为读I/O和写I/O提供了FIFO队列。读FIFO队列的最大等待时间为500ms，写FIFO队列的最大等待时间为5s。FIFO队列内的I/O请求优先级要比CFQ队列中的高，而读FIFO队列的优先级又比写FIFO队列的优先级高。</p><h4 id="ANTICIPATORY"><a href="#ANTICIPATORY" class="headerlink" title="ANTICIPATORY"></a>ANTICIPATORY</h4><p>CFQ和DEADLINE考虑的焦点在于满足零散I/O请求上。对于连续的I/O请求，比如顺序读，并没有做优化。在DEADLINE的基础上，为每个读I/O都设置了6ms的等待时间窗口。如果在6ms内OS收到了相邻位置的读I/O请求，就可以立即满足。</p><h3 id="零拷贝"><a href="#零拷贝" class="headerlink" title="零拷贝"></a>零拷贝</h3><p>所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中，而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能，减少了内核和用户模式之间的上下文切换。对 Linux操作系统而言，零拷贝技术依赖于底层的 sendfile（）方法实现。对应于 Java 语言，FileChannal.transferTo（）方法的底层实现就是sendfile（）方法。</p><p>单纯从概念上理解“零拷贝”比较抽象，这里简单地介绍一下它。考虑这样一种常用的情形：你需要将静态内容（类似图片、文件）展示给用户。这个情形就意味着需要先将静态内容从磁盘中复制出来放到一个内存buf中，然后将这个buf通过套接字（Socket）传输给用户，进而用户获得静态内容。这看起来再正常不过了，但实际上这是很低效的流程，我们把上面的这种情形抽象成下面的过程：</p><p><img src="/images/53a3e4fa4b60bb77.jpg" alt="img"></p>"<p>首先调用read（）将静态内容（这里假设为文件A）读取到tmp_buf，然后调用write（）将tmp_buf写入Socket，如图5-23所示。<br>在这个过程中，文件A经历了4次复制的过程：<br>（1）调用read（）时，文件A中的内容被复制到了内核模式下的Read Buffer中。<br>（2）CPU控制将内核模式数据复制到用户模式下。<br>（3）调用write（）时，将用户模式下的内容复制到内核模式下的Socket Buffer中。<br>（4）将内核模式下的Socket Buffer的数据复制到网卡设备中传送。</p><p><img src="/images/e2bf9fde52db46e4.jpg" alt="img" style="zoom:50%;"></p>"<p>从上面的过程可以看出，数据平白无故地从内核模式到用户模式“走了一圈”，浪费了 2次复制过程：第一次是从内核模式复制到用户模式；第二次是从用户模式再复制回内核模式，即上面4次过程中的第2步和第3步。而且在上面的过程中，内核和用户模式的上下文的切换也是4次。</p><p>如果采用了零拷贝技术，那么应用程序可以直接请求内核把磁盘中的数据传输给 Socket，如图</p><p><img src="/images/acd1ff6fd900a5ba.jpg" alt="img" style="zoom:67%;"></p>"<p>零拷贝技术通过DMA（Direct Memory Access）技术将文件内容复制到内核模式下的Read Buffer 中。不过没有数据被复制到 Socket Buffer，相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备（协议引擎）。这里数据只经历了2次复制就从磁盘中传送出去了，并且上下文切换也变成了2次。零拷贝是针对内核模式而言的，数据在内核模式下实现了零拷贝。</p><h1 id="深入服务端"><a href="#深入服务端" class="headerlink" title="深入服务端"></a>深入服务端</h1><h2 id="时间轮"><a href="#时间轮" class="headerlink" title="时间轮"></a>时间轮</h2><p>Kafka中存在大量的延时操作，比如延时生产、延时拉取和延时删除等。JDK中Timer和DelayQueue的插入和删除操作的平均时间复杂度为O（nlogn）并不能满足Kafka的高性能要求，而基于时间轮可以将插入和删除操作的时间复杂度都降为O（1）。</p><p>Kafka中的时间轮（TimingWheel）是一个存储定时任务的环形队列，底层采用数组实现，数组中的每个元素可以存放一个定时任务列表（TimerTaskList）。TimerTaskList是一个环形的双向链表，链表中的每一项表示的都是定时任务项（TimerTaskEntry），其中封装了真正的定时任务（TimerTask）。</p><p>时间轮由多个时间格组成，每个时间格代表当前时间轮的基本时间跨度（tickMs）。时间轮的时间格个数是固定的，可用wheelSize来表示，那么整个时间轮的总体时间跨度（interval）可以通过公式 tickMs×wheelSize计算得出。时间轮还有一个表盘指针（currentTime），用来表示时间轮当前所处的时间，currentTime是tickMs的整数倍。currentTime可以将整个时间轮划分为到期部分和未到期部分，currentTime当前指向的时间格也属于到期部分，表示刚好到期，需要处理此时间格所对应的TimerTaskList中的所有任务。</p><p><img src="/images/c9ae2038c3e97040.jpg" alt="img" style="zoom:67%;"></p>"<p>第一层的时间轮tickMs=1ms、wheelSize=20、interval=20ms。第二层的时间轮的tickMs为第一层时间轮的interval，即20ms。每一层时间轮的wheelSize是固定的，都是20，那么第二层的时间轮的总体时间跨度interval为400ms。以此类推，这个400ms也是第三层的tickMs的大小，第三层的时间轮的总体时间跨度为8000ms。<br>对于350ms的定时任务，显然第一层时间轮不能满足条件，所以就升级到第二层时间轮中，最终被插入第二层时间轮中时间格17所对应的TimerTaskList。如果此时又有一个定时为450ms的任务，那么显然第二层时间轮也无法满足条件，所以又升级到第三层时间轮中，最终被插入第三层时间轮中时间格1的TimerTaskList。注意到在到期时间为[400ms，800ms）区间内的多个任务（比如446ms、455ms和473ms的定时任务）都会被放入第三层时间轮的时间格1，时间格1对应的TimerTaskList的超时时间为400ms。随着时间的流逝，当此TimerTaskList到期之时，原本定时为450ms的任务还剩下50ms的时间，还不能执行这个任务的到期操作。这里就有一个时间轮降级的操作，会将这个剩余时间为 50ms 的定时任务重新提交到层级时间轮中，此时第一层时间轮的总体时间跨度不够，而第二层足够，所以该任务被放到第二层时间轮到期时间为[40ms，60ms）的时间格中。再经历40ms之后，此时这个任务又被“察觉”，不过还剩余10ms，还是不能立即执行到期操作。所以还要再有一次时间轮的降级，此任务被添加到第一层时间轮到期时间为[10ms，11ms）的时间格中，之后再经历 10ms 后，此任务真正到期，最终执行相应的到期操作。</p><p><img src="/images/b87aab11e78936f3.jpg" alt="img" style="zoom: 87%;"></p>"<h2 id="延时操作"><a href="#延时操作" class="headerlink" title="延时操作"></a>延时操作</h2><p><img src="/images/72c0732ecda2e516.jpg" alt="img" style="zoom:67%;"></p>"<h2 id="控制器"><a href="#控制器" class="headerlink" title="控制器"></a>控制器</h2><p>在 Kafka 集群中会有一个或多个 broker，其中有一个 broker 会被选举为控制器（Kafka Controller），它负责管理整个集群中所有分区和副本的状态。当某个分区的leader副本出现故障时，由控制器负责为该分区选举新的leader副本。当检测到某个分区的ISR集合发生变化时，由控制器负责通知所有broker更新其元数据信息。</p><h3 id="控制器的选举及异常恢复"><a href="#控制器的选举及异常恢复" class="headerlink" title="控制器的选举及异常恢复"></a>控制器的选举及异常恢复</h3><p>Kafka中的控制器选举工作依赖于ZooKeeper，成功竞选为控制器的broker会在ZooKeeper中创建/controller这个临时（EPHEMERAL）节点，此临时节点的内容参考如下：</p><p><img src="/images/2d344c7f751519c6.jpg" alt="img" style="zoom:80%;"></p>"<p>其中version在目前版本中固定为1，brokerid表示成为控制器的broker的id编号，timestamp表示竞选成为控制器时的时间戳。</p><p>在任意时刻，集群中有且仅有一个控制器。每个 broker 启动的时候会去尝试读取/controller节点的brokerid的值，如果读取到brokerid的值不为-1，则表示已经有其他 broker 节点成功竞选为控制器，所以当前 broker 就会放弃竞选；如果 ZooKeeper 中不存在/controller节点，或者这个节点中的数据异常，那么就会尝试去创建/controller节点。当前broker去创建节点的时候，也有可能其他broker同时去尝试创建这个节点，只有创建成功的那个broker才会成为控制器，而创建失败的broker竞选失败。每个broker都会在内存中保存当前控制器的brokerid值，这个值可以标识为activeControllerId。</p><p>ZooKeeper 中还有一个与控制器有关的/controller_epoch 节点，这个节点是持久（PERSISTENT）节点，节点中存放的是一个整型的controller_epoch值。controller_epoch用于记录控制器发生变更的次数，即记录当前的控制器是第几代控制器，我们也可以称之为“控制器的纪元”。<br>controller_epoch的初始值为1，即集群中第一个控制器的纪元为1，当控制器发生变更时，每选出一个新的控制器就将该字段值加1。每个和控制器交互的请求都会携带controller_epoch这个字段，如果请求的controller_epoch值小于内存中的controller_epoch值，则认为这个请求是向已经过期的控制器所发送的请求，那么这个请求会被认定为无效的请求。如果请求的controller_epoch值大于内存中的controller_epoch值，那么说明已经有新的控制器当选了。由此可见，Kafka 通过 controller_epoch 来保证控制器的唯一性，进而保证相关操作的一致性。</p><p>控制器在选举成功之后会读取 ZooKeeper 中各个节点的数据来初始化上下文信息（ControllerContext），并且需要管理这些上下文信息。比如为某个主题增加了若干分区，控制器在负责创建这些分区的同时要更新上下文信息，并且需要将这些变更信息同步到其他普通的broker 节点中。不管是监听器触发的事件，还是定时任务触发的事件，或者是其他事件，都会读取或更新控制器中的上下文信息，那么这样就会涉及多线程间的同步。如果单纯使用锁机制来实现，那么整体的性能会大打折扣。针对这一现象，Kafka 的控制器使用单线程基于事件队列的模型，将每个事件都做一层封装，然后按照事件发生的先后顺序暂存到 LinkedBlockingQueue 中，最后使用一个专用的线程（ControllerEventThread）按照FIFO（First Input First Output，先入先出）的原则顺序处理各个事件，这样不需要锁机制就可以在多线程间维护线程安全，具体可以参考图6-14。</p><p><img src="/images/8a9bfe99de938f3f.jpg" alt="img"></p>"<p>如果broker 在数据变更前是控制器，在数据变更后自身的 brokerid 值与新的 activeControllerId 值不一致，那么就需要“退位”，关闭相应的资源，比如关闭状态机、注销相应的监听器等。</p><h3 id="分区leader的选举"><a href="#分区leader的选举" class="headerlink" title="分区leader的选举"></a>分区leader的选举</h3><p>分区leader副本的选举由控制器负责具体实施。当创建分区（创建主题或增加分区都有创建分区的动作）或分区上线（比如分区中原先的leader副本下线，此时分区需要选举一个新的leader 上线来对外提供服务）的时候都需要执行 leader 的选举动作，对应的选举策略为OfflinePartitionLeaderElectionStrategy。这种策略的基本思路是按照 AR 集合中副本的顺序查找第一个存活的副本，并且这个副本在ISR集合中。一个分区的AR集合在分配的时候就被指定，并且只要不发生重分配的情况，集合内部副本的顺序是保持不变的，而分区的ISR集合中副本的顺序可能会改变。</p><p>举个例子，集群中有3个节点：broker0、broker1和broker2，在某一时刻具有3个分区且副本因子为3的主题topic-leader的具体信息如下：</p><p><img src="/images/063cc40defc1dd04.jpg" alt="img" style="zoom:80%;"></p>"<p>此时关闭broker0，那么对于分区2而言，存活的AR就变为[1，2]，同时ISR变为[2，1]。此时查看主题topic-leader的具体信息（参考如下），分区2的leader就变为了1而不是2。</p><p><img src="/images/aba466ecea0b6ad9.jpg" alt="img" style="zoom:80%;"></p>"<h1 id="深入客户端"><a href="#深入客户端" class="headerlink" title="深入客户端"></a>深入客户端</h1><h2 id="分区分配策略"><a href="#分区分配策略" class="headerlink" title="分区分配策略"></a>分区分配策略</h2><p>Kafka提供了消费者客户端参数partition.assignment.strategy来设置消费者与订阅主题之间的分区分配策略。默认情况下，此参数的值为 org.apache.kafka.clients.consumer.RangeAssignor，即采用RangeAssignor分配策略。</p><h3 id="RangeAssignor分配策略"><a href="#RangeAssignor分配策略" class="headerlink" title="RangeAssignor分配策略"></a>RangeAssignor分配策略</h3><p>RangeAssignor 分配策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度，然后将分区按照跨度进行平均分配，以保证分区尽可能均匀地分配给所有的消费者。对于每一个主题，RangeAssignor策略会将消费组内所有订阅这个主题的消费者按照名称的字典序排序，然后为每个消费者划分固定的分区范围，如果不够平均分配，那么字典序靠前的消费者会被多分配一个分区。</p><p>假设n=分区数/消费者数量，m=分区数%消费者数量，那么前m个消费者每个分配n+1个分区，后面的（消费者数量-m）个消费者每个分配n个分区。</p><p>假设上面例子中2个主题都只有3个分区，那么订阅的所有分区可以标识为：t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为：</p><p><img src="/images/0318981be7607432.jpg" alt="img" style="zoom:67%;"></p>"<h3 id="RoundRobinAssignor分配策略"><a href="#RoundRobinAssignor分配策略" class="headerlink" title="RoundRobinAssignor分配策略"></a>RoundRobinAssignor分配策略</h3><p>RoundRobinAssignor分配策略的原理是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序，然后通过轮询方式逐个将分区依次分配给每个消费者。RoundRobinAssignor分配策略对应的 partition.assignment.strategy 参数值为 org.apache.kafka.clients.consumer.RoundRobinAssignor。</p><p>如果同一个消费组内的消费者订阅的信息是不相同的，那么在执行分区分配的时候就不是完全的轮询分配，有可能导致分区分配得不均匀。如果某个消费者没有订阅消费组内的某个主题，那么在分配分区的时候此消费者将分配不到这个主题的任何分区。</p><p>举个例子，假设消费组内有3个消费者（C0、C1和C2），它们共订阅了3个主题（t0、t1、t2），这3个主题分别有1、2、3个分区，即整个消费组订阅了t0p0、t1p0、t1p1、t2p0、t2p1、t2p2这6个分区。具体而言，消费者C0订阅的是主题t0，消费者C1订阅的是主题t0和t1，消费者C2订阅的是主题t0、t1和t2，那么最终的分配结果为：</p><p><img src="/images/fa0a984f7c903a24.jpg" alt="img" style="zoom:50%;"></p>"<h3 id="StickyAssignor分配策略"><a href="#StickyAssignor分配策略" class="headerlink" title="StickyAssignor分配策略"></a>StickyAssignor分配策略</h3><p>我们再来看一下StickyAssignor分配策略，“sticky”这个单词可以翻译为“黏性的”，Kafka从0.11.x版本开始引入这种分配策略，它主要有两个目的：</p><p>（1）分区的分配要尽可能均匀。<br>（2）分区的分配尽可能与上次分配的保持相同。</p><p>当两者发生冲突时，第一个目标优先于第二个目标。鉴于这两个目标，StickyAssignor分配策略的具体实现要比RangeAssignor和RoundRobinAssignor这两种分配策略要复杂得多。</p><h2 id="消费者协调器和组协调器"><a href="#消费者协调器和组协调器" class="headerlink" title="消费者协调器和组协调器"></a>消费者协调器和组协调器</h2><p>了解了Kafka 中消费者的分区分配策略之后是否会有这样的疑问：如果消费者客户端中配置了两个分配策略，那么以哪个为准呢？如果有多个消费者，彼此所配置的分配策略并不完全相同，那么以哪个为准？多个消费者之间的分区分配是需要协同的，那么这个协同的过程又是怎样的呢？这一切都是交由消费者协调器（ConsumerCoordinator）和组协调器（GroupCoordinator）来完成的，它们之间使用一套组协调协议进行交互。</p><h3 id="旧版消费者客户端的问题"><a href="#旧版消费者客户端的问题" class="headerlink" title="旧版消费者客户端的问题"></a>旧版消费者客户端的问题</h3><p>消费者协调器和组协调器的概念是针对新版的消费者客户端而言的，Kafka 建立之初并没有它们。旧版的消费者客户端是使用ZooKeeper的监听器（Watcher）来实现这些功能的。</p><p>每个消费组（＜group＞）在ZooKeeper中都维护了一个/consumers/＜group＞/ids路径，在此路径下使用临时节点记录隶属于此消费组的消费者的唯一标识（consumerIdString），consumerIdString由消费者启动时创建。<br>与/consumers/＜group＞/ids同级的还有两个节点：owners和offsets，/consumers/＜group＞/owner 路径下记录了分区和消费者的对应关系，/consumers/＜group＞/offsets路径下记录了此消费组在分区中对应的消费位移。</p><p><img src="/images/70e3a725f58b9ca3.jpg" alt="img" style="zoom:67%;"></p>"<p>每个消费者在启动时都会在/consumers/＜group＞/ids 和/brokers/ids 路径上注册一个监听器。当/consumers/＜group＞/ids路径下的子节点发生变化时，表示消费组中的消费者发生了变化；当/brokers/ids路径下的子节点发生变化时，表示broker出现了增减。这样通过ZooKeeper所提供的Watcher，每个消费者就可以监听消费组和Kafka集群的状态了。</p><p>这种方式下每个消费者对ZooKeeper的相关路径分别进行监听，当触发再均衡操作时，一个消费组下的所有消费者会同时进行再均衡操作，而消费者之间并不知道彼此操作的结果，这样可能导致Kafka工作在一个不正确的状态。与此同时，这种严重依赖于ZooKeeper集群的做法还有两个比较严重的问题。</p><p>（1）羊群效应（Herd Effect）：所谓的羊群效应是指ZooKeeper中一个被监听的节点变化，大量的 Watcher 通知被发送到客户端，导致在通知期间的其他操作延迟，也有可能发生类似死锁的情况。<br>（2）脑裂问题（Split Brain）：消费者进行再均衡操作时每个消费者都与ZooKeeper进行通信以判断消费者或broker变化的情况，由于ZooKeeper本身的特性，可能导致在同一时刻各个消费者获取的状态不一致，这样会导致异常问题发生。</p><h3 id="再均衡的原理"><a href="#再均衡的原理" class="headerlink" title="再均衡的原理"></a>再均衡的原理</h3><p>新版的消费者客户端对此进行了重新设计，将全部消费组分成多个子集，每个消费组的子集在服务端对应一个GroupCoordinator对其进行管理，GroupCoordinator是Kafka服务端中用于管理消费组的组件。而消费者客户端中的ConsumerCoordinator组件负责与GroupCoordinator进行交互。</p><p>ConsumerCoordinator与GroupCoordinator之间最重要的职责就是负责执行消费者再均衡的操作，包括前面提及的分区分配的工作也是在再均衡期间完成的。就目前而言，一共有如下几种情形会触发再均衡的操作：<br>· 有新的消费者加入消费组。<br>· 有消费者宕机下线。消费者并不一定需要真正下线，例如遇到长时间的 GC、网络延迟导致消费者长时间未向GroupCoordinator发送心跳等情况时，GroupCoordinator会认为消费者已经下线。<br>· 有消费者主动退出消费组（发送 LeaveGroupRequest 请求）。比如客户端调用了unsubscrible（）方法取消对某些主题的订阅。<br>· 消费组所对应的GroupCoorinator节点发生了变更。<br>· 消费组内所订阅的任一主题或者主题的分区数量发生变化。</p><p>当有消费者加入消费组时，消费者、消费组及组协调器之间会经历一下几个阶段。</p><h4 id="第一阶段（FIND-COORDINATOR）"><a href="#第一阶段（FIND-COORDINATOR）" class="headerlink" title="第一阶段（FIND_COORDINATOR）"></a>第一阶段（FIND_COORDINATOR）</h4><p>消费者需要确定它所属的消费组对应的GroupCoordinator所在的broker，并创建与该broker相互通信的网络连接。如果消费者已经保存了与消费组对应的 GroupCoordinator 节点的信息，并且与它之间的网络连接是正常的，那么就可以进入第二阶段。否则，就需要向集群中的某个节点发送FindCoordinatorRequest请求来查找对应的GroupCoordinator，这里的“某个节点”并非是集群中的任意节点，而是负载最小的节点</p><p>Kafka 在收到 FindCoordinatorRequest 请求之后，会根据 coordinator_key（也就是groupId）查找对应的GroupCoordinator节点，如果找到对应的GroupCoordinator则会返回其相对应的node_id、host和port信息。</p><p><img src="/images/7bf38dda5d6acd59.jpg" alt="img" style="zoom:67%;"></p>"<ol><li>先根据消费组groupId的哈希值计算__consumer_offsets中的分区编号</li><li>找到对应的__consumer_offsets中的分区之后，再寻找此分区leader副本所在的broker节点，该broker节点即为这个groupId所对应的GroupCoordinator节点</li></ol><h4 id="第二阶段（JOIN-GROUP）"><a href="#第二阶段（JOIN-GROUP）" class="headerlink" title="第二阶段（JOIN_GROUP）"></a>第二阶段（JOIN_GROUP）</h4><p>在成功找到消费组所对应的 GroupCoordinator 之后就进入加入消费组的阶段，在此阶段的消费者会向GroupCoordinator发送JoinGroupRequest请求，并处理响应。</p><p><img src="/images/9287b9f4e98a39a5.jpg" alt="img" style="zoom:67%;"></p>"<p>JoinGroupRequest中的group_protocols域为数组类型，其中可以囊括多个分区分配策略</p><h5 id="选举消费组的leader"><a href="#选举消费组的leader" class="headerlink" title="选举消费组的leader"></a>选举消费组的leader</h5><p>GroupCoordinator需要为消费组内的消费者选举出一个消费组的leader，这个选举的算法也很简单，分两种情况分析。如果消费组内还没有 leader，那么第一个加入消费组的消费者即为消费组的 leader。如果某一时刻 leader 消费者由于某些原因退出了消费组，那么会重新选举一个新的leader，这个重新选举leader的过程又更“随意”了，相关代码如下：</p><p><img src="/images/dc8a457149c3401c.jpg" alt="img" style="zoom: 67%;"></p>"<h5 id="选举分区分配策略"><a href="#选举分区分配策略" class="headerlink" title="选举分区分配策略"></a>选举分区分配策略</h5><p>每个消费者都可以设置自己的分区分配策略，对消费组而言需要从各个消费者呈报上来的各个分配策略中选举一个彼此都“信服”的策略来进行整体上的分区分配。这个分区分配的选举并非由leader消费者决定，而是根据各个消费者呈报的分配策略来实施。最终的分配策略基本上可以看作被各个消费者支持的最多的策略</p><h4 id="第三阶段（SYNC-GROUP）"><a href="#第三阶段（SYNC-GROUP）" class="headerlink" title="第三阶段（SYNC_GROUP）"></a>第三阶段（SYNC_GROUP）</h4><p>leader 消费者根据在第二阶段中选举出来的分区分配策略来实施具体的分区分配，在此之后需要将分配的方案同步给各个消费者，此时leader消费者并不是直接和其余的普通消费者同步分配方案，而是通过 GroupCoordinator 这个“中间人”来负责转发同步分配方案的。在第三阶段，也就是同步阶段，各个消费者会向GroupCoordinator发送SyncGroupRequest请求来同步分配方案，如图：</p><p><img src="/images/e7761e7bc44e89ff.jpg" alt="img" style="zoom:67%;"></p>"<p>服务端在收到消费者发送的SyncGroupRequest请求之后会交由GroupCoordinator来负责具体的逻辑处理。GroupCoordinator会将从 leader 消费者发送过来的分配方案提取出来，连同整个消费组的元数据信息一起存入Kafka的__consumer_offsets主题中，最后发送响应给各个消费者以提供给各个消费者各自所属的分配方案。</p><p>当消费者收到所属的分配方案之后开启心跳任务，消费者定期向服务端的GroupCoordinator发送HeartbeatRequest来确定彼此在线。</p><h4 id="第四阶段（HEARTBEAT）"><a href="#第四阶段（HEARTBEAT）" class="headerlink" title="第四阶段（HEARTBEAT）"></a>第四阶段（HEARTBEAT）</h4><p>进入这个阶段之后，消费组中的所有消费者就会处于正常工作状态。在正式消费之前，消费者还需要确定拉取消息的起始位置。假设之前已经将最后的消费位移提交到了GroupCoordinator，并且GroupCoordinator将其保存到了Kafka内部的__consumer_offsets主题中，此时消费者可以通过OffsetFetchRequest请求获取上次提交的消费位移并从此处继续消费。</p><p>消费者通过向 GroupCoordinator 发送心跳来维持它们与消费组的从属关系，以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳，就被认为是活跃的，说明它还在读取分区中的消息。</p><h2 id="consumer-offsets剖析"><a href="#consumer-offsets剖析" class="headerlink" title="__consumer_offsets剖析"></a>__consumer_offsets剖析</h2><p>一般情况下，当集群中第一次有消费者消费消息时会自动创建主题__consumer_offsets，不过它的副本因子还受offsets.topic.replication.factor参数的约束。客户端提交消费位移是使用 OffsetCommitRequest 请求实现的，OffsetCommitRequest 的结构如图</p><p><img src="/images/a85a64d872c2804f.jpg" alt="img" style="zoom:80%;"></p>"<p>请求体第一层中的group_id、generation_id和member_id在前面的内容中已经介绍过多次了，retention_time 表示当前提交的消费位移所能保留的时长，不过对于消费者而言这个值保持为-1。也就是说，按照 broker 端的配置 offsets.retention.minutes 来确定保留时长。offsets.retention.minutes的默认值为10080，即7天，超过这个时间后消费位移的信息就会被删除（使用墓碑消息和日志压缩策略）。注意这个参数在2.0.0版本之前的默认值为1440，即1天，很多关于消费位移的异常也是由这个参数的值配置不当造成的。有些定时消费的任务在执行完某次消费任务之后保存了消费位移，之后隔了一段时间再次执行消费任务，如果这个间隔时间超过offsets.retention.minutes的配置值，那么原先的位移信息就会丢失，最后只能根据客户端参数 auto.offset.reset 来决定开始消费的位置，遇到这种情况时就需要根据实际情况来调配offsets.retention.minutes参数的值。</p><p>同消费组的元数据信息一样，最终提交的消费位移也会以消息的形式发送至主题__consumer_offsets，与消费位移对应的消息也只定义了 key 和 value 字段的具体内容，它不依赖于具体版本的消息格式，以此做到与具体的消息格式无关。</p><p><img src="/images/d3272a8e04043a2f.jpg" alt="img" style="zoom:80%;"></p>"<h2 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h2><p>Kafka从0.11.0.0版本开始引入了幂等和事务这两个特性，以此来实现EOS（exactly once semantics，精确一次处理语义）。</p><h3 id="幂等"><a href="#幂等" class="headerlink" title="幂等"></a>幂等</h3><p>生产者在进行重试的时候有可能会重复写入消息，而使用Kafka的幂等性功能之后就可以避免这种情况。</p><p>开启幂等性功能的方式很简单，只需要显式地将生产者客户端参数enable.idempotence设置为true即可（这个参数的默认值为false）</p><p>为了实现生产者的幂等性，Kafka为此引入了producer id（以下简称PID）和序列号（sequence number）这两个概念，分别对应 v2 版的日志格式中RecordBatch的producer id和first seqence这两个字段。每个新的生产者实例在初始化的时候都会被分配一个PID，这个PID对用户而言是完全透明的。对于每个PID，消息发送到的每一个分区都有对应的序列号，这些序列号从0开始单调递增。生产者每发送一条消息就会将＜PID，分区＞对应的序列号的值加1。</p><p>broker端会在内存中为每一对＜PID，分区＞维护一个序列号。对于收到的每一条消息，只有当它的序列号的值（SN_new）比broker端中维护的对应的序列号的值（SN_old）大1（即SN_new=SN_old+1）时，broker才会接收它。如果SN_new＜SN_old+1，那么说明消息被重复写入，broker可以直接将其丢弃。如果SN_new＞SN_old+1，那么说明中间有数据尚未写入，出现了乱序，暗示可能有消息丢失，对应的生产者会抛出OutOfOrderSequenceException，这个异常是一个严重的异常，后续的诸如 send（）、beginTransaction（）、commitTransaction（）等方法的调用都会抛出IllegalStateException的异常。</p><p>引入序列号来实现幂等也只是针对每一对＜PID，分区＞而言的，也就是说，Kafka的幂等只能保证单个生产者会话（session）中单分区的幂等。</p><h3 id="事务-1"><a href="#事务-1" class="headerlink" title="事务"></a>事务</h3><p>幂等性并不能跨多个分区运作，而事务<a href="javascript:void(0" target="_blank" rel="noopener">[1]</a>;)可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功，要么全部失败，不存在部分成功、部分失败的可能。</p><p>为了实现事务，应用程序必须提供唯一的 transactionalId，这个 transactionalId 通过客户端参数transactional.id来显式设置，参考如下：</p><p><img src="/images/312da967f5114422.jpg" alt="img"></p>"<p>事务要求生产者开启幂等特性，因此通过将transactional.id参数设置为非空从而开启事务特性的同时需要将 enable.idempotence 设置为 true</p><p>transactionalId与PID一一对应，两者之间所不同的是transactionalId由用户显式设置，而PID是由Kafka内部分配的。另外，为了保证新的生产者启动后具有相同transactionalId的旧生产者能够立即失效，每个生产者通过transactionalId获取PID的同时，还会获取一个单调递增的producer epoch</p><p>从生产者的角度分析，通过事务，Kafka 可以保证跨生产者会话的消息幂等发送，以及跨生产者会话的事务恢复。前者表示具有相同 transactionalId 的新生产者实例被创建且工作的时候，旧的且拥有相同transactionalId的生产者实例将不再工作。后者指当某个生产者实例宕机后，新的生产者实例可以保证任何未完成的旧事务要么被提交（Commit），要么被中止（Abort），如此可以使新的生产者实例从一个正常的状态开始工作。<br>而从消费者的角度分析，事务能保证的语义相对偏弱。出于以下原因，Kafka 并不能保证已提交的事务中的所有消息都能够被消费：消息归档，通过seek（）方法消费导致消息遗漏等</p><p>在消费端有一个参数isolation.level，与事务有着莫大的关联，这个参数的默认值为“read_uncommitted”，意思是说消费端应用可以看到（消费到）未提交的事务，当然对于已提交的事务也是可见的。这个参数还可以设置为“read_committed”，表示消费端应用不可以看到尚未提交的事务内的消息。举个例子，如果生产者开启事务并向某个分区值发送3条消息msg1、msg2和msg3，在执行commitTransaction（）或abortTransaction（）方法前，设置为“read_committed”的消费端应用是消费不到这些消息的，不过在KafkaConsumer内部会缓存这些消息，直到生产者执行 commitTransaction（）方法之后它才能将这些消息推送给消费端应用。反之，如果生产者执行了 abortTransaction（）方法，那么 KafkaConsumer 会将这些缓存的消息丢弃而不推送给消费端应用。</p><p>日志文件中除了普通的消息，还有一种消息专门用来标志一个事务的结束，它就是控制消息（ControlBatch）。控制消息一共有两种类型：COMMIT和ABORT，分别用来表征事务已经成功提交或已经被成功中止。KafkaConsumer 可以通过这个控制消息来判断对应的事务是被提交了还是被中止了，然后结合参数isolation.level配置的隔离级别来决定是否将相应的消息返回给消费端应用</p><p><img src="/images/5d554daf3cf40e4f.jpg" alt="img" style="zoom:67%;"></p>"<p>为了实现事务的功能，Kafka还引入了事务协调器（TransactionCoordinator）来负责处理事务，这一点可以类比一下组协调器（GroupCoordinator）。每一个生产者都会被指派一个特定的TransactionCoordinator，所有的事务逻辑包括分派 PID 等都是由 TransactionCoordinator 来负责实施的。TransactionCoordinator 会将事务状态持久化到内部主题__transaction_state 中。下面就以最复杂的consume-transform-produce的流程为例来分析Kafka事务的实现原理。</p><p><img src="/images/image-20220503100242094.png" alt="image-20220503100242094" style="zoom: 67%;"></p>"<h1 id="可靠性探究"><a href="#可靠性探究" class="headerlink" title="可靠性探究"></a>可靠性探究</h1><h2 id="副本剖析"><a href="#副本剖析" class="headerlink" title="副本剖析"></a>副本剖析</h2><p>Kafka从0.8版本开始为分区引入了多副本机制，通过增加副本数量来提升数据容灾能力。同时，Kafka通过多副本机制实现故障自动转移，在Kafka集群中某个broker节点失效的情况下仍然保证服务可用。</p><h3 id="失效副本"><a href="#失效副本" class="headerlink" title="失效副本"></a>失效副本</h3><p>正常情况下，分区的所有副本都处于ISR集合中，但是难免会有异常情况发生，从而某些副本被剥离出ISR集合中。在ISR集合之外，也就是处于同步失效或功能失效（比如副本处于非存活状态）的副本统称为失效副本，失效副本对应的分区也就称为同步失效分区，即under-replicated分区。</p><p><img src="/images/3df511d99b0f7fd3.jpg" alt="img" style="zoom:67%;"></p>"<p>一般有两种情况会导致副本失效：</p><p>· follower副本进程卡住，在一段时间内根本没有向leader副本发起同步请求，比如频繁的Full GC。<br>· follower副本进程同步过慢，在一段时间内都无法追赶上leader副本，比如I/O开销过大。</p><h3 id="ISR的伸缩"><a href="#ISR的伸缩" class="headerlink" title="ISR的伸缩"></a>ISR的伸缩</h3><p>Kafka 在启动的时候会开启两个与 ISR 相关的定时任务，名称分别为“isr-expiration”和“isr-change-propagation”。</p><p>isr-expiration任务会周期性地检测每个分区是否需要缩减其ISR集合。这个周期和replica.lag.time.max.ms参数有关，大小是这个参数值的一半，默认值为5000ms。当检测到ISR集合中有失效副本时，就会收缩ISR集合。如果某个分区的ISR集合发生变更，则会将变更后的数据记录到 ZooKeeper 对应的/brokers/topics/＜topic＞/partition/＜parititon＞/state节点中。</p><p>当 ISR 集合发生变更时还会将变更后的记录缓存到 isrChangeSet 中，isr-change-propagation任务会周期性（固定值为 2500ms）地检查 isrChangeSet，如果发现isrChangeSet中有ISR集合的变更记录，那么它会在ZooKeeper的/isr_change_notification路径下创建一个以 isr_change_开头的持久顺序节点（比如/isr_change_notification/isr_change_0000000000），并将isrChangeSet中的信息保存到这个节点中。</p><p>随着follower副本不断与leader副本进行消息同步，follower副本的LEO也会逐渐后移，并最终追赶上leader副本，此时该follower副本就有资格进入ISR集合。追赶上leader副本的判定准则是此副本的LEO是否不小于leader副本的HW，注意这里并不是和leader副本的LEO相比。ISR扩充之后同样会更新ZooKeeper中的/brokers/topics/＜topic＞/partition/＜parititon＞/state节点和isrChangeSet，之后的步骤就和ISR收缩时的相同。</p><h3 id="Leader-Epoch的介入"><a href="#Leader-Epoch的介入" class="headerlink" title="Leader Epoch的介入"></a>Leader Epoch的介入</h3><p>如果leader副本发生切换，那么同步过程又该如何处理呢？在0.11.0.0版本之前，Kafka使用的是基于HW的同步机制，但这样有可能出现数据丢失或leader副本和follower副本数据不一致的问题。</p><p>在某一时刻，B中有2条消息m1和m2，A从B（leader副本）中同步了这两条消息，此时A和B的LEO都为2，同时HW都为1；之后A再向B中发送请求以拉取消息，FetchRequest请求中带上了A的LEO信息，B在收到请求之后更新了自己的HW为2；B中虽然没有更多的消息，但还是在延时一段时间之后（延时拉取）返回FetchResponse，并在其中包含了HW信息；最后A根据FetchResponse中的HW信息更新自己的HW为2。</p><p><img src="/images/0d46d37620552363.jpg" alt="img" style="zoom:67%;"></p>"<p>可以看到整个过程中两者之间的HW同步有一个间隙，在A写入消息m2之后（LEO更新为2）需要再一轮的FetchRequest/FetchResponse才能更新自身的HW为2。如果在这个时候A宕机了，那么在A重启之后会根据之前HW位置（这个值会存入本地的复制点文件replication-offset-checkpoint）进行日志截断，这样便会将m2这条消息删除，此时A只剩下m1这一条消息，之后A再向B发送FetchRequest请求拉取消息。</p><p><img src="/images/b4889269e4c37ac1.jpg" alt="img" style="zoom:67%;"></p>"<p>此时若B 再宕机，那么 A 就会被选举为新的leader。B 恢复之后会成为follower，由于follower副本HW不能比leader副本的HW高，所以还会做一次日志截断，以此将HW调整为1。这样一来m2这条消息就丢失了（就算B不能恢复，这条消息也同样丢失）。</p><p><img src="/images/9f538c9ab9cf1178.jpg" alt="img" style="zoom:67%;"></p>"<p>Kafka从0.11.0.0开始引入了leader epoch的概念，在需要截断数据的时候使用leader epoch作为参考依据而不是原本的HW。leader epoch代表leader的纪元信息（epoch），初始值为0。每当leader变更一次，leader epoch的值就会加1，相当于为leader增设了一个版本号。与此同时，每个副本中还会增设一个矢量＜LeaderEpoch=＞StartOffset＞，其中StartOffset表示当前LeaderEpoch下写入的第一条消息的偏移量。每个副本的Log下都有一个leader-epoch-checkpoint文件，在发生leader epoch变更时，会将对应的矢量对追加到这个文件中</p><p>下面我们再来看一下引入 leader epoch 之后如何应付前面所说的数据丢失和数据不一致的场景。（LE是LeaderEpoch的缩写，当前A和B中的LE都为0）</p><p><img src="/images/8890d6012575a8a2.jpg" alt="img" style="zoom:67%;"></p>"<p>同样 A 发生重启，之后 A 不是先忙着截断日志而是先发送 OffsetsForLeaderEpochRequest请求给 B（OffsetsForLeaderEpochRequest ，其中包含 A 当前的LeaderEpoch值），B作为目前的leader在收到请求之后会返回当前的LEO（LogEndOffset，注意图中LE0和LEO的不同），与请求对应的响应为OffsetsForLeaderEpochResponse</p><p><img src="/images/fbf0eb581910f25d.jpg" alt="img" style="zoom:67%;"></p>"<p>A在收到2之后发现和目前的LEO相同，也就不需要截断日志了。之后B发生了宕机，A成为新的leader，那么对应的LE=0也变成了LE=1，对应的消息m2此时就得到了保留。之后不管B有没有恢复，后续的消息都可以以LE1为LeaderEpoch陆续追加到A中。</p><p><img src="/images/b42c4ac3e1481424.jpg" alt="img" style="zoom:67%;"></p>"<p>再来看一下leader epoch如何应对数据不一致的场景。如图所示，当前A为leader，B为follower，A中有2条消息m1和m2，而B中有1条消息m1。假设A和B同时“挂掉”，然后B第一个恢复过来并成为新的leader。</p><p><img src="/images/0df2016689745581.jpg" alt="img" style="zoom:67%;"></p>"<p>之后B写入消息m3，并将LEO和HW更新至2，如图所示。注意此时的LeaderEpoch已经从LE0增至LE1了。</p><p><img src="/images/69b257de2be11c58.jpg" alt="img" style="zoom:67%;"></p>"<p>紧接着A也恢复过来成为follower并向B发送OffsetsForLeaderEpochRequest请求，此时A的LeaderEpoch为LE0。B根据LE0查询到对应的offset为1并返回给A，A就截断日志并删除了消息m2，如图所示。之后A发送FetchRequest至B请求来同步数据，最终A和B中都有两条消息m1和m3，HW和LEO都为2，并且LeaderEpoch都为LE1，如此便解决了数据不一致的问题。</p><p><img src="/images/5924977dba9c4362.jpg" alt="img" style="zoom:67%;"></p>"<h3 id="为什么不支持读写分离"><a href="#为什么不支持读写分离" class="headerlink" title="为什么不支持读写分离"></a>为什么不支持读写分离</h3><p>主读从写可以均摊一定的负载却不能做到完全的负载均衡，比如对于数据写压力很大而读压力很小的情况，从节点只能分摊很少的负载压力，而绝大多数压力还是在主节点上。而在Kafka中却可以达到很大程度上的负载均衡，而且这种均衡是在主写主读的架构上实现的。我们来看一下Kafka的生产消费模型，如图所示。</p><p><img src="/images/74027dec24cff2ff.jpg" alt="img" style="zoom:67%;"></p>"<p>在实际应用中，配合监控、告警、运维相结合的生态平台，在绝大多数情况下Kafka都能做到很大程度上的负载均衡。总的来说，Kafka 只支持主写主读有几个优点：可以简化代码的实现逻辑，减少出错的可能；将负载粒度细化均摊，与主写从读相比，不仅负载效能更好，而且对用户可控；没有延时的影响；在副本稳定的情况下，不会出现数据不一致的情况。为此，Kafka 又何必再去实现对它而言毫无收益的主写从读的功能呢？这一切都得益于 Kafka 优秀的架构设计，从某种意义上来说，主写从读是由于设计上的缺陷而形成的权宜之计。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;基本概念&quot;&gt;&lt;a href=&quot;#基本概念&quot; class=&quot;headerlink&quot; title=&quot;基本概念&quot;&gt;&lt;/a&gt;基本概念&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;/images/abd2e7ebfe90a4b0.jpg&quot; alt=&quot;img&quot; style=&quot;zoom:
      
    
    </summary>
    
      <category term="复习" scheme="http://yoursite.com/categories/%E5%A4%8D%E4%B9%A0/"/>
    
    
      <category term="kafka" scheme="http://yoursite.com/tags/kafka/"/>
    
  </entry>
  
  <entry>
    <title>redis设计与实现</title>
    <link href="http://yoursite.com/2022/03/02/%E5%A4%8D%E4%B9%A0%E6%80%BB%E7%BB%93/redis/"/>
    <id>http://yoursite.com/2022/03/02/复习总结/redis/</id>
    <published>2022-03-01T16:00:00.000Z</published>
    <updated>2022-04-15T08:44:53.498Z</updated>
    
    <content type="html"><![CDATA[<h1 id="数据结构与对象"><a href="#数据结构与对象" class="headerlink" title="数据结构与对象"></a>数据结构与对象</h1><h2 id="SDS"><a href="#SDS" class="headerlink" title="SDS"></a>SDS</h2><ul><li>数据结构</li></ul><p><img src="/images/image-20220327155838151.png" alt="image-20220327155838151" style="zoom:50%;"></p>"<ul><li><p>相较于原生c语言字符串的区别？</p><p>​            常数复杂度获取字符串长度<br>​            杜绝缓冲区溢出<br>​            减少修改字符串时带来的内存重分配次数<br>​                空间预分配<br>​                惰性空间释放<br>​            二进制安全</p></li></ul><h2 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h2><ul><li><p>数据结构</p><p>​            <img src="/images/image-20220327160246557.png" alt="image-20220327160246557" style="zoom: 33%;"></p>"</li></ul><h2 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h2><ul><li><p>数据结构</p><pre><code>&lt;img src=&quot;images/image-20220327160401494.png&quot; alt=&quot;image-20220327160401494&quot; style=&quot;zoom: 25%;&quot; /&gt;</code></pre><p>ht 属性是一个包含两个项的数组， 数组中的每个项都是一个 dictht 哈希表， 一般情况下， 字典只使用 ht[0] 哈希表， ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。<br>除了 ht[1] 之外， 另一个和 rehash 有关的属性就是 rehashidx ： 它记录了 rehash 目前的进度， 如果目前没有在进行 rehash ， 那么它的值为 -1 。</p></li><li><p>hash算法</p><pre><code>MurmurHash2算法</code></pre></li><li><p>冲突解决</p><pre><code>链地址法，可参考java hashmap的实现</code></pre></li><li><p>rehash的过程</p><p>扩展和收缩哈希表的工作可以通过执行rehash（重新散列）操作来完成，Redis对字典的哈希表执行rehash的步骤如下：<br>1）为字典的ht[1]哈希表分配空间，这个哈希表的空间大小取决于要执行的操作，以及ht[0]当前包含的键值对数量（也即是ht[0].used属性的值）：</p><pre><code>❑如果执行的是扩展操作，那么ht[1]的大小为第一个大于等于ht[0].used*2的2 n（2的n次方幂）；❑如果执行的是收缩操作，那么ht[1]的大小为第一个大于等于ht[0].used的2 n。</code></pre><p>2）将保存在ht[0]中的所有键值对rehash到ht[1]上面：rehash指的是重新计算键的哈希值和索引值，然后将键值对放置到ht[1]哈希表的指定位置上。<br>3）当ht[0]包含的所有键值对都迁移到了ht[1]之后（ht[0]变为空表），释放ht[0]，将ht[1]设置为ht[0]，并在ht[1]新创建一个空白哈希表，为下一次rehash做准备。</p></li><li><p>什么时候触发rehash</p><ul><li><p>扩展</p><p>1）服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令，并且哈希表的负载因子大于等于1。</p><p>2）服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令，并且哈希表的负载因子大于等于5。</p></li></ul><ul><li>收缩<br>当哈希表的负载因子小于0.1时，程序自动开始对哈希表执行收缩操作</li></ul></li><li><p>渐进式rehash步骤</p><p>为了避免rehash对服务器性能造成影响，服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1]，而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。</p><p>1）为ht[1]分配空间，让字典同时持有ht[0]和ht[1]两个哈希表。</p><p>2）在字典中维持一个索引计数器变量rehashidx，并将它的值设置为0，表示rehash工作正式开始。</p><p>3）在rehash进行期间，每次对字典执行添加、删除、查找或者更新操作时，程序除了执行指定的操作以外，还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]，当rehash工作完成之后，程序将rehashidx属性的值增一。</p><p>4）随着字典操作的不断执行，最终在某个时间点上，ht[0]的所有键值对都会被rehash至ht[1]，这时程序将rehashidx属性的值设为-1，表示rehash操作已完成。</p><p>渐进式rehash的好处在于它采取分而治之的方式，将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上，从而避免了集中式rehash而带来的庞大计算量。</p></li><li><p>怎么在rehash过程中的增删改查？</p><p>因为在进行渐进式rehash的过程中，字典会同时使用ht[0]和ht[1]两个哈希表，所以在渐进式rehash进行期间，字典的删除（delete）、查找（find）、更新（update）等操作会在两个哈希表上进行。例如，要在字典里面查找一个键的话，程序会先在ht[0]里面进行查找，如果没找到的话，就会继续到ht[1]里面进行查找，诸如此类。</p><p>另外，在渐进式rehash执行期间，新添加到字典的键值对一律会被保存到ht[1]里面，而ht[0]则不再进行任何添加操作，这一措施保证了ht[0]包含的键值对数量会只减不增，并随着rehash操作的执行而最终变成空表。</p></li></ul><h2 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h2><p>​        有序集合底层实现<br>​        java 代码实现：<a href="https://juejin.cn/post/6844903847521976327" target="_blank" rel="noopener">https://juejin.cn/post/6844903847521976327</a><br>​        数据结构</p><p>​        <img src="/images/image-20220327200520224.png" alt="image-20220327200520224" style="zoom:30%;"></p>"<h2 id="整数集合"><a href="#整数集合" class="headerlink" title="整数集合"></a>整数集合</h2><p>​        是集合键的实现之一<br>​        数据结构</p><p><img src="/images/image-20220327200658260.png" alt="image-20220327200658260" style="zoom:30%;"></p>"<h2 id="压缩列表"><a href="#压缩列表" class="headerlink" title="压缩列表"></a>压缩列表</h2><p>压缩列表（ziplist）是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项，并且每个列表项要么就是小整数值，要么就是长度比较短的字符串，那么Redis就会使用压缩列表来做列表键的底层实现</p><ul><li><p>数据结构</p><p><img src="/images/image-20220327200900372.png" alt="image-20220327200900372" style="zoom:33%;">            </p>"</li></ul><h2 id="对象系统"><a href="#对象系统" class="headerlink" title="对象系统"></a>对象系统</h2><h3 id="类型与编码"><a href="#类型与编码" class="headerlink" title="类型与编码"></a>类型与编码</h3><ul><li><p>结构体</p><p><img src="/images/image-20220327203439662.png" alt="image-20220327203439662" style="zoom: 33%;"></p>"</li><li><p>类型</p><p><img src="/images/image-20220327203538383.png" alt="image-20220327203538383" style="zoom:33%;"></p>"</li><li><p>编码</p><p><img src="/images/image-20220327203639653.png" alt="image-20220327203639653" style="zoom:33%;"></p>"</li></ul><h3 id="内存回收"><a href="#内存回收" class="headerlink" title="内存回收"></a>内存回收</h3><p>因为C语言并不具备自动内存回收功能，所以Redis在自己的对象系统中构建了一个引用计数（reference counting）技术实现的内存回收机制，通过这一机制，程序可以通过跟踪对象的引用计数信息，在适当的时候自动释放对象并进行内存回收。</p><p><img src="/images/image-20220327204211701.png" alt="image-20220327204211701" style="zoom:33%;"></p>"<h3 id="对象共享"><a href="#对象共享" class="headerlink" title="对象共享"></a>对象共享</h3><p><img src="/images/image-20220327204051657.png" alt="image-20220327204051657" style="zoom:33%;"></p>"<p>在Redis中，让多个键共享同一个值对象需要执行以下两个步骤：<br>    1）将数据库键的值指针指向一个现有的值对象；<br>    2）将被共享的值对象的引用计数增一。<br>Redis会在初始化服务器时，创建一万个字符串对象，这些对象包含了从0到9999的所有整数值，当服务器需要用到值为0到9999的字符串对象时，服务器就会使用这些共享对象，而不是新创建对象<br>这些共享对象不单单只有字符串键可以使用，那些在数据结构中嵌套了字符串对象的对象（linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象，以及zset编码的有序集合对象)都可以使用这些共享对象。<br>Redis只对包含整数值的字符串对象进行共享</p><h3 id="空转时长"><a href="#空转时长" class="headerlink" title="空转时长"></a>空转时长</h3><p>redisObject结构包含的最后一个属性为lru属性，该属性记录了对象最后一次被命令程序访问的时间</p><h1 id="单机数据库"><a href="#单机数据库" class="headerlink" title="单机数据库"></a>单机数据库</h1><h2 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h2><ul><li><p>数据结构</p><pre><code>&lt;img src=&quot;images/image-20220327212351861.png&quot; alt=&quot;image-20220327212351861&quot; style=&quot;zoom:33%;&quot; /&gt;</code></pre></li><li><p>键空间</p><p>redisDb结构的dict字典保存了数据库中的所有键值对，我们将这个字典称为键空间（key space）</p><p><img src="/images/image-20220327212614427.png" alt="image-20220327212614427" style="zoom:30%;"></p>"</li><li><p>读写键空间时的维护操作</p><p>当使用Redis命令对数据库进行读写时，服务器不仅会对键空间执行指定的读写操作，还会执行一些额外的维护操作</p><p>1.在读取一个键之后，服务器会更新键的LRU（最后一次使用）时间，这个值可以用于计算键的闲置时间，使用OBJECT idletime命令可以查看键key的闲置时间。</p><p>2.如果服务器在读取一个键时发现该键已经过期，那么服务器会先删除这个过期键，然后才执行余下的其他操作，本章稍后对过期键的讨论会详细说明这一点。</p><p>3.如果有客户端使用WATCH命令监视了某个键，那么服务器在对被监视的键进行修改之后，会将这个键标记为脏（dirty），从而让事务程序注意到这个键已经被修改过，第19章会详细说明这一点。</p><p>4.服务器每次修改一个键之后，都会对脏（dirty）键计数器的值增1，这个计数器会触发服务器的持久化以及复制操作，第10章、第11章和第15章都会说到这一点。</p></li></ul><h3 id="键的生存时间或过期时间"><a href="#键的生存时间或过期时间" class="headerlink" title="键的生存时间或过期时间"></a>键的生存时间或过期时间</h3><h4 id="过期键的保存"><a href="#过期键的保存" class="headerlink" title="过期键的保存"></a>过期键的保存</h4><p>redisDb结构的expires字典保存了数据库中所有键的过期时间，我们称这个字典为过期字典</p><h4 id="过期键删除策略：惰性删除-定期删除"><a href="#过期键删除策略：惰性删除-定期删除" class="headerlink" title="过期键删除策略：惰性删除+定期删除"></a>过期键删除策略：惰性删除+定期删除</h4><ul><li><p>惰性删除<br>过期键的惰性删除策略由db.c/expireIfNeeded函数实现，所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查</p><p><img src="/images/image-20220327232346093.png" alt="image-20220327232346093" style="zoom:33%;">                      </p>"</li><li><p>定期删除</p><pre><code>每当Redis的服务器周期性操作redis.c/serverCron函数执行时，activeExpireCycle函数就会被调用，它在规定的时间内，分多次遍历服务器中的各个数据库，从数据库的expires字典中随机检查一部分键的过期时间，并删除其中的过期键。</code></pre></li></ul><h4 id="AOF，RDB和复制功能对过期键的处理"><a href="#AOF，RDB和复制功能对过期键的处理" class="headerlink" title="AOF，RDB和复制功能对过期键的处理"></a>AOF，RDB和复制功能对过期键的处理</h4><ul><li><p>RDB</p><p>在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时，程序会对数据库中的键进行检查，已过期的键不会被保存到新创建的RDB文件中。</p><ul><li>载入rdb文件<ol><li>如果服务器以主服务器模式运行，那么在载入RDB文件时，程序会对文件中保存的键进行检查，未过期的键会被载入到数据库中，而过期键则会被忽略，所以过期键对载入RDB文件的主服务器不会造成影响。</li><li>如果服务器以从服务器模式运行，那么在载入RDB文件时，文件中保存的所有键，不论是否过期，都会被载入到数据库中。不过，因为主从服务器在进行数据同步的时候，从服务器的数据库就会被清空，所以一般来讲，过期键对载入RDB文件的从服务器也不会造成影响</li></ol></li></ul></li><li><p>AOF<br>当服务器以AOF持久化模式运行时，如果数据库中的某个键已经过期，但它还没有被惰性删除或者定期删除，那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后，程序会向AOF文件追加（append）一条DEL命令，来显式地记录该键已被删除。<br>和生成RDB文件时类似，在执行AOF重写的过程中，程序会对数据库中的键进行检查，已过期的键不会被保存到重写后的AOF文件中。</p></li><li><p>复制</p><p>主服务器在删除一个过期键之后，会显式地向所有从服务器发送一个DEL命令，告知从服务器删除这个过期键。❑从服务器在执行客户端发送的读命令时，即使碰到过期键也不会将过期键删除，而是继续像处理未过期的键一样来处理过期键。❑从服务器只有在接到主服务器发来的DEL命令之后，才会删除过期键。</p></li></ul><h2 id="RDB"><a href="#RDB" class="headerlink" title="RDB"></a>RDB</h2><h3 id="创建与载入"><a href="#创建与载入" class="headerlink" title="创建与载入"></a>创建与载入</h3><ul><li>持久化<br>RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件，通过该文件可以还原生成RDB文件时的数据库状态</li><li>创建命令<ul><li>SAVE，BGSAVE<br>当执行save命令时，redis服务器会被阻塞，所有请求命令都会被拒绝<br>bgsave命令由子进程执行，不阻塞服务器<br>save,bgsave,bgrewriteaof不能同时执行</li></ul></li><li>载入流程<br>载入rdb文件期间，服务器会一致处于阻塞状态</li><li>自动间隔性保存<br>Redis允许用户通过设置服务器配置的save选项，让服务器每隔一段时间自动执行一次BGSAVE命令。<ul><li>计数<br>1.dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后，服务器对数据库状态（服务器中的所有数据库）进行了多少次修改（包括写入、删除、更新等操作）。<br>2.lastsave属性是一个UNIX时间戳，记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。</li><li>检查保存条件是否满足<br>每次执行完save，dirty就会被清0<br>Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次，该函数用于对正在运行的服务器进行维护，它的其中一项工作就是检查save选项所设置的保存条件是否已经满足，如果满足的话，就执行BGSAVE命令。</li></ul></li></ul><h2 id="AOF"><a href="#AOF" class="headerlink" title="AOF"></a>AOF</h2><ul><li>AOF与RDB区别<br>与RDB持久化通过保存数据库中的键值对来记录数据库状态不同，AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的</li></ul><h3 id="持久化的实现"><a href="#持久化的实现" class="headerlink" title="持久化的实现"></a>持久化的实现</h3><ul><li><p>命令追加<br>当服务器执行完命令后，会把协议内容添加到aof_buf缓冲区的末尾</p></li><li><p>文件写入与同步<br>flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定，appendfsync选项的默认值为everysec</p></li><li><p>写入与同步的区别<br> 写入是指写入内存缓冲区，同步是指同缓冲区写入到磁盘</p></li><li><p>载入与数据还原</p><p><img src="/images/image-20220328145953801.png" alt="image-20220328145953801" style="zoom:33%;"></p>"</li></ul><h3 id="AOF重写"><a href="#AOF重写" class="headerlink" title="AOF重写"></a>AOF重写</h3><p>为了解决aof文件体积膨胀的问题</p><p>创建一个新的AOF文件来替代现有的AOF文件，新旧两个AOF文件所保存的数据库状态相同，但新AOF文件不会包含任何浪费空间的冗余命令，所以新AOF文件的体积通常会比旧AOF文件的体积要小得多</p><h3 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h3><p>首先从数据库中读取键现在的值，然后用一条命令去记录键值对，代替之前记录这个键值对的多条命令，这就是AOF重写功能的实现原理。（不需要分析现有的AOF文件）<br>在实际中，为了避免在执行命令时造成客户端输入缓冲区溢出，重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时，会先检查键所包含的元素数量，如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值，那么重写程序将使用多条命令来记录键的值，而不单单使用一条命令。</p><h3 id="后台重写"><a href="#后台重写" class="headerlink" title="后台重写"></a>后台重写</h3><p>子进程进行AOF重写期间，服务器进程（父进程）可以继续处理命令请求</p><h4 id="服务器进程和重写子进程同时运行，怎么保证数据一致？"><a href="#服务器进程和重写子进程同时运行，怎么保证数据一致？" class="headerlink" title="服务器进程和重写子进程同时运行，怎么保证数据一致？"></a>服务器进程和重写子进程同时运行，怎么保证数据一致？</h4><p><img src="/images/image-20220328151858653.png" alt="image-20220328151858653" style="zoom:33%;"></p>"<p>在子进程执行AOF重写期间，服务器进程需要执行以下三个工作：<br>    ​    1）执行客户端发来的命令。<br>    ​    2）将执行后的写命令追加到AOF缓冲区。<br>    ​    3）将执行后的写命令追加到AOF重写缓冲区。<br>这样一来可以保证：</p><ol><li><p>AOF缓冲区的内容会定期被写入和同步到AOF文件，对现有AOF文件的处理工作会如常进行。</p></li><li><p>从创建子进程开始，服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。</p><pre><code>当子进程完成AOF重写工作之后，它会向父进程发送一个信号，父进程在接到该信号之后，会调用一个信号处理函数，并执行以下工作：</code></pre><p> 1）将AOF重写缓冲区中的所有内容写入到新AOF文件中，这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。<br> 2）对新的AOF文件进行改名，原子地（atomic）覆盖现有的AOF文件，完成新旧两个AOF文件的替换。</p></li></ol><h2 id="服务器"><a href="#服务器" class="headerlink" title="服务器"></a>服务器</h2><p>还原数据库状态</p><ol><li>载入RDB文件或者AOF文件</li><li>如果启用了AOF持久化功能，使用AOF文件恢复数据库状态</li><li>如果没启用，使用RDB文件恢复</li></ol><h1 id="多机数据库"><a href="#多机数据库" class="headerlink" title="多机数据库"></a>多机数据库</h1><h2 id="复制"><a href="#复制" class="headerlink" title="复制"></a>复制</h2><p>命令：slaveof master_ip master_port</p><h3 id="旧版功能的实现"><a href="#旧版功能的实现" class="headerlink" title="旧版功能的实现"></a>旧版功能的实现</h3><p>2大步骤：同步（sync）和命令传播（command propagate）两个操作</p><ol><li>同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。</li><li>命令传播操作则用于在主服务器的数据库状态被修改， 导致主从服务器的数据库状态出现不一致时， 让主从服务器的数据库重新回到一致状态。</li></ol><p><img src="/images/image-20220328181413466.png" alt="image-20220328181413466" style="zoom:33%;"></p>"<h4 id="同步"><a href="#同步" class="headerlink" title="同步"></a>同步</h4><p><img src="/images/image-20220328165530576.png" alt="image-20220328165530576" style="zoom:33%;"></p>"<ul><li><p>触发时机<br>当客户端向从服务器发送 SLAVEOF 命令， 要求从服务器复制主服务器时</p></li><li><p>具体步骤</p><ol><li>从服务器向主服务器发送 SYNC 命令。</li><li>收到 SYNC 命令的主服务器执行 BGSAVE 命令， 在后台生成一个 RDB 文件， 并使用一个缓冲区记录从现在开始执行的所有写命令。</li><li>当主服务器的 BGSAVE 命令执行完毕时， 主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器， 从服务器接收并载入这个 RDB 文件， 将自己的数据库状态更新至主服务器执行 BGSAVE 命令时的数据库状态。</li><li>主服务器将记录在缓冲区里面的所有写命令发送给从服务器， 从服务器执行这些写命令， 将自己的数据库状态更新至主服务器数据库当前所处的状态。</li></ol></li></ul><h4 id="命令传播"><a href="#命令传播" class="headerlink" title="命令传播"></a>命令传播</h4><ul><li><p>为什么要命令传播<br>  每当主服务器执行客户端发送的写命令时， 主服务器的数据库就有可能会被修改， 并导致主从服务器状态不再一致</p></li><li><p>怎么做<br>  主服务器会将自己执行的写命令 —— 也即是造成主从服务器不一致的那条写命令 —— 发送给从服务器执行， 当从服务器执行了相同的写命令之后， 主从服务器将再次回到一致状态。</p></li></ul><h4 id="缺陷"><a href="#缺陷" class="headerlink" title="缺陷"></a>缺陷</h4><ul><li>断线后复制效率低<br>断线恢复连接后：从库向主库发送SYNC命令，执行同步操作</li></ul><h3 id="新版功能的实现"><a href="#新版功能的实现" class="headerlink" title="新版功能的实现"></a>新版功能的实现</h3><ul><li><p>相对老版本的优化点</p><p>使用PSYNC代替SYNC，PSYNC在断线后复制时，可以仅同步断线后主服务器执行的命令</p></li></ul><h4 id="PSYNC"><a href="#PSYNC" class="headerlink" title="PSYNC"></a>PSYNC</h4><ul><li><p>PSYNC命令具有完整重同步（full resynchronization）和部分重同步（partial resynchronization）两种模式：\</p><ul><li>其中完整重同步用于处理初次复制情况：完整重同步的执行步骤和SYNC命令的执行步骤基本一样，它们都是通过让主服务器创建并发送RDB文件，以及向从服务器发送保存在缓冲区里面的写命令来进行同步。</li><li>而部分重同步则用于处理断线后重复制情况：当从服务器在断线后重新连接主服务器时，如果条件允许，主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器，从服务器只要接收并执行这些写命令，就可以将数据库更新至主服务器当前所处的状态。</li></ul></li><li><p>部分重同步功能由以下三个部分构成：</p><ol><li><p>主服务器的复制偏移量（replication offset）和从服务器的复制偏移量。</p></li><li><p>主服务器的复制积压缓冲区（replication backlog）。</p></li><li><p>服务器的运行ID（run ID）。</p></li></ol></li></ul><h5 id="复制偏移量"><a href="#复制偏移量" class="headerlink" title="复制偏移量"></a>复制偏移量</h5><ul><li><p>执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量：</p><pre><code>1.主服务器每次向从服务器传播N个字节的数据时，就将自己的复制偏移量的值加上N。2.从服务器每次收到主服务器传播来的N个字节的数据时，就将自己的复制偏移量的值加上N。</code></pre></li><li><p>通过对比主从服务器的复制偏移量，程序可以很容易地知道主从服务器是否处于一致状态：</p><ol><li><p>如果主从服务器处于一致状态，那么主从服务器两者的偏移量总是相同的。</p></li><li><p>相反，如果主从服务器两者的偏移量并不相同，那么说明主从服务器并未处于一致状态</p></li></ol></li></ul><h4 id="复制积压缓冲区"><a href="#复制积压缓冲区" class="headerlink" title="复制积压缓冲区"></a>复制积压缓冲区</h4><ul><li><p>主服务器维护的一个固定长度，先进先出的队列，默认大小1M</p></li><li><p>同步记录时，也会记录在复制积压区</p><p><img src="/images/image-20220328171446304.png" alt="image-20220328171446304" style="zoom:33%;"></p>"<p><img src="/images/image-20220328171505476.png" alt="image-20220328171505476" style="zoom:33%;"></p>"</li><li><p>如何利用积压区进行断线后恢复数据<br>当从服务器重新连上主服务器时，从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器，主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作： </p><ol><li><p>如果offset偏移量之后的数据（也即是偏移量offset+1开始的数据）仍然存在于复制积压缓冲区里面，那么主服务器将对从服务器执行部分重同步操作。</p></li><li><p>相反，如果offset偏移量之后的数据已经不存在于复制积压缓冲区，那么主服务器将对从服务器执行完整重同步操作。</p></li></ol></li></ul><h4 id="服务器运行ID"><a href="#服务器运行ID" class="headerlink" title="服务器运行ID"></a>服务器运行ID</h4><p>除了复制偏移量和复制积压缓冲区之外，实现部分重同步还需要用到服务器运行ID（run ID）：</p><ol><li><p>每个Redis服务器，不论主服务器还是从服务，都会有自己的运行ID。</p></li><li><p>运行ID在服务器启动时自动生成，由40个随机的十六进制字符组成，例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3。</p></li></ol><p>   怎么用</p><p>   当从服务器对主服务器进行初次复制时，主服务器会将自己的运行ID传送给从服务器，而从服务器则会将这个运行ID保存起来。<br>   当从服务器断线并重新连上一个主服务器时，从服务器将向当前连接的主服务器发送之前保存的运行ID：<br>   1.如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同，那么说明从服务器断线之前复制的就是当前连接的这个主服务器，主服务器可以继续尝试执行部分重同步操作。<br>   2.相反地，如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同，那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器，主服务器将对从服务器执行完整重同步操作。</p><h3 id="复制的实现流程"><a href="#复制的实现流程" class="headerlink" title="复制的实现流程"></a>复制的实现流程</h3><ol><li><p>设置主服务器的地址和端口 slaveof &lt;master_ip&gt; &lt;master_port&gt;</p></li><li><p>建立套接字<br> 如果从服务器创建的套接字能成功连接（connect）到主服务器，那么从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器，这个处理器将负责执行后续的复制工作</p></li><li><p>发送PING命令</p></li><li><p>身份验证</p></li><li><p>发送端口信息<br> 主服务器把端口信息记录在从服务器对应的客户端状态的slave_listening_port属性中<br> 从服务器也是一种客户端<br> slave_listening_port属性的唯一作用是：执行INFO replication命令时打印出从服务器的端口号</p></li><li><p>同步</p></li><li><p>命令传播</p></li></ol><h3 id="心跳检测"><a href="#心跳检测" class="headerlink" title="心跳检测"></a>心跳检测</h3><ul><li><p>怎么心跳检测<br>命令传播阶段，从服务器默认每秒向主服务器发送命令：REPLCONF ACK &lt;replication_offset&gt;</p></li><li><p>作用是什么</p><ul><li><p>辅助实现min-slaves</p></li><li><p>检测命令丢失<br>如果因为网络故障，主服务器传播给从服务器的写命令在半路丢失，那么当从服务器向主服务器发送REPLCONF ACK命令时，<br>主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量，然后主服务器就会根据从服务器提交的复制偏移量，<br>在复制积压缓冲区里面找到从服务器缺少的数据，并将这些数据重新发送给从服务器。</p></li></ul></li></ul><h2 id="sentinel"><a href="#sentinel" class="headerlink" title="sentinel"></a>sentinel</h2><ul><li>是什么<br>Sentinel（哨岗、哨兵）是Redis的高可用性（high availability）解决方案：由一个或多个Sentinel实例（instance）组成的Sentinel系统（system）可以监视任意多个主服务器，以及这些主服务器属下的所有从服务器，并在被监视的主服务器进入下线状态时，自动将下线主服务器属下的某个从服务器升级为新的主服务器，然后由新的主服务器代替已下线的主服务器继续处理命令请求。</li></ul><h3 id="故障转移的整个过程"><a href="#故障转移的整个过程" class="headerlink" title="故障转移的整个过程"></a>故障转移的整个过程</h3><h4 id="启动并初始化sentinel"><a href="#启动并初始化sentinel" class="headerlink" title="启动并初始化sentinel"></a>启动并初始化sentinel</h4><p>1）初始化服务器。</p><p>2）将普通Redis服务器使用的代码替换成Sentinel专用代码。</p><p>3）初始化Sentinel状态。</p><p>4）根据给定的配置文件，初始化Sentinel的监视主服务器列表。</p><p>5）创建连向主服务器的网络连接。</p><p>Sentinel本质上只是一个运行在特殊模式下的Redis服务器</p><ul><li><p>数据结构</p><p><img src="/images/image-20220328204312385.png" alt="image-20220328204312385" style="zoom:33%;"></p>"<p><img src="/images/image-20220328204341504.png" alt="image-20220328204341504" style="zoom:33%;"></p>"</li><li><p>创建与主服务器的连接<br>Sentinel会创建两个连向主服务器的异步网络连接：</p><ul><li><p>一个是命令连接，这个连接专门用于向主服务器发送命令，并接收命令回复。</p></li><li><p>另一个是订阅连接，这个连接专门用于订阅主服务器的<strong>sentinel</strong>:hello频道。<br>为什么是2个</p><p><img src="/images/image-20220328205719679.png" alt="image-20220328205719679" style="zoom:33%;">        </p>"</li></ul></li></ul><h4 id="获取主服务器的信息"><a href="#获取主服务器的信息" class="headerlink" title="获取主服务器的信息"></a>获取主服务器的信息</h4><ul><li><p>Sentinel默认会以每十秒一次的频率，通过命令连接向被监视的主服务器发送INFO命令，并通过分析INFO命令的回复来获取主服务器的当前信息。</p></li><li><p>通过分析主服务器返回的INFO命令回复，Sentinel可以获取以下两方面的信息：</p><ul><li>一方面是关于主服务器本身的信息，包括run_id域记录的服务器运行ID，以及role域记录的服务器角色；</li><li>另一方面是关于主服务器属下所有从服务器的信息，每个从服务器都由一个”slave”字符串开头的行记录，每行的ip=域记录了从服务器的IP地址，而port=域则记录了从服务器的端口号。根据这些IP地址和端口号<br> Sentinel无须用户提供从服务器的地址信息，就可以自动发现从服务器</li></ul><p><img src="/images/image-20220328210037050.png" alt="image-20220328210037050" style="zoom:33%;"></p>"</li></ul><h4 id="获取从服务器的信息"><a href="#获取从服务器的信息" class="headerlink" title="获取从服务器的信息"></a>获取从服务器的信息</h4><ul><li><p>sentinel发现从服务器时，会建立命令连接和订阅连接</p><p><img src="/images/image-20220328210311702.png" alt="image-20220328210311702" style="zoom:25%;"></p>"</li><li><p>在创建命令连接之后，Sentinel在默认情况下，会以每十秒一次的频率通过命令连接向从服务器发送INFO命令</p></li></ul><h4 id="向主服务器和从服务器发消息"><a href="#向主服务器和从服务器发消息" class="headerlink" title="向主服务器和从服务器发消息"></a>向主服务器和从服务器发消息</h4><p>Sentinel会以每两秒一次的频率，通过命令连接向所有被监视的主服务器和从服务器发送命令</p><p><img src="/images/image-20220328210346132.png" alt="image-20220328210346132" style="zoom:33%;">                    </p>"<p>以s_开头的参数记录的是Sentinel本身的信息，m_开头的参数记录的则是主服务器的信息</p><h4 id="接收来自主服务器和从服务器的频道信息"><a href="#接收来自主服务器和从服务器的频道信息" class="headerlink" title="接收来自主服务器和从服务器的频道信息"></a>接收来自主服务器和从服务器的频道信息</h4><ol><li><p>对于每个与Sentinel连接的服务器，Sentinel既通过命令连接向服务器的<strong>sentinel</strong>:hello频道发送信息，又通过订阅连接从服务器的<strong>sentinel</strong>:hello频道接收信息</p></li><li><p>对于监视同一个服务器的多个Sentinel来说，一个Sentinel发送的信息会被其他Sentinel接收到，这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知，也会被用于更新其他Sentinel对被监视服务器的认知。</p></li><li><p>更新sentinels字典<br>Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外，所有同样监视这个主服务器的其他Sentinel的资料</p><p><img src="/images/image-20220328211612900.png" alt="image-20220328211612900" style="zoom:33%;"></p>"</li><li><p>创建连向其他Sentinel的命令连接</p><ul><li>当Sentinel通过频道信息发现一个新的Sentinel时，它不仅会为新Sentinel在sentinels字典中创建相应的实例结构，还会创建一个连向新Sentinel的命令连接，而新Sentinel也同样会创建连向这个Sentinel的命令连接</li><li>Sentinel之间不会创建订阅连接</li></ul></li></ol><h4 id="主观下线"><a href="#主观下线" class="headerlink" title="主观下线"></a>主观下线</h4><ol><li><p>在默认情况下，Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例（包括主服务器、从服务器、其他Sentinel在内)发送PING命令，并通过实例返回的PING命令回复来判断实例是否在线。</p></li><li><p>回复</p><ul><li><p>有效回复：实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。</p></li><li><p>无效回复：实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复，或者在指定时限内没有返回任何回复</p></li><li><p>Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度：如果一个实例在down-after-milliseconds毫秒内，连续向Sentinel返回无效回复，那么Sentinel会修改这个实例所对应的实例结构，在结构的flags属性中打开SRI_S_DOWN标识，以此来表示这个实例已经进入主观下线状态</p></li><li><p>多个Sentinel设置的主观下线时长可能不同</p></li></ul></li></ol><h4 id="检查客观下线状态"><a href="#检查客观下线状态" class="headerlink" title="检查客观下线状态"></a>检查客观下线状态</h4><ol><li><p>当Sentinel将一个主服务器判断为主观下线之后，为了确认这个主服务器是否真的下线了，它会向同样监视这一主服务器的其他Sentinel进行询问，看它们是否也认为主服务器已经进入了下线状态（可以是主观下线或者客观下线）。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后，Sentinel就会将从服务器判定为客观下线，并对主服务器执行故障转移操作。</p></li><li><p>命令询问其他Sentinel是否同意主服务器已下线</p></li><li><p>根据其他Sentinel发回的SENTINEL is-master-down-by-addr命令回复，Sentinel将统计其他Sentinel同意主服务器已下线的数量，当这一数量达到配置指定的判断客观下线所需的数量时，Sentinel会将主服务器实例结构flags属性的SRI_O_DOWN标识打开，表示主服务器已经进入客观下线状态</p></li></ol><h4 id="选举领头Sentinel"><a href="#选举领头Sentinel" class="headerlink" title="选举领头Sentinel"></a>选举领头Sentinel</h4><p>当一个主服务器被判断为客观下线时，监视这个下线主服务器的各个Sentinel会进行协商，选举出一个领头Sentinel，并由领头Sentinel对下线主服务器执行故障转移操作。<br>选举算法是对Raft算法的领头选举方法的实现</p><h4 id="故障转移"><a href="#故障转移" class="headerlink" title="故障转移"></a>故障转移</h4><ul><li><p>3个步骤<br>1）在已下线主服务器属下的所有从服务器里面，挑选出一个从服务器，并将其转换为主服务器。<br>2）让已下线主服务器属下的所有从服务器改为复制新的主服务器。<br>3）将已下线主服务器设置为新的主服务器的从服务器，当这个旧的主服务器重新上线时，它就会成为新的主服务器的从服务器。</p></li><li><p>怎么选举新的主服务器<br>挑选出一个状态良好、数据完整的从服务器，然后向这个从服务器发送SLAVEOF no one命令，将这个从服务器转换为主服务器。</p><p>如果有多个具有相同最高优先级的从服务器，那么领头Sentinel将按照从服务器的复制偏移量，对具有相同最高优先级的所有从服务器进行排序，并选出其中偏移量最大的从服务器（复制偏移量最大的从服务器就是保存着最新数据的从服务器）。最后，如果有多个优先级最高、复制偏移量最大的从服务器，那么领头Sentinel将按照运行ID对这些从服务器进行排序，并选出其中运行ID最小的从服务器。</p></li></ul><h2 id="集群"><a href="#集群" class="headerlink" title="集群"></a>集群</h2><p>Redis集群是Redis提供的分布式数据库方案，集群通过分片（sharding）来进行数据共享，并提供复制和故障转移功能。</p><h3 id="节点"><a href="#节点" class="headerlink" title="节点"></a>节点</h3><ul><li><p>怎么加入集群</p><p><img src="/images/image-20220328222139682.png" alt="image-20220328222139682" style="zoom: 50%;"></p>"<p>向一个节点node发送CLUSTER MEET命令，可以让node节点与ip和port所指定的节点进行握手（handshake），当握手成功时，node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中</p></li><li><p>节点启动<br>一个节点就是一个运行在集群模式下的Redis服务器，Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式</p><p><img src="/images/image-20220328222454024.png" alt="image-20220328222454024" style="zoom:33%;"></p>"</li><li><p>集群数据结构</p><p>每个节点都会使用一个clusterNode结构来记录自己的状态，并为集群中的所有其他节点（包括主节点和从节点)都创建一个相应的clusterNode结构，以此来记录其他节点的状态<br>每个节点都保存着一个clusterState结构，这个结构记录了在当前节点的视角下，集群目前所处的状态<br><img src="/images/image-20220328222804037.png" alt="image-20220328222804037" style="zoom: 33%;"></p>"</li><li><p>CLUSTER MEET命令的实现</p><p><img src="/images/image-20220328222848607.png" alt="image-20220328222848607" style="zoom:33%;"></p>"</li></ul><h3 id="槽指派"><a href="#槽指派" class="headerlink" title="槽指派"></a>槽指派</h3><ul><li><p>Redis集群通过分片的方式来保存数据库中的键值对：集群的整个数据库被分为16384个槽（slot），数据库中的每个键都属于这16384个槽的其中一个，集群中的每个节点可以处理0个或最多16384个槽。</p></li><li><p>cluster addslots<br>通过向节点发送CLUSTER ADDSLOTS命令，我们可以将一个或多个槽指派（assign）给节点负责</p></li><li><p>记录节点的槽指派信息</p><ol><li>clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽</li></ol><p><img src="/images/image-20220328223145203.png" alt="image-20220328223145203" style="zoom:33%;"></p>"<ol start="2"><li><p><img src="/images/image-20220328223236704.png" alt="image-20220328223236704" style="zoom:33%;"></p>"<p>1表示负责处理</p></li></ol></li><li><p>传播节点的槽指派信息<br>节点也会将自己的slots数组通过消息发送给集群中的其他节点，以此来告知其他节点自己目前负责处理哪些槽。<br>因为集群中的每个节点都会将自己的slots数组通过消息发送给集群中的其他节点，并且每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面，因此，集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。</p></li><li><p>记录集群所有槽的指派信息</p><p>clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息</p><p><img src="/images/image-20220328223529391.png" alt="image-20220328223529391" style="zoom:33%;">                    </p>"</li></ul><h3 id="在集群中执行命令"><a href="#在集群中执行命令" class="headerlink" title="在集群中执行命令"></a>在集群中执行命令</h3><p><img src="/images/image-20220328223919377.png" alt="image-20220328223919377" style="zoom:33%;"></p>"<ol><li><p>计算键属于哪个槽</p><p><img src="/images/image-20220328224004552.png" alt="image-20220328224004552" style="zoom:33%;"></p>"</li><li><p>判断槽是否由当前节点负责处理<br>当节点计算出键所属的槽i之后，节点就会检查自己在clusterState.slots数组中的项i，判断键所在的槽是否由自己负责</p></li><li><p>MOVED错误<br>当节点发现键所在的槽并非由自己负责处理的时候，节点就会向客户端返回一个MOVED错误，指引客户端转向至正在负责槽的节点。</p></li></ol><h3 id="重新分片"><a href="#重新分片" class="headerlink" title="重新分片"></a>重新分片</h3><ol><li><p>重新分片操作可以在线（online）进行，在重新分片的过程中，集群不需要下线，并且源节点和目标节点都可以继续处理命令请求。</p></li><li><p>槽迁移的过程</p><p><img src="/images/image-20220328230826290.png" alt="image-20220328230826290" style="zoom:33%;"></p>"</li></ol><h3 id="ASK错误"><a href="#ASK错误" class="headerlink" title="ASK错误"></a>ASK错误</h3><ul><li><p>属于被迁移槽的一部分键值对保存在源节点里面，而另一部分键值对则保存在目标节点里面。</p><p>   <img src="/images/image-20220330222326638.png" alt="image-20220330222326638" style="zoom:33%;"></p>"</li><li><p>CLUSTER SETSLOT IMPORTING命令的实现</p><pre><code>clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽</code></pre></li></ul><pre><code>&lt;img src=&quot;images/image-20220330222610460.png&quot; alt=&quot;image-20220330222610460&quot; style=&quot;zoom:33%;&quot; /&gt;</code></pre><ul><li>CLUSTER SETSLOT MIGRATING命令的实现<pre><code>clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽</code></pre></li></ul><pre><code>&lt;img src=&quot;images/image-20220330222636281.png&quot; alt=&quot;image-20220330222636281&quot; style=&quot;zoom:33%;&quot; /&gt;</code></pre><ul><li><p>ASK错误</p><pre><code>如果节点没有在自己的数据库里找到键key，那么节点会检查自己的clusterState.migrating_slots_to[i]，看键key所属的槽i是否正在进行迁移，如果槽i的确在进行迁移的话，那么节点会向客户端发送一个ASK错误，引导客户端到正在导入槽i的节点去查找键key</code></pre></li><li><p>ASKING命令</p><p>   在一般情况下，如果客户端向节点发送一个关于槽i的命令，而槽i又没有指派给这个节点的话，那么节点将向客户端返回一个MOVED错误；但是，如果节点的clusterState.importing_slots_from[i]显示节点正在导入槽i，并且发送命令的客户端带有REDIS_ASKING标识，那么节点将破例执行这个关于槽i的命令一次</p><p>   <img src="/images/image-20220330222810822.png" alt="image-20220330222810822" style="zoom:33%;"></p>"</li></ul><h3 id="复制与故障转移"><a href="#复制与故障转移" class="headerlink" title="复制与故障转移"></a>复制与故障转移</h3><p>Redis集群中的节点分为主节点（master）和从节点（slave），其中主节点用于处理槽，而从节点则用于复制某个主节点，并在被复制的主节点下线时，代替下线主节点继续处理命令请求。</p><ul><li><p>设置从节点</p><p><img src="/images/image-20220330223354761.png" alt="image-20220330223354761" style="zoom:50%;"><br><img src="images/image-20220330223410477.png" alt="image-20220330223410477" style="zoom:33%;"><br><img src="images/image-20220330223753669.png" alt="image-20220330223753669" style="zoom:33%;">            </p>"</li></ul><h4 id="故障检测"><a href="#故障检测" class="headerlink" title="故障检测"></a>故障检测</h4><ol><li><p>集群中的每个节点都会定期地向集群中的其他节点发送PING消息，以此来检测对方是否在线，如果接收PING消息的节点没有在规定的时间内，向发送PING消息的节点返回PONG消息，那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线（probable fail，PFAIL）</p></li><li><p>当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时，主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构，并将主节点B的下线报告（failure report）添加到clusterNode结构的fail_reports链表里面</p><p><img src="/images/image-20220330223915432.png" alt="image-20220330223915432" style="zoom: 25%;"></p>"</li><li><p>如果在一个集群里面，半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线，那么这个主节点x将被标记为已下线（FAIL），将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息，所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。</p></li></ol><h4 id="故障转移-1"><a href="#故障转移-1" class="headerlink" title="故障转移"></a>故障转移</h4><p>当一个从节点发现自己正在复制的主节点进入了已下线状态时，从节点将开始对下线主节点进行故障转移，以下是故障转移的执行步骤：<br>      1）复制下线主节点的所有从节点里面，会有一个从节点被选中。<br>      2）被选中的从节点会执行SLAVEOF no one命令，成为新的主节点。<br>      3）新的主节点会撤销所有对已下线主节点的槽指派，并将这些槽全部指派给自己。<br>      4）新的主节点向集群广播一条PONG消息，这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点，并且这个主节点已经接管了原本由已下线节点负责处理的槽。<br>      5）新的主节点开始接收和自己负责处理的槽有关的命令请求，故障转移完成。</p><h4 id="选举新的主节点"><a href="#选举新的主节点" class="headerlink" title="选举新的主节点"></a>选举新的主节点</h4><p><img src="/images/image-20220415164437638-0012281.png" alt="image-20220415164437638"></p>"<ol><li><p>其他集群节点给从节点投票</p></li><li><p>基于Raft算法的领头选举（leader election)方法来实现的</p></li></ol><h3 id="消息"><a href="#消息" class="headerlink" title="消息"></a>消息</h3><p><img src="/images/image-20220330224909157.png" alt="image-20220330224909157" style="zoom:50%;"></p>"]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;数据结构与对象&quot;&gt;&lt;a href=&quot;#数据结构与对象&quot; class=&quot;headerlink&quot; title=&quot;数据结构与对象&quot;&gt;&lt;/a&gt;数据结构与对象&lt;/h1&gt;&lt;h2 id=&quot;SDS&quot;&gt;&lt;a href=&quot;#SDS&quot; class=&quot;headerlink&quot; title=
      
    
    </summary>
    
      <category term="复习" scheme="http://yoursite.com/categories/%E5%A4%8D%E4%B9%A0/"/>
    
    
      <category term="redis" scheme="http://yoursite.com/tags/redis/"/>
    
  </entry>
  
  <entry>
    <title>一致性模型</title>
    <link href="http://yoursite.com/2021/08/03/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%EF%BC%9A%E4%B8%80%E8%87%B4%E6%80%A7%E6%A8%A1%E5%9E%8B/"/>
    <id>http://yoursite.com/2021/08/03/分布式/分布式系统：一致性模型/</id>
    <published>2021-08-03T09:53:56.024Z</published>
    <updated>2021-08-03T11:18:29.275Z</updated>
    
    <content type="html"><![CDATA[<p>分布式系统中一个重要的问题就是数据复制，数据复制一般是为了增强系统的可用性或提高性能。而实现数据复制的一个主要难题就是保持各个副本的一致性。本文首先讨论数据复制的场景中一致性模型如此重要的原因，然后讨论一致性模型的含义，最后分析常用的一致性模型。</p><h2 id="为什么需要一致性模型"><a href="#为什么需要一致性模型" class="headerlink" title="为什么需要一致性模型"></a>为什么需要一致性模型</h2><p><strong>数据复制主要的目的有两个：可用性和性能。</strong>首先数据复制可以提高系统的可用性。在保持多副本的情况，有一个副本不可用，系统切换到其他副本就会恢复。常用的 MySQL 主备同步方案就是一个典型的例子。另一方面，数据复制能够提供系统的性能。当分布式系统需要在服务器数量和地理区域上进行扩展时，数据复制是一个相当重要的手段。有了多个数据副本，就能将请求分流；在多个区域提供服务时，也能通过就近原则提高客户端访问数据的效率。常用的 CDN 技术就是一个典型的例子。<br>但是数据复制是要付出代价的。<strong>数据复制带来了多副本数据一致性的问题。</strong>一个副本的数据更新之后，其他副本必须要保持同步，否则数据不一致就可能导致业务出现问题。因此，每次更新数据对所有副本进行修改的时间以及方式决定了复制代价的大小。全局同步与性能实际上是矛盾的，而为了提高性能，往往会采用放宽一致性要求的方法。因此，<strong>我们需要用一致性模型来理解和推理在分布式系统中数据复制需要考虑的问题和基本假设。</strong></p><h2 id="什么是一致性模型"><a href="#什么是一致性模型" class="headerlink" title="什么是一致性模型"></a>什么是一致性模型</h2><p>首先我们要定义一下一致性模型的术语：</p><ol><li><strong>数据存储</strong>：在分布式系统中指分布式共享数据库、分布式文件系统等。</li><li><strong>读写操作</strong>：更改数据的操作称为写操作（包括新增、修改、删除），其他操作称为读操作。</li></ol><p>下面是一致性模型的定义：<br><strong>一致性模型本质上是进程与数据存储的约定：如果进程遵循某些规则，那么进程对数据的读写操作都是可预期的。</strong></p><p>上面的定义可能比较抽象，我们用常见的强一致性模型来通俗的解释一下：<strong>在线性一致性模型中，进程对一个数据项的读操作，它期待数据存储返回的是该数据在最后一次写操作之后的结果。</strong>这在单机系统里面很容易实现，在 MySQL 中只要使用加锁读的方式就能保证读取到数据在最后一次写操作之后的结果。但在分布式系统中，因为没有全局时钟，导致要精确定义哪次写操作是最后一次写操作是非常困难的事情，因此产生了一系列的一致性模型。<strong>每种模型都有效限制了在对一个数据项执行读操作所应该返回的值。</strong>举个例子：假设记录值 X 在节点 M 和 N 上都有副本，当客户端 A 修改了副本 M 上 X 的值，一段时间之后，客户端 B 从 N 上读取 X 的值，此时一致性模型会决定客户端 B 是否能够读取到 A 写入的值。</p><p>一致性模型主要可以分为两类：<strong>能够保证所有进程对数据的读写顺序都保持一致</strong>的一致性模型称为<strong>强一致性模型</strong>，而不能保证的一致性模型称为<strong>弱一致性模型</strong>。</p><h2 id="强一致性模型"><a href="#强一致性模型" class="headerlink" title="强一致性模型"></a>强一致性模型</h2><h3 id="线性一致性（Linearizable-Consistency）"><a href="#线性一致性（Linearizable-Consistency）" class="headerlink" title="线性一致性（Linearizable Consistency）"></a>线性一致性（Linearizable Consistency）</h3><p>线性一致性也叫严格一致性（Strict Consistency）或者原子一致性（Atomic Consistency），它的条件是：</p><ol><li><strong>所有进程任何一次读都能读取到某个数据最近的一次写的数据。</strong></li><li><strong>所有进程看到的操作顺序都跟全局时钟下的顺序一致。</strong></li></ol><p>线性一致性是对一致性要求最高的一致性模型，它要求每次写入的值都能够立即被所有进程读取到，就现有技术是不可能实现的。因为它要求所有操作都实时同步，实时同步的前提就是时钟同步。但是在分布式系统中要做到全局完全一致时钟现有技术是做不到的。首先通信是必然有延迟的，一旦有延迟，时钟的同步就没法做到一致。当然不排除以后新的技术能够做到，但目前而言线性一致性是无法实现的。</p><h3 id="顺序一致性（Sequential-Consistency）"><a href="#顺序一致性（Sequential-Consistency）" class="headerlink" title="顺序一致性（Sequential Consistency）"></a>顺序一致性（Sequential Consistency）</h3><p>顺序一致性是 Lamport（1979）在解决多处理器系统共享存储器时首次提出来的。参考我之前写的文章《<a href="https://blog.xiaohansong.com/lamport-logic-clock.html" target="_blank" rel="noopener">分布式系统：Lamport 逻辑时钟</a>》。它的条件是：</p><ol><li><strong>任何一次读写操作都是按照某种特定的顺序。</strong></li><li><strong>所有进程看到的读写操作顺序都保持一致。</strong></li></ol><p>首先我们先来分析一下线性一致性和顺序一致性的相同点在哪里。他们都能够保证所有进程对数据的读写顺序保持一致。线性一致性的实现很简单，就按照全局时钟（可以简单理解为物理时钟）为参考系，所有进程都按照全局时钟的时间戳来区分事件的先后，那么必然所有进程看到的数据读写操作顺序一定是一样的，因为它们的参考系是一样的。而顺序一致性使用的是<a href="https://blog.xiaohansong.com/lamport-logic-clock.html" target="_blank" rel="noopener">逻辑时钟</a>来作为分布式系统中的全局时钟，进而所有进程也有了一个统一的参考系对读写操作进行排序，因此所有进程看到的数据读写操作顺序也是一样的。</p><p>那么线性一致性和顺序一致性的区别在哪里呢？通过上面的分析可以发现，<strong>顺序一致性虽然通过逻辑时钟保证所有进程保持一致的读写操作顺序，但这些读写操作的顺序跟实际上发生的顺序并不一定一致。</strong>而线性一致性是严格保证跟实际发生的顺序一致的。另外，线性一致性还对数据同步的实时性有严格要求，而<strong>顺序一致性并不要求实时同步。</strong></p><h2 id="弱一致性模型"><a href="#弱一致性模型" class="headerlink" title="弱一致性模型"></a>弱一致性模型</h2><h3 id="因果一致性（Causal-Consistency）"><a href="#因果一致性（Causal-Consistency）" class="headerlink" title="因果一致性（Causal Consistency）"></a>因果一致性（Causal Consistency）</h3><p>因果一致性是一种弱化的顺序一致性模型，因为它将具有潜在因果关系的事件和没有因果关系的事件区分开了。那么什么是因果关系？如果事件 B 是由事件 A 引起的或者受事件 A 的影响，那么这两个事件就具有因果关系。<br>举个分布式数据库的示例，假设进程 P1 对数据项 x 进行了写操作，然后进程 P2 先读取了 x，然后对 y 进行了写操作，那么对 x 的读操作和对 y 的写操作就具有潜在的因果关系，因为 y 的计算可能依赖于 P2 读取到 x 的值（也就是 P1 写的值）。<br>另一方面，如果两个进程同时对两个不同的数据项进行写操作，那么这两个事件就不具备因果关系。无因果关系的操作称为并发操作。这里只是简单陈述了一下，深入的分析见我之前写的文章《<a href="https://blog.xiaohansong.com/vertor-clock.html" target="_blank" rel="noopener">分布式系统：向量时钟</a>》。<br>因果一致性的条件包括：</p><ol><li><strong>所有进程必须以相同的顺序看到具有因果关系的读写操作。</strong></li><li><strong>不同进程可以以不同的顺序看到并发的读写操作。</strong></li></ol><p>下面我们来分析一下为什么说因果一致性是一种弱化的顺序一致性模型。顺序一致性虽然不保证事件发生的顺序跟实际发生的保持一致，但是它能够保证所有进程看到的读写操作顺序是一样的。而<strong>因果一致性更进一步弱化了顺序一致性中对读写操作顺序的约束，仅保证有因果关系的读写操作有序，没有因果关系的读写操作（并发事件）则不做保证。</strong>也就是说如果是无因果关系的数据操作不同进程看到的值是有可能是不一样，而有因果关系的数据操作不同进程看到的值保证是一样的。</p><h3 id="最终一致性（Eventual-Consistency）"><a href="#最终一致性（Eventual-Consistency）" class="headerlink" title="最终一致性（Eventual Consistency）"></a>最终一致性（Eventual Consistency）</h3><p>最终一致性是更加弱化的一致性模型，因果一致性起码还保证了有因果关系的数据不同进程读取到的值保证是一样的，而<strong>最终一致性只保证所有副本的数据最终在某个时刻会保持一致。</strong><br>从某种意义上讲，最终一致性保证的数据在某个时刻会最终保持一致就像是在说：“人总有一天会死”一样。实际上我们更加关心的是：</p><ol><li><strong>“最终”到底是多久？通常来说，实际运行的系统需要能够保证提供一个有下限的时间范围。</strong></li><li><strong>多副本之间对数据更新采用什么样的策略？一段时间内可能数据可能多次更新，到底以哪个数据为准？一个常用的数据更新策略就是以时间戳最新的数据为准。</strong></li></ol><p>由于最终一致性对数据一致性的要求比较低，在对性能要求高的场景中是经常使用的一致性模型。</p><h3 id="以客户端为中心的一致性（Client-centric-Consistency）"><a href="#以客户端为中心的一致性（Client-centric-Consistency）" class="headerlink" title="以客户端为中心的一致性（Client-centric Consistency）"></a>以客户端为中心的一致性（Client-centric Consistency）</h3><p>前面我们讨论的一致性模型都是针对数据存储的多副本之间如何做到一致性，考虑这么一种场景：在最终一致性的模型中，如果客户端在数据不同步的时间窗口内访问不同的副本的同一个数据，会出现读取同一个数据却得到不同的值的情况。为了解决这个问题，有人提出了以客户端为中心的一致性模型。<strong>以客户端为中心的一致性为单一客户端提供一致性保证，保证该客户端对数据存储的访问的一致性，但是它不为不同客户端的并发访问提供任何一致性保证。</strong><br>举个例子：客户端 A 在副本 M 上读取 x 的最新值为 1，假设副本 M 挂了，客户端 A 连接到副本 N 上，此时副本 N 上面的 x 值为旧版本的 0，那么一致性模型会保证客户端 A 读取到的 x 的值为 1，而不是旧版本的 0。一种可行的方案就是给数据 x 加版本标记，同时客户端 A 会缓存 x 的值，通过比较版本来识别数据的新旧，保证客户端不会读取到旧的值。</p><p>以客户端为中心的一致性包含了四种子模型：</p><ol><li><strong>单调读一致性（Monotonic-read Consistency）</strong>：如果一个进程读取数据项 x 的值，那么该进程对于 x 后续的所有读操作要么读取到第一次读取的值要么读取到更新的值。即保证客户端不会读取到旧值。</li><li><strong>单调写一致性（Monotonic-write Consistency）</strong>：一个进程对数据项 x 的写操作必须在该进程对 x 执行任何后续写操作之前完成。即保证客户端的写操作是串行的。</li><li><strong>读写一致性（Read-your-writes Consistency）</strong>：一个进程对数据项 x 执行一次写操作的结果总是会被该进程对 x 执行的后续读操作看见。即保证客户端能读到自己最新写入的值。</li><li><strong>写读一致性（Writes-follow-reads Consistency）</strong>：同一个进程对数据项 x 执行的读操作之后的写操作，保证发生在与 x 读取值相同或比之更新的值上。即保证客户端对一个数据项的写操作是基于该客户端最新读取的值。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>数据复制导致了一致性的问题，为了保持副本的一致性可能会严重地影响性能，唯一的解决办法就是放松一致性的要求。通过一致性模型我们可以理解和推理在分布式系统中数据复制需要考虑的问题和基本假设，便于结合具体的业务场景做权衡。每种模型都有效地限制了对一个数据项执行度操作应返回的值。通常来说限制越少的模型越容易应用，但一致性的保证就越弱。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>转自:<a href="https://blog.xiaohansong.com/consistency-model.html" target="_blank" rel="noopener">分布式系统：一致性模型</a></p><p>《分布式系统原理与范型》<br><a href="http://book.mixu.net/distsys/" target="_blank" rel="noopener">Distributed systems for fun and profit</a><br><a href="https://en.wikipedia.org/wiki/Consistency_model" target="_blank" rel="noopener">Consistency_model</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;分布式系统中一个重要的问题就是数据复制，数据复制一般是为了增强系统的可用性或提高性能。而实现数据复制的一个主要难题就是保持各个副本的一致性。本文首先讨论数据复制的场景中一致性模型如此重要的原因，然后讨论一致性模型的含义，最后分析常用的一致性模型。&lt;/p&gt;
&lt;h2 id=&quot;为
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>一致性协议</title>
    <link href="http://yoursite.com/2021/08/03/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%EF%BC%9A%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/"/>
    <id>http://yoursite.com/2021/08/03/分布式/分布式系统：一致性协议/</id>
    <published>2021-08-03T09:50:49.389Z</published>
    <updated>2021-08-03T09:53:47.401Z</updated>
    
    <content type="html"><![CDATA[<p>一致性模型本质上是进程与数据存储的约定，通过一致性模型我们可以理解和推理在分布式系统中数据复制需要考虑的问题和基本假设。那么，一致性模型的具体实现有一些呢？本文会介绍一致性协议实现的主要思想和方法。</p><h2 id="什么是一致性协议"><a href="#什么是一致性协议" class="headerlink" title="什么是一致性协议"></a>什么是一致性协议</h2><p><strong>一致性协议描述了特定一致性模型的实际实现。</strong>一致性模型就像是接口，而一致性协议就像是接口的具体实现。一致性模型提供了分布式系统中数据复制时保持一致性的约束，为了实现一致性模型的约束，需要通过一致性协议来保证。</p><p>一致性协议根据是否允许数据分歧可以分为两种：</p><ul><li><strong>单主协议（不允许数据分歧）</strong>：整个分布式系统就像一个单体系统，所有写操作都由主节点处理并且同步给其他副本。例如主备同步、2PC、Paxos 都属于这类协议。</li><li><strong>多主协议（允许数据分歧）</strong>：所有写操作可以由不同节点发起，并且同步给其他副本。例如 Gossip、POW。</li></ul><p>可以发现，<strong>它们的核心区别在于是否允许多个节点发起写操作，单主协议只允许由主节点发起写操作，因此它可以保证操作有序性，一致性更强。而多主协议允许多个节点发起写操作，因此它不能保证操作的有序性，只能做到弱一致性。</strong></p><p>值得注意的是，一致性协议的分类方式有很多种，主要是看从哪个角度出发进行归类，常用的另一个归类方式是根据同步/异步复制来划分，这里就不多做讨论了。下面对单主协议和多主协议分别做一些共性的分析，篇幅所限，不会深入到协议细节。</p><h2 id="单主协议"><a href="#单主协议" class="headerlink" title="单主协议"></a>单主协议</h2><p>单主协议的共同点在于都会用一个主节点来负责写操作，这样能够保证全局写的顺序一致性，它有另一个名字叫定序器，非常的形象。</p><h3 id="主备复制"><a href="#主备复制" class="headerlink" title="主备复制"></a>主备复制</h3><p>主备复制可以说是最常用的数据复制方法，也是最基础的方法，很多其他协议都是基于它的变种。 <strong>主备复制要求所有的写操作都在主节点上进行，然后将操作的日志发送给其他副本。</strong>可以发现由于主备复制是有延迟的，所以它实现的是最终一致性。</p><p>主备复制的实现方式：主节点处理完写操作之后立即返回结果给客户端，写操作的日志异步同步给其他副本。这样的好处是性能高，客户端不需要等待数据同步，缺点是如果主节点同步数据给副本之前数据缺失了，那么这些数据就永久丢失了。MySQL 的主备同步就是典型的异步复制。</p><h3 id="两阶段提交"><a href="#两阶段提交" class="headerlink" title="两阶段提交"></a>两阶段提交</h3><p>两阶段提交（2PC）是关系型数据库常用的保持分布式事务一致性的协议，它也属于同步复制协议，即数据都同步完成之后才返回客户端结果。可以发现 2PC 保证所有节点数据一致之后才返回给客户端，实现了顺序一致性。</p><p>2PC 把数据复制分为两步：</p><ol><li><strong>表决阶段</strong>：主节点将数据发送给所有副本，每个副本都要响应提交或者回滚，如果副本投票提交，那么它会将数据放到暂存区域，等待最终提交。</li><li><strong>提交阶段</strong>：主节点收到其他副本的响应，如果副本都认为可以提交，那么就发送确认提交给所有副本让它们提交更新，数据就会从暂存区域移到永久区域。只要有一个副本返回回滚就整体回滚。</li></ol><p>可以发现 2PC 是典型的 CA 系统，为了保证一致性和可用性，2PC 一旦出现网络分区或者节点不可用就会被拒绝写操作，把系统变成只读的。由于 2PC 容易出现节点宕机导致一直阻塞的情况，所以在数据复制的场景中不常用，一般多用于分布式事务中（注：实际应用过程中会有很多优化）。</p><h3 id="分区容忍的一致性协议"><a href="#分区容忍的一致性协议" class="headerlink" title="分区容忍的一致性协议"></a>分区容忍的一致性协议</h3><p>分区容忍的一致性协议跟所有的单主协议一样，它也是只有一个主节点负责写入（提供顺序一致性），但它跟 2PC 的区别在于它只需要保证大多数节点（一般是超过半数）达成一致就可以返回客户端结果，这样可以提高了性能，同时也能容忍网络分区（少数节点分区不会导致整个系统无法运行）。分区容忍的一致性算法保证大多数节点数据一致后才返回客户端，同样实现了顺序一致性。</p><p>下面用一个简单的示例来说明这类算法的核心思想。假设现在有一个分布式文件系统，它的文件都被复制到 3 个服务器上，我们规定：要更新一个文件，客户端必须先访问至少 2 个服务器（大多数），得到它们同意之后才能执行更新，同时每个文件都会有版本号标识；要读取文件的时候，客户端也必须要访问至少 2 个服务器获取该文件的版本号，如果所有的版本号一致，那么该版本必定是最新的版本，因为如果前面的更新操作要求必须要有大多数服务器的同意才能更新文件。</p><p>以上就是我们熟知的 Paxos、ZAB、Raft 等分区容忍的一致性协议的核心思想：<strong>一致性的保证不一定非要所有节点都保持一致，只要大多数节点更新了，对于整个分布式系统来说数据也是一致性的。</strong>上面只是一个简单的阐述，真正的算法实现是比较复杂的，这里就不展开了。</p><p>分区容忍的一致性协议如 Paxos 是典型的 CP 系统，为了保证一致性和分区容忍，在网络分区的情况下，允许大多数节点的写入，通过大多数节点的一致性实现整个系统的一致性，同时让少数节点停止服务（不能读写），放弃整体系统的可用性，也就是说客户端访问到少数节点时会失败。</p><p>值得注意的是，根据 CAP 理论，假设现在有三个节点 A、B、C，当 C 被网络分区时，有查询请求过来，此时 C 因为不能和其他节点通信，所以 C 无法对查询做出响应，也就不具备可用性。但在工程实现上，这个问题是可以被绕过的，当客户端访问 C 无法得到响应时，它可以去访问 A、B，实际上对于整个系统来说还是部分可用性的，并不是说 CP 的系统一定就失去可用性。详细的分析参考<a href="https://blog.xiaohansong.com/cap-theorem.html" target="_blank" rel="noopener">分布式系统：CAP 理论的前世今生</a></p><h2 id="多主协议"><a href="#多主协议" class="headerlink" title="多主协议"></a>多主协议</h2><p>相比单主协议为了实现顺序一致性，不允许多个节点并发写，多主协议恰恰相反，只保证最终一致性，允许多个节点并发写，能够显著提升系统性能。由于多主协议一般提供的都是最终一致性，所以常用在对数据一致性要求不高的场景中。</p><p>Gossip 协议就是一种典型的多主协议，很多分布式系统都使用它来做数据复制，例如比特币，作为一条去中心化的公链，所有节点的数据同步都用的是 Gossip 协议。此外，Gossip 协议也在一些分布式数据库中如 Dynamo 中被用来做分布式故障检测的状态同步，当有节点故障离开集群时，其他节点可以快速检测到。</p><p>从名称上就可以看出 Gossip 协议的核心思想，Gossip 是流言八卦的意思，想想我们日常生活人与人之间传八卦的场景，在学校里面一个八卦一旦有一个人知道了，通过人传人，基本上整个学校的人最终都会知道了。因此 Gossip 协议的核心思想就是：<strong>每个节点都可以对其他节点发送消息，接收到消息的节点随机选择其他节点发送消息，接收到消息的节点也做同样的事情。</strong></p><p><strong>多主协议允许运行多个节点并发写，就一定会出现对一个数据并发写导致数据冲突的情况，因此这类协议都需要解决并发写的问题。</strong>单主协议通过主节点控制写入，保证不会出现并发写的情况，因为所有写操作最终都会通过主节点排序，从某种意义上讲，使用单主协议的系统对于写入实际上是串行的，因此其性能是有瓶颈的。而多主协议允许多节点并发写，提搞了写入的性能，但是实际上它是把数据合并的操作延迟了，单主协议在写入的时候就进行了数据合并，因此读取数据的时候如果出现数据冲突的时候，就需要对数据进行合并，保证全局一致性。</p><p>前面我们提到比特币使用的是 Gossip 协议做数据复制，那么问题来了，不是说多主协议性能会比较高吗，为什么比特币的性能那么差？这里实际上要分开来看，由于比特币是去中心化的，但是它的支付功能需要保证全局数据一致性，因此它用了一种很巧妙的一致性算法 POW：所有节点都做一道数学题，谁先算出答案谁有权利将交易写到链上，然后利用 Gossip 协议传播它的答案和交易，其他节点验证它的答案正确就将数据保存起来。</p><p>到这里你可能会有一个疑问：POW 作为多主协议为什么性能这么低？任何协议都有它适用的场景。在比特币这个场景中，它对于数据一致性是有强需求的，理论上用单主协议是最优的选择。但是比特币作为去中心化的数字货币是不会使用单主协议的，否则又变成中心化的系统了。因此比特币只能选择多主协议，通过 POW 协议将比特币整条链操作进行了<strong>近似串行化</strong>，这样才能降低出现双花的概率（并发写的时候一个比特币被消费多次），鱼与熊掌不可兼得，既然要强一致性，那么只能牺牲性能来换取。</p><p>由于多主协议允许了数据分歧，那么就需要有解决数据冲突的策略来保证最终一致性。如果要严格区分的话，比特币实际上应用了两个一致性协议：</p><ul><li><strong>POW</strong>：决定节点的记账权，起到类似单主协议中定序器的作用。注意 POW 也是多主协议，尽管概率很低，但是它有可能出现多个节点同时算出答案，一起出块（并发写）的情况，此时我们称比特币出现了分叉，即出现了数据冲突。</li><li><strong>Gossip</strong>：用于将出块的交易同步到全球所有节点。由于 POW 会出现并发写的情况，当一个节点同时接受到多个节点写入请求时，就需要解决数据冲突的问题。<strong>比特币解决数据冲突的方式就是当出现分叉时，选取最长的那条链作为主链，其他分叉的链上的交易会被回滚，等待重新打包出块。</strong></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要<strong>从是否允许数据分歧的角度将分布式一致性协议分为两种：单主协议和多主协议。</strong>其中单主协议会用一个主节点来负责写操作，这样能够保证全局写的顺序一致性，但因此也牺牲了一部分性能。而多主协议则允许写操作可以由不同节点发起，并且同步给其他副本，只能保证最终一致性，但因此也提升了系统并发写入的性能。对数据一致性要求高的场景例如分布式数据库，主要会使用单主协议，对数据一致性要求不高例如故障检测，主要会使用多主协议来提高性能，当然也有特例，像比特币为了去中心化使用 POW 和 Gossip 结合进行数据复制。</p><p>值得注意的是，文中提到的单主、多主协议只是我个人对分布式一致性协议的一种分类方式，帮助我们更好的理解。读者可以看一下参考资料，看一下不同作者对分布式协议是如何分类的，这样对分布式一致性协议会有更深入的理解。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>转自：<a href="https://blog.xiaohansong.com/consistency-protocol.html" target="_blank" rel="noopener">分布式系统：一致性协议</a></p><ul><li>《分布式系统原理与范型》</li><li><a href="http://book.mixu.net/distsys/" target="_blank" rel="noopener">Distributed systems for fun and profit</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;一致性模型本质上是进程与数据存储的约定，通过一致性模型我们可以理解和推理在分布式系统中数据复制需要考虑的问题和基本假设。那么，一致性模型的具体实现有一些呢？本文会介绍一致性协议实现的主要思想和方法。&lt;/p&gt;
&lt;h2 id=&quot;什么是一致性协议&quot;&gt;&lt;a href=&quot;#什么是一致
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>持续一致性</title>
    <link href="http://yoursite.com/2021/08/03/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%EF%BC%9A%E6%8C%81%E7%BB%AD%E4%B8%80%E8%87%B4%E6%80%A7/"/>
    <id>http://yoursite.com/2021/08/03/分布式/分布式系统：持续一致性/</id>
    <published>2021-08-03T09:43:51.571Z</published>
    <updated>2021-08-03T11:39:51.476Z</updated>
    
    <content type="html"><![CDATA[<p>在分布式系统中，数据复制一般是为了增强系统的可用性或提高性能，但是数据一致性跟系统性能往往是矛盾的，对于数据复制的一致性问题没有最好的解决方法。除非放宽对一致性的要求才能获取特定场景下面的有效解决方法。那么放宽一致性的标准是什么？为此，Yu 和 Vahdat 提出了一种用于衡量不一致性以及表述系统中能够容忍哪些不一致性的模型：持续一致性。</p><h2 id="什么是持续一致性"><a href="#什么是持续一致性" class="headerlink" title="什么是持续一致性"></a>什么是持续一致性</h2><p>在上一篇文章《<a href="https://blog.xiaohansong.com/consistency-model.html" target="_blank" rel="noopener">分布式系统：一致性模型</a>》中可以发现，在数据复制的场景中，数据一致性跟系统性能是矛盾，对数据一致性的要求越高，系统的整体性能越低。对于数据复制的一致性问题没有最好的解决方法。目前实际应用的一致性模型大部分都是通过放宽一致性的要求来提升性能，例如因果一致性放弃了对无因果关系的事件的顺序的一致性，减少了一致性所要付出的代价。<br>日常生活中，一个人是否近视，近视多少度是有统一的标准可以依据的，否则医生没法做出判断。同样的，作为系统的设计者面对一致性问题的时候也需要有一个标准，在性能和一致性之间做出权衡，那么现在问题来了：衡量不一致性的标准是什么？<br>为了解决上面的问题，Yu 和 Vahdat 提出了<strong>一种用于衡量不一致性以及表述系统中能够容忍哪些不一致性的模型：持续一致性。</strong></p><p>持续一致性定义了不一致性的三个独立坐标轴：<strong>数值偏差、顺序偏差、新旧偏差</strong>（不一致性的三个衡量标准），这些偏差构成了持续一致性的范围：</p><ul><li><strong>数值偏差限制了一个副本有多少未看到的其他副本写操作的权重（权重主要用于衡量不同写操作的重要性，当假设所有写操作权重相等时，权重即写操作的数量；当写操作的对象是数值时，可以用数值的差值作为权重），用于衡量当前副本值跟全局最终值之间的偏差。可以简单理解为未全局更新的写操作数量。</strong>例如在股票市场的价格记录的复制场景中，应用可以指定两个副本间的价格偏差不能超过 0.02 美元，这就是这个系统能够容忍的最大数值偏差。</li><li><strong>顺序偏差限制了一个副本中暂存写操作的数量，用于衡量暂存的写操作在本地副本的顺序与最终提交的写操作全局最终顺序之间的差异。</strong>顺序偏差相对来说比较难理解，首先当允许副本间有差异的时候，那么必定有一个时刻副本会暂存一些写操作，这些写操作在全局提交之后才会成为永久更新，但是这些写操作不一定都能提交成功，它可能会回滚，这意味着副本暂存写操作的顺序跟最终提交的顺序不一定一致。然而暂存的写操作有哪些会回滚导致顺序不一致无法预测，因此为了方便起见，直接取暂存写操作的数量作为顺序偏差，因为这是顺序偏差的上限。这就是顺序偏差的计算规则的由来。举个例子，如果要计算两阶段分布式事务的顺序偏差，那么它的顺序偏差就是准备阶段写操作的数量。</li><li><strong>新旧偏差限制了副本间同步写操作的延迟时间，即消息延迟。</strong>例如在天气预报的数据更新场景中，天气数据的更新不能超过 4 个小时的延迟，这段时间就是天气预报系统能够容忍的新旧偏差。</li></ul><p>上面的概念比较难理解，下面举一个简单的例子进行分析。</p><h2 id="一致性的衡量标准"><a href="#一致性的衡量标准" class="headerlink" title="一致性的衡量标准"></a>一致性的衡量标准</h2><h3 id="一致性单元"><a href="#一致性单元" class="headerlink" title="一致性单元"></a>一致性单元</h3><p>在解释不一致性的偏差之前，需要定义一下什么是非一致性。首先，Yu 和 Vahdat 引入一致性单元的概念，<strong>一致性单元表示的是在一致性模型中度量的数据单元。</strong>例如单个股票的价格可以定义为一个一致性单元，也可以把多个股票的价格作为一个一致性单元，这取决于应用场景。</p><p>对于每个一致性单元，持续一致性可以用三维向量定义为：<strong>一致性 = (数值偏差，顺序偏差，新旧偏差)</strong>。当所有偏差都为 0 时，就达到了线性一致性的要求。</p><p>在给出一致性单元的定义之后，下面对一致性的偏差给出更具体的定义。</p><ul><li><strong>数值偏差表示对于一个副本（一致性单元） R，有多少其他副本的更新没有应用到 R 上，并且这些更新的影响是什么。</strong></li><li><strong>顺序偏差表示对于一个副本（一致性单元） R，R 有多少暂存的更新操作</strong></li><li><strong>新旧偏差表示对于一个副本（一致性单元） R，R 有多长时间没有更新数据</strong></li></ul><h3 id="一致性衡量的例子"><a href="#一致性衡量的例子" class="headerlink" title="一致性衡量的例子"></a>一致性衡量的例子</h3><p><img src="/images/15532224355834.jpg" alt="img"></p>"<ul><li>标为灰色的操作表示已提交的更新，白色的操作为未提交的更新</li><li>&lt;5,B&gt; 表示对数据 B 执行操作时的向量时钟的值为 5，可简单理解为数据版本</li><li>数值偏差定义为 n(w)<ul><li>n = 副本 R 未看到的其他副本的更新数量</li><li>w = 偏差的权重 = 副本 R 一致性单元中所有变量当前值与全局值的数值差值</li></ul></li></ul><p>在上面的示例中，可以看到两个副本上有包含 x，y 的一致性单元上。这两个变量的初始值都为 0。注意，由于副本 A 最后的操作是 &lt;23, A&gt;，所以它的向量时钟为 (24, 5)。（参考<a href="https://blog.xiaohansong.com/vertor-clock.html" target="_blank" rel="noopener">向量时钟</a>）</p><p>首先来分析顺序偏差，副本 A 从副本 B 接受了 x+=2 的操作，并且提交为永久更新，注意此时副本 B 的 x+=2 的操作并未提交。副本 A 有三个暂存的写操作：&lt;10, A&gt;、&lt;14, A&gt;、&lt;23, A&gt;，所以此时它的顺序偏差为 3。而副本 B 有两个暂存的写操作，所以它的顺序偏差为 2。</p><p>接下来分析一下数值偏差，副本 A 还没有看到来自副本 B 的操作是 &lt;16, B&gt;，因此其数值偏差为 1，而权重的计算会稍微难理解一点，首先在这个例子中，由于一致性单元的变量是数值对象，所以这里权重可以定义为数值的差。在当前的图示状态中，假设所有值都会被提交为永久更新，一致性单元的最终值为：x=3，y=5，而此时副本 A 的值为：x=3，y=4，副本 A 的数值偏差权重为：(3+5)-(3+4)=1。同理，副本 B 还没有看到来自副本 A 的操作有：&lt;10, A&gt;、&lt;14, A&gt;、&lt;23, A&gt;，因此其数值偏差为 3，副本 B 的值为：x=2，y=1，其数值偏差权重为 (3+5)-(2+1)=5。综上，<strong>数值偏差的计算比较直观，就是副本的当前未看到其他副本的更新数量，而权重相对比较难理解，它反映的是当前副本一致性单元的快照跟全局快照的数值差值。</strong>值得注意的是，权重的计算跟数据的类型息息相关，主要取决于系统中对于数据更新权重的定义，像数值类型就可以用差值来衡量，像字符类型就没法用这种方式来计算，一种可行的方案就是认为权重都相等，此时权重就等于写操作的数量。</p><p>最后分析一下新旧偏差，上面的例子中没有体现出新旧偏差，但是前面已经举了天气预报的例子。实际上新旧偏差是相对好理解的，在分布式系统中消息传递是有延迟的，而这个延迟的时间就是我们所说的新旧偏差。有那么一段时间对 X 的数据在副本间是不一致的，因为数据传输过程中有延迟，所以新旧偏差在除了线性一致性模型之外一致性模型都是存在的。</p><p>通过上面的分析可以发现，限制顺序偏差可以通过控制单个副本的暂存更新数量完成，但是要限制数值偏差和新旧偏差则需要依赖所有副本的协调。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>持续一致性模型给出了一种用于衡量不一致性以及表述系统中能够容忍哪些不一致性的标准，包括顺序偏差、数值偏差、新旧偏差。持续一致性就像是一把尺子，给出了度量分布式系统中不一致性的标准和方法。它的最大特点是从副本的视角出发给出了一致性衡量的方法，而不是笼统从整个系统去讨论一致性，具有更强的可操作性。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>转自：<a href="https://blog.xiaohansong.com/Continuous-Consistency.html" target="_blank" rel="noopener">分布式系统：持续一致性</a></p><ul><li>《分布式系统原理与范型》</li><li><a href="https://www.usenix.org/publications/library/proceedings/osdi2000/full_papers/yuvahdat/yuvahdat.pdf" target="_blank" rel="noopener">Design and evaluation of a continuous consistency model for replicated services</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在分布式系统中，数据复制一般是为了增强系统的可用性或提高性能，但是数据一致性跟系统性能往往是矛盾的，对于数据复制的一致性问题没有最好的解决方法。除非放宽对一致性的要求才能获取特定场景下面的有效解决方法。那么放宽一致性的标准是什么？为此，Yu 和 Vahdat 提出了一种用于
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>向量时钟</title>
    <link href="http://yoursite.com/2021/07/28/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%EF%BC%9A%E5%90%91%E9%87%8F%E6%97%B6%E9%92%9F/"/>
    <id>http://yoursite.com/2021/07/28/分布式/分布式系统：向量时钟/</id>
    <published>2021-07-28T07:33:39.678Z</published>
    <updated>2021-08-03T09:51:42.730Z</updated>
    
    <content type="html"><![CDATA[<p>在上一篇文章<a href="https://blog.xiaohansong.com/lamport-logic-clock.html" target="_blank" rel="noopener">分布式系统：Lamport 逻辑时钟</a>中我们知道Lamport 逻辑时钟帮助我们得到了分布式系统中的事件全序关系，但是对于同时发生的关系却不能很好的描述，导致无法描述事件的因果关系。向量时钟是在 Lamport 时间戳基础上演进的另一种逻辑时钟方法，它通过向量结构不但记录本节点的 Lamport 时间戳，同时也记录了其他节点的 Lamport 时间戳，因此能够很好描述同时发生关系以及事件的因果关系。</p><p><strong>注意：</strong></p><ul><li>本文中的因果关系指的是时序关系，即时间的前后，并不是逻辑上的原因和结果</li><li>本文中提及的时间戳如无特别说明，都指的是逻辑时钟的时间戳，不是物理时钟的时间戳</li></ul><h2 id="为什么需要向量时钟"><a href="#为什么需要向量时钟" class="headerlink" title="为什么需要向量时钟"></a>为什么需要向量时钟</h2><p>首先我们来回顾一下 Lamport 逻辑时钟算法，它提供了一种判断分布式系统中事件全序关系的方法：如果 a -&gt; b，那么 C(a) &lt; C(b)，但是 C(a) &lt; C(b) 并不能说明 a -&gt; b。也就是说<strong>C(a) &lt; C(b) 是 a -&gt; b 的必要不充分条件，我们不能通过 Lamport 时间戳对事件 a、b 的因果关系进行判断。</strong> 下面我们举一个例子来说明。<br><img src="/images/15497196777863.jpg" alt="图1"><br>假设有三个进程在发消息，Ts(mi)表示消息mi的发送时间戳，Tr(mi)表示消息mi的接受时间戳，显然 Ts(mi) &lt; Tr(mi)，但是这个能说明什么呢？</p>"<p>我们可以发现在进程 P2 中，Tr(m1) &lt; Ts(m3)，说明 m3 是在 m1 被接收之后发送的，也就是说 m3 的发送跟 m1 的接收有关系。难道通过 Lamport 时间戳就能区分事件的因果的关系了吗？答案是 No，我们仔细看可以发现，虽然 Tr(m1) &lt; Ts(m2)，但实际上 m2 的发送跟 m1 并没有关系。</p><p>综上所述，我们可以发现 Lamport 逻辑时钟算法中每个进程只拥有自己的本地时间，没有其他进程的时间，导致无法描述事件的因果关系。如果每个进程都能够知道其他所有进程的时间，是否就能够得到事件的因果关系了呢？为此，有人提出了向量时钟算法，在 Lamport 逻辑时钟的基础上进行了改良，提出了一种在分布式系统中描述事件因果关系的算法。</p><p>可能有人会有疑问：向量时钟到底有什么用呢？举一个常见的工程应用：数据冲突检测。分布式系统中数据一般存在多个副本，多个副本可能被同时更新，这会引起副本间数据不一致，此时冲突检测就非常重要。<strong>基于向量时钟我们可以获得任意两个事件的顺序关系，结果要么是有因果关系（先后顺序），要么是没有因果关系（同时发生）。</strong>通过向量时钟，我们能够识别到如果两个数据更新操作是同时发生的关系，那么说明出现了数据冲突。后面我们会详细说明相关的实现。</p><h2 id="什么是向量时钟"><a href="#什么是向量时钟" class="headerlink" title="什么是向量时钟"></a>什么是向量时钟</h2><p>通过上面的分析我们知道向量时钟算法是<strong>在 Lamport 逻辑时钟的基础上进行了改良，用于在分布式系统中描述事件因果关系的算法。</strong>那么为什么叫向量时钟呢？前面我们知道如果每个进程都能够知道其他所有进程的时间，就能够通过计算得到事件的因果关系。向量时钟算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程：每个进程发送事件时都会将当前进程已知的所有进程时间写入到一个向量中，附带在消息中。这就是向量时钟命名的由来。</p><h2 id="如何实现向量时钟"><a href="#如何实现向量时钟" class="headerlink" title="如何实现向量时钟"></a>如何实现向量时钟</h2><p>假设分布式系统中有 N 个进程，每个进程都有一个本地的向量时间戳 Ti，向量时钟算法实现如下：</p><ol><li>对于进程 i 来说，Ti[i] 是进程 i 本地的逻辑时间</li><li>当进程 i 当有新的事件发生时，Ti[i] = Ti[i] + 1</li><li>当进程 i 发送消息时将它的向量时间戳(MT=Ti)附带在消息中。</li><li>接受消息的进程 j 更新本地的向量时间戳：Tj[k] = max(Tj[k], MT[k]) for k = 1 to N。（MT即消息中附带的向量时间戳）</li></ol><p>下图是向量时钟的示例：<br><img src="/images/15503020949302.jpg" alt="img"></p>"<p>那么如何利用向量时钟判断事件的因果关系呢？我们知道分布式系统中的事件要么是有因果关系（先后顺序），要么是没有因果关系（同时发生），下面我们来看一下如何利用向量时钟判断时间的因果关系。</p><p><strong>假设有事件 a、b 分别在节点 P、Q 上发生，向量时钟分别为 Ta、Tb，如果 Tb[Q] &gt; Ta[Q] 并且 Tb[P] &gt;= Ta[P]，则a发生于b之前，记作 a -&gt; b，此时说明事件 a、b 有因果关系；<br>反之，如果 Tb[Q] &gt; Ta[Q] 并且 Tb[P] &lt; Ta[P]，则认为a、b同时发生，记作 a <-> b。例如上图中节点 B 上的第 4 个事件 (A:2，B:4，C:1) 与节点 C 上的第 2 个事件 (B:3，C:2) 没有因果关系，属于同时发生事件。</-></strong></p><h2 id="向量时钟的实际应用"><a href="#向量时钟的实际应用" class="headerlink" title="向量时钟的实际应用"></a>向量时钟的实际应用</h2><p>前面我们提到向量时钟可以用来<strong>检测</strong>分布式系统中多副本更新的数据冲突问题，注意是检测（发现问题），它并不能解决问题。数据冲突的解决是另一个课题，这里不展开了。</p><p>亚马逊的 Dynamo 是一个分布式Key/Value存储系统，为了高可用，即使在出现网络分区或者机器宕机时依然可读可写。当网络分区恢复之后，多个副本同步数据一定会出现数据不一致的情况，那么如何检测数据冲突呢？参考向量时钟（Vector clock）的思想，Dynamo 中使用了版本向量（Version vector）来检测数据冲突，下面我们来看看算法的实现。</p><p><img src="/images/15503054707718.jpg" alt="img"></p>"<ol><li>client 端写入数据，该请求被 Sx 处理并创建相应的 vector ([Sx, 1])，记为数据 D1</li><li>第 2 次请求也被 Sx 处理，数据修改为 D2，vector 修改为([Sx, 2])</li><li>第 3、4 次请求分别被 Sy、Sz 处理，client 端先读取到 D2，然后 D3、D4 被写入 Sy、Sz</li><li>第 5 次更新时 client 端读取到 D2、D3 和 D4 3个数据版本，通过类似向量时钟判断同时发生关系的方法可判断 D3、D4 是同时发生的事件，因此存在数据冲突，最终通过一定方法解决数据冲突并写入 D5</li></ol><p>注意，向量时钟和版本向量并不是同一个东西，版本向量借鉴了向量时钟中利用向量来判断事件的因果关系的思想，用于检测数据冲突。向量时钟还有其他的应用，例如强制因果通信（Enforcing Causal Communication），这里不展开了，有兴趣的读者自行谷歌。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>向量时钟算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程，通过向量时间戳就能够比较任意两个事件的因果关系（先后关系或者同时发生关系）。向量时钟被用于解决数据冲突检测、强制因果通信等需要判断事件因果关系的问题。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>转自：<a href="https://blog.xiaohansong.com/vertor-clock.html" target="_blank" rel="noopener">分布式系统：向量时钟</a></p><ul><li><p><a href="https://zhuanlan.zhihu.com/p/23278509" target="_blank" rel="noopener">分布式系统理论基础 - 时间、时钟和事件顺序</a></p></li><li><p><a href="https://medium.com/@balrajasubbiah/lamport-clocks-and-vector-clocks-b713db1890d7" target="_blank" rel="noopener">Lamport Clocks And Vector Clocks</a></p></li><li><p><a href="https://blog.xiaohansong.com/、http://edisonxu.com/2018/11/02/clocks.html" target="_blank" rel="noopener">Vector Clock/Version Clock</a></p></li><li><p><a href="http://s3.amazonaws.com/AllThingsDistributed/sosp/amazon-dynamo-sosp2007.pdf" target="_blank" rel="noopener">Dynamo: Amazon’s Highly Available Key-value Store </a></p></li><li><p><a href="http://basho.com/posts/technical/why-vector-clocks-are-hard/" target="_blank" rel="noopener">Why Vector Clocks Are Hard</a></p></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在上一篇文章&lt;a href=&quot;https://blog.xiaohansong.com/lamport-logic-clock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;分布式系统：Lamport 逻辑时钟&lt;/a&gt;中我们知道Lamport 逻
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>Lamport 逻辑时钟</title>
    <link href="http://yoursite.com/2021/07/28/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%EF%BC%9ALamport%20%E9%80%BB%E8%BE%91%E6%97%B6%E9%92%9F/"/>
    <id>http://yoursite.com/2021/07/28/分布式/分布式系统：Lamport 逻辑时钟/</id>
    <published>2021-07-28T07:12:15.062Z</published>
    <updated>2022-04-07T14:01:12.340Z</updated>
    
    <content type="html"><![CDATA[<p>分布式系统解决了传统单体架构的单点问题和性能容量问题，另一方面也带来了很多的问题，其中一个问题就是多节点的时间同步问题：不同机器上的物理时钟难以同步，导致无法区分在分布式系统中多个节点的事件时序。1978年Lamport在《<a href="http://research.microsoft.com/users/lamport/pubs/time-clocks.pdf" target="_blank" rel="noopener">Time, Clocks and the Ordering of Events in a Distributed System</a>》中提出了逻辑时钟的概念，来解决分布式系统中区分事件发生的时序问题。</p><h2 id="什么是逻辑时钟"><a href="#什么是逻辑时钟" class="headerlink" title="什么是逻辑时钟"></a>什么是逻辑时钟</h2><p>逻辑时钟是为了区分现实中的物理时钟提出来的概念，一般情况下我们提到的时间都是指物理时间，但实际上很多应用中，只要所有机器有相同的时间就够了，这个时间不一定要跟实际时间相同。更进一步，如果两个节点之间不进行交互，那么它们的时间甚至都不需要同步。<strong>因此问题的关键点在于节点间的交互要在事件的发生顺序上达成一致，而不是对于时间达成一致。</strong></p><p>综上，<strong>逻辑时钟指的是分布式系统中用于区分事件的发生顺序的时间机制。</strong>从某种意义上讲，现实世界中的物理时间其实是逻辑时钟的特例。</p><h2 id="为什么需要逻辑时钟"><a href="#为什么需要逻辑时钟" class="headerlink" title="为什么需要逻辑时钟"></a>为什么需要逻辑时钟</h2><p>时间是在现实生活中是很重要的概念，有了时间我们就能比较事情发生的先后顺序。如果是单个计算机内执行的事务，由于它们共享一个计时器，所以能够很容易通过时间戳来区分先后。同理在分布式系统中也通过时间戳的方式来区分先后行不行？</p><p>答案是NO，因为在分布式系统中的不同节点间保持它们的时钟一致是一件不容易的事情。因为每个节点的CPU都有自己的计时器，而不同计时器之间会产生时间偏移，最终导致不同节点上面的时间不一致。也就是说如果A节点的时钟走的比B节点的要快1分钟，那么即使B先发出的消息（附带B的时间戳），A的消息（附带A的时间戳）在后一秒发出，A的消息也会被认为先于B发生。</p><p>那么是否可以通过某种方式来同步不同节点的物理时钟呢？答案是有的，NTP就是常用的时间同步算法，但是即使通过算法进行同步，总会有误差，这种误差在某些场景下（金融分布式事务）是不能接受的。</p><p>因此，<strong>Lamport提出逻辑时钟就是为了解决分布式系统中的时序问题，即如何定义a在b之前发生。</strong>值得注意的是，并不是说分布式系统只能用逻辑时钟来解决这个问题，如果以后有某种技术能够让不同节点的时钟完全保持一致，那么使用物理时钟来区分先后是一个更简单有效的方式。</p><h2 id="如何实现逻辑时钟"><a href="#如何实现逻辑时钟" class="headerlink" title="如何实现逻辑时钟"></a>如何实现逻辑时钟</h2><h3 id="时序关系与相对论"><a href="#时序关系与相对论" class="headerlink" title="时序关系与相对论"></a>时序关系与相对论</h3><p>通过前面的讨论我们知道通过物理时钟（即绝对参考系）来区分先后顺序的前提是所有节点的时钟完全同步，但目前并不现实。因此，在没有绝对参考系的情况下，在一个分布式系统中，你无法判断事件A是否发生在事件B之前，除非A和B存在某种依赖关系，即分布式系统中的事件仅仅是部分有序的。</p><p>上面的结论跟狭义相对论有异曲同工之妙，在狭义相对论中，不同观察者在同一参考系中观察到的事件先后顺序是一致的，但是在不同的观察者在不同的参考系中对两个事件谁先发生可能具有不同的看法。当且仅当事件A是由事件B引起的时候，事件A和B之间才存在一个先后关系。<strong>两个事件可以建立因果关系的前提是：两个事件之间可以用等于或小于光速的速度传递信息。</strong> 值得注意的是这里的因果关系指的是时序关系，即时间的前后，并不是逻辑上的原因和结果。</p><p>那么是否我们可以参考狭义相对论来定义分布式系统中两个事件的时序呢？在分布式系统中，网络是不可靠的，所以我们去掉<strong>可以</strong>和<strong>速度</strong>的约束，可以得到<strong>两个事件可以建立因果（时序）关系的前提是：两个事件之间是否发生过信息传递。</strong>在分布式系统中，进程间通信的手段（共享内存、消息发送等）都属于信息传递，如果两个进程间没有任何交互，实际上他们之间内部事件的时序也无关紧要。但是有交互的情况下，特别是多个节点的要保持同一副本的情况下，事件的时序非常重要。</p><h3 id="Lamport-逻辑时钟"><a href="#Lamport-逻辑时钟" class="headerlink" title="Lamport 逻辑时钟"></a>Lamport 逻辑时钟</h3><p>分布式系统中按是否存在节点交互可分为三类事件，一类发生于节点内部，二是发送事件，三是接收事件。注意：<strong>以下文章中提及的时间戳如无特别说明，都指的是Lamport 逻辑时钟的时间戳，不是物理时钟的时间戳</strong></p><blockquote><p>逻辑时钟定义</p><p>Clock Condition.对于任意事件𝑎a, 𝑏b：如果𝑎→𝑏a→b（→→表示a先于b发生），那么𝐶(𝑎)&lt;𝐶(𝑏)C(a)&lt;C(b), 反之不然, 因为有可能是并发事件<br>C1.如果𝑎a和𝑏b都是进程𝑃𝑖Pi里的事件，并且𝑎a在𝑏b之前，那么𝐶𝑖(𝑎)&lt;𝐶𝑖(𝑏)Ci(a)&lt;Ci(b)<br>C2.如果𝑎a是进程𝑃𝑖Pi里关于某消息的发送事件，𝑏b是另一进程𝑃𝑗Pj里关于该消息的接收事件，那么𝐶𝑖(𝑎)&lt;𝐶𝑗(𝑏)Ci(a)&lt;Cj(b)</p></blockquote><p>Lamport 逻辑时钟原理如下：<br><img src="/images/15489326333600.jpg" alt="图1"></p>"<ol><li>每个事件对应一个Lamport时间戳，初始值为0</li><li>如果事件在节点内发生，本地进程中的时间戳加1</li><li>如果事件属于发送事件，本地进程中的时间戳加1并在消息中带上该时间戳</li><li>如果事件属于接收事件，本地进程中的时间戳 = Max(本地时间戳，消息中的时间戳) + 1</li></ol><p>假设有事件𝑎、𝑏，𝐶(𝑎)、𝐶(𝑏)a、b，C(a)、C(b)分别表示事件𝑎、𝑏a、b对应的Lamport时间戳，如果𝑎a发生在𝑏b之前(happened before)，记作 𝑎→𝑏a→b，则有𝐶(𝑎)&lt;𝐶(𝑏)C(a)&lt;C(b)，例如图1中有 𝐶1→𝐵1C1→B1，那么 𝐶(𝐶1)&lt;𝐶(𝐵1)C(C1)&lt;C(B1)。通过该定义，事件集中Lamport时间戳不等的事件可进行比较，我们获得事件的偏序关系(partial order)。<strong>注意：如果𝐶(𝑎)&lt;𝐶(𝑏)C(a)&lt;C(b)，并不能说明𝑎→𝑏a→b，也就是说𝐶(𝑎)&lt;𝐶(𝑏)C(a)&lt;C(b)是𝑎→𝑏a→b的必要不充分条件</strong></p><p>如果𝐶(𝑎)=𝐶(𝑏)C(a)=C(b)，那𝑎、𝑏a、b事件的顺序又是怎样的？值得注意的是当𝐶(𝑎)=𝐶(𝑏)C(a)=C(b)的时候，它们肯定不是因果关系，所以它们之间的先后其实并不会影响结果，我们这里只需要给出一种确定的方式来定义它们之间的先后就能得到全序关系。<strong>注意：Lamport逻辑时钟只保证因果关系（偏序）的正确性，不保证绝对时序的正确性。</strong></p><p>一种可行的方式是利用给进程编号，利用进程编号的大小来排序。假设𝑎、𝑏a、b分别在节点𝑃、𝑄P、Q上发生，𝑃𝑖、𝑄𝑗Pi、Qj分别表示我们给𝑃、𝑄P、Q的编号，如果 𝐶(𝑎)=𝐶(𝑏)C(a)=C(b) 并且 𝑃𝑖&lt;𝑄𝑗Pi&lt;Qj，同样定义为𝑎a发生在𝑏b之前，记作 𝑎⇒𝑏a⇒b（全序关系）。假如我们对图1的𝐴、𝐵、𝐶A、B、C分别编号𝐴𝑖=1、𝐵𝑗=2、𝐶𝑘=3Ai=1、Bj=2、Ck=3，因 𝐶(𝐵4)=𝐶(𝐶3)C(B4)=C(C3) 并且 𝐵𝑗&lt;𝐶𝑘Bj&lt;Ck，则 𝐵4⇒𝐶3B4⇒C3。</p><p>通过以上定义，我们可以对所有事件排序，获得事件的全序关系(total order)。上图例子，我们可以进行排序：𝐶1⇒𝐵1⇒𝐵2⇒𝐴1⇒𝐵3⇒𝐴2⇒𝐶2⇒𝐵4⇒𝐶3⇒𝐴3⇒𝐵5⇒𝐶4⇒𝐶5⇒𝐴4C1⇒B1⇒B2⇒A1⇒B3⇒A2⇒C2⇒B4⇒C3⇒A3⇒B5⇒C4⇒C5⇒A4</p><p>观察上面的全序关系你可以发现，从时间轴来看𝐵5B5是早于𝐴3A3发生的，但是在全序关系里面我们根据上面的定义给出的却是𝐴3A3早于𝐵5B5，可以发现Lamport逻辑时钟是一个正确的算法，即有因果关系的事件时序不会错，但并不是一个公平的算法，即没有因果关系的事件时序不一定符合实际情况。</p><h2 id="如何使用逻辑时钟解决分布式锁问题"><a href="#如何使用逻辑时钟解决分布式锁问题" class="headerlink" title="如何使用逻辑时钟解决分布式锁问题"></a>如何使用逻辑时钟解决分布式锁问题</h2><p>上面的分析过于理论，下面我们来尝试使用逻辑时钟来解决分布式锁问题。</p><p>分布式锁问题本质上是对于共享资源的抢占问题，我们先对问题进行定义：</p><ol><li>已经获得资源授权的进程，必须在资源分配给其他进程之前释放掉它；</li><li>资源请求必须按照请求发生的顺序进行授权；</li><li>在获得资源授权的所有进程最终释放资源后，所有的资源请求必须都已经被授权了。</li></ol><p>首先我们假设，<strong>对于任意的两个进程𝑃𝑖Pi和𝑃𝑗Pj，它们之间传递的消息是按照发送顺序被接收到的, 并且所有的消息最终都会被接收到。</strong><br>每个进程会维护一个它自己的对其他所有进程都不可见的请求队列。我们假设该请求队列初始时刻只有一个消息(𝑇0:𝑃0)(T0:P0)资源请求，𝑃0P0代表初始时刻获得资源授权的那个进程，T0小于任意时钟初始值</p><ol><li>为请求该项资源，进程𝑃𝑖Pi发送一个(𝑇𝑚:𝑃𝑖)(Tm:Pi)资源请求（请求锁）消息给其他所有进程，并将该消息放入自己的请求队列，在这里𝑇𝑚Tm代表了消息的时间戳</li><li>当进程𝑃𝑗Pj收到(𝑇𝑚:𝑃𝑖)(Tm:Pi)资源请求消息后，将它放到自己的请求队列中，并发送一个带时间戳的确认消息给𝑃𝑖Pi。(注：如果𝑃𝑗Pj已经发送了一个时间戳大于𝑇𝑚Tm的消息，那就可以不发送)</li><li>释放该项资源（释放锁）时，进程𝑃𝑖Pi从自己的消息队列中删除所有的(𝑇𝑚:𝑃𝑖)(Tm:Pi)资源请求，同时给其他所有进程发送一个带有时间戳的𝑃𝑖Pi资源释放消息</li><li>当进程𝑃𝑗Pj收到𝑃𝑖Pi资源释放消息后，它就从自己的消息队列中删除所有的(𝑇𝑚:𝑃𝑖)(Tm:Pi)资源请求</li><li>当同时满足如下两个条件时，就将资源分配（锁占用）给进程Pi：<ul><li>按照全序关系排序后，(𝑇𝑚:𝑃𝑖)(Tm:Pi)资源请求排在它的请求队列的最前面</li><li>𝑖i已经从所有其他进程都收到了时间戳&gt;𝑇𝑚Tm的消息、</li></ul></li></ol><p>下面我会用图例来说明上面算法运作的过程，假设我们有3个进程，根据算法说明，初始化状态各个进程队列里面都是(0:0)状态，此时锁属于P0。<br><img src="/images/15490064189797.jpg" alt="初始状态"></p>"<p>接下来P1会发出请求资源的消息给所有其他进程，并且放到自己的请求队列里面，根据逻辑时钟算法，P1的时钟走到1，而接受消息的P0和P2的时钟为消息时间戳+1。<br><img src="/images/15490066250546.jpg" alt="请求资源"></p>"<p>收到P1的请求之后，P0和P2要发送确认消息给P1表示自己收到了。注意，由于目前请求队列里面第一个不是P1发出的请求，所以此时锁仍属于P0。但是由于收到了确认消息，此时P1已经满足了获取资源的第一个条件：<strong>P1已经收到了其他所有进程时间戳大于1的消息。</strong><br><img src="/images/15498551753772.jpg" alt="返回确认"></p>"<p>假设P0此时释放了锁（这里为了方便演示做了这个假设，实际上P0什么时候释放资源都可以，算法都是正确的，读者可自行推导），发送释放资源的消息给P1和P2，P1和P2收到消息之后把请求(0:0)从队列里面删除。<br><img src="/images/15498552492316.jpg" alt="img"></p>"<p>当P0释放了资源之后，我们发现P1满足了获取资源的两个条件：<strong>它的请求在队列最前面；P1已经收到了其他所有进程时间戳大于1的消息。</strong>也就是说此时P1就获取到了锁。</p><p>值得注意的是，这个算法并不是容错的，有一个进程挂了整个系统就挂了，因为需要等待所有其他进程的响应，同时对网络的要求也很高。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>如果你之前看过2PC，Paxos之类的算法，相信你看到最后一定会有一种似曾相识的感觉。实际上，Lamport提出的逻辑时钟可以说是分布式一致性算法的开山鼻祖，后续的所有分布式算法都有它的影子。我们不能想象现实世界中没有时间，而逻辑时钟定义了分布式系统里面的时间概念，解决了分布式系统中区分事件发生的时序问题。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>转自：<a href="https://blog.xiaohansong.com/lamport-logic-clock.html" target="_blank" rel="noopener">分布式系统：Lamport 逻辑时钟</a></p><ul><li><a href="http://research.microsoft.com/users/lamport/pubs/time-clocks.pdf" target="_blank" rel="noopener">Time, Clocks and the Ordering of Events in a Distributed System</a></li><li><a href="https://www.zhihu.com/question/30084741/answer/71115362" target="_blank" rel="noopener">将物理与计算机结合可以做些什么？</a></li><li><a href="https://zhuanlan.zhihu.com/p/23278509" target="_blank" rel="noopener">分布式系统理论基础 - 时间、时钟和事件顺序</a></li><li><a href="http://www.cnblogs.com/fxjwind/archive/2013/04/13/3017892.h$T_m$l" target="_blank" rel="noopener">全序, 分布式一致性的本质</a></li><li><a href="http://betathoughts.blogspot.com/2007/06/brief-history-of-consensus-2pc-and.html" target="_blank" rel="noopener">A brief history of Consensus, 2PC and Transaction Commit</a></li><li><a href="http://ju.outofmemory.cn/entry/47601" target="_blank" rel="noopener">我对Lamport Logical Clock的理解</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;分布式系统解决了传统单体架构的单点问题和性能容量问题，另一方面也带来了很多的问题，其中一个问题就是多节点的时间同步问题：不同机器上的物理时钟难以同步，导致无法区分在分布式系统中多个节点的事件时序。1978年Lamport在《&lt;a href=&quot;http://research.
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>分布式ID生成方式</title>
    <link href="http://yoursite.com/2021/07/22/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8FID%E7%94%9F%E6%88%90%E6%96%B9%E5%BC%8F/"/>
    <id>http://yoursite.com/2021/07/22/分布式/分布式ID生成方式/</id>
    <published>2021-07-22T05:22:34.328Z</published>
    <updated>2021-07-25T09:00:15.402Z</updated>
    
    <content type="html"><![CDATA[<h4 id="一、为什么要用分布式ID？"><a href="#一、为什么要用分布式ID？" class="headerlink" title="一、为什么要用分布式ID？"></a>一、为什么要用分布式ID？</h4><p>在说分布式ID的具体实现之前，我们来简单分析一下为什么用分布式ID？分布式ID应该满足哪些特征？</p><h6 id="1、什么是分布式ID？"><a href="#1、什么是分布式ID？" class="headerlink" title="1、什么是分布式ID？"></a>1、什么是分布式ID？</h6><p>拿MySQL数据库举个栗子：</p><p>在我们业务数据量不大的时候，单库单表完全可以支撑现有业务，数据再大一点搞个MySQL主从同步读写分离也能对付。</p><p>但随着数据日渐增长，主从同步也扛不住了，就需要对数据库进行分库分表，但分库分表后需要有一个唯一ID来标识一条数据，数据库的自增ID显然不能满足需求；特别一点的如订单、优惠券也都需要有<code>唯一ID</code>做标识。此时一个能够生成<code>全局唯一ID</code>的系统是非常必要的。那么这个<code>全局唯一ID</code>就叫<code>分布式ID</code>。</p><h6 id="2、那么分布式ID需要满足那些条件？"><a href="#2、那么分布式ID需要满足那些条件？" class="headerlink" title="2、那么分布式ID需要满足那些条件？"></a>2、那么分布式ID需要满足那些条件？</h6><ul><li>全局唯一：必须保证ID是全局性唯一的，基本要求</li><li>高性能：高可用低延时，ID生成响应要块，否则反倒会成为业务瓶颈</li><li>高可用：100%的可用性是骗人的，但是也要无限接近于100%的可用性</li><li>好接入：要秉着拿来即用的设计原则，在系统设计和实现上要尽可能的简单</li><li>趋势递增：最好趋势递增，这个要求就得看具体业务场景了，一般不严格要求</li></ul><h4 id="二、-分布式ID都有哪些生成方式？"><a href="#二、-分布式ID都有哪些生成方式？" class="headerlink" title="二、 分布式ID都有哪些生成方式？"></a>二、 分布式ID都有哪些生成方式？</h4><p>今天主要分析一下以下9种，分布式ID生成器方式以及优缺点：</p><ul><li>UUID</li><li>数据库自增ID</li><li>数据库多主模式</li><li>号段模式</li><li>Redis</li><li>雪花算法（SnowFlake）</li><li>滴滴出品（TinyID）</li><li>百度 （Uidgenerator）</li><li>美团（Leaf）</li></ul><p>那么它们都是如何实现？以及各自有什么优缺点？我们往下看</p><h5 id="1、基于UUID"><a href="#1、基于UUID" class="headerlink" title="1、基于UUID"></a>1、基于UUID</h5><p>在Java的世界里，想要得到一个具有唯一性的ID，首先被想到可能就是<code>UUID</code>，毕竟它有着全球唯一的特性。那么<code>UUID</code>可以做<code>分布式ID</code>吗？<strong>答案是可以的，但是并不推荐！</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123; </span><br><span class="line">       String uuid = UUID.randomUUID().toString().replaceAll(<span class="string">"-"</span>,<span class="string">""</span>);</span><br><span class="line">       System.out.println(uuid);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p><code>UUID</code>的生成简单到只有一行代码，输出结果 <code>c2b8c2b9e46c47e3b30dca3b0d447718</code>，但UUID却并不适用于实际的业务需求。像用作订单号<code>UUID</code>这样的字符串没有丝毫的意义，看不出和订单相关的有用信息；而对于数据库来说用作业务<code>主键ID</code>，它不仅是太长还是字符串，存储性能差查询也很耗时，所以不推荐用作<code>分布式ID</code>。</p><p><strong>优点：</strong></p><ul><li>生成足够简单，本地生成无网络消耗，具有唯一性</li></ul><p><strong>缺点：</strong></p><ul><li>无序的字符串，不具备趋势自增特性</li><li>没有具体的业务含义</li><li>长度过长16 字节128位，36位长度的字符串，存储以及查询对MySQL的性能消耗较大，MySQL官方明确建议主键要尽量越短越好，作为数据库主键 <code>UUID</code> 的无序性会导致数据位置频繁变动，严重影响性能。</li></ul><h5 id="2、基于数据库自增ID"><a href="#2、基于数据库自增ID" class="headerlink" title="2、基于数据库自增ID"></a>2、基于数据库自增ID</h5><p>基于数据库的<code>auto_increment</code>自增ID完全可以充当<code>分布式ID</code>，具体实现：需要一个单独的MySQL实例用来生成ID，建表结构如下：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">DATABASE</span> <span class="string">`SEQ_ID`</span>;</span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> SEQID.SEQUENCE_ID (</span><br><span class="line">    <span class="keyword">id</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">unsigned</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> auto_increment, </span><br><span class="line">    <span class="keyword">value</span> <span class="built_in">char</span>(<span class="number">10</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">default</span> <span class="string">''</span>,</span><br><span class="line">    PRIMARY <span class="keyword">KEY</span> (<span class="keyword">id</span>),</span><br><span class="line">) <span class="keyword">ENGINE</span>=MyISAM;</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> SEQUENCE_ID(<span class="keyword">value</span>)  <span class="keyword">VALUES</span> (<span class="string">'values'</span>);</span><br></pre></td></tr></table></figure><p>当我们需要一个ID的时候，向表中插入一条记录返回<code>主键ID</code>，但这种方式有一个比较致命的缺点，访问量激增时MySQL本身就是系统的瓶颈，用它来实现分布式服务风险比较大，不推荐！</p><p><strong>优点：</strong></p><ul><li>实现简单，ID单调自增，数值类型查询速度快</li></ul><p><strong>缺点：</strong></p><ul><li>DB单点存在宕机风险，无法扛住高并发场景</li></ul><h5 id="3、基于数据库集群模式"><a href="#3、基于数据库集群模式" class="headerlink" title="3、基于数据库集群模式"></a>3、基于数据库集群模式</h5><p>前边说了单点数据库方式不可取，那对上边的方式做一些高可用优化，换成主从模式集群。害怕一个主节点挂掉没法用，那就做双主模式集群，也就是两个Mysql实例都能单独的生产自增ID。</p><p>那这样还会有个问题，两个MySQL实例的自增ID都从1开始，<strong>会生成重复的ID怎么办？</strong></p><p><strong>解决方案</strong>：设置<code>起始值</code>和<code>自增步长</code></p><p>MySQL_1 配置：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> @@auto_increment_offset = <span class="number">1</span>;     <span class="comment">-- 起始值</span></span><br><span class="line"><span class="keyword">set</span> @@auto_increment_increment = <span class="number">2</span>;  <span class="comment">-- 步长</span></span><br></pre></td></tr></table></figure><p>MySQL_2 配置：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">set</span> @@auto_increment_offset = <span class="number">2</span>;     <span class="comment">-- 起始值</span></span><br><span class="line"><span class="keyword">set</span> @@auto_increment_increment = <span class="number">2</span>;  <span class="comment">-- 步长</span></span><br></pre></td></tr></table></figure><p>这样两个MySQL实例的自增ID分别就是：</p><blockquote><p>1、3、5、7、9<br>2、4、6、8、10</p></blockquote><p>那如果集群后的性能还是扛不住高并发咋办？就要进行MySQL扩容增加节点，这是一个比较麻烦的事。</p><p><img src="/images/640.png" alt="图片"></p>"<p>从上图可以看出，水平扩展的数据库集群，有利于解决数据库单点压力的问题，同时为了ID生成特性，将自增步长按照机器数量来设置。</p><p>增加第三台<code>MySQL</code>实例需要人工修改一、二两台<code>MySQL实例</code>的起始值和步长，把<code>第三台机器的ID</code>起始生成位置设定在比现有<code>最大自增ID</code>的位置远一些，但必须在一、二两台<code>MySQL实例</code>ID还没有增长到<code>第三台MySQL实例</code>的<code>起始ID</code>值的时候，否则<code>自增ID</code>就要出现重复了，<strong>必要时可能还需要停机修改</strong>。</p><p><strong>优点：</strong></p><ul><li>解决DB单点问题</li></ul><p><strong>缺点：</strong></p><ul><li>不利于后续扩容，而且实际上单个数据库自身压力还是大，依旧无法满足高并发场景。</li></ul><h5 id="4、基于数据库的号段模式"><a href="#4、基于数据库的号段模式" class="headerlink" title="4、基于数据库的号段模式"></a>4、基于数据库的号段模式</h5><p>号段模式是当下分布式ID生成器的主流实现方式之一，号段模式可以理解为从数据库批量的获取自增ID，每次从数据库取出一个号段范围，例如 (1,1000] 代表1000个ID，具体的业务服务将本号段，生成1~1000的自增ID并加载到内存。表结构如下：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> id_generator (</span><br><span class="line">  <span class="keyword">id</span> <span class="built_in">int</span>(<span class="number">10</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  max_id <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'当前最大id'</span>,</span><br><span class="line">  step <span class="built_in">int</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'号段的布长'</span>,</span><br><span class="line">  biz_type    <span class="built_in">int</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'业务类型'</span>,</span><br><span class="line">  <span class="keyword">version</span> <span class="built_in">int</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'版本号'</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (<span class="string">`id`</span>)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>biz_type ：代表不同业务类型</p><p>max_id ：当前最大的可用id</p><p>step ：代表号段的长度</p><p>version ：是一个乐观锁，每次都更新version，保证并发时数据的正确性</p><table><thead><tr><th style="text-align:left">id</th><th style="text-align:left">biz_type</th><th style="text-align:left">max_id</th><th style="text-align:left">step</th><th style="text-align:left">version</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">101</td><td style="text-align:left">1000</td><td style="text-align:left">2000</td><td style="text-align:left">0</td></tr></tbody></table><p>等这批号段ID用完，再次向数据库申请新号段，对<code>max_id</code>字段做一次<code>update</code>操作，<code>update max_id= max_id + step</code>，update成功则说明新号段获取成功，新的号段范围是<code>(max_id ,max_id +step]</code>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> id_generator <span class="keyword">set</span> max_id = <span class="comment">#&#123;max_id+step&#125;, version = version + 1 where version = # &#123;version&#125; and biz_type = XXX</span></span><br></pre></td></tr></table></figure><p>由于多业务端可能同时操作，所以采用版本号<code>version</code>乐观锁方式更新，这种<code>分布式ID</code>生成方式不强依赖于数据库，不会频繁的访问数据库，对数据库的压力小很多。</p><h5 id="5、基于Redis模式"><a href="#5、基于Redis模式" class="headerlink" title="5、基于Redis模式"></a>5、基于Redis模式</h5><p><code>Redis</code>也同样可以实现，原理就是利用<code>redis</code>的 <code>incr</code>命令实现ID的原子性自增。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; set seq_id 1     // 初始化自增ID为1</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379&gt; incr seq_id      // 增加1，并返回递增后的数值</span><br><span class="line">(integer) 2</span><br></pre></td></tr></table></figure><p>用<code>redis</code>实现需要注意一点，要考虑到redis持久化的问题。<code>redis</code>有两种持久化方式<code>RDB</code>和<code>AOF</code></p><ul><li><code>RDB</code>会定时打一个快照进行持久化，假如连续自增但<code>redis</code>没及时持久化，而这会Redis挂掉了，重启Redis后会出现ID重复的情况。</li><li><code>AOF</code>会对每条写命令进行持久化，即使<code>Redis</code>挂掉了也不会出现ID重复的情况，但由于incr命令的特殊性，会导致<code>Redis</code>重启恢复的数据时间过长。</li></ul><h5 id="6、基于雪花算法（Snowflake）模式"><a href="#6、基于雪花算法（Snowflake）模式" class="headerlink" title="6、基于雪花算法（Snowflake）模式"></a>6、基于雪花算法（Snowflake）模式</h5><p>雪花算法（Snowflake）是twitter公司内部分布式项目采用的ID生成算法，开源后广受国内大厂的好评，在该算法影响下各大公司相继开发出各具特色的分布式生成器。</p><p><img src="/images/image-20210725165138363.png" alt="image-20210725165138363"></p>"<p><code>Snowflake</code>生成的是Long类型的ID，一个Long类型占8个字节，每个字节占8比特，也就是说一个Long类型占64个比特。</p><p>Snowflake ID组成结构：<code>正数位</code>（占1比特）+ <code>时间戳</code>（占41比特）+ <code>机器ID</code>（占5比特）+ <code>数据中心</code>（占5比特）+ <code>自增值</code>（占12比特），总共64比特组成的一个Long类型。</p><ul><li>第一个bit位（1bit）：Java中long的最高位是符号位代表正负，正数是0，负数是1，一般生成ID都为正数，所以默认为0。</li><li>时间戳部分（41bit）：毫秒级的时间，不建议存当前时间戳，而是用（当前时间戳 - 固定开始时间戳）的差值，可以使产生的ID从更小的值开始；41位的时间戳可以使用69年，(1L &lt;&lt; 41) / (1000L <em> 60 </em> 60 <em> 24 </em> 365) = 69年</li><li>工作机器id（10bit）：也被叫做<code>workId</code>，这个可以灵活配置，机房或者机器号组合都可以。</li><li>序列号部分（12bit），自增值支持同一毫秒内同一个节点可以生成4096个ID</li></ul><p>根据这个算法的逻辑，只需要将这个算法用Java语言实现出来，封装为一个工具方法，那么各个业务应用可以直接使用该工具方法来获取分布式ID，只需保证每个业务应用有自己的工作机器id即可，而不需要单独去搭建一个获取分布式ID的应用。</p><p><strong>Java版本的<code>Snowflake</code>算法实现：</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数，然后转化为62进制变成一个短地址URL</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * https://github.com/beyondfengyu/SnowFlake</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SnowFlakeShortUrl</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 起始的时间戳</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> START_TIMESTAMP = <span class="number">1480166465631L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 每一部分占用的位数</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> SEQUENCE_BIT = <span class="number">12</span>;   <span class="comment">//序列号占用的位数</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> MACHINE_BIT = <span class="number">5</span>;     <span class="comment">//机器标识占用的位数</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> DATA_CENTER_BIT = <span class="number">5</span>; <span class="comment">//数据中心占用的位数</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 每一部分的最大值</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> MAX_SEQUENCE = -<span class="number">1L</span> ^ (-<span class="number">1L</span> &lt;&lt; SEQUENCE_BIT);</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> MAX_MACHINE_NUM = -<span class="number">1L</span> ^ (-<span class="number">1L</span> &lt;&lt; MACHINE_BIT);</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> MAX_DATA_CENTER_NUM = -<span class="number">1L</span> ^ (-<span class="number">1L</span> &lt;&lt; DATA_CENTER_BIT);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 每一部分向左的位移</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> MACHINE_LEFT = SEQUENCE_BIT;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">long</span> TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> dataCenterId;  <span class="comment">//数据中心</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> machineId;     <span class="comment">//机器标识</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> sequence = <span class="number">0L</span>; <span class="comment">//序列号</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> lastTimeStamp = -<span class="number">1L</span>;  <span class="comment">//上一次时间戳</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">long</span> <span class="title">getNextMill</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">long</span> mill = getNewTimeStamp();</span><br><span class="line">        <span class="keyword">while</span> (mill &lt;= lastTimeStamp) &#123;</span><br><span class="line">            mill = getNewTimeStamp();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> mill;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">long</span> <span class="title">getNewTimeStamp</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> System.currentTimeMillis();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据指定的数据中心ID和机器标志ID生成指定的序列号</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> dataCenterId 数据中心ID</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> machineId    机器标志ID</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">SnowFlakeShortUrl</span><span class="params">(<span class="keyword">long</span> dataCenterId, <span class="keyword">long</span> machineId)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (dataCenterId &gt; MAX_DATA_CENTER_NUM || dataCenterId &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0！"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (machineId &gt; MAX_MACHINE_NUM || machineId &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"MachineId can't be greater than MAX_MACHINE_NUM or less than 0！"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">this</span>.dataCenterId = dataCenterId;</span><br><span class="line">        <span class="keyword">this</span>.machineId = machineId;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 产生下一个ID</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">long</span> <span class="title">nextId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">long</span> currTimeStamp = getNewTimeStamp();</span><br><span class="line">        <span class="keyword">if</span> (currTimeStamp &lt; lastTimeStamp) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"Clock moved backwards.  Refusing to generate id"</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (currTimeStamp == lastTimeStamp) &#123;</span><br><span class="line">            <span class="comment">//相同毫秒内，序列号自增</span></span><br><span class="line">            sequence = (sequence + <span class="number">1</span>) &amp; MAX_SEQUENCE;</span><br><span class="line">            <span class="comment">//同一毫秒的序列数已经达到最大</span></span><br><span class="line">            <span class="keyword">if</span> (sequence == <span class="number">0L</span>) &#123;</span><br><span class="line">                currTimeStamp = getNextMill();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">//不同毫秒内，序列号置为0</span></span><br><span class="line">            sequence = <span class="number">0L</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        lastTimeStamp = currTimeStamp;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> (currTimeStamp - START_TIMESTAMP) &lt;&lt; TIMESTAMP_LEFT <span class="comment">//时间戳部分</span></span><br><span class="line">                | dataCenterId &lt;&lt; DATA_CENTER_LEFT       <span class="comment">//数据中心部分</span></span><br><span class="line">                | machineId &lt;&lt; MACHINE_LEFT             <span class="comment">//机器标识部分</span></span><br><span class="line">                | sequence;                             <span class="comment">//序列号部分</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SnowFlakeShortUrl snowFlake = <span class="keyword">new</span> SnowFlakeShortUrl(<span class="number">2</span>, <span class="number">3</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; (<span class="number">1</span> &lt;&lt; <span class="number">4</span>); i++) &#123;</span><br><span class="line">            <span class="comment">//10进制</span></span><br><span class="line">            System.out.println(snowFlake.nextId());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="7、百度（uid-generator）"><a href="#7、百度（uid-generator）" class="headerlink" title="7、百度（uid-generator）"></a>7、百度（uid-generator）</h5><p><code>uid-generator</code>是由百度技术部开发，项目GitHub地址 <a href="https://github.com/baidu/uid-generator" target="_blank" rel="noopener">https://github.com/baidu/uid-generator</a></p><p><code>uid-generator</code>是基于<code>Snowflake</code>算法实现的，与原始的<code>snowflake</code>算法不同在于，<code>uid-generator</code>支持自<code>定义时间戳</code>、<code>工作机器ID</code>和 <code>序列号</code> 等各部分的位数，而且<code>uid-generator</code>中采用用户自定义<code>workId</code>的生成策略。</p><p><code>uid-generator</code>需要与数据库配合使用，需要新增一个<code>WORKER_NODE</code>表。当应用启动时会向数据库表中去插入一条数据，插入成功后返回的自增ID就是该机器的<code>workId</code>数据由host，port组成。</p><p><strong>对于<code>uid-generator</code> ID组成结构</strong>：</p><p><code>workId</code>，占用了22个bit位，时间占用了28个bit位，序列化占用了13个bit位，需要注意的是，和原始的<code>snowflake</code>不太一样，时间的单位是秒，而不是毫秒，<code>workId</code>也不一样，而且同一应用每次重启就会消费一个<code>workId</code>。</p><blockquote><p>参考文献<br><a href="https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md" target="_blank" rel="noopener">https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md</a></p></blockquote><h5 id="8、美团（Leaf）"><a href="#8、美团（Leaf）" class="headerlink" title="8、美团（Leaf）"></a>8、美团（Leaf）</h5><p><code>Leaf</code>由美团开发，github地址：<a href="https://github.com/Meituan-Dianping/Leaf" target="_blank" rel="noopener">https://github.com/Meituan-Dianping/Leaf</a></p><p><code>Leaf</code>同时支持号段模式和<code>snowflake</code>算法模式，可以切换使用。</p><h5 id="号段模式"><a href="#号段模式" class="headerlink" title="号段模式"></a>号段模式</h5><p>先导入源码 <a href="https://github.com/Meituan-Dianping/Leaf" target="_blank" rel="noopener">https://github.com/Meituan-Dianping/Leaf</a> ，在建一张表<code>leaf_alloc</code></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">DROP</span> <span class="keyword">TABLE</span> <span class="keyword">IF</span> <span class="keyword">EXISTS</span> <span class="string">`leaf_alloc`</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`leaf_alloc`</span> (</span><br><span class="line">  <span class="string">`biz_tag`</span> <span class="built_in">varchar</span>(<span class="number">128</span>)  <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span> <span class="keyword">COMMENT</span> <span class="string">'业务key'</span>,</span><br><span class="line">  <span class="string">`max_id`</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'1'</span> <span class="keyword">COMMENT</span> <span class="string">'当前已经分配了的最大id'</span>,</span><br><span class="line">  <span class="string">`step`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'初始步长，也是动态调整的最小步长'</span>,</span><br><span class="line">  <span class="string">`description`</span> <span class="built_in">varchar</span>(<span class="number">256</span>)  <span class="keyword">DEFAULT</span> <span class="literal">NULL</span> <span class="keyword">COMMENT</span> <span class="string">'业务key的描述'</span>,</span><br><span class="line">  <span class="string">`update_time`</span> <span class="built_in">timestamp</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="keyword">CURRENT_TIMESTAMP</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="keyword">CURRENT_TIMESTAMP</span> <span class="keyword">COMMENT</span> <span class="string">'数据库维护的更新时间'</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (<span class="string">`biz_tag`</span>)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span>;</span><br></pre></td></tr></table></figure><p>然后在项目中开启<code>号段模式</code>，配置对应的数据库信息，并关闭<code>snowflake</code>模式</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">leaf.name=com.sankuai.leaf.opensource.test</span><br><span class="line">leaf.segment.enable=true</span><br><span class="line">leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&amp;characterEncoding=utf8&amp;characterSetResults=utf8</span><br><span class="line">leaf.jdbc.username=root</span><br><span class="line">leaf.jdbc.password=root</span><br><span class="line"></span><br><span class="line">leaf.snowflake.enable=false</span><br><span class="line">#leaf.snowflake.zk.address=</span><br><span class="line">#leaf.snowflake.port=</span><br></pre></td></tr></table></figure><p>启动<code>leaf-server</code> 模块的 <code>LeafServerApplication</code>项目就跑起来了</p><p>号段模式获取分布式自增ID的测试url ：http：//localhost：8080/api/segment/get/leaf-segment-test</p><p>监控号段模式：<a href="http://localhost:8080/cache" target="_blank" rel="noopener">http://localhost:8080/cache</a></p><h5 id="snowflake模式"><a href="#snowflake模式" class="headerlink" title="snowflake模式"></a>snowflake模式</h5><p><code>Leaf</code>的snowflake模式依赖于<code>ZooKeeper</code>，不同于<code>原始snowflake</code>算法也主要是在<code>workId</code>的生成上，<code>Leaf</code>中<code>workId</code>是基于<code>ZooKeeper</code>的顺序Id来生成的，每个应用在使用<code>Leaf-snowflake</code>时，启动时都会都在<code>Zookeeper</code>中生成一个顺序Id，相当于一台机器对应一个顺序节点，也就是一个<code>workId</code>。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">leaf.snowflake.enable=true</span><br><span class="line">leaf.snowflake.zk.address=127.0.0.1</span><br><span class="line">leaf.snowflake.port=2181</span><br></pre></td></tr></table></figure><p>snowflake模式获取分布式自增ID的测试url：<a href="http://localhost:8080/api/snowflake/get/test" target="_blank" rel="noopener">http://localhost:8080/api/snowflake/get/test</a></p><h5 id="9、滴滴（Tinyid）"><a href="#9、滴滴（Tinyid）" class="headerlink" title="9、滴滴（Tinyid）"></a>9、滴滴（Tinyid）</h5><p><code>Tinyid</code>由滴滴开发，Github地址：<a href="https://github.com/didi/tinyid。" target="_blank" rel="noopener">https://github.com/didi/tinyid。</a></p><p><code>Tinyid</code>是基于号段模式原理实现的与<code>Leaf</code>如出一辙，每个服务获取一个号段（1000,2000]、（2000,3000]、（3000,4000]</p><p><img src="/images/image-20210725165640092.png" alt="image-20210725165640092"></p>"<p><code>Tinyid</code>提供<code>http</code>和<code>tinyid-client</code>两种方式接入</p><h5 id="Http方式接入"><a href="#Http方式接入" class="headerlink" title="Http方式接入"></a>Http方式接入</h5><p>（1）导入Tinyid源码：</p><p>git clone <a href="https://github.com/didi/tinyid.git" target="_blank" rel="noopener">https://github.com/didi/tinyid.git</a></p><p>（2）创建数据表：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`tiny_id_info`</span> (</span><br><span class="line">  <span class="string">`id`</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">unsigned</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> AUTO_INCREMENT <span class="keyword">COMMENT</span> <span class="string">'自增主键'</span>,</span><br><span class="line">  <span class="string">`biz_type`</span> <span class="built_in">varchar</span>(<span class="number">63</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span> <span class="keyword">COMMENT</span> <span class="string">'业务类型，唯一'</span>,</span><br><span class="line">  <span class="string">`begin_id`</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'0'</span> <span class="keyword">COMMENT</span> <span class="string">'开始id，仅记录初始值，无其他含义。初始化时begin_id和max_id应相同'</span>,</span><br><span class="line">  <span class="string">`max_id`</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'0'</span> <span class="keyword">COMMENT</span> <span class="string">'当前最大id'</span>,</span><br><span class="line">  <span class="string">`step`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">DEFAULT</span> <span class="string">'0'</span> <span class="keyword">COMMENT</span> <span class="string">'步长'</span>,</span><br><span class="line">  <span class="string">`delta`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'1'</span> <span class="keyword">COMMENT</span> <span class="string">'每次id增量'</span>,</span><br><span class="line">  <span class="string">`remainder`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'0'</span> <span class="keyword">COMMENT</span> <span class="string">'余数'</span>,</span><br><span class="line">  <span class="string">`create_time`</span> <span class="built_in">timestamp</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'2010-01-01 00:00:00'</span> <span class="keyword">COMMENT</span> <span class="string">'创建时间'</span>,</span><br><span class="line">  <span class="string">`update_time`</span> <span class="built_in">timestamp</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'2010-01-01 00:00:00'</span> <span class="keyword">COMMENT</span> <span class="string">'更新时间'</span>,</span><br><span class="line">  <span class="string">`version`</span> <span class="built_in">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'0'</span> <span class="keyword">COMMENT</span> <span class="string">'版本号'</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (<span class="string">`id`</span>),</span><br><span class="line">  <span class="keyword">UNIQUE</span> <span class="keyword">KEY</span> <span class="string">`uniq_biz_type`</span> (<span class="string">`biz_type`</span>)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> AUTO_INCREMENT=<span class="number">1</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8 <span class="keyword">COMMENT</span> <span class="string">'id信息表'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`tiny_id_token`</span> (</span><br><span class="line">  <span class="string">`id`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">unsigned</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> AUTO_INCREMENT <span class="keyword">COMMENT</span> <span class="string">'自增id'</span>,</span><br><span class="line">  <span class="string">`token`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span> <span class="keyword">COMMENT</span> <span class="string">'token'</span>,</span><br><span class="line">  <span class="string">`biz_type`</span> <span class="built_in">varchar</span>(<span class="number">63</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span> <span class="keyword">COMMENT</span> <span class="string">'此token可访问的业务类型标识'</span>,</span><br><span class="line">  <span class="string">`remark`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span> <span class="keyword">COMMENT</span> <span class="string">'备注'</span>,</span><br><span class="line">  <span class="string">`create_time`</span> <span class="built_in">timestamp</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'2010-01-01 00:00:00'</span> <span class="keyword">COMMENT</span> <span class="string">'创建时间'</span>,</span><br><span class="line">  <span class="string">`update_time`</span> <span class="built_in">timestamp</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'2010-01-01 00:00:00'</span> <span class="keyword">COMMENT</span> <span class="string">'更新时间'</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (<span class="string">`id`</span>)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> AUTO_INCREMENT=<span class="number">1</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8 <span class="keyword">COMMENT</span> <span class="string">'token信息表'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`tiny_id_info`</span> (<span class="string">`id`</span>, <span class="string">`biz_type`</span>, <span class="string">`begin_id`</span>, <span class="string">`max_id`</span>, <span class="string">`step`</span>, <span class="string">`delta`</span>, <span class="string">`remainder`</span>, <span class="string">`create_time`</span>, <span class="string">`update_time`</span>, <span class="string">`version`</span>)</span><br><span class="line"><span class="keyword">VALUES</span></span><br><span class="line">    (<span class="number">1</span>, <span class="string">'test'</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">100000</span>, <span class="number">1</span>, <span class="number">0</span>, <span class="string">'2018-07-21 23:52:58'</span>, <span class="string">'2018-07-22 23:19:27'</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`tiny_id_info`</span> (<span class="string">`id`</span>, <span class="string">`biz_type`</span>, <span class="string">`begin_id`</span>, <span class="string">`max_id`</span>, <span class="string">`step`</span>, <span class="string">`delta`</span>, <span class="string">`remainder`</span>, <span class="string">`create_time`</span>, <span class="string">`update_time`</span>, <span class="string">`version`</span>)</span><br><span class="line"><span class="keyword">VALUES</span></span><br><span class="line">    (<span class="number">2</span>, <span class="string">'test_odd'</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">100000</span>, <span class="number">2</span>, <span class="number">1</span>, <span class="string">'2018-07-21 23:52:58'</span>, <span class="string">'2018-07-23 00:39:24'</span>, <span class="number">3</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`tiny_id_token`</span> (<span class="string">`id`</span>, <span class="string">`token`</span>, <span class="string">`biz_type`</span>, <span class="string">`remark`</span>, <span class="string">`create_time`</span>, <span class="string">`update_time`</span>)</span><br><span class="line"><span class="keyword">VALUES</span></span><br><span class="line">    (<span class="number">1</span>, <span class="string">'0f673adf80504e2eaa552f5d791b644c'</span>, <span class="string">'test'</span>, <span class="string">'1'</span>, <span class="string">'2017-12-14 16:36:46'</span>, <span class="string">'2017-12-14 16:36:48'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`tiny_id_token`</span> (<span class="string">`id`</span>, <span class="string">`token`</span>, <span class="string">`biz_type`</span>, <span class="string">`remark`</span>, <span class="string">`create_time`</span>, <span class="string">`update_time`</span>)</span><br><span class="line"><span class="keyword">VALUES</span></span><br><span class="line">    (<span class="number">2</span>, <span class="string">'0f673adf80504e2eaa552f5d791b644c'</span>, <span class="string">'test_odd'</span>, <span class="string">'1'</span>, <span class="string">'2017-12-14 16:36:46'</span>, <span class="string">'2017-12-14 16:36:48'</span>);</span><br></pre></td></tr></table></figure><p>（3）配置数据库：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">datasource.tinyid.names=primary</span><br><span class="line">datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver</span><br><span class="line">datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8</span><br><span class="line">datasource.tinyid.primary.username=root</span><br><span class="line">datasource.tinyid.primary.password=123456</span><br></pre></td></tr></table></figure><p>（4）启动<code>tinyid-server</code>后测试</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&amp;token=0f673adf80504e2eaa552f5d791b644c&apos;</span><br><span class="line">返回结果: 3</span><br><span class="line"></span><br><span class="line">批量获取分布式自增ID:</span><br><span class="line">http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&amp;token=0f673adf80504e2eaa552f5d791b644c&amp;batchSize=10&apos;</span><br><span class="line">返回结果:  4,5,6,7,8,9,10,11,12,13</span><br></pre></td></tr></table></figure><h5 id="Java客户端方式接入"><a href="#Java客户端方式接入" class="headerlink" title="Java客户端方式接入"></a>Java客户端方式接入</h5><p>重复Http方式的（2）（3）操作</p><p>引入依赖</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">     <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.xiaoju.uemc.tinyid<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">     <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>tinyid-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">     <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;tinyid.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>配置文件</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">tinyid.server =localhost:9999</span><br><span class="line">tinyid.token =0f673adf80504e2eaa552f5d791b644c</span><br></pre></td></tr></table></figure><p><code>test</code> 、<code>tinyid.token</code>是在数据库表中预先插入的数据，<code>test</code> 是具体业务类型，<code>tinyid.token</code>表示可访问的业务类型</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取单个分布式自增ID</span></span><br><span class="line">Long id =  TinyId . nextId( <span class="string">" test "</span> );</span><br><span class="line"></span><br><span class="line"><span class="comment">// 按需批量分布式自增ID</span></span><br><span class="line">List&lt; Long &gt; ids =  TinyId . nextId( <span class="string">" test "</span> , <span class="number">10</span> );</span><br></pre></td></tr></table></figure><h1 id="备注"><a href="#备注" class="headerlink" title="备注"></a>备注</h1><p>参考自：<a href="https://mp.weixin.qq.com/s?__biz=MzAxNTM4NzAyNg==&amp;mid=2247483785&amp;idx=1&amp;sn=8b828a8ae1701b810fe3969be536cb14&amp;chksm=9b859174acf21862f0b95e0502a1a441c496a5488f5466b2e147d7bb9de072bde37c4db25d7a&amp;token=745402269&amp;lang=zh_CN#rd" target="_blank" rel="noopener">分布式id</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h4 id=&quot;一、为什么要用分布式ID？&quot;&gt;&lt;a href=&quot;#一、为什么要用分布式ID？&quot; class=&quot;headerlink&quot; title=&quot;一、为什么要用分布式ID？&quot;&gt;&lt;/a&gt;一、为什么要用分布式ID？&lt;/h4&gt;&lt;p&gt;在说分布式ID的具体实现之前，我们来简单分析一下为什
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>分布式一致性协议之2PC和3PC</title>
    <link href="http://yoursite.com/2021/07/22/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE%E4%B9%8B2PC%E5%92%8C3PC/"/>
    <id>http://yoursite.com/2021/07/22/分布式/分布式一致性协议之2PC和3PC/</id>
    <published>2021-07-22T02:58:05.934Z</published>
    <updated>2021-07-25T10:09:45.046Z</updated>
    
    <content type="html"><![CDATA[<p>由于分布式系统的各个服务可能分布在不同的节点上，如果各节点直接没有相互的通信获取其他节点状态，那么各个节点是无法知道其他节点的任务处理结果的。</p><p>如果在分布式系统中发起一个事务，该事务涉及多个不同节点，那么为了保证事务 ACID 特性，就需要引入一个协调者来统一调度事务涉及的多个节点，被调度的节点称为事务参与者。由此衍生出 2PC 和 3PC 协议，本文就来详细介绍 2PC 和 3PC 的工作机制。</p><h1 id="2PC-两阶段提交，Two-Phase-Commit"><a href="#2PC-两阶段提交，Two-Phase-Commit" class="headerlink" title="2PC(两阶段提交，Two-Phase Commit)"></a>2PC(两阶段提交，Two-Phase Commit)</h1><p>顾名思义，分为两个阶段：Prepare 和 Commit</p><h2 id="Prepare：提交事务请求"><a href="#Prepare：提交事务请求" class="headerlink" title="Prepare：提交事务请求"></a>Prepare：提交事务请求</h2><p>基本流程如下图：</p><p><img src="/images/image-20210722132355791.png" alt="image-20210722132355791"></p>"<ol><li>询问 协调者向所有参与者发送事务请求，询问是否可执行事务操作，然后等待各个参与者的响应。</li><li>执行 各个参与者接收到协调者事务请求后，执行事务操作(例如更新一个关系型数据库表中的记录)，并将 Undo 和 Redo 信息记录事务日志中。</li><li>响应 如果参与者成功执行了事务并写入 Undo 和 Redo 信息，则向协调者返回 YES 响应，否则返回 NO 响应。当然，参与者也可能宕机，从而不会返回响应。</li></ol><h2 id="Commit：执行事务提交"><a href="#Commit：执行事务提交" class="headerlink" title="Commit：执行事务提交"></a>Commit：执行事务提交</h2><p>执行事务提交分为两种情况，正常提交和回退。</p><h3 id="正常提交事务"><a href="#正常提交事务" class="headerlink" title="正常提交事务"></a>正常提交事务</h3><p>流程如下图：</p><p><img src="/images/image-20210725170447566.png" alt="image-20210725170447566"></p>"<ol><li>commit 请求 协调者向所有参与者发送 Commit 请求。</li><li>事务提交 参与者收到 Commit 请求后，执行事务提交，提交完成后释放事务执行期占用的所有资源。</li><li>反馈结果 参与者执行事务提交后向协调者发送 Ack 响应。</li><li>完成事务 接收到所有参与者的 Ack 响应后，完成事务提交。</li></ol><h3 id="中断事务"><a href="#中断事务" class="headerlink" title="中断事务"></a>中断事务</h3><p>在执行 Prepare 步骤过程中，如果某些参与者执行事务失败、宕机或与协调者之间的网络中断，那么协调者就无法收到所有参与者的 YES 响应，或者某个参与者返回了 No 响应，此时，协调者就会进入回退流程，对事务进行回退。流程如下图红色部分(将 Commit 请求替换为红色的 Rollback 请求)：</p><p><img src="/images/image-20210725170631168.png" alt="image-20210725170631168"></p>"<ol><li>rollback 请求 协调者向所有参与者发送 Rollback 请求。</li><li>事务回滚 参与者收到 Rollback 后，使用 Prepare 阶段的 Undo 日志执行事务回滚，完成后释放事务执行期占用的所有资源。</li><li>反馈结果 参与者执行事务回滚后向协调者发送 Ack 响应。</li><li>中断事务 接收到所有参与者的 Ack 响应后，完成事务中断。</li></ol><h2 id="2PC-的问题"><a href="#2PC-的问题" class="headerlink" title="2PC 的问题"></a>2PC 的问题</h2><ol><li><p>同步阻塞 </p><p>参与者在等待协调者的指令时，其实是在等待其他参与者的响应，在此过程中，参与者是无法进行其他操作的，也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息，那么会导致参与者一直阻塞下去。</p></li><li><p>单点 </p><p>在 2PC 中，一切请求都来自协调者，所以协调者的地位是至关重要的，如果协调者宕机，那么就会使参与者一直阻塞并一直占用事务资源。</p><p>如果协调者也是分布式，使用选主方式提供服务，那么在一个协调者挂掉后，可以选取另一个协调者继续后续的服务，可以解决单点问题。但是，新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长等)，所以也无法顺利处理上一个事务。</p></li><li><p>数据不一致 </p><p>Commit 事务过程中 Commit 请求/Rollback 请求可能因为协调者宕机或协调者与参与者网络问题丢失，那么就导致了部分参与者没有收到 Commit/Rollback 请求，而其他参与者则正常收到执行了 Commit/Rollback 操作，没有收到请求的参与者则继续阻塞。这时，参与者之间的数据就不再一致了。</p><p>当参与者执行 Commit/Rollback 后会向协调者发送 Ack，然而协调者不论是否收到所有的参与者的 Ack，该事务也不会再有其他补救措施了，协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成功”。</p></li><li><p>环境可靠性依赖 </p><p>协调者 Prepare 请求发出后，等待响应，然而如果有参与者宕机或与协调者之间的网络中断，都会导致协调者无法收到所有参与者的响应，那么在 2PC 中，协调者会等待一定时间，然后超时后，会触发事务中断，在这个过程中，协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。</p></li></ol><h1 id="3PC-三阶段提交，Three-Phase-Commit"><a href="#3PC-三阶段提交，Three-Phase-Commit" class="headerlink" title="3PC(三阶段提交，Three-Phase Commit)"></a>3PC(三阶段提交，Three-Phase Commit)</h1><p>上面说明了 2PC 协议的多个缺点，那么 3PC 就是在 2PC 的基础上，为了解决 2PC 的某些缺点而设计的，3PC 分为三个阶段：CanCommit，PreCommit 和 doCommit。</p><h2 id="CanCommit"><a href="#CanCommit" class="headerlink" title="CanCommit"></a>CanCommit</h2><p>流程如下图：</p><p><img src="/images/image-20210725171459893.png" alt="image-20210725171459893"></p>"<ol><li><p>事务询问 协调者向所有参与者发送事务 canCommit 请求，请求中包含事务内容，询问是否可以执行事务提交操作，并开始等待响应。</p></li><li><p>反馈询问结果 参与者收到 canCommit 请求后，分析事务内容，判断自身是否可以执行事务，如果可以，那么就返回 Yes 响应，进入预备状态，否则返回 No 响应。</p><p>注意：此过程中并没有执行事务(对比 2PC 的 Prepare 阶段，参与者是执行了事务的)。</p></li></ol><h2 id="PreCommit"><a href="#PreCommit" class="headerlink" title="PreCommit"></a>PreCommit</h2><p>流程图如下：</p><p><img src="/images/image-20210725175826520.png" alt="image-20210725175826520"></p>"<p>PreCommit 阶段根据各参与者返回的 CanCommit 响应，决定下一步动作。如果收到了所有参与者的 Yes 响应，则执行事务预提交，否则(收到了至少一个 No 响应或一定时长内没有收到所有参与者的 Yes 响应，如 3PC 第一张图片中红色部分)，执行事务中断。</p><h3 id="事务预提交"><a href="#事务预提交" class="headerlink" title="事务预提交"></a>事务预提交</h3><ol><li>发送 PreCommit 请求 协调者发送 PreCommit 请求，并进入 Prepared 阶段。</li><li>参与者处理 PreCommit 参与者收到 PreCommit 请求后，执行事务操作，并将 Undo 和 Redo 信息记录事务日志中。</li><li>反馈执行结果 如果参与者成功执行了事务并写入 Undo 和 Redo 信息，那么反馈 Ack 给协调者，并等待下一步指令。</li></ol><h3 id="事务中断"><a href="#事务中断" class="headerlink" title="事务中断"></a>事务中断</h3><p>上图中，红色的 Abort 表示协调者发送的不是 PreCommit 请求，而是 Abort 请求。</p><ol><li>发送事务中断请求 协调者向所有参与者发送 Abort 请求。</li><li>中断事务 参与者收到 Abort 请求后，会触发事务中断。此外，如果参与者在等待协调者指令超时，会自己触发事务中断，在 2PC 中，参与者会一直阻塞的等待协调者指令，所以 3PC 中解决了因为这种情况带来的阻塞。</li></ol><h2 id="doCommit"><a href="#doCommit" class="headerlink" title="doCommit"></a>doCommit</h2><p>流程图如下：</p><p><img src="/images/image-20210725180239277.png" alt="image-20210725180239277"></p>"<p>协调者根据第二阶段的响应决定最终操作，如果协调者收到了所有参与者在 PreCommit 阶段的 Ack 响应，那么会进入执行事务提交阶段，否则执行事务中断。</p><h3 id="事务提交"><a href="#事务提交" class="headerlink" title="事务提交"></a>事务提交</h3><ol><li>发送提交请求 协调者收到所有参与者在 PreCommit 阶段返回的 Ack 响应后，向所有参与者发送 doCommit 请求，并进入提交状态。</li><li>事务提交 参与者收到 Commit 请求后，执行事务提交，提交完成后释放事务执行期占用的所有资源。</li><li>反馈结果 参与者完成事务提交之后，向协调者返回 Ack 响应。</li><li>完成事务 协调者收到所有参与者的 Ack 响应后，完成事务。</li></ol><h3 id="事务中断-1"><a href="#事务中断-1" class="headerlink" title="事务中断"></a>事务中断</h3><ol><li><p>发送事务中断请求 协调者向所有参与者发送 Abort 请求。</p></li><li><p>事务回滚 参与者收到 Abort 请求后，会使用第二阶段记录的 Undo 信息进行事务回滚，并在完成回滚后释放所有事务资源。</p><p>注意：因为第一阶段并没有任何参与者实际执行事务，所以在第二阶段(PreCommit 阶段)执行事务中断，是不需要事务回滚的，也就不需要下面的反馈结果，直接中断事务即可。</p></li><li><p>反馈回滚结果 参与者执行事务回滚后向协调者发送 Ack 响应。</p></li><li><p>中断事务 协调者接收到所有参与者反馈的 Ack 响应后，完成事务中断。</p></li></ol><h2 id="3PC-的改进和缺点"><a href="#3PC-的改进和缺点" class="headerlink" title="3PC 的改进和缺点"></a>3PC 的改进和缺点</h2><h3 id="改进"><a href="#改进" class="headerlink" title="改进"></a>改进</h3><ol><li>降低了阻塞<ul><li>参与者返回 CanCommit 请求的响应后，等待第二阶段指令，若等待超时，则自动 abort，降低了阻塞；</li><li>参与者返回 PreCommit 请求的响应后，等待第三阶段指令，若等待超时，则自动 commit 事务，也降低了阻塞；</li></ul></li><li>解决单点故障问题<ul><li>参与者返回 CanCommit 请求的响应后，等待第二阶段指令，若协调者宕机，等待超时后自动 abort，；</li><li>参与者返回 PreCommit 请求的响应后，等待第三阶段指令，若协调者宕机，等待超时后自动 commit 事务；</li></ul></li></ol><h3 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h3><p>数据不一致问题仍然是存在的，比如第三阶段协调者发出了 abort 请求，然后有些参与者没有收到 abort，那么就会自动 commit，造成数据不一致。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>从上面讲述来看，2PC 和 3PC 都无法完美解决分布式数据一致性问题，虽然无法保证事务的 ACID 特性，但两阶段的思想在很多实际架构中有这广泛应用，例如 JTA 事务以及一些数据库的数据同步。</p><p>引用一句话，是 Google Chubby 作者说的：</p><p>“There is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos.”</p><p>Paxos 是兰伯特提出的一个解决分布式一致性的算法，后面再写文讲述其原理。</p><h1 id="备注"><a href="#备注" class="headerlink" title="备注"></a>备注</h1><p>参考自:<a href="https://juejin.cn/post/6844903814898647048#heading-16" target="_blank" rel="noopener">分布式一致性协议之2PC和3PC</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;由于分布式系统的各个服务可能分布在不同的节点上，如果各节点直接没有相互的通信获取其他节点状态，那么各个节点是无法知道其他节点的任务处理结果的。&lt;/p&gt;
&lt;p&gt;如果在分布式系统中发起一个事务，该事务涉及多个不同节点，那么为了保证事务 ACID 特性，就需要引入一个协调者来统一
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>拜占庭将军问题</title>
    <link href="http://yoursite.com/2021/07/20/%E5%88%86%E5%B8%83%E5%BC%8F/%E6%8B%9C%E5%8D%A0%E5%BA%AD%E9%97%AE%E9%A2%98/"/>
    <id>http://yoursite.com/2021/07/20/分布式/拜占庭问题/</id>
    <published>2021-07-20T12:46:08.041Z</published>
    <updated>2021-07-20T12:54:15.961Z</updated>
    
    <content type="html"><![CDATA[<p>一个数据在一个节点需要同步到另外一个节点的过程中，在未完成同步的时候，会出现数据不一致的情况，所以此时必然存在分区容错性（Partition tolerance）。分布式系统只能从一致性（Consistency）或可用性（Availability）之间去选择。</p><p>CAP讲的是分布式一致性，而这次我们来聊聊分布式共识性。很多开发者一直以为一致性与共识性是同一个东西，但两者讲的是完全不同的东西。</p><ul><li>一致性：A点同步B点数据，然后两者之间的数据可以达成一致。</li><li>共识性：一个或多个节点提议了一个值应当是什么后，采用一种大家都认可的方法，使得系统中所有进程对这个值达成一致意见。</li></ul><p><img src="/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_d91e2c0f-1eff-4a66-82a2-fc4e2559cfc8.png" alt="企业微信截图_d91e2c0f-1eff-4a66-82a2-fc4e2559cfc8"></p>"<p>共识性比较常见的场景就是选主，例如redis主挂掉了，集群通用共识性算法选出一个主。比特币之类的电子货币也需要更复杂的共识性算法。</p><p>下面我们一步步聊下分布式共识性的一些常见算法与问题。</p><h1 id="拜占庭将军问题"><a href="#拜占庭将军问题" class="headerlink" title="拜占庭将军问题"></a>拜占庭将军问题</h1><p>Leslie Lamport(论文排版系统LaTeX的开发者，同时也是2013年的图灵奖得主)在其论文中描述了如下系统：</p><blockquote><p>一组拜占庭将军分别各率领一支军队共同围困一座城市。</p><p>为了简化模型，将各支军队的行动策略限定为进攻或撤离两种。因为部分军队进攻部分军队撤离可能会造成灾难性后果，因此各位将军必须通过投票来达成一致策略，即所有军队一起进攻或所有军队一起撤离。</p><p>同时各位将军分处城市不同方向，他们只能通过信使互相联系。在投票过程中每位将军都将自己投票给进攻还是撤退的信息通过信使分别通知其他所有将军，这样一来每位将军根据自己的投票和其他所有将军送来的信息就可以知道共同的投票结果而决定行动策略。</p></blockquote><p>此系统的名字叫做<strong>拜占庭将军问题</strong>。从描述中，可以显然知道，将军们需要通过少数服从多数的算法在分布式的场景下进行投票决议一个一致性的决定去执行。</p><p>在拜占庭将军问题中，默认是认为信使是不会被截获并且消息会传递到的。更多的情况中，将军中可能会出现叛徒、信使会被截获冒充、消息无法到达。而叛徒或信使冒充会恶意地向其他将军投票，给不同将军展示不同的投票结果，从而破坏了将军们执行的一致性。而此类错误则称为<strong>拜占庭错误</strong>。</p><p>如果系统能处理拜占庭将军错误正常运行的话，则称系统拥有<strong>拜占庭容错「Byzantine fault tolerance」</strong>，简称为<strong>BFT</strong>。</p><h1 id="举个例子"><a href="#举个例子" class="headerlink" title="举个例子"></a>举个例子</h1><p>假设当时有5位将军投票（单数投票的结果必能形成少数服从多数），其中有1名是叛徒，4名忠诚的将军中出2人投进攻，2人投撤离的。</p><p>这时候叛徒可能故意给2名投进攻的将军送信表示投进攻，而给另外2名投撤离的将军送信表示投撤离。这样在2名投进攻的将领看来，投票结果是3人投进攻，从而发起进攻；而在2名投撤离的将军看来则是3人投撤离。这样各支军队的一致协同就遭到了破坏，结果是灾难性的。</p><p><img src="/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_55bfbd22-4717-4212-911e-3b18d2df08fb.png" alt="企业微信截图_55bfbd22-4717-4212-911e-3b18d2df08fb"></p>"<p>即使这5个将军都是忠诚的，但投票结果是需要信使在将军之间去传递的，而这些信使在传递过程中是有可能被截冒充或者并没有传递到将军的投票结果，最终还是会影响军队的一致协同。</p><p>上述的故事映射到计算机系统中，将军便成了计算机，而信使则是通信系统。有人会觉得这个问题可以通过加密或签名的方式解决，但本质上加密过程、签名算法也会出错。虽然加密和签名一定程度是可以解决这个问题，但这个问题并不是要讨论这些加密签名的强度，而是更多地在于研究集群系统客观上已经出现错误了，他们要怎么在存在错误的情况下让系统正常的工作。</p><h1 id="经典的简单解决"><a href="#经典的简单解决" class="headerlink" title="经典的简单解决"></a>经典的简单解决</h1><p>首先要知道，为什么这个标题是<strong>经典的简单解决</strong>？因为这个解决只是个简单的解决，在现代系统很多场景中，并不具有普遍的解决能力。</p><p>大家看完上面的例子，可能会涌现一种想法，就是在收到来自同一个将军的投票后，交换各自的结果检验看该将军是否叛徒。例如A将军把进攻指令发给B将军，把撤离指令发给C将军，那么BC将军交互一下来自A将军的指令，就可以知道A将军是个叛徒，然后把他揪出来干掉，不再听他的指令。</p><p>但是这种做法根本不能解决问题。虽然在BC交换指令后，可以知道有叛徒的存在，但其实你并不能确定A就是叛徒，因为有可能BC交换指令的过程出现”拜错“，所以上面的思路并不能解决问题。</p><p>回到问题本身，我们是需要在存在错误的情况下让系统正常进行，所以我们只需要设计一套系统在兼容这些”叛徒“就足够了。怎么理解？回到拜占庭军队上，拜占庭军队攻下一座城池至少需要6个将军，那么让军队装备更多将军，例如10个，在通过两两交互指令验证完消息后，可以知道有多少个叛徒的存在。只要忠诚的将军数大于等于6那么就可以执行指令（进攻或撤离），否则军队则按兵不动。这个容错率可以根据自己的系统进行设置，在这个方案被提出时，容错率描述是1/3。</p><p><img src="/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_9b7c86b3-0655-4fc3-be1a-c3db9e20977f.png" alt="企业微信截图_9b7c86b3-0655-4fc3-be1a-c3db9e20977f"></p>"<p>开头也说到这个方案在现代系统并不具有普遍解决问题的能力。一是类似比特币这种分布式记账本千千万个节点，如果要进行两两的信息验证，这个过程和开销是非常大的，会变得不实际。另外就是并不是所有性质的系统都能允许错误节点的执行，例如注册中心、交易中心等。</p><h1 id="先进的解决——比特币的工作量证明"><a href="#先进的解决——比特币的工作量证明" class="headerlink" title="先进的解决——比特币的工作量证明"></a>先进的解决——比特币的工作量证明</h1><p>在“简单解决”的方案提出之后，有非常多的方案算法被提出，实用拜占庭容错（PBFT）、联邦拜占庭协议（FBA）、授权拜占庭容错算法（dBFT）等等。由于其中的复杂度与文章篇幅问题，不一一赘述，有兴趣可以到网上查阅。</p><p>但其中一个比较有意思的是比特币中所用到的<strong>工作量证明「Proof Of Work，POW」</strong>可以大概提一下。</p><blockquote><p><strong>工作量证明</strong>是一种对应服务与资源滥用、或是拒绝服务攻击的经济对策。一般是要求用户进行一些耗时适当的复杂运算，并且答案能被服务方快速验算，以此耗用的时间、设备与能源做为担保成本，以确保服务与资源是被真正的需求所使用。（来自维基百科的解释）</p></blockquote><p><img src="/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_a507a70a-86e3-43d6-b9d8-5c7901dad6d7.png" alt="企业微信截图_a507a70a-86e3-43d6-b9d8-5c7901dad6d7"></p>"<p>结合比特币的场景去理解，用户是需要通过挖矿来获得比特币，而挖矿是需要花费大量的计算资源的。这个挖矿的过程其实是比特币设计的一道解密算法，用户（节点）是需要一定量的计算才能获得答案，然后其他给节点验算，成功后最终获得比特币奖励争取记账权。一句话概括工作量证明就是不校验你的过程，只看你的结果，但获取这个结果是有壁垒的。具体的算法原理在后续讲到共识性算法的应用我们再用新篇幅去阐述。</p><p>那么比特币怎样才能造假呢？其实它本质依然是少数服从多数的投票，节点获取结果后是需要其他节点进行验证投票的，如果你拥有大于50%的假节点，的确是可以篡改数据，控制交易。但是工作量证明引入使得构造一个节点的成本已经足够大了，在千千万的节点下想要构造大于50%的假节点，估计有这种财力去实现的人已经可以统治地球了。</p><p>拜占庭将军错误看似一个非常严重的问题，能造成灾难性后果，但其实在大部分场景下并不会出现“拜错”。下一篇将会落到比较应用层面的共识性算法，聊下市面上主流的分布式中间件是怎么在不考虑“拜错”的情况下，解决分布式共识性问题的。</p><h1 id="备注"><a href="#备注" class="headerlink" title="备注"></a>备注</h1><p>摘自：<a href="https://juejin.cn/post/6844903991168466952" target="_blank" rel="noopener">拜占庭问题</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;一个数据在一个节点需要同步到另外一个节点的过程中，在未完成同步的时候，会出现数据不一致的情况，所以此时必然存在分区容错性（Partition tolerance）。分布式系统只能从一致性（Consistency）或可用性（Availability）之间去选择。&lt;/p&gt;
&lt;p
      
    
    </summary>
    
    
      <category term="拜占庭问题" scheme="http://yoursite.com/tags/%E6%8B%9C%E5%8D%A0%E5%BA%AD%E9%97%AE%E9%A2%98/"/>
    
  </entry>
  
  <entry>
    <title>复盘</title>
    <link href="http://yoursite.com/2021/06/24/%E5%B7%A5%E7%A8%8B/%E5%A4%8D%E7%9B%98&amp;%E6%80%BB%E7%BB%93/"/>
    <id>http://yoursite.com/2021/06/24/工程/复盘&amp;总结/</id>
    <published>2021-06-24T09:51:10.134Z</published>
    <updated>2022-04-07T14:09:53.154Z</updated>
    
    <content type="html"><![CDATA[<table><thead><tr><th style="text-align:left">阶段</th><th style="text-align:left">步骤</th><th style="text-align:left">需要解决的问题</th><th style="text-align:left">常用工具</th></tr></thead><tbody><tr><td style="text-align:left">准备阶段</td><td style="text-align:left">策划团队复盘方案</td><td style="text-align:left">对什么进行复盘？达到什么目标?涉及到哪些工作？那些人需要参加？</td><td style="text-align:left">列清单检查</td></tr><tr><td style="text-align:left"></td><td style="text-align:left">组织团队复盘会议</td><td style="text-align:left">复盘会议的时间、地点、以何种形式召开？需要多长时间？会议的议程和分工？</td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">提前准备</td><td style="text-align:left">那些人需要做哪些准备？提供哪些资料？</td><td style="text-align:left"></td></tr><tr><td style="text-align:left">会议阶段</td><td style="text-align:left">开场</td><td style="text-align:left">让与会者明确会议的目的、议程以及规则</td><td style="text-align:left">提问、头脑风暴</td></tr><tr><td style="text-align:left"></td><td style="text-align:left">顺序研讨，深入挖掘</td><td style="text-align:left">预期目标是什么？</td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">实际发生了什么？结果如何？</td><td style="text-align:left"></td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">差异根本的原因是什么？</td><td style="text-align:left"></td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">从中能学到哪些经验教训？</td><td style="text-align:left"></td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">后续如何改进？</td><td style="text-align:left"></td><td style="text-align:left"></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">收尾</td><td style="text-align:left">是否充分发表了意见？是否达成了共识？后续行动措施是否明确？</td><td style="text-align:left">列清单检查</td></tr><tr><td style="text-align:left">会后落地</td><td style="text-align:left">整理并分享复盘结果</td><td style="text-align:left">那些人需要了解到这些信息？</td><td style="text-align:left">复盘报告</td></tr><tr><td style="text-align:left"></td><td style="text-align:left">跟进实施，推动落地</td><td style="text-align:left">复盘后续行动是否落实到位了？</td><td style="text-align:left">行动计划表</td></tr><tr><td style="text-align:left"></td><td style="text-align:left">评估与改善</td><td style="text-align:left">复盘有哪些价值？还有哪些改进之处？</td><td style="text-align:left">效果检查清单</td></tr></tbody></table><p>1.复盘是一个不断校正路线的过程，行军打仗，最怕方向和路线错误。方向错误，再努力也到达不了目的地；路线错误，就会徒增到达目的地过程中的困难和险阻，甚至困难会大到让我们到达不了目的地。而复盘，就如同行军过程中不断检查GPS，校正自己的轨迹是否在正确的航线上。如果不在，随时调整。</p><p>2.孙陶然总结出复盘四步法：</p><p>(1).目标结果 (2).过程再现 (3).得失分析 ( 4).规律总结</p><p>3.目标结果：找出刚开始设定的目标，对照实际达成的结果，对比是否达成了目标？</p><p>目标一定要在开始之前就设定，而且目标设定要清晰，否则复盘的时候就无法对比目标结果。</p><p>4.过程再现：回顾一下，从开始到结束整个过程是怎样的？过程大致分几个阶段？每个阶段都发生了什么？又都是如何应对的？</p><p>5.得失分析：在分析过程之中，哪些地方我们做的好？哪些地方做的不好？复盘必须既对事也对人，复盘每个人在事情之中，哪些地方做的不好？为什么？那些地方做的好，好在哪里？复盘，必须对照业务找问题。</p><p>6.规律总结：只要我们能够总结出规律，我们就可以掌握和驾驭。</p><p>7.复盘，最重要的目的就是：规律总结。</p><p>包括两方面的规律：一是认知方面，通过复盘，对思考问题和解决问题的方法有哪些心得？对某些事物的认知有哪些心得？如果能总结出普遍使用的规律性的东西，是对认知的一个提升。另一个是实践方面，如果历史重演，加入再次遇到类似的项目，我们应该如何做？如果总结出规律，未来在类似的事情上一定可以做的更好，不犯同样的错误，这就是我们说的吃一堑长一智，甚至吃一堑长三智。</p><p>8.我觉得真正最有效的学习，不是看书，也不是跟别人学，而是从自己的实践中跟自己学，跟别人学那是别人的东西，看书学，如果书里的东西没有亲身经历过，其实是没感觉的，甚至是看不懂的。</p><p>9.跟自己学，就要懂复盘的方法，也就是复盘的重要意义。</p><p>10.我会把“复盘意识”作为一个重要意识来训练自己，经常不断地“念咒”：复盘，复盘，复盘。真正变成自己的意识。</p><p>11.实践出真知，然后把真知要上升到理论，再理论指导实践！我一直用这种循环提升自己的认知能力！</p><p>12.小复盘：每做完一件事情，哪怕十分钟；中复盘：每完成一个项目；大复盘：每年，或者人生的每个阶段，我们都对照目标结果，拿出时间进行过程回顾，分析得失以及总结规律。</p><p>13.中国人古语讲：吾日三省吾身，包括佛教讲的悟，就是要认知自己，反省自身。</p><p>14.西方人就比较擅长数学表达，用逻辑学，科学实验来纠正偏见，量化的东西来让自己提升。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;阶段&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;步骤&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;需要解决的问题&lt;/th&gt;
&lt;th st
      
    
    </summary>
    
      <category term="工程" scheme="http://yoursite.com/categories/%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="复盘" scheme="http://yoursite.com/tags/%E5%A4%8D%E7%9B%98/"/>
    
  </entry>
  
  <entry>
    <title>mysql--redo log与binlog</title>
    <link href="http://yoursite.com/2021/05/16/mysql/mysql--redo%20log%E4%B8%8Ebinlog/"/>
    <id>http://yoursite.com/2021/05/16/mysql/mysql--redo log与binlog/</id>
    <published>2021-05-16T02:42:46.592Z</published>
    <updated>2022-04-18T05:36:17.334Z</updated>
    
    <content type="html"><![CDATA[<p>之前我们了解了一条查询语句的执行流程，并介绍了执行过程中涉及的处理模块。一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块，最后到达存储引擎。</p><p>那么，一条 SQL 更新语句的执行流程又是怎样的呢？</p><p>首先我们创建一个表 user_info，主键为 id，创建语句如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CopyCREATE TABLE `T` (</span><br><span class="line">  `ID` int(11) NOT NULL,</span><br><span class="line">  `c` int(11) DEFAULT NULL,</span><br><span class="line">  PRIMARY KEY (`ID`)</span><br><span class="line">) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</span><br></pre></td></tr></table></figure><p>插入一条数据：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO T VALUES (&apos;2&apos;, &apos;1&apos;);</span><br></pre></td></tr></table></figure><p>如果要将 ID=2 这一行的 c 的值加 1，SQL 语句为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE T SET c = c + 1 WHERE ID = 2;</span><br></pre></td></tr></table></figure><p>前面介绍过 SQL 语句基本的执行链路，这里把那张图拿过来。因为，更新语句同样会走一遍查询语句走的流程。</p><p><img src="/images/image-20210516105246680.png" alt="image-20210516105246680"></p>"<ol><li>通过连接器，客户端与 MySQL 建立连接</li><li>update 语句会把 T 表上的所有查询缓存结果清空</li><li>分析器会通过词法分析和语法分析识别这是一条更新语句</li><li>优化器会决定使用 ID 这个索引（聚簇索引）</li><li>执行器负责具体执行，找到匹配的一行，然后更新</li><li>更新过程中还会涉及 redo log（重做日志）和 binlog（归档日志）的操作</li></ol><p>其中，这两种日志默认在数据库的 data 目录下，redo log 是 ib_logfile0 格式的，binlog 是 xxx-bin.000001 格式的。</p><p>接下来让我们分别去研究下日志模块中的 redo log 和 binlog。</p><h2 id="日志模块：redo-log"><a href="#日志模块：redo-log" class="headerlink" title="日志模块：redo log"></a>日志模块：redo log</h2><p>在 MySQL 中，如果每一次的更新操作都需要写进磁盘，然后磁盘也要找到对应的那条记录，然后再更新，整个过程 IO 成本、查找成本都很高。为了解决这个问题，MySQL 的设计者就采用了日志（redo log）来提升更新效率。</p><p>而日志和磁盘配合的整个过程，其实就是 MySQL 里的 WAL 技术，WAL 的全称是 Write-Ahead Logging，它的关键点就是先写日志，再写磁盘。</p><p>具体来说，当有一条记录需要更新的时候，InnoDB 引擎就会先把记录写到 redo log（redolog buffer）里面，并更新内存（buffer pool），这个时候更新就算完成了。同时，InnoDB 引擎会在适当的时候（如系统空闲时），将这个操作记录更新到磁盘里面（刷脏页）。</p><p>redo log 是 InnoDB 存储引擎层的日志，又称重做日志文件，redo log 是循环写的，redo log 不是记录数据页更新之后的状态，而是记录这个页做了什么改动。</p><p>redo log 是固定大小的，比如可以配置为一组 4 个文件，每个文件的大小是 1GB，那么日志总共就可以记录 4GB 的操作。从头开始写，写到末尾就又回到开头循环写，如下图所示。</p><p><img src="/images/image-20210516105316792.png" alt="image-20210516105316792"></p>"<p>图中展示了一组 4 个文件的 redo log 日志，checkpoint 是当前要擦除的位置，擦除记录前需要先把对应的数据落盘（更新内存页，等待刷脏页）。write pos 到 checkpoint 之间的部分可以用来记录新的操作，如果 write pos 和 checkpoint 相遇，说明 redolog 已满，这个时候数据库停止进行数据库更新语句的执行，转而进行 redo log 日志同步到磁盘中。checkpoint 到 write pos 之间的部分等待落盘（先更新内存页，然后等待刷脏页）。</p><p>有了 redo log 日志，那么在数据库进行异常重启的时候，可以根据 redo log 日志进行恢复，也就达到了 crash-safe。</p><p>redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候，表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1，这样可以保证 MySQL 异常重启之后数据不丢失。</p><h1 id="日志模块：binlog"><a href="#日志模块：binlog" class="headerlink" title="日志模块：binlog"></a>日志模块：binlog</h1><p>MySQL 整体来看，其实就有两块：一块是 Server 层，它主要做的是 MySQL 功能层面的事情；还有一块是引擎层，负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志，而 Server 层也有自己的日志，称为 binlog（归档日志）。</p><p>binlog 属于逻辑日志，是以二进制的形式记录的是这个语句的原始逻辑，依靠 binlog 是没有 crash-safe 能力的。</p><p>binlog 有两种模式，statement 格式的话是记 sql 语句，row 格式会记录行的内容，记两条，更新前和更新后都有。</p><p>sync_binlog 这个参数设置成 1 的时候，表示每次事务的 binlog 都持久化到磁盘。这个参数也建议设置成 1，这样可以保证 MySQL 异常重启之后 binlog 不丢失。</p><p>为什么会有两份日志呢？</p><p>因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM，但是 MyISAM 没有 crash-safe 的能力，binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的，既然只依靠 binlog 是没有 crash-safe 能力的，所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。</p><p>redo log 和 binlog 区别：</p><ol><li>redo log 是 InnoDB 引擎特有的；binlog 是 MySQL 的 Server 层实现的，所有引擎都可以使用。</li><li>redo log 是物理日志，记录的是在某个数据页上做了什么修改；binlog 是逻辑日志，记录的是这个语句的原始逻辑。</li><li>redo log 是循环写的，空间固定会用完；binlog 是可以追加写入的。追加写是指 binlog 文件写到一定大小后会切换到下一个，并不会覆盖以前的日志。</li></ol><p>有了对这两个日志的概念性理解后，再来看执行器和 InnoDB 引擎在执行这个 update 语句时的内部流程。</p><ol><li>执行器先找引擎取 ID=2 这一行。ID 是主键，引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中，就直接返回给执行器；否则，需要先从磁盘读入内存，然后再返回。</li><li>执行器拿到引擎给的行数据，把这个值加上 1，比如原来是 N，现在就是 N+1，得到新的一行数据，再调用引擎接口写入这行新数据。</li><li>引擎将这行新数据更新到内存（InnoDB Buffer Pool）中，同时将这个更新操作记录到 redo log 里面，此时 redo log 处于 prepare 状态。然后告知执行器执行完成了，随时可以提交事务。</li><li>执行器生成这个操作的 binlog，并把 binlog 写入磁盘。</li><li>执行器调用引擎的提交事务接口，引擎把刚刚写入的 redo log 改成提交（commit）状态，更新完成。</li></ol><p>下图为 update 语句的执行流程图，图中灰色框表示是在 InnoDB 内部执行的，绿色框表示是在执行器中执行的。</p><p><img src="/images/image-20210516105339524.png" alt="image-20210516105339524"></p>"<p>其中将 redo log 的写入拆成了两个步骤：prepare 和 commit，这就是两阶段提交（2PC）。</p><h1 id="两阶段提交（2PC）"><a href="#两阶段提交（2PC）" class="headerlink" title="两阶段提交（2PC）"></a>两阶段提交（2PC）</h1><p>MySQL 使用两阶段提交主要解决 binlog 和 redo log 的数据一致性的问题。</p><p>redo log 和 binlog 都可以用于表示事务的提交状态，而两阶段提交就是让这两个状态保持逻辑上的一致。下图为 MySQL 二阶段提交简图：</p><p><img src="/images/image-20210516105352324.png" alt="image-20210516105352324" style="zoom: 33%;"></p>"<p>两阶段提交原理描述:</p><ol><li>InnoDB redo log 写盘，InnoDB 事务进入 prepare 状态。</li><li>如果前面 prepare 成功，binlog 写盘，那么再继续将事务日志持久化到 binlog，如果持久化成功，那么 InnoDB 事务则进入 commit 状态(在 redo log 里面写一个 commit 记录;binlog fsync成就确保了事务的提交)</li></ol><p>备注: 每个事务 binlog 的末尾，会记录一个 XID event，标志着事务是否提交成功，也就是说，recovery 过程中，binlog 最后一个 XID event 之后的内容都应该被 purge。</p><h1 id="日志相关问题"><a href="#日志相关问题" class="headerlink" title="日志相关问题"></a>日志相关问题</h1><h2 id="怎么进行数据恢复？"><a href="#怎么进行数据恢复？" class="headerlink" title="怎么进行数据恢复？"></a>怎么进行数据恢复？</h2><p>binlog 会记录所有的逻辑操作，并且是采用追加写的形式。当需要恢复到指定的某一秒时，比如今天下午二点发现中午十二点有一次误删表，需要找回数据，那你可以这么做：</p><ul><li>首先，找到最近的一次全量备份，从这个备份恢复到临时库</li><li>然后，从备份的时间点开始，将备份的 binlog 依次取出来，重放到中午误删表之前的那个时刻。</li></ul><p>这样你的临时库就跟误删之前的线上库一样了，然后你可以把表数据从临时库取出来，按需要恢复到线上库去。</p><h2 id="redo-log-和-binlog-是怎么关联起来的"><a href="#redo-log-和-binlog-是怎么关联起来的" class="headerlink" title="redo log 和 binlog 是怎么关联起来的?"></a>redo log 和 binlog 是怎么关联起来的?</h2><p>redo log 和 binlog 有一个共同的数据字段，叫 XID。崩溃恢复的时候，会按顺序扫描 redo log：</p><ul><li>如果碰到既有 prepare、又有 commit 的 redo log，就直接提交；</li><li>如果碰到只有 parepare、而没有 commit 的 redo log，就拿着 XID 去 binlog 找对应的事务。</li></ul><h2 id="MySQL-怎么知道-binlog-是完整的"><a href="#MySQL-怎么知道-binlog-是完整的" class="headerlink" title="MySQL 怎么知道 binlog 是完整的?"></a>MySQL 怎么知道 binlog 是完整的?</h2><p>一个事务的 binlog 是有完整格式的：</p><ul><li>statement 格式的 binlog，最后会有 COMMIT</li><li>row 格式的 binlog，最后会有一个 XID event</li></ul><p>在 MySQL 5.6.2 版本以后，还引入了 binlog-checksum 参数，用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因，可能会在日志中间出错的情况，MySQL 可以通过校验 checksum 的结果来发现。所以，MySQL 是有办法验证事务 binlog 的完整性的。</p><h2 id="redo-log-一般设置多大？"><a href="#redo-log-一般设置多大？" class="headerlink" title="redo log 一般设置多大？"></a>redo log 一般设置多大？</h2><p>redo log 太小的话，会导致很快就被写满，然后不得不强行刷 redo log，这样 WAL 机制的能力就发挥不出来了。</p><p>如果是几个 TB 的磁盘的话，直接将 redo log 设置为 4 个文件，每个文件 1GB。</p><h2 id="数据写入后的最终落盘，是从-redo-log-更新过来的还是从-buffer-pool-更新过来的呢？"><a href="#数据写入后的最终落盘，是从-redo-log-更新过来的还是从-buffer-pool-更新过来的呢？" class="headerlink" title="数据写入后的最终落盘，是从 redo log 更新过来的还是从 buffer pool 更新过来的呢？"></a>数据写入后的最终落盘，是从 redo log 更新过来的还是从 buffer pool 更新过来的呢？</h2><p>实际上，redo log 并没有记录数据页的完整数据，所以它并没有能力自己去更新磁盘数据页，也就不存在由 redo log 更新过去数据最终落盘的情况。</p><ol><li>数据页被修改以后，跟磁盘的数据页不一致，称为脏页。最终数据落盘，就是把内存中的数据页写盘。这个过程与 redo log 毫无关系。</li><li>在崩溃恢复场景中，InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新，就会将它读到内存，然后让 redo log 更新内存内容。更新完成后，内存页变成脏页，就回到了第一种情况的状态。</li></ol><h2 id="redo-log-buffer-是什么？是先修改内存，还是先写-redo-log-文件？"><a href="#redo-log-buffer-是什么？是先修改内存，还是先写-redo-log-文件？" class="headerlink" title="redo log buffer 是什么？是先修改内存，还是先写 redo log 文件？"></a>redo log buffer 是什么？是先修改内存，还是先写 redo log 文件？</h2><p>在一个事务的更新过程中，日志是要写多次的。比如下面这个事务：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Copybegin;</span><br><span class="line">INSERT INTO T1 VALUES (&apos;1&apos;, &apos;1&apos;);</span><br><span class="line">INSERT INTO T2 VALUES (&apos;1&apos;, &apos;1&apos;);</span><br><span class="line">commit;</span><br></pre></td></tr></table></figure><p>这个事务要往两个表中插入记录，插入数据的过程中，生成的日志都得先保存起来，但又不能在还没 commit 的时候就直接写到 redo log 文件里。</p><p>因此就需要 redo log buffer 出场了，它就是一块内存，用来先存 redo 日志的。也就是说，在执行第一个 insert 的时候，数据的内存被修改了，redo log buffer 也写入了日志。</p><p>但是，真正把日志写到 redo log 文件，是在执行 commit 语句的时候做的。</p><p>以下是我截取的部分 redo log buffer 的源代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">Copy<span class="comment">/** redo log buffer */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">log_t</span>&#123;</span></span><br><span class="line"><span class="keyword">char</span>pad1[CACHE_LINE_SIZE];</span><br><span class="line"><span class="keyword">lsn_t</span>lsn;</span><br><span class="line">ulintbuf_free;   <span class="comment">// buffer 内剩余空间的起始点的 offset</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifndef</span> UNIV_HOTBACKUP</span></span><br><span class="line"><span class="keyword">char</span>pad2[CACHE_LINE_SIZE];</span><br><span class="line">LogSysMutexmutex;</span><br><span class="line">LogSysMutexwrite_mutex;</span><br><span class="line"><span class="keyword">char</span>pad3[CACHE_LINE_SIZE];</span><br><span class="line">FlushOrderMutexlog_flush_order_mutex;</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span> <span class="comment">/* !UNIV_HOTBACKUP */</span></span></span><br><span class="line">byte*buf_ptr;    <span class="comment">// 隐性的 buffer</span></span><br><span class="line">byte*buf;        <span class="comment">// 真正操作的 buffer</span></span><br><span class="line"><span class="keyword">bool</span>first_in_use;</span><br><span class="line">ulintbuf_size;   <span class="comment">// buffer大小</span></span><br><span class="line"><span class="keyword">bool</span>check_flush_or_checkpoint;</span><br><span class="line">UT_LIST_BASE_NODE_T(<span class="keyword">log_group_t</span>) log_groups;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifndef</span> UNIV_HOTBACKUP</span></span><br><span class="line"><span class="comment">/** The fields involved in the log buffer flush @&#123; */</span></span><br><span class="line">ulintbuf_next_to_write;</span><br><span class="line"><span class="keyword">volatile</span> <span class="keyword">bool</span>is_extending;</span><br><span class="line"><span class="keyword">lsn_t</span>write_lsn;<span class="comment">/*!&lt; last written lsn */</span></span><br><span class="line"><span class="keyword">lsn_t</span>current_flush_lsn;</span><br><span class="line"><span class="keyword">lsn_t</span>flushed_to_disk_lsn;</span><br><span class="line">ulintn_pending_flushes;</span><br><span class="line"><span class="keyword">os_event_t</span>flush_event;</span><br><span class="line">ulintn_log_ios;</span><br><span class="line">ulintn_log_ios_old;</span><br><span class="line"><span class="keyword">time_t</span>last_printout_time;</span><br><span class="line"></span><br><span class="line"><span class="comment">/** Fields involved in checkpoints @&#123; */</span></span><br><span class="line"><span class="keyword">lsn_t</span>log_group_capacity; </span><br><span class="line"><span class="keyword">lsn_t</span>max_modified_age_async;</span><br><span class="line"><span class="keyword">lsn_t</span>max_modified_age_sync;</span><br><span class="line"><span class="keyword">lsn_t</span>max_checkpoint_age_async;</span><br><span class="line"><span class="keyword">lsn_t</span>max_checkpoint_age;</span><br><span class="line"><span class="keyword">ib_uint64_t</span>next_checkpoint_no;</span><br><span class="line"><span class="keyword">lsn_t</span>last_checkpoint_lsn;</span><br><span class="line"><span class="keyword">lsn_t</span>next_checkpoint_lsn;</span><br><span class="line"><span class="keyword">mtr_buf_t</span>*append_on_checkpoint;</span><br><span class="line">ulintn_pending_checkpoint_writes;</span><br><span class="line"><span class="keyword">rw_lock_t</span>checkpoint_lock;</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span> <span class="comment">/* !UNIV_HOTBACKUP */</span></span></span><br><span class="line">byte*checkpoint_buf_ptr;</span><br><span class="line">byte*checkpoint_buf;</span><br><span class="line"><span class="comment">/* @&#125; */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>redo log buffer 本质上只是一个 byte 数组，但是为了维护这个 buffer 还需要设置很多其他的 meta data，这些 meta data 全部封装在 log_t 结构体中。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这篇文章主要介绍了 MySQL 里面最重要的两个日志，即物理日志 redo log（重做日志）和逻辑日志 binlog（归档日志），还讲解了有与日志相关的一些问题。</p><p>另外还介绍了与 MySQL 日志系统密切相关的两阶段提交（2PC），两阶段提交是解决分布式系统的一致性问题常用的一个方案，类似的还有 三阶段提交（3PC） 和 PAXOS 算法。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;之前我们了解了一条查询语句的执行流程，并介绍了执行过程中涉及的处理模块。一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块，最后到达存储引擎。&lt;/p&gt;
&lt;p&gt;那么，一条 SQL 更新语句的执行流程又是怎样的呢？&lt;/p&gt;
&lt;p&gt;首先我们创建一个表 use
      
    
    </summary>
    
    
      <category term="mysql" scheme="http://yoursite.com/tags/mysql/"/>
    
      <category term="explain" scheme="http://yoursite.com/tags/explain/"/>
    
  </entry>
  
  <entry>
    <title>HikariCP原理分析</title>
    <link href="http://yoursite.com/2021/04/21/HikariCP/HikariCP%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/"/>
    <id>http://yoursite.com/2021/04/21/HikariCP/HikariCP原理分析/</id>
    <published>2021-04-21T00:00:43.938Z</published>
    <updated>2022-04-08T06:03:12.213Z</updated>
    
    <content type="html"><![CDATA[<h2 id="零、类图和流程图"><a href="#零、类图和流程图" class="headerlink" title="零、类图和流程图"></a><strong>零、类图和流程图</strong></h2><p>开始前先来了解下HikariCP获取一个连接时类间的交互流程，方便下面详细流程的阅读。</p><p>获取连接时的类间交互：</p><p><img src="/images/1569484-20191014234722009-1969368728.png" alt="img"></p>"<p><strong>图1</strong></p><h2 id="一、主流程1：获取连接流程"><a href="#一、主流程1：获取连接流程" class="headerlink" title="一、主流程1：获取连接流程"></a><strong>一、主流程1：获取连接流程</strong></h2><p>HikariCP获取连接时的入口是HikariDataSource里的getConnection方法，现在来看下该方法的具体流程：</p><p><img src="/images/1569484-20191021141700729-705695809.png" alt="img"></p>"<p><strong>主流程1</strong></p><p>上述为HikariCP获取连接时的流程图，由图1可知，每个<strong>datasource</strong>对象里都会持有一个<strong>HikariPool</strong>对象，记为<strong>pool</strong>，初始化后的datasource对象pool是空的，所以第一次<strong>getConnection</strong>的时候会进行<strong>实例化pool</strong>属性（参考<strong>主流程1</strong>），初始化的时候需要将当前datasource里的config属性传过去，用于pool的初始化，最终标记<strong>sealed</strong>，然后根据<strong>pool</strong>对象调用<strong>getConnection</strong>方法（参考<strong>流程1.1</strong>），获取成功后返回连接对象。</p><h2 id="二、主流程2：初始化池对象"><a href="#二、主流程2：初始化池对象" class="headerlink" title="二、主流程2：初始化池对象"></a><strong>二、主流程2：初始化池对象</strong></h2><p><img src="/images/1569484-20191021141409263-482942368.png" alt="img"></p>"<p><strong>主流程2</strong></p><p> 该流程用于初始化整个连接池，这个流程会给连接池内所有的属性做初始化的工作，其中比较主要的几个流程上图已经指出，简单概括一下：</p><ol><li>利用config初始化各种连接池属性，并且产生一个用于生产物理连接的数据源DriverDataSource</li><li>初始化存放连接对象的核心类<strong>connectionBag</strong></li><li>初始化一个延时任务线程池类型的对象<strong>houseKeepingExecutorService</strong>，用于后续执行一些延时/定时类任务（比如连接泄漏检查延时任务，参考<strong>流程2.2</strong>以及<strong>主流程4</strong>，除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的，参考<strong>主流程3</strong>）</li><li>预热连接池，HikariCP会在该流程的<strong>checkFailFast</strong>里初始化好一个连接对象放进池子内，当然触发该流程得保证initializationTimeout &gt; 0时（默认值1），这个配置属性表示留给预热操作的时间（默认值1在预热失败时不会发生重试）。与Druid通过initialSize控制预热连接对象数不一样的是，HikariCP仅预热进池一个连接对象。</li><li>初始化一个线程池对象<strong>addConnectionExecutor</strong>，用于后续扩充连接对象</li><li>初始化一个线程池对象<strong>closeConnectionExecutor</strong>，用于关闭一些连接对象，怎么触发关闭任务呢？可以参考<strong>流程1.1.2</strong></li></ol><h2 id="三、流程1-1：通过HikariPool获取连接对象"><a href="#三、流程1-1：通过HikariPool获取连接对象" class="headerlink" title="三、流程1.1：通过HikariPool获取连接对象"></a><strong>三、流程1.1：通过HikariPool获取连接对象</strong></h2><p><img src="/images/1569484-20191021141919023-1679145701.png" alt="img"></p>"<p><strong>流程1.1</strong></p><p>从最开始的结构图可知，每个<strong>HikariPool</strong>里都维护一个<strong>ConcurrentBag</strong>对象，用于存放连接对象，由上图可以看到，实际上<strong>HikariPool</strong>的<strong>getConnection</strong>就是从<strong>ConcurrentBag</strong>里获取连接的（调用其<strong>borrow</strong>方法获得，对应<strong>ConnectionBag主流程</strong>），在长连接检查这块，与之前说的<strong>Druid</strong>不同，这里的长连接判活检查在连接对象没有被标记为“已丢弃”时，只要距离上次使用超过500ms每次取出都会进行检查（500ms是默认值，可通过配置com.zaxxer.hikari.aliveBypassWindowMs的系统参数来控制），emmmm，也就是说<strong>HikariCP</strong>对长连接的活性检查很频繁，但是其并发性能依旧优于<strong>Druid</strong>，说明频繁的长连接检查并不是导致连接池性能高低的关键所在。</p><p>这个其实是由于<strong>HikariCP</strong>的<strong>无锁</strong>实现，在高并发时对CPU的负载没有其他连接池那么高而产生的并发性能差异，后面会说<strong>HikariCP</strong>的具体做法，即使是<strong>Druid</strong>，在<strong>获取连接、生成连接、归还连接</strong>时都进行了<strong>锁控制</strong>，因为通过<a href="https://www.cnblogs.com/hama1993/p/11421576.html" target="_blank" rel="noopener">上篇文章</a>可以知道，<strong>Druid</strong>里的连接池资源是多线程共享的，不可避免的会有锁竞争，有锁竞争意味着线程状态的变化会很频繁，线程状态变化频繁意味着CPU上下文切换也将会很频繁。</p><p>回到<strong>流程1.1</strong>，如果拿到的连接为空，直接报错，不为空则进行相应的检查，如果检查通过，则包装成<strong>ConnectionProxy</strong>对象返回给业务方，不通过则调用<strong>closeConnection</strong>方法关闭连接（对应<strong>流程1.1.2</strong>，该流程会触发<strong>ConcurrentBag</strong>的<strong>remove</strong>方法丢弃该连接，然后把实际的驱动连接交给<strong>closeConnectionExecutor</strong>线程池，异步关闭驱动连接）。</p><h2 id="四、流程1-1-1：连接判活"><a href="#四、流程1-1-1：连接判活" class="headerlink" title="四、流程1.1.1：连接判活"></a><strong>四、流程1.1.1：连接判活</strong></h2><p><img src="/images/1569484-20191102143540548-1462781308.png" alt="img"></p>"<p><strong>流程1.1.1</strong></p><p>承接上面的流程1.1里的判活流程，来看下判活是如何做的，首先说验证方法（注意这里该方法接受的这个connection对象<strong>不是poolEntry</strong>，而是poolEntry持有的实际驱动的连接对象），在之前介绍Druid的时候就知道，Druid是根据驱动程序里是否存在ping方法来判断是否启用ping的方式判断连接是否存活，但是到了HikariCP则更加简单粗暴，仅根据是否配置了connectionTestQuery觉定是否启用ping：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.isUseJdbc4Validation = config.getConnectionTestQuery() == <span class="keyword">null</span>;</span><br></pre></td></tr></table></figure><p>所以一般驱动如果不是特别低的版本，不建议配置该项，否则便会走<strong>createStatement+excute</strong>的方式，相比ping简单发送心跳数据，这种方式显然更低效。</p><p>此外，这里在刚进来还会通过驱动的连接对象重新给它设置一遍<strong>networkTimeout</strong>的值，使之变成<strong>validationTimeout</strong>，表示一次验证的超时时间，为啥这里要重新设置这个属性呢？因为在使用ping方法校验时，是没办法通过类似<strong>statement</strong>那样可以<strong>setQueryTimeout</strong>的，所以只能由网络通信的超时时间来控制，这个时间可以通过jdbc的连接参数socketTimeout来控制：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jdbc:mysql://127.0.0.1:3306/xxx?socketTimeout=250</span><br></pre></td></tr></table></figure><p>这个值最终会被赋值给HikariCP的<strong>networkTimeout</strong>字段，这就是为什么最后那一步使用这个字段来还原驱动连接超时属性的原因；说到这里，最后那里为啥要再次还原呢？这就很容易理解了，因为验证结束了，连接对象还存活的情况下，它的<strong>networkTimeout</strong>的值这时仍然等于<strong>validationTimeout</strong>（不合预期），显然在拿出去用之前，需要恢复成本来的值，也就是HikariCP里的<strong>networkTimeout</strong>属性。</p><h2 id="五、流程1-1-2：关闭连接对象"><a href="#五、流程1-1-2：关闭连接对象" class="headerlink" title="五、流程1.1.2：关闭连接对象"></a><strong>五、流程1.1.2：关闭连接对象</strong></h2><p><img src="/images/1569484-20191102153003891-998499070.png" alt="img"></p>"<p><strong>流程1.1.2</strong></p><p>这个流程简单来说就是把<strong>流程1.1.1</strong>中验证不通过的死连接，主动关闭的一个流程，首先会把这个连接对象从ConnectionBag里移除，然后把实际的物理连接交给一个线程池去异步执行，这个线程池就是在<strong>主流程2</strong>里初始化池的时候初始化的线程池closeConnectionExecutor，然后异步任务内开始实际的关连接操作，因为主动关闭了一个连接相当于少了一个连接，所以还会触发一次扩充连接池（参考<strong>主流程5</strong>）操作。 </p><h2 id="六、流程2-1：HikariCP监控设置"><a href="#六、流程2-1：HikariCP监控设置" class="headerlink" title="六、流程2.1：HikariCP监控设置"></a><strong>六、流程2.1：HikariCP监控设置</strong></h2><p>不同于Druid那样监控指标那么多，<strong>HikariCP</strong>会把我们非常关心的几项指标暴露给我们，比如当前连接池内闲置连接数、总连接数、一个连接被用了多久归还、创建一个物理连接花费多久等，<strong>HikariCP</strong>的连接池的监控我们这一节专门详细的分解一下，首先找到<strong>HikariCP</strong>下面的<strong>metrics</strong>文件夹，这下面放置了一些规范实现的监控接口等，还有一些现成的实现（比如HikariCP自带对prometheus、micrometer、dropwizard的支持，不太了解后面两个，<strong>prometheus</strong>下文直接称为<strong>普罗米修斯</strong>）：</p><p><img src="/images/1569484-20191103154837315-1164755097.png" alt="img"></p>"<p><strong>图2</strong></p><p>下面，来着重看下接口的定义：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//这个接口的实现主要负责收集一些动作的耗时</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IMetricsTracker</span> <span class="keyword">extends</span> <span class="title">AutoCloseable</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="comment">//这个方法触发点在创建实际的物理连接时（主流程3），用于记录一个实际的物理连接创建所耗费的时间</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">recordConnectionCreatedMillis</span><span class="params">(<span class="keyword">long</span> connectionCreatedMillis)</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//这个方法触发点在getConnection时（主流程1），用于记录获取一个连接时实际的耗时</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">recordConnectionAcquiredNanos</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> elapsedAcquiredNanos)</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//这个方法触发点在回收连接时（主流程6），用于记录一个连接从被获取到被回收时所消耗的时间</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">recordConnectionUsageMillis</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> elapsedBorrowedMillis)</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//这个方法触发点也在getConnection时（主流程1），用于记录获取连接超时的次数，每发生一次获取连接超时，就会触发一次该方法的调用</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">recordConnectionTimeout</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">close</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>触发点都了解清楚后，再来看看<strong>MetricsTrackerFactory</strong>的接口定义：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//用于创建IMetricsTracker实例，并且按需记录PoolStats对象里的属性（这个对象里的属性就是类似连接池当前闲置连接数之类的线程池状态类指标）</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MetricsTrackerFactory</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="comment">//返回一个IMetricsTracker对象，并且把PoolStats传了过去</span></span><br><span class="line">    <span class="function">IMetricsTracker <span class="title">create</span><span class="params">(String poolName, PoolStats poolStats)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的接口用法见注释，针对新出现的<strong>PoolStats</strong>类，我们来看看它做了什么：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">PoolStats</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AtomicLong reloadAt; <span class="comment">//触发下次刷新的时间（时间戳）</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">long</span> timeoutMs; <span class="comment">//刷新下面的各项属性值的频率，默认1s，无法改变</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 总连接数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> totalConnections;</span><br><span class="line">    <span class="comment">// 闲置连接数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> idleConnections;</span><br><span class="line">    <span class="comment">// 活动连接数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> activeConnections;</span><br><span class="line">    <span class="comment">// 由于无法获取到可用连接而阻塞的业务线程数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> pendingThreads;</span><br><span class="line">    <span class="comment">// 最大连接数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> maxConnections;</span><br><span class="line">    <span class="comment">// 最小连接数</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">volatile</span> <span class="keyword">int</span> minConnections;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">PoolStats</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> timeoutMs)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.timeoutMs = timeoutMs;</span><br><span class="line">        <span class="keyword">this</span>.reloadAt = <span class="keyword">new</span> AtomicLong();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//这里以获取最大连接数为例，其他的跟这个差不多</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getMaxConnections</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (shouldLoad()) &#123; <span class="comment">//是否应该刷新</span></span><br><span class="line">            update(); <span class="comment">//刷新属性值，注意这个update的实现在HikariPool里，因为这些属性值的直接或间接来源都是HikariPool</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> maxConnections;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">update</span><span class="params">()</span></span>; <span class="comment">//实现在↑上面已经说了</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">shouldLoad</span><span class="params">()</span> </span>&#123; <span class="comment">//按照更新频率来决定是否刷新属性值</span></span><br><span class="line">        <span class="keyword">for</span> (; ; ) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">long</span> now = currentTime();</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">long</span> reloadTime = reloadAt.get();</span><br><span class="line">            <span class="keyword">if</span> (reloadTime &gt; now) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (reloadAt.compareAndSet(reloadTime, plusMillis(now, timeoutMs))) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实际上这里就是这些属性获取和触发刷新的地方，那么这个对象是在哪里被生成并且丢给<strong>MetricsTrackerFactory</strong>的<strong>create</strong>方法的呢？这就是本节所需要讲述的要点：<strong>主流程2</strong>里的设置监控器的流程，来看看那里发生了什么事吧：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//监控器设置方法（此方法在HikariPool中，metricsTracker属性就是HikariPool用来触发IMetricsTracker里方法调用的）</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setMetricsTrackerFactory</span><span class="params">(MetricsTrackerFactory metricsTrackerFactory)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (metricsTrackerFactory != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">//MetricsTrackerDelegate是包装类，是HikariPool的一个静态内部类，是实际持有IMetricsTracker对象的类，也是实际触发IMetricsTracker里方法调用的类</span></span><br><span class="line">        <span class="comment">//这里首先会触发MetricsTrackerFactory类的create方法拿到IMetricsTracker对象，然后利用getPoolStats初始化PoolStat对象，然后也一并传给MetricsTrackerFactory</span></span><br><span class="line">        <span class="keyword">this</span>.metricsTracker = <span class="keyword">new</span> MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">//不启用监控，直接等于一个没有实现方法的空类</span></span><br><span class="line">        <span class="keyword">this</span>.metricsTracker = <span class="keyword">new</span> NopMetricsTrackerDelegate();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> PoolStats <span class="title">getPoolStats</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">//初始化PoolStats对象，并且规定1s触发一次属性值刷新的update方法</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> PoolStats(SECONDS.toMillis(<span class="number">1</span>)) &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">update</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="comment">//实现了PoolStat的update方法，刷新各个属性的值</span></span><br><span class="line">            <span class="keyword">this</span>.pendingThreads = HikariPool.<span class="keyword">this</span>.getThreadsAwaitingConnection();</span><br><span class="line">            <span class="keyword">this</span>.idleConnections = HikariPool.<span class="keyword">this</span>.getIdleConnections();</span><br><span class="line">            <span class="keyword">this</span>.totalConnections = HikariPool.<span class="keyword">this</span>.getTotalConnections();</span><br><span class="line">            <span class="keyword">this</span>.activeConnections = HikariPool.<span class="keyword">this</span>.getActiveConnections();</span><br><span class="line">            <span class="keyword">this</span>.maxConnections = config.getMaximumPoolSize();</span><br><span class="line">            <span class="keyword">this</span>.minConnections = config.getMinimumIdle();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>到这里HikariCP的监控器就算是注册进去了，所以要想实现自己的监控器拿到上面的指标，要经过如下步骤：</p><ol><li>新建一个类实现<strong>IMetricsTracker</strong>接口，我们这里将该类记为IMetricsTrackerImpl</li><li>新建一个类实现<strong>MetricsTrackerFactory</strong>接口，我们这里将该类记为MetricsTrackerFactoryImpl，并且将上面的IMetricsTrackerImpl在其create方法内实例化</li><li>将MetricsTrackerFactoryImpl实例化后调用<strong>HikariPool</strong>的<strong>setMetricsTrackerFactory</strong>方法注册到Hikari连接池。</li></ol><p>上面没有提到<strong>PoolStats</strong>里的属性怎么监控，这里来说下，由于create方法是调用一次就没了，create方法只是接收了<strong>PoolStats</strong>对象的实例，如果不处理，那么随着create调用的结束，这个实例针对监控模块来说就失去持有了，所以这里如果想要拿到<strong>PoolStats</strong>里的属性，就需要开启一个守护线程，让其持有<strong>PoolStats</strong>对象实例，并且定时获取其内部属性值，然后<strong>push</strong>给监控系统，如果是普罗米修斯等使用<strong>pull</strong>方式获取监控数据的监控系统，可以效仿<strong>HikariCP</strong>原生普罗米修斯监控的实现，自定义一个<strong>Collector</strong>对象来接收<strong>PoolStats</strong>实例，这样普罗米修斯就可以定期拉取了，比如<strong>HikariCP</strong>根据普罗米修斯监控系统自己定义的<strong>MetricsTrackerFactory</strong>实现（对应<strong>图2</strong>里的<strong>PrometheusMetricsTrackerFactory</strong>类）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> IMetricsTracker <span class="title">create</span><span class="params">(String poolName, PoolStats poolStats)</span> </span>&#123;</span><br><span class="line">    getCollector().add(poolName, poolStats); <span class="comment">//将接收到的PoolStats对象直接交给Collector，这样普罗米修斯服务端每触发一次采集接口的调用，PoolStats都会跟着执行一遍内部属性获取流程</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> PrometheusMetricsTracker(poolName, <span class="keyword">this</span>.collectorRegistry); <span class="comment">//返回IMetricsTracker接口的实现类</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//自定义的Collector</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> HikariCPCollector <span class="title">getCollector</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (collector == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">//注册到普罗米修斯收集中心</span></span><br><span class="line">        collector = <span class="keyword">new</span> HikariCPCollector().register(<span class="keyword">this</span>.collectorRegistry);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> collector;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过上面的解释可以知道在HikariCP中如何自定义一个自己的监控器，以及相比Druid的监控，有什么区别。</p><p>工作中很多时候都是需要自定义的，我司虽然也是用的普罗米修斯监控，但是因为HikariCP原生的普罗米修斯收集器里面对监控指标的命名并不符合我司的规范，所以就自定义了一个，有类似问题的不妨也试一试。</p><p>这一节没有画图，纯代码，因为画图不太好解释这部分的东西，这部分内容与连接池整体流程关系也不大，充其量获取了连接池本身的一些属性，在连接池里的触发点也在上面代码段的注释里说清楚了，看代码定义可能更好理解一些。</p><h2 id="七、流程2-2：连接泄漏的检测与告警"><a href="#七、流程2-2：连接泄漏的检测与告警" class="headerlink" title="七、流程2.2：连接泄漏的检测与告警"></a><strong>七、流程2.2：连接泄漏的检测与告警</strong></h2><p>本节对应<strong>主流程2</strong>里的<strong>子流程2.2</strong>，在初始化池对象时，初始化了一个叫做<strong>leakTaskFactory</strong>的属性，本节来看下它具体是用来做什么的。</p><h3 id="7-1：它是做什么的？"><a href="#7-1：它是做什么的？" class="headerlink" title="7.1：它是做什么的？"></a><strong>7.1：它是做什么的？</strong></h3><p>一个连接被拿出去使用时间超过<strong>leakDetectionThreshold（可配置，默认0）</strong>未归还的，会触发一个连接泄漏警告，通知业务方目前存在连接泄漏的问题。</p><h3 id="7-2：过程详解"><a href="#7-2：过程详解" class="headerlink" title="7.2：过程详解"></a><strong>7.2：过程详解</strong></h3><p>该属性是<strong>ProxyLeakTaskFactory</strong>类型对象，且它还会持有<strong>houseKeepingExecutorService</strong>这个线程池对象，用于生产<strong>ProxyLeakTask</strong>对象，然后利用上面的<strong>houseKeepingExecutorService</strong>延时运行该对象里的<strong>run</strong>方法。该流程的<strong>触发点</strong>在上面的<strong>流程1.1</strong>最后包装成<strong>ProxyConnection</strong>对象的那一步，来看看具体的流程图：</p><p><img src="/images/1569484-20191105235656069-1454840938.png" alt="img"></p>"<p><strong>流程2.2</strong></p><p>每次在<strong>流程1.1</strong>那里生成<strong>ProxyConnection</strong>对象时，都会触发上面的流程，由流程图可以知道，<strong>ProxyConnection</strong>对象持有<strong>PoolEntry</strong>和<strong>ProxyLeakTask</strong>的对象，其中初始化<strong>ProxyLeakTask</strong>对象时就用到了<strong>leakTaskFactory</strong>对象，通过其<strong>schedule</strong>方法可以进行<strong>ProxyLeakTask</strong>的初始化，并将其实例传递给<strong>ProxyConnection</strong>进行初始化赋值（ps：由图知<strong>ProxyConnection</strong>在触发回收事件时，会主动取消这个泄漏检查任务，这也是<strong>ProxyConnection</strong>需要持有<strong><em>\</em>ProxyLeakTask**</strong>对象的原因）。</p><p>在上面的流程图中可以知道，只有在<strong>leakDetectionThreshold</strong>不等于0的时候才会生成一个带有实际延时任务的<strong>ProxyLeakTask</strong>对象，否则返回无实际意义的空对象。所以要想启用连接泄漏检查，首先要把<strong>leakDetectionThreshold</strong>配置设置上，这个属性表示经过该时间后借出去的连接仍未归还，则触发连接泄漏告警。</p><p><strong>ProxyConnection</strong>之所以要持有<strong>ProxyLeakTask</strong>对象，是因为它可以监听到连接是否触发归还操作，如果触发，则调用<strong>cancel</strong>方法取消延时任务，防止误告。</p><p>由此流程可以知道，跟Druid一样，HikariCP也有连接对象泄漏检查，与Druid主动回收连接相比，HikariCP实现更加简单，仅仅是在触发时打印警告日志，不会采取具体的强制回收的措施。</p><p>与Druid一样，默认也是关闭这个流程的，因为实际开发中一般使用第三方框架，框架本身会保证及时的close连接，防止连接对象泄漏，开启与否还是取决于业务是否需要，如果一定要开启，如何设置<strong>leakDetectionThreshold</strong>的大小也是需要考虑的一件事。</p><h2 id="八、主流程3：生成连接对象"><a href="#八、主流程3：生成连接对象" class="headerlink" title="八、主流程3：生成连接对象"></a><strong>八、主流程3：生成连接对象</strong></h2><p>本节来讲下<strong>主流程2</strong>里的<strong>createEntry</strong>方法，这个方法利用PoolBase里的<strong>DriverDataSource</strong>对象生成一个实际的连接对象（如果忘记<strong>DriverDatasource</strong>是哪里初始化的了，可以看下<strong>主流程2</strong>里<strong>PoolBase</strong>的<strong>initializeDataSource</strong>方法的作用），然后用<strong>PoolEntry</strong>类包装成<strong>PoolEntry</strong>对象，现在来看下这个包装类有哪些主要属性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">PoolEntry</span> <span class="keyword">implements</span> <span class="title">IConcurrentBagEntry</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = LoggerFactory.getLogger(PoolEntry<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    <span class="comment">//通过cas来修改state属性</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> AtomicIntegerFieldUpdater stateUpdater;</span><br><span class="line"></span><br><span class="line">    Connection connection; <span class="comment">//实际的物理连接对象</span></span><br><span class="line">    <span class="keyword">long</span> lastAccessed; <span class="comment">//触发回收时刷新该时间，表示“最近一次使用时间”</span></span><br><span class="line">    <span class="keyword">long</span> lastBorrowed; <span class="comment">//getConnection里borrow成功后刷新该时间，表示“最近一次借出的时间”</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@SuppressWarnings</span>(<span class="string">"FieldCanBeLocal"</span>)</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">int</span> state = <span class="number">0</span>; <span class="comment">//连接状态，枚举值：IN_USE（使用中）、NOT_IN_USE（闲置中）、REMOVED（已移除）、RESERVED（标记为保留中）</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">boolean</span> evict; <span class="comment">//是否被标记为废弃，很多地方用到（比如流程1.1靠这个判断连接是否已被废弃，再比如主流程4里时钟回拨时触发的直接废弃逻辑）</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> ScheduledFuture&lt;?&gt; endOfLife; <span class="comment">//用于在超过连接生命周期（maxLifeTime）时废弃连接的延时任务，这里poolEntry要持有该对象，主要是因为在对象主动被关闭时（意味着不需要在超过maxLifeTime时主动失效了），需要cancel掉该任务</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> FastList openStatements; <span class="comment">//当前该连接对象上生成的所有的statement对象，用于在回收连接时主动关闭这些对象，防止存在漏关的statement</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> HikariPool hikariPool; <span class="comment">//持有pool对象</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> isReadOnly; <span class="comment">//是否为只读</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> isAutoCommit; <span class="comment">//是否存在事务</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面就是整个<strong>PoolEntry</strong>对象里所有的属性，这里再说下<strong>endOfLife</strong>对象，它是一个利用<strong>houseKeepingExecutorService</strong>这个线程池对象做的延时任务，这个延时任务一般在创建好连接对象后<strong>maxLifeTime左右</strong>的时间触发，具体来看下<strong>createEntry</strong>代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> PoolEntry <span class="title">createPoolEntry</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> PoolEntry poolEntry = newPoolEntry(); <span class="comment">//生成实际的连接对象</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="keyword">long</span> maxLifetime = config.getMaxLifetime(); <span class="comment">//拿到配置好的maxLifetime</span></span><br><span class="line">        <span class="keyword">if</span> (maxLifetime &gt; <span class="number">0</span>) &#123; <span class="comment">//&lt;=0的时候不启用主动过期策略</span></span><br><span class="line">            <span class="comment">// 计算需要减去的随机数</span></span><br><span class="line">            <span class="comment">// 源注释：variance up to 2.5% of the maxlifetime</span></span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">long</span> variance = maxLifetime &gt; <span class="number">10_000</span> ? ThreadLocalRandom.current().nextLong(maxLifetime / <span class="number">40</span>) : <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">long</span> lifetime = maxLifetime - variance; <span class="comment">//生成实际的延时时间</span></span><br><span class="line">            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(</span><br><span class="line">                    () -&gt; &#123; <span class="comment">//实际的延时任务，这里直接触发softEvictConnection，而softEvictConnection内则会标记该连接对象为废弃状态，然后尝试修改其状态为STATE_RESERVED，若成功，则触发closeConnection（对应流程1.1.2）</span></span><br><span class="line">                        <span class="keyword">if</span> (softEvictConnection(poolEntry, <span class="string">"(connection has passed maxLifetime)"</span>, <span class="keyword">false</span> <span class="comment">/* not owner */</span>)) &#123;</span><br><span class="line">                            addBagItem(connectionBag.getWaitingThreadCount()); <span class="comment">//回收完毕后，连接池内少了一个连接，就会尝试新增一个连接对象</span></span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line">                    lifetime, MILLISECONDS)); <span class="comment">//给endOfLife赋值，并且提交延时任务，lifetime后触发</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> poolEntry;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//触发新增连接任务</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addBagItem</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> waiting)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//前排提示：addConnectionQueue和addConnectionExecutor的关系和初始化参考主流程2</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//当添加连接的队列里已提交的任务超过那些因为获取不到连接而发生阻塞的线程个数时，就进行提交连接新增连接的任务</span></span><br><span class="line">        <span class="keyword">final</span> <span class="keyword">boolean</span> shouldAdd = waiting - addConnectionQueue.size() &gt;= <span class="number">0</span>; <span class="comment">// Yes, &gt;= is intentional.</span></span><br><span class="line">        <span class="keyword">if</span> (shouldAdd) &#123;</span><br><span class="line">            <span class="comment">//提交任务给addConnectionExecutor这个线程池，PoolEntryCreator是一个实现了Callable接口的类，下面将通过流程图的方式介绍该类的call方法</span></span><br><span class="line">            addConnectionExecutor.submit(poolEntryCreator);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>通过上面的流程，可以知道，HikariCP一般通过<strong>createEntry</strong>方法来新增一个连接入池，每个连接被包装成PoolEntry对象，在创建好对象时，同时会提交一个延时任务来关闭废弃该连接，这个时间就是我们配置的<strong>maxLifeTime</strong>，为了保证不在同一时间失效，HikariCP还会利用<strong>maxLifeTime</strong>减去一个随机数作为最终的延时任务延迟时间，然后在触发废弃任务时，还会触发<strong>addBagItem</strong>，进行连接添加任务（因为废弃了一个连接，需要往池子里补充一个），该任务则交给由<strong>主流程2</strong>里定义好的<strong>addConnectionExecutor</strong>线程池执行，那么，现在来看下这个异步添加连接对象的任务流程：</p><p> <img src="/images/1569484-20191112161548177-116128962.png" alt="img"></p>"<p><strong>addConnectionExecutor的call流程</strong></p><p>这个流程就是往连接池里加连接用的，跟<strong>createEntry</strong>结合起来说是因为这俩流程是紧密相关的，除此之外，<strong>主流程5</strong>（fillPool，扩充连接池）也会触发该任务。</p><h2 id="九、主流程4：连接池缩容"><a href="#九、主流程4：连接池缩容" class="headerlink" title="九、主流程4：连接池缩容"></a><strong>九、主流程4：连接池缩容</strong></h2><p>HikariCP会按照minIdle定时清理闲置过久的连接，这个定时任务在主流程2初始化连接池对象时被启用，跟上面的流程一样，也是利用<strong>houseKeepingExecutorService</strong>这个线程池对象做该定时任务的执行器。</p><p>来看下主流程2里是怎么启用该任务的：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//housekeepingPeriodMs的默认值是30s，所以定时任务的间隔为30s</span></span><br><span class="line"><span class="keyword">this</span>.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(<span class="keyword">new</span> HouseKeeper(), <span class="number">100L</span>, housekeepingPeriodMs, MILLISECONDS);</span><br></pre></td></tr></table></figure><p>那么本节主要来说下HouseKeeper这个类，该类实现了Runnable接口，回收逻辑主要在其run方法内，来看看run方法的逻辑流程图：</p><p><img src="/images/1569484-20191111172424204-1819059708.png" alt="img"></p>"<p><strong>主流程4：连接池缩容</strong></p><p>上面的流程就是HouseKeeper的run方法里具体做的事情，由于系统时间回拨会导致该定时任务回收一些连接时产生误差，因此存在如下判断：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//now就是当前系统时间，previous就是上次触发该任务时的时间，housekeepingPeriodMs就是隔多久触发该任务一次</span></span><br><span class="line"><span class="comment">//也就是说plusMillis(previous, housekeepingPeriodMs)表示当前时间</span></span><br><span class="line"><span class="comment">//如果系统时间没被回拨，那么plusMillis(now, 128)一定是大于当前时间的，如果被系统时间被回拨</span></span><br><span class="line"><span class="comment">//回拨的时间超过128ms，那么下面的判断就成立，否则永远不会成立</span></span><br><span class="line"><span class="keyword">if</span> (plusMillis(now, <span class="number">128</span>) &lt; plusMillis(previous, housekeepingPeriodMs))</span><br></pre></td></tr></table></figure><p>这是hikariCP在解决系统时钟被回拨时做出的一种措施，通过流程图可以看到，它是直接把池子里所有的连接对象取出来挨个儿的标记成废弃，并且尝试把状态值修改为STATE_RESERVED（后面会说明这些状态，这里先不深究）。如果系统时钟没有发生改变（绝大多数情况会命中这一块的逻辑），由图知，会把当前池内所有处于闲置状态（STATE_NOT_IN_USE）的连接拿出来，然后计算需要检查的范围，然后循环着修改连接的状态：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//拿到所有处于闲置状态的连接</span></span><br><span class="line"><span class="keyword">final</span> List notInUse = connectionBag.values(STATE_NOT_IN_USE);</span><br><span class="line"><span class="comment">//计算出需要被检查闲置时间的数量，简单来说，池内需要保证最小minIdle个连接活着，所以需要计算出超出这个范围的闲置对象进行检查</span></span><br><span class="line"><span class="keyword">int</span> toRemove = notInUse.size() - config.getMinIdle();</span><br><span class="line"><span class="keyword">for</span> (PoolEntry entry : notInUse) &#123;</span><br><span class="line">  <span class="comment">//在检查范围内，且闲置时间超出idleTimeout，然后尝试将连接对象状态由STATE_NOT_IN_USE变为STATE_RESERVED成功</span></span><br><span class="line">  <span class="keyword">if</span> (toRemove &gt; <span class="number">0</span> &amp;&amp; elapsedMillis(entry.lastAccessed, now) &gt; idleTimeout &amp;&amp; connectionBag.reserve(entry)) &#123;</span><br><span class="line">    closeConnection(entry, <span class="string">"(connection has passed idleTimeout)"</span>); <span class="comment">//满足上述条件，进行连接关闭</span></span><br><span class="line">    toRemove--;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">fillPool(); <span class="comment">//因为可能回收了一些连接，所以要再次触发连接池扩充流程检查下是否需要新增连接。</span></span><br></pre></td></tr></table></figure><p>上面的代码就是流程图里对应的没有回拨系统时间时的流程逻辑。该流程在<strong>idleTimeout</strong>大于0（默认等于0）并且<strong>minIdle</strong>小于<strong>maxPoolSize</strong>的时候才会启用，默认是不启用的，若需要启用，可以按照条件来配置。</p><h2 id="十、主流程5：扩充连接池"><a href="#十、主流程5：扩充连接池" class="headerlink" title="十、主流程5：扩充连接池"></a><strong>十、主流程5：扩充连接池</strong></h2><p>这个流程主要依附HikariPool里的<strong>fillPool</strong>方法，这个方法已经在上面很多流程里出现过了，它的作用就是在触发连接废弃、连接池连接不够用时，发起扩充连接数的操作，这是个很简单的过程，下面看下源码（为了使代码结构更加清晰，对源码做了细微改动）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// PoolEntryCreator关于call方法的实现流程在主流程3里已经看过了，但是这里却有俩PoolEntryCreator对象，</span></span><br><span class="line"><span class="comment">// 这是个较细节的地方，用于打日志用，不再说这部分，为了便于理解，只需要知道这俩对象执行的是同一块call方法即可</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> PoolEntryCreator poolEntryCreator = <span class="keyword">new</span> PoolEntryCreator(<span class="keyword">null</span>);</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> PoolEntryCreator postFillPoolEntryCreator = <span class="keyword">new</span> PoolEntryCreator(<span class="string">"After adding "</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">fillPool</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 这个判断就是根据当前池子里相关数据，推算出需要扩充的连接数，</span></span><br><span class="line">  <span class="comment">// 判断方式就是利用最大连接数跟当前连接总数的差值，与最小连接数与当前池内闲置的连接数的差值，取其最小的那一个得到</span></span><br><span class="line">  <span class="keyword">int</span> needAdd = Math.min(maxPoolSize - connectionBag.size(),</span><br><span class="line">  minIdle - connectionBag.getCount(STATE_NOT_IN_USE));</span><br><span class="line"></span><br><span class="line">  <span class="comment">//减去当前排队的任务，就是最终需要新增的连接数</span></span><br><span class="line">  <span class="keyword">final</span> <span class="keyword">int</span> connectionsToAdd = needAdd - addConnectionQueue.size();</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; connectionsToAdd; i++) &#123;</span><br><span class="line">    <span class="comment">//一般循环的最后一次会命中postFillPoolEntryCreator任务，其实就是在最后一次会打印一次日志而已（可以忽略该干扰逻辑）</span></span><br><span class="line">    addConnectionExecutor.submit((i &lt; connectionsToAdd - <span class="number">1</span>) ? poolEntryCreator : postFillPoolEntryCreator);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由该过程可以知道，最终这个新增连接的任务也是交由<strong>addConnectionExecutor</strong>线程池来处理的，而任务的主题也是<strong>PoolEntryCreator</strong>，这个流程可以参考<strong>主流程3.</strong></p><p>然后<strong>needAdd</strong>的推算：</p><p><strong>Math.min(最大连接数 - 池内当前连接总数, 最小连接数 - 池内闲置的连接数)</strong></p><p>根据这个方式判断，可以保证池内的连接数永远不会超过<strong>maxPoolSize</strong>，也永远不会低于<strong>minIdle</strong>。在连接吃紧的时候，可以保证每次触发都以<strong>minIdle</strong>的数量扩容。因此如果在<strong>maxPoolSize</strong>跟<strong>minIdle</strong>配置的值一样的话，在池内连接吃紧的时候，就不会发生任何扩容了。</p><h2 id="十一、主流程6：连接回收"><a href="#十一、主流程6：连接回收" class="headerlink" title="十一、主流程6：连接回收"></a><strong>十一、主流程6：连接回收</strong></h2><p>最开始说过，最终真实的物理连接对象会被包装成PoolEntry对象，存放进ConcurrentBag，然后获取时，PoolEntry对象又会被再次包装成ProxyConnection对象暴露给使用方的，那么触发连接回收，实际上就是触发ProxyConnection里的close方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">close</span><span class="params">()</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">  <span class="comment">// 原注释：Closing statements can cause connection eviction, so this must run before the conditional below</span></span><br><span class="line">  closeStatements(); <span class="comment">//此连接对象在业务方使用过程中产生的所有statement对象，进行统一close，防止漏close的情况</span></span><br><span class="line">  <span class="keyword">if</span> (delegate != ClosedConnection.CLOSED_CONNECTION) &#123;</span><br><span class="line">    leakTask.cancel(); <span class="comment">//取消连接泄漏检查任务，参考流程2.2</span></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (isCommitStateDirty &amp;&amp; !isAutoCommit) &#123; <span class="comment">//在存在执行语句后并且还打开了事务，调用close时需要主动回滚事务</span></span><br><span class="line">        delegate.rollback(); <span class="comment">//回滚</span></span><br><span class="line">        lastAccess = currentTime(); <span class="comment">//刷新"最后一次使用时间"</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      delegate = ClosedConnection.CLOSED_CONNECTION;</span><br><span class="line">      poolEntry.recycle(lastAccess); <span class="comment">//触发回收</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个就是ProxyConnection里的close方法，可以看到它最终会调用PoolEntry的recycle方法进行回收，除此之外，连接对象的最后一次使用时间也是在这个时候刷新的，该时间是个很重要的属性，可以用来判断一个连接对象的闲置时间，来看下PoolEntry的<strong>recycle</strong>方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">recycle</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> lastAccessed)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (connection != <span class="keyword">null</span>) &#123;</span><br><span class="line">    <span class="keyword">this</span>.lastAccessed = lastAccessed; <span class="comment">//刷新最后使用时间</span></span><br><span class="line">    hikariPool.recycle(<span class="keyword">this</span>); <span class="comment">//触发HikariPool的回收方法，把自己传过去</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>之前有说过，每个PoolEntry对象都持有HikariPool的对象，方便触发连接池的一些操作，由上述代码可以看到，最终还是会触发HikariPool里的recycle方法，再来看下HikariPool的recycle方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">recycle</span><span class="params">(<span class="keyword">final</span> PoolEntry poolEntry)</span> </span>&#123;</span><br><span class="line">  metricsTracker.recordConnectionUsage(poolEntry); <span class="comment">//监控指标相关，忽略</span></span><br><span class="line">  connectionBag.requite(poolEntry); <span class="comment">//最终触发connectionBag的requite方法归还连接，该流程参考ConnectionBag主流程里的requite方法部分</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上就是连接回收部分的逻辑，相比其他流程，还是比较简洁的。</p><h2 id="十二、ConcurrentBag主流程"><a href="#十二、ConcurrentBag主流程" class="headerlink" title="十二、ConcurrentBag主流程"></a><strong>十二、ConcurrentBag主流程</strong></h2><p> 这个类用来存放最终的PoolEntry类型的连接对象，提供了基本的增删查的功能，被HikariPool持有，上面那么多的操作，几乎都是在HikariPool中完成的，HikariPool用来管理实际的连接生产动作和回收动作，实际操作的却是ConcurrentBag类，梳理下上面所有流程的触发点：</p><ul><li><strong>主流程2：</strong>初始化HikariPool时初始化<strong>ConcurrentBag（构造方法）</strong>，预热时通过<strong>createEntry</strong>拿到连接对象，调用<strong>ConcurrentBag.add</strong>添加连接到ConcurrentBag。</li><li><strong>流程1.1：</strong>通过HikariPool获取连接时，通过调用<strong>ConcurrentBag.borrow</strong>拿到一个连接对象。</li><li><strong>主流程6：</strong>通过<strong>ConcurrentBag.requite</strong>归还一个连接。</li><li><strong>流程1.1.2：</strong>触发关闭连接时，会通过<strong>ConcurrentBag.remove</strong>移除连接对象，由前面的流程可知关闭连接触发点为：连接超过最大生命周期maxLifeTime主动废弃、健康检查不通过主动废弃、连接池缩容。</li><li><strong>主流程3：</strong>通过异步添加连接时，通过调用<strong>ConcurrentBag.add</strong>添加连接到ConcurrentBag，由前面的流程可知添加连接触发点为：连接超过最大生命周期maxLifeTime主动废弃连接后、连接池扩容。</li><li><strong>主流程4：</strong>连接池缩容任务，通过调用<strong>ConcurrentBag.values</strong>筛选出需要的做操作的连接对象，然后再通过<strong>ConcurrentBag.reserve</strong>完成对连接对象状态的修改，然后会通过<strong>流程1.1.2</strong>触发关闭和移除连接操作。</li></ul><p>通过触发点整理，可以知道该结构里的主要方法，就是上面触发点里标记为<strong>橙色</strong>的部分，然后来具体看下该类的基本定义和主要方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConcurrentBag</span>&lt;<span class="title">T</span> <span class="keyword">extends</span> <span class="title">IConcurrentBagEntry</span>&gt; <span class="keyword">implements</span> <span class="title">AutoCloseable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> CopyOnWriteArrayList&lt;T&gt; sharedList; <span class="comment">//最终存放PoolEntry对象的地方，它是一个CopyOnWriteArrayList</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> weakThreadLocals; <span class="comment">//默认false，为true时可以让一个连接对象在下方threadList里的list内处于弱引用状态，防止内存泄漏（参见备注1）</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ThreadLocal&lt;List&lt;Object&gt;&gt; threadList; <span class="comment">//线程级的缓存，从sharedList拿到的连接对象，会被缓存进当前线程内，borrow时会先从缓存中拿，从而达到池内无锁实现</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> IBagStateListener listener; <span class="comment">//内部接口，HikariPool实现了该接口，主要用于ConcurrentBag主动通知HikariPool触发添加连接对象的异步操作（也就是主流程3里的addConnectionExecutor所触发的流程）</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AtomicInteger waiters; <span class="comment">//当前因为获取不到连接而发生阻塞的业务线程数，这个在之前的流程里也出现过，比如主流程3里addBagItem就会根据该指标进行判断是否需要新增连接</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">boolean</span> closed; <span class="comment">//标记当前ConcurrentBag是否已被关闭</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> SynchronousQueue&lt;T&gt; handoffQueue; <span class="comment">//这是个即产即销的队列，用于在连接不够用时，及时获取到add方法里新创建的连接对象，详情可以参考下面borrow和add的代码</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//内部接口，PoolEntry类实现了该接口</span></span><br><span class="line">    <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IConcurrentBagEntry</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//连接对象的状态，前面的流程很多地方都已经涉及到了，比如主流程4的缩容</span></span><br><span class="line">        <span class="keyword">int</span> STATE_NOT_IN_USE = <span class="number">0</span>; <span class="comment">//闲置</span></span><br><span class="line">        <span class="keyword">int</span> STATE_IN_USE = <span class="number">1</span>; <span class="comment">//使用中</span></span><br><span class="line">        <span class="keyword">int</span> STATE_REMOVED = -<span class="number">1</span>; <span class="comment">//已废弃</span></span><br><span class="line">        <span class="keyword">int</span> STATE_RESERVED = -<span class="number">2</span>; <span class="comment">//标记保留，介于闲置和废弃之间的中间状态，主要由缩容那里触发修改</span></span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">boolean</span> <span class="title">compareAndSet</span><span class="params">(<span class="keyword">int</span> expectState, <span class="keyword">int</span> newState)</span></span>; <span class="comment">//尝试利用cas修改连接对象的状态值</span></span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">void</span> <span class="title">setState</span><span class="params">(<span class="keyword">int</span> newState)</span></span>; <span class="comment">//设置状态值</span></span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">int</span> <span class="title">getState</span><span class="params">()</span></span>; <span class="comment">//获取状态值</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//参考上面listener属性的解释</span></span><br><span class="line">    <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IBagStateListener</span> </span>&#123;</span><br><span class="line">        <span class="function"><span class="keyword">void</span> <span class="title">addBagItem</span><span class="params">(<span class="keyword">int</span> waiting)</span></span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//获取连接方法</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> T <span class="title">borrow</span><span class="params">(<span class="keyword">long</span> timeout, <span class="keyword">final</span> TimeUnit timeUnit)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//回收连接方法</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">requite</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//添加连接方法</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//移除连接方法</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">remove</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据连接状态值获取当前池子内所有符合条件的连接集合</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> List <span class="title">values</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> state)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//获取当前池子内所有的连接</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> List <span class="title">values</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//利用cas把传入的连接对象的state从 STATE_NOT_IN_USE 变为 STATE_RESERVED</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">reserve</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//获取当前池子内符合传入状态值的连接数量</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getCount</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> state)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//省略...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从这个基本结构就可以稍微看出HikariCP是如何优化传统连接池实现的了，相比Druid来说，HikariCP更加偏向无锁实现，尽量避免锁竞争的发生。</p><h3 id="12-1：borrow"><a href="#12-1：borrow" class="headerlink" title="12.1：borrow"></a><strong>12.1：borrow</strong></h3><p>这个方法用来获取一个可用的连接对象，触发点为<strong>流程1.1</strong>，HikariPool就是利用该方法获取连接的，下面来看下该方法做了什么：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> T <span class="title">borrow</span><span class="params">(<span class="keyword">long</span> timeout, <span class="keyword">final</span> TimeUnit timeUnit)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">    <span class="comment">// 源注释：Try the thread-local list first</span></span><br><span class="line">    <span class="keyword">final</span> List&lt;Object&gt; list = threadList.get(); <span class="comment">//首先从当前线程的缓存里拿到之前被缓存进来的连接对象集合</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = list.size() - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">        <span class="keyword">final</span> Object entry = list.remove(i); <span class="comment">//先移除，回收方法那里会再次add进来</span></span><br><span class="line">        <span class="keyword">final</span> T bagEntry = weakThreadLocals ? ((WeakReference&lt;T&gt;) entry).get() : (T) entry; <span class="comment">//默认不启用弱引用</span></span><br><span class="line">        <span class="comment">// 获取到对象后，通过cas尝试把其状态从STATE_NOT_IN_USE 变为 STATE_IN_USE，注意，这里如果其他线程也在使用这个连接对象，</span></span><br><span class="line">        <span class="comment">// 并且成功修改属性，那么当前线程的cas会失败，那么就会继续循环尝试获取下一个连接对象</span></span><br><span class="line">        <span class="keyword">if</span> (bagEntry != <span class="keyword">null</span> &amp;&amp; bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) &#123;</span><br><span class="line">            <span class="keyword">return</span> bagEntry; <span class="comment">//cas设置成功后，表示当前线程绕过其他线程干扰，成功获取到该连接对象，直接返回</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 源注释：Otherwise, scan the shared list ... then poll the handoff queue</span></span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">int</span> waiting = waiters.incrementAndGet(); <span class="comment">//如果缓存内找不到一个可用的连接对象，则认为需要“回源”，waiters+1</span></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (T bagEntry : sharedList) &#123;</span><br><span class="line">            <span class="comment">//循环sharedList，尝试把连接状态值从STATE_NOT_IN_USE 变为 STATE_IN_USE</span></span><br><span class="line">            <span class="keyword">if</span> (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) &#123;</span><br><span class="line">                <span class="comment">// 源注释：If we may have stolen another waiter's connection, request another bag add.</span></span><br><span class="line">                <span class="keyword">if</span> (waiting &gt; <span class="number">1</span>) &#123; <span class="comment">//阻塞线程数大于1时，需要触发HikariPool的addBagItem方法来进行添加连接入池，这个方法的实现参考主流程3</span></span><br><span class="line">                    listener.addBagItem(waiting - <span class="number">1</span>);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span> bagEntry; <span class="comment">//cas设置成功，跟上面的逻辑一样，表示当前线程绕过其他线程干扰，成功获取到该连接对象，直接返回</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//走到这里说明不光线程缓存里的列表竞争不到连接对象，连sharedList里也找不到可用的连接，这时则认为需要通知HikariPool，该触发添加连接操作了</span></span><br><span class="line">        listener.addBagItem(waiting);</span><br><span class="line"></span><br><span class="line">        timeout = timeUnit.toNanos(timeout); <span class="comment">//这时候开始利用timeout控制获取时间</span></span><br><span class="line">        <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="keyword">long</span> start = currentTime();</span><br><span class="line">            <span class="comment">//尝试从handoffQueue队列里获取最新被加进来的连接对象（一般新入的连接对象除了加进sharedList之外，还会被offer进该队列）</span></span><br><span class="line">            <span class="keyword">final</span> T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);</span><br><span class="line">            <span class="comment">//如果超出指定时间后仍然没有获取到可用的连接对象，或者获取到对象后通过cas设置成功，这两种情况都不需要重试，直接返回对象</span></span><br><span class="line">            <span class="keyword">if</span> (bagEntry == <span class="keyword">null</span> || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) &#123;</span><br><span class="line">                <span class="keyword">return</span> bagEntry;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//走到这里说明从队列内获取到了连接对象，但是cas设置失败，说明又该对象又被其他线程率先拿去用了，若时间还够，则再次尝试获取</span></span><br><span class="line">            timeout -= elapsedNanos(start); <span class="comment">//timeout减去消耗的时间，表示下次循环可用时间</span></span><br><span class="line">        &#125; <span class="keyword">while</span> (timeout &gt; <span class="number">10_000</span>); <span class="comment">//剩余时间大于10s时才继续进行，一般情况下，这个循环只会走一次，因为timeout很少会配的比10s还大</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>; <span class="comment">//超时，仍然返回null</span></span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        waiters.decrementAndGet(); <span class="comment">//这一步出去后，HikariPool收到borrow的结果，算是走出阻塞，所以waiters-1</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>仔细看下注释，该过程大致分成三个主要步骤：</p><ol><li>从线程缓存获取连接</li><li>获取不到再从<strong>sharedList</strong>里获取</li><li>都获取不到则触发添加连接逻辑，并尝试从队列里获取新生成的连接对象</li></ol><h3 id="12-2：add"><a href="#12-2：add" class="headerlink" title="12.2：add"></a><strong>12.2：add</strong></h3><p>这个流程会添加一个连接对象进入bag，通常由<strong>主流程3</strong>里的<strong>addBagItem</strong>方法通过<strong>addConnectionExecutor</strong>异步任务触发添加操作，该方法主流程如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    sharedList.add(bagEntry); <span class="comment">//直接加到sharedList里去</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 源注释：spin until a thread takes it or none are waiting</span></span><br><span class="line">    <span class="comment">// 参考borrow流程，当存在线程等待获取可用连接，并且当前新入的这个连接状态仍然是闲置状态，且队列里无消费者等待获取时，发起一次线程调度</span></span><br><span class="line">    <span class="keyword">while</span> (waiters.get() &gt; <span class="number">0</span> &amp;&amp; bagEntry.getState() == STATE_NOT_IN_USE &amp;&amp; !handoffQueue.offer(bagEntry)) &#123; <span class="comment">//注意这里会offer一个连接对象入队列</span></span><br><span class="line">        yield();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>结合borrow来理解的话，这里在存在等待线程时会添加一个连接对象入队列，可以让<strong>borrow</strong>里发生等待的地方更容易poll到这个连接对象。</p><h3 id="12-3：requite"><a href="#12-3：requite" class="headerlink" title="12.3：requite"></a><strong>12.3：requite</strong></h3><p>这个流程会回收一个连接，该方法的触发点在<strong>主流程6</strong>，具体代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">requite</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">    bagEntry.setState(STATE_NOT_IN_USE); <span class="comment">//回收意味着使用完毕，更改state为STATE_NOT_IN_USE状态</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; waiters.get() &gt; <span class="number">0</span>; i++) &#123; <span class="comment">//如果存在等待线程的话，尝试传给队列，让borrow获取</span></span><br><span class="line">        <span class="keyword">if</span> (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> ((i &amp; <span class="number">0xff</span>) == <span class="number">0xff</span>) &#123;</span><br><span class="line">            parkNanos(MICROSECONDS.toNanos(<span class="number">10</span>));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            yield();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> List&lt;Object&gt; threadLocalList = threadList.get();</span><br><span class="line">    <span class="keyword">if</span> (threadLocalList.size() &lt; <span class="number">50</span>) &#123; <span class="comment">//线程内连接集合的缓存最多50个，这里回收连接时会再次加进当前线程的缓存里，方便下次borrow获取</span></span><br><span class="line">        threadLocalList.add(weakThreadLocals ? <span class="keyword">new</span> WeakReference&lt;&gt;(bagEntry) : bagEntry); <span class="comment">//默认不启用弱引用，若启用的话，则缓存集合里的连接对象没有内存泄露的风险</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-4：remove"><a href="#12-4：remove" class="headerlink" title="12.4：remove"></a><strong>12.4：remove</strong></h3><p>这个负责从池子里移除一个连接对象，触发点在<strong>流程1.1.2</strong>，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">remove</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 下面两个cas操作，都是从其他状态变为移除状态，任意一个成功，都不会走到下面的warn log</span></span><br><span class="line">    <span class="keyword">if</span> (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) &amp;&amp; !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) &amp;&amp; !closed) &#123;</span><br><span class="line">        LOGGER.warn(<span class="string">"Attempt to remove an object from the bag that was not borrowed or reserved: &#123;&#125;"</span>, bagEntry);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 直接从sharedList移除掉</span></span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">boolean</span> removed = sharedList.remove(bagEntry);</span><br><span class="line">    <span class="keyword">if</span> (!removed &amp;&amp; !closed) &#123;</span><br><span class="line">        LOGGER.warn(<span class="string">"Attempt to remove an object from the bag that does not exist: &#123;&#125;"</span>, bagEntry);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> removed;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意的是，移除时仅仅移除了<strong>sharedList</strong>里的对象，各个线程内缓存的那一份集合里对应的对象并没有被移除，这个时候会不会存在该连接再次从缓存里拿到呢？会的，但是不会返回出去，而是直接<strong>remove</strong>掉了，仔细看<strong>borrow</strong>的代码发现状态不是闲置状态的时候，取出来时就会<strong>remove</strong>掉，然后也拿不出去，自然也不会触发回收方法。</p><h3 id="12-5：values"><a href="#12-5：values" class="headerlink" title="12.5：values"></a><strong>12.5：values</strong></h3><p>该方法存在重载方法，用于返回当前池子内连接对象的集合，触发点在主流程4，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> List <span class="title">values</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> state)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//过滤出来符合状态值的对象集合逆序后返回出去</span></span><br><span class="line">    <span class="keyword">final</span> List list = sharedList.stream().filter(e -&gt; e.getState() == state).collect(Collectors.toList());</span><br><span class="line">    Collections.reverse(list);</span><br><span class="line">    <span class="keyword">return</span> list;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> List <span class="title">values</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">//返回全部连接对象（注意下方clone为浅拷贝）</span></span><br><span class="line">    <span class="keyword">return</span> (List) sharedList.clone();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-6：reserve"><a href="#12-6：reserve" class="headerlink" title="12.6：reserve"></a><strong>12.6：reserve</strong></h3><p>该方法单纯将连接对象的状态值由STATE_NOT_IN_USE修改为STATE_RESERVED，触发点仍然是<strong>主流程4</strong>，缩容时使用，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">reserve</span><span class="params">(<span class="keyword">final</span> T bagEntry)</span></span>&#123;</span><br><span class="line">   <span class="keyword">return</span> bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-7：getCount"><a href="#12-7：getCount" class="headerlink" title="12.7：getCount"></a><strong>12.7：getCount</strong></h3><p>该方法用于返回池内符合某个状态值的连接的总数量，触发点为<strong>主流程5</strong>，扩充连接池时用于获取闲置连接总数，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getCount</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> state)</span></span>&#123;</span><br><span class="line">   <span class="keyword">int</span> count = <span class="number">0</span>;</span><br><span class="line">   <span class="keyword">for</span> (IConcurrentBagEntry e : sharedList) &#123;</span><br><span class="line">      <span class="keyword">if</span> (e.getState() == state) &#123;</span><br><span class="line">         count++;</span><br><span class="line">      &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> count;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上就是<strong>ConcurrentBag</strong>的主要方法和处理连接对象的主要流程。</p><h2 id="十三、总结"><a href="#十三、总结" class="headerlink" title="十三、总结"></a><strong>十三、总结</strong></h2><p>到这里基本上一个连接的生产到获取到回收到废弃一整个生命周期在HikariCP内是如何管理的就说完了，相比之前的Druid的实现，有很大的不同，主要是HikariCP的无锁获取连接，本篇没有涉及<strong>FastList</strong>的说明，因为从连接管理这个角度确实很少用到该结构，用到FastList的地方主要在存储连接对象生成的statement对象以及用于存储线程内缓存起来的连接对象；</p><p>除此之外HikariCP还利用javassist技术编译期生成了<strong>ProxyConnection</strong>的初始化，这里也没有相关说明，网上有关HikariCP的优化有很多文章，大多数都提到了字节码优化、fastList、concurrentBag的实现，本篇主要通过深入解析<strong>HikariPool</strong>和<strong>ConcurrentBag</strong>的实现，来说明HikariCP相比Druid具体做了哪些不一样的操作。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://www.cnblogs.com/hama1993/p/11421579.html" target="_blank" rel="noopener">池化技术（二）HikariCP是如何管理数据库连接的？</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;零、类图和流程图&quot;&gt;&lt;a href=&quot;#零、类图和流程图&quot; class=&quot;headerlink&quot; title=&quot;零、类图和流程图&quot;&gt;&lt;/a&gt;&lt;strong&gt;零、类图和流程图&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;开始前先来了解下HikariCP获取一个连接时类间的交互流
      
    
    </summary>
    
      <category term="HikariCP" scheme="http://yoursite.com/categories/HikariCP/"/>
    
    
      <category term="HikariCP" scheme="http://yoursite.com/tags/HikariCP/"/>
    
  </entry>
  
  <entry>
    <title>mybatis--缓存原理分析2</title>
    <link href="http://yoursite.com/2021/04/16/mybatis/mybatis--%E7%BC%93%E5%AD%98%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%902/"/>
    <id>http://yoursite.com/2021/04/16/mybatis/mybatis--缓存原理分析2/</id>
    <published>2021-04-16T10:00:22.122Z</published>
    <updated>2021-04-19T11:32:36.260Z</updated>
    
    <content type="html"><![CDATA[<h1 id="1-MyBatis一级缓存实现"><a href="#1-MyBatis一级缓存实现" class="headerlink" title="1 MyBatis一级缓存实现"></a>1 MyBatis一级缓存实现</h1><h2 id="1-1-什么是一级缓存？"><a href="#1-1-什么是一级缓存？" class="headerlink" title="1.1 什么是一级缓存？"></a>1.1 什么是一级缓存？</h2><p>每当我们使用MyBatis开启一次和数据库的会话，<code>MyBatis会创建出一个SqlSession对象表示一次数据库会话</code>。</p><p>在对数据库的一次会话中，我们有可能会反复地执行完全相同的查询语句，如果不采取一些措施的话，每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询，那么它们的结果极有可能完全相同，由于查询一次数据库的代价很大，这有可能造成很大的资源浪费。</p><p>为了解决这一问题，减少资源的浪费，MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存，将每次查询到的结果结果缓存起来，当下次查询的时候，如果判断先前有个完全一样的查询，会直接从缓存中直接将结果取出，返回给用户，不需要再进行一次数据库查询了。</p><p>如下图所示，MyBatis会在一次会话的表示—-<code>一个SqlSession对象中创建一个本地缓存(local cache)，对于每一次查询，都会尝试根据查询的条件去本地缓存中查找是否在缓存中，如果在缓存中，就直接从缓存中取出，然后返回给用户；否则，从数据库读取数据，将查询结果存入缓存并返回给用户</code>。</p><p><img src="/images/03111316_A8GK.png" alt="输入图片说明"></p>"<p><strong><code>对于会话（Session）级别的数据缓存，我们称之为一级数据缓存，简称一级缓存。</code></strong></p><h2 id="1-2-MyBatis中的一级缓存是怎样组织的？"><a href="#1-2-MyBatis中的一级缓存是怎样组织的？" class="headerlink" title="1.2 MyBatis中的一级缓存是怎样组织的？"></a>1.2 MyBatis中的一级缓存是怎样组织的？</h2><p>（即SqlSession中的缓存是怎样组织的？）</p><p>由于MyBatis使用SqlSession对象表示一次数据库的会话，那么，<code>对于会话级别的一级缓存也应该是在SqlSession中控制的</code>。实际上, MyBatis只是一个MyBatis对外的接口，<code>SqlSession将它的工作交给了Executor执行器这个角色来完成，负责完成对数据库的各种操作</code>。当创建了一个SqlSession对象时，<code>MyBatis会为这个SqlSession对象创建一个新的Executor执行器，而缓存信息就被维护在这个Executor执行器中</code>，MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。<code>SqlSession、Executor、Cache</code>之间的关系如下列类图所示：</p><p><img src="/images/03111641_oEOr.png" alt="输入图片说明"></p>"<p>如上述的类图所示，Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache，<code>则对于BaseExecutor对象而言，它将使用PerpetualCache对象维护缓存</code>。</p><p>综上，<code>SqlSession对象、Executor对象、Cache对象</code>之间的关系如下图所示：</p><p><img src="/images/03111935_7joy.png" alt="输入图片说明"></p>"<p>由于Session级别的一级缓存实际上就是使用PerpetualCache维护的，那么PerpetualCache是怎样实现的呢？</p><p>PerpetualCache实现原理其实很简单，<code>其内部就是通过一个简单的HashMap&lt;k,v&gt; 来实现的，没有其他的任何限制</code>。如下是PerpetualCache的实现代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.apache.ibatis.cache.impl;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;  </span><br><span class="line"><span class="keyword">import</span> java.util.Map;  </span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.locks.ReadWriteLock;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.cache.Cache;  </span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.cache.CacheException;  </span><br><span class="line">  </span><br><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * 使用简单的HashMap来维护缓存 </span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Clinton Begin </span></span><br><span class="line"><span class="comment"> */</span>  </span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PerpetualCache</span> <span class="keyword">implements</span> <span class="title">Cache</span> </span>&#123;  </span><br><span class="line">  </span><br><span class="line">  <span class="keyword">private</span> String id;  </span><br><span class="line">  </span><br><span class="line">  <span class="keyword">private</span> Map&lt;Object, Object&gt; cache = <span class="keyword">new</span> HashMap&lt;Object, Object&gt;();  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">PerpetualCache</span><span class="params">(String id)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">this</span>.id = id;  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> String <span class="title">getId</span><span class="params">()</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">return</span> id;  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getSize</span><span class="params">()</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">return</span> cache.size();  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">putObject</span><span class="params">(Object key, Object value)</span> </span>&#123;  </span><br><span class="line">    cache.put(key, value);  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> Object <span class="title">getObject</span><span class="params">(Object key)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">return</span> cache.get(key);  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> Object <span class="title">removeObject</span><span class="params">(Object key)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">return</span> cache.remove(key);  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">clear</span><span class="params">()</span> </span>&#123;  </span><br><span class="line">    cache.clear();  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> ReadWriteLock <span class="title">getReadWriteLock</span><span class="params">()</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">null</span>;  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">equals</span><span class="params">(Object o)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">if</span> (getId() == <span class="keyword">null</span>) <span class="keyword">throw</span> <span class="keyword">new</span> CacheException(<span class="string">"Cache instances require an ID."</span>);  </span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span> == o) <span class="keyword">return</span> <span class="keyword">true</span>;  </span><br><span class="line">    <span class="keyword">if</span> (!(o <span class="keyword">instanceof</span> Cache)) <span class="keyword">return</span> <span class="keyword">false</span>;  </span><br><span class="line">  </span><br><span class="line">    Cache otherCache = (Cache) o;  </span><br><span class="line">    <span class="keyword">return</span> getId().equals(otherCache.getId());  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">hashCode</span><span class="params">()</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">if</span> (getId() == <span class="keyword">null</span>) <span class="keyword">throw</span> <span class="keyword">new</span> CacheException(<span class="string">"Cache instances require an ID."</span>);  </span><br><span class="line">    <span class="keyword">return</span> getId().hashCode();  </span><br><span class="line">  &#125;  </span><br><span class="line">  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="1-3-一级缓存的生命周期有多长？"><a href="#1-3-一级缓存的生命周期有多长？" class="headerlink" title="1.3 一级缓存的生命周期有多长？"></a>1.3 一级缓存的生命周期有多长？</h2><ol><li>MyBatis在开启一个数据库会话时，会创建一个新的SqlSession对象，SqlSession对象中会有一个新的Executor对象，Executor对象中持有一个新的PerpetualCache对象；<code>当会话结束时，SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉</code>。</li><li>如果<code>SqlSession调用了close()方法</code>，会释放掉一级缓存PerpetualCache对象，一级缓存将不可用；</li><li>如果<code>SqlSession调用了clearCache()</code>，会清空PerpetualCache对象中的数据，但是该对象仍可使用；</li><li>SqlSession中<code>执行了任何一个update操作(update()、delete()、insert())</code>，都会清空PerpetualCache对象的数据，<code>但是该对象可以继续使用</code>；</li></ol><p><img src="/images/03112403_wK6s.png" alt="输入图片说明"></p>"<h2 id="1-4-SqlSession-一级缓存的工作流程"><a href="#1-4-SqlSession-一级缓存的工作流程" class="headerlink" title="1.4 SqlSession 一级缓存的工作流程"></a>1.4 SqlSession 一级缓存的工作流程</h2><ol><li>对于某个查询，<code>根据statementId,params,rowBounds来构建一个key值</code>，根据这个key值去缓存Cache中取出对应的key值存储的缓存结果；</li><li>判断从Cache中根据特定的key值取的数据数据是否为空，即是否命中；</li><li>如果命中，则直接将缓存结果返回；</li><li>如果没命中： 4.1 去数据库中查询数据，得到查询结果； 4.2 将key和查询到的结果分别作为key,value对存储到Cache中； 4.3 将查询结果返回；</li><li>结束。</li></ol><p><img src="/https://static.oschina.net/uploads/img/201601/03112754_vQKY.png" alt="输入图片说明"></p>"<h2 id="1-5-Cache接口的设计以及CacheKey的定义"><a href="#1-5-Cache接口的设计以及CacheKey的定义" class="headerlink" title="1.5 Cache接口的设计以及CacheKey的定义"></a>1.5 Cache接口的设计以及CacheKey的定义</h2><p>如下图所示，MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为<code>其Cache提供者的SPI(Service Provider Interface)</code> ，<code>所有的MyBatis内部的Cache缓存，都应该实现这一接口</code>。MyBatis定义了一个PerpetualCache实现类实现了Cache接口，实际上，<code>在SqlSession对象里的Executor对象内维护的Cache类型实例对象，就是PerpetualCache子类创建的</code>。</p><p>（MyBatis内部还有很多Cache接口的实现，一级缓存只会涉及到这一个PerpetualCache子类，Cache的其他实现将会放到二级缓存中介绍）。</p><p><img src="/images/03113206_t3CW.png" alt="输入图片说明"></p>"<p>我们知道，Cache最核心的实现其实就是一个Map，将本次查询使用的特征值作为key，将查询结果作为value存储到Map中。现在最核心的问题出现了：<code>怎样来确定一次查询的特征值？</code>换句话说就是：<code>怎样判断某两次查询是完全相同的查询？</code>也可以这样说：<code>如何确定Cache中的key值？</code></p><p>MyBatis认为，对于两次查询，如果以下条件都完全一样，那么就认为它们是完全相同的两次查询：</p><blockquote><ol><li>传入的 statementId</li><li>查询时要求的结果集中的结果范围 （结果的范围通过rowBounds.offset和rowBounds.limit表示）</li><li>这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串（boundSql.getSql() ）</li><li>传递给java.sql.Statement要设置的参数值</li></ol></blockquote><p><strong>现在分别解释上述四个条件：</strong></p><ol><li>传入的statementId，对于MyBatis而言，你要使用它，<code>必须需要一个statementId，它代表着你将执行什么样的Sql</code>；</li><li>MyBatis自身提供的分页功能是通过RowBounds来实现的，它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集，这种分页功能是基于查询结果的再过滤，而不是进行数据库的物理分页；</li><li>由于MyBatis底层还是依赖于JDBC实现的，那么，对于两次完全一模一样的查询，MyBatis要保证对于底层JDBC而言，也是完全一致的查询才行。而对于JDBC而言，两次查询，只要传入给JDBC的SQL语句完全一致，传入的参数也完全一致，就认为是两次查询是完全一致的。</li><li>上述的第3个条件正是要求保证传递给JDBC的SQL语句完全一致；第4条则是保证传递给JDBC的参数也完全一致；即3、4两条MyBatis最本质的要求就是：<code>调用JDBC的时候，传入的SQL语句要完全相同，传递给JDBC的参数值也要完全相同</code>。</li></ol><p>综上所述,CacheKey由以下条件决定：<strong><code>statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值</code></strong>；</p><ol><li><strong>CacheKey的创建</strong></li></ol><p>对于每次的查询请求，Executor都会根据传递的参数信息以及动态生成的SQL语句，将上面的条件根据一定的计算规则，创建一个对应的CacheKey对象。</p><p>我们知道创建CacheKey的目的，就两个：</p><blockquote><ol><li>根据CacheKey作为key,去Cache缓存中查找缓存结果；</li><li>如果查找缓存命中失败，则通过此CacheKey作为key，将从数据库查询到的结果作为value，组成key,value对存储到Cache缓存中；</li></ol></blockquote><p>CacheKey的构建被放置到了Executor接口的实现类BaseExecutor中，定义如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * 所属类:  org.apache.ibatis.executor.BaseExecutor </span></span><br><span class="line"><span class="comment"> * 功能   :   根据传入信息构建CacheKey </span></span><br><span class="line"><span class="comment"> */</span>  </span><br><span class="line"><span class="function"><span class="keyword">public</span> CacheKey <span class="title">createCacheKey</span><span class="params">(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">if</span> (closed) <span class="keyword">throw</span> <span class="keyword">new</span> ExecutorException(<span class="string">"Executor was closed."</span>);  </span><br><span class="line">    CacheKey cacheKey = <span class="keyword">new</span> CacheKey();  </span><br><span class="line">    <span class="comment">//1.statementId  </span></span><br><span class="line">    cacheKey.update(ms.getId());  </span><br><span class="line">    <span class="comment">//2. rowBounds.offset  </span></span><br><span class="line">    cacheKey.update(rowBounds.getOffset());  </span><br><span class="line">    <span class="comment">//3. rowBounds.limit  </span></span><br><span class="line">    cacheKey.update(rowBounds.getLimit());  </span><br><span class="line">    <span class="comment">//4. SQL语句  </span></span><br><span class="line">    cacheKey.update(boundSql.getSql());  </span><br><span class="line">    <span class="comment">//5. 将每一个要传递给JDBC的参数值也更新到CacheKey中  </span></span><br><span class="line">    List&lt;ParameterMapping&gt; parameterMappings = boundSql.getParameterMappings();  </span><br><span class="line">    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();  </span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; parameterMappings.size(); i++) &#123; <span class="comment">// mimic DefaultParameterHandler logic  </span></span><br><span class="line">        ParameterMapping parameterMapping = parameterMappings.get(i);  </span><br><span class="line">        <span class="keyword">if</span> (parameterMapping.getMode() != ParameterMode.OUT) &#123;  </span><br><span class="line">            Object value;  </span><br><span class="line">            String propertyName = parameterMapping.getProperty();  </span><br><span class="line">            <span class="keyword">if</span> (boundSql.hasAdditionalParameter(propertyName)) &#123;  </span><br><span class="line">                value = boundSql.getAdditionalParameter(propertyName);  </span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (parameterObject == <span class="keyword">null</span>) &#123;  </span><br><span class="line">                value = <span class="keyword">null</span>;  </span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) &#123;  </span><br><span class="line">                value = parameterObject;  </span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;  </span><br><span class="line">                MetaObject metaObject = configuration.newMetaObject(parameterObject);  </span><br><span class="line">                value = metaObject.getValue(propertyName);  </span><br><span class="line">            &#125;  </span><br><span class="line">            <span class="comment">//将每一个要传递给JDBC的参数值也更新到CacheKey中  </span></span><br><span class="line">            cacheKey.update(value);  </span><br><span class="line">        &#125;  </span><br><span class="line">    &#125;  </span><br><span class="line">    <span class="keyword">return</span> cacheKey;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li><strong>CacheKey的hashcode生成算法</strong></li></ol><p>刚才已经提到，Cache接口的实现，本质上是使用的HashMap&lt;k,v&gt;,而构建CacheKey的目的就是为了作为HashMap&lt;k,v&gt;中的key值。<code>而HashMap是通过key值的hashcode 来组织和存储的，那么，构建CacheKey的过程实际上就是构造其hashCode的过程</code>。下面的代码就是CacheKey的核心hashcode生成算法，感兴趣的话可以看一下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">update</span><span class="params">(Object object)</span> </span>&#123;  </span><br><span class="line">    <span class="keyword">if</span> (object != <span class="keyword">null</span> &amp;&amp; object.getClass().isArray()) &#123;  </span><br><span class="line">        <span class="keyword">int</span> length = Array.getLength(object);  </span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; length; i++) &#123;  </span><br><span class="line">            Object element = Array.get(object, i);  </span><br><span class="line">            doUpdate(element);  </span><br><span class="line">        &#125;  </span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;  </span><br><span class="line">        doUpdate(object);  </span><br><span class="line">    &#125;  </span><br><span class="line">&#125;  </span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">doUpdate</span><span class="params">(Object object)</span> </span>&#123;  </span><br><span class="line"> </span><br><span class="line">    <span class="comment">//1. 得到对象的hashcode;    </span></span><br><span class="line">    <span class="keyword">int</span> baseHashCode = object == <span class="keyword">null</span> ? <span class="number">1</span> : object.hashCode();  </span><br><span class="line">    <span class="comment">//对象计数递增  </span></span><br><span class="line">    count++;  </span><br><span class="line">    checksum += baseHashCode;  </span><br><span class="line">    <span class="comment">//2. 对象的hashcode 扩大count倍  </span></span><br><span class="line">    baseHashCode *= count;  </span><br><span class="line">    <span class="comment">//3. hashCode * 拓展因子（默认37）+拓展扩大后的对象hashCode值  </span></span><br><span class="line">    hashcode = multiplier * hashcode + baseHashCode;  </span><br><span class="line">    updateList.add(object);  </span><br><span class="line">&#125;  </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* cache key比较过程</span></span><br><span class="line"><span class="comment">* 先比较hashcode，checksum，count，再比较updateList；</span></span><br><span class="line"><span class="comment">* 这样可以减少比较的耗时</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">equals</span><span class="params">(Object object)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span> == object) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!(object <span class="keyword">instanceof</span> CacheKey)) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> CacheKey cacheKey = (CacheKey) object;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (hashcode != cacheKey.hashcode) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (checksum != cacheKey.checksum) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (count != cacheKey.count) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; updateList.size(); i++) &#123;</span><br><span class="line">      Object thisObject = updateList.get(i);</span><br><span class="line">      Object thatObject = cacheKey.updateList.get(i);</span><br><span class="line">      <span class="keyword">if</span> (!ArrayUtil.equals(thisObject, thatObject)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p><strong><code>MyBatis认为的完全相同的查询，不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同，你只要保证statementId，rowBounds,最后生成的SQL语句，以及这个SQL语句所需要的参数完全一致就可以了。</code></strong></p><h2 id="1-6-一级缓存的性能分析"><a href="#1-6-一级缓存的性能分析" class="headerlink" title="1.6 一级缓存的性能分析"></a>1.6 一级缓存的性能分析</h2><ol><li><strong>MyBatis对会话（Session）级别的一级缓存设计的比较简单，就简单地使用了HashMap来维护，并没有对HashMap的容量和大小进行限制</strong></li></ol><p>读者有可能就觉得不妥了：如果我一直使用某一个SqlSession对象查询数据，这样会不会导致HashMap太大，<code>而导致 java.lang.OutOfMemoryError错误啊？</code> 读者这么考虑也不无道理，不过MyBatis的确是这样设计的。</p><p>MyBatis这样设计也有它自己的理由：</p><blockquote><p>a. 一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多，执行完就会消亡；</p><p>b. 对于某一个SqlSession对象而言，只要执行update操作（update、insert、delete），都会将这个SqlSession对象中对应的一级缓存清空掉，所以一般情况下不会出现缓存过大，影响JVM内存空间的问题；</p><p>c. 可以手动地释放掉SqlSession对象中的缓存。</p></blockquote><ol start="2"><li><strong>一级缓存是一个粗粒度的缓存，没有更新缓存和缓存过期的概念</strong></li></ol><p>MyBatis的一级缓存就是使用了简单的HashMap，MyBatis只负责将查询数据库的结果存储到缓存中去， 不会去判断缓存存放的时间是否过长、是否过期，因此也就没有对缓存的结果进行更新这一说了。</p><p>根据一级缓存的特性，在使用的过程中，我认为应该注意：</p><blockquote><ol><li>对于数据变化频率很大，并且需要高时效准确性的数据要求，我们使用SqlSession查询的时候，要控制好SqlSession的生存时间，SqlSession的生存时间越长，它其中缓存的数据有可能就越旧，从而造成和真实数据库的误差；同时对于这种情况，用户也可以手动地适时清空SqlSession中的缓存；</li><li>对于只执行、并且频繁执行大范围的select操作的SqlSession对象，SqlSession对象的生存时间不应过长。</li></ol></blockquote><h1 id="2-MyBatis二级缓存实现"><a href="#2-MyBatis二级缓存实现" class="headerlink" title="2 MyBatis二级缓存实现"></a>2 MyBatis二级缓存实现</h1><p>MyBatis的二级缓存是<code>Application级别的缓存</code>，它可以提高对数据库查询的效率，以提高应用的性能。 </p><h2 id="2-1-MyBatis的缓存机制整体设计以及二级缓存的工作模式"><a href="#2-1-MyBatis的缓存机制整体设计以及二级缓存的工作模式" class="headerlink" title="2.1 MyBatis的缓存机制整体设计以及二级缓存的工作模式"></a>2.1 MyBatis的缓存机制整体设计以及二级缓存的工作模式</h2><p><img src="/images/03115709_jEgW.png" alt="输入图片说明"></p>"<p>如上图所示，当开一个会话时，一个SqlSession对象会使用一个Executor对象来完成会话操作，<code>MyBatis的二级缓存机制的关键就是对这个Executor对象做文章</code>。如果用户配置了<code>&quot;cacheEnabled=true&quot;</code>，那么MyBatis在为SqlSession对象创建Executor对象时，<code>会对Executor对象加上一个装饰者：CachingExecutor</code>，这时SqlSession使用CachingExecutor对象来完成操作请求。<code>CachingExecutor对于查询请求，会先判断该查询请求在Application级别的二级缓存中是否有缓存结果</code>，如果有查询结果，则直接返回缓存结果；如果缓存中没有，再交给真正的Executor对象来完成查询操作，<code>之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中</code>，然后在返回给用户。</p><p><img src="/images/03115935_Curi.png" alt="输入图片说明"></p>"<p>CachingExecutor是Executor的装饰者，以增强Executor的功能，使其具有缓存查询的功能，<code>这里用到了设计模式中的装饰者模式</code>，CachingExecutor和Executor的接口的关系如下类图所示：</p><p><img src="/images/03120051_C97i.png" alt="输入图片说明"></p>"<h2 id="2-2-MyBatis二级缓存的划分"><a href="#2-2-MyBatis二级缓存的划分" class="headerlink" title="2.2 MyBatis二级缓存的划分"></a>2.2 MyBatis二级缓存的划分</h2><p>MyBatis并不是简单地对整个Application就只有一个Cache缓存对象，它将缓存划分的更细，即是Mapper级别的，即每一个Mapper都可以拥有一个Cache对象，具体如下：</p><ol><li><strong>为每一个Mapper分配一个Cache缓存对象（使用<cache>节点配置）</cache></strong></li></ol><p><code>MyBatis将Application级别的二级缓存细分到Mapper级别</code>，即对于每一个Mapper.xml,如果在其中使用了<cache> 节点，则MyBatis会为这个Mapper创建一个Cache缓存对象，如下图所示：</cache></p><p><img src="/images/03120505_vx4q.png" alt="输入图片说明"></p>"<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">注：上述的每一个Cache对象，都会有一个自己所属的namespace命名空间，并且会将Mapper的 namespace作为它们的ID；</span><br></pre></td></tr></table></figure><ol><li><strong>多个Mapper共用一个Cache缓存对象（使用<cache-ref>节点配置）</cache-ref></strong></li></ol><p>如果你想让多个Mapper公用一个Cache的话，你可以使用<cache-ref namespace>节点，来指定你的这个Mapper使用到了哪一个Mapper的Cache缓存。</cache-ref></p><p><img src="/images/03120638_Qwwn.png" alt="输入图片说明"></p>"<h2 id="2-3-使用二级缓存，必须要具备的条件"><a href="#2-3-使用二级缓存，必须要具备的条件" class="headerlink" title="2.3 使用二级缓存，必须要具备的条件"></a>2.3 使用二级缓存，必须要具备的条件</h2><p>MyBatis对二级缓存的支持粒度很细，<code>它会指定某一条查询语句是否使用二级缓存</code>。</p><p>虽然在Mapper中配置了<cache>,并且为此Mapper分配了Cache对象，<code>这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中</code>，我们必须指定Mapper中的某条选择语句是否支持缓存，<code>即如下所示，在&lt;select&gt; 节点中配置useCache=&quot;true&quot;，Mapper才会对此Select的查询支持缓存特性</code>，否则，不会对此Select查询，不会经过Cache缓存。如下所示，Select语句配置了useCache=”true”，则表明这条Select语句的查询会使用二级缓存。</cache></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;select id=&quot;selectByMinSalary&quot; resultMap=&quot;BaseResultMap&quot; parameterType=&quot;java.util.Map&quot; useCache=&quot;true&quot;&gt;</span><br></pre></td></tr></table></figure><p>总之，要想使某条Select查询支持二级缓存，你需要保证：</p><blockquote><ol><li>MyBatis支持二级缓存的总开关：全局配置变量参数 cacheEnabled=true</li><li>该select语句所在的Mapper，配置了<cache> 或<cached-ref>节点，并且有效</cached-ref></cache></li><li>该select语句的参数 useCache=true</li></ol></blockquote><h2 id="2-4-一级缓存和二级缓存的使用顺序"><a href="#2-4-一级缓存和二级缓存的使用顺序" class="headerlink" title="2.4 一级缓存和二级缓存的使用顺序"></a>2.4 一级缓存和二级缓存的使用顺序</h2><p>请注意，如果你的MyBatis使用了二级缓存，并且你的Mapper和select语句也配置使用了二级缓存，那么在执行select查询的时候，MyBatis会先从二级缓存中取输入，其次才是一级缓存，即MyBatis查询数据的顺序是：<code>二级缓存 ———&gt; 一级缓存 ——&gt; 数据库</code>。</p><h2 id="2-5-二级缓存实现的选择"><a href="#2-5-二级缓存实现的选择" class="headerlink" title="2.5 二级缓存实现的选择"></a>2.5 二级缓存实现的选择</h2><p>MyBatis对二级缓存的设计非常灵活，<code>它自己内部实现了一系列的Cache缓存实现类，并提供了各种缓存刷新策略如LRU，FIFO等等</code>；另外，MyBatis还允许用户自定义Cache接口实现，用户是需要实现org.apache.ibatis.cache.Cache接口，然后将Cache实现类配置在<cache type>节点的type属性上即可；除此之外，MyBatis还支持跟第三方内存缓存库如Memecached的集成，总之，使用MyBatis的二级缓存有三个选择:</cache></p><blockquote><ol><li>MyBatis自身提供的缓存实现；</li><li>用户自定义的Cache接口实现；</li><li>跟第三方内存缓存库的集成；</li></ol></blockquote><h2 id="2-6-MyBatis自身提供的二级缓存的实现"><a href="#2-6-MyBatis自身提供的二级缓存的实现" class="headerlink" title="2.6 MyBatis自身提供的二级缓存的实现"></a>2.6 MyBatis自身提供的二级缓存的实现</h2><p>MyBatis自身提供了丰富的，并且功能强大的二级缓存的实现，它拥有一系列的Cache接口装饰者，可以满足各种对缓存操作和更新的策略。</p><p>MyBatis定义了大量的Cache的装饰器来增强Cache缓存的功能，如下类图所示。</p><p>对于每个Cache而言，都有一个容量限制，MyBatis各供了各种策略来对Cache缓存的容量进行控制，以及对Cache中的数据进行刷新和置换。MyBatis主要提供了以下几个刷新和置换策略：</p><blockquote><p>LRU：（Least Recently Used）,最近最少使用算法，即如果缓存中容量已经满了，会将缓存中最近最少被使用的缓存记录清除掉，然后添加新的记录；</p><p>FIFO：（First in first out）,先进先出算法，如果缓存中的容量已经满了，那么会将最先进入缓存中的数据清除掉；</p><p>Scheduled：指定时间间隔清空算法，该算法会以指定的某一个时间间隔将Cache缓存中的数据清空；</p></blockquote><p><img src="/images/03121356_gkcB.png" alt="输入图片说明"></p>"<h1 id="3-如何细粒度地控制你的MyBatis二级缓存"><a href="#3-如何细粒度地控制你的MyBatis二级缓存" class="headerlink" title="3 如何细粒度地控制你的MyBatis二级缓存"></a>3 如何细粒度地控制你的MyBatis二级缓存</h1><h2 id="3-1-一个关于MyBatis的二级缓存的实际问题"><a href="#3-1-一个关于MyBatis的二级缓存的实际问题" class="headerlink" title="3.1 一个关于MyBatis的二级缓存的实际问题"></a>3.1 一个关于MyBatis的二级缓存的实际问题</h2><p>现有AMapper.xml中定义了对数据库表 ATable 的CRUD操作，BMapper定义了对数据库表BTable的CRUD操作；</p><p>假设 MyBatis 的二级缓存开启，并且 AMapper 中使用了二级缓存，AMapper对应的二级缓存为ACache；</p><p>除此之外，AMapper 中还定义了一个跟BTable有关的查询语句，类似如下所述：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectATableWithJoin"</span> <span class="attr">resultMap</span>=<span class="string">"BaseResultMap"</span> <span class="attr">useCache</span>=<span class="string">"true"</span>&gt;</span>  </span><br><span class="line">      select * from ATable left join BTable on ....  </span><br><span class="line"><span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>执行以下操作：</strong></p><ol><li>执行AMapper中的”selectATableWithJoin” 操作，此时会将查询到的结果放置到AMapper对应的二级缓存ACache中；</li><li>执行BMapper中对BTable的更新操作(update、delete、insert)后，BTable的数据更新；</li><li>再执行1完全相同的查询，这时候会直接从AMapper二级缓存ACache中取值，将ACache中的值直接返回；</li></ol><p><strong>好，问题就出现在第3步上：</strong></p><p>由于AMapper的“selectATableWithJoin” 对应的SQL语句需要和BTable进行join查找，而在第 2 步BTable的数据已经更新了，但是第 3 步查询的值是第 1 步的缓存值，已经极有可能跟真实数据库结果不一样，即ACache中缓存数据过期了！</p><p><strong>总结来看，就是：</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">对于某些使用了 join连接的查询，如果其关联的表数据发生了更新，join连接的查询由于先前缓存的原因，导致查询结果和真实数据不同步；</span><br></pre></td></tr></table></figure><p><strong>从MyBatis的角度来看，这个问题可以这样表述：</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">对于某些表执行了更新(update、delete、insert)操作后，如何去清空跟这些表有关联的查询语句所造成的缓存；</span><br></pre></td></tr></table></figure><h2 id="3-2-当前MyBatis二级缓存的工作机制"><a href="#3-2-当前MyBatis二级缓存的工作机制" class="headerlink" title="3.2 当前MyBatis二级缓存的工作机制"></a>3.2 当前MyBatis二级缓存的工作机制</h2><p><img src="/images/03144546_yr9P.png" alt="输入图片说明"></p>"<p>MyBatis二级缓存的一个重要特点：<code>即松散的Cache缓存管理和维护</code></p><p><code>一个Mapper中定义的增删改查操作只能影响到自己关联的Cache对象</code>。如上图所示的Mapper namespace1中定义的若干CRUD语句，产生的缓存只会被放置到相应关联的Cache1中，即Mapper namespace2,namespace3,namespace4 中的CRUD的语句不会影响到Cache1。</p><p><strong><code>可以看出，Mapper之间的缓存关系比较松散，相互关联的程度比较弱。</code></strong></p><p>现在再回到上面描述的问题，<code>如果我们将AMapper和BMapper共用一个Cache对象</code>，那么，当BMapper执行更新操作时，可以清空对应Cache中的所有的缓存数据，这样的话，数据不是也可以保持最新吗？</p><p>确实这个也是一种解决方案，<code>不过，它会使缓存的使用效率变的很低！</code>AMapper和BMapper的任意的更新操作都会将共用的Cache清空，会频繁地清空Cache，导致Cache实际的命中率和使用率就变得很低了，所以这种策略实际情况下是不可取的。</p><p><strong><code>最理想的解决方案就是：</code></strong></p><p><strong>对于某些表执行了更新(update、delete、insert)操作后，如何去清空跟这些表有关联的查询语句所造成的缓存；</strong>这样，就是以很细的粒度管理MyBatis内部的缓存，使得缓存的使用率和准确率都能大大地提升。</p><h2 id="3-3-mybatis-enhanced-cache插件的设计和工作原理"><a href="#3-3-mybatis-enhanced-cache插件的设计和工作原理" class="headerlink" title="3.3 mybatis-enhanced-cache插件的设计和工作原理"></a>3.3 mybatis-enhanced-cache插件的设计和工作原理</h2><p>该插件主要由两个构件组成：<code>EnhancedCachingExecutor和EnhancedCachingManager</code>。源码地址：<a href="https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fgithub.com%2FLuanLouis%2Fmybatis-enhanced-cache" target="_blank" rel="noopener">https://github.com/LuanLouis/mybatis-enhanced-cache</a>。</p><p>EnhancedCachingExecutor是针对于Executor的拦截器，拦截Executor的几个关键的方法；<strong>EnhancedCachingExecutor主要做以下几件事：</strong></p><ol><li>每当有Executor执行query操作时， 1.1 记录下该查询StatementId和CacheKey，然后将其添加到EnhancedCachingManager中； 1.2 记录下该查询StatementId和此StatementId所属Mapper内的Cache缓存对象引用，添加到EnhancedCachingManager中；</li><li>每当Executor执行了update操作时，将此update操作的StatementId传递给EnhancedCachingManager，让EnhancedCachingManager根据此update的StatementId的配置，去清空指定的查询语句所产生的缓存；</li></ol><p><strong>另一个构件：EnhancedCachingManager，它也是本插件的核心，它维护着以下几样东西：</strong></p><ol><li>整个MyBatis的所有查询所产生的CacheKey集合（以statementId分类）；</li><li>所有的使用过了的查询的statementId 及其对应的Cache缓存对象的引用；</li><li>update类型的StatementId和查询StatementId集合的映射，用于当Update类型的语句执行时，根据此映射决定应该清空哪些查询语句产生的缓存；</li></ol><p><strong>如下图所示：</strong></p><p><img src="/images/03145439_ST5b.png" alt="输入图片说明"></p>"<p><strong><code>原理很简单，就是 当执行了某个update操作时，根据配置信息去清空指定的查询语句在Cache中所产生的缓存数据。</code></strong></p><h2 id="3-4-mybatis-enhanced-cache-插件的使用实例"><a href="#3-4-mybatis-enhanced-cache-插件的使用实例" class="headerlink" title="3.4 mybatis-enhanced-cache 插件的使用实例"></a>3.4 mybatis-enhanced-cache 插件的使用实例</h2><ol><li><strong>配置MyBatis配置文件</strong></li></ol><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">plugins</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">plugin</span> <span class="attr">interceptor</span>=<span class="string">"org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor"</span>&gt;</span>  </span><br><span class="line">       <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"dependency"</span> <span class="attr">value</span>=<span class="string">"dependencys.xml"</span>/&gt;</span>  </span><br><span class="line">       <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"cacheEnabled"</span> <span class="attr">value</span>=<span class="string">"true"</span>/&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br></pre></td></tr></table></figure><p>其中，<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。</property></p><ol><li><strong>配置StatementId之间的依赖关系</strong></li></ol><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>  </span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">statements</span>&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">statement</span> <span class="attr">id</span>=<span class="string">"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"</span>&gt;</span>  </span><br><span class="line">            <span class="tag">&lt;<span class="name">observer</span> <span class="attr">id</span>=<span class="string">"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;/<span class="name">statement</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">statements</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><p><statement>节点配置的是更新语句的statementId，其内的子节点<observer> 配置的是当更新语句执行后，应当清空缓存的查询语句的StatementId。子节点<observer>可以有多个。</observer></observer></statement></p><p>如上的配置，则说明，如果”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey” 更新语句执行后，由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。</p><ol><li><strong>配置DepartmentsMapper.xml 和EmployeesMapper.xml</strong></li></ol><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8" ?&gt;</span>  </span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">mapper</span> <span class="meta-keyword">PUBLIC</span> <span class="meta-string">"-//mybatis.org//DTD Mapper 3.0//EN"</span> <span class="meta-string">"http://mybatis.org/dtd/mybatis-3-mapper.dtd"</span> &gt;</span>  </span><br><span class="line"><span class="tag">&lt;<span class="name">mapper</span> <span class="attr">namespace</span>=<span class="string">"com.louis.mybatis.dao.DepartmentsMapper"</span> &gt;</span>     </span><br><span class="line">    <span class="tag">&lt;<span class="name">cache</span>&gt;</span><span class="tag">&lt;/<span class="name">cache</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">resultMap</span> <span class="attr">id</span>=<span class="string">"BaseResultMap"</span> <span class="attr">type</span>=<span class="string">"com.louis.mybatis.model.Department"</span> &gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">id</span> <span class="attr">column</span>=<span class="string">"DEPARTMENT_ID"</span> <span class="attr">property</span>=<span class="string">"departmentId"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"DEPARTMENT_NAME"</span> <span class="attr">property</span>=<span class="string">"departmentName"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"MANAGER_ID"</span> <span class="attr">property</span>=<span class="string">"managerId"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"LOCATION_ID"</span> <span class="attr">property</span>=<span class="string">"locationId"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> /&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">resultMap</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">sql</span> <span class="attr">id</span>=<span class="string">"Base_Column_List"</span> &gt;</span>  </span><br><span class="line">        DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">sql</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">update</span> <span class="attr">id</span>=<span class="string">"updateByPrimaryKey"</span> <span class="attr">parameterType</span>=<span class="string">"com.louis.mybatis.model.Department"</span> &gt;</span>  </span><br><span class="line">        update HR.DEPARTMENTS  </span><br><span class="line">        set DEPARTMENT_NAME = #&#123;departmentName,jdbcType=VARCHAR&#125;,  </span><br><span class="line">        MANAGER_ID = #&#123;managerId,jdbcType=DECIMAL&#125;,  </span><br><span class="line">        LOCATION_ID = #&#123;locationId,jdbcType=DECIMAL&#125;  </span><br><span class="line">        where DEPARTMENT_ID = #&#123;departmentId,jdbcType=DECIMAL&#125;  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">update</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectByPrimaryKey"</span> <span class="attr">resultMap</span>=<span class="string">"BaseResultMap"</span> <span class="attr">parameterType</span>=<span class="string">"java.lang.Integer"</span> &gt;</span>  </span><br><span class="line">        select   </span><br><span class="line">        <span class="tag">&lt;<span class="name">include</span> <span class="attr">refid</span>=<span class="string">"Base_Column_List"</span> /&gt;</span>  </span><br><span class="line">        from HR.DEPARTMENTS  </span><br><span class="line">        where DEPARTMENT_ID = #&#123;departmentId,jdbcType=DECIMAL&#125;  </span><br><span class="line">    <span class="tag">&lt;/<span class="name">select</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;/<span class="name">mapper</span>&gt;</span> </span><br><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>  </span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">mapper</span> <span class="meta-keyword">PUBLIC</span> <span class="meta-string">"-//mybatis.org//DTD Mapper 3.0//EN"</span> <span class="meta-string">"http://mybatis.org/dtd/mybatis-3-mapper.dtd"</span>&gt;</span>  </span><br><span class="line"><span class="tag">&lt;<span class="name">mapper</span> <span class="attr">namespace</span>=<span class="string">"com.louis.mybatis.dao.EmployeesMapper"</span>&gt;</span>  </span><br><span class="line">    <span class="tag">&lt;<span class="name">cache</span> <span class="attr">eviction</span>=<span class="string">"LRU"</span> <span class="attr">flushInterval</span>=<span class="string">"100000"</span> <span class="attr">size</span>=<span class="string">"10000"</span>/&gt;</span>   </span><br><span class="line">    <span class="tag">&lt;<span class="name">resultMap</span> <span class="attr">id</span>=<span class="string">"BaseResultMap"</span> <span class="attr">type</span>=<span class="string">"com.louis.mybatis.model.Employee"</span>&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">id</span> <span class="attr">column</span>=<span class="string">"EMPLOYEE_ID"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> <span class="attr">property</span>=<span class="string">"employeeId"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"FIRST_NAME"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> <span class="attr">property</span>=<span class="string">"firstName"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"LAST_NAME"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> <span class="attr">property</span>=<span class="string">"lastName"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"EMAIL"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> <span class="attr">property</span>=<span class="string">"email"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"PHONE_NUMBER"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> <span class="attr">property</span>=<span class="string">"phoneNumber"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"HIRE_DATE"</span> <span class="attr">jdbcType</span>=<span class="string">"DATE"</span> <span class="attr">property</span>=<span class="string">"hireDate"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"JOB_ID"</span> <span class="attr">jdbcType</span>=<span class="string">"VARCHAR"</span> <span class="attr">property</span>=<span class="string">"jobId"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"SALARY"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> <span class="attr">property</span>=<span class="string">"salary"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"COMMISSION_PCT"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> <span class="attr">property</span>=<span class="string">"commissionPct"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"MANAGER_ID"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> <span class="attr">property</span>=<span class="string">"managerId"</span> /&gt;</span>  </span><br><span class="line">        <span class="tag">&lt;<span class="name">result</span> <span class="attr">column</span>=<span class="string">"DEPARTMENT_ID"</span> <span class="attr">jdbcType</span>=<span class="string">"DECIMAL"</span> <span class="attr">property</span>=<span class="string">"departmentId"</span> /&gt;</span>  </span><br><span class="line">     <span class="tag">&lt;/<span class="name">resultMap</span>&gt;</span>   </span><br><span class="line">     <span class="tag">&lt;<span class="name">sql</span> <span class="attr">id</span>=<span class="string">"Base_Column_List"</span>&gt;</span>  </span><br><span class="line">        EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,   </span><br><span class="line">        COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID  </span><br><span class="line">     <span class="tag">&lt;/<span class="name">sql</span>&gt;</span>    </span><br><span class="line">     <span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectWithDepartments"</span> <span class="attr">parameterType</span>=<span class="string">"java.lang.Integer"</span> <span class="attr">resultMap</span>=<span class="string">"BaseResultMap"</span> <span class="attr">useCache</span>=<span class="string">"true"</span> &gt;</span>  </span><br><span class="line">        select   </span><br><span class="line">        *  </span><br><span class="line">        from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID  </span><br><span class="line">        where EMPLOYEE_ID = #&#123;employeeId,jdbcType=DECIMAL&#125;  </span><br><span class="line">     <span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">mapper</span>&gt;</span></span><br></pre></td></tr></table></figure><ol><li><strong>测试代码：</strong></li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SelectDemo3</span> </span>&#123;  </span><br><span class="line">   <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger loger = Logger.getLogger(SelectDemo3<span class="class">.<span class="keyword">class</span>)</span>;  </span><br><span class="line">   <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;  </span><br><span class="line">       InputStream inputStream = Resources.getResourceAsStream(<span class="string">"mybatisConfig.xml"</span>);  </span><br><span class="line">       SqlSessionFactoryBuilder builder = <span class="keyword">new</span> SqlSessionFactoryBuilder();  </span><br><span class="line">       SqlSessionFactory factory = builder.build(inputStream);  </span><br><span class="line">         </span><br><span class="line">       SqlSession sqlSession = factory.openSession(<span class="keyword">true</span>);  </span><br><span class="line">       SqlSession sqlSession2 = factory.openSession(<span class="keyword">true</span>);  </span><br><span class="line">       <span class="comment">//3.使用SqlSession查询  </span></span><br><span class="line">       Map&lt;String,Object&gt; params = <span class="keyword">new</span> HashMap&lt;String,Object&gt;();  </span><br><span class="line">       params.put(<span class="string">"employeeId"</span>,<span class="number">10</span>);  </span><br><span class="line">       <span class="comment">//a.查询工资低于10000的员工  </span></span><br><span class="line">       Date first = <span class="keyword">new</span> Date();  </span><br><span class="line">       <span class="comment">//第一次查询  </span></span><br><span class="line">       List&lt;Employee&gt; result = sqlSession.selectList(<span class="string">"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"</span>,params);  </span><br><span class="line">       sqlSession.commit();  </span><br><span class="line">       checkCacheStatus(sqlSession);  </span><br><span class="line">       params.put(<span class="string">"employeeId"</span>, <span class="number">11</span>);  </span><br><span class="line">       result = sqlSession.selectList(<span class="string">"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"</span>,params);  </span><br><span class="line">       sqlSession.commit();  </span><br><span class="line">       checkCacheStatus(sqlSession);  </span><br><span class="line">       params.put(<span class="string">"employeeId"</span>, <span class="number">12</span>);  </span><br><span class="line">       result = sqlSession.selectList(<span class="string">"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"</span>,params);  </span><br><span class="line">       sqlSession.commit();  </span><br><span class="line">       checkCacheStatus(sqlSession);  </span><br><span class="line">       params.put(<span class="string">"employeeId"</span>, <span class="number">13</span>);  </span><br><span class="line">       result = sqlSession.selectList(<span class="string">"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"</span>,params);  </span><br><span class="line">       sqlSession.commit();  </span><br><span class="line">       checkCacheStatus(sqlSession);  </span><br><span class="line">       Department department = sqlSession.selectOne(<span class="string">"com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey"</span>,<span class="number">10</span>);  </span><br><span class="line">       department.setDepartmentName(<span class="string">"updated"</span>);  </span><br><span class="line">       sqlSession2.update(<span class="string">"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"</span>, department);  </span><br><span class="line">       sqlSession.commit();  </span><br><span class="line">       checkCacheStatus(sqlSession);  </span><br><span class="line">   &#125;      </span><br><span class="line">   <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">checkCacheStatus</span><span class="params">(SqlSession sqlSession)</span>  </span></span><br><span class="line"><span class="function">   </span>&#123;  </span><br><span class="line">       loger.info(<span class="string">"------------Cache Status------------"</span>);  </span><br><span class="line">       Iterator&lt;String&gt; iter = sqlSession.getConfiguration().getCacheNames().iterator();  </span><br><span class="line">       <span class="keyword">while</span>(iter.hasNext())  </span><br><span class="line">       &#123;  </span><br><span class="line">           String it = iter.next();  </span><br><span class="line">           loger.info(it+<span class="string">":"</span>+sqlSession.getConfiguration().getCache(it).getSize());  </span><br><span class="line">       &#125;  </span><br><span class="line">       loger.info(<span class="string">"------------------------------------"</span>);     </span><br><span class="line">   &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>结果分析：</strong></p><p>从上述的结果可以看出，前四次执行了“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句，EmployeesMapper对应的Cache缓存中存储的结果缓存有1个增加到4个。</p><p>当执行了”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey”后，EmployeeMapper对应的缓存Cache结果被清空了，即”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey”更新语句引起了EmployeeMapper中的”com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”缓存的清空。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;1-MyBatis一级缓存实现&quot;&gt;&lt;a href=&quot;#1-MyBatis一级缓存实现&quot; class=&quot;headerlink&quot; title=&quot;1 MyBatis一级缓存实现&quot;&gt;&lt;/a&gt;1 MyBatis一级缓存实现&lt;/h1&gt;&lt;h2 id=&quot;1-1-什么是一级缓存？&quot;
      
    
    </summary>
    
      <category term="mybatis" scheme="http://yoursite.com/categories/mybatis/"/>
    
    
      <category term="mybatis" scheme="http://yoursite.com/tags/mybatis/"/>
    
  </entry>
  
  <entry>
    <title>mybatis--缓存原理分析1</title>
    <link href="http://yoursite.com/2021/04/15/mybatis/mybatis--%E7%BC%93%E5%AD%98%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%901/"/>
    <id>http://yoursite.com/2021/04/15/mybatis/mybatis--缓存原理分析1/</id>
    <published>2021-04-15T08:19:23.746Z</published>
    <updated>2021-04-19T11:32:30.936Z</updated>
    
    <content type="html"><![CDATA[<h1 id="1-缓存介绍"><a href="#1-缓存介绍" class="headerlink" title="1 缓存介绍"></a>1 缓存介绍</h1><p> MyBatis支持声明式数据缓存（declarative data caching）。当一条SQL语句被标记为“可缓存”后，首次执行它时从数据库获取的所有数据会被存储在一段高速缓存中，今后执行这条语句时就会从高速缓存中读取结果，而不是再次命中数据库。MyBatis提供了默认下基于Java HashMap的缓存实现，以及用于与OSCache、Ehcache、Hazelcast和Memcached连接的默认连接器。MyBatis还提供API供其他缓存实现使用。</p><p><code>重点的那句话就是：MyBatis执行SQL语句之后，这条语句就是被缓存，以后再执行这条语句的时候，会直接从缓存中拿结果，而不是再次执行SQL</code>。</p><p>这也就是大家常说的MyBatis一级缓存，<code>一级缓存的作用域scope是SqlSession</code>。MyBatis同时还提供了<code>一种全局作用域global scope的缓存，这也叫做二级缓存</code>，也称作全局缓存。</p><p><strong>MyBatis将数据缓存设计成两级结构，分为一级缓存、二级缓存：</strong></p><p><code>一级缓存是Session会话级别的缓存，位于表示一次数据库会话的SqlSession对象之中，又被称之为本地缓存</code>。一级缓存是MyBatis内部实现的一个特性，用户不能配置，<code>默认情况下自动支持的缓存</code>，用户没有定制它的权利（<code>不过这也不是绝对的，可以通过开发插件对它进行修改</code>）；</p><p><code>二级缓存是Application应用级别的缓存</code>，它的是<code>生命周期很长</code>，跟Application的声明周期一样，也就是说<code>它的作用范围是整个Application应用</code>。</p><p><strong>MyBatis中一级缓存和二级缓存的组织如下图所示：</strong></p><p><img src="/images/03143018_c6Sb.png" alt="输入图片说明"></p>"<h1 id="2-一级缓存"><a href="#2-一级缓存" class="headerlink" title="2 一级缓存"></a>2 一级缓存</h1><p> <strong>一级缓存的工作机制：</strong></p><p><code>一级缓存是Session会话级别的</code>，一般而言，一个SqlSession对象会使用一个Executor对象来完成会话操作，<code>Executor对象会维护一个Cache缓存</code>，以提高查询性能。关于一级缓存的详细实现，可参见<a href="http://my.oschina.net/xianggao/blog/591482#OSC_h1_43" target="_blank" rel="noopener">MyBatis一级缓存实现</a>。</p><h2 id="2-1-缓存测试"><a href="#2-1-缓存测试" class="headerlink" title="2.1 缓存测试"></a>2.1 缓存测试</h2><p>同个session进行两次相同查询：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        User user = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        User user2 = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis只进行1次数据库查询：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: <span class="number">1</span>(Integer)</span><br><span class="line">&lt;==      Total: <span class="number">1</span></span><br><span class="line">User&#123;id=<span class="number">1</span>, name=<span class="string">'format'</span>, age=<span class="number">23</span>, birthday=Sun Oct <span class="number">12</span> <span class="number">23</span>:<span class="number">20</span>:<span class="number">13</span> CST <span class="number">2014</span>&#125;</span><br><span class="line">User&#123;id=<span class="number">1</span>, name=<span class="string">'format'</span>, age=<span class="number">23</span>, birthday=Sun Oct <span class="number">12</span> <span class="number">23</span>:<span class="number">20</span>:<span class="number">13</span> CST <span class="number">2014</span>&#125;</span><br></pre></td></tr></table></figure><p>同个session进行两次不同的查询：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        User user = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        User user2 = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">2</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis进行两次数据库查询：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 2(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=2, name=&apos;FFF&apos;, age=50, birthday=Sat Dec 06 17:12:01 CST 2014&#125;</span><br></pre></td></tr></table></figure><p>不同session，进行相同查询：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    SqlSession sqlSession2 = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        User user = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        User user2 = (User)sqlSession2.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">        sqlSession2.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis进行了两次数据库查询：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br></pre></td></tr></table></figure><p>同个session,查询之后更新数据，再次查询相同的语句：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        User user = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        user.setAge(<span class="number">100</span>);</span><br><span class="line">        sqlSession.update(<span class="string">"org.format.mybatis.cache.UserMapper.update"</span>, user);</span><br><span class="line">        User user2 = (User)sqlSession.selectOne(<span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">        sqlSession.commit();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>更新操作之后缓存会被清除：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">==&gt;  Preparing: update USERS SET NAME = ? , AGE = ? , BIRTHDAY = ? where ID = ?</span><br><span class="line">==&gt; Parameters: format(String), 23(Integer), 2014-10-12 23:20:13.0(Timestamp), 1(Integer)</span><br><span class="line">&lt;==    Updates: 1</span><br><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br></pre></td></tr></table></figure><p>很明显，结果验证了一级缓存的概念，<strong><code>在同个SqlSession中，查询语句相同的sql会被缓存，但是一旦执行新增或更新或删除操作，缓存就会被清除</code></strong>。</p><h2 id="2-2-源码分析"><a href="#2-2-源码分析" class="headerlink" title="2.2 源码分析"></a>2.2 源码分析</h2><p>在分析MyBatis的一级缓存之前，我们先简单看下MyBatis中几个重要的类和接口：</p><blockquote><p>org.apache.ibatis.session.Configuration类：MyBatis全局配置信息类<br>org.apache.ibatis.session.SqlSessionFactory接口：操作SqlSession的工厂接口，具体的实现类是DefaultSqlSessionFactory<br>org.apache.ibatis.session.SqlSession接口：执行sql，管理事务的接口，具体的实现类是DefaultSqlSession<br>org.apache.ibatis.executor.Executor接口：sql执行器，SqlSession执行sql最终是通过该接口实现的，常用的实现类有SimpleExecutor和CachingExecutor,这些实现类都使用了装饰者设计模式</p></blockquote><p><code>一级缓存的作用域是SqlSession</code>，那么我们就先看一下SqlSession的select过程：</p><ol><li>这是DefaultSqlSession（SqlSession接口实现类，MyBatis默认使用这个类）的selectList源码（我们例子上使用的是selectOne方法，调用selectOne方法最终会执行selectList方法）：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;E&gt; <span class="function">List&lt;E&gt; <span class="title">selectList</span><span class="params">(String statement, Object parameter, RowBounds rowBounds)</span> </span>&#123;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     MappedStatement ms = configuration.getMappedStatement(statement);</span><br><span class="line">     List&lt;E&gt; result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);</span><br><span class="line">     <span class="keyword">return</span> result;</span><br><span class="line">   &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">     <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">"Error querying database.  Cause: "</span> + e, e);</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     ErrorContext.instance().reset();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>我们看到SqlSession最终会调用Executor接口的方法。接下来我们看下DefaultSqlSession中的executor接口属性具体是哪个实现类。DefaultSqlSession的构造过程（DefaultSqlSessionFactory内部）：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> SqlSession <span class="title">openSessionFromDataSource</span><span class="params">(ExecutorType execType, TransactionIsolationLevel level, <span class="keyword">boolean</span> autoCommit)</span> </span>&#123;</span><br><span class="line">   Transaction tx = <span class="keyword">null</span>;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     <span class="keyword">final</span> Environment environment = configuration.getEnvironment();</span><br><span class="line">     <span class="keyword">final</span> TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);</span><br><span class="line">     tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);</span><br><span class="line">     <span class="keyword">final</span> Executor executor = configuration.newExecutor(tx, execType, autoCommit);</span><br><span class="line">     <span class="keyword">return</span> <span class="keyword">new</span> DefaultSqlSession(configuration, executor);</span><br><span class="line">   &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">     closeTransaction(tx); <span class="comment">// may have fetched a connection so lets call close()</span></span><br><span class="line">     <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">"Error opening session.  Cause: "</span> + e, e);</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     ErrorContext.instance().reset();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>我们看到DefaultSqlSessionFactory构造DefaultSqlSession的时候，Executor接口的实现类是由Configuration构造的：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Executor <span class="title">newExecutor</span><span class="params">(Transaction transaction, ExecutorType executorType, <span class="keyword">boolean</span> autoCommit)</span> </span>&#123;</span><br><span class="line">   executorType = executorType == <span class="keyword">null</span> ? defaultExecutorType : executorType;</span><br><span class="line">   executorType = executorType == <span class="keyword">null</span> ? ExecutorType.SIMPLE : executorType;</span><br><span class="line">   Executor executor;</span><br><span class="line">   <span class="keyword">if</span> (ExecutorType.BATCH == executorType) &#123;</span><br><span class="line">     executor = <span class="keyword">new</span> BatchExecutor(<span class="keyword">this</span>, transaction);</span><br><span class="line">   &#125; <span class="keyword">else</span> <span class="keyword">if</span> (ExecutorType.REUSE == executorType) &#123;</span><br><span class="line">     executor = <span class="keyword">new</span> ReuseExecutor(<span class="keyword">this</span>, transaction);</span><br><span class="line">   &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">     executor = <span class="keyword">new</span> SimpleExecutor(<span class="keyword">this</span>, transaction);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> (cacheEnabled) &#123;</span><br><span class="line">     executor = <span class="keyword">new</span> CachingExecutor(executor, autoCommit);</span><br><span class="line">   &#125;</span><br><span class="line">   executor = (Executor) interceptorChain.pluginAll(executor);</span><br><span class="line">   <span class="keyword">return</span> executor;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Executor根据ExecutorType的不同而创建，最常用的是SimpleExecutor，本文的例子也是创建这个实现类。 最后我们发现如果cacheEnabled这个属性为true的话，那么executor会被包一层装饰器，这个装饰器是 CachingExecutor。其中cacheEnabled这个属性是mybatis总配置文件中settings节点中cacheEnabled子节点的值，默认就是true，也就是说我们在mybatis总配置文件中不配cacheEnabled的话，它也是默认为打开的。</p><ol start="4"><li>现在，问题就剩下一个了，CachingExecutor执行sql的时候到底做了什么？带着这个问题，我们继续走下去（CachingExecutor的query方法）：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;E&gt; <span class="function">List&lt;E&gt; <span class="title">query</span><span class="params">(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   Cache cache = ms.getCache();</span><br><span class="line">   <span class="keyword">if</span> (cache != <span class="keyword">null</span>) &#123;</span><br><span class="line">     flushCacheIfRequired(ms);</span><br><span class="line">     <span class="keyword">if</span> (ms.isUseCache() &amp;&amp; resultHandler == <span class="keyword">null</span>) &#123;</span><br><span class="line">       ensureNoOutParams(ms, parameterObject, boundSql);</span><br><span class="line">       <span class="keyword">if</span> (!dirty) &#123;</span><br><span class="line">         cache.getReadWriteLock().readLock().lock();</span><br><span class="line">         <span class="keyword">try</span> &#123;</span><br><span class="line">           <span class="meta">@SuppressWarnings</span>(<span class="string">"unchecked"</span>)</span><br><span class="line">           List&lt;E&gt; cachedList = (List&lt;E&gt;) cache.getObject(key);</span><br><span class="line">           <span class="keyword">if</span> (cachedList != <span class="keyword">null</span>) <span class="keyword">return</span> cachedList;</span><br><span class="line">         &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">           cache.getReadWriteLock().readLock().unlock();</span><br><span class="line">         &#125;</span><br><span class="line">       &#125;</span><br><span class="line">       List&lt;E&gt; list = delegate.&lt;E&gt; query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);</span><br><span class="line">       tcm.putObject(cache, key, list); <span class="comment">// issue #578. Query must be not synchronized to prevent deadlocks</span></span><br><span class="line">       <span class="keyword">return</span> list;</span><br><span class="line">     &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> delegate.&lt;E&gt;query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中Cache cache = ms.getCache();这句代码中，<code>这个cache实际上就是个二级缓存</code>，由于我们没有开启二级缓存(二级缓存的内容下面会分析)，因此这里执行了最后一句话。这里的delegate也就是SimpleExecutor,<code>SimpleExecutor没有Override父类的query方法，因此最终执行了SimpleExecutor的父类BaseExecutor的query方法</code>。</p><ol start="5"><li>所以一级缓存最重要的代码就是BaseExecutor的query方法!</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;E&gt; <span class="function">List&lt;E&gt; <span class="title">query</span><span class="params">(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   ErrorContext.instance().resource(ms.getResource()).activity(<span class="string">"executing a query"</span>).object(ms.getId());</span><br><span class="line">   <span class="keyword">if</span> (closed) <span class="keyword">throw</span> <span class="keyword">new</span> ExecutorException(<span class="string">"Executor was closed."</span>);</span><br><span class="line">   <span class="keyword">if</span> (queryStack == <span class="number">0</span> &amp;&amp; ms.isFlushCacheRequired()) &#123;</span><br><span class="line">     clearLocalCache();</span><br><span class="line">   &#125;</span><br><span class="line">   List&lt;E&gt; list;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     queryStack++;</span><br><span class="line">     list = resultHandler == <span class="keyword">null</span> ? (List&lt;E&gt;) localCache.getObject(key) : <span class="keyword">null</span>;</span><br><span class="line">     <span class="keyword">if</span> (list != <span class="keyword">null</span>) &#123;</span><br><span class="line">       handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);</span><br><span class="line">     &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">       list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);</span><br><span class="line">     &#125;</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     queryStack--;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">if</span> (queryStack == <span class="number">0</span>) &#123;</span><br><span class="line">     <span class="keyword">for</span> (DeferredLoad deferredLoad : deferredLoads) &#123;</span><br><span class="line">       deferredLoad.load();</span><br><span class="line">     &#125;</span><br><span class="line">     deferredLoads.clear(); <span class="comment">// issue #601</span></span><br><span class="line">     <span class="keyword">if</span> (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) &#123;</span><br><span class="line">       clearLocalCache(); <span class="comment">// issue #482</span></span><br><span class="line">     &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> list;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>BaseExecutor的属性localCache是个PerpetualCache类型的实例</code>，PerpetualCache 类是实现了MyBatis的Cache缓存接口的实现类之一，内部有个Map 类型的属性用来存储缓存数据。 <code>这个localCache的类型在BaseExecutor内部是写死的</code>。 这个localCache就是一级缓存！</p><ol start="6"><li>接下来我们看下为何执行新增或更新或删除操作，一级缓存就会被清除这个问题。首先MyBatis处理新增或删除的时候，最终都是调用update方法，也就是说新增或者删除操作在MyBatis眼里都是一个更新操作。我们看下DefaultSqlSession的update方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">update</span><span class="params">(String statement, Object parameter)</span> </span>&#123;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     dirty = <span class="keyword">true</span>;</span><br><span class="line">     MappedStatement ms = configuration.getMappedStatement(statement);</span><br><span class="line">     <span class="keyword">return</span> executor.update(ms, wrapCollection(parameter));</span><br><span class="line">   &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">     <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">"Error updating database.  Cause: "</span> + e, e);</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     ErrorContext.instance().reset();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很明显，这里调用了CachingExecutor的update方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">update</span><span class="params">(MappedStatement ms, Object parameterObject)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   flushCacheIfRequired(ms);</span><br><span class="line">   <span class="keyword">return</span> delegate.update(ms, parameterObject);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的<code>flushCacheIfRequired方法清除的是二级缓存</code>，我们之后会分析。 CachingExecutor委托给了(之前已经分析过)SimpleExecutor的update方法，SimpleExecutor没有 Override父类BaseExecutor的update方法，因此我们看BaseExecutor的update方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">update</span><span class="params">(MappedStatement ms, Object parameter)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   ErrorContext.instance().resource(ms.getResource()).activity(<span class="string">"executing an update"</span>).object(ms.getId());</span><br><span class="line">   <span class="keyword">if</span> (closed) <span class="keyword">throw</span> <span class="keyword">new</span> ExecutorException(<span class="string">"Executor was closed."</span>);</span><br><span class="line">   clearLocalCache();</span><br><span class="line">   <span class="keyword">return</span> doUpdate(ms, parameter);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>我们看到了关键的一句代码： clearLocalCache(); 进去看看：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">clearLocalCache</span><span class="params">()</span> </span>&#123;</span><br><span class="line">   <span class="keyword">if</span> (!closed) &#123;</span><br><span class="line">     localCache.clear();</span><br><span class="line">     localOutputParameterCache.clear();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>没错，就是这条，<code>sqlsession没有关闭的话，进行新增、删除、修改操作的话就是清除一级缓存，也就是SqlSession的缓存</code>。</p><h1 id="3-二级缓存"><a href="#3-二级缓存" class="headerlink" title="3 二级缓存"></a>3 二级缓存</h1><p>二级缓存的作用域是全局，换句话说，二级缓存已经脱离SqlSession的控制了。<code>二级缓存的作用域是全局的，二级缓存在SqlSession关闭或提交之后才会生效</code>。</p><p>在分析MyBatis的二级缓存之前，我们先简单看下MyBatis中一个关于二级缓存的类(其他相关的类和接口之前已经分析过)：</p><blockquote><p>org.apache.ibatis.mapping.MappedStatement：</p><p>MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点，即一个<select>、<update>或者<insert>标签。Mybatis框架在初始化阶段会对XML配置文件进行读取，将其中的sql语句节点对象化为一个个MappedStatement对象。</insert></update></select></p></blockquote><p><strong>二级缓存的工作机制：</strong></p><p>一个SqlSession对象会使用一个Executor对象来完成会话操作，<code>MyBatis的二级缓存机制的关键就是对这个Executor对象做文章</code>。如果用户配置了”cacheEnabled=true”，那么MyBatis在为SqlSession对象创建Executor对象时，会对Executor对象加上一个装饰者：<code>CachingExecutor，这时SqlSession使用CachingExecutor对象来完成操作请求</code>。CachingExecutor对于查询请求，会先判断该查询请求在Application级别的二级缓存中是否有缓存结果，如果有查询结果，则直接返回缓存结果；如果缓存中没有，再交给真正的Executor对象来完成查询操作，之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中，然后在返回给用户。</p><p>MyBatis的二级缓存设计得比较灵活，<code>你可以使用MyBatis自己定义的二级缓存实现</code>；你也可以<code>通过实现org.apache.ibatis.cache.Cache接口自定义缓存</code>；也可以<code>使用第三方内存缓存库</code>，如Memcached等。</p><p><img src="/images/03143543_Fujp.png" alt="输入图片说明"></p>"<p><img src="/images/03143643_VQTT.png" alt="输入图片说明"></p>"<h2 id="3-1-缓存配置"><a href="#3-1-缓存配置" class="headerlink" title="3.1 缓存配置"></a>3.1 缓存配置</h2><p> 二级缓存跟一级缓存不同，<code>一级缓存不需要配置任何东西，且默认打开</code>。 二级缓存就需要配置一些东西。本文就说下最简单的配置，在mapper文件上加上这句配置即可。其实二级缓存跟3个配置有关：</p><blockquote><ol><li>mybatis全局配置文件中的setting中的cacheEnabled需要为true(默认为true，不设置也行)</li><li>mapper配置文件中需要加入<cache>节点</cache></li><li>mapper配置文件中的select节点需要加上属性useCache需要为true(默认为true，不设置也行)</li></ol></blockquote><h2 id="3-2-缓存测试"><a href="#3-2-缓存测试" class="headerlink" title="3.2 缓存测试"></a>3.2 缓存测试</h2><p> 不同SqlSession，查询相同语句，第一次查询之后commit SqlSession：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testCache2</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    SqlSession sqlSession2 = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        String sql = <span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>;</span><br><span class="line">        User user = (User)sqlSession.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        <span class="comment">// 注意，这里一定要提交。 不提交还是会查询两次数据库</span></span><br><span class="line">        sqlSession.commit();</span><br><span class="line">        User user2 = (User)sqlSession2.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">        sqlSession2.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis仅进行了一次数据库查询：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br></pre></td></tr></table></figure><p>不同SqlSession，查询相同语句，第一次查询之后close SqlSession：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testCache2</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    SqlSession sqlSession2 = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        String sql = <span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>;</span><br><span class="line">        User user = (User)sqlSession.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        sqlSession.close();</span><br><span class="line">        User user2 = (User)sqlSession2.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession2.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis仅进行了一次数据库查询：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br></pre></td></tr></table></figure><p>不同SqlSesson，查询相同语句。 第一次查询之后SqlSession不提交：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testCache2</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    SqlSession sqlSession = sqlSessionFactory.openSession();</span><br><span class="line">    SqlSession sqlSession2 = sqlSessionFactory.openSession();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        String sql = <span class="string">"org.format.mybatis.cache.UserMapper.getById"</span>;</span><br><span class="line">        User user = (User)sqlSession.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user);</span><br><span class="line">        User user2 = (User)sqlSession2.selectOne(sql, <span class="number">1</span>);</span><br><span class="line">        log.debug(user2);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        sqlSession.close();</span><br><span class="line">        sqlSession2.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>MyBatis执行了两次数据库查询：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br><span class="line">==&gt;  Preparing: select * from USERS WHERE ID = ?</span><br><span class="line">==&gt; Parameters: 1(Integer)</span><br><span class="line">&lt;==      Total: 1</span><br><span class="line">User&#123;id=1, name=&apos;format&apos;, age=23, birthday=Sun Oct 12 23:20:13 CST 2014&#125;</span><br></pre></td></tr></table></figure><h2 id="3-3-源码分析"><a href="#3-3-源码分析" class="headerlink" title="3.3 源码分析"></a>3.3 源码分析</h2><ol><li>XMLMappedBuilder（解析每个mapper配置文件的解析类，每一个mapper配置都会实例化一个XMLMapperBuilder类）的解析方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">configurationElement</span><span class="params">(XNode context)</span> </span>&#123;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     String namespace = context.getStringAttribute(<span class="string">"namespace"</span>);</span><br><span class="line">     <span class="keyword">if</span> (namespace.equals(<span class="string">""</span>)) &#123;</span><br><span class="line">         <span class="keyword">throw</span> <span class="keyword">new</span> BuilderException(<span class="string">"Mapper's namespace cannot be empty"</span>);</span><br><span class="line">     &#125;</span><br><span class="line">     builderAssistant.setCurrentNamespace(namespace);</span><br><span class="line">     cacheRefElement(context.evalNode(<span class="string">"cache-ref"</span>));</span><br><span class="line">     cacheElement(context.evalNode(<span class="string">"cache"</span>));</span><br><span class="line">     parameterMapElement(context.evalNodes(<span class="string">"/mapper/parameterMap"</span>));</span><br><span class="line">     resultMapElements(context.evalNodes(<span class="string">"/mapper/resultMap"</span>));</span><br><span class="line">     sqlElement(context.evalNodes(<span class="string">"/mapper/sql"</span>));</span><br><span class="line">     buildStatementFromContext(context.evalNodes(<span class="string">"select|insert|update|delete"</span>));</span><br><span class="line">   &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">     <span class="keyword">throw</span> <span class="keyword">new</span> BuilderException(<span class="string">"Error parsing Mapper XML. Cause: "</span> + e, e);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>我们看到了解析cache的那段代码：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">cacheElement</span><span class="params">(XNode context)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">   <span class="keyword">if</span> (context != <span class="keyword">null</span>) &#123;</span><br><span class="line">     String type = context.getStringAttribute(<span class="string">"type"</span>, <span class="string">"PERPETUAL"</span>);</span><br><span class="line">     Class&lt;? extends Cache&gt; typeClass = typeAliasRegistry.resolveAlias(type);</span><br><span class="line">     String eviction = context.getStringAttribute(<span class="string">"eviction"</span>, <span class="string">"LRU"</span>);</span><br><span class="line">     Class&lt;? extends Cache&gt; evictionClass = typeAliasRegistry.resolveAlias(eviction);</span><br><span class="line">     Long flushInterval = context.getLongAttribute(<span class="string">"flushInterval"</span>);</span><br><span class="line">     Integer size = context.getIntAttribute(<span class="string">"size"</span>);</span><br><span class="line">     <span class="keyword">boolean</span> readWrite = !context.getBooleanAttribute(<span class="string">"readOnly"</span>, <span class="keyword">false</span>);</span><br><span class="line">     Properties props = context.getChildrenAsProperties();</span><br><span class="line">     builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>解析完cache标签之后会<code>使用builderAssistant的userNewCache方法</code>，这里的builderAssistant是一个MapperBuilderAssistant类型的帮助类，每个XMLMappedBuilder构造的时候都会实例化这个属性，<code>MapperBuilderAssistant类内部有个Cache类型的currentCache属性</code>，这个属性也就是mapper配置文件中 cache节点所代表的值：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Cache <span class="title">useNewCache</span><span class="params">(Class&lt;? extends Cache&gt; typeClass,</span></span></span><br><span class="line"><span class="function"><span class="params">    Class&lt;? extends Cache&gt; evictionClass,</span></span></span><br><span class="line"><span class="function"><span class="params">    Long flushInterval,</span></span></span><br><span class="line"><span class="function"><span class="params">    Integer size,</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">boolean</span> readWrite,</span></span></span><br><span class="line"><span class="function"><span class="params">    Properties props)</span> </span>&#123;</span><br><span class="line">        typeClass = valueOrDefault(typeClass, PerpetualCache<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">        evictionClass = valueOrDefault(evictionClass, LruCache<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">        Cache cache = <span class="keyword">new</span> CacheBuilder(currentNamespace)</span><br><span class="line">       .implementation(typeClass)</span><br><span class="line">       .addDecorator(evictionClass)</span><br><span class="line">       .clearInterval(flushInterval)</span><br><span class="line">       .size(size)</span><br><span class="line">       .readWrite(readWrite)</span><br><span class="line">       .properties(props)</span><br><span class="line">       .build();</span><br><span class="line">   configuration.addCache(cache);</span><br><span class="line">   currentCache = cache;</span><br><span class="line">   <span class="keyword">return</span> cache;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>OK，现在mapper配置文件中的cache节点被解析到了<code>XMLMapperBuilder实例中的builderAssistant属性中的currentCache值里</code>。</p><ol start="3"><li>接下来XMLMapperBuilder会解析select节点，解析select节点的时候使用XMLStatementBuilder进行解析(也包括其他insert，update，delete节点)：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">parseStatementNode</span><span class="params">()</span> </span>&#123;</span><br><span class="line">   String id = context.getStringAttribute(<span class="string">"id"</span>);</span><br><span class="line">   String databaseId = context.getStringAttribute(<span class="string">"databaseId"</span>);</span><br><span class="line"></span><br><span class="line">   <span class="keyword">if</span> (!databaseIdMatchesCurrent(id, databaseId, <span class="keyword">this</span>.requiredDatabaseId)) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">   Integer fetchSize = context.getIntAttribute(<span class="string">"fetchSize"</span>);</span><br><span class="line">   Integer timeout = context.getIntAttribute(<span class="string">"timeout"</span>);</span><br><span class="line">   String parameterMap = context.getStringAttribute(<span class="string">"parameterMap"</span>);</span><br><span class="line">   String parameterType = context.getStringAttribute(<span class="string">"parameterType"</span>);</span><br><span class="line">   Class&lt;?&gt; parameterTypeClass = resolveClass(parameterType);</span><br><span class="line">   String resultMap = context.getStringAttribute(<span class="string">"resultMap"</span>);</span><br><span class="line">   String resultType = context.getStringAttribute(<span class="string">"resultType"</span>);</span><br><span class="line">   String lang = context.getStringAttribute(<span class="string">"lang"</span>);</span><br><span class="line">   LanguageDriver langDriver = getLanguageDriver(lang);</span><br><span class="line"></span><br><span class="line">   Class&lt;?&gt; resultTypeClass = resolveClass(resultType);</span><br><span class="line">   String resultSetType = context.getStringAttribute(<span class="string">"resultSetType"</span>);</span><br><span class="line">   StatementType statementType = StatementType.valueOf(context.getStringAttribute(<span class="string">"statementType"</span>, StatementType.PREPARED.toString()));</span><br><span class="line">   ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);</span><br><span class="line"></span><br><span class="line">   String nodeName = context.getNode().getNodeName();</span><br><span class="line">   SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));</span><br><span class="line">   <span class="keyword">boolean</span> isSelect = sqlCommandType == SqlCommandType.SELECT;</span><br><span class="line">   <span class="keyword">boolean</span> flushCache = context.getBooleanAttribute(<span class="string">"flushCache"</span>, !isSelect);</span><br><span class="line">   <span class="keyword">boolean</span> useCache = context.getBooleanAttribute(<span class="string">"useCache"</span>, isSelect);</span><br><span class="line">   <span class="keyword">boolean</span> resultOrdered = context.getBooleanAttribute(<span class="string">"resultOrdered"</span>, <span class="keyword">false</span>);</span><br><span class="line"></span><br><span class="line">   <span class="comment">// Include Fragments before parsing</span></span><br><span class="line">   XMLIncludeTransformer includeParser = <span class="keyword">new</span> XMLIncludeTransformer(configuration, builderAssistant);</span><br><span class="line">   includeParser.applyIncludes(context.getNode());</span><br><span class="line"></span><br><span class="line">   <span class="comment">// Parse selectKey after includes and remove them.</span></span><br><span class="line">   processSelectKeyNodes(id, parameterTypeClass, langDriver);</span><br><span class="line"></span><br><span class="line">   <span class="comment">// Parse the SQL (pre: &lt;selectKey&gt; and &lt;include&gt; were parsed and removed)</span></span><br><span class="line">   SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);</span><br><span class="line">   String resultSets = context.getStringAttribute(<span class="string">"resultSets"</span>);</span><br><span class="line">   String keyProperty = context.getStringAttribute(<span class="string">"keyProperty"</span>);</span><br><span class="line">   String keyColumn = context.getStringAttribute(<span class="string">"keyColumn"</span>);</span><br><span class="line">   KeyGenerator keyGenerator;</span><br><span class="line">   String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;</span><br><span class="line">   keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, <span class="keyword">true</span>);</span><br><span class="line">   <span class="keyword">if</span> (configuration.hasKeyGenerator(keyStatementId)) &#123;</span><br><span class="line">     keyGenerator = configuration.getKeyGenerator(keyStatementId);</span><br><span class="line">   &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">     keyGenerator = context.getBooleanAttribute(<span class="string">"useGeneratedKeys"</span>,</span><br><span class="line">         configuration.isUseGeneratedKeys() &amp;&amp; SqlCommandType.INSERT.equals(sqlCommandType))</span><br><span class="line">         ? <span class="keyword">new</span> Jdbc3KeyGenerator() : <span class="keyword">new</span> NoKeyGenerator();</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,</span><br><span class="line">       fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,</span><br><span class="line">       resultSetTypeEnum, flushCache, useCache, resultOrdered,</span><br><span class="line">       keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码前面都是解析一些标签的属性，我们看到了最后一行使用builderAssistant添加MappedStatement，其中builderAssistant属性是构造XMLStatementBuilder的时候通过XMLMappedBuilder传入的，我们继续看builderAssistant的addMappedStatement方法：</p><p><img src="/images/28151344_ntIp.png" alt="输入图片说明"></p>"<ol><li>进入setStatementCache：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setStatementCache</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">boolean</span> isSelect,</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">boolean</span> flushCache,</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">boolean</span> useCache,</span></span></span><br><span class="line"><span class="function"><span class="params">    Cache cache,</span></span></span><br><span class="line"><span class="function"><span class="params">    MappedStatement.Builder statementBuilder)</span> </span>&#123;</span><br><span class="line">        flushCache = valueOrDefault(flushCache, !isSelect);</span><br><span class="line">        useCache = valueOrDefault(useCache, isSelect);</span><br><span class="line">        statementBuilder.flushCacheRequired(flushCache);</span><br><span class="line">        statementBuilder.useCache(useCache);</span><br><span class="line">        statementBuilder.cache(cache);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终mapper配置文件中的<cache>被设置到了XMLMapperBuilder的builderAssistant属性中，XMLMapperBuilder中使用XMLStatementBuilder遍历CRUD节点，<code>遍历CRUD节点的时候将这个cache节点设置到这些CRUD节点中</code>，这个cache就是所谓的二级缓存！</cache></p><ol start="4"><li>接下来我们回过头来看查询的源码，CachingExecutor的query方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;E&gt; <span class="function">List&lt;E&gt; <span class="title">query</span><span class="params">(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)</span></span></span><br><span class="line"><span class="function">     <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   Cache cache = ms.getCache();</span><br><span class="line">   <span class="keyword">if</span> (cache != <span class="keyword">null</span>) &#123;</span><br><span class="line">     flushCacheIfRequired(ms);</span><br><span class="line">     <span class="keyword">if</span> (ms.isUseCache() &amp;&amp; resultHandler == <span class="keyword">null</span>) &#123;</span><br><span class="line">       ensureNoOutParams(ms, parameterObject, boundSql);</span><br><span class="line">       <span class="meta">@SuppressWarnings</span>(<span class="string">"unchecked"</span>)</span><br><span class="line">       List&lt;E&gt; list = (List&lt;E&gt;) tcm.getObject(cache, key);</span><br><span class="line">       <span class="keyword">if</span> (list == <span class="keyword">null</span>) &#123;</span><br><span class="line">         list = delegate.&lt;E&gt; query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);</span><br><span class="line">         tcm.putObject(cache, key, list); <span class="comment">// issue #578. Query must be not synchronized to prevent deadlocks</span></span><br><span class="line">       &#125;</span><br><span class="line">       <span class="keyword">return</span> list;</span><br><span class="line">     &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> delegate.&lt;E&gt; query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="5"><li>进入TransactionalCacheManager的putObject方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">putObject</span><span class="params">(Cache cache, CacheKey key, Object value)</span> </span>&#123;</span><br><span class="line">   getTransactionalCache(cache).putObject(key, value);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> TransactionalCache <span class="title">getTransactionalCache</span><span class="params">(Cache cache)</span> </span>&#123;</span><br><span class="line">   TransactionalCache txCache = transactionalCaches.get(cache);</span><br><span class="line">   <span class="keyword">if</span> (txCache == <span class="keyword">null</span>) &#123;</span><br><span class="line">     txCache = <span class="keyword">new</span> TransactionalCache(cache);</span><br><span class="line">     transactionalCaches.put(cache, txCache);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> txCache;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>TransactionalCache的putObject方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">putObject</span><span class="params">(Object key, Object object)</span> </span>&#123;</span><br><span class="line">   entriesToRemoveOnCommit.remove(key);</span><br><span class="line">   entriesToAddOnCommit.put(key, <span class="keyword">new</span> AddEntry(delegate, key, object));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们看到，数据被加入到了entriesToAddOnCommit中，这个entriesToAddOnCommit是什么东西呢，它是TransactionalCache的一个Map属性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> Map&lt;Object, AddEntry&gt; entriesToAddOnCommit;</span><br></pre></td></tr></table></figure><p>AddEntry是TransactionalCache内部的一个类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">AddEntry</span> </span>&#123;</span><br><span class="line">   <span class="keyword">private</span> Cache cache;</span><br><span class="line">   <span class="keyword">private</span> Object key;</span><br><span class="line">   <span class="keyword">private</span> Object value;</span><br><span class="line"></span><br><span class="line">   <span class="function"><span class="keyword">public</span> <span class="title">AddEntry</span><span class="params">(Cache cache, Object key, Object value)</span> </span>&#123;</span><br><span class="line">     <span class="keyword">this</span>.cache = cache;</span><br><span class="line">     <span class="keyword">this</span>.key = key;</span><br><span class="line">     <span class="keyword">this</span>.value = value;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">()</span> </span>&#123;</span><br><span class="line">     cache.putObject(key, value);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>好了，现在我们发现使用二级缓存之后：<code>查询数据的话，先从二级缓存中拿数据，如果没有的话，去一级缓存中拿，一级缓存也没有的话再查询数据库</code>。有了数据之后在<code>丢到TransactionalCache这个对象的entriesToAddOnCommit属性中</code>。</p><p><strong>接下来我们来验证为什么SqlSession commit或close之后，二级缓存才会生效这个问题。</strong></p><ol><li>DefaultSqlSession的commit方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">(<span class="keyword">boolean</span> force)</span> </span>&#123;</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     executor.commit(isCommitOrRollbackRequired(force));</span><br><span class="line">     dirty = <span class="keyword">false</span>;</span><br><span class="line">   &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">     <span class="keyword">throw</span> ExceptionFactory.wrapException(<span class="string">"Error committing transaction.  Cause: "</span> + e, e);</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     ErrorContext.instance().reset();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>CachingExecutor的commit方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">(<span class="keyword">boolean</span> required)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">   delegate.commit(required);</span><br><span class="line">   tcm.commit();</span><br><span class="line">   dirty = <span class="keyword">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>tcm.commit即 TransactionalCacheManager的commit方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">()</span> </span>&#123;</span><br><span class="line">   <span class="keyword">for</span> (TransactionalCache txCache : transactionalCaches.values()) &#123;</span><br><span class="line">     txCache.commit();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="4"><li>TransactionalCache的commit方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">()</span> </span>&#123;</span><br><span class="line">   delegate.getReadWriteLock().writeLock().lock();</span><br><span class="line">   <span class="keyword">try</span> &#123;</span><br><span class="line">     <span class="keyword">if</span> (clearOnCommit) &#123;</span><br><span class="line">       delegate.clear();</span><br><span class="line">     &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">       <span class="keyword">for</span> (RemoveEntry entry : entriesToRemoveOnCommit.values()) &#123;</span><br><span class="line">         entry.commit();</span><br><span class="line">       &#125;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">for</span> (AddEntry entry : entriesToAddOnCommit.values()) &#123;</span><br><span class="line">       entry.commit();</span><br><span class="line">     &#125;</span><br><span class="line">     reset();</span><br><span class="line">   &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">     delegate.getReadWriteLock().writeLock().unlock();</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="5"><li>发现调用了AddEntry的commit方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">commit</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    cache.putObject(key, value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>发现了！ <code>AddEntry的commit方法会把数据丢到cache中，也就是丢到二级缓存中</code>！</p><p>关于为何调用close方法后，二级缓存才会生效，<code>因为close方法内部会调用commit方法</code>。本文就不具体说了。 读者有兴趣的话看一看源码就知道为什么了。</p><h1 id="4-Cache接口"><a href="#4-Cache接口" class="headerlink" title="4 Cache接口"></a>4 Cache接口</h1><p>org.apache.ibatis.cache.Cache是MyBatis的缓存接口，想要实现自定义的缓存需要实现这个接口。<code>MyBatis中关于Cache接口的实现类也使用了装饰者设计模式</code>。我们看下它的一些实现类：</p><p><img src="/images/28154528_8UNe.jpg" alt="输入图片说明"></p>"<p><strong>简单说明：</strong></p><blockquote><p>LRU – 最近最少使用的:移除最长时间不被使用的对象。</p><p>FIFO – 先进先出:按对象进入缓存的顺序来移除它们。</p><p>SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。</p><p>WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。</p></blockquote><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">cache</span></span></span><br><span class="line">  eviction="FIFO" &lt;!-- 可以通过cache节点的eviction属性设置，也可以设置其他的属性。--&gt;</span><br><span class="line">  flushInterval="60000"</span><br><span class="line">  size="512"</span><br><span class="line">  readOnly="true"/&gt;</span><br></pre></td></tr></table></figure><p><strong>cache-ref节点：</strong>mapper配置文件中还可以加入cache-ref节点，它有个属性namespace。如果每个mapper文件都是用cache-ref，且namespace都一样，那么就代表着真正意义上的全局缓存。如果只用了cache节点，那仅代表这个这个mapper内部的查询被缓存了，其他mapper文件的不起作用，这并不是所谓的全局缓存。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://my.oschina.net/xianggao/blog/552272" target="_blank" rel="noopener">【深入浅出MyBatis系列十一】缓存源码分析</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;1-缓存介绍&quot;&gt;&lt;a href=&quot;#1-缓存介绍&quot; class=&quot;headerlink&quot; title=&quot;1 缓存介绍&quot;&gt;&lt;/a&gt;1 缓存介绍&lt;/h1&gt;&lt;p&gt; MyBatis支持声明式数据缓存（declarative data caching）。当一条SQL语句被标
      
    
    </summary>
    
      <category term="mybatis" scheme="http://yoursite.com/categories/mybatis/"/>
    
    
      <category term="mybatis" scheme="http://yoursite.com/tags/mybatis/"/>
    
  </entry>
  
  <entry>
    <title>spring--ioc生命周期</title>
    <link href="http://yoursite.com/2021/04/11/spring%E6%BA%90%E7%A0%81/spring--ioc%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/"/>
    <id>http://yoursite.com/2021/04/11/spring源码/spring--ioc生命周期/</id>
    <published>2021-04-11T11:36:34.592Z</published>
    <updated>2021-04-11T13:32:27.225Z</updated>
    
    <content type="html"><![CDATA[<p>Spring 有很多特性，支撑这些特性的是优良的设计思想，IOC（DI）就是其中最典型的控制反转思想，或者叫依赖注入。本系列文章追踪了容器的初始化、以及获取bean的过程。下面总结了几个核心流程图。</p><h2 id="1-spring容器中Bean生命周期"><a href="#1-spring容器中Bean生命周期" class="headerlink" title="1.spring容器中Bean生命周期"></a>1.spring容器中Bean生命周期</h2><p><img src="/images/584866-20171026101746488-698711766.png" alt="img"></p>"<h2 id="2-IOC容器中核心接口"><a href="#2-IOC容器中核心接口" class="headerlink" title="2.IOC容器中核心接口"></a>2.IOC容器中核心接口</h2><p>　　Spring Ioc容器的核心是BeanFactory和BeanDefinition。分别对应对象工厂和依赖配置的概念。虽然我们通常使用的是ApplicationContext的实现类，但ApplicationContext只是封装和扩展了BeanFactory的功能。XML的配置形式只是Spring依赖注入的一种常用形式而已，而AnnotationConfigApplicationContext配合Annotation注解和泛型，早已经提供了更简易的配置方式，AnnotationConfigApplicationContext和AnnotationConfigWebApplicationContext则是实现无XML配置的核心接口，但无论你使用任何配置，最后都会映射到BeanDefinition。</p><p>​    其次，这里特别要注意的还是BeanDefinition， Bean在XML文件里面的展现形式是<bean id="…">…</bean>，当这个节点被加载到内存中，就被抽象为BeanDefinition了，在XML Bean节点中的那些关键字，在BeanDefinition中都有相对应的成员变量。如何把一个XML节点转换成BeanDefinition，这个工作自然是由BeanDefinitionReader来完成的。Spring通过定义BeanDefinition来管理基于Spring的应用中的各种对象以及它们之间的相互依赖关系。BeanDefinition抽象了我们对Bean的定义，是让容器起作用的主要数据类型。我们知道在计算机世界里，所有的功能都是建立在通过数据对现实进行抽象的基础上的。Ioc容器是用BeanDefinition来管理对象依赖关系的，对Ioc容器而言，BeanDefinition就是对控制反转模式中管理的对象依赖关系的数据抽象，也是容器实现控制反转的核心数据结构，有了他们容器才能发挥作用。</p><h2 id="3-IOC容器启动流程"><a href="#3-IOC容器启动流程" class="headerlink" title="3.IOC容器启动流程"></a>3.IOC容器启动流程</h2><p>不管通过何种渠道定义的bean，xml配置还是注解,最终容器启动时，都会调用AbstractAplicationContext的Refesh()方法，流程图整理如下，可对应第一节中的Bean生命周期图，先beanFactory初始化、后置处理器，再bean后处理器，实例化bean。</p><p><img src="/images/584866-20171026112344519-377170380.png" alt="img"></p>"<h2 id="4-IOC依赖注入流程"><a href="#4-IOC依赖注入流程" class="headerlink" title="4.IOC依赖注入流程"></a>4.IOC依赖注入流程</h2><p>这里的依赖注入就是获取bean时依赖注入相关属性（或者其他bean）。主要是AbstractBeanFactory和AbstractAutoWireCapableBeanFactory.</p><p>主要分两步：</p><p>1.bean实例化和初始化</p><p>2.根据实例化bean获取bean对象。流程图如下：</p><p><img src="/images/584866-20171026153731945-1167568767.png" alt="img"></p>"<p>值得一提的是bean初始化方法：AbstractAutoWireCapableBeanFactory.initializeBean中封装了</p><p>1.Aware接口方法：<br>BeanNameAware：setBeanName<br>ResourceLoaderAware：setBeanClassLoader<br>BeanFactoryAware：setBeanFactory<br>2.BeanPostProcessors： postProcessBeforeInitialization初始化前置处理器<br>3.InitializingBean：afterPropertiesSet<br>4.init-method自定义初始化方法<br>5.BeanPostProcessors ：PostProcessorsAfterInitialization初始化后置处理器</p><p>这些接口方法全部在bean的生命周期中可以查到。</p><p>我们可以对比Bean生命周期图、容器启动流程图、IOC依赖注入流程图，可以发现这三个图，相互印证，互相依存。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p> <a href="https://www.cnblogs.com/dennyzhangdd/p/7730050.html" target="_blank" rel="noopener">Spring IOC（四）总结升华篇</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Spring 有很多特性，支撑这些特性的是优良的设计思想，IOC（DI）就是其中最典型的控制反转思想，或者叫依赖注入。本系列文章追踪了容器的初始化、以及获取bean的过程。下面总结了几个核心流程图。&lt;/p&gt;
&lt;h2 id=&quot;1-spring容器中Bean生命周期&quot;&gt;&lt;a h
      
    
    </summary>
    
      <category term="spring源码分析" scheme="http://yoursite.com/categories/spring%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    
    
      <category term="spring" scheme="http://yoursite.com/tags/spring/"/>
    
      <category term="ioc" scheme="http://yoursite.com/tags/ioc/"/>
    
  </entry>
  
  <entry>
    <title>spring容器--refresh方法</title>
    <link href="http://yoursite.com/2021/04/09/spring%E6%BA%90%E7%A0%81/spring%E5%AE%B9%E5%99%A8--refresh%E5%88%86%E6%9E%90/"/>
    <id>http://yoursite.com/2021/04/09/spring源码/spring容器--refresh分析/</id>
    <published>2021-04-09T07:40:15.001Z</published>
    <updated>2021-04-11T01:43:20.880Z</updated>
    
    <content type="html"><![CDATA[<p>Spring容器创建之后，会调用它的refresh方法刷新Spring应用的上下文。</p><p>首先整体查看AbstractApplicationContext#refresh源码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">refresh</span><span class="params">()</span> <span class="keyword">throws</span> BeansException, IllegalStateException </span>&#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (<span class="keyword">this</span>.startupShutdownMonitor) &#123;</span><br><span class="line">        <span class="comment">//刷新前的预处理;</span></span><br><span class="line">        prepareRefresh();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取BeanFactory；默认实现是DefaultListableBeanFactory，在创建容器的时候创建的</span></span><br><span class="line">        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//BeanFactory的预准备工作（BeanFactory进行一些设置，比如context的类加载器，BeanPostProcessor和XXXAware自动装配等）</span></span><br><span class="line">        prepareBeanFactory(beanFactory);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//BeanFactory准备工作完成后进行的后置处理工作</span></span><br><span class="line">            postProcessBeanFactory(beanFactory);</span><br><span class="line"></span><br><span class="line">            <span class="comment">//执行BeanFactoryPostProcessor的方法；</span></span><br><span class="line">            invokeBeanFactoryPostProcessors(beanFactory);</span><br><span class="line"></span><br><span class="line">            <span class="comment">//注册BeanPostProcessor（Bean的后置处理器），在创建bean的前后等执行</span></span><br><span class="line">            registerBeanPostProcessors(beanFactory);</span><br><span class="line"></span><br><span class="line">            <span class="comment">//初始化MessageSource组件（做国际化功能；消息绑定，消息解析）；</span></span><br><span class="line">            initMessageSource();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//初始化事件派发器</span></span><br><span class="line">            initApplicationEventMulticaster();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//子类重写这个方法，在容器刷新的时候可以自定义逻辑；如创建Tomcat，Jetty等WEB服务器</span></span><br><span class="line">            onRefresh();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//注册应用的监听器。就是注册实现了ApplicationListener接口的监听器bean，这些监听器是注册到ApplicationEventMulticaster中的</span></span><br><span class="line">            registerListeners();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//初始化所有剩下的非懒加载的单例bean</span></span><br><span class="line">            finishBeanFactoryInitialization(beanFactory);</span><br><span class="line"></span><br><span class="line">            <span class="comment">//完成context的刷新。主要是调用LifecycleProcessor的onRefresh()方法，并且发布事件（ContextRefreshedEvent）</span></span><br><span class="line">            finishRefresh();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="prepareRefresh方法"><a href="#prepareRefresh方法" class="headerlink" title="prepareRefresh方法"></a>prepareRefresh方法</h1><p>表示在真正做refresh操作之前需要准备做的事情：</p><ul><li>设置Spring容器的启动时间，</li><li>开启活跃状态，撤销关闭状态，。</li><li>初始化context environment（上下文环境）中的占位符属性来源。</li><li>验证环境信息里一些必须存在的属性</li></ul><h1 id="ConfigurableListableBeanFactory-beanFactory-obtainFreshBeanFactory"><a href="#ConfigurableListableBeanFactory-beanFactory-obtainFreshBeanFactory" class="headerlink" title="ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory()"></a>ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory()</h1><p>让这个类（AbstractApplicationContext）的子类刷新内部bean工厂。</p><ul><li>AbstractRefreshableApplicationContext容器：实际上就是重新创建一个bean工厂，并设置工厂的一些属性。</li><li>GenericApplicationContext容器：获取创建容器的就创建的bean工厂，并且设置工厂的ID.</li></ul><h1 id="prepareBeanFactory方法"><a href="#prepareBeanFactory方法" class="headerlink" title="prepareBeanFactory方法"></a>prepareBeanFactory方法</h1><p>上一步已经把工厂建好了，但是还不能投入使用，因为工厂里什么都没有，还需要配置一些东西。看看这个方法的注释</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Configure the factory's standard context characteristics,</span></span><br><span class="line"><span class="comment"> * such as the context's ClassLoader and post-processors.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> beanFactory the BeanFactory to configure</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>他说配置这个工厂的标准环境，比如context的类加载器和post-processors后处理器。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">prepareBeanFactory</span><span class="params">(ConfigurableListableBeanFactory beanFactory)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//设置BeanFactory的类加载器</span></span><br><span class="line">    beanFactory.setBeanClassLoader(getClassLoader());</span><br><span class="line">    <span class="comment">//设置支持表达式解析器</span></span><br><span class="line">    beanFactory.setBeanExpressionResolver(<span class="keyword">new</span> StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));</span><br><span class="line">    beanFactory.addPropertyEditorRegistrar(<span class="keyword">new</span> ResourceEditorRegistrar(<span class="keyword">this</span>, getEnvironment()));</span><br><span class="line"></span><br><span class="line">    <span class="comment">//添加部分BeanPostProcessor【ApplicationContextAwareProcessor】</span></span><br><span class="line">    beanFactory.addBeanPostProcessor(<span class="keyword">new</span> ApplicationContextAwareProcessor(<span class="keyword">this</span>));</span><br><span class="line">    <span class="comment">//设置忽略的自动装配的接口EnvironmentAware、EmbeddedValueResolverAware、xx,因为ApplicationContextAwareProcessor#invokeAwareInterfaces已经把这5个接口的实现工作做了</span></span><br><span class="line">    beanFactory.ignoreDependencyInterface(EnvironmentAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    beanFactory.ignoreDependencyInterface(ResourceLoaderAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    beanFactory.ignoreDependencyInterface(MessageSourceAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    beanFactory.ignoreDependencyInterface(ApplicationContextAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//注册可以解析的自动装配；我们能直接在任何组件中自动注入：BeanFactory、ResourceLoader、ApplicationEventPublisher、ApplicationContext</span></span><br><span class="line">    <span class="comment">//其他组件中可以通过 @autowired 直接注册使用</span></span><br><span class="line">    beanFactory.registerResolvableDependency(BeanFactory<span class="class">.<span class="keyword">class</span>, <span class="title">beanFactory</span>)</span>;</span><br><span class="line">    beanFactory.registerResolvableDependency(ResourceLoader<span class="class">.<span class="keyword">class</span>, <span class="title">this</span>)</span>;</span><br><span class="line">    beanFactory.registerResolvableDependency(ApplicationEventPublisher<span class="class">.<span class="keyword">class</span>, <span class="title">this</span>)</span>;</span><br><span class="line">    beanFactory.registerResolvableDependency(ApplicationContext<span class="class">.<span class="keyword">class</span>, <span class="title">this</span>)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//添加BeanPostProcessor【ApplicationListenerDetector】后置处理器，在bean初始化前后的一些工作</span></span><br><span class="line">    beanFactory.addBeanPostProcessor(<span class="keyword">new</span> ApplicationListenerDetector(<span class="keyword">this</span>));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Detect a LoadTimeWeaver and prepare for weaving, if found.</span></span><br><span class="line">    <span class="keyword">if</span> (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) &#123;</span><br><span class="line">        beanFactory.addBeanPostProcessor(<span class="keyword">new</span> LoadTimeWeaverAwareProcessor(beanFactory));</span><br><span class="line">        <span class="comment">// Set a temporary ClassLoader for type matching.</span></span><br><span class="line">        beanFactory.setTempClassLoader(<span class="keyword">new</span> ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//给BeanFactory中注册一些能用的组件；</span></span><br><span class="line">    <span class="keyword">if</span> (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) &#123;</span><br><span class="line">        <span class="comment">//环境信息ConfigurableEnvironment</span></span><br><span class="line">        beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) &#123;</span><br><span class="line">      <span class="comment">//系统属性，systemProperties【Map&lt;String, Object&gt;】</span></span><br><span class="line">        beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) &#123;</span><br><span class="line">      <span class="comment">//系统环境变量systemEnvironment【Map&lt;String, Object&gt;】</span></span><br><span class="line">        beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="postProcessBeanFactory方法"><a href="#postProcessBeanFactory方法" class="headerlink" title="postProcessBeanFactory方法"></a>postProcessBeanFactory方法</h1><p>上面对bean工厂进行了许多配置，现在需要对bean工厂进行一些处理。不同的Spring容器做不同的操作。比如GenericWebApplicationContext容器的操作会在BeanFactory中添加ServletContextAwareProcessor用于处理ServletContextAware类型的bean初始化的时候调用setServletContext或者setServletConfig方法(跟ApplicationContextAwareProcessor原理一样)。</p><p>GenericWebApplicationContext#postProcessBeanFactory源码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">postProcessBeanFactory</span><span class="params">(ConfigurableListableBeanFactory beanFactory)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>.servletContext != <span class="keyword">null</span>) &#123;</span><br><span class="line">        beanFactory.addBeanPostProcessor(<span class="keyword">new</span> ServletContextAwareProcessor(<span class="keyword">this</span>.servletContext));</span><br><span class="line">        beanFactory.ignoreDependencyInterface(ServletContextAware<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, <span class="keyword">this</span>.servletContext);</span><br><span class="line">    WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, <span class="keyword">this</span>.servletContext);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>AnnotationConfigServletWebServerApplicationContext#postProcessBeanFactory方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">postProcessBeanFactory</span><span class="params">(ConfigurableListableBeanFactory beanFactory)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.postProcessBeanFactory(beanFactory);</span><br><span class="line">    <span class="comment">// 查看basePackages属性，如果设置了会使用ClassPathBeanDefinitionScanner去扫描basePackages包下的bean并注册</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>.basePackages != <span class="keyword">null</span> &amp;&amp; <span class="keyword">this</span>.basePackages.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">this</span>.scanner.scan(<span class="keyword">this</span>.basePackages);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 查看annotatedClasses属性，如果设置了会使用AnnotatedBeanDefinitionReader去注册这些bean</span></span><br><span class="line">    <span class="keyword">if</span> (!<span class="keyword">this</span>.annotatedClasses.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">this</span>.reader.register(ClassUtils.toClassArray(<span class="keyword">this</span>.annotatedClasses));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="invokeBeanFactoryPostProcessors方法"><a href="#invokeBeanFactoryPostProcessors方法" class="headerlink" title="invokeBeanFactoryPostProcessors方法"></a>invokeBeanFactoryPostProcessors方法</h1><p>先介绍两个接口：</p><ul><li><code>BeanFactoryPostProcessor</code>：用来修改Spring容器中已经存在的bean的定义，使用ConfigurableListableBeanFactory对bean进行处理</li><li><code>BeanDefinitionRegistryPostProcessor</code>：继承BeanFactoryPostProcessor，作用跟BeanFactoryPostProcessor一样，只不过是使用BeanDefinitionRegistry对bean进行处理</li></ul><p>在Spring容器中找出实现了BeanFactoryPostProcessor接口的processor并执行。Spring容器会委托给PostProcessorRegistrationDelegate的invokeBeanFactoryPostProcessors方法执行。</p><p>注:</p><ol><li>在springboot的web程序初始化AnnotationConfigServletWebServerApplicationContext容器时，会初始化内部属性AnnotatedBeanDefinitionReader reader，这个reader构造的时候会在BeanFactory中注册一些post processor，包括BeanPostProcessor和BeanFactoryPostProcessor(比如ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor)：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">AnnotationConfigUtils.registerAnnotationConfigProcessors(<span class="keyword">this</span>.registry);</span><br></pre></td></tr></table></figure><ol start="2"><li>在使用mybatis时，一般配置了MapperScannerConfigurer的bean，这个bean就是继承的BeanDefinitionRegistryPostProcessor，所以也是这个地方把扫描的mybatis的接口注册到容器中的。</li></ol><p>invokeBeanFactoryPostProcessors方法处理BeanFactoryPostProcessor的逻辑如下：</p><p>从Spring容器中找出BeanDefinitionRegistryPostProcessor类型的bean(这些processor是在容器刚创建的时候通过构造AnnotatedBeanDefinitionReader的时候注册到容器中的)，然后按照优先级分别执行，优先级的逻辑如下：</p><ol><li>实现PriorityOrdered接口的BeanDefinitionRegistryPostProcessor先全部找出来，然后排序后依次执行</li><li>实现Ordered接口的BeanDefinitionRegistryPostProcessor找出来，然后排序后依次执行</li><li>没有实现PriorityOrdered和Ordered接口的BeanDefinitionRegistryPostProcessor找出来执行并依次执行</li></ol><p>接下来从Spring容器内查找BeanFactoryPostProcessor接口的实现类，然后执行(如果processor已经执行过，则忽略)，这里的查找规则跟上面查找BeanDefinitionRegistryPostProcessor一样，先找PriorityOrdered，然后是Ordered，最后是两者都没。</p><p>​    这里需要说明的是ConfigurationClassPostProcessor这个processor是优先级最高的被执行的processor(实现了PriorityOrdered接口)。这个ConfigurationClassPostProcessor会去BeanFactory中找出所有有@Configuration注解的bean，然后使用ConfigurationClassParser去解析这个类。ConfigurationClassParser内部有个Map&lt;ConfigurationClass, ConfigurationClass&gt;类型的configurationClasses属性用于保存解析的类，ConfigurationClass是一个对要解析的配置类的封装，内部存储了配置类的注解信息、被<code>@Bean</code>注解修饰的方法、<code>@ImportResource</code>注解修饰的信息、<code>ImportBeanDefinitionRegistrar</code>等都存储在这个封装类中。</p><p>​    这里ConfigurationClassPostProcessor最先被处理还有另外一个原因是如果程序中有自定义的BeanFactoryPostProcessor，那么这个PostProcessor首先得通过ConfigurationClassPostProcessor被解析出来，然后才能被Spring容器找到并执行。(ConfigurationClassPostProcessor不先执行的话，这个Processor是不会被解析的，不会被解析的话也就不会执行了)。</p><ol><li>处理@PropertySources注解：进行一些配置信息的解析</li><li>处理@ComponentScan注解：使用ComponentScanAnnotationParser扫描basePackage下的需要解析的类(@SpringBootApplication注解也包括了@ComponentScan注解，只不过basePackages是空的，空的话会去获取当前@Configuration修饰的类所在的包)，并注册到BeanFactory中(这个时候bean并没有进行实例化，而是进行了注册。具体的实例化在finishBeanFactoryInitialization方法中执行)。对于扫描出来的类，递归解析</li><li>处理@Import注解：先递归找出所有的注解，然后再过滤出只有@Import注解的类，得到@Import注解的值。比如查找@SpringBootApplication注解的@Import注解数据的话，首先发现@SpringBootApplication不是一个@Import注解，然后递归调用修饰了@SpringBootApplication的注解，发现有个@EnableAutoConfiguration注解，再次递归发现被@Import(EnableAutoConfigurationImportSelector.class)修饰，还有@AutoConfigurationPackage注解修饰，再次递归@AutoConfigurationPackage注解，发现被@Import(AutoConfigurationPackages.Registrar.class)注解修饰，所以@SpringBootApplication注解对应的@Import注解有2个，分别是@Import(AutoConfigurationPackages.Registrar.class)和@Import(EnableAutoConfigurationImportSelector.class)。找出所有的@Import注解之后，开始处理逻辑：<ol><li>遍历这些@Import注解内部的属性类集合</li><li>如果这个类是个ImportSelector接口的实现类，实例化这个ImportSelector，如果这个类也是DeferredImportSelector接口的实现类，那么加入ConfigurationClassParser的deferredImportSelectors属性中让第6步处理。否则调用ImportSelector的selectImports方法得到需要Import的类，然后对这些类递归做@Import注解的处理</li><li>如果这个类是ImportBeanDefinitionRegistrar接口的实现类，设置到配置类的importBeanDefinitionRegistrars属性中</li><li>其它情况下把这个类入队到ConfigurationClassParser的importStack(队列)属性中，然后把这个类当成是@Configuration注解修饰的类递归重头开始解析这个类</li></ol></li><li>处理@ImportResource注解：获取@ImportResource注解的locations属性，得到资源文件的地址信息。然后遍历这些资源文件并把它们添加到配置类的importedResources属性中</li><li>处理@Bean注解：获取被@Bean注解修饰的方法，然后添加到配置类的beanMethods属性中</li><li>处理DeferredImportSelector：处理第3步@Import注解产生的DeferredImportSelector，进行selectImports方法的调用找出需要import的类，然后再调用第3步相同的处理逻辑处理</li></ol><p>​    这里@SpringBootApplication注解被@EnableAutoConfiguration修饰，@EnableAutoConfiguration注解被@Import(EnableAutoConfigurationImportSelector.class)修饰，所以在第3步会找出这个@Import修饰的类EnableAutoConfigurationImportSelector，这个类刚好实现了DeferredImportSelector接口，接着就会在第6步被执行。第6步selectImport得到的类就是自动化配置类。</p><p>​    <strong>EnableAutoConfigurationImportSelector的selectImport方法会在spring-boot-autoconfigure包的META-INF里面的spring.factories文件中找出key为org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的值，有109个，这109个就是所谓的自动化配置类(XXXAutoConfiguration)。（如果引入了mybatis和pagehelper，也会在对应的XXXautoconfigure包的META-INF里面的spring.factories找到EnableAutoConfiguration，这样可能最后得到的自动配置类会大于109个。）然后在过滤排除一下不需要的配置，最后返回实际用到的。</strong></p><p>ConfigurationClassParser解析完成之后，被解析出来的类会放到configurationClasses属性中。然后使用ConfigurationClassBeanDefinitionReader去解析这些类。</p><p>这个时候这些bean只是被加载到了Spring容器中。下面这段代码是ConfigurationClassBeanDefinitionReader的解析bean过程：这个时候这些bean只是被加载到了Spring容器中。下面这段代码是<code>ConfigurationClassBeanDefinitionReader#loadBeanDefinitions</code>的解析bean过程：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadBeanDefinitions</span><span class="params">(Set&lt;ConfigurationClass&gt; configurationModel)</span> </span>&#123;</span><br><span class="line">    TrackedConditionEvaluator trackedConditionEvaluator = <span class="keyword">new</span> TrackedConditionEvaluator();</span><br><span class="line">    <span class="keyword">for</span> (ConfigurationClass configClass : configurationModel) &#123;</span><br><span class="line">        <span class="comment">//对每一个配置类，调用loadBeanDefinitionsForConfigurationClass方法</span></span><br><span class="line">        loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">loadBeanDefinitionsForConfigurationClass</span><span class="params">(ConfigurationClass configClass,</span></span></span><br><span class="line"><span class="function"><span class="params">        TrackedConditionEvaluator trackedConditionEvaluator)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//使用条件注解判断是否需要跳过这个配置类</span></span><br><span class="line">    <span class="keyword">if</span> (trackedConditionEvaluator.shouldSkip(configClass)) &#123;</span><br><span class="line">        <span class="comment">//跳过配置类的话在Spring容器中移除bean的注册</span></span><br><span class="line">        String beanName = configClass.getBeanName();</span><br><span class="line">        <span class="keyword">if</span> (StringUtils.hasLength(beanName) &amp;&amp; <span class="keyword">this</span>.registry.containsBeanDefinition(beanName)) &#123;</span><br><span class="line">            <span class="keyword">this</span>.registry.removeBeanDefinition(beanName);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">this</span>.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (configClass.isImported()) &#123;</span><br><span class="line">        <span class="comment">//如果自身是被@Import注释所import的，注册自己</span></span><br><span class="line">        registerBeanDefinitionForImportedConfigurationClass(configClass);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//注册方法中被@Bean注解修饰的bean</span></span><br><span class="line">    <span class="keyword">for</span> (BeanMethod beanMethod : configClass.getBeanMethods()) &#123;</span><br><span class="line">        loadBeanDefinitionsForBeanMethod(beanMethod);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//注册@ImportResource注解注释的资源文件中的bean</span></span><br><span class="line">    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());</span><br><span class="line">    <span class="comment">//注册@Import注解中的ImportBeanDefinitionRegistrar接口的registerBeanDefinitions</span></span><br><span class="line">    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>​    invokeBeanFactoryPostProcessors方法总结来说就是从Spring容器中找出BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor接口的实现类并按照一定的规则顺序进行执行。 其中<strong><code>ConfigurationClassPostProcessor</code></strong>这个BeanDefinitionRegistryPostProcessor优先级最高，它会对项目中的@Configuration注解修饰的类(@Component、@ComponentScan、@Import、@ImportResource修饰的类也会被处理)进行解析，解析完成之后把这些bean注册到BeanFactory中。需要注意的是这个时候注册进来的bean还没有实例化。</p><p>下面这图就是对ConfigurationClassPostProcessor后置器的总结：</p><p> <img src="/images/738818-20191127222404533-1125615815.png" alt="img"></p>"<h1 id="registerBeanPostProcessors方法"><a href="#registerBeanPostProcessors方法" class="headerlink" title="registerBeanPostProcessors方法"></a>registerBeanPostProcessors方法</h1><p>​    从Spring容器中找出的BeanPostProcessor接口的bean，并设置到BeanFactory的属性中。之后bean被实例化的时候会调用这个BeanPostProcessor。</p><p>​    该方法委托给了PostProcessorRegistrationDelegate类的registerBeanPostProcessors方法执行。这里的过程跟invokeBeanFactoryPostProcessors类似：</p><ol><li>先找出实现了PriorityOrdered接口的BeanPostProcessor并排序后加到BeanFactory的BeanPostProcessor集合中</li><li>找出实现了Ordered接口的BeanPostProcessor并排序后加到BeanFactory的BeanPostProcessor集合中</li><li>没有实现PriorityOrdered和Ordered接口的BeanPostProcessor加到BeanFactory的BeanPostProcessor集合中</li></ol><p>​    这些已经存在的BeanPostProcessor在postProcessBeanFactory方法中已经说明，都是由AnnotationConfigUtils的registerAnnotationConfigProcessors方法注册的。这些BeanPostProcessor包括有AutowiredAnnotationBeanPostProcessor(处理被@Autowired注解修饰的bean并注入)、RequiredAnnotationBeanPostProcessor(处理被@Required注解修饰的方法)、CommonAnnotationBeanPostProcessor(处理@PreDestroy、@PostConstruct、@Resource等多个注解的作用)等。</p><p>如果是自定义的BeanPostProcessor，已经被ConfigurationClassPostProcessor注册到容器内。</p><p>这些BeanPostProcessor会在这个方法内被实例化(通过调用BeanFactory的getBean方法，如果没有找到实例化的类，就会去实例化)。</p><h1 id="initMessageSource方法"><a href="#initMessageSource方法" class="headerlink" title="initMessageSource方法"></a>initMessageSource方法</h1><p>初始化MessageSource组件（做国际化功能；消息绑定，消息解析）,这个接口提供了消息处理功能。主要用于国际化/i18n。</p><h1 id="initApplicationEventMulticaster方法"><a href="#initApplicationEventMulticaster方法" class="headerlink" title="initApplicationEventMulticaster方法"></a>initApplicationEventMulticaster方法</h1><p>在Spring容器中初始化事件广播器，事件广播器用于事件的发布。</p><p>程序首先会检查bean工厂中是否有bean的名字和这个常量(applicationEventMulticaster)相同的，如果没有则说明没有那么就使用默认的ApplicationEventMulticaster 的实现：SimpleApplicationEventMulticaster</p><h1 id="onRefresh方法"><a href="#onRefresh方法" class="headerlink" title="onRefresh方法"></a>onRefresh方法</h1><p>一个模板方法，不同的Spring容器做不同的事情。</p><p>比如web程序的容器ServletWebServerApplicationContext中会调用createWebServer方法去创建内置的Servlet容器。</p><p>目前SpringBoot只支持3种内置的Servlet容器：</p><ul><li>Tomcat</li><li>Jetty</li><li>Undertow</li></ul><h1 id="registerListeners方法"><a href="#registerListeners方法" class="headerlink" title="registerListeners方法"></a>registerListeners方法</h1><p>​    注册应用的监听器。就是注册实现了ApplicationListener接口的监听器bean，这些监听器是注册到ApplicationEventMulticaster中的。这不会影响到其它监听器bean。在注册完以后，还会将其前期的事件发布给相匹配的监听器。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">registerListeners</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">//1、从容器中拿到所有已经创建的ApplicationListener</span></span><br><span class="line">    <span class="keyword">for</span> (ApplicationListener&lt;?&gt; listener : getApplicationListeners()) &#123;</span><br><span class="line">        <span class="comment">//2、将每个监听器添加到事件派发器中；</span></span><br><span class="line">        getApplicationEventMulticaster().addApplicationListener(listener);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Do not initialize FactoryBeans here: We need to leave all regular beans</span></span><br><span class="line">    <span class="comment">// uninitialized to let post-processors apply to them!</span></span><br><span class="line">    <span class="comment">// 1.获取所有还没有创建的ApplicationListener</span></span><br><span class="line">    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener<span class="class">.<span class="keyword">class</span>, <span class="title">true</span>, <span class="title">false</span>)</span>;</span><br><span class="line">    <span class="keyword">for</span> (String listenerBeanName : listenerBeanNames) &#123;</span><br><span class="line">        <span class="comment">//2、将每个监听器添加到事件派发器中；</span></span><br><span class="line">        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// earlyApplicationEvents 中保存之前的事件，</span></span><br><span class="line">    Set&lt;ApplicationEvent&gt; earlyEventsToProcess = <span class="keyword">this</span>.earlyApplicationEvents;</span><br><span class="line">    <span class="keyword">this</span>.earlyApplicationEvents = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (earlyEventsToProcess != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (ApplicationEvent earlyEvent : earlyEventsToProcess) &#123;</span><br><span class="line">            <span class="comment">//3、派发之前步骤产生的事件；</span></span><br><span class="line">            getApplicationEventMulticaster().multicastEvent(earlyEvent);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="finishBeanFactoryInitialization方法"><a href="#finishBeanFactoryInitialization方法" class="headerlink" title="finishBeanFactoryInitialization方法"></a>finishBeanFactoryInitialization方法</h1><p>实例化BeanFactory中已经被注册但是未实例化的所有实例(懒加载的不需要实例化)。</p><p>比如invokeBeanFactoryPostProcessors方法中根据各种注解解析出来的类，在这个时候都会被初始化。</p><p>实例化的过程各种BeanPostProcessor开始起作用。</p><p>后面在详细分析此步骤</p><h1 id="finishRefresh方法"><a href="#finishRefresh方法" class="headerlink" title="finishRefresh方法"></a>finishRefresh方法</h1><p>refresh做完之后需要做的其他事情。</p><ul><li>初始化生命周期处理器，并设置到Spring容器中(LifecycleProcessor)</li><li>调用生命周期处理器的onRefresh方法，这个方法会找出Spring容器中实现了SmartLifecycle接口的类并进行start方法的调用</li><li>发布ContextRefreshedEvent事件告知对应的ApplicationListener进行响应的操作</li></ul><p>如果是web容器ServletWebServerApplicationContext还会启动web服务和发布消息</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">finishRefresh</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.finishRefresh();</span><br><span class="line">    WebServer webServer = startWebServer();</span><br><span class="line">    <span class="keyword">if</span> (webServer != <span class="keyword">null</span>) &#123;</span><br><span class="line">        publishEvent(<span class="keyword">new</span> ServletWebServerInitializedEvent(webServer, <span class="keyword">this</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://www.cnblogs.com/grasp/p/11942735.html" target="_blank" rel="noopener">spring容器的refresh方法分析</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Spring容器创建之后，会调用它的refresh方法刷新Spring应用的上下文。&lt;/p&gt;
&lt;p&gt;首先整体查看AbstractApplicationContext#refresh源码&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;t
      
    
    </summary>
    
      <category term="spring源码分析" scheme="http://yoursite.com/categories/spring%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    
    
      <category term="spring" scheme="http://yoursite.com/tags/spring/"/>
    
      <category term="springMVC" scheme="http://yoursite.com/tags/springMVC/"/>
    
  </entry>
  
</feed>
