<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>simyy</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://simyy.cn/"/>
  <updated>2024-12-31T15:49:16.113Z</updated>
  <id>http://simyy.cn/</id>
  
  <author>
    <name>simyy</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>2024最后一天的碎碎念</title>
    <link href="http://simyy.cn/2024/12/31/2024-last-life-note/"/>
    <id>http://simyy.cn/2024/12/31/2024-last-life-note/</id>
    <published>2024-12-31T10:55:00.000Z</published>
    <updated>2024-12-31T15:49:16.113Z</updated>
    
    <content type="html"><![CDATA[<p>2024的最后一天随便记录下~</p><a id="more"></a><h2 id="碎碎念"><a href="#碎碎念" class="headerlink" title="碎碎念"></a>碎碎念</h2><p>前几天突然想起来整理备忘录，突然发现了一篇22年写的规划，快三年了，自己啥也没做，要说有成长吧，应该也有。这一年以来一直持续的在看书学习，也算坚持下来了，看了不少关于历史、演讲的书籍，尤其是历史书对我的影响比较大，一个历史人物的一生也就在潦潦草草几页纸，也感叹人生的短暂。还记得初中那会，我还是历史课代表（印象中是，我记得我每次都要给班级同学发卷子），现在已经忘记了那些历史知识。<br>今年刚好是毕业的第十个年头，在互联网干了十年，赶上了互联网的尾巴，没吃肉喝了点汤，薪水一直还可以，就是没有大的发展，还是一如既往的一线牛马。再过个年周岁都37了，也算个大龄程序员了，心智还像个小孩子，做事上还是斤斤计较、吹毛求疵，看着新来的大学生，自己是真的老了，不是心理老，是身体老，啥运动也搞不动了，脸上的皱纹也多了，连衣服都买一堆一样的。</p><p>回顾自己的前半生，也算是个命运多舛的人。</p><p>我出生在一个贫困县小镇双职工家庭，父亲是一个教师，母亲是一个工人，看起来比身边很多同事都强上不少，不过随着工人下岗潮的到来，母亲再就没有了收入，连下岗的赔付款都没有，从此生活上也非常拮据了。还记得小时候因为没有冰箱，吃到的肉都是用盐浸过的，这样才能放的住，所以肉的味道也和新买来的不一样，不过我的父母还是努力给我最好的生活，他们吃小米让我吃大米。父亲是一个情绪极其不稳定的人，尝尝和母亲吵架，因为我的学习吵架，在整个童年记忆里，我只记得我很小时候他经常带我去公园玩，我很开心，但到了上学期间只记得吵架，不停的吵架。也因为父亲的性格，和亲戚关系都很差，不过现在想想这只是一部分原因，还是因为我们家太穷了，在别人眼里你没有任何价值，事办不了，玩也玩不到一起，索性就不要联系了，那时候的自卑感就越发明显，我发现我和其他人不同，别人轻易可以有的，我都没有，全班同学都有补课，我也没有，我是班级里最另类那个。</p><p>我小学时候学习并不好，好的时候10多名，不好的时候40多名，好在数学不错，上了初中，我的记忆力开始发挥特长，我在全班可以稳定在前5名，甚至我还记得考过全校的第十名，那可能是我在中学时最高光的时刻，那会在胡同里我都是当做别人家的孩子，不过在我父亲眼里，我就是比不上他们单位某某的孩子。到了高中，我开始去网吧打游戏，把自己的饭钱和零花钱省下来，去打游戏，去逃避现实，我不再想学习，还记得当年考试前我去翻物理书，记得清清楚楚的f=ma，不过那时候成绩也没有太差，全年级100名左右，努力点上个一本，差点上个二本。果然不出所料，分数线刚好低了几分，二本录取了。后面我决定复习，现在想想也不知道为什么自己会这么决定，可能还是不甘心吧，那会初中成绩差不多的至少都考上了一本，复习那会成绩还不错，不过那时候我就发现了我的心理素质越来越差，也导致了最终发挥失常，报考的院校全部滑档，最后被一所省内二本录取了，那时候我也死心了，就这样去上学吧。可能我骨子里还是那个不服输的性格，军训了一周左右，我每天基本睡不着，室友有喝酒的、抽烟的、打牌的，和这些人做同学是我一辈子都没想过的，我就给家里打了一通电话，我要回去复读，但父亲是不同意的，他觉得不能再复读了，后面是母亲和叔叔来接的我，我自己去学校找的老师，老师给我安排到了他的班级，我也是那年考上了985（最后走了提前批，比录取分高了50分，虽然有些后悔）。</p><p>虽然看起来我个子不高，也很瘦弱，但从小胡同里的孩子王，带着大家踢球，球机还不错，在学校也能交到一些朋友，从那时候开始我就是开始有了自己的主意，敢和高年级的踢球甚至打架，敢和老师对着干，但做所有的事都会有自己的原则和理由。还记得高中那会，班主任因为收礼，就要把我从第一排移动到倒数第二排，美其名曰是照顾近视眼的同学，我当时就发火了，谁爱换谁换，我就不换，从那时候起可能也注定我的学习老师不在管，不过他那个人也没管过谁，就知道收礼。现在想起来，我的初中班主任，是个特别好的人，她和年轻也很漂亮，大家都非常喜欢她，她也喜欢学习成绩好的同学，所以就这样我在初中时候的成绩是越来越好，同样高中复习班的老师也非常好。可能还是家庭的原因，我的自卑感一直很强，毕业后再也没和老师联系过。</p><p>读研也算是一个幸运，本该保研的却因为自己英语太差，没过6级错过了。找工作受挫，开始临时考研，连资料都是同学给的（他不考了），学了三个月过了分数线，虽然找了老师但最后并没有录取我，那时候还是很自卑，觉得还是自己太差了。不过女朋友的一句话改变了我的一生，他让我去碰碰运气，肯定有专业没录取完，刚到那计算机的老师就问你是本校的么，是的话就你了，就这样我被计算机专业录取了，因祸得福。也算一段神奇的经历了，所有人都觉得我不可能考上研究生，就学了3个月，但我真的考上了，我是通过背书的方式，我连为啥需要这个公式来解这个题可能都清楚，但是我记下来了（父亲的遗传，也算一种天赋吧）。</p><p>读研期间，我意外的被某公司挑选去实习，又意外的被淘汰（理由居然是我们校招的名额给了清北的，所以没坑了）。索性回到学校，那时候秋招都过去了很多公司，常见的大厂都结束了招聘，我赶在尾巴上拿到了一个还算幸运的几个offer，最后选择去了北京（因为薪水高）。</p><p>工作后，我先后跳槽了5次，中间有长有短，最长的4年半左右，最短的1个月，我算是一个有主见的人，当发现周围环境对自己不利的时候，就会主动求变，去找新的机会，那会跳槽是一个很常见的事，不过太多就不好了，也导致了我职业中期的困难重重，虽然我是985本硕毕业，但履历上看起来并不好，大小公司穿插起来，怎么看也不是怎么靠谱，也从而在每家公司都做的一般。虽然每个公司的同事们都比较认可我，做事踏实，有想法，不过在老板眼里，这些都不是啥优势，我的性格和自卑感让我的职业发展遇到了瓶颈，不知道如何和老板相处，更不懂如何向上管理，做的再多也没用，老黄牛一样的工作方式也只能是公司里的老黄牛。目前在这家公司已经工作了四年多的时间，经历了两次晋升答辩均已失败告终，虽然自己有些不甘，但也不太在意这些，岁数大了跳槽也难了，在国内这个市场经验和能力不重要，重要的是年纪，也挺奇怪的，小年轻的刚毕业拿着十年工作经验的人50%左右的薪水，却缺干着20%的工作，老板说新人是未来，老人学不动了，也干不动了，可能这也是国内独有的景色，什么行业最后都能和打螺丝一样。</p><h2 id="失败的投资"><a href="#失败的投资" class="headerlink" title="失败的投资"></a>失败的投资</h2><p>几年前就有换房的想法，这个房子隔音差楼上楼下的出租噪音让我很难入睡，多个夜晚都是凌晨四五点才入睡。不过家里人根本不理解我，我也理解不了我自己，为啥加班加点的工作，到最后连一个像样的房子都住不上。老婆说，是为了孩子的教育，当时买房的时候没考虑过，就是阴差阳错买了个学区房，现在搬走她觉得可惜，所以坚持留着这套自住，给孩子上学用。这样导致了我们的本金有限，想置换更好的房子就要背负上更多得的房贷。我的内心是更倾向于买一个更好的房子，然后出租这套用来换贷款，可后面被我老婆否决了，她觉得压力太大，只能买能承受住的，也就造成了难以接受的结果。最终我们购入了一套郊区新房，距离7~9公里左右，随着房价一直在下降，现在是后悔莫及，我的性格是有缺陷的，对于这种失败的买房经历，内心真的放不下，时常懊悔自己的选择，其实当时自己也观察了很多二手和附近的新房，我更倾向于购买这些，但最终还是妥协了，也导致了这次失败的经历，不仅在金钱上损失50W左右，也导致我的置换计划搁置至少4年左右，一步错步步错，当时很多同事都劝我买一套附近的新房，我们却因为价格选择郊区房，住不上，出租还亏，当时脑子是真坏掉了，居然选了这么一个最多的选项。这两天听到儿子同学家，就在附近低首付买入了一套我理想中的房子，真的是懊悔不已，这种房子再想买入，可能又要额外加100万，加上亏损的50万，这房子让我以后高攀不起了（例外要加150万才能买得到，一步错步步错）。</p><p>从最开始24年初的后悔，到现在已经有一年了，亏钱还是小事，最重要的亏损是时间啊，我的一生也才有几个这样的时间。从买车到买房，几个选择都是错的，我都是在将就别人，妥协自己的选择，最后都是后悔，大家都后悔。我自己算是一个理想的人，但在做决定的时候会犹犹豫豫，容易被他人误导，也导致这些遗憾。有些时候这些事让我更加自卑，我认为我可能就是这种命，每次选择都是错的。今天突然发现，这件事已经恶心我有一年的时间了，那破房子还要延期交房，之前说25年中，现在说要26年中，唉，隔壁的新房都要入住了，这样可能就是命吧，我有时候真想第二天就不要醒来，我一点痛苦也没有，我也不亏欠谁多少，可能只有我儿子吧。</p><h2 id="生活"><a href="#生活" class="headerlink" title="生活"></a>生活</h2><p>生活上，除了懊悔错误的购房经历，再也没有什么值得写的了，孩子三岁了上了幼儿园，虽然我是想多和他玩会，但我还是沉不下心来，也不想在他身上投入更多的精力，我也不知道我为什么是这样的父亲，我满脑子里都是房子、工作、赚钱，我不想在孩子身上浪费时间，我只想换一个一个生活环境，有一个自己的安静的书房，可以做自己喜欢做的事，买一辆自己喜欢的汽车，出去兜风，可现实上我什么都做不到，我就像是一个赚钱的机器，每天早上10点半到公司，每周2~3天要早上八点起来送下孩子去幼儿园，抓紧地铁上的时间学习半小时（看书），然后工作到中午吃完，再抓紧时间看书学习，下午一直工作到晚饭，吃饭遛弯，然后在干活一直早晚上十点半左右下班，基本到家都是11点左右了，这时候孩子睡了，我才感觉这时候才是我自己，我开始熬夜，看手机，学习，一直到临晨2点才开始入睡，每天我都能挤出来不到3个小时的时间给自己，可能代价就是透支生命吧，我现在每天都感觉身上没劲，不过只有那三个小时我才是开心的，因为周末我还要带孩子，我没有自己的时间，也没有权利买自己喜欢的东西，除了一个PS4，我好像没有给自己买过任何东西，我那么爱玩游戏的一个人，居然连一个台式机都没有，家里只有一台可以学习工作的mac，今天写下来这么多字，我突然意识到我真的是一个机器，一个只会服务于他人的机器，只会赚钱，没朋友，没生活，没盼头，所以说，有的人活着但是已经死了，我感觉我现在就是这种状态，我努力上班工作学习，但是什么都改变不了。</p><h2 id="成长"><a href="#成长" class="headerlink" title="成长"></a>成长</h2><p>这一年，看了很多书，也学了很多的道理，认知上有了很大的提高，也不在和老黄年一样的干活，更多的思考和落地，去探索更多的可能性。不过也受限于自己的职位，真的能落地的不多，不过也算是有一些成就。上半年还在未答辩没有通过而失落，不过经过第二次失败已经放下了执念，我能做的事就那么多，就那么十几分钟怎么就决定了一个人行不行呢，都是人情罢了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;2024的最后一天随便记录下~&lt;/p&gt;
    
    </summary>
    
      <category term="生活" scheme="http://simyy.cn/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
      <category term="生活" scheme="http://simyy.cn/tags/%E7%94%9F%E6%B4%BB/"/>
    
  </entry>
  
  <entry>
    <title>mysql连接池的坑</title>
    <link href="http://simyy.cn/2022/08/14/mysql-thread-pool/"/>
    <id>http://simyy.cn/2022/08/14/mysql-thread-pool/</id>
    <published>2022-08-14T03:05:55.000Z</published>
    <updated>2022-08-14T03:37:08.096Z</updated>
    
    <content type="html"><![CDATA[<p>数据库连接池的配置不仅会影响client的连接，也会影响server端的性能。</p><a id="more"></a><h3 id="建连的坑"><a href="#建连的坑" class="headerlink" title="建连的坑"></a>建连的坑</h3><blockquote><p>线上环境的p95的数据的性能很差，通过trace链路分析出『均是由于数据库的sql执行的RT较长导致』，这样就比较奇怪了，同样的sql为什么会这么不稳定；<br>尤其在服务发布或流量徒增的过程中，问题现象更加明显；通过DBA的分析得出结论『数据库在有突增的新建连接时会影响sql的执行』，。</p></blockquote><p>针对线上集群，单个数据库端口连接数非常多，每次业务有新的发布会创建比较多的连接，首先会导致『新建连接比较慢』，其次会导致『新建连接过程中会影响已有链接的RT』</p><p><strong>建连慢</strong></p><blockquote><p>建连慢的主要原因在于tcp的建连队列大小，如果设置的队列过小会导致丢弃部分建连的请求，导致服务不可用；<br><a href="https://stackoverflow.com/questions/36594400/what-is-backlog-in-tcp-connections" target="_blank" rel="noopener">https://stackoverflow.com/questions/36594400/what-is-backlog-in-tcp-connections</a></p></blockquote><p>调整内核配置参数，解决单端口连接过多的问题：</p><ul><li>tcp_max_sync_backlog 2048-&gt;8192</li><li>somaxconn 128-&gt;2048</li></ul><p><strong>建连RT影响</strong></p><blockquote><p>建连登录认证也是一个sql，正常都会进入sql队列，与正常的sql在一个队列中，所以会影响正常的sql执行;<br><a href="https://dev.mysql.com/doc/refman/8.0/en/thread-pool-tuning.html" target="_blank" rel="noopener">https://dev.mysql.com/doc/refman/8.0/en/thread-pool-tuning.html</a></p></blockquote><ul><li>thread_pool_stall_limit：线程池中无可用线程时设置建新线程的等待时间，单位为毫秒，默认是500；</li><li>thread_pool_idle_timeout：设置空闲线程销毁前的等待时间，单位为秒，默认是60；</li></ul><p>通过减小timmer检测间隔并提高连接池的超时时间，调整线程的创建和回收策略来优化RT的问题：</p><ul><li>thread_pool_stall_limit 500ms-&gt;10ms</li><li>thread_pool_idle_timeout 60s-&gt;600s</li></ul><p>由于以上配置会由于减少线程的回收和增加线程的创建频率均会对占用一定的服务器资源和性能，因此在评估这些参数的时候可以逐渐增加或减少该值，并在当前数据库负载的情况下找到一个合理的值；</p><h3 id="建接数量"><a href="#建接数量" class="headerlink" title="建接数量"></a>建接数量</h3><blockquote><p>mysql的默认建连数据是xxx，实际可以按照数据库的性能做一定的调整；<br>数据库的连接会占用服务器的cpu和内存，所以谨慎的限制数据库client和server的连接数是非常重要的；<br>根据经验，32核64G内存的数据库可以配置到万级别的数据库连接数量，同时会有上千台的服务器与单分片的数据库保持着连接没，例如，1000台服务器，没台服务的连接池配置为50，则整体5W的连接数；</p></blockquote><p>数据库连接数较多情况下，可能会导致那个数据库夯住：</p><ul><li>故障切换时由于大量的请求同时打到DB，连接数会有翻倍的可能，导致连接数超过数据库上限；</li><li>故障切换时会导致业务访问DB的重试，可能导致连接数超过数据库上限；</li><li>当服务已有连接数过多的时候，业务如果同时发布但并未销毁原有容器，则可能创建最多翻倍的连接，并且会申请最多翻倍的数据库临时内存；</li></ul><h3 id="mysql线程池"><a href="#mysql线程池" class="headerlink" title="mysql线程池"></a>mysql线程池</h3><blockquote><p>参考文档：<a href="https://dbaplus.cn/news-11-1989-1.html" target="_blank" rel="noopener">https://dbaplus.cn/news-11-1989-1.html</a></p></blockquote><p>mysql线程池采用分组管理，连接按照分组算法分配到不同的分组，此后相同请求会在次分组内调度线程执行；此外线程池使用Timer线程定期检查分组运行的情况，并为阻塞的group分配线程。</p><p><img src="/images/mysql_thread_pool_1.jpg" alt="线程池的组成部分"></p><p><strong>group分组</strong></p><ul><li>工作线程：用于处理请求和任务；</li><li>监听线程：用于接收请求并加入任务队列；</li><li>任务队列：用于存放待执行的IO任务，分为高优先级队列和低优先级队列（高优先级队列的任务会优先被处理）；</li></ul><p><strong>timer线程</strong></p><p>timer线程会定期扫描各个group检查是否被阻塞，当发现group内有任务堆积，就会给group创建新的worker进行处理；</p><p><img src="/images/mysql_thread_pool_2.jpg" alt="线程池的工作流程"></p><p><strong>工作流程</strong></p><ul><li>客户端优先会向server发起请求，根据threadid%thread_pool_size确定路由的分组；</li><li>监听线程在收到请求后，会按照队列情况分别处理：<ul><li>队列无请求，则转换为工作线程立即处理；</li><li>队列有请求，则将任务加入任务队列中，排队等待被处理；</li></ul></li><li>工作线程从队列中获取等待被处理的任务，依次进行处理，如果队列为空，则会在等待配置的超时时间后销毁；</li><li>timmer线程会定期检查group是否阻塞，如果存在任务阻塞会创建新的工作线程；</li></ul><h3 id="配置参数"><a href="#配置参数" class="headerlink" title="配置参数"></a>配置参数</h3><p><strong>thread_handling</strong></p><ul><li>one-thread-per-connection（默认，一个连接一个线程）</li><li>pool-of-threads（线程池）</li></ul><p><strong>thread_pool_size</strong></p><ul><li>线程池的分组数量，默认为cpu数量；</li></ul><p><strong>thread_pool_oversubscribe</strong></p><ul><li>分组内的最大线程数，监听不再其内；</li></ul><p><strong>thread_pool_high_prio_mode</strong></p><ul><li>transactions：启动事务的语句放到高优先级队列；</li><li>statements：所有的语句都会放到高优先级队列，不会使用优先级队列；</li><li>none：不使用高优先级队列；</li></ul><p><strong>thread_pool_high_prio_tickets</strong></p><ul><li>控制每个连接最多被放入高优先级队列中的次数，默认为4294967295；</li><li>仅在transactions模式下生效，如果超过阈值，则直接放入低优先级队列；</li></ul><p><strong>thread_pool_idle_timeout</strong></p><ul><li>工作线程最大空闲时间，默认为60秒，超过限制后会退出；</li></ul><p><strong>thread_pool_max_threads</strong></p><ul><li>限制线程池最大的线程数，超过该限制后将无法再创建更多的线程，默认为100000；</li></ul><p><strong>thread_pool_stall_limit</strong></p><ul><li>timer线程的检测group是否异常的时间间隔，默认为500ms；</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;数据库连接池的配置不仅会影响client的连接，也会影响server端的性能。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="MySQL" scheme="http://simyy.cn/tags/MySQL/"/>
    
      <category term="数据库" scheme="http://simyy.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
  </entry>
  
  <entry>
    <title>页面组件化的架构设计与思考</title>
    <link href="http://simyy.cn/2022/03/20/view-module-desgin/"/>
    <id>http://simyy.cn/2022/03/20/view-module-desgin/</id>
    <published>2022-03-20T02:17:20.000Z</published>
    <updated>2022-08-28T03:10:05.451Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要用于分享前后端组件化架构设计的实践经验与思考。</p><a id="more"></a><h4 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h4><blockquote><p><strong>职责不清</strong>：前后端没有明显的业务边界，业务逻辑耦合严重；</p></blockquote><p>职责不清会让需求拆解和分析造成困扰，也会造成逻辑的前后端的高度耦合，提高后期的维护成本。</p><p>由于页面的任何改动都需要前后端的支持，整体改动的开发联调工作量以及排期冲突问题都难以接受。</p><blockquote><p><strong>页面性能</strong>：前后端交互逻辑由于年久失修，存在大量的无效请求和重复请求；</p></blockquote><p>由于页面存在大量的无效业务逻辑和资源请求，导致页面渲染性能较差，影响用户体验；</p><p>后端的无效资源请求对服务器和数据库都会造成不必要的压力，属于<code>资源的浪费</code>；</p><h4 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h4><blockquote><p><strong>O1：开发提效</strong></p></blockquote><p>从根本上解决前后端耦合的问题，前端仅关注组件化通用能力的建设，业务逻辑全部内聚在后端；</p><p>页面的动态渲染功能实现配置化、定制化，用于满足不同改动量的需求；</p><blockquote><p><strong>O2：体验优化</strong></p></blockquote><p>后端接口调用链路优化，提高资源利用率，减少无效请求和无效的处理逻辑；</p><p>页面资源依赖优化，从整体页面的视角来分析解决页面渲染性能的问题；</p><h4 id="落地"><a href="#落地" class="headerlink" title="落地"></a>落地</h4><p>考虑到页面逻辑比较复杂，需要人肉来分析识别有效的业务逻辑，因此拆分两个阶段来完成目标：</p><p><strong>阶段1：梳理前端的业务逻辑，优化无效的资源请求，业务逻辑内聚在后端</strong></p><p><strong>阶段2：页面组件化schema设计，实现组件spi扩展、资源自动装配等能力</strong></p><h5 id="阶段1"><a href="#阶段1" class="headerlink" title="阶段1"></a>阶段1</h5><blockquote><p>阶段1主要解决了页面渲染层面业务逻辑的耦合问题，初步实现了前后端分离的目标。</p></blockquote><ul><li>前后端业务逻辑梳理汇总</li><li>页面抽象和页面组件化拆分</li><li>后端资源加载内聚统一</li></ul><p><img src="/images/view-module-desgin-1.jpeg" alt=""></p><p>业务逻辑的梳理是关键，为了避免逻辑的遗漏，通过开发和测试的不同视角来分析和对比业务逻辑：</p><ul><li>开发：从代码层面，阅读前后端的代码逻辑并梳理出一套页面业务逻辑；</li><li>测试：从功能层面，覆盖页面的所有case用例输出一套页面的测试case;</li></ul><p>为了实现页面的自动降级和分段加载，需要对页面整体做一次组件化抽象，拆分思路如下：</p><ul><li>按职责拆分：以功能属性来拆分组件，组件功能内聚（职责单一）；</li><li>按顺序加载：以功能的优先级来分组组件，分段来加载不同组件，实现页面加载的体验优化；</li></ul><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></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">Context</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> AResource aResource;</span><br><span class="line">    <span class="keyword">private</span> AResource bResource;</span><br><span class="line">    <span class="keyword">private</span> AResource cResource;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Context</span><span class="params">(AResource aResource, ...)</span> </span>&#123;</span><br><span class="line">        ....</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="阶段2"><a href="#阶段2" class="headerlink" title="阶段2"></a>阶段2</h5><blockquote><p>阶段2主要解决组件的扩展能力以及资源管理的进一步优化。</p></blockquote><p><img src="/images/view-module-desgin-2.jpeg" alt=""></p><ul><li>组件的通用化设计</li><li>基于SPI的扩展能力</li><li><code>写时复制</code>的资源管理</li></ul><p>组件的通用化设计不仅包含展示层面的组件设计，也包含表单、联动的设计模式。</p><p>组件的抽象定义是为了解决组件的复用性，通过定义通用的展示、表单组件，实现自定义的页面组件渲染能力。</p><p>为了提高页面的扩展能力，对于通用组建提供基于SPI的扩展点，业务上可以很快的实现组件的重载和扩展，同时对不同的业务实现业务的隔离。</p><p>在第一阶段，虽然把资源管理独立出来，但对于不同场景下的资源加载并没有单独优化，因此考虑从资源管理上入手，利用写时复制的思想来解决资源惰性加载的能力，并通过线程cache的手段避免资源的频繁加载，最终实现一个无需使用者关心的资源中心。</p><p><strong>Schema加载</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Schema解析起接口定义，通过实现ISchemaParser接口来注册加载新的组件</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ISchemaParser</span> </span>&#123;</span><br><span class="line">    <span class="comment">// Schema枚举</span></span><br><span class="line">    <span class="function">SchemaEnum <span class="title">getSchemaEnum</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="comment">// Schema结构</span></span><br><span class="line">    <span class="function">SchemaStructure <span class="title">get</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Schema配置</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SchemaParserConfiguration</span> <span class="keyword">implements</span> <span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;ISchemaParser&gt; schemaParsers; <span class="comment">// 利用Spring自动注入</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> Map&lt;SchemaEnum, ISchemaParser&gt; mapping = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">public</span> SchemaStructure <span class="title">getBySchemaEnum</span><span class="params">(SchemaEnum schemaEnum)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (mapping.containsKey(schemaEnum)) &#123;</span><br><span class="line">            <span class="keyword">return</span> mapping.get(schemaEnum).get();</span><br><span class="line">        &#125;</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="comment">// 利用InitializingBean初始化映射mapping</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (CollectionUtils.isEmpty(schemaParsers)) &#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">for</span> (ISchemaParser schemaParser : schemaParsers) &#123;</span><br><span class="line">            mapping.put(schemaParser.getSchemaEnum(), schemaParser);</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>schema转换</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">// Schema转换接口定义，通过实现ISchemaTransfer来实现转换器的注册</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ISchemaTransfer</span>&lt;<span class="title">T</span> <span class="keyword">extends</span> <span class="title">IView</span>, <span class="title">R</span>, <span class="title">C</span> <span class="keyword">extends</span> <span class="title">BaseContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function">ViewTypeE <span class="title">viewType</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function">R <span class="title">transfer</span><span class="params">(T t, C context)</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Schema转换配置</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SchemaConfiguration</span> <span class="keyword">implements</span> <span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;ISchemaTransfer&gt; schemaTransfers;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;ViewTypeE, ISchemaTransfer&gt; mapping = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> ISchemaTransfer <span class="title">getByType</span><span class="params">(ViewTypeE viewTypeE)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> mapping.getOrDefault(viewTypeE, <span class="keyword">null</span>);</span><br><span class="line">    &#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">public</span> <span class="keyword">void</span> <span class="title">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (schemaTransfers != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">for</span> (ISchemaTransfer schemaTransfer : schemaTransfers) &#123;</span><br><span class="line">                mapping.put(schemaTransfer.viewType(), schemaTransfer);</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>资源上下文管理</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></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">Context</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, Object&gt; req; <span class="comment">// 存储入参</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> IAContextA aContext; <span class="comment">// 绑定资源，通过外部Spring注入后作为初始化参数引入</span></span><br><span class="line">    <span class="keyword">private</span> IBContext bContext;</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="function"><span class="keyword">public</span> <span class="keyword">static</span> Context <span class="title">init</span><span class="params">(SpingBean springBean)</span> </span>&#123;</span><br><span class="line">        IAContext aContext = <span class="keyword">new</span> DefaultAContext();</span><br><span class="line">        aContext.setSpringBean(springBean);</span><br><span class="line">    </span><br><span class="line">        Context context = <span class="keyword">new</span> Context();</span><br><span class="line">        context.setAContext(aContex);</span><br><span class="line">        <span class="keyword">return</span> context;</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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">IAContext</span> </span>&#123;</span><br><span class="line">    <span class="function">Resp <span class="title">method</span><span class="params">(Req req)</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultAContext</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SpingBean springBean;</span><br><span class="line">    ...</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">public</span> Resp <span class="title">method</span><span class="params">(Req req)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 线程级别的缓存，避免资源重复调用</span></span><br><span class="line">        <span class="keyword">return</span> threadCached(cachedPrefix, uniqueKey, () -&gt; a.method(req));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>基于spi的模块化设计</strong></p><p>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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IView</span> </span>&#123;</span><br><span class="line">    <span class="function">ViewTypeE <span class="title">viewType</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、定义组件spi，以预下单为例，抽象出标题、价格、数量、规格等能力，并指定页面类型为预下单；</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IPreOrderView</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">BaseContext</span>&gt; <span class="keyword">extends</span> <span class="title">IView</span>, <span class="title">ICustomMatch</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> ViewTypeE <span class="title">viewType</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> ViewTypeE.PRE_ORDER_PAGE;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">String <span class="title">getTitle</span><span class="params">(C context)</span></span>;</span><br><span class="line">    <span class="function">String <span class="title">getTotalPrice</span><span class="params">(C context)</span></span>;</span><br><span class="line">    <span class="function">String <span class="title">getTotalCount</span><span class="params">(C context)</span></span>;</span><br><span class="line">    <span class="function">String <span class="title">getGoodsSpecification</span><span class="params">(C context)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>3、实现spi，主要是利用资源上下文来获取相关资源，渲染模块信息；</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="meta">@DefaultView</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultPreOrderView</span> <span class="keyword">implements</span> <span class="title">IPreOrderView</span>&lt;<span class="title">SingleOrderContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T extends BaseContext&gt; <span class="function"><span class="keyword">boolean</span> <span class="title">matched</span><span class="params">(IView view, T context)</span> </span>&#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><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 默认实现</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> DefaultView &#123;</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="meta">@Component</span></span><br><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> ExtensionView &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>5、基于构造器的思想，利用资源上线来构件通用的组件，</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IViewBuilder</span>&lt;<span class="title">T</span> <span class="keyword">extends</span> <span class="title">IView</span>, <span class="title">C</span> <span class="keyword">extends</span> <span class="title">ViewContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function">List&lt;T&gt; <span class="title">build</span><span class="params">(C context)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>页面定义，利用多个spi来组装一个完整的页面结构</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></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">IViewBuilder</span> </span>&#123;</span><br><span class="line">    &lt;C extends BaseContext&gt; <span class="function">List&lt;IView&gt; <span class="title">build</span><span class="params">(C context)</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="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractViewBuilder</span>&lt;<span class="title">T</span> <span class="keyword">extends</span> <span class="title">IView</span>&gt; <span class="keyword">implements</span> <span class="title">IViewBuilder</span>, <span class="title">ApplicationContextAware</span>, <span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> MatcherConfiguration matcherConfiguration = <span class="keyword">new</span> MatcherConfiguration(); <span class="comment">// 匹配器</span></span><br><span class="line">    <span class="keyword">private</span> ApplicationContext applicationContext; </span><br><span class="line">    <span class="keyword">private</span> List&lt;Class&lt;? extends T&gt;&gt; definitions;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends T&gt;, List&lt;T&gt;&gt; defaultImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends T&gt;, List&lt;T&gt;&gt; extensionImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> Class&lt;T&gt; <span class="title">getViewType</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> List&lt;Class&lt;? extends T&gt;&gt; getScanDefinitions();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;C extends BaseContext&gt; <span class="function">List&lt;IView&gt; <span class="title">build</span><span class="params">(C context)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> matcherConfiguration.doFilter(definitions, defaultImplements, extensionImplements, context);</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">     * 初始化定义实现</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> Exception</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">initSpiImplements</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        definitions = getScanDefinitions();</span><br><span class="line">        ...</span><br><span class="line">        Map&lt;String, T&gt; beans = applicationContext.getBeansOfType(getViewType());</span><br><span class="line">        beans.values().stream().forEach(bean -&gt; &#123;</span><br><span class="line">            <span class="keyword">for</span> (Class&lt;? extends T&gt; definition : definitions) &#123;</span><br><span class="line">                <span class="keyword">if</span> (definition.isInstance(bean)) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (bean.getClass().getAnnotation(DefaultView.class) != <span class="keyword">null</span>) &#123;</span><br><span class="line">                        register(defaultImplements, bean, definition);</span><br><span class="line">                        log.info(<span class="string">"ViewBuilder For &#123;&#125; default &#123;&#125;"</span>, getViewType().getSimpleName(), definition.getSimpleName());</span><br><span class="line">                    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (bean.getClass().getAnnotation(ExtensionView.class) != <span class="keyword">null</span>) &#123;</span><br><span class="line">                        register(extensionImplements, bean, definition);</span><br><span class="line">                        log.info(<span class="string">"ViewBuilder For &#123;&#125; extension &#123;&#125;"</span>, getViewType().getSimpleName(), definition.getSimpleName());</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        log.info(<span class="string">"ViewBuilder For &#123;&#125; ignore &#123;&#125;"</span>, getViewType().getSimpleName(), definition.getSimpleName());</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><span class="line">    &#125;</span><br><span class="line">    ...</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PreOrderViewBuilder</span> <span class="keyword">extends</span> <span class="title">AbstractViewBuilder</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> List&lt;Class&lt;? extends IView&gt;&gt; spiDefinitions = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="comment">// 指定顺序</span></span><br><span class="line">        spiDefinitions.add(IPreOrderView.class);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> List&lt;Class&lt;? extends IView&gt;&gt; getScanDefinitions() &#123;</span><br><span class="line">        <span class="keyword">return</span> spiDefinitions;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>6、为了实现组件的扩展能力，实现了自定义的匹配接口；</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IMatch</span> </span>&#123;</span><br><span class="line">    &lt;T extends BaseContext&gt; <span class="function"><span class="keyword">boolean</span> <span class="title">matched</span><span class="params">(IView view, T context)</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ICustomMatch</span>&lt;<span class="title">CC</span> <span class="keyword">extends</span> <span class="title">BaseContext</span>&gt; <span class="keyword">extends</span> <span class="title">IMatch</span> </span>&#123;</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="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractMatcher</span> <span class="keyword">implements</span> <span class="title">IMatch</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> &lt;T extends BaseContext&gt; <span class="function"><span class="keyword">boolean</span> <span class="title">doMatch</span><span class="params">(IView view, T context)</span></span>;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T extends BaseContext&gt; <span class="function"><span class="keyword">boolean</span> <span class="title">matched</span><span class="params">(IView view, T context)</span> </span>&#123;</span><br><span class="line">        ...</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">// 匹配器的注册</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MatcherConfiguration</span>&lt;<span class="title">T</span> <span class="keyword">extends</span> <span class="title">IView</span>, <span class="title">R</span> <span class="keyword">extends</span> <span class="title">BaseContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> List&lt;IMatch&gt; MATCHERS = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        MATCHERS.add(<span class="keyword">new</span> CustomMatcher());</span><br><span class="line">        MATCHERS.add(<span class="keyword">new</span> StateMatcher());</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="保障手段"><a href="#保障手段" class="headerlink" title="保障手段"></a>保障手段</h5><p>为了降低重构后页面整体的风险，通过组件隔离、页面灰度、整体页面监控等多个手段来解决页面的问题发现和降级问题。</p><p><strong>隔离</strong></p><p>为了提高整体页面的可用性，通过隔离不同组件的加载和渲染，保证整体页面基本可用的状态。</p><p><strong>灰度</strong></p><p>灰度策略一般选用用户id作为灰度维度，但针对复杂的页面来说，测试很难充分的覆盖每一种业务场景，因此可以考虑基于场景来灰度，具体可以从最基础的业务场景出发进行灰度，逐渐放量到不同的业务场景。</p><p><strong>监控</strong></p><p>监控方面，可以从前端监控和后端监控两个方面入手：</p><p>前端监控可以覆盖用户的实际使用场景，包含页面的打开成功率、页面打开耗时、组件渲染成功率等。</p><p>后端监控可以从系统整体性能和异常发现角度来考虑，包含接口的RT、组件的渲染成功率、组件的渲染异常等角度来发现问题。</p><h4 id="多表单联动"><a href="#多表单联动" class="headerlink" title="多表单联动"></a>多表单联动</h4><p>简单的form标单很容易通过schema这套协议来实现，但对于复杂表单联动很难通过简单的schema协议来实现；</p><blockquote><p>利用后端来维护整个页面的状态来实现多表单的联动。</p></blockquote><p>目前我们通过后端来维护整个页面的渲染逻辑，也就是包含多个组件的联动关系由后端整体下发，举个例子：</p><ul><li>页面分表有两个表单项，B表单项会根据A表单项的选择结果来返回；</li><li>前端第一次请求后端，会返回A所有的可选项目，B表单不会返回；</li><li>当用户选择好A表单后，会向后端请求一次新的页面信息，此时会下发A所有的可选项目以及A当前的选择，并且会下发B表单的可选项；</li><li>当用户选择B表单后，等待用户提交表单，此时完成一次表单的提交；</li></ul><p>经过上面这个过程，有没有发现什么问题？</p><p>页面联动的逻辑前端并不感知，依赖前端的每一次操作都会向后端请求，获取整个页面的最新状态；</p><p>由于引入了页面状态，会导致后端代码变得及其复杂，在实践过程中，前端的工作量整体上是有略微的降低，但后端的工作量会明显的提高；</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文主要用于分享前后端组件化架构设计的实践经验与思考。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="领域建模" scheme="http://simyy.cn/tags/%E9%A2%86%E5%9F%9F%E5%BB%BA%E6%A8%A1/"/>
    
      <category term="架构设计" scheme="http://simyy.cn/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    
  </entry>
  
  <entry>
    <title>如何设计组件化流程引擎？</title>
    <link href="http://simyy.cn/2022/01/15/simple-flow/"/>
    <id>http://simyy.cn/2022/01/15/simple-flow/</id>
    <published>2022-01-15T03:58:57.000Z</published>
    <updated>2022-09-04T04:34:21.169Z</updated>
    
    <content type="html"><![CDATA[<p>本文简单介绍下流程引擎以及实现一个最基础功能的组件化的流程引擎。</p><a id="more"></a><h4 id="核心问题"><a href="#核心问题" class="headerlink" title="核心问题"></a>核心问题</h4><p>什么是流程编排的核心要解决的问题?</p><p><code>流程可编排</code>是流程引擎的核心，是解决服务流程的核心；</p><p><code>流程定义清晰</code>是流程引擎的关键，只有清晰的流程才能保证编排的可维护性；</p><p><code>流程易于扩展</code>是流程引擎的核心竞争力，只有更好的扩展能力才能让流程的编排效率提升；</p><h4 id="绕不开的BPM规范"><a href="#绕不开的BPM规范" class="headerlink" title="绕不开的BPM规范"></a>绕不开的BPM规范</h4><p><code>BPM（Business Process Manager、业务流程管理）</code>主要用于管理复杂业务关系以及业务流程。</p><p><code>BPMN（Business Process Modeling Notation）</code>是BPM的一种建模语言，BMPN2.0是最新版本。</p><p>工作流引擎以<code>Activiti</code>为代表，使用BPMN2.0标准定义进行编排管理；</p><p>功能完善、相对重量级的产品如下：</p><ul><li><a href="https://github.com/kiegroup/jbpm" target="_blank" rel="noopener">https://github.com/kiegroup/jbpm</a></li><li><a href="https://github.com/Activiti/Activiti" target="_blank" rel="noopener">https://github.com/Activiti/Activiti</a></li><li><a href="https://github.com/Netflix/conductor" target="_blank" rel="noopener">https://github.com/Netflix/conductor</a></li><li><a href="https://github.com/flowable/flowable-engine" target="_blank" rel="noopener">https://github.com/flowable/flowable-engine</a></li><li><a href="https://camunda.com/" target="_blank" rel="noopener">https://camunda.com/</a></li></ul><p>功能基本可用，相对轻量级的产品如下：</p><ul><li><a href="https://github.com/alibaba/SmartEngine" target="_blank" rel="noopener">https://github.com/alibaba/SmartEngine</a></li><li><a href="https://github.com/dromara/liteflow" target="_blank" rel="noopener">https://github.com/dromara/liteflow</a></li></ul><h4 id="BMPN的可视化"><a href="#BMPN的可视化" class="headerlink" title="BMPN的可视化"></a>BMPN的可视化</h4><p>流程编排的可视化是降低编排复杂度的关键，除了传统的XML的编排方式，可视化的编排方式更加易于维护。</p><p>这里推荐使用开源的<code>bmpmn-js</code>来实现页面可视化。</p><p><a href="https://github.com/bpmn-io/bpmn-js" target="_blank" rel="noopener">https://github.com/bpmn-io/bpmn-js</a></p><p>如果需要对其功能点和扩展方式进行进一步的了解，可以在其使用实例中了解扩展的写法。</p><p><a href="https://github.com/bpmn-io/bpmn-js-examples" target="_blank" rel="noopener">https://github.com/bpmn-io/bpmn-js-examples</a></p><h4 id="如何设计组件化流程引擎？"><a href="#如何设计组件化流程引擎？" class="headerlink" title="如何设计组件化流程引擎？"></a>如何设计组件化流程引擎？</h4><blockquote><p>目标: 我们到底要解决什么问题?</p></blockquote><p>传统的工作流引擎常用于企业内部审批流、工作流等方面，注重业务流程的编排和扩展，</p><p>而此次要设计的流程引擎是关注代码逻辑流程的编排，</p><p>因此，不考虑使用繁琐的Activiti流程，而采用类似liteflow的方式来实现基础的流程编排功能；</p><p><img src="/images/simple-flow-1.jpeg" alt="类图"></p><p><strong>流程节点</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></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">IFlowNode</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">exec</span><span class="params">(C context)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>节点能力</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><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></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">IFlowRetryCapacity</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">int</span> <span class="title">retryTimes</span><span class="params">()</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IFlowRetryNode</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; <span class="keyword">extends</span> <span class="title">IFlowRetryCapacity</span>, <span class="title">IFlowNode</span>&lt;<span class="title">C</span>&gt; </span>&#123;</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IFlowRetry</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">retry</span><span class="params">(IFlowRetryNode retryNode, C context)</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="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractFlowRetry</span> <span class="keyword">implements</span> <span class="title">IFlowRetry</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">abstract</span> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doRetry</span><span class="params">(IFlowRetryNode retryNode, IFlowContext flowContext)</span></span>;</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">public</span> <span class="keyword">void</span> <span class="title">retry</span><span class="params">(IFlowRetryNode retryNode, IFlowContext flowContext)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (retryNode.retryTimes() &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        log.info(<span class="string">"FlowRetry start uniqueKey=&#123;&#125; retry=&#123;&#125;"</span>, flowContext.getUniqueKey(), retryNode.getClass().getSimpleName());</span><br><span class="line">        doRetry(retryNode, flowContext);</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">/** 基于spring的重试策略 **/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringFlowRetry</span> <span class="keyword">extends</span> <span class="title">AbstractFlowRetry</span> <span class="keyword">implements</span> <span class="title">IFlowRetry</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ConcurrentHashMap&lt;Integer, RetryTemplate&gt; retryMap = <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</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">public</span> <span class="keyword">void</span> <span class="title">doRetry</span><span class="params">(IFlowRetryNode retryNode, IFlowContext flowContext)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (retryNode.retryTimes() &lt;= <span class="number">0</span>) &#123;</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> (!retryMap.contains(retryNode.retryTimes())) &#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (<span class="keyword">this</span>) &#123;</span><br><span class="line">                RetryTemplate retryTemplate = RetryTemplate.builder()</span><br><span class="line">                        .maxAttempts(retryNode.retryTimes())</span><br><span class="line">                        .retryOn(Throwable.class)</span><br><span class="line">                        .build();</span><br><span class="line">                retryMap.put(retryNode.retryTimes(), retryTemplate);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        retryMap.get(retryNode.retryTimes()).execute((context) -&gt; &#123;</span><br><span class="line">            log.info(<span class="string">"SpringFlowRetry retry uniqueKey=&#123;&#125; retryNode=&#123;&#125; retryCount=&#123;&#125;"</span>, flowContext.getUniqueKey(),</span><br><span class="line">                    retryNode.getClass().getSimpleName(), context.getRetryCount());</span><br><span class="line">            retryNode.exec(flowContext);</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"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>流程链</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><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></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">FlowChain</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 流程节点的定义</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Class&lt;? extends IFlowNode&gt;&gt; flowNodeDefinition = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="comment">// 用于节点重试</span></span><br><span class="line">    <span class="keyword">private</span> IFlowRetry&lt;C&gt; flowRetry = <span class="keyword">new</span> SpringFlowRetry();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> FlowChain <span class="title">add</span><span class="params">(Class&lt;? extends IFlowNode&gt; definition)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.flowNodeDefinition.add(definition);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</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">isNull</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> CollectionUtils.isEmpty(flowNodeDefinition);</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">forEach</span><span class="params">(Function&lt;Class&lt;? extends IFlowNode&gt;, List&lt;IFlowNode&gt;&gt; function)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (isNull()) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        flowNodeDefinition.forEach(function::apply);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> List&lt;Class&lt;? extends IFlowNode&gt;&gt; getFlowNodes() &#123;</span><br><span class="line">        <span class="keyword">return</span> flowNodeDefinition;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> IFlowRetry&lt;C&gt; <span class="title">getFlowRetry</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> flowRetry;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>流程定义</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><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><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</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">IFlow</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function">FlowChain <span class="title">getFlowChain</span><span class="params">()</span></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="comment">     * <span class="doctag">@param</span> context</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">execute</span><span class="params">(C context)</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="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractFlowTemplate</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; <span class="keyword">extends</span> <span class="title">AbstractFlowScanner</span> <span class="keyword">implements</span> <span class="title">IFlow</span>&lt;<span class="title">C</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> FlowNodeSelector selector = <span class="keyword">new</span> FlowNodeSelector(</span><br><span class="line">            getDefaultImplements(),</span><br><span class="line">            getMutexExtensionImplements(),</span><br><span class="line">            getShareExtensionImplements());</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">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(C context)</span> </span>&#123;</span><br><span class="line">        FlowChain&lt;C&gt; flowChain = getFlowChain();</span><br><span class="line">        <span class="keyword">if</span> (flowChain == <span class="keyword">null</span> || flowChain.isNull()) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 流程链的顺序执行</span></span><br><span class="line">        <span class="keyword">for</span> (Object flowNode : flowChain.getFlowNodes()) &#123;</span><br><span class="line">            <span class="comment">// 流程的过滤，通过selector来选择当前上下文需要命中的流程节点</span></span><br><span class="line">            List&lt;IFlowNode&gt; flowNodes = selector.select((Class&lt;? extends IFlowNode&gt;) flowNode, context);</span><br><span class="line">            <span class="keyword">if</span> (CollectionUtils.isNotEmpty(flowNodes)) &#123;</span><br><span class="line">                flowNodes.forEach(bean -&gt; &#123;</span><br><span class="line">                    <span class="comment">// 支持流程节点的重试</span></span><br><span class="line">                    <span class="keyword">if</span> (bean <span class="keyword">instanceof</span> IFlowRetryNode &amp;&amp; ((IFlowRetryNode) bean).retryTimes() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                        flowChain.getFlowRetry().retry((IFlowRetryNode) bean, context);</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        bean.exec(context);</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><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> List&lt;Class&lt;? extends IFlowNode&gt;&gt; getFlowNodes() &#123;</span><br><span class="line">        <span class="keyword">return</span> getFlowChain() != <span class="keyword">null</span> ? getFlowChain().getFlowNodes() : <span class="keyword">null</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">/** 流程扫描 **/</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractFlowScanner</span> <span class="keyword">implements</span> <span class="title">ApplicationContextAware</span>, <span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ApplicationContext applicationContext;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Class&lt;? extends IFlowNode&gt;&gt; flowNodes;</span><br><span class="line">    <span class="comment">// 默认流程节点</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends IFlowNode&gt; , List&lt;IFlowNode&gt;&gt; defaultImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    <span class="comment">// 互斥流程节点</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends IFlowNode&gt; , List&lt;IFlowNode&gt;&gt; mutexExtensionImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    <span class="comment">// 共享流程节点</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends IFlowNode&gt; , List&lt;IFlowNode&gt;&gt; shareExtensionImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    <span class="comment">// 支持优先级</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;Class&lt;? extends IFlowNode&gt; , PriorityQueue&lt;PriorityFlowNode&gt;&gt; sharePriorityExtensionImplements = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    <span class="keyword">private</span> Comparator&lt;PriorityFlowNode&gt; comparator = (o1, o2) -&gt; o2.priority - o1.priority;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> List&lt;Class&lt;? extends IFlowNode&gt;&gt; getFlowNodes();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Map&lt;Class&lt;? extends IFlowNode&gt;, List&lt;IFlowNode&gt;&gt; getDefaultImplements() &#123;</span><br><span class="line">        <span class="keyword">return</span> defaultImplements;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Map&lt;Class&lt;? extends IFlowNode&gt;, List&lt;IFlowNode&gt;&gt; getMutexExtensionImplements() &#123;</span><br><span class="line">        <span class="keyword">return</span> mutexExtensionImplements;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Map&lt;Class&lt;? extends IFlowNode&gt;, List&lt;IFlowNode&gt;&gt; getShareExtensionImplements() &#123;</span><br><span class="line">        <span class="keyword">return</span> shareExtensionImplements;</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">synchronized</span> <span class="keyword">void</span> <span class="title">initFlowNodeImplements</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        flowNodes = getFlowNodes();</span><br><span class="line">        <span class="keyword">if</span> (CollectionUtils.isEmpty(flowNodes)) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        Map&lt;String, IFlowNode&gt; beans = applicationContext.getBeansOfType(IFlowNode.class);</span><br><span class="line">        <span class="keyword">if</span> (MapUtils.isEmpty(beans)) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        beans.values().stream().forEach(bean -&gt; &#123;</span><br><span class="line">            <span class="keyword">for</span> (Class&lt;? extends IFlowNode&gt; flowNode : flowNodes) &#123;</span><br><span class="line">                <span class="keyword">if</span> (flowNode.isInstance(bean)) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (bean.getClass().getAnnotation(DefaultFlowNode.class) != <span class="keyword">null</span>) &#123;</span><br><span class="line">                        registerFlowNode(defaultImplements, bean, flowNode);</span><br><span class="line">                        log.info(<span class="string">"AbstractFlowBuilder register default=&#123;&#125;"</span>, bean.getClass().getSimpleName());</span><br><span class="line">                    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (bean.getClass().getAnnotation(ExtensionFlowNode.class) != <span class="keyword">null</span>) &#123;</span><br><span class="line">                        ExtensionFlowNode extensionFlowNode = bean.getClass().getAnnotation(ExtensionFlowNode.class);</span><br><span class="line"><span class="comment">//                        registerFlowNode(FlowExtensionType.MUTEX.equals(extensionFlowNode.type())</span></span><br><span class="line"><span class="comment">//                                ? mutexExtensionImplements : shareExtensionImplements, bean, flowNode);</span></span><br><span class="line">                        <span class="keyword">if</span> (FlowExtensionType.MUTEX.equals(extensionFlowNode.type())) &#123;</span><br><span class="line">                            registerFlowNode(mutexExtensionImplements, bean, flowNode);</span><br><span class="line">                        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                            registerPriorityFlowNode(bean, flowNode, extensionFlowNode.priority());</span><br><span class="line">                        &#125;</span><br><span class="line">                        log.info(<span class="string">"AbstractFlowBuilder register extension=&#123;&#125;"</span>, bean.getClass().getSimpleName());</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        log.info(<span class="string">"AbstractFlowBuilder register ignore &#123;&#125;"</span>, bean.getClass().getSimpleName());</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><span class="line"></span><br><span class="line">        refreshShareExtensionImplements();</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">refreshShareExtensionImplements</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// priority 转 list</span></span><br><span class="line">        <span class="keyword">for</span> (Class&lt;? extends IFlowNode&gt; flowNode : sharePriorityExtensionImplements.keySet()) &#123;</span><br><span class="line">            PriorityQueue&lt;PriorityFlowNode&gt; priorityQueue = sharePriorityExtensionImplements.get(flowNode);</span><br><span class="line">            <span class="keyword">if</span> (priorityQueue != <span class="keyword">null</span>) &#123;</span><br><span class="line">                List&lt;IFlowNode&gt; list = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">                <span class="keyword">while</span> (!priorityQueue.isEmpty()) &#123;</span><br><span class="line">                    list.add(priorityQueue.poll().getFlowNode());</span><br><span class="line">                &#125;</span><br><span class="line">                shareExtensionImplements.put(flowNode, list);</span><br><span class="line">            &#125;</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">registerFlowNode</span><span class="params">(Map&lt;Class&lt;? extends IFlowNode&gt; , List&lt;IFlowNode&gt;&gt; store, IFlowNode bean, Class&lt;? extends IFlowNode&gt; definition)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!store.containsKey(definition)) &#123;</span><br><span class="line">            store.put(definition, <span class="keyword">new</span> ArrayList&lt;&gt;());</span><br><span class="line">        &#125;</span><br><span class="line">        store.get(definition).add(bean);</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">registerPriorityFlowNode</span><span class="params">(IFlowNode bean, Class&lt;? extends IFlowNode&gt; definition, <span class="keyword">int</span> priority)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!sharePriorityExtensionImplements.containsKey(definition)) &#123;</span><br><span class="line">            sharePriorityExtensionImplements.put(definition, <span class="keyword">new</span> PriorityQueue&lt;&gt;(comparator));</span><br><span class="line">        &#125;</span><br><span class="line">        sharePriorityExtensionImplements.get(definition).add(<span class="keyword">new</span> PriorityFlowNode(bean, priority));</span><br><span class="line">    &#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">public</span> <span class="keyword">void</span> <span class="title">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        initFlowNodeImplements();</span><br><span class="line">    &#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">public</span> <span class="keyword">void</span> <span class="title">setApplicationContext</span><span class="params">(ApplicationContext applicationContext)</span> <span class="keyword">throws</span> BeansException </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.applicationContext = applicationContext;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">PriorityFlowNode</span> </span>&#123;</span><br><span class="line">        <span class="keyword">private</span> IFlowNode flowNode;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">int</span> priority = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">PriorityFlowNode</span><span class="params">(IFlowNode flowNode, <span class="keyword">int</span> priority)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.flowNode = flowNode;</span><br><span class="line">            <span class="keyword">this</span>.priority = priority;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>流程扩展</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><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="comment">/** 扩展类型 **/</span></span><br><span class="line">ffpublic <span class="keyword">enum</span> FlowExtensionType &#123;</span><br><span class="line">    MUTEX, <span class="comment">// 互斥、不同的扩展会相互隔离</span></span><br><span class="line">    SHARE, <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="meta">@Component</span></span><br><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> DefaultFlowNode &#123;</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="meta">@Component</span></span><br><span class="line"><span class="meta">@Target</span>(ElementType.TYPE)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> ExtensionFlowNode &#123;</span><br><span class="line">    <span class="function">FlowExtensionType <span class="title">type</span><span class="params">()</span> <span class="keyword">default</span> FlowExtensionType.SHARE</span>;</span><br><span class="line">    <span class="function"><span class="keyword">int</span> <span class="title">priority</span><span class="params">()</span> <span class="keyword">default</span> 1</span>; <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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IMatcher</span>&lt;<span class="title">C</span> <span class="keyword">extends</span> <span class="title">IFlowContext</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">match</span><span class="params">(C context)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="待完善的功能点"><a href="#待完善的功能点" class="headerlink" title="待完善的功能点"></a>待完善的功能点</h5><ul><li>流程分叉（流程编排过程中存在if-else的问题）</li><li>流程持久化</li><li>流程重试</li><li>xml编排</li><li>bpmn可视化编排</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文简单介绍下流程引擎以及实现一个最基础功能的组件化的流程引擎。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="架构" scheme="http://simyy.cn/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="流程引擎" scheme="http://simyy.cn/tags/%E6%B5%81%E7%A8%8B%E5%BC%95%E6%93%8E/"/>
    
  </entry>
  
  <entry>
    <title>Kafka Replication</title>
    <link href="http://simyy.cn/2020/09/01/kafka-replication/"/>
    <id>http://simyy.cn/2020/09/01/kafka-replication/</id>
    <published>2020-09-01T11:22:03.000Z</published>
    <updated>2020-09-01T11:22:46.000Z</updated>
    
    <content type="html"><![CDATA[<p>记录与翻译Kafka多副本的设计原理。</p><a id="more"></a><h3 id="Kafka-Replication"><a href="#Kafka-Replication" class="headerlink" title="Kafka Replication"></a>Kafka Replication</h3><blockquote><p><a href="https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Replication" target="_blank" rel="noopener">Kafka Replication</a></p></blockquote><p>Kafka通过数据写入的多个副本来实现数据的持久化与高可用。</p><p>为了保证消息的成功投递，可以把消息投递的整个生命周期分为三部分：</p><p>① <code>生产消息</code>：消息从Producer传递到Broker；</p><p>② <code>存储消息</code>：消息写入到Broker的磁盘；</p><p>③ <code>消费消息</code>：消息从Broker传递到Comsumer。</p><p><code>存储消息</code>是数据持久化范畴，而<code>如何保证消息不丢</code>是多副本设计的初衷。</p><p><strong>设计目标</strong></p><p>考虑到Kafka不同场景的应用，设计目标包括：</p><p>① <code>可配置的持久化策略</code>：高吞吐量/低时延，低吞吐量/高时延（通过调整备份副本的个数）；</p><p>② <code>自动的副本备份策略</code>：自动调整备份，自动化的集群扩容能力。</p><blockquote><p><code>强一致</code>需要保证数据写入多个备份，从而导致写入时延的增加；<code>弱一致</code>不需要保证备份写入，会导致数据存在软状态，从而获得更好的性能。</p></blockquote><p><strong>解决问题</strong></p><p>多副本场景下，存在两个关键问题：</p><p>① <code>副本分片策略</code>：副本该如何保存与分配到不同的Broker；</p><p>② <code>消息广播策略</code>：消息该如何广播到所有的副本。</p><h4 id="Data-Replication"><a href="#Data-Replication" class="headerlink" title="Data Replication"></a>Data Replication</h4><p>多副本的数据复制可以分为<code>同步复制</code>与<code>异步复制</code>。</p><p>无论那种复制策略都需要考虑以下问题：</p><p>① 如何实现<code>数据广播</code>；</p><p>② 如何权衡<code>数据复制的份数</code>；</p><p>③ <code>副本宕机的异常处理</code>；</p><p>④ <code>副本宕机恢复数据</code>的一致性如何保证。</p><hr><p>常见的复制策略分为两种：<code>主备复制</code>与<code>选举复制</code>。</p><p><strong>Primary-Backup Replication</strong></p><p><code>主备复制</code>的场景下，主节点在写入数据完成后会<code>阻塞等待所有从节点</code>的写入完成。</p><p>在从节点写入的过程中，如果发生故障（宕机、断网、超时）就需要主节点来协调管理<code>可用的从节点列表</code>。</p><p>对于故障恢复后的从节点，需要从主节点拉取最新的数据才能重新加入集群。</p><blockquote><p>如果副本数量为N，则主备策略最多可以容忍N-1个节点异常。</p></blockquote><p><strong>Quorum-Based Replication</strong></p><p><code>选举复制</code>的场景下，主节点在写入数据完成后会<code>阻塞等待多数从节点</code>的写入完成。</p><blockquote><p>选举的特点在于不需要等待所有从节点的写入。</p><p>考虑到集群数量是预设的，由于半数以上的写入策略，从而最大的异常容忍度是可知的。</p><p>如果副本数量为2N+1，则选举策略最多可以容忍N个节点异常。</p></blockquote><hr><p>那么，两种策略如何权衡？</p><p>主要从<code>时延性能</code>、<code>可用性</code>两方面考虑，</p><p>① 选举复制只需要写入大部分节点就可返回，因此选举复制的时延低；</p><p>② 主备复制可以容忍更多数量的异常节点；</p><p>③ 选举复制至少最多容忍半数的异常节点。</p><p>考虑到，副本数量会直接影响到磁盘的有效使用率，<code>Kafka选择主备来尽量减少磁盘空间的浪费</code>。</p><h4 id="Synchronous-Replication"><a href="#Synchronous-Replication" class="headerlink" title="Synchronous Replication"></a>Synchronous Replication</h4><p>Kafka使用<code>主备</code>作为同步复制策略。</p><p>主节点维护一个<code>同步副本的列表（In-Sync Replicas（ISR））</code>，用于<code>记录与主节点保持同步的从节点</code>。</p><p>为了维护多个副本的可用性，Kafka使用<code>Zookeeper</code>来保存<code>Leader节点</code>与<code>ISR</code>。</p><hr><p>数据存储分为两部分：<code>本地日志</code>与<code>偏移量</code>。</p><p>其中，<code>LEO（The Log End Offset）</code>记录当前日志的最新偏移量，<code>HW（High Watermark）</code>记录最后一次Commited的偏移量。</p><blockquote><p>LEO是用来标记日志同步的进度，而HW是用来标记Commited的进度。</p></blockquote><h5 id="Writes"><a href="#Writes" class="headerlink" title="Writes"></a>Writes</h5><p>Client通过Zookeeper获取目标分区最新的Leader节点。</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">       Zookeeper</span><br><span class="line">      /</span><br><span class="line">Client ----&gt; Broker-Partition(Leader) ----&gt; Broker-Partition (Follower)</span><br></pre></td></tr></table></figure><p>副本之间的通信采用Socket直连的方式来提高性能。</p><p>主节点在接收到数据后会优先写入本地磁盘（日志），然后同步写入多个副本。</p><blockquote><p>主节点（Partition）的写入是有序的，但不同主节点之间写入的顺序是无序的。</p></blockquote><p>消息的写入判断条件是<code>ISR列表中所有从节点均写入完成</code>。</p><blockquote><p>为了提高副本的写入效率，仅保证消息写入到副本节点的内存（不保证持久化到磁盘）。</p></blockquote><p>通过提供<code>可配置化的Flush策略</code>来提供不同的应用场景：<code>同步刷盘</code>与<code>异步刷盘</code>。</p><h5 id="Reads"><a href="#Reads" class="headerlink" title="Reads"></a>Reads</h5><p>读操作仅从Leader主节点读取数据，而主节点是根据Zookeeper来获取的。</p><h4 id="Failure-Scenarios"><a href="#Failure-Scenarios" class="headerlink" title="Failure Scenarios"></a>Failure Scenarios</h4><p><strong>Follower Failure</strong></p><p>从节点异常情况下，会被主节点从ISR列表中移除，直到从节点恢复。</p><p>从节点的恢复不仅需要机器恢复正常，也需要数据恢复到同步状态。</p><p>因此，<code>ISR列表是Kafka实现主备自动切换的关键</code>。</p><p><strong>Leader Failure</strong></p><p>主节点异常的三种场景：</p><p>① 未写入本地日志（无影响）；</p><p>② 写入本地日志，未写入从节点；</p><p>③ 写入本地日志，写入部分从节点；</p><p>由于第①种场景中没有持有化数据，因此不会存在主从切换数据不一致的问题。</p><p>为了实现主从切换后数据的一致性，被选举为Leader的节点一定拥有最新的数据，</p><p>Kafka的主备切换主节点的选举策略如下：</p><p>① Zookeeper中注册ISR信息，用来保证元数据的一致性；</p><p>② 基于Zookeeper的临时节点来选举主节点，用来保证Leader的唯一性；</p><p>③ 利用Zookeeper的Watcher功能实现自动的选举功能；</p><p>④ Leader需要保证ISR中节点的同步进度，即时移除超时等异常的从节点。</p><h4 id="Asynchronous-Replication"><a href="#Asynchronous-Replication" class="headerlink" title="Asynchronous Replication"></a>Asynchronous Replication</h4><p>异步复制是数据写入主节点后直接返回给Client，通过异步的方式同步数据到从节点。</p><p>因此，异步复制不能保证数据的持久化，当发生主从切换时可能造成数据的不一致（重复消息、丢消息）。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;记录与翻译Kafka多副本的设计原理。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="消息队列" scheme="http://simyy.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
      <category term="Kafka" scheme="http://simyy.cn/tags/Kafka/"/>
    
      <category term="Paper" scheme="http://simyy.cn/tags/Paper/"/>
    
  </entry>
  
  <entry>
    <title>Kafka论文笔记</title>
    <link href="http://simyy.cn/2020/08/23/kafka-paper/"/>
    <id>http://simyy.cn/2020/08/23/kafka-paper/</id>
    <published>2020-08-23T07:39:23.000Z</published>
    <updated>2020-08-23T07:51:02.000Z</updated>
    
    <content type="html"><![CDATA[<p>记录Kafka论文内容。</p><a id="more"></a><blockquote><p><a href="http://notes.stephenholiday.com/Kafka.pdf" target="_blank" rel="noopener">Kafka: a Distributed Messaging System for Log Processing</a></p></blockquote><p><code>Kafka</code>是一个<code>高吞吐量</code>、<code>低延时</code>、<code>可伸缩</code>的<code>分布式</code>消息系统。</p><h3 id="Other-System"><a href="#Other-System" class="headerlink" title="Other System"></a>Other System</h3><p>传统的消息系统一般作为<code>消息总线</code>，用于处理异步的数据流。</p><p>然而，这种消息总线的处理方式并不适合当下的<code>日志处理</code>的场景，究其原因在于：</p><blockquote><p>① 传统的消息系统关注于<code>消息投递</code>，大量的日志会导致系统的过载。</p></blockquote><p>在分布式场景下，消息投递的保证是通过<code>Sync + ACK</code>的方式来实现的。</p><p>因此，在传统消息系统中，消息投递过程是<code>同步</code>的，无法支持大量日志流的场景。</p><blockquote><p>② 传统的消息系统大多都不是以<code>吞吐量</code>为作为首要目标，使用<code>TCP</code>作为消息投递的通信方式。</p></blockquote><p><code>TCP</code>是可靠的通信协议，并不适用于<code>高吞吐量</code>为目标的消息通信方式。</p><blockquote><p>③ 传统的消息系统<code>缺少分布式的支持</code>。</p></blockquote><p>对于海量的日志消息，单机必然无法满足，<code>多机的环境下的分区存储</code>是不可避免的。</p><blockquote><p>④ 传统的消息系统是建立在<code>消息即时消费的假设</code>之上，从而缺少对于大量消息持久化存储的能力。</p></blockquote><p>对于海量的日志消息，消息的消费不一定是即时的，从而可能导致大量的消息堆积，</p><p>这也就是第③点的问题，如何通过<code>分区</code>来支持<code>海量消息的堆积</code>问题。</p><hr><p>近几年海量日志处理的场景下产生一些日志聚合工具。</p><p><code>Scribe</code>是Facebook用于收集日志数据的中间件，它基于Socket来收集日志数据，并通过日志聚合定期写入HDFS。</p><p>也就是说，Scribe是用于解决日志写入HDFS性能不足的一种手段。</p><p><code>Flume</code>是Cloudera开发的日志聚合中间件，支持<code>pipes</code>与<code>sinks</code>的灵活扩展以及<code>分布式</code>的支持。</p><p><code>HedWig</code>是Yahoo开发的一个分布式的<code>发布订阅</code>系统，主要用于记录消息的消费记录。</p><h3 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h3><p>Kafka引入了<code>Broker</code>用于消息的存储。</p><p>为了实现海量数据的存储，Kafka集群一般包含多个Broker。</p><p>为了实现负载均衡，每一个<code>Topic</code>会被分为多个<code>Partition</code>，每一个<code>Broker</code>会存储一个或多个<code>Partion</code>。</p><p><img src="/images/kafka_paper_1.png" alt=""></p><h4 id="Partition"><a href="#Partition" class="headerlink" title="Partition"></a>Partition</h4><p><code>Partition</code>实际上是一个逻辑日志，它由于含多个<code>Segment</code>（物理日志）组成，每个<code>Segment</code>的大小约为<code>1GB</code>。</p><p>每当一个生产者发布一条消息到某个<code>Partition</code>时，该条消息会追到到该<code>Partition</code>的最后一个<code>Segment</code>日志中。</p><blockquote><p>为了实现高性能，Kafka先内存缓存消息再flush磁盘，刷盘策略有：</p><p>① 内存缓存到指定数量的消息后自动flush磁盘；</p><p>② 固定时间阈值自动flush磁盘。</p></blockquote><p>在Kafka中，消息没有分配一个唯一固定的消息ID，而是采用一个基于日志的逻辑的偏移量来定位消息。</p><blockquote><p>偏移量的使用可以避免磁盘的随机查找，通过偏移量可以准确的确定消息的位置。</p></blockquote><p>对于消息的顺序写入，<code>消息的定位</code>不仅仅要记录消息的<code>偏移量（Offset）</code>，还要记录消息的<code>长度（Length）</code>。</p><p><code>偏移量（Offset）</code>虽然是不连续的（记录消息的起始位置），但可以保证单调递增。</p><p><img src="/images/kafka_paper_2.png" alt=""></p><p>Kafka使用在内存维护了一个<code>Offset的内存索引</code>，记录了<code>每个Segment内第一个消息的偏移量</code>。</p><p><strong>高效传输</strong></p><p>为了实现高效的数据传输，Kafka的设计如下，</p><p>① <code>消息的批量发送与拉取</code>主要用于减少请求次数；</p><p>② 直接使用<code>PageCache</code>来避免的内存缓存；</p><p>③ 没有使用内存，因此避免了不必要的<code>GC</code>；</p><p>④ 使用<code>sendfile</code>减少内存拷贝。</p><p><strong>无状态的Broker</strong></p><blockquote><p>Broker是<code>无状态</code>的，其仅仅作为消息的存储，并不会保留任何Consumer的消费情况。</p></blockquote><p>Broker的无状态特性可以降低消息系统的复杂性。</p><p>由于Broker的无状态，无法感知消息消费的情况，也就无法即时清除，因此一般采用定期清除策略来清除历史消息。</p><p>此外，基于Broker的无状态，Consumer可以实现<code>消费的回退以及重试</code>。</p><h4 id="Distributed-Coordination"><a href="#Distributed-Coordination" class="headerlink" title="Distributed Coordination"></a>Distributed Coordination</h4><p><strong>路由</strong></p><p>消息投递到Partition的<code>路由策略</code>有：</p><p>① 随机选择：基于Random的随机选择；</p><p>② 哈希路由：基于<code>PartitionKey</code>的路由策略；</p><p><strong>并行</strong></p><blockquote><p><code>Kafka使用Partition作为最小的并行处理单元</code>。</p></blockquote><p>一个Partition仅能被一个Consumer消费，因此，Partition的数量要多于Consumer的数量。</p><p><strong>协作</strong></p><p>为了实现多个无状态的Broker之间的协作，引入了<code>Zookeeper</code>。</p><p>① Broker的<code>注册与监控</code>；</p><p>② 基于Zookeeper的监控来做<code>rebalance</code>；</p><p>③ 维护Consumer的<code>消费进度（Offert）</code>。</p><p>Broker/Consumer的注册是动态的（存在扩容与缩容的情况），使用ZK的<code>临时节点</code>。</p><p>由于Broker是无状态的，因此Consumer的Offset的维护使用ZK的<code>持久化节点</code>。</p><p><strong>rebalance</strong></p><p>当监听到ZK中Broker/Consumer的注册信息有变化时，对应的Consumer就会发起<code>rebalance</code>。</p><p>rebalance是按照<code>group</code>来分组的，每个<code>group</code>维护着自己的Offset。</p><p>第一个监听到变化的Consumer会释放所有Partition再进行rebalance。</p><blockquote><p>Partition的占用必然是通过ZK实现的。</p></blockquote><h4 id="Delivery-Guarantees"><a href="#Delivery-Guarantees" class="headerlink" title="Delivery Guarantees"></a>Delivery Guarantees</h4><p>Kafka仅支持<code>至少一次</code>的消息投递。</p><blockquote><p><code>仅一次</code>的消息投递需要<code>两阶段提交（2PC）</code>的支持，这与高吞吐量的目标不相符。</p></blockquote><p>由于Partition是最小的并行处理单元，消息在Partition内部是有序的，</p><p>因此，通过PartitionKey的路由策略可以保证一定的顺序消费。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>本文仅是Kafka论文的记录，其内容与目前Kafka版本的实现可能有些不同。</p><p>本文还缺少对于<code>Replication</code>的介绍，会在后面的文章中有分析介绍。</p><p>对于Broker的无状态特性，Kafka后期并没有完全维持，例如，Consumer的Offset就从Zookeeper中迁移至Broker中。</p><p>Kafka作为独立的中间件，需要Zookeeper作为协调者来说有些太重了，因此社区正打算干掉ZK，例如，<a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum" target="_blank" rel="noopener">KIP-500</a>打算把元数据都利用TOPIC来存储。</p><p>此外，随着Kafka的应用越来越广泛，消息投递的语义也不仅仅支持<code>至少一次</code>这种场景了，此时就需要考虑<code>CAP</code>理论了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;记录Kafka论文内容。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="消息队列" scheme="http://simyy.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
      <category term="Kafka" scheme="http://simyy.cn/tags/Kafka/"/>
    
      <category term="Paper" scheme="http://simyy.cn/tags/Paper/"/>
    
  </entry>
  
  <entry>
    <title>Redis 跳跃表</title>
    <link href="http://simyy.cn/2020/08/08/redis-skiplist/"/>
    <id>http://simyy.cn/2020/08/08/redis-skiplist/</id>
    <published>2020-08-08T13:30:00.000Z</published>
    <updated>2020-08-08T14:43:38.000Z</updated>
    
    <content type="html"><![CDATA[<p>时隔四年，补课。</p><a id="more"></a><h3 id="Skip-List"><a href="#Skip-List" class="headerlink" title="Skip List"></a>Skip List</h3><blockquote><p>关键词：链表、多级索引、动态结构</p></blockquote><p>跳表是基于链表建立多级索引的动态数据结构。</p><p>在链表的基础之上，跳表利用空间换时间的方法，把单链表的时间复杂度从<code>O(n)</code>降低到跳表的<code>O(log(n))</code>。</p><p>在跳表索引的构建上，需要维护索引大小平衡性，避免退化为单链表。</p><h3 id="Source-Code"><a href="#Source-Code" class="headerlink" title="Source Code"></a>Source Code</h3><p>Redis中<code>ZSET</code>也被称为<code>有序集合（Sorted Set）</code>，是通过跳表来实现的。</p><p>Redis为什么使用跳表来实现ZSET？</p><p>① Redis并不是仅使用跳表来实现；</p><blockquote><p>在数据量比较少时使用ziplist来实现，当数据量比较多的时候才会采用skiplist实现；</p></blockquote><p>② 基于双向链表的跳表很容易实现<code>ZRANGE</code>与<code>ZRERANGE</code>，而红黑树等树形结构就没这么容易；</p><p>③ 跳表节点的插入与删除的复杂度更低，而红黑树等平衡树都需要旋转平衡操作。</p><blockquote><p>在高并发场景下跳表性能更加，不过针对Redis来说单线程的数据变更来说红黑树的平衡旋转应该不是瓶颈。</p></blockquote><p>下面将简单介绍下redis跳表的<code>结构</code>、<code>节点插入</code>、<code>节点排序</code>以及<code>范围检索</code>。</p><h4 id="zskiplist"><a href="#zskiplist" class="headerlink" title="zskiplist"></a>zskiplist</h4><p><img src="/images/redis-skiplist-1.png" alt=""></p><p>ZSET是通过两部分组成：<code>KV字典（dict）</code>与<code>跳表索引（zskiplist）</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* ZSET结构体 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zset</span> &#123;</span></span><br><span class="line">    dict *dict;          <span class="comment">// KV字典</span></span><br><span class="line">    zskiplist *zsl;      <span class="comment">// 跳表索引</span></span><br><span class="line">&#125; zset;</span><br></pre></td></tr></table></figure><p>其中，<code>KV字典（dict）</code>是基于哈希的数据存储的基础结构，<code>跳表索引（zskiplist）</code>是基于跳表的数据遍历搜索结构。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 跳表节点 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> &#123;</span></span><br><span class="line">    sds ele;                          <span class="comment">// 具体元素</span></span><br><span class="line">    <span class="keyword">double</span> score;                     <span class="comment">// 用于排序的分值</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">backward</span>;</span>     <span class="comment">// 向后的指针</span></span><br><span class="line">    <span class="comment">/* 节点层次 */</span> </span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistLevel</span> &#123;</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">forward</span>;</span>  <span class="comment">// 向前的指针</span></span><br><span class="line">        <span class="keyword">unsigned</span> <span class="keyword">long</span> span;            <span class="comment">// 当前节点与forward节点之间的节点数</span></span><br><span class="line">    &#125; level[];</span><br><span class="line">&#125; zskiplistNode;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 跳表结构体 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplist</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">header</span>, *<span class="title">tail</span>;</span>  <span class="comment">// 跳表的头节点与尾节点</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> length;               <span class="comment">// 跳表的长度</span></span><br><span class="line">    <span class="keyword">int</span> level;                         <span class="comment">// 跳表的层次数</span></span><br><span class="line">&#125; zskiplist;</span><br></pre></td></tr></table></figure><p><img src="/images/redis-skiplist-2.png" alt=""></p><h4 id="zslGetRank"><a href="#zslGetRank" class="headerlink" title="zslGetRank"></a>zslGetRank</h4><p><code>zslGetRank</code>是跳表中用于获取指定元素排名的功能。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 获取排名 */</span></span><br><span class="line"><span class="function"><span class="keyword">unsigned</span> <span class="keyword">long</span> <span class="title">zslGetRank</span><span class="params">(zskiplist *zsl, <span class="keyword">double</span> score, sds ele)</span> </span>&#123;</span><br><span class="line">    zskiplistNode *x;</span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> rank = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">int</span> i;</span><br><span class="line">    <span class="comment">/* 从跳表的最高层开始遍历 */</span></span><br><span class="line">    x = zsl-&gt;header;</span><br><span class="line">    <span class="keyword">for</span> (i = zsl-&gt;level<span class="number">-1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">        <span class="comment">/* 找到当前层级中与目标值最接近的位置 */</span></span><br><span class="line">        <span class="keyword">while</span> (x-&gt;level[i].forward &amp;&amp;</span><br><span class="line">             <span class="comment">// 当前层有forward的score小于目标值 或 当前层有forward的ele小于目标值</span></span><br><span class="line">            (x-&gt;level[i].forward-&gt;score &lt; score ||</span><br><span class="line">                (x-&gt;level[i].forward-&gt;score == score &amp;&amp;</span><br><span class="line">                sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt;= <span class="number">0</span>))) &#123;</span><br><span class="line">            rank += x-&gt;level[i].span;</span><br><span class="line">            x = x-&gt;level[i].forward;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/* x might be equal to zsl-&gt;header, so test if obj is non-NULL */</span></span><br><span class="line">        <span class="keyword">if</span> (x-&gt;ele &amp;&amp; sdscmp(x-&gt;ele,ele) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> rank;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="zslInsert"><a href="#zslInsert" class="headerlink" title="zslInsert"></a>zslInsert</h4><p><code>zslInsert</code>是跳表的插入逻辑。</p><p>插入流程大体如下：</p><ul><li><code>位置搜索</code>：从跳表的最高层到最底层开始遍历，记录每层的跳跃节点以及移动位置；</li><li><code>构造新节点</code>：随机生成一个level用于构造带插入节点；</li><li><code>初始化新增层级</code>：如果level大于原有最大层级，则需要对新增的层级初始化；</li><li><code>节点加入到跳跃表</code>：把待插入节点的层级节点加入到跳表层级的每一层，并重新计算span；</li><li><code>更新跳表指针</code>：更新节点的backward。</li></ul><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><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="comment">/* 插入数据 */</span></span><br><span class="line"><span class="function">zskiplistNode *<span class="title">zslInsert</span><span class="params">(zskiplist *zsl, <span class="keyword">double</span> score, sds ele)</span> </span>&#123;</span><br><span class="line">    zskiplistNode *update[ZSKIPLIST_MAXLEVEL]; <span class="comment">// 记录每层插入节点的跳跃位置</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">int</span> rank[ZSKIPLIST_MAXLEVEL];    <span class="comment">// 记录到达每层跳跃节点的移动位置</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">/* 从跳表的最高层开始遍历 */</span></span><br><span class="line">    x = zsl-&gt;header;</span><br><span class="line">    <span class="keyword">for</span> (i = zsl-&gt;level<span class="number">-1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">        <span class="comment">/* 初始化rank，如果是最高层则为0，否则为上一层的值 */</span></span><br><span class="line">        rank[i] = i == (zsl-&gt;level<span class="number">-1</span>) ? <span class="number">0</span> : rank[i+<span class="number">1</span>];</span><br><span class="line">        <span class="comment">/* 找到当前层级中与目标值最接近的位置 */</span></span><br><span class="line">        <span class="keyword">while</span> (x-&gt;level[i].forward &amp;&amp;  </span><br><span class="line">                <span class="comment">// 当前层有forward的score小于目标值 或 当前层有forward的ele小于目标值</span></span><br><span class="line">                (x-&gt;level[i].forward-&gt;score &lt; score ||</span><br><span class="line">                    (x-&gt;level[i].forward-&gt;score == score </span><br><span class="line">                    &amp;&amp; sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; <span class="number">0</span>)))</span><br><span class="line">        &#123;</span><br><span class="line">            rank[i] += x-&gt;level[i].span;    <span class="comment">// 更新每层的偏移动位置</span></span><br><span class="line">            x = x-&gt;level[i].forward;       <span class="comment">// 更新跳跃节点</span></span><br><span class="line">        &#125;</span><br><span class="line">        update[i] = x;                    <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">    level = zslRandomLevel();</span><br><span class="line">    <span class="keyword">if</span> (level &gt; zsl-&gt;level) &#123;</span><br><span class="line">        <span class="keyword">for</span> (i = zsl-&gt;level; i &lt; level; i++) &#123;</span><br><span class="line">            <span class="comment">/* 初始化移动位置以及跳跃位置 */</span></span><br><span class="line">            rank[i] = <span class="number">0</span>;</span><br><span class="line">            update[i] = zsl-&gt;header;</span><br><span class="line">            update[i]-&gt;level[i].span = zsl-&gt;length;</span><br><span class="line">        &#125;</span><br><span class="line">        zsl-&gt;level = level; <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">    x = zslCreateNode(level,score,ele);</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; level; i++) &#123;</span><br><span class="line">        <span class="comment">/* 链表插入（利用update记录的每层插入节点） */</span></span><br><span class="line">        x-&gt;level[i].forward = update[i]-&gt;level[i].forward;</span><br><span class="line">        update[i]-&gt;level[i].forward = x;</span><br><span class="line">        <span class="comment">/* span计算（利用rank记录的每层移动位置（从第i层到第0层）） */</span></span><br><span class="line">        x-&gt;level[i].span = update[i]-&gt;level[i].span - (rank[<span class="number">0</span>] - rank[i]);</span><br><span class="line">        update[i]-&gt;level[i].span = (rank[<span class="number">0</span>] - rank[i]) + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 大于当前层级的span都需要span+1 */</span></span><br><span class="line">    <span class="keyword">for</span> (i = level; i &lt; zsl-&gt;level; i++) &#123;</span><br><span class="line">        update[i]-&gt;level[i].span++;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 更新新增节点的前向指针，如果是链表头节点则忽略，否则更新为0层节点 */</span></span><br><span class="line">    x-&gt;backward = (update[<span class="number">0</span>] == zsl-&gt;header) ? <span class="literal">NULL</span> : update[<span class="number">0</span>];</span><br><span class="line">    <span class="comment">/* 第0层为正向指针，节点本身带有的反向指针 */</span></span><br><span class="line">    <span class="keyword">if</span> (x-&gt;level[<span class="number">0</span>].forward)</span><br><span class="line">        x-&gt;level[<span class="number">0</span>].forward-&gt;backward = x;</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        zsl-&gt;tail = x;</span><br><span class="line">    zsl-&gt;length++;</span><br><span class="line">    <span class="keyword">return</span> x;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="zrangeGenericCommand"><a href="#zrangeGenericCommand" class="headerlink" title="zrangeGenericCommand"></a>zrangeGenericCommand</h4><p><code>zrangeGenericCommand</code>是跳表的范围查询逻辑。</p><p>范围查询的流程大体如下：</p><ul><li><code>计算遍历位置以及长度</code>：主要在于<code>start</code>、<code>end</code>、<code>rangelen</code>的边界条件以及内容计算；</li><li><code>搜索目标范围的起始节点</code>：利用<code>zslGetElementByRank</code>获取正序或倒序序列的起始位置；</li><li><code>以起始节点开始遍历指定范围的节点</code>：基于上一步计算的起始节点开始循环遍历（基于双向链表），需要注意遍历的方向；</li></ul><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 范围查询 ZRANGE */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">zrangeGenericCommand</span><span class="params">(client *c, <span class="keyword">int</span> reverse)</span> </span>&#123;</span><br><span class="line">    robj *key = c-&gt;argv[<span class="number">1</span>]; <span class="comment">// KEY</span></span><br><span class="line">    <span class="keyword">long</span> start;            <span class="comment">// 开始位置</span></span><br><span class="line">    <span class="keyword">long</span> end;              <span class="comment">// 结束位置</span></span><br><span class="line">    <span class="keyword">long</span> rangelen;          <span class="comment">// 扫描范围</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* start end 赋值 */</span></span><br><span class="line">    <span class="keyword">if</span> ((getLongFromObjectOrReply(c, c-&gt;argv[<span class="number">2</span>], &amp;start, <span class="literal">NULL</span>) != C_OK) ||</span><br><span class="line">        (getLongFromObjectOrReply(c, c-&gt;argv[<span class="number">3</span>], &amp;end, <span class="literal">NULL</span>) != C_OK)) <span class="keyword">return</span>;</span><br><span class="line">    </span><br><span class="line">    ...</span><br><span class="line">    llen = zsetLength(zobj); <span class="comment">// 跳表长度</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* 记录扫描长度 */</span></span><br><span class="line">    rangelen = (end-start)+<span class="number">1</span>;</span><br><span class="line">    <span class="comment">/* ziplist */</span></span><br><span class="line">    <span class="keyword">if</span> (zobj-&gt;encoding == OBJ_ENCODING_ZIPLIST) &#123;</span><br><span class="line">        ...</span><br><span class="line">    <span class="comment">/* skiplist */</span>    </span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (zobj-&gt;encoding == OBJ_ENCODING_SKIPLIST) &#123;</span><br><span class="line">        zset *zs = zobj-&gt;ptr;</span><br><span class="line">        zskiplist *zsl = zs-&gt;zsl;</span><br><span class="line">        zskiplistNode *ln;</span><br><span class="line">        sds ele;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/* 反向 or 正向，利用zslGetElementByRank获取起始节点 */</span></span><br><span class="line">        <span class="keyword">if</span> (reverse) &#123;</span><br><span class="line">            <span class="comment">/* 默认从跳表尾部开始 */</span></span><br><span class="line">            ln = zsl-&gt;tail;</span><br><span class="line">            <span class="keyword">if</span> (start &gt; <span class="number">0</span>)</span><br><span class="line">                <span class="comment">/* zslGetElementByRank仅支持正序排序，则反序起始位置为 llen-start（rank以llen结束） */</span></span><br><span class="line">                ln = zslGetElementByRank(zsl, llen-start);</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">            ln = zsl-&gt;header-&gt;level[<span class="number">0</span>].forward;</span><br><span class="line">            <span class="keyword">if</span> (start &gt; <span class="number">0</span>)</span><br><span class="line">                 <span class="comment">/* zslGetElementByRank仅支持正序排序，则正序起始位置为 start+1（rank从1开始） */</span></span><br><span class="line">                ln = zslGetElementByRank(zsl,start+<span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">/* 从起始节点开始，遍历(正向 or 反向)获取所有数据 */</span></span><br><span class="line">        <span class="keyword">while</span>(rangelen--) &#123;</span><br><span class="line">            ele = ln-&gt;ele;</span><br><span class="line">            addReplyBulkCBuffer(c,ele,sdslen(ele));</span><br><span class="line">            <span class="keyword">if</span> (withscores)</span><br><span class="line">                addReplyDouble(c,ln-&gt;score);</span><br><span class="line">            ln = reverse ? ln-&gt;backward : ln-&gt;level[<span class="number">0</span>].forward;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        serverPanic(<span class="string">"Unknown sorted set encoding"</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">/* 获取指定排序的元素 */</span></span><br><span class="line"><span class="function">zskiplistNode* <span class="title">zslGetElementByRank</span><span class="params">(zskiplist *zsl, <span class="keyword">unsigned</span> <span class="keyword">long</span> rank)</span> </span>&#123;</span><br><span class="line">    zskiplistNode *x;</span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> traversed = <span class="number">0</span>; <span class="comment">// 记录移动次数</span></span><br><span class="line">    <span class="keyword">int</span> i;</span><br><span class="line">    <span class="comment">/* 从跳表的最高层开始遍历 */</span></span><br><span class="line">    x = zsl-&gt;header;</span><br><span class="line">    <span class="keyword">for</span> (i = zsl-&gt;level<span class="number">-1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">        <span class="comment">/* 如果当前层的移动次数 小于 rank，则继续当前层遍历，否则，进入下一层 */</span></span><br><span class="line">        <span class="keyword">while</span> (x-&gt;level[i].forward &amp;&amp; (traversed + x-&gt;level[i].span) &lt;= rank)</span><br><span class="line">        &#123;</span><br><span class="line">            traversed += x-&gt;level[i].span;</span><br><span class="line">            x = x-&gt;level[i].forward;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">/* 如果遍历到指定排序位置 */</span></span><br><span class="line">        <span class="keyword">if</span> (traversed == rank) &#123;</span><br><span class="line">            <span class="keyword">return</span> x;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h3><p><a href="https://snayan.github.io/post/2019/algorithm_skip_list/" target="_blank" rel="noopener">数据结构和算法-跳表的原理及实现</a><br><a href="https://lotabout.me/2018/skip-list/" target="_blank" rel="noopener">跳表──没听过但很犀利的数据结构</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;时隔四年，补课。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="缓存" scheme="http://simyy.cn/tags/%E7%BC%93%E5%AD%98/"/>
    
      <category term="Redis" scheme="http://simyy.cn/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>InnoDB Locking</title>
    <link href="http://simyy.cn/2020/08/08/mysql-innodb-lock/"/>
    <id>http://simyy.cn/2020/08/08/mysql-innodb-lock/</id>
    <published>2020-08-08T09:44:57.000Z</published>
    <updated>2020-08-12T09:04:32.000Z</updated>
    
    <content type="html"><![CDATA[<p>翻译 &amp; 理解。</p><a id="more"></a><p><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html" target="_blank" rel="noopener">mysql v8.0 innodb locking</a></p><h3 id="Shared-and-Exclusive-Locks"><a href="#Shared-and-Exclusive-Locks" class="headerlink" title="Shared and Exclusive Locks"></a>Shared and Exclusive Locks</h3><p>InnoDB实现了两种标准的<code>行锁（row-level locks）</code>：</p><ul><li><p><strong>共享锁</strong>（<code>shared(S) locks</code>）允许持有该锁的事务<code>读取</code>数据行；</p></li><li><p><strong>排它锁</strong>（<code>exclusive(X) locks</code>）允许持有该锁的事务<code>更新或删除</code>数据行。</p></li></ul><blockquote><p>共享锁被称为共享的原因在于：多个事务可以同时持有共享锁，从而实现数据的并发读取。</p></blockquote><p>【例1】针对同一行数据r行，假设事务T1已持有该行的<code>共享锁</code>，则事务T2的请求加锁结果如下：</p><ul><li>如果事务T2尝试获取该行的<code>共享锁</code>，则会立即生效，此时，事务T1与事务T2均持有r行的共享锁；</li><li>如果事务T2尝试获取该行的<code>排它锁</code>，由于锁冲突，导致事务T2进入阻塞等待状态（直到事务T1释放锁）。</li></ul><p>【例2】针对同一行数据r行，假设事务T1已经持有该行的<code>排他锁</code>，则事务T2无论请求<code>共享锁</code>还是<code>排它锁</code>都会进入阻塞等待状态（直到事务T1释放锁）。</p><h3 id="Intention-Locks"><a href="#Intention-Locks" class="headerlink" title="Intention Locks"></a>Intention Locks</h3><p>InnoDB实现了<code>多粒度的锁（multiple granularity locking）</code>，用于支持行锁与表锁的共存。</p><p><code>意向锁（Intention Locks）</code>是<code>表锁（table-level locks）</code>，用于表示事务稍后需要哪种类型的锁(共享的或排他的)来锁定表中的某一行。</p><ul><li><strong>意向共享锁</strong>（<code>intention shared lock (IS)</code>）表示事务打算对表中的各个行设置<code>共享锁</code>；</li><li><strong>意向排他锁</strong>（<code>intention exclusive lock (IX)</code>）表示事务打算对表中的各个行设置<code>排他锁</code>。</li></ul><p><strong>意向锁与行锁的加锁规则</strong></p><ul><li>如果事务希望获取某行的<code>S锁（row-level locks）</code>之前，必须先获取表上的<code>IS锁（table-level locks）或更强的锁</code>;</li><li>如果事务希望获取某行的<code>X锁（row-level locks）</code>之前，必须先获取表上的<code>IX锁（table-level locks）</code>。</li></ul><p><strong>意向锁与表锁之间的兼容规则</strong></p><table><thead><tr><th>-</th><th>X</th><th>IX</th><th>S</th><th>IS</th></tr></thead><tbody><tr><td>X</td><td>Conflict</td><td>Conflict</td><td>Conflict</td><td>Conflict</td></tr><tr><td>IX</td><td>Conflict</td><td>Compatible</td><td>Conflict</td><td>Compatible</td></tr><tr><td>S</td><td>Conflict</td><td>Conflict</td><td>Compatible</td><td>Compatible</td></tr><tr><td>IS</td><td>Conflict</td><td>Compatible</td><td>Compatible</td><td>Compatible</td></tr></tbody></table><p>【例】如果事务T1已经持有<code>表的IX锁</code>，则事务T2获取不同<code>表锁</code>的结果如下：</p><ul><li>如果事务T2尝试获取<code>X锁或S锁</code>，则会造成锁冲突，事务T2进入阻塞等待（直到事务T1释放表的IX锁）；</li><li>如果事务T2尝试获取<code>IX锁或IS锁</code>，由于兼容规则，事务T2可以正常获取<code>IX锁或IS锁</code>。</li></ul><blockquote><p>注意<code>意向锁</code>与<code>表锁</code>、<code>行锁</code>的兼容规则的不同。</p></blockquote><hr><p>以下为用于测试初始化的SQL语句，</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 建表</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`t`</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">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`c`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">DEFAULT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`d`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">DEFAULT</span> <span class="literal">NULL</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">KEY</span> <span class="string">`C`</span>(<span class="string">`C`</span>),</span><br><span class="line">  <span class="keyword">UNIQUE</span> <span class="keyword">KEY</span> <span class="string">`d`</span> (<span class="string">`d`</span>)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8mb4 <span class="keyword">COLLATE</span>=utf8mb4_general_ci;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始化</span></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`test`</span>.<span class="string">`t`</span>(<span class="string">`id`</span>, <span class="string">`c`</span>, <span class="string">`d`</span>) <span class="keyword">VALUES</span> (<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`test`</span>.<span class="string">`t`</span>(<span class="string">`id`</span>, <span class="string">`c`</span>, <span class="string">`d`</span>) <span class="keyword">VALUES</span> (<span class="number">5</span>, <span class="number">5</span>, <span class="number">5</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="string">`test`</span>.<span class="string">`t`</span>(<span class="string">`id`</span>, <span class="string">`c`</span>, <span class="string">`d`</span>) <span class="keyword">VALUES</span> (<span class="number">10</span>, <span class="number">10</span>, <span class="number">10</span>);</span><br></pre></td></tr></table></figure><hr><h3 id="Record-Locks"><a href="#Record-Locks" class="headerlink" title="Record Locks"></a>Record Locks</h3><p><code>记录锁（record locks）</code>是索引上的一种行锁。</p><p>如下面的sql，会对<code>c1=10</code>的行设置<code>记录锁record lock</code>，阻止其他事务对<code>id=0</code>的行进行<code>插入、更新、删除</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">SELECT</span> * <span class="keyword">FROM</span> t <span class="keyword">WHERE</span> <span class="keyword">id</span> = <span class="number">0</span> <span class="keyword">FOR</span> <span class="keyword">UPDATE</span>;</span><br></pre></td></tr></table></figure><h4 id="Clustered-and-Secondary-Indexes"><a href="#Clustered-and-Secondary-Indexes" class="headerlink" title="Clustered and Secondary Indexes"></a>Clustered and Secondary Indexes</h4><blockquote><p><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html" target="_blank" rel="noopener">mysql v8.0 Clustered and Secondary Indexes</a></p></blockquote><p>InnoDB使用<code>聚簇索引（clustered index）</code>来存储数据，该索引的构建方式如下：</p><p>① InnoDB使用表中显示的<code>主键（primary key）</code>来构建聚簇索引；</p><p>② 如果没有主键，InnoDB会尝试寻找一个<code>非空（NOT NULL）</code>的<code>唯一索引（UNIQUE INDEX）</code>来构建聚簇索引；</p><p>③ 如果表没有主键且没有非空的唯一索引，InnoDB会为表生成一个<code>隐式的ROW-ID</code>的自增字段，构建名为<code>GEN_CLUST_INDEX</code>的聚簇索引。</p><h4 id="How-the-Clustered-Index-Speeds-Up-Queries"><a href="#How-the-Clustered-Index-Speeds-Up-Queries" class="headerlink" title="How the Clustered Index Speeds Up Queries"></a>How the Clustered Index Speeds Up Queries</h4><p>聚簇索引的特点在于<code>把所有的数据存储在叶子节点</code>，实现了<code>数据与索引的分离</code>。</p><p>正常来说，数据占用的空间往往远大于索引，考虑不同索引实现的场景：</p><p>【场景1】如果采用BTree来实现，由于BTree的数据与索引是存储在同一个节点上，所以：</p><ul><li>查询时可以直接从节点上获取数据，不需要遍历整棵树</li><li>每个节点上存储的索引有限，当遇到深层次遍历时会触发大量的磁盘IO</li><li>范围查询性能差，需要再次遍历也子节点</li></ul><p>【场景2】如果采用B+Tree来实现，由于B+Tree的数据与索引是分离的，所以：</p><ul><li>查询时需要遍历整棵数才可以到叶子节点获取数据</li><li>非叶子节点不存储数据从而可以存储更多的索引数据，当遇到深层次遍历时所触发的磁盘IO较少，从而拥有更好的性能</li><li>范围查询性能好，利用叶子节点的顺序链表来遍历查询</li></ul><h4 id="How-Secondary-Indexes-Relate-to-the-Clustered-Index"><a href="#How-Secondary-Indexes-Relate-to-the-Clustered-Index" class="headerlink" title="How Secondary Indexes Relate to the Clustered Index"></a>How Secondary Indexes Relate to the Clustered Index</h4><p>除了聚簇索引，其他搜索都被称为<code>二级索引</code>。</p><p>二级索引中不会包含行的所有数据，仅会包括关联行的主键。</p><p>也就是说，二级索引的检索过程为：先检索二级索引，找到目标行的主键索引KEY，然后在检索聚簇索引，最终找到目标行的数据。</p><p>由于二级索引需要关联聚簇索引的KEY，因此聚簇索引的主键选择上不应该选择过大的数据结构。</p><blockquote><p>有一点疑问：为什么二级索引不采用BTree实现？</p><p>如果采用BTree实现的话，指定的目标查询不会遍历到叶子节点，这是BTree的优势。<br>但是如果需要范围查询的时候，性能就不如B+Tree这种实现了，这应该是采用该索引的原因。</p><p>实际上，MySQL InnoDB可以设置两种索引类型：<code>Btree</code>和<code>Hash</code>，如果该索引不会有顺序查询，使用<code>Hash</code>索引更合适。</p></blockquote><h3 id="Gap-Locks"><a href="#Gap-Locks" class="headerlink" title="Gap Locks"></a>Gap Locks</h3><p><code>间隙锁（gap locks）</code>是对索引记录之间（两个记录之间、第一个记录之前、最后一个记录之后）间隙的锁。</p><p>间隙锁的作用在于<code>防止其他事务操作当前间隙内的数据</code>。</p><p>【例】下面的SQL会增加一个<code>间隙锁</code>，区间为(10, 20)，因此，其他事务无法在间隙内插入、更新或删除，例如插入15会被阻塞。</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">SELECT</span> * <span class="keyword">FROM</span> t <span class="keyword">WHERE</span> c <span class="keyword">BETWEEN</span> <span class="number">10</span> <span class="keyword">and</span> <span class="number">20</span> <span class="keyword">FOR</span> <span class="keyword">UPDATE</span></span><br></pre></td></tr></table></figure><blockquote><p>针对不同的索引类型，会出现不同的加锁情况：</p><ul><li>对于<code>唯一索引(UNIQUE INDEX)</code>来说，锁定某行时仅需要<code>锁定数据行（不需要间隙锁的）</code>；</li><li>对于<code>普通索引（INDEX）</code>来说，锁定某行时不仅需要<code>锁定数据行</code>，还要<code>锁定前后间隙</code>；</li></ul></blockquote><p>对于测试数据来说，索引（不同字段之间数据值相同）存在的间隙有：<code>(-∞, 0)</code>、<code>(0, 5)</code>、<code>(5, 10)</code>、<code>(10, +∞)</code>。</p><blockquote><p>不同的事务的间隙锁可以共存（甚至X锁与S锁），例如，在区间(0, 5)上，事务A持有S锁，事务B持有X锁。</p></blockquote><p>在<code>读已提交（READ COMMITTED</code>）的隔离级别下，间隙锁不再用于搜索与索引扫描，仅用于外键约束检查和重复键检查。</p><h4 id="Case-1"><a href="#Case-1" class="headerlink" title="Case 1"></a>Case 1</h4><p>在初始化测试数据的条件下，考虑c为普通索引，加锁结果如下：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加排它锁成功，间隙锁(0,5)与(5,10)、记录锁&#123;5&#125;</span></span><br><span class="line"><span class="comment"># 其中，间隙锁(5,10)是由于临键锁(5,10]降级而来，从5向右遍历发现10这个节点不满足=5，所以去除c=10的记录锁)</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁失败，由于事务A持有间隙锁(0, 5)导致冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务B提示：lock_mode X locks gap before rec insert intention waiting Record lock</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁成功失败，由于事务A持有间隙锁(0, 5)（临键锁(5, 10]降级而来）导致冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">8</span>, <span class="number">8</span>, <span class="number">8</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务C提示：lock_mode X insert intention waiting Record lock</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务D：加插入意向锁成功，由于13不在锁定范围之内</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">13</span>, <span class="number">13</span>, <span class="number">13</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务E：加入排他锁失败，由于记录锁&#123;5&#125;的冲突</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">set</span> c=<span class="number">3</span> <span class="keyword">where</span> c=<span class="number">5</span>; <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务E提示：lock_mode X waiting Record lock</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务F：加入排它锁成功，由于c=10，没有加锁</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">set</span> c=<span class="number">99</span> <span class="keyword">where</span> c=<span class="number">10</span>;</span><br></pre></td></tr></table></figure><p><code>c=5</code>是一个普通索引的加锁行为，不仅对当前的行加记录锁，而且会对两侧的间隙加锁（左侧是间隙锁，右侧为临键锁）。</p><blockquote><p><strong>临键锁的退化</strong>：</p><p>① 在RR级别下，加锁的单位为<code>临键锁</code>；</p><p>② 针对二级索引的等值(c=5)的加锁情况，会按照索引顺序继续寻找不同的行，进而退化为<code>间隙锁</code>，例如<code>事务F</code>并没有阻塞。</p></blockquote><h4 id="Case-2"><a href="#Case-2" class="headerlink" title="Case 2"></a>Case 2</h4><p>在初始化测试数据的条件下，考虑c为普通索引，加锁结果如下：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加排它锁成功，间隙锁(0, 5)、记录锁&#123;5&#125;、临键锁(5, 10]</span></span><br><span class="line"><span class="comment"># 注意，c between 0 and 5 为非等值查询，因此不存在临键锁的退化</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c <span class="keyword">between</span> <span class="number">0</span> <span class="keyword">and</span> <span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加排它锁失败，由于存在记录锁&#123;5&#125;的冲突</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">set</span> c=<span class="number">3</span> <span class="keyword">where</span> c=<span class="number">5</span>;  <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁失败，由于存在临键锁(5, 10]的冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">7</span>, <span class="number">7</span>, <span class="number">7</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务D：加排它锁失败，由于存在临键锁(5, 10])</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">set</span> c=<span class="number">99</span> <span class="keyword">where</span> c=<span class="number">10</span>; <span class="comment"># blocking</span></span><br></pre></td></tr></table></figure><p><code>between</code>类似于一个范围查询，会把范围内的都加锁（无论是行还是间隙）。</p><blockquote><p>注意事务D的语句会被blocking，也证明了非等值语句的情况下，临键锁是不会退化的。</p></blockquote><h3 id="Next-Key-Locks"><a href="#Next-Key-Locks" class="headerlink" title="Next-Key Locks"></a>Next-Key Locks</h3><p><code>临键锁（next-key locks）</code>是记录锁与间隙锁的一种组合，锁住索引记录以及索引记录之前的间隙（<code>(左开右闭的区间]</code>）。</p><blockquote><p>RR级别下，加锁的最小单元是<code>临键锁（next-key locks）</code>，都是按照左开右闭的区间来加锁的。</p><p>不过，也存在锁降级的情况：</p><p>① 唯一索引下，降级为行锁</p><p>② 普通索引下，如果是等值查询的话，第二个<code>临键锁（next-key locks）</code>会退化为<code>间隙锁（gap locks）</code>。</p></blockquote><p>对于测试数据中c索引来说，临键锁（不同字段之间数据值相同）的区间有：<code>(-∞, 0]</code>、<code>(0, 5]</code>、<code>(5, 10]</code>、<code>(10, +∞)</code>。</p><blockquote><p><strong>锁降级</strong>：唯一索引上，临键锁会降级为记录锁（不包括左右边缘）。</p></blockquote><p>临键锁仅发生在<code>可重复度（REPEATABLE READ ）</code>的隔离级别，主要解决了<code>幻读（phantom read）</code>。</p><h4 id="Case-1-1"><a href="#Case-1-1" class="headerlink" title="Case 1"></a>Case 1</h4><p>唯一索引上的锁降级情况如下，</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加排他锁成功，记录锁&#123;5&#125;</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁成功</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁成功</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">7</span>, <span class="number">7</span>, <span class="number">7</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务D：加排它锁失败，c=5 找到 id=5，由于记录锁导致加锁失败</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">where</span> c=<span class="number">99</span> <span class="keyword">where</span> c=<span class="number">5</span>; <span class="comment"># blocking</span></span><br></pre></td></tr></table></figure><h4 id="Case-2-1"><a href="#Case-2-1" class="headerlink" title="Case 2"></a>Case 2</h4><p>在RR隔离级别下，在测试数据初始化的条件下，考虑c为普通索引，锁操作如下，</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加排它锁成功，间隙锁(0,5)与(5,10)、记录锁&#123;5&#125;</span></span><br><span class="line"><span class="comment"># 其中，间隙锁(5,10)是由于临键锁(5,10]降级而来，从5向右遍历发现10这个节点不满足=5，所以去除c=10的记录锁)</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加排他锁失败，由于临间隙锁(5, 10)的冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">8</span>, <span class="number">8</span>, <span class="number">8</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务B提示：lock_mode X locks gap before rec insert intention waiting</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加排他锁失败，不存在锁冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">13</span>, <span class="number">13</span>, <span class="number">13</span>); <span class="comment"># ok</span></span><br></pre></td></tr></table></figure><p><code>c=5</code>为普通索引上的加锁，包括间隙锁(0, 5)、记录锁{5}、间隙锁(5, 10)(等值查询情况下的锁退化)。</p><h4 id="Case-3"><a href="#Case-3" class="headerlink" title="Case 3"></a>Case 3</h4><p>在RR隔离级别下，在测试数据初始化的条件下，考虑d为唯一索引，锁操作如下，</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="comment"># 事务A：加排他锁成功，记录锁&#123;5&#125;</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> d=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁成功，无冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">8</span>, <span class="number">8</span>, <span class="number">8</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁成功，无冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">13</span>, <span class="number">13</span>, <span class="number">13</span>); <span class="comment"># ok</span></span><br></pre></td></tr></table></figure><h4 id="Case-4"><a href="#Case-4" class="headerlink" title="Case 4"></a>Case 4</h4><p>在RR隔离级别下，在测试数据初始化的条件下，考虑c为普通索引，锁操作如下，</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="comment"># 事务A：加排他锁成功，间隙锁(5, 10)，记录锁&#123;10&#125;，临键锁(10, +∞)</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c &gt; <span class="number">8</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁失败，由于存在临键锁(10, +∞)冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">13</span>, <span class="number">13</span>, <span class="number">13</span>); <span class="comment"># blocking...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务B提示：lock_mode X insert intention waiting Record lock</span></span><br></pre></td></tr></table></figure><h4 id="Case-5"><a href="#Case-5" class="headerlink" title="Case 5"></a>Case 5</h4><p>在RR隔离级别下，在测试数据初始化的条件下，考虑d为唯一索引，锁操作如下，</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="comment"># 事务A：加排他锁成功，间隙锁(5, 10), 记录锁&#123;10&#125;, 临键锁(10, +∞)</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> d &gt; <span class="number">8</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁失败，由于存在间隙锁(5, 10)冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">7</span>, <span class="number">7</span>, <span class="number">7</span>); <span class="comment"># blocking...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁失败，由于存在临键锁(10, +∞)冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">13</span>, <span class="number">13</span>, <span class="number">13</span>); <span class="comment"># blocking...</span></span><br></pre></td></tr></table></figure><h3 id="Insert-Intention-Locks"><a href="#Insert-Intention-Locks" class="headerlink" title="Insert Intention Locks"></a>Insert Intention Locks</h3><p><code>插入意图锁（insert intention locks）</code>是一种间隙锁，当执行<code>插入（insert）</code>操作是触发。</p><p>【例1】以下SQL会加入插入意向锁，锁住的间隙是(0, 5)，</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">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span>(<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>);</span><br></pre></td></tr></table></figure><p><strong>多事务的插入意向锁</strong></p><p>考虑到插入意向锁是间隙锁，因此，不同事务的插入意向锁可以共存。</p><p>不同事务同时持有相同间隙的插入意向锁的情况下，如果插入的位置不同（值不同），此时各个事务会正常执行插入操作，</p><p>否则，如果出现插入到相同位置，该事务会一直等待，直到插入该位置的事务释放锁。</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：持有插入意向锁成功，锁定区间(0, 5)，此时会给3增加行锁</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>) <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：持有插入意向锁成功，锁定区间(0, 5)，与事务A插入位置不同，所以无冲突，执行成功</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">4</span>, <span class="number">4</span>, <span class="number">4</span>) <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：由于事务A已经对于同一位置写入数据，由于3已经被A持有了行锁，因此，当前事务阻塞等待事务A的释放</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>) <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务C提示：lock mode S waiting Record lock</span></span><br></pre></td></tr></table></figure><blockquote><p>上面发现，相同位置数据的数据会提示S锁，插入不应该是X锁么？</p><p>对于唯一索引来说，插入前需要先检查是否存在重复数据，此时X锁先会降级为S锁来实现当前读。</p></blockquote><h3 id="AUTO-INC-Locks"><a href="#AUTO-INC-Locks" class="headerlink" title="AUTO-INC Locks"></a>AUTO-INC Locks</h3><p><code>自增锁（auto-inc locks）</code>是<code>表锁（table-level locks）</code>，用于实现自增主键。</p><p>由于自增锁属于表锁，性能必然很差，因此，考虑到自增主键使用的场景，自增锁的锁定范围并不是整个事务，而是锁定的<code>insert sql</code>语句级别，</p><p>也就说，不同的事务之间，在<code>insert</code>时是交替完成的，虽然是表锁但对事务的并发插入并没有太大的影响。</p><h3 id="Phantom-Reads"><a href="#Phantom-Reads" class="headerlink" title="Phantom Reads"></a>Phantom Reads</h3><p><code>幻读（phantom reads）</code>是由于不同事务之间新增数据导致的前后数量不一致的问题。</p><blockquote><p><code>幻读</code>是insert引起的，<code>不可重复读</code>是由update或delete引起的。</p><p><code>幻读</code>产生原因在于新增的，<code>不可重复读</code>产生的原因在于更新。</p></blockquote><p>在RR隔离级别下，通过<code>临键锁（next-key locks）</code>解决了幻读。</p><blockquote><p><code>select for update</code>属于当前读（排它锁），而<code>select</code>属于快照读（无锁）。</p><p><code>select lock in share mode</code>属于共享锁，无法与排它锁共存。</p></blockquote><h4 id="Case-1-2"><a href="#Case-1-2" class="headerlink" title="Case 1"></a>Case 1</h4><p>即使InnoDB实现了MVCC解决了幻读的情况，但不同事务之间仍然存在先后插入数据的冲突问题。</p><p>在测试数据初始化的条件下，观察以下加锁结果：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：快照读，不会加锁</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span> &gt; <span class="number">2</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁成功，锁定id=3（记录锁）</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：加插入意向锁失败，由于事务B已经持有id=3的记录锁</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>);  <span class="comment"># blocking...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务A提示：lock mode S waiting Record lock</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：提交事务，写入id=3的数据</span></span><br><span class="line"><span class="keyword">commit</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：由于事务B已经提交事务，返回失败"Duplicate entry '3' for key 'PRIMARY'"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：失败后再次执行快照读（前后数据不变，观察不到事务B已经写入数据），虽然提示3已经写入，但还是观察不到</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span> &gt; <span class="number">2</span>; <span class="comment"># ok</span></span><br></pre></td></tr></table></figure><h3 id="Dead-Locks"><a href="#Dead-Locks" class="headerlink" title="Dead Locks"></a>Dead Locks</h3><p><code>死锁（dead locks）</code>是由于多个事务相互持有互相等待的锁导致的。</p><h4 id="Case-1-3"><a href="#Case-1-3" class="headerlink" title="Case 1"></a>Case 1</h4><p><strong>insert-insert-insert-rollback 三个事务写入的场景下的死锁</strong></p><p>在测试数据初始化的条件下，观察以下加锁结果：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加插入意向锁(0,5)，id=3的排他锁（X锁）</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁失败，优先获取S锁，查看数据是否存在，进入阻塞等待事务A的释放</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># blocking...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：同上</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># blocking...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：回滚，释放事务A上的锁</span></span><br><span class="line"><span class="keyword">rollback</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，事务B与事务C同时持有S锁，都在等待对方释放S锁进而添加X锁（存在死锁）</span></span><br><span class="line"><span class="comment"># 最终结果：</span></span><br><span class="line"><span class="comment"># 事务C：加插入意向锁(0,5)，id=3的排他锁（X锁）</span></span><br><span class="line"><span class="comment"># 事务B：加锁失败，出现死锁，返回异常：Deadlock found when trying to get lock; try restarting transaction</span></span><br></pre></td></tr></table></figure><p>由于两个事务持有同一个记录的S锁，彼此等待对方释放S锁，进而进入死锁。</p><h4 id="Case-2-2"><a href="#Case-2-2" class="headerlink" title="Case 2"></a>Case 2</h4><p><strong>update-insert 两个事务写入重叠锁区间场景下的死锁</strong></p><p>在测试数据初始化的条件下，观察以下加锁结果：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：加排他锁成功，间隙锁(0,5)、记录锁&#123;5&#125;、临键锁(5, 10]</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加排他锁成功，间隙锁(5,10)、记录锁&#123;10&#125;、临键锁(10, +∞)</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c=<span class="number">10</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时，存在锁重叠，区间为(5, 10]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：加插入意向锁失败，由于事务B的间隙锁(5,10)冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">7</span>, <span class="number">7</span>, <span class="number">7</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加插入意向锁失败，由于事务A的临键锁(5, 10]冲突</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">7</span>, <span class="number">7</span>, <span class="number">7</span>); <span class="comment"># Deadlock found when trying to get lock; try restarting transaction</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：加插入意向锁成功，由于事务A出现死锁。</span></span><br></pre></td></tr></table></figure><h4 id="Case-3-1"><a href="#Case-3-1" class="headerlink" title="Case 3"></a>Case 3</h4><p><strong>S X X</strong></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="comment"># 事务A：id=5 S锁</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">share</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：id=5 请求 X锁</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：id=5 请求 X锁</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">update</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：Deadlock found when trying to get lock; try restarting transaction</span></span><br></pre></td></tr></table></figure><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="comment"># 事务A：id=5 S锁</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">share</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：id=5 请求 X锁</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span>; <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：id=5 请求 X锁</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">5</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：Deadlock found when trying to get lock; try restarting transaction</span></span><br></pre></td></tr></table></figure><p>临键锁会分为两段加锁：<code>加间隙锁</code>与<code>加记录锁</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="comment"># 事务A：间隙锁(0, 5)(5, 10)，记录锁&#123;5&#125;</span></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t <span class="keyword">where</span> c=<span class="number">5</span> <span class="keyword">for</span> <span class="keyword">share</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：加间隙锁成功(0,5)(5,10)，加记录锁&#123;5&#125;冲突，等待</span></span><br><span class="line"><span class="keyword">update</span> t <span class="keyword">set</span> c=<span class="number">99</span> <span class="keyword">where</span> c=<span class="number">5</span>; <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务A：由于事务B持有间隙锁，因此出现死锁，事务B异常退出</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment">#ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：Deadlock found when trying to get lock; try restarting transaction</span></span><br></pre></td></tr></table></figure><h3 id="limit"><a href="#limit" class="headerlink" title="limit"></a>limit</h3><p><code>limit</code>对加锁范围仍然有影响：<code>仅会对扫描到的数据范围进行加锁</code>。</p><h4 id="Case-1-4"><a href="#Case-1-4" class="headerlink" title="Case 1"></a>Case 1</h4><p>针对唯一索引的limit场景如下，</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务A：扫描id&gt;0的数据，由于limit仅为1，则仅会锁住id=5的这条数据</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span> &gt; <span class="number">0</span> <span class="keyword">limit</span> <span class="number">1</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> <span class="keyword">id</span> &gt; <span class="number">5</span> <span class="keyword">limit</span> <span class="number">1</span>; <span class="comment"># ok</span></span><br></pre></td></tr></table></figure><h4 id="Case-2-3"><a href="#Case-2-3" class="headerlink" title="Case 2"></a>Case 2</h4><p>针对非唯一索引的limit场景如下，</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="comment"># 事务A：扫描c&gt;0的数据，由于limit仅为1，间隙锁(0, 5)，行锁&#123;5&#125; 由于limit1，不会锁住5后面的区间</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> c &gt; <span class="number">0</span> <span class="keyword">limit</span> <span class="number">1</span>; <span class="comment"># ok</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务B：由于事务A存在间隙锁(0, 5)，加锁失败</span></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t <span class="keyword">values</span> (<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>); <span class="comment"># blocking</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 事务C：无锁冲突，加锁成功</span></span><br><span class="line"><span class="keyword">delete</span> <span class="keyword">from</span> t <span class="keyword">where</span> c &gt; <span class="number">5</span> <span class="keyword">limit</span> <span class="number">1</span>; <span class="comment"># ok</span></span><br></pre></td></tr></table></figure><h3 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h3><p>数据库上的锁都是为了避免数据并发更新导致的问题，这与系统中所使用锁的初衷是一样的。</p><p>在InnoDB上，锁都是建立索引的基础之上，这里包括聚簇索引、二级索引等。</p><p>对于唯一索引来说，针对每个特定的值的遍历与查询是可以明确目标的，因此，不需要间隙锁，仅使用记录锁就可以了。</p><p>对于普通索引来说，针对某个特定的值的遍历与查询是不固定的，可能存在多个相同的值，此时就需要锁定目标值的前后，防止加入相同的值。</p><p>间隙锁是可以共存的，即使是冲突的锁，临键锁解决了幻读的问题。</p><p>插入意向锁是间隙锁，需要注意插入冲突的判断条件是位置是否相同，即使是相同范围的间隙锁，虽然插入位置不同也不会造成冲突。</p><p>对于自增锁来说，虽然是表锁，但锁定的时间仅限于插入SQL语句，不会跟随整个事务，因此，性能并没有那么差。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;翻译 &amp;amp; 理解。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="MySQL" scheme="http://simyy.cn/tags/MySQL/"/>
    
      <category term="数据库" scheme="http://simyy.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="锁" scheme="http://simyy.cn/tags/%E9%94%81/"/>
    
  </entry>
  
  <entry>
    <title>Local Cache &amp; Distributed Cache</title>
    <link href="http://simyy.cn/2020/07/29/cache-usage/"/>
    <id>http://simyy.cn/2020/07/29/cache-usage/</id>
    <published>2020-07-29T12:41:08.000Z</published>
    <updated>2020-07-31T08:17:46.000Z</updated>
    
    <content type="html"><![CDATA[<p>总结下缓存更新思路。</p><a id="more"></a><h4 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h4><p><code>缓存</code>是提高系统性能的关键，也必然会带来<code>数据一致性</code>的问题。</p><p>常见的缓存大概有以下几种，</p><h5 id="客户端（浏览器）缓存"><a href="#客户端（浏览器）缓存" class="headerlink" title="客户端（浏览器）缓存"></a>客户端（浏览器）缓存</h5><p>使用用户本地缓存来减少服务端的请求压力，会造成客户端与服务端数据不一致的问题。</p><blockquote><p>客户端缓存可以有效的降低服务端的压力，例如，抢购以及电商大促时常常先把一些静态数据缓存到客户端本地。</p></blockquote><h5 id="cdn缓存"><a href="#cdn缓存" class="headerlink" title="cdn缓存"></a>cdn缓存</h5><p>利用分布式的<code>边缘服务器</code>对服务端的数据进行缓存，客户端<code>就近获取数据</code>。</p><p>cdn缓存可以理解为利用中间件（分布式缓存）对服务端整体缓存，同样会造成数据不一致的问题。</p><blockquote><p>cdn缓存一般用于静态数据，例如，前端相关的css、js，商品详情页的静态数据。</p></blockquote><h5 id="nginx缓存"><a href="#nginx缓存" class="headerlink" title="nginx缓存"></a>nginx缓存</h5><p>nginx缓存一般仅用于服务端的静态数据的缓存，一般常用于前端相关的css/js等。</p><h5 id="本地缓存"><a href="#本地缓存" class="headerlink" title="本地缓存"></a>本地缓存</h5><p>本地缓存是解决高并发的终极手段，但一定会存在数据不一致的问题。</p><p>大量的本地缓存会占用大量的业务内存，合理的设置LRU以及热点数据的评估是使用本地缓存的关键问题。</p><blockquote><p>为了实现高并发，往往会采用数据的最终一致性。</p></blockquote><h5 id="分布式缓存"><a href="#分布式缓存" class="headerlink" title="分布式缓存"></a>分布式缓存</h5><p>分布式缓存是目前微服务下最常用的缓存手段。</p><p>在一些QPS不高的业务下，并不需要本地缓存，仅仅使用分布式缓存就能实现很好的性能。</p><p>由于分布式缓存引入了新的中间件，会增加整个链路的网络IO开销。</p><blockquote><p>常见的分布式缓存有Redis、Memcached。</p></blockquote><h4 id="缓存更新"><a href="#缓存更新" class="headerlink" title="缓存更新"></a>缓存更新</h4><p>缓存更新的方式大体可以分为两种：<code>同步更新</code>与<code>异步更新</code>。</p><blockquote><p>由于更新的异步化，写入数据的同时不能保证数据的缓存刷新，因此，很难保证数据的强一致性。</p></blockquote><p>同步更新大多适用于数据强一致性，而异步更新大多适用于数据的最终一致性。</p><hr><p>以下几种实例中，均使用<code>Memcached</code>作为分布式缓存，<code>MySQL</code>作为持久化存储。</p><h5 id="基于分布式缓存的同步更新"><a href="#基于分布式缓存的同步更新" class="headerlink" title="基于分布式缓存的同步更新"></a>基于分布式缓存的同步更新</h5><blockquote><p>此方案是最基础的分布式缓存方案，关键在于同步的DB写入与缓存刷新。</p></blockquote><p><strong>read</strong></p><p>优先读取缓存，如果不存在，则回源到DB，重新加载到缓存。</p><p><strong>write</strong></p><p>优先写DB，写成功后同步清理缓存。</p><p><img src="/images/cache-usage-1-sync.png" alt="基于分布式缓存的同步更新"></p><h5 id="基于分布式缓存的线程池异步更新"><a href="#基于分布式缓存的线程池异步更新" class="headerlink" title="基于分布式缓存的线程池异步更新"></a>基于分布式缓存的线程池异步更新</h5><p><strong>read</strong></p><p>同上，优先读取缓存，如果不存在，则回源到DB，重新加载到缓存。</p><p><strong>write</strong></p><p>仅写DB，但写入成功后会直接返回，触发异步线程来做缓存的更新，存在缓存的不一致问题。</p><p><img src="/images/cache-usage-2-async-thread.png" alt="基于分布式缓存的线程池异步更新"></p><p>如图所示，与同步更新相比，异步线程不能保证数据更新的时效性，如果线程出现异常会导致<code>长时间的缓存不一致</code>的问题。</p><p>此外，<code>如何设置合理的异步线程池</code>也是一个关键问题。</p><h5 id="基于分布式缓存的消息异步更新"><a href="#基于分布式缓存的消息异步更新" class="headerlink" title="基于分布式缓存的消息异步更新"></a>基于分布式缓存的消息异步更新</h5><p><strong>read</strong></p><p>同上，优先读取缓存，如果不存在，则回源到DB，重新加载到缓存。</p><p><strong>write</strong></p><p>仅写DB，但写入成功后会直接返回，使用mq作为异步操作持久化的媒介，从而解决异步线程池的问题，但仍然无法解决性能上的问题。</p><p><img src="/images/cache-usage-3-async-mq.png" alt="基于分布式缓存的消息异步更新"></p><p>如图所示，使用<code>RocketMQ</code>中间件来作为异步化的媒介，通过<code>Consumer</code>来触发缓存的刷新。</p><h5 id="基于分布式缓存的binlog异步更新"><a href="#基于分布式缓存的binlog异步更新" class="headerlink" title="基于分布式缓存的binlog异步更新"></a>基于分布式缓存的binlog异步更新</h5><p><strong>read</strong></p><p>同上，优先读取缓存，如果不存在，则回源到DB，重新加载到缓存。</p><p><strong>write</strong></p><p>仅写DB，但写入成功后会直接返回，使用binlog来避免对于mq中间件的依赖，订阅binlog日志来实现缓存的生成。</p><p><img src="/images/cache-usage-2-async-binlog.png" alt="基于分布式缓存的binlog异步更新"></p><p>这种方案引入了binlog日志，从复杂度上降低了直接的依赖，但也引入了间接的依赖。</p><p><code>Instagram</code>才用了这种方式来实现高性能的服务，具体可参考<a href="https://www.youtube.com/watch?v=hnpzNAPiC0E" target="_blank" rel="noopener">Scaling Instagram Infrastructure</a>。</p><h5 id="基于本地缓存的binlog异步更新"><a href="#基于本地缓存的binlog异步更新" class="headerlink" title="基于本地缓存的binlog异步更新"></a>基于本地缓存的binlog异步更新</h5><p><strong>read</strong></p><p>优先读本地缓存，如果不存在，则回源到DB，重新加载到本地缓存。</p><p>此方案不需要两次网络IO（分布式缓存的Request与Response），性能上一定是更好的，但一致性很难保证。</p><p><strong>write</strong></p><p>优先写DB，写入成功并更新本地缓存后会直接返回。</p><p>为了解决分布式场景下本地缓存的同步更新，引入了<code>Redis</code>的发布订阅机制。</p><blockquote><p>此方案仅更新了本地缓存，如果可以保证该用户请求均路由到同一机器，是可以解决当前用户可见性问题。</p></blockquote><p><img src="/images/cache-usage-4-local-pub-sub.png" alt="基于本地缓存的binlog异步更新1"></p><p>考虑到中间件的可靠性与时延问题，增加了<code>SyncScheduler</code>来实现定时同步机制。</p><p><img src="/images/cache-usage-5-local-pub-sub-with-syncshceduler.png" alt="基于本地缓存的binlog异步更新2"></p><h5 id="基于分布式缓存的异步持久化"><a href="#基于分布式缓存的异步持久化" class="headerlink" title="基于分布式缓存的异步持久化"></a>基于分布式缓存的异步持久化</h5><p><strong>read</strong></p><p>仅读取缓存，缓存数据的加载是通过缓存中间件来实现，服务不会直接与DB交互。</p><p><strong>write</strong></p><p>仅写缓存，缓存需要持久化策略，通过binlog通过到MySQL。</p><blockquote><p>此方案使用分布式缓存作为<code>数据库的前置缓存</code>，通过实现缓存的持久化机制来同步数据到DB。</p><p>此方案需要具有缓存中间件定制化开发的能力，缓存异常恢复也是实现高可用服务的关键。</p></blockquote><p><img src="/images/cache-usage-6-cache-persist.png" alt="基于分布式缓存的异步持久化"></p><h5 id="基于本地缓存的分治缓存策略"><a href="#基于本地缓存的分治缓存策略" class="headerlink" title="基于本地缓存的分治缓存策略"></a>基于本地缓存的分治缓存策略</h5><p><strong>read</strong></p><p>需要路由的支持，实现用户的特定路由，从而达到某个用户的所有操作均打到同一台机器上。</p><p>由于每台机器仅接受特定用户的数据，可以通过引入<code>Zookeeper</code>来获取一定的标识来预加载特定的数据。</p><p>仅读取本地缓存，通过<code>SyncSheduler</code>模块主动或定时拉取缓存数据。</p><p><strong>write</strong></p><p>仅写本地缓存，需要同步写入到数据库，从而保证数据的持久化。</p><blockquote><p>此方案是基于路由能力，实现一套特定路由方案的缓存一致性策略。</p><p>通过保证用户请求路由的一致性，使用服务内存缓存特定数据，从而利用分布式服务来代替分布式缓存，从而满足数据的一致性。</p></blockquote><p><img src="/images/cache-usage-7-local-mod.png" alt="基于本地缓存的分治缓存策略"></p><p>这种方案没有具体实施过，特定的路由规则必然带来额外的复杂度。</p><p>这种方案在大型网站的库存系统中是否有过实现也未可知。</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p><code>缓存更新</code>是分布式场景下无法避免的问题，大体可以分为<code>同步</code>与<code>异步</code>。</p><p>高性能与数据一致性往往不可兼得，<code>强一致性与最终一致性之间如何取舍</code>是架构设计的必经之路。</p><p>项目设计之初并不需要复杂的缓存设计，在满足性能的前提下架构的设计越简单越好。</p><p>如果DB的性能足以满足当前的需求，就不必引入分布式缓存。</p><p>如果分布式缓存的性能足以满足当前的需求，就不必引入本地缓存。</p><p>如果同步同步更新的性能满足当前的需求，就不必引入异步更新策略。</p><p>如果对数据的实时性要求不高，就没必要一定要同步调用，可以通过各种异步策略来实现。</p><p>如果线程池的方式满足当前的需求，就没必要引入为的中间件。</p><p>…</p><p>总之，缓存设计简单就好。</p><h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><p><a href="https://github.com/liukelin/canal_mysql_nosql_sync" target="_blank" rel="noopener">基于 Canal 的 MySql RabbitMQ Redis/memcached/mongodb 的nosql同步 </a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;总结下缓存更新思路。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="缓存" scheme="http://simyy.cn/tags/%E7%BC%93%E5%AD%98/"/>
    
  </entry>
  
  <entry>
    <title>JVM Thread State</title>
    <link href="http://simyy.cn/2020/07/26/java-thread-state/"/>
    <id>http://simyy.cn/2020/07/26/java-thread-state/</id>
    <published>2020-07-26T11:24:00.000Z</published>
    <updated>2020-07-28T01:59:22.000Z</updated>
    
    <content type="html"><![CDATA[<p>JVM线程状态与操作系统的线程状态。</p><a id="more"></a><h3 id="JVM-Thread-State"><a href="#JVM-Thread-State" class="headerlink" title="JVM Thread State"></a>JVM Thread State</h3><blockquote><p>虚拟机中线程状态并不是操作系统真实的线程状态。</p></blockquote><p><code>Thread.State</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * A thread can be in only one state at a given point in time.</span></span><br><span class="line"><span class="comment"> * These states are virtual machine states which do not reflect</span></span><br><span class="line"><span class="comment"> * any operating system thread states.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span>   1.5</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> #getState</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> State &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a thread which has not yet started.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    NEW,</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a runnable thread.  A thread in the runnable</span></span><br><span class="line"><span class="comment">     * state is executing in the Java virtual machine but it may</span></span><br><span class="line"><span class="comment">     * be waiting for other resources from the operating system</span></span><br><span class="line"><span class="comment">     * such as processor.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    RUNNABLE,</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a thread blocked waiting for a monitor lock.</span></span><br><span class="line"><span class="comment">     * A thread in the blocked state is waiting for a monitor lock</span></span><br><span class="line"><span class="comment">     * to enter a synchronized block/method or</span></span><br><span class="line"><span class="comment">     * reenter a synchronized block/method after calling</span></span><br><span class="line"><span class="comment">     * &#123;<span class="doctag">@link</span> Object#wait() Object.wait&#125;.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    BLOCKED,</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a waiting thread.</span></span><br><span class="line"><span class="comment">     * A thread is in the waiting state due to calling one of the</span></span><br><span class="line"><span class="comment">     * following methods:</span></span><br><span class="line"><span class="comment">     * &lt;ul&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> Object#wait() Object.wait&#125; with no timeout&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> #join() Thread.join&#125; with no timeout&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> LockSupport#park() LockSupport.park&#125;&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     * &lt;/ul&gt;</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * &lt;p&gt;A thread in the waiting state is waiting for another thread to</span></span><br><span class="line"><span class="comment">     * perform a particular action.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * For example, a thread that has called &lt;tt&gt;Object.wait()&lt;/tt&gt;</span></span><br><span class="line"><span class="comment">     * on an object is waiting for another thread to call</span></span><br><span class="line"><span class="comment">     * &lt;tt&gt;Object.notify()&lt;/tt&gt; or &lt;tt&gt;Object.notifyAll()&lt;/tt&gt; on</span></span><br><span class="line"><span class="comment">     * that object. A thread that has called &lt;tt&gt;Thread.join()&lt;/tt&gt;</span></span><br><span class="line"><span class="comment">     * is waiting for a specified thread to terminate.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    WAITING,</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a waiting thread with a specified waiting time.</span></span><br><span class="line"><span class="comment">     * A thread is in the timed waiting state due to calling one of</span></span><br><span class="line"><span class="comment">     * the following methods with a specified positive waiting time:</span></span><br><span class="line"><span class="comment">     * &lt;ul&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> #sleep Thread.sleep&#125;&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> Object#wait(long) Object.wait&#125; with timeout&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> #join(long) Thread.join&#125; with timeout&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> LockSupport#parkNanos LockSupport.parkNanos&#125;&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     *   &lt;li&gt;&#123;<span class="doctag">@link</span> LockSupport#parkUntil LockSupport.parkUntil&#125;&lt;/li&gt;</span></span><br><span class="line"><span class="comment">     * &lt;/ul&gt;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    TIMED_WAITING,</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Thread state for a terminated thread.</span></span><br><span class="line"><span class="comment">     * The thread has completed execution.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    TERMINATED;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>RUNNABLE</code>状态需要注意一下，包含了<code>就绪等待</code>与<code>运行</code>两种实际的运行情况。</p><p><code>BLOCKED</code>与<code>Monitor Lock</code>有关。</p><p><code>WAITING</code>/<code>TIMED_WAITING</code>也同样是基于<code>Monitor</code>实现。</p><figure class="highlight cpp"><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="class"><span class="keyword">class</span> <span class="title">ObjectMonitor</span> &#123;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">volatile</span> markOop   _header;              <span class="comment">// 对象头</span></span><br><span class="line">  <span class="keyword">void</span>*     <span class="keyword">volatile</span> _object;              <span class="comment">// 关联对象</span></span><br><span class="line">  <span class="keyword">void</span> *  <span class="keyword">volatile</span> _owner;                 <span class="comment">// 锁的持有者</span></span><br><span class="line">  ObjectWaiter * <span class="keyword">volatile</span> _EntryList ;     <span class="comment">// 阻塞线程列表</span></span><br><span class="line">  ObjectWaiter * <span class="keyword">volatile</span> _WaitSet;        <span class="comment">// 调用wait()等待monitor的线程</span></span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="Linux-Process-State"><a href="#Linux-Process-State" class="headerlink" title="Linux Process State"></a>Linux Process State</h3><blockquote><p><code>Linux</code>仅有定义<code>Process State</code>，也就是进程状态，具体包含五个状态：</p><ul><li><code>TASK_RUNNING(运行)</code></li><li><code>TASK_INTERRUPTIBLE(中断)</code></li><li><code>TASK_UNINTERUPTIBLE(不可中断)</code></li><li><code>_TASK_TRACED(跟踪)</code></li><li><code>_TASK_STOPPED(停止)</code></li></ul></blockquote><h3 id="Java-Thread-yield"><a href="#Java-Thread-yield" class="headerlink" title="Java Thread yield"></a>Java Thread yield</h3><p><code>Thread.yield</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><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="comment">/**</span></span><br><span class="line"><span class="comment">  * A hint to the scheduler that the current thread is willing to yield</span></span><br><span class="line"><span class="comment">  * its current use of a processor. The scheduler is free to ignore this</span></span><br><span class="line"><span class="comment">  * hint.</span></span><br><span class="line"><span class="comment">  *</span></span><br><span class="line"><span class="comment">  * &lt;p&gt; Yield is a heuristic attempt to improve relative progression</span></span><br><span class="line"><span class="comment">  * between threads that would otherwise over-utilise a CPU. Its use</span></span><br><span class="line"><span class="comment">  * should be combined with detailed profiling and benchmarking to</span></span><br><span class="line"><span class="comment">  * ensure that it actually has the desired effect.</span></span><br><span class="line"><span class="comment">  *</span></span><br><span class="line"><span class="comment">  * &lt;p&gt; It is rarely appropriate to use this method. It may be useful</span></span><br><span class="line"><span class="comment">  * for debugging or testing purposes, where it may help to reproduce</span></span><br><span class="line"><span class="comment">  * bugs due to race conditions. It may also be useful when designing</span></span><br><span class="line"><span class="comment">  * concurrency control constructs such as the ones in the</span></span><br><span class="line"><span class="comment">  * &#123;<span class="doctag">@link</span> java.util.concurrent.locks&#125; package.</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">static</span> <span class="keyword">native</span> <span class="keyword">void</span> <span class="title">yield</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><p><code>Thread.yield</code>不会改变<code>RUNNABLE</code>的线程状态，只是会让出当前处理器进入就绪状态，等待下一次时间分片的分配执行。</p><p><a href="http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/tip/src/share/vm/prims/jvm.cpp" target="_blank" rel="noopener">openjdk jvm.cpp</a></p><figure class="highlight cpp"><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">JVM_ENTRY(<span class="keyword">void</span>, JVM_Yield(JNIEnv *env, jclass threadClass))</span><br><span class="line">  JVMWrapper(<span class="string">"JVM_Yield"</span>);</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// When ConvertYieldToSleep is off (default), this matches the classic VM use of yield.</span></span><br><span class="line">  <span class="comment">// Critical for similar threading behaviour</span></span><br><span class="line">  <span class="keyword">if</span> (ConvertYieldToSleep) &#123;</span><br><span class="line">    os::sleep(thread, MinSleepInterval, <span class="literal">false</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    os::yield();</span><br><span class="line">  &#125;</span><br><span class="line">JVM_END</span><br></pre></td></tr></table></figure><h3 id="Java-Thread-join"><a href="#Java-Thread-join" class="headerlink" title="Java Thread join"></a>Java Thread join</h3><p><code>Thread.join</code>是让当前线程进入<code>BLOCKED</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><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></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">synchronized</span> <span class="keyword">void</span> <span class="title">join</span><span class="params">(<span class="keyword">long</span> millis)</span></span></span><br><span class="line"><span class="function">    <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">if</span> (millis == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">while</span> (isAlive()) &#123;</span><br><span class="line">            wait(<span class="number">0</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        # 空转</span><br><span class="line">        <span class="keyword">while</span> (isAlive()) &#123;</span><br><span class="line">            <span class="keyword">long</span> delay = millis - now;</span><br><span class="line">            <span class="keyword">if</span> (delay &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            wait(delay);</span><br><span class="line">            now = System.currentTimeMillis() - base;</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>从源码中可知，<code>Thread.join</code>是利用<code>synchronized</code>的<code>Monitor</code>来实现的线程的阻塞。</p><h3 id="Java-Thread-sleep"><a href="#Java-Thread-sleep" class="headerlink" title="Java Thread sleep"></a>Java Thread sleep</h3><p><code>Thread.sleep</code>是让当前线程进入<code>TIMED_WAITING</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><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="comment"> * Causes the currently executing thread to sleep (temporarily cease</span></span><br><span class="line"><span class="comment"> * execution) for the specified number of milliseconds, subject to</span></span><br><span class="line"><span class="comment"> * the precision and accuracy of system timers and schedulers. The thread</span></span><br><span class="line"><span class="comment"> * does not lose ownership of any monitors.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span>  millis</span></span><br><span class="line"><span class="comment"> *         the length of time to sleep in milliseconds</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span>  IllegalArgumentException</span></span><br><span class="line"><span class="comment"> *          if the value of &#123;<span class="doctag">@code</span> millis&#125; is negative</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span>  InterruptedException</span></span><br><span class="line"><span class="comment"> *          if any thread has interrupted the current thread. The</span></span><br><span class="line"><span class="comment"> *          &lt;i&gt;interrupted status&lt;/i&gt; of the current thread is</span></span><br><span class="line"><span class="comment"> *          cleared when this exception is thrown.</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">static</span> <span class="keyword">native</span> <span class="keyword">void</span> <span class="title">sleep</span><span class="params">(<span class="keyword">long</span> millis)</span> <span class="keyword">throws</span> InterruptedException</span>;</span><br></pre></td></tr></table></figure><p>下面是JVM中源码实现，</p><p>首先，调用<code>thread-&gt;osthread()-&gt;set_state(SLEEPING)</code>，设置当前线程的状态，</p><p>然后，调用<code>os::sleep(thread, millis, true)</code>，触发<code>os::sleep</code>功能。</p><blockquote><p><code>os::sleep</code>的大概流程：</p><ul><li>挂起进程（或线程）并修改其运行状态；</li><li>用<code>os::sleep</code>提供的参数来设置一个定时器；</li><li>当定时器会触发，内核收到中断后修改进程（或线程）的运行状态（进入就绪队列等待调度）。</li></ul></blockquote><p><a href="http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/tip/src/share/vm/prims/jvm.cpp" target="_blank" rel="noopener">openjdk jvm.cpp</a></p><figure class="highlight cpp"><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">JVM_ENTRY(<span class="keyword">void</span>, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))</span><br><span class="line">  JVMWrapper(<span class="string">"JVM_Sleep"</span>);</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// Save current thread state and restore it at the end of this block.</span></span><br><span class="line">  <span class="comment">// And set new thread state to SLEEPING.</span></span><br><span class="line">  <span class="function">JavaThreadSleepState <span class="title">jtss</span><span class="params">(thread)</span></span>;</span><br><span class="line"></span><br><span class="line">  HS_DTRACE_PROBE1(hotspot, thread__sleep__begin, millis);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (millis == <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// When ConvertSleepToYield is on, this matches the classic VM implementation of</span></span><br><span class="line">    <span class="comment">// JVM_Sleep. Critical for similar threading behaviour (Win32)</span></span><br><span class="line">    <span class="comment">// It appears that in certain GUI contexts, it may be beneficial to do a short sleep</span></span><br><span class="line">    <span class="comment">// for SOLARIS</span></span><br><span class="line">    <span class="keyword">if</span> (ConvertSleepToYield) &#123;</span><br><span class="line">      os::yield();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      ThreadState old_state = thread-&gt;osthread()-&gt;get_state();</span><br><span class="line">      thread-&gt;osthread()-&gt;set_state(SLEEPING);</span><br><span class="line">      os::sleep(thread, MinSleepInterval, <span class="literal">false</span>);</span><br><span class="line">      thread-&gt;osthread()-&gt;set_state(old_state);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    ThreadState old_state = thread-&gt;osthread()-&gt;get_state();</span><br><span class="line">    thread-&gt;osthread()-&gt;set_state(SLEEPING);</span><br><span class="line">    <span class="keyword">if</span> (os::sleep(thread, millis, <span class="literal">true</span>) == OS_INTRPT) &#123;</span><br><span class="line">      <span class="comment">// An asynchronous exception (e.g., ThreadDeathException) could have been thrown on</span></span><br><span class="line">      <span class="comment">// us while we were sleeping. We do not overwrite those.</span></span><br><span class="line">      <span class="keyword">if</span> (!HAS_PENDING_EXCEPTION) &#123;</span><br><span class="line">        HS_DTRACE_PROBE1(hotspot, thread__sleep__end,<span class="number">1</span>);</span><br><span class="line">        <span class="comment">// TODO-<span class="doctag">FIXME:</span> THROW_MSG returns which means we will not call set_state()</span></span><br><span class="line">        <span class="comment">// to properly restore the thread state.  That's likely wrong.</span></span><br><span class="line">        THROW_MSG(vmSymbols::java_lang_InterruptedException(), <span class="string">"sleep interrupted"</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    thread-&gt;osthread()-&gt;set_state(old_state);</span><br><span class="line">  &#125;</span><br><span class="line">  HS_DTRACE_PROBE1(hotspot, thread__sleep__end,<span class="number">0</span>);</span><br><span class="line">JVM_END</span><br></pre></td></tr></table></figure><p>源码中<code>SLEEPING</code>状态对应着<code>JVMTI</code>状态如下，实际上就是<code>State.TIMED_WAITING</code>状态。</p><figure class="highlight cpp"><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="comment">// Java Thread Status for JVMTI and M&amp;M use.</span></span><br><span class="line"><span class="comment">// This thread status info is saved in threadStatus field of</span></span><br><span class="line"><span class="comment">// java.lang.Thread java class.</span></span><br><span class="line"><span class="keyword">enum</span> ThreadStatus &#123;</span><br><span class="line">...</span><br><span class="line">SLEEPING = JVMTI_THREAD_STATE_ALIVE +          <span class="comment">// Thread.sleep()</span></span><br><span class="line">       JVMTI_THREAD_STATE_WAITING +</span><br><span class="line">       JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +</span><br><span class="line">       JVMTI_THREAD_STATE_SLEEPING,</span><br><span class="line">...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;JVM线程状态与操作系统的线程状态。&lt;/p&gt;
    
    </summary>
    
      <category term="编程语言" scheme="http://simyy.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
      <category term="编程语言" scheme="http://simyy.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
      <category term="Java" scheme="http://simyy.cn/tags/Java/"/>
    
      <category term="多线程" scheme="http://simyy.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Netty设计原理</title>
    <link href="http://simyy.cn/2020/07/19/netty/"/>
    <id>http://simyy.cn/2020/07/19/netty/</id>
    <published>2020-07-19T11:23:15.000Z</published>
    <updated>2020-07-22T02:37:12.000Z</updated>
    
    <content type="html"><![CDATA[<p>Netty设计原理。</p><a id="more"></a><h3 id="事件模型"><a href="#事件模型" class="headerlink" title="事件模型"></a>事件模型</h3><p><code>Netty</code>是一个基于<code>Java NIO</code>的高性能的通信框架。</p><p>Netty是<code>Reactor</code>的一种实现，通过分离<code>Boss线程</code>与<code>Worker线程</code>实现高性能的IO操作。</p><p><img src="/images/netty-1.png" alt=""></p><p><code>Selector</code>是事件驱动的核心，是基于<code>select/epoll/kqueue</code>等实现。</p><p><code>BossGroup</code>负责不断循环执行<code>select</code>来获取当前已经就绪的事件。</p><p><code>WorkerGroup</code>负责具体的IO操作(<code>OP_READ</code>/<code>OP_WRITE</code>等)。</p><blockquote><p>注意：BossGroup中线程的数量与绑定的端口有关，如果仅绑定一个端口，线程数为1即可。</p></blockquote><p><code>ServerBootstrapAcceptor</code>负责<code>ParentGroup</code>到<code>ChildGroup</code>之间<code>Channel</code>的传递.</p><h3 id="Channel"><a href="#Channel" class="headerlink" title="Channel"></a>Channel</h3><p><code>Channel</code>是IO操作的媒介，Netty中的Channel与JDK中NIO是一一映射的，</p><ul><li>NioSocketChannel <----> SocketChannel</----></li><li>NioServerSocketChannel <----> ServerSocketChannel</----></li></ul><p><code>Channel</code>是NIO通信的基础，<code>selector</code>是需要注册到<code>Channel</code>来能实现事件驱动的绑定。</p><h4 id="pipeline"><a href="#pipeline" class="headerlink" title="pipeline"></a>pipeline</h4><p>Netty实现了一套<code>pipeline</code>以支持IO流的自定义处理。</p><p><img src="/images/netty-2.png" alt=""></p><p>如图所示，<code>pipeline</code>保存了用于此Channel的所有Handler，</p><p>其中，可以被分为两种：</p><ul><li><code>ChannelInboundHandler</code>：处理读操作（对应着请求）的处理器</li><li><code>ChannelOutboundHandler</code>：处理写操作（对应着回复）的处理器</li></ul><p><code>channel.read()</code>是Channel的读操作，调用<code>findContextInbound</code>获取Inbound处理器，</p><p><code>channel.forceFlush()</code>是Channel的写入操作，调用<code>findContextOutbound</code>获取outbound处理器。</p><blockquote><p>在使用过程中，需要注意Inbound/outbound的顺序。</p></blockquote><p>那么，pipeline到底什么用，举个例子：</p><ul><li>利用<code>ByteToMessageDecoder（ChannelInboundHandler）</code>对读到的数据进行解码，</li><li>利用<code>MessageToByteEncoder（ChannelOutboundHandler）</code>来对回复的数据进行编码。</li></ul><h4 id="selector"><a href="#selector" class="headerlink" title="selector"></a>selector</h4><p><code>NioSocketChannel &amp; NioServerSocketChannel</code>使用<code>SelectorProvider</code>来获取当前平台默认的事件驱动模型。</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">SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider()</span><br></pre></td></tr></table></figure><p><code>SelectionKey</code>是<code>selector</code>获取到已就绪的事件，利用<code>processSelectedKey</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><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">void</span> <span class="title">processSelectedKey</span><span class="params">(SelectionKey k, AbstractNioChannel ch)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 如果是写事件</span></span><br><span class="line">    <span class="keyword">if</span> ((readyOps &amp; SelectionKey.OP_WRITE) != <span class="number">0</span>) &#123;</span><br><span class="line">        ch.unsafe().forceFlush();</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="keyword">if</span> ((readyOps &amp; (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != <span class="number">0</span> || readyOps == <span class="number">0</span>) &#123;</span><br><span class="line">        unsafe.read();</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="EventLoop"><a href="#EventLoop" class="headerlink" title="EventLoop"></a>EventLoop</h3><p><code>EventLoop</code>是Netty中用于<code>绑定Channel</code>并<code>处理Channel IO</code>的实现。</p><p>NioEventLoop继承自<code>SingleThreadEventExecutor</code>，也就是说，每一个NioEventLoop就是<code>一个单线程</code>。</p><p><code>SingleThreadEventExecutor</code>是一个单线程的线程池，利用队列实现任务的<code>非阻塞的异步执行</code>。</p><ul><li>运行状态：<code>volatile int state</code>，会涉及到<code>UnSafe.CAS</code>操作</li><li>任务队列：<code>Queue&lt;Runnable&gt; taskQueue</code></li><li>延迟队列：<code>PriorityQueue&lt;ScheduledFutureTask&lt;?&gt;&gt; scheduledTaskQueue</code></li></ul><p>任务执行<code>execute(Runnable task)</code>会把当前任务添加到<code>taskQueue</code>，最后通过<code>runAllTasks</code>来触发任务的执行。</p><p><img src="/images/netty-3.png" alt=""></p><p>上图为<code>NioEventLoop.run</code>函数的大体内容，主要分为两部分，</p><ul><li><code>任务执行</code>：遍历触发<code>taskQueue</code>的缓存的未执行的任务</li><li><code>事件遍历</code>：通过<code>selector.select()</code>获取已就绪的事件</li></ul><h3 id="EventLoopGroup"><a href="#EventLoopGroup" class="headerlink" title="EventLoopGroup"></a>EventLoopGroup</h3><p><code>EventLoopGroup</code>是用来管理<code>EventLoop</code>的，类似于线程池的管理。</p><p>EventLoopGroup继承自<code>EventExecutorGroup</code>，它实现了整个事件池的管理。</p><p><code>MultithreadEventLoopGroup</code>是EventExecutorGroup的抽象实现，负责管理事件执行器的生命周期，</p><p>主要变量有：</p><ul><li>处理器的存储：<code>EventExecutor[] children</code></li><li>处理器的选择：<code>EventExecutorChooserFactory.EventExecutorChooser chooser</code></li></ul><p><code>EventExecutor</code>是基于<code>AbstractExecutorService</code>的实现，等价于<code>Reator模型中的Handler</code>。</p><p>Netty会根据<code>chooser</code>逻辑来选择一个事件执行器来处理Channel上的事件。</p><h3 id="ByteBuf"><a href="#ByteBuf" class="headerlink" title="ByteBuf"></a>ByteBuf</h3><p><code>ByteBuf</code>是Netty提供的一套高性能Byte缓冲区。</p><p><img src="/images/netty-4.png" alt=""></p><p><strong>读写索引</strong></p><p><code>ByteBuf</code>使用<code>readIndex</code>与<code>writeIndex</code>来记录读写情况。</p><p><code>ByteBuffer</code>使用<code>position、limit、capacity</code>来记录读写情况，通过flip切换读写。</p><p><strong>动态扩展</strong></p><p><code>ByteBuf</code>当内存大小不足以写入数据会触发空间扩容（<code>ensureWritable0</code>）。</p><p><code>ByteBuffer</code>的容量固定，超出容量大小会报错。</p><p><strong>零拷贝</strong></p><p><code>CompositeByteBuf</code>是多个ByteBuf的逻辑拼接，使用<code>Component数组</code>来存储ByteBuf的引用，不会产生复制。</p><p>当Component数组大小不足时候，支持自动扩容数组大小，此时的扩容也仅仅涉及到引用的复制。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>Netty是典型的Reactor模型，</p><ul><li>Master与Worker线程分离</li><li>基于线程池的Executor</li><li>基于pipeline的Handler处理机制</li><li>ByteBuf降低缓冲区复制的性能损耗</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Netty设计原理。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="编程语言" scheme="http://simyy.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
      <category term="Java" scheme="http://simyy.cn/tags/Java/"/>
    
      <category term="网络模型" scheme="http://simyy.cn/tags/%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/"/>
    
  </entry>
  
  <entry>
    <title>Redlock - Distributed locks with Redis</title>
    <link href="http://simyy.cn/2020/07/04/redis-redlock/"/>
    <id>http://simyy.cn/2020/07/04/redis-redlock/</id>
    <published>2020-07-04T06:53:10.000Z</published>
    <updated>2020-07-07T11:10:44.000Z</updated>
    
    <content type="html"><![CDATA[<p>Redlock - 基于Redis的分布式锁。</p><a id="more"></a><blockquote><p>除了自己的思考以外，内容来源如下：</p><p><a href="https://redis.io/topics/distlock" target="_blank" rel="noopener">Distributed locks with Redis</a><br><a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener">How to do distributed locking</a><br><a href="http://antirez.com/news/101" target="_blank" rel="noopener">Is Redlock safe?</a></p></blockquote><h3 id="SETNX-amp-SET-NX"><a href="#SETNX-amp-SET-NX" class="headerlink" title="SETNX &amp; SET NX"></a>SETNX &amp; SET NX</h3><p>通常，大家利用Redis实现分布式锁最简单的方案是<code>命令SETNX</code>。</p><p><code>SETNX</code>全称为<code>SET IF NOT EXIST</code>，也就是说如果不存在则设置该值。</p><p><strong>那为什么<code>SETNX</code>可以实现分布式锁的功能呢？</strong></p><p>由于Redis的单线程（无锁化的命令执行）特性，可以保证不会出现多个客户端同时执行SETNX的情况。</p><h4 id="Implements"><a href="#Implements" class="headerlink" title="Implements"></a>Implements</h4><p>在Redlock之前，Redis分布式锁大体包含以下四种实现方式，</p><p>不过仔细考量下这种实现方式就会发现一些问题，具体如下：</p><p><code>① 加锁成功后没有释放锁，导致死锁</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 加锁</span></span><br><span class="line">SETNX LOCK_KEY <span class="number">1</span></span><br><span class="line"><span class="comment">// 业务逻辑</span></span><br><span class="line"><span class="keyword">do</span> somethind ...</span><br><span class="line"><span class="comment">// 释放锁</span></span><br><span class="line">EXPIRE LOCK_KEY <span class="number">0</span></span><br></pre></td></tr></table></figure><p>在上面的<code>业务逻辑</code>中如果出现业务异常导致无法触发<code>释放锁</code>的操作，从而导致<code>LOCK_KEY</code>无法释放，</p><p>此时，其他业务线程就会一致等待<code>LOCK_KEY</code>的释放，从而造成<code>死锁</code>。</p><p><code>② 为解决死锁的问题，加锁后立即设置超时时间，降低死锁可能性</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 加锁</span></span><br><span class="line">SETNX LOCK_KEY <span class="number">1</span></span><br><span class="line"><span class="comment">// 设置过期时间</span></span><br><span class="line">EXPIRE LOCK_KEY <span class="number">5</span>s</span><br><span class="line"><span class="comment">// 业务逻辑</span></span><br><span class="line"><span class="keyword">do</span> somethind ...</span><br><span class="line"><span class="comment">// 释放锁</span></span><br><span class="line">EXPIRE LOCK_KEY <span class="number">0</span></span><br></pre></td></tr></table></figure><p>与①中不同之处在于<code>新增了[设置默认时间]</code>，避免了业务逻辑异常导致无法执行<code>释放锁</code>的操作，</p><p>但仍然存在<code>执行加锁成功但设置默认时间失败</code>的情况，可理解为<code>加锁与过期设置操作的非原子性</code>，也会造成<code>死锁</code>。</p><p><code>③ 利用时间特性来生成特定的LOCK_KEY来解决非原子性操作</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 生成特定的时间锁</span></span><br><span class="line">LOCK_KEY = FUNC(LOCK_KEY, TIME)</span><br><span class="line"><span class="comment">// 加锁</span></span><br><span class="line">SETNX LOCK_KEY <span class="number">1</span></span><br><span class="line"><span class="comment">// 业务逻辑</span></span><br><span class="line"><span class="keyword">do</span> somethind ...</span><br><span class="line"><span class="comment">// 释放锁</span></span><br><span class="line">EXPIRE LOCK_KEY <span class="number">0</span></span><br></pre></td></tr></table></figure><p>利用时间来生成特定的时间锁来解决死锁问题，但是会生成大量无效KEY。</p><p>由于与时间相关，<code>业务逻辑的处理时间可能大于锁的有效期</code>，就会造成<code>业务逻辑还没有处理完的同时其他线程就可以竞争『同一把锁』</code>。</p><blockquote><p>『同一把锁』是指原始锁是同样的，也就是未执行<code>FUNC(LOCK_KEY, TIME)</code>之前的<code>LOCK_KEY</code>。</p><p>当执行完<code>LOCK_KEY = FUNC(LOCK_KEY, TIME)</code>后，LOCK_KEY将与TIME相关，也就是说<code>不同时段使用不同的逻辑锁</code>。</p></blockquote><p><code>④ 使用SET EX NX来解决SETNX与EXPIRE非原子性的问题</code>，</p><blockquote><p><a href="https://redis.io/commands/set" target="_blank" rel="noopener">&gt;= 2.6.12: Added the EX, PX, NX and XX options. </a></p></blockquote><p>此方法需要Redis版本大于等于<code>v2.6.12</code>，如下，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 加锁并设置过期时间</span></span><br><span class="line">SET LOCK_KEY <span class="number">1</span> EX <span class="number">5</span>s NX</span><br><span class="line"><span class="comment">// 业务逻辑</span></span><br><span class="line"><span class="keyword">do</span> somethind ...</span><br><span class="line"><span class="comment">// 释放锁</span></span><br><span class="line">EXPIRE LOCK_KEY <span class="number">0</span></span><br></pre></td></tr></table></figure><p>通过该命令很好的解决了方法②中的不足，也不许引入③中的复杂逻辑，实现了<code>设置与过期的原子性操作</code>，</p><p>但其仍然没有解决<code>业务逻辑超长的执行时间导致的锁自动过期的问题</code>。</p><hr><p>上面讨论的实际上都是<code>如何解决互斥</code>，那么如何保证<code>分布式锁的容错性</code>并没有讨论。</p><p>假设线程<code>锁定的默认时间 &gt; Master节点宕机恢复时间</code>，考虑以下主备场景，</p><ul><li>A线程尝试加锁并加锁成功；</li><li>B线程尝试加锁并加锁失败；</li><li>Master节点宕机；</li><li>尝试切换Slave为Master提供服务；</li><li>C线程尝试加锁并加锁成功；</li></ul><p>以上步骤中，<code>Master节点宕机</code>是关键，再主备场景下主从存在延时，</p><p>假设<code>Master节点</code>宕机前未将A线程的加锁数据同步至<code>Slave节点</code>，</p><p>那么，此时Slave切换为Master后中缺少A线程的加锁数据，从而C线程获取了锁造成了资源的竞争。</p><blockquote><p>最近由于所谓的政治舆论压力，作者将<code>master-slave</code>改为了<code>master-replica</code>。</p></blockquote><h3 id="Redlock"><a href="#Redlock" class="headerlink" title="Redlock"></a>Redlock</h3><p><code>Redlock</code>主要就是解决分布式场景下<code>互斥</code>、<code>死锁</code>以及<code>容错</code>这三个关键问题。</p><blockquote><p>Redlock核心思想：基于Quorum机制，采用大于一半的投票方式来实现分布式锁。</p></blockquote><p><strong>Redlock具体步骤如下：</strong></p><p>① 采用<code>N(奇数)台相互独立</code>的Redis实例<code>参与投票</code>；</p><p>② Client随机生成<code>random_val</code>，向实例发送<code>set(key, random_val, nx=True, px=TTL)</code>命令的方式获取<code>投票结果</code>；</p><blockquote><p>每次循环调用Redis实例与上面讲到的单实例情况下分布式锁的实现大体相同，其中，<code>TTL是业务方来控制的，不设置的话有可能导致死锁</code>。</p></blockquote><p>③ 由于①中说明存在多个实例，则Client端通过<code>顺序请求</code>的方式来获取多个实例的投票。</p><blockquote><p>为了避免某个实例问题导致的阻塞情况，增加一个<code>默认的超时时间以防止长时间的阻塞</code>。</p></blockquote><p>④ 完成③中的投票后，统计投票结果以及投票过程的耗时，<code>当通过半数(N/2+1)的(设置)成功投票以及超时校验后则认为成功加锁</code>；</p><blockquote><p>不仅仅需要过半的成功投票，还需要校验整个加锁过程的耗时是否超时，其中锁默认的有效期(defaultValidityTime)是业务相关的。</p><p>当④中成功获取锁后，锁的有效期需要减去投票过程的耗时：<code>validityTime = TTL - elapsedTime</code>。</p></blockquote><p>⑤ Client<code>加锁失败</code>的话需要<code>释放掉已经设置成功的实例</code>。</p><blockquote><p>加锁失败的原因有<code>加锁超时</code>、<code>投票未过半</code>两种情况，无论那种情况都需要释放掉已经设置的锁（这里会尝试释放所有实例的锁）。</p></blockquote><h4 id="Safety"><a href="#Safety" class="headerlink" title="Safety"></a>Safety</h4><p>原文<code>Safety arguments</code>中讨论了Redlock是如何保证加锁的安全性。</p><p><strong>锁定策略</strong></p><p>通过<code>半数投票</code>机制来实现锁定策略，具体实现如下，</p><figure class="highlight python"><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"># N为实例数量，m为投票数量</span></span><br><span class="line"><span class="keyword">if</span> m &gt; N/<span class="number">2</span> + <span class="number">1</span>:</span><br><span class="line">    successed</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">    failed</span><br></pre></td></tr></table></figure><p><strong>锁定时间</strong></p><p>锁的实际有效期计算公式如下，</p><figure class="highlight python"><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"># TTL为锁的过期时间，T1为开始加锁时间，T2为结束加锁时间，CLOCK_DRIFT为时钟偏移</span></span><br><span class="line">MIN_VALIDITY = TTL - (T2 - T1) - CLOCK_DRIFT,</span><br></pre></td></tr></table></figure><blockquote><p>可以理解为：<code>在尝试加锁的开始已经属于锁定时间的一部分</code>，锁的具体锁定时间与加锁过程耗时有关。</p></blockquote><p>此外，如果任务优先执行完会立即释放锁，从而实际锁定时间可能会小于<code>MIN_VALIDITY</code>。</p><h4 id="Liveness"><a href="#Liveness" class="headerlink" title="Liveness"></a>Liveness</h4><p><strong>锁的释放</strong></p><p>锁的释放也包括两种情况：<code>主动释放</code>与<code>自动释放</code>。</p><p>自动释放是利用Redis自动过期实现的，而主动释放是基于Client主动触发来实现的，具体如下，</p><figure class="highlight python"><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="keyword">if</span> processTime &lt; MIN_VALIDITY:</span><br><span class="line">    <span class="keyword">for</span> server <span class="keyword">in</span> servers:</span><br><span class="line">        <span class="keyword">if</span> server.get(LOCK_KEY) == current_val;</span><br><span class="line">            server.<span class="keyword">del</span> LOCK_KEY</span><br></pre></td></tr></table></figure><p>在Client删除实例的LOCK_KEY时会去判断VAL值是否是该节点之前设置过的。</p><h3 id="Unsafety"><a href="#Unsafety" class="headerlink" title="Unsafety"></a>Unsafety</h3><p><strong>STW</strong></p><p><code>Garbage Collector（垃圾回收器）</code>是用于帮助应用自动回收已过期的内存空间的一种辅助工具。</p><p>如何衡量一个垃圾回收器的性能的好坏，大多体现在如何保证更小<code>Stop The World</code>的。</p><p>先看下面这个场景，当STW时间大于持有锁的有效时间时，就会造成分布式锁的异常。</p><p><img src="/images/redis-lock-unsafe-lock.png" alt="STW unsafe lock"></p><p>如图所示，<code>Stop The World</code>导致了线程执行时延，</p><p>线程从暂停中恢复时已经超过了锁的有效期，从而导致锁失效，此时另外一个线程尝试并成功获取锁，造成资源竞争。</p><blockquote><p>原文提到：<code>STW恢复后再次验证锁的有效性</code>来避免锁定超时的情况。</p><p>但这里其实存在一个问题：<code>如果进程执行了一部分逻辑怎么办？</code></p><p>在MySQL中，基于undolog与redview的MVCC实现了事务的功能。</p><p>那么在分布式的场景下业务如何保证锁失效导致的资源竞争的问题，是支持业务回滚？还是支持垃圾数据的容错？</p><p>实际上，无论进程在做什么都应该有一个耗时的上限，而这个上限是由具体的业务场景决定的（甚至需要考虑STW的时间）。</p></blockquote><p><strong>Blocking</strong></p><p>除了垃圾回收造成的长时间STW，进程运行过程中会遇到资源抢占等一系列造成阻塞的问题。</p><p>例如，磁盘容量不足、内存容量不足、IO阻塞、CPU的异常抢占策略等等，都有可能造成进程长时间的Blocking。</p><h3 id="Safety-with-fencing-token"><a href="#Safety-with-fencing-token" class="headerlink" title="Safety with fencing token"></a>Safety with fencing token</h3><p><img src="/images/redis-lock-fencing-tokens.png" alt="fencing-tokens"></p><p>Martin提出<code>fencing token</code>来解决上面延时导致的过期问题。</p><blockquote><p><code>fencing token</code>可以理解为<code>zookeeper中自增的zxid</code>或<code>znode的版本号</code>。</p><p>zookeeper中，zxid是用于实现事务的全局有序，而znode的版本号是用来解决资源竞争的新老问题（也就是fancing token）。</p></blockquote><p><code>fencing token</code>是用于解决新老版本的问题，在分布式锁场景下用于解决锁异常情况下的有效性问题。</p><p><code>fencing token</code>一定是<code>全局唯一</code>且<code>全局有序</code>，</p><ul><li><code>全局唯一</code>是为了保证锁的唯一性，不允许存在两个进程获取到同一把锁；</li><li><code>全局有序</code>是为了保证锁的有效性，总会认为最后持有的锁有效，也就是所谓的ID最大的有效。</li></ul><hr><p>Antirez并不认为Redlock存在这个问题，由于Redlock实现了随机数的VAL机制保证了唯一性，已经满足了锁竞争的场景。</p><p>如果进程获取锁当前的VAL与本地设置的VAL不同，那么自然可以认为锁已经被抢占了。</p><h3 id="Safety-with-timing-assumptions"><a href="#Safety-with-timing-assumptions" class="headerlink" title="Safety with timing assumptions"></a>Safety with timing assumptions</h3><p>Martin提出Redlock锁的有效期与Redis实例的时钟强依赖，而Redis实例的时钟是不可靠的。</p><p>假设一种场景，假设共五个Redis实例1~5，两个客户端A与B参与投票：</p><ul><li>客户端A 在 1、2、3 三个节点抢占成功；</li><li>此时，节点3时钟前跳，导致客户端A的抢占失效（客户端A并无感知）；</li><li>客户端B 在 3、4、5 两个节点抢占成功；</li></ul><p>因此，当时钟发生异常的情况下，客户端A与客户端B均成功获取锁，导致分布式锁失效。</p><p><img src="/images/redis-lock-time-forward.png" alt="time forward"></p><p>此外，linux提供了两种获取系统时间：<code>CLOCK_REALTIME</code>与<code>CLOCK_MONOTONIC</code><a href="https://stackoverflow.com/questions/3523442/difference-between-clock-realtime-and-clock-monotonic" target="_blank" rel="noopener">参考</a>，</p><ul><li><code>CLOCK_REALTIME</code>：会发生向前或向后的时钟跳跃，包括认为修改与NTP的影响；</li><li><code>CLOCK_MONOTONIC</code>：只会收到NTP的影响，合理的运维可以尽量避免此问题。</li></ul><blockquote><p>Redis使用<code>CLOCK_REALTIME</code>，存在时钟跳跃的风险。</p></blockquote><h3 id="Is-Redlock-safe"><a href="#Is-Redlock-safe" class="headerlink" title="Is Redlock safe"></a>Is Redlock safe</h3><p>Redis作者antirez对Martin提出的不安全问题给出了自己的理由。</p><p>Redlock是一种基于客户端实现分布式锁的思路，不仅仅适用于redis。</p><p>Redlock的目标用于是哪些使用原本使用Redis作为分布式锁的用户，提高原有分布式锁的容错性。</p><p>类似于Zookeeper，只要满足半数投票就可以认为加锁成功，允许部分机器出现异常情况。</p><hr><p>Redlock无法保证绝对的安全性，这也是它可以实现高性能的关键。</p><p>常见的共识系统为了避免时钟问题，使用<code>fencing token</code>来的全局有序来保证绝对的安全性，而如何保证全局有序又将成为为系统性能的瓶颈。</p><h3 id="Thoughts"><a href="#Thoughts" class="headerlink" title="Thoughts"></a>Thoughts</h3><p>虽然很久之前就有打算对这块内容整理一下，不过一拖再拖。</p><p>微服务场景下通常会涉及到分布式锁的问题，常用的实现有：</p><ul><li>MySQL</li><li>Redis</li><li>Zookeeper</li><li>Etcd</li></ul><p>其中，基于数据库来实现可以解决了大部分的场景，ZK/ETCD提供了强一致性的保证，Redis提供了更好的性能。</p><p>如何选择还是要看业务场景，分布式锁对于微服务来说等同于JVM的STW、PYTHON中的GIL，无锁话设计才是性能提升的关键。</p><h3 id="Reading"><a href="#Reading" class="headerlink" title="Reading"></a>Reading</h3><p><a href="https://zhuanlan.zhihu.com/p/151436396" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/151436396</a><br><a href="https://dbaplus.cn/news-159-3080-1.html" target="_blank" rel="noopener">https://dbaplus.cn/news-159-3080-1.html</a><br><a href="https://zhuanlan.zhihu.com/p/76294773" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/76294773</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Redlock - 基于Redis的分布式锁。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="Redis" scheme="http://simyy.cn/tags/Redis/"/>
    
      <category term="分布式锁" scheme="http://simyy.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"/>
    
  </entry>
  
  <entry>
    <title>HTTPS</title>
    <link href="http://simyy.cn/2020/06/29/https/"/>
    <id>http://simyy.cn/2020/06/29/https/</id>
    <published>2020-06-29T04:34:45.000Z</published>
    <updated>2020-07-01T08:13:48.000Z</updated>
    
    <content type="html"><![CDATA[<p>记录HTTPS认证过程。</p><a id="more"></a><p><a href="https://zh.wikipedia.org/wiki/超文本传输安全协议" target="_blank" rel="noopener">HTTPS</a>是基于<code>SSL/TLS</code>来加密的HTTP安全协议。</p><p><img src="/images/https-1.png" alt=""></p><p>如图所示，HTTPS协议可分为两部分：<code>安全认证</code>与<code>加密传输</code>。</p><p><strong>安全认证</strong></p><blockquote><p>Client &amp; Server 利用CA证书验证、RSA非对称加密实现传输密钥的安全传递。</p></blockquote><p>1&gt; Client发起认证申请，向Server请求<code>CA证书</code>及<code>RSA公钥</code>；</p><p>2&gt; Server接收到认证申请，向Client端下发<code>CA证书</code>及<code>RSA公钥</code>；</p><p>3&gt; Client验证<code>CA证书</code>的有效性（签名校验、有效期校验等）；</p><p>4&gt; Client生成<code>AES密钥</code>，向Server发送<code>RSA公钥</code>加密过的<code>AES密钥</code>；</p><p>5&gt; Server接受到加密过的<code>AES密钥</code>，利用<code>RSA私钥</code>解密得到<code>AES密钥</code>。</p><p><strong>加密传输</strong></p><blockquote><p>Client &amp; Server 利用AES对称加密实现数据的加密与解密。</p></blockquote><p>1&gt; Client利用<code>AES密钥</code>加密Request请求；</p><p>2&gt; Server利用<code>AES密钥</code>解密接受到的请求，再次利用<code>AES密钥</code>加密Response返回；</p><p>3&gt; Client端利用<code>AES密钥</code>解密接受到的返回。</p><blockquote><p>那为什么需要使用两种加密方式呢？</p><p>由于RSA适用于不适用于大文件的加密，会导致整个加密过程很缓慢，相反，AES会好很多。</p><p><a href="https://www.quora.com/How-much-faster-is-AES-than-RSA" target="_blank" rel="noopener">How much faster is AES than RSA?</a></p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;记录HTTPS认证过程。&lt;/p&gt;
    
    </summary>
    
      <category term="计算机原理" scheme="http://simyy.cn/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8E%9F%E7%90%86/"/>
    
    
      <category term="计算机原理" scheme="http://simyy.cn/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8E%9F%E7%90%86/"/>
    
      <category term="网络" scheme="http://simyy.cn/tags/%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>InnoDB MVCC</title>
    <link href="http://simyy.cn/2020/06/23/mysql-mvcc/"/>
    <id>http://simyy.cn/2020/06/23/mysql-mvcc/</id>
    <published>2020-06-22T16:25:12.000Z</published>
    <updated>2020-08-10T02:51:54.000Z</updated>
    
    <content type="html"><![CDATA[<p>梳理下MVCC的具体实现。</p><a id="more"></a><p>全文基于MySQLV8.0版本。</p><h3 id="HiddenRow"><a href="#HiddenRow" class="headerlink" title="HiddenRow"></a>HiddenRow</h3><p>InnoDB的每一行数据都隐藏了四个字段，具体如下：</p><ul><li><code>DB_TRX_ID</code>：记录更新该行最后一次的<code>事务ID</code>。</li></ul><blockquote><p>事务ID是不断递增的，最新的事务ID永远大于原有事务ID，便于校验事务是否已经过期。</p></blockquote><ul><li><p><code>DB_ROLL_PTR</code>：记录该行<code>回滚段（Undo Log）的指针</code>，用于查找该行的历史数据。</p></li><li><p><code>DB_ROW_ID</code>：在没有主键的情况下生成的<code>隐藏单调自增ID</code>。</p></li></ul><blockquote><p>被物理删除的DB_ROW_ID会被重用，。</p></blockquote><ul><li><code>DELELE_BIT</code>：用于标识该记录<code>是否被删除</code>。</li></ul><blockquote><p>DELELE_BIT状态位属于逻辑删除，MySQL利用后台运行<code>purge</code>线程来实现异步的物理删除。</p></blockquote><p><img src="/images/mysql-mvcc-1.png" alt=""></p><p>其中，<code>DB_TRX_ID</code>与<code>DB_ROLL_PTR</code>是实现MVCC的关键。</p><h3 id="UndoLog"><a href="#UndoLog" class="headerlink" title="UndoLog"></a>UndoLog</h3><blockquote><p><code>undolog</code>是事务回滚与可重复读的关键。</p></blockquote><p><img src="/images/mysql-mvcc-4.png" alt=""></p><p><code>undolog</code>属于逻辑日志，包含每行数据的所有版本的数据，用于支持事务与MVCC特性。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* undo结构 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trx_undo_t</span> &#123;</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* undo ID */</span></span><br><span class="line">    ulint id;</span><br><span class="line">    <span class="comment">/* 类型：insert or update */</span></span><br><span class="line">    ulint type;  </span><br><span class="line">    <span class="comment">/* 事务ID */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> trx_id;</span><br><span class="line">    <span class="comment">/* undo对应回滚段 */</span></span><br><span class="line">    <span class="keyword">trx_rseg_t</span> *rseg;</span><br><span class="line">    <span class="comment">/* undo链表 */</span></span><br><span class="line">    UT_LIST_NODE_T(<span class="keyword">trx_undo_t</span>) undo_list;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>undo_list</code>是一个链表结构，链表节点正是<code>trx_undo_t</code>，也就是说UndoLog在内存里是按照链表存储的。</p><p>此外，每一个<code>trx_undo_t</code>都绑定了对应的<code>事务ID</code>、<code>操作类型</code>以及对应的<code>回滚段</code>。</p><blockquote><p><code>redolog</code>属于物理日志，被称为重做日志，记录数据的修改，防止数据丢失，主要用于数据库重启恢复的时候被使用。</p><p>为了提高持久化效率，可用<code>Redo Log Buffer（重做日志缓冲）</code>来缓存变更，达到阈值大小或定期flush到<code>Redo Log File（重做日志文件）</code>。</p><p>为确保每次日志都能写入到事务日志文件，每次日志缓存的写入都会调用操作系统<code>fsync操作</code>（将OS Buffer中的日志刷到磁盘上的Log File中）。</p></blockquote><h3 id="trx-sys-t"><a href="#trx-sys-t" class="headerlink" title="trx_sys_t"></a>trx_sys_t</h3><p><code>trx_sys_t</code>可以被称为事务管理系统，包含<code>MVCC</code>、<code>事务</code>以及<code>回滚段</code>的管理。</p><p><img src="/images/mysql-mvcc-2.png" alt=""></p><p>利用<code>volatile</code>来实现<code>待分配事务max_trx_id的可见性</code>。</p><p>活跃的用户事务存储在<code>mysql_trx_list</code>事务链表中。</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/* trx_t类型为节点的链表 */</span></span><br><span class="line"><span class="function"><span class="keyword">typedef</span> <span class="title">UT_LIST_BASE_NODE_T</span><span class="params">(<span class="keyword">trx_t</span>)</span> trx_ut_list_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 事务管理系统 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trx_sys_t</span> &#123;</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* 多版本控制器 */</span></span><br><span class="line">    MVCC *mvcc;</span><br><span class="line">    <span class="comment">/* 下一个被分配的事务ID */</span></span><br><span class="line">    <span class="keyword">volatile</span> <span class="keyword">trx_id_t</span> max_trx_id;</span><br><span class="line">    <span class="comment">/* 最小的活跃事务ID */</span></span><br><span class="line">    <span class="built_in">std</span>::atomic&lt;<span class="keyword">trx_id_t</span>&gt; min_active_id;</span><br><span class="line">    <span class="comment">/* 最大的活跃事务ID */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> rw_max_trx_id;</span><br><span class="line">    <span class="comment">/* 事务链表, 包含用户事务、系统事务与恢复事务 */</span></span><br><span class="line">    <span class="keyword">trx_ut_list_t</span> rw_trx_list;</span><br><span class="line">    <span class="comment">/* 用户事务链表 */</span></span><br><span class="line">    <span class="keyword">trx_ut_list_t</span> mysql_trx_list;</span><br><span class="line">    <span class="comment">/* 回滚段列表，用于存储UndoLog */</span></span><br><span class="line">    Rsegs rsegs;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Rsegs</code>回滚段是由<code>trx_rseg_t</code>组成的数组，也就是说<code>事务管理系统是由多个回滚段组成的</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* trx_rseg_t类型为节点的数组 */</span></span><br><span class="line"><span class="keyword">using</span> Rsegs_Vector = <span class="built_in">std</span>::<span class="built_in">vector</span>&lt;<span class="keyword">trx_rseg_t</span> *, ut_allocator&lt;<span class="keyword">trx_rseg_t</span> *&gt;&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 回滚段 */</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Rsegs</span> &#123;</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">private</span>:</span><br><span class="line">        <span class="comment">/* 回滚段对应的事务ID */</span></span><br><span class="line">        <span class="keyword">trx_id_t</span> m_trx_no;</span><br><span class="line">        <span class="comment">/* 具体的回滚段数据 */</span></span><br><span class="line">        Rsegs_Vector m_rsegs;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="trx-rseg-t"><a href="#trx-rseg-t" class="headerlink" title="trx_rseg_t"></a>trx_rseg_t</h4><p><code>trx_rseg_t</code>是事务回滚段的基本结构，是组成回滚段的基本单元。</p><blockquote><p>UndoLog的存储是基于Rollback Segment实现的。</p></blockquote><p>从回滚段的结构中可以得到：<code>UndoLog</code>是被分为<code>update</code>与<code>insert</code>两种类型的。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 事务回滚段 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trx_rseg_t</span> &#123;</span></span><br><span class="line">    <span class="comment">/* 回滚段id */</span></span><br><span class="line">    ulint id;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* update undo日志缓存及链表 */</span></span><br><span class="line">    UT_LIST_BASE_NODE_T(<span class="keyword">trx_undo_t</span>) update_undo_list;</span><br><span class="line">    UT_LIST_BASE_NODE_T(<span class="keyword">trx_undo_t</span>) update_undo_cached;</span><br><span class="line">    <span class="comment">/* insert undo日志缓存及链表 */</span></span><br><span class="line">    UT_LIST_BASE_NODE_T(<span class="keyword">trx_undo_t</span>) insert_undo_list;</span><br><span class="line">    UT_LIST_BASE_NODE_T(<span class="keyword">trx_undo_t</span>) insert_undo_cached;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>trx_undo_t</code>的具体结构在上面已经介绍过了，是具体存储UndoLog的结构。</p><h3 id="ReadView"><a href="#ReadView" class="headerlink" title="ReadView"></a>ReadView</h3><p><code>ReadView</code>是用来判断undolog中多版本数据的可见性。</p><blockquote><p>每一个事务都包含一个<code>ReadView</code>。</p></blockquote><p><code>trx_t</code>是事务的结构体，每一个事务都包含一个<code>read_view</code>字段。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 事务 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trx_t</span> &#123;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">/* 事务ID */</span></span><br><span class="line">  <span class="keyword">trx_id_t</span> id;</span><br><span class="line">  <span class="comment">/* 解决可见性 */</span></span><br><span class="line">  ReadView *read_view;</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* ReadView事务可见范围 */</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ReadView</span> &#123;</span></span><br><span class="line">    <span class="comment">/* 大于该ID的事务不可见（high water mark） */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> m_low_limit_id;</span><br><span class="line">    <span class="comment">/* 小于该ID的事务不可见（low water mark） */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> m_up_limit_id;</span><br><span class="line">    <span class="comment">/* 创建ReadView的事务ID */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> m_creator_trx_id;</span><br><span class="line">    <span class="comment">/* 创建ReadView时活跃事务 */</span></span><br><span class="line">    <span class="keyword">ids_t</span> m_ids;</span><br><span class="line">    <span class="comment">/* 用于记录可被purge清理的事务 */</span></span><br><span class="line">    <span class="keyword">trx_id_t</span> m_low_limit_no;</span><br><span class="line">    <span class="comment">/* 状态 */</span></span><br><span class="line">    <span class="keyword">bool</span> m_closed;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">typedef</span> <span class="title">UT_LIST_NODE_T</span><span class="params">(ReadView)</span> node_t</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/* 具体view链表 */</span></span><br><span class="line">    byte pad1[<span class="number">64</span> - <span class="keyword">sizeof</span>(<span class="keyword">node_t</span>)];</span><br><span class="line">    <span class="keyword">node_t</span> m_view_list;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> UT_LIST_NODE_T(TYPE)/</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> &#123;</span>/</span><br><span class="line">       TYPE *   prev;       <span class="comment">/* pointer to the previous node, NULL if start of list */</span>/</span><br><span class="line">       TYPE *   next;       <span class="comment">/* pointer to next node, NULL if end of list */</span>/</span><br><span class="line">&#125;/</span><br></pre></td></tr></table></figure><p>InnoDB每次创建一个事务，都会创建一个包含当前活跃事务列表以及保证事务可见性的ReadView。</p><blockquote><p>事务的可见性的判断规则如下：</p><p>① <code>m_ids</code>中大于<code>m_low_limit_id</code>的事务不可见；<br>② <code>m_ids</code>中小于<code>m_up_limit_id</code>的事务可见。</p></blockquote><p><img src="/images/mysql-mvcc-3.png" alt=""></p><hr><p>针对相同事务的不同隔离级别，<code>m_low_limit_id</code>与<code>m_up_limit_id</code>是不同的，</p><ul><li>对于RC来说，已提交事务的数据对当前事务来说是可见的；</li><li>对于RR来说，事务启动前所有未提交事务的数据对当前事务来说都是不可见的。</li></ul><h3 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h3><p><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html" target="_blank" rel="noopener">innodb-multi-versioning</a><br><a href="https://dev.mysql.com/doc/dev/mysql-server/8.0.12/trx0sys_8h_source.html" target="_blank" rel="noopener">trx0sys_8h_source</a><br><a href="https://dev.mysql.com/doc/dev/mysql-server/8.0.12/trx0trx_8h_source.html" target="_blank" rel="noopener">trx0trx_8h_source</a><br><a href="https://dev.mysql.com/doc/dev/mysql-server/8.0.12/read0types_8h_source.html" target="_blank" rel="noopener">read0types_8h_source</a><br><a href="http://www.leviathan.vip/2019/02/14/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-Undo-Log/" target="_blank" rel="noopener">InnoDB事务分析-undo log</a><br><a href="http://www.leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/" target="_blank" rel="noopener">InnoDB的事务分析-MVCC</a><br><a href="https://colinback.github.io/szyblogs/database/2018/06/15/innodb-kernel-12/" target="_blank" rel="noopener">InnoDB 内核阅读笔记（十二）- 事务处理</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;梳理下MVCC的具体实现。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="MySQL" scheme="http://simyy.cn/tags/MySQL/"/>
    
      <category term="数据库" scheme="http://simyy.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="MVCC" scheme="http://simyy.cn/tags/MVCC/"/>
    
  </entry>
  
  <entry>
    <title>线程池的思考</title>
    <link href="http://simyy.cn/2020/06/21/threadpool/"/>
    <id>http://simyy.cn/2020/06/21/threadpool/</id>
    <published>2020-06-20T16:31:38.000Z</published>
    <updated>2020-06-23T02:55:40.000Z</updated>
    
    <content type="html"><![CDATA[<p>到底该如何设计与使用线程池？</p><a id="more"></a><h3 id="线程成本"><a href="#线程成本" class="headerlink" title="线程成本"></a>线程成本</h3><p>线程的创建与消耗释有成本的，大体可以分为：<code>CPU资源的消耗</code>与<code>内存资源的消耗</code>。</p><p><strong>CPU资源的消耗</strong></p><p>CPU资源的消耗是指大量的线程会对CPU进行竞争抢占，<code>大量的线程上下文切换会导致CPU的吞吐量降低</code>。</p><p>那么，该如何设置线程数呢？</p><blockquote><p>Using profiling, you can estimate the ratio of waiting time (WT) to service time (ST) for a typical request.<br>If we call this ratio WT/ST, for an N-processor system, you’ll want to have approximately N*(1+WT/ST) threads<br> to keep the processors fully utilized. <a href="https://www.ibm.com/developerworks/library/j-jtp0730/index.html" target="_blank" rel="noopener">来源</a></p></blockquote><p>假设等待时间为WT，实际运行时间为ST，CPU核心数为N，则最佳线程数Num为：</p><p><code>Num = N * (1 + WT / ST)</code></p><p>考虑两种CPU运算：<code>IO密集型</code>与<code>CPU密集型</code>。</p><hr><p>对于IO密集型来说，线程实际执行的时间很短，假设线程执行时间与线程等待时间近似，</p><p>此时，<code>WT/ST≈1</code>、<code>Num=N*(1+1)=2*N</code>。</p><hr><p>对于CPU密集型来说，线程实际的执行时间很长，会远大于线程等待时间，</p><p>此时，<code>WT/ST≈0</code>、<code>Num=N*(1+0)=N</code>，</p><p>但由于线程切换是基于等待线程的，因此需要增加一个等待线程来避免新线程创建导致的CPU空闲，</p><p>因此，Num = N + 1</p><blockquote><p>上面的计算都是基于CPU充分使用的前提下，那么如果保证CPU性能的冗余来估计线程数量呢？</p><p><code>Num = N * U * (1 + WT / ST)</code>，其中，U为CPU的目标使用率。</p></blockquote><p><strong>内存资源的消耗</strong></p><p>首先，线程的申请包括<code>线程资源的申请</code>、<code>线程资源的初始化</code>以及<code>线程资源的释放</code>。</p><blockquote><p>Java中线程独占的资源有<code>虚拟机栈</code>、<code>本地方法栈</code>与<code>计数器</code>。</p></blockquote><p>除了线程本身的内存占用，线程执行的任务也会占用大量的内存资源。</p><p>线程任务占用的内存与具体的业务逻辑有关，如果使用大量的并发线程必然导致内存资源的不足。</p><hr><p>例如，一个报表系统，通过接收MQ消息，定时触发报表的生成与上传，</p><p>其中，MQ消息接受后会把报表生成与上传的任务放到线程池中异步执行，</p><p>但由于近期数据量翻倍增长，导致报表数据翻倍，从而每个任务占用的内存也翻倍了，</p><p>在使用原有的线程池时，同样的线程数会占用翻倍的内存，从而产生内存不足的情况。</p><blockquote><p>线程执行由于需要资源的申请、初始化，一定会带有一定的时延。</p></blockquote><h3 id="线程池"><a href="#线程池" class="headerlink" title="线程池"></a>线程池</h3><blockquote><p>为什么需要使用线程池？</p><ul><li>线程池降低资源（CPU与内存）消耗；</li><li>线程池提高任务响应速度；</li><li>线程池提供线程的可管理性；</li><li>线程池提供扩展能力（任务拒绝策略）。</li></ul></blockquote><h4 id="核心原理"><a href="#核心原理" class="headerlink" title="核心原理"></a>核心原理</h4><p>线程池的核心原理是<code>线程资源复用</code>与<code>线程资源的隔离与管理</code>。</p><h5 id="线程资源复用"><a href="#线程资源复用" class="headerlink" title="线程资源复用"></a>线程资源复用</h5><blockquote><p>线程池通过保证一定数量存活的线程来实现资源复用。</p></blockquote><p>线程复用的优点：</p><p>① 避免资源的频繁申请与释放，提高CPU与内存的使用效率；</p><p>② 避免资源申请的时延，提高任务响应能力。</p><h5 id="线程资源隔离与管理"><a href="#线程资源隔离与管理" class="headerlink" title="线程资源隔离与管理"></a>线程资源隔离与管理</h5><blockquote><p>线程池通过限制存活线程的数量以及任务队列来实现资源的隔离与管理。</p></blockquote><p><code>资源的隔离</code>是指不同的业务场景使用不同的线程池，从而实现线程资源的隔离。</p><p><code>资源的管理</code>是指线程池可以控制线程的数量、缓存任务的数量以及任务执行的顺序。</p><p>资源隔离与管理的优点：</p><p>① 避免无限的线程申请导致资源耗尽的情况；</p><p>② 协调任务线程与工作线程对CPU资源的竞争，避免大量任务线程对工作线程的资源（CPU与Memory）抢占。</p><h5 id="关键属性"><a href="#关键属性" class="headerlink" title="关键属性"></a>关键属性</h5><p><strong>核心线程数</strong>：默认存活的线程数量<code>CoreSize</code>；</p><p><strong>最大线程数</strong>：最大存活的线程数量<code>MaxPoolSize</code>；</p><p><strong>空闲线程存活时间</strong>：核心线程之外的空闲线程最长存活时间<code>KeepliveTime</code>；</p><p>以上三个参数是用来设置线程池中存活线程的数量，是影响线程池的吞吐量以及任务执行时延的关键。</p><p><strong>任务队列</strong>：队列类型与大小<code>WorkQueue(n)</code>；</p><p><strong>任务拒绝策略</strong>：当线程池无法及时响应任务时的执行策略（仅配置有界队列才会触发）。</p><p>以上两个参数是用来设置线程池可以缓存任务的数量以及过多任务的拒绝策略，是影响线程池的最大响应能力以及降级策略的关键。</p><h5 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h5><p>线程池的实现原理如图所示，</p><p><img src="/images/threadpool-1.png" alt=""></p><p><strong>线程调度策略</strong></p><p>线程池的调度是基于任务队列实现的。</p><blockquote><p>基于任务队列，线程池实现了任务与线程解耦。<br>任务队列负责任务的存储以及分配，工作线程负责任务的执行。 </p></blockquote><p>任务投递到线程池会优先加入任务队列，工作线程会从任务队列竞争获取待处理的任务。</p><p>具体调度规则如下：</p><ul><li>WorkerCount &lt; CoreSize : 创建新线程</li><li>WorkerCount &gt;= CoreSize : 任务添加到阻塞队列</li><li>WorkerCount &gt;= CoreSize &amp; 阻塞队列已满 : 创建新的线程</li><li>WorkerCount &gt;= CoreSize &amp; 阻塞队列已满 &amp; WorkerCount &gt;= MaxPoolSize  : 执行任务拒绝策略</li></ul><p><strong>任务分配策略</strong></p><p>任务分配模式是基于<code>任务队列</code>与<code>拒绝策略</code>组成。</p><p>任务队列可以分为三类：<code>有界队列</code>、<code>无界队列</code>和<code>同步移交队列</code>。</p><p>有界队列是指<code>队列大小有限</code>，缓存任务的数量存在上限，<code>当缓存数量达到上限时会触发拒绝策略</code>。</p><p>无界队列是指<code>队列大小无限</code>，如果线程池的吞吐量不足会导致缓存任务不断增多，由于<code>无界队列不会触发拒绝策略</code>，因此，可能导致<code>内存不足</code>。</p><p>同步队列是指任务直接交给工作线程，<code>不存在任务缓存的情况，提交任务会阻塞至空闲线程的认领</code>。</p><blockquote><p>无界队列：LinkedBlockingQueue、DelayedWorkQueue<br>有界队列：ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue<br>同步队列：SynchronousQueue</p></blockquote><h5 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h5><p><strong>newFixedThreadPool</strong></p><p>使用<code>LinkedBlockingQueue</code>作为无界队列（存在OOM的可能），线程数量固定，因此，此线程池的吞吐量有限。</p><p><strong>newCachedThreadPool</strong></p><p>使用<code>SynchronousQueue</code>作为同步队列，虽然不存在无限增大的任务队列，但是存在无限增加的线程，因此，此线程池的线程数量不可控。</p><p><strong>newScheduledThreadPool</strong></p><p>使用<code>DelayedWorkQueue</code>作为无界队列，线程数量无限，任务数量与线程数量均不可控，因此，此线程池也存在很大的安全隐患。</p><blockquote><p>阿里巴巴开发者手册规范：</p><p>线程池不允许使用Executors去创建，而是通过ThreadPoolExecutor的方式，这样的处理方式让写的同学更加明确线程池的运行规则，避免资源耗尽的风险。</p></blockquote><h4 id="衡量标准"><a href="#衡量标准" class="headerlink" title="衡量标准"></a>衡量标准</h4><p><strong>线程分类</strong></p><p>线程一般可以分为两种类型：<code>IO密集型</code>与<code>计算密集型</code>。</p><p>IO密集型不会占用大量的CPU资源，大部分线程处于IO阻塞的情况，因此可以使用更多的线程来处理任务。</p><p>计算密集型会占用大量的CPU资源，线程切换只会影响计算效率，因此不建议使用大量的并发线程。</p><p><strong>CPU与内存使用率</strong></p><p>大量的计算密集型线程会导致CPU的抢占，会降低CPU的使用效率，导致更大的平均时延。</p><p>大量的IO密集型线程由于IO阻塞会占用更多的内存资源，会降低内存的使用效率，可能造成OOM。</p><p><strong>吞吐量</strong></p><p>吞吐量是指线程池在单位时间内可以执行的任务数量。</p><p>工作线程是影响任务并发量的关键因素，也就是如何设计<code>CoreSize</code>与<code>MaxCoreSize</code>是影响线程池吞吐量的关键。</p><p>此外，业务任务往往会占用大量的内存资源，从而也需要考虑<code>是否存在核心线程数过多导致的资源浪费</code>。</p><h4 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h4><h5 id="并行资源请求"><a href="#并行资源请求" class="headerlink" title="并行资源请求"></a>并行资源请求</h5><p>如果一个业务请求逻辑会涉及多个资源调用，可以使用线程池来并行请求多个资源。</p><p>虽然这种方式一定会降低资源请求的时延，但也会导致线程数量的倍数增长。</p><blockquote><p>队列的选择</p></blockquote><p>一般建议使用有界队列，由于可能触发任务丢弃策略，因此需要自定义实现降级策略。</p><blockquote><p>线程数该如何选择?</p></blockquote><p>吞吐量的计算公式为：<code>TPS = 完成任务数量 / 完成的时间</code></p><p>大量高并发任务请求会造成线程池吞吐量不足的情况，需要考虑降级策略。</p><p>增大线程数量可以提高任务并行能力，从而提供线程池的吞吐量，这种用法其实是很危险的，属于空间换时间的一种策略。</p><p>也存在资源浪费的问题，由于前面集中资源出现问题，导致后面的资源无需加载的情况。</p><blockquote><p>是否采取这样的方式与具体的业务场景有关，线程池是否可控，是否需要考虑降级策略等等。</p></blockquote><h5 id="并行批处理任务"><a href="#并行批处理任务" class="headerlink" title="并行批处理任务"></a>并行批处理任务</h5><p>数据合聚合与处理任务，由于数据量的不断增大，原有顺序执行的方式所需要的时间成倍增加，因此，需要拆分为多个子任务并行执行。</p><p>后台任务一般属于CPU密集型，线程数应该与CPU数量一致，过多线程只会增加CPU的抢占。</p><p>并行任务可能会占用多倍的内存，因此需要仔细计算内存占用情况是否会导致机器内存不足。</p><h4 id="降级策略"><a href="#降级策略" class="headerlink" title="降级策略"></a>降级策略</h4><p>线程池只是一种资源集中管理与优化的手段，它并不能解决资源不足与资源竞争的问题。</p><p>在线程池无法支持当前任务的情况下，需要提前设计适当的降级策略。</p><p>线程池的降级策略实际可以理解为<code>线程池的拒绝策略</code>。</p><blockquote><p>那么如何设计线程池的拒绝策略？</p><p>① 动态调整线程池配置，线程池的配置应该与具体的业务场景有关。</p><p>在高并发的场景下，动态调整线程池的配置对机器CPU、内存会造成不同情况的影响，严重情况下导致系统不可用。</p><p>② 异常报警，是异常监控的必要手段。</p><p>③ 任务持久化到DB、MQ，利用异步线程、延迟消息等方式再次触发。</p></blockquote><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p><a href="https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html" target="_blank" rel="noopener">Java线程池实现原理及其在美团业务中的实践</a><br><a href="http://ifeve.com/how-to-calculate-threadpool-size/" target="_blank" rel="noopener">如何合理地估算线程池大小？</a><br><a href="http://xifxiong.online/java-threadpool/" target="_blank" rel="noopener">Java线程池-ThreadPoolExecutor原理分析</a><br><a href="https://www.ibm.com/developerworks/library/j-jtp0730/index.html" target="_blank" rel="noopener">Thread pools and work queues</a><br><a href="https://www.geeksforgeeks.org/thread-pools-java/" target="_blank" rel="noopener">Thread Pools in Java</a><br><a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html" target="_blank" rel="noopener">Oracle ThreadPoolExecutor</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;到底该如何设计与使用线程池？&lt;/p&gt;
    
    </summary>
    
      <category term="编程语言" scheme="http://simyy.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
      <category term="计算机原理" scheme="http://simyy.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8E%9F%E7%90%86/"/>
    
    
      <category term="编程语言" scheme="http://simyy.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
      <category term="Java" scheme="http://simyy.cn/tags/Java/"/>
    
      <category term="多线程" scheme="http://simyy.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
      <category term="线程池" scheme="http://simyy.cn/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    
  </entry>
  
  <entry>
    <title>Redis 高可用</title>
    <link href="http://simyy.cn/2020/06/07/redis-ha/"/>
    <id>http://simyy.cn/2020/06/07/redis-ha/</id>
    <published>2020-06-07T08:53:13.000Z</published>
    <updated>2020-06-07T08:54:54.000Z</updated>
    
    <content type="html"><![CDATA[<p>简单记录下Redis高可用方案。</p><a id="more"></a><h4 id="Replication"><a href="#Replication" class="headerlink" title="Replication"></a>Replication</h4><p><code>Replication</code>被称为<code>主从</code>架构，通过增加复制副本来提高数据的容错性与服务的可用性。</p><p><img src="/images/redis-ha-1.png" alt=""></p><p>数据的同步方式有<code>AOF与RDB</code>，其中<code>RDB</code>属于内存镜像的全量同步，<code>AOF</code>属于基于操作日志的增量同步。</p><p><strong>优点</strong></p><ul><li>主从架构解决单节点可用性问题；</li><li>读写分离，提高系统性能。</li></ul><p><strong>缺点</strong></p><ul><li>当主节点出现异常情况下，需要手动调整从节点为主节点；</li><li>写压力全部集中在主节点，造成了并发写入性能的瓶颈；</li><li>由于所有的数据复制都是基于单个主节点完成的，无法支持大量的横向扩展。</li></ul><h4 id="Sentinel"><a href="#Sentinel" class="headerlink" title="Sentinel"></a>Sentinel</h4><p><code>Sentinel</code>被称为<code>哨兵</code>，通过增加额外的监控系统来实现主从的动态切换，解决<code>Replication</code>的问题。</p><p><img src="/images/redis-ha-2.png" alt=""></p><p>由于Sentinel仍然存在这但点问题，因此一般会采用多个Sentinel来实现监控集群，实现高可用的双重保护。</p><p><strong>优点</strong></p><ul><li>解决了Replicaion无法自动切换的问题。</li></ul><p><strong>缺点</strong></p><ul><li>引入了额外的监控系统，系统复杂度增大；</li><li>单点写问题仍然没有解决，无法支撑大量的横向扩展。</li></ul><h4 id="Codis"><a href="#Codis" class="headerlink" title="Codis"></a>Codis</h4><p><code>Codis</code>是第三方实现的一套高可用方案，利用中间件实现Redis的集群功能。</p><p><img src="/images/redis-ha-3.png" alt=""></p><p><img src="/images/redis-ha-4.png" alt=""></p><p>其中，<code>codis proxy</code>被设计为无状态，可利用<code>HAProxy</code>或<code>Zookeeper</code>实现高可用。</p><p><strong>优点</strong></p><ul><li>解决了单节点写入的瓶颈；</li><li>支持大量的很想扩展。</li></ul><p><strong>缺点</strong></p><ul><li>引入了多个中间件，运维成本高；</li><li>每个中间件都存在可用性问题；</li><li>无法支持不同key的事务；</li><li>增加了一层代理影响系统性能。</li></ul><h4 id="Cluster"><a href="#Cluster" class="headerlink" title="Cluster"></a>Cluster</h4><p><code>Cluster</code>是<code>无中心化</code>架构，集群中的各个节点相互通信达成共识，并把客户端请求通过<code>slot</code>路由到目标数据节点。</p><p><img src="/images/redis-ha-5.png" alt=""></p><p><strong>优点</strong></p><ul><li>去中心化，高容错性；</li><li>支持大量横向扩容；</li><li>支持高可用；</li><li>不存在单点问题；</li><li>运维成本低。</li></ul><p><strong>缺点</strong></p><ul><li>client复杂，需要处理slot信息；</li><li>不同key会路由到不同的节点，无法支持事务。</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;简单记录下Redis高可用方案。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="缓存" scheme="http://simyy.cn/tags/%E7%BC%93%E5%AD%98/"/>
    
      <category term="分布式" scheme="http://simyy.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Redis" scheme="http://simyy.cn/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>Redis AE事件驱动</title>
    <link href="http://simyy.cn/2020/06/05/redis-ae/"/>
    <id>http://simyy.cn/2020/06/05/redis-ae/</id>
    <published>2020-06-05T10:58:39.000Z</published>
    <updated>2020-06-05T11:24:40.000Z</updated>
    
    <content type="html"><![CDATA[<p>AE事件驱动是Redis命令解析处理的基础。</p><a id="more"></a><h4 id="事件驱动"><a href="#事件驱动" class="headerlink" title="事件驱动"></a>事件驱动</h4><p><code>Redis</code>实现了一套事件驱动器<code>AE</code>，理由也很简单，逻辑简单可控。</p><blockquote><p><code>Memcached</code>的事件驱动时基于<code>libevent</code>的。</p></blockquote><h5 id="事件结构"><a href="#事件结构" class="headerlink" title="事件结构"></a>事件结构</h5><p><img src="/images/redis-ae-1.png" alt=""></p><p><code>aeEventLoop</code>是整个事件驱动的核心，事件的注册与触发都基于此。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 事件循环结构体 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">aeEventLoop</span> &#123;</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">int</span> setsize;                  <span class="comment">// 支持连接的最大句柄数量</span></span><br><span class="line">    aeFileEvent *events;           <span class="comment">// 已注册的事件</span></span><br><span class="line">    aeFiredEvent *fired;           <span class="comment">// 已触发的事件</span></span><br><span class="line">    aeTimeEvent *timeEventHead;     <span class="comment">// 时间事件(定时触发)</span></span><br><span class="line">    aeBeforeSleepProc *beforesleep; <span class="comment">// 事件循环sleep之前的的执行函数 </span></span><br><span class="line">    aeBeforeSleepProc *aftersleep;  <span class="comment">// 事件循环sleep之后的的执行函数</span></span><br><span class="line">    <span class="keyword">void</span> *apidata;                <span class="comment">// select/poll/epoll的API交互数据</span></span><br><span class="line">    ...</span><br><span class="line">&#125; aeEventLoop;</span><br></pre></td></tr></table></figure><p><code>aeFileEvent</code>是具体的事件结构，其中，包含了<code>事件类型</code>与<code>对应的处理函数</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> AE_NONE 0       <span class="comment">// 未注册</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> AE_READABLE 1   <span class="comment">// 可读</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> AE_WRITABLE 2   <span class="comment">// 可写</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> AE_BARRIER 4    <span class="comment">// 强制先写后读</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 文件(IO)事件结构体 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">aeFileEvent</span> &#123;</span></span><br><span class="line">    <span class="comment">/* 读写标记 */</span></span><br><span class="line">    <span class="keyword">int</span> mask; <span class="comment">/* one of AE_(READABLE|WRITABLE|BARRIER) */</span></span><br><span class="line">    aeFileProc *rfileProc; <span class="comment">// 读事件对应的执行函数</span></span><br><span class="line">    aeFileProc *wfileProc; <span class="comment">// 写事件对应的执行函数</span></span><br><span class="line">    <span class="keyword">void</span> *clientData;     <span class="comment">// 事件相关的数据</span></span><br><span class="line">&#125; aeFileEvent;</span><br></pre></td></tr></table></figure><h5 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h5><p><img src="/images/redis-ae-2.png" alt=""></p><p><code>aeCreateEventLoop</code>是用于初始化事件循环结构体。</p><p><code>setsize</code>是Redis支持句柄的数量，在eventloop初始化时用于<code>初始化事件的存储大小</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 初始化事件循环 */</span></span><br><span class="line"><span class="function">aeEventLoop *<span class="title">aeCreateEventLoop</span><span class="params">(<span class="keyword">int</span> setsize)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 初始化事件循环</span></span><br><span class="line">    <span class="keyword">if</span> ((eventLoop = zmalloc(<span class="keyword">sizeof</span>(*eventLoop))) == <span class="literal">NULL</span>) <span class="keyword">goto</span> err;</span><br><span class="line">    <span class="comment">// 设置默认支持最大连接的数量（也就是可存储的事件数量）</span></span><br><span class="line">    eventLoop-&gt;events = zmalloc(<span class="keyword">sizeof</span>(aeFileEvent)*setsize);</span><br><span class="line">    <span class="comment">// 已触发的事件的最大数量与注册事件的最大数量相同</span></span><br><span class="line">    eventLoop-&gt;fired = zmalloc(<span class="keyword">sizeof</span>(aeFiredEvent)*setsize);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 创建事件循环触发器（select/epoll/...）</span></span><br><span class="line">    <span class="comment">// 具体参考：IO多路复用的多种实现</span></span><br><span class="line">    <span class="keyword">if</span> (aeApiCreate(eventLoop) == <span class="number">-1</span>) <span class="keyword">goto</span> err;</span><br><span class="line">    <span class="comment">/* Events with mask == AE_NONE are not set. So let's initialize the</span></span><br><span class="line"><span class="comment">     * vector with it. */</span></span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; setsize; i++)</span><br><span class="line">        eventLoop-&gt;events[i].mask = AE_NONE;</span><br><span class="line">    <span class="keyword">return</span> eventLoop;</span><br><span class="line">    <span class="keyword">return</span> eventLoop;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Redis</code>支持多个地址端口的绑定，文件句柄都存储在<code>server.ipfd</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> ipfd[CONFIG_BINDADDR_MAX]; <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="function"><span class="keyword">void</span> <span class="title">initServer</span><span class="params">(<span class="keyword">void</span>)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// socket.listen绑定监听端口</span></span><br><span class="line">    <span class="keyword">if</span> (server.port != <span class="number">0</span> &amp;&amp;</span><br><span class="line">        listenToPort(server.port,server.ipfd,&amp;server.ipfd_count) == C_ERR)</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 每一个fd创建一个可读事件，并设置socket.accept回调</span></span><br><span class="line">    <span class="keyword">for</span> (j = <span class="number">0</span>; j &lt; server.ipfd_count; j++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,</span><br><span class="line">            acceptTcpHandler,<span class="literal">NULL</span>) == AE_ERR)</span><br><span class="line">            ...</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>aeCreateFileEvent</code>是用于为监听的句柄创建事件及其对应的处理器，其中，<code>aeApiAddEvent</code>存在多种IO多路复用的实现。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 为句柄创建事件 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">aeCreateFileEvent</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> fd, <span class="keyword">int</span> mask,</span></span></span><br><span class="line"><span class="function"><span class="params">        aeFileProc *proc, <span class="keyword">void</span> *clientData)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 获取句柄对应的事件</span></span><br><span class="line">    aeFileEvent *fe = &amp;eventLoop-&gt;events[fd];</span><br><span class="line">    <span class="comment">// 将句柄加入到事件监听中（select/epoll/...）</span></span><br><span class="line">    <span class="comment">// 具体参考：IO多路复用的多种实现</span></span><br><span class="line">    <span class="keyword">if</span> (aeApiAddEvent(eventLoop, fd, mask) == <span class="number">-1</span>)</span><br><span class="line">        <span class="keyword">return</span> AE_ERR;</span><br><span class="line">    fe-&gt;mask |= mask;</span><br><span class="line">    <span class="comment">// 设置事件回调（区分读写）</span></span><br><span class="line">    <span class="keyword">if</span> (mask &amp; AE_READABLE) fe-&gt;rfileProc = proc;</span><br><span class="line">    <span class="keyword">if</span> (mask &amp; AE_WRITABLE) fe-&gt;wfileProc = proc;</span><br><span class="line">    <span class="comment">// 设置客户端信息</span></span><br><span class="line">    fe-&gt;clientData = clientData;</span><br><span class="line">    <span class="keyword">return</span> AE_OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/images/redis-ae-3.png" alt=""></p><p><code>acceptTcpHandler</code>是<code>Socket.Accept</code>接收到新的客户端请求的处理器，用于设置后续的可读事件，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 处理监听到的Accept请求 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">acceptTcpHandler</span><span class="params">(aeEventLoop *el, <span class="keyword">int</span> fd, <span class="keyword">void</span> *privdata, <span class="keyword">int</span> mask)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">while</span>(max--) &#123;</span><br><span class="line">        <span class="comment">// Accept句柄fd监听到的请求，返回客户端句柄cfd</span></span><br><span class="line">        cfd = anetTcpAccept(server.neterr, fd, cip, <span class="keyword">sizeof</span>(cip), &amp;cport);</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">/* Accept成功后设置可读事件 */</span></span><br><span class="line">        acceptCommonHandler(cfd,<span class="number">0</span>,cip);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>acceptCommonHandler</code>是用于Accept后用于<code>初始化客户端并设置可读事件</code>的处理器。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">acceptCommonHandler</span><span class="params">(<span class="keyword">int</span> fd, <span class="keyword">int</span> flags, <span class="keyword">char</span> *ip)</span> </span>&#123;</span><br><span class="line">    client *c;</span><br><span class="line">    <span class="comment">/* 创建客户端（内部初始化可读事件） */</span></span><br><span class="line">    <span class="keyword">if</span> ((c = createClient(fd)) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        ...</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">/* 初始化客户端并设置可读事件 */</span></span><br><span class="line"><span class="function">client *<span class="title">createClient</span><span class="params">(<span class="keyword">int</span> fd)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 初始化客户端</span></span><br><span class="line">    client *c = zmalloc(<span class="keyword">sizeof</span>(client));</span><br><span class="line">    <span class="keyword">if</span> (fd != <span class="number">-1</span>) &#123;</span><br><span class="line">        anetNonBlock(<span class="literal">NULL</span>,fd);                         <span class="comment">// 是否阻塞</span></span><br><span class="line">        anetEnableTcpNoDelay(<span class="literal">NULL</span>,fd);                  <span class="comment">// 是否关闭Nagle算法</span></span><br><span class="line">        <span class="keyword">if</span> (server.tcpkeepalive)</span><br><span class="line">            anetKeepAlive(<span class="literal">NULL</span>,fd,server.tcpkeepalive);   <span class="comment">// 设置keepalive</span></span><br><span class="line">        <span class="comment">// 为当前客户端创建可读事件，并设置读取处理函数readQueryFromClient（下面有详细介绍）</span></span><br><span class="line">        <span class="keyword">if</span> (aeCreateFileEvent(server.el,fd,AE_READABLE,</span><br><span class="line">            readQueryFromClient, c) == AE_ERR)</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>到此，<code>Redis</code>服务端的<code>Socket绑定与监听</code>、<code>客户端初始化</code>等初始化逻辑已经分析完毕。</p></blockquote><h5 id="主流程"><a href="#主流程" class="headerlink" title="主流程"></a>主流程</h5><p>Redis的处理过程是<code>单线程的</code>，事件驱动的核心就在<code>aeMain</code>这个循环体内。</p><blockquote><p>基于内核提供的<code>select/poll/epoll</code>来轮询事件实现循环执行。</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">aeMain</span><span class="params">(aeEventLoop *eventLoop)</span> </span>&#123;</span><br><span class="line">    eventLoop-&gt;stop = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 事件循环</span></span><br><span class="line">    <span class="keyword">while</span> (!eventLoop-&gt;stop) &#123;</span><br><span class="line">        <span class="keyword">if</span> (eventLoop-&gt;beforesleep != <span class="literal">NULL</span>)</span><br><span class="line">            <span class="comment">// 触发事件循环的前置处理器</span></span><br><span class="line">            eventLoop-&gt;beforesleep(eventLoop);</span><br><span class="line">        <span class="comment">// 事件处理</span></span><br><span class="line">        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="事件处理"><a href="#事件处理" class="headerlink" title="事件处理"></a>事件处理</h5><p><img src="/images/redis-ae-4.png" alt=""></p><p><code>aeProcessEvents</code>是具体的处理逻辑，包括了<code>事件获取</code>、<code>事件分发</code>。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 事件处理函数 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">aeProcessEvents</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> flags)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 处理数量</span></span><br><span class="line">    <span class="keyword">int</span> processed = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// IO多路复用API, 获取并设置等待处理的事件数量（eventloop.fired）</span></span><br><span class="line">    <span class="keyword">int</span> numevents = aeApiPoll(eventLoop, tvp);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 触发事件循环的后置处理器</span></span><br><span class="line">    <span class="keyword">if</span> (eventLoop-&gt;aftersleep != <span class="literal">NULL</span> &amp;&amp; flags &amp; AE_CALL_AFTER_SLEEP)</span><br><span class="line">        eventLoop-&gt;aftersleep(eventLoop);</span><br><span class="line">        </span><br><span class="line">    <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="keyword">for</span> (j = <span class="number">0</span>; j &lt; numevents; j++) &#123;</span><br><span class="line">        aeFileEvent *fe = &amp;eventLoop-&gt;events[eventLoop-&gt;fired[j].fd];</span><br><span class="line">        <span class="keyword">int</span> mask = eventLoop-&gt;fired[j].mask;  <span class="comment">// 事件类型</span></span><br><span class="line">        <span class="keyword">int</span> fd = eventLoop-&gt;fired[j].fd;      <span class="comment">// 事件句柄</span></span><br><span class="line">        <span class="keyword">int</span> fired = <span class="number">0</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="keyword">int</span> invert = fe-&gt;mask &amp; AE_BARRIER;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 如果未开启先写后读且为读事件</span></span><br><span class="line">        <span class="keyword">if</span> (!invert &amp;&amp; fe-&gt;mask &amp; mask &amp; AE_READABLE) &#123;</span><br><span class="line">            fe-&gt;rfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">            fired++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果为写事件</span></span><br><span class="line">        <span class="keyword">if</span> (fe-&gt;mask &amp; mask &amp; AE_WRITABLE) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!fired || fe-&gt;wfileProc != fe-&gt;rfileProc) &#123;</span><br><span class="line">                fe-&gt;wfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">                fired++;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果开启先写后读且为读事件</span></span><br><span class="line">        <span class="keyword">if</span> (invert &amp;&amp; fe-&gt;mask &amp; mask &amp; AE_READABLE) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!fired || fe-&gt;wfileProc != fe-&gt;rfileProc) &#123;</span><br><span class="line">                fe-&gt;rfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">                fired++;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 处理数量+1</span></span><br><span class="line">        processed++;</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="keyword">if</span> (flags &amp; AE_TIME_EVENTS)</span><br><span class="line">        processed += processTimeEvents(eventLoop);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> processed; <span class="comment">/* return the number of processed file/time events */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>从上面的源码分析得出：<code>事件的收集工作是系统负责的，Redis仅通过每次的循环来不断拿到最新的触发事件</code>。</p></blockquote><p><strong>AE事件驱动的原理：IO多路复用</strong></p><p>在上面的处理过程中，使用了<code>aeApiPoll</code>这个函数，按不同平台的实现方式有：</p><ul><li><code>ae_evport</code>：Solaris 10</li><li><code>ae_kqueue</code>：OS X / FreeBSD</li><li><code>ae_select</code>：通用的</li><li><code>ae_epoll</code>：Linux</li></ul><blockquote><p><a href="https://stackoverflow.com/questions/26420947/what-are-the-underlying-differences-among-select-epoll-kqueue-and-evport" target="_blank" rel="noopener">What are the underlying differences among select, epoll, kqueue, and evport?</a></p></blockquote><p><code>Select</code>最多支持1024个文件句柄，由于每次都需要遍历所有的操作符的状态，因此，时间复杂度是O(n)；</p><p><code>Evport</code>、<code>Epoll</code>、<code>KQueue</code>支持更多的文件句柄，基于系统的实现策略不需要遍历操作符，时间复杂度是O(1)。</p><p>这里仅分析基于<code>epoll</code>的实现。</p><h6 id="事件注册"><a href="#事件注册" class="headerlink" title="事件注册"></a>事件注册</h6><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">aeApiAddEvent</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> fd, <span class="keyword">int</span> mask)</span> </span>&#123;</span><br><span class="line">    aeApiState *state = eventLoop-&gt;apidata;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">ee</span> = &#123;</span><span class="number">0</span>&#125;;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 识别读写事件，设置EPOLLIN/EPOLLOUT</span></span><br><span class="line">    <span class="keyword">if</span> (mask &amp; AE_READABLE) ee.events |= EPOLLIN;</span><br><span class="line">    <span class="keyword">if</span> (mask &amp; AE_WRITABLE) ee.events |= EPOLLOUT;</span><br><span class="line">    ee.data.fd = fd;</span><br><span class="line">    <span class="comment">// 调用epoll的epoll_ctl注册需要监听的事件</span></span><br><span class="line">    <span class="keyword">if</span> (epoll_ctl(state-&gt;epfd,op,fd,&amp;ee) == <span class="number">-1</span>) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h6 id="事件获取"><a href="#事件获取" class="headerlink" title="事件获取"></a>事件获取</h6><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// AE事件获取</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">aeApiPoll</span><span class="params">(aeEventLoop *eventLoop, struct timeval *tvp)</span> </span>&#123;</span><br><span class="line">    aeApiState *state = eventLoop-&gt;apidata;</span><br><span class="line">    <span class="keyword">int</span> retval, numevents = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用epoll的epoll_wait获取等待事件（类似select调用）</span></span><br><span class="line">    retval = epoll_wait(state-&gt;epfd,state-&gt;events,eventLoop-&gt;setsize,</span><br><span class="line">            tvp ? (tvp-&gt;tv_sec*<span class="number">1000</span> + tvp-&gt;tv_usec/<span class="number">1000</span>) : <span class="number">-1</span>);</span><br><span class="line">    <span class="keyword">if</span> (retval &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">int</span> j;</span><br><span class="line">        <span class="comment">// 循环遍历所有已触发的事件</span></span><br><span class="line">        numevents = retval;</span><br><span class="line">        <span class="keyword">for</span> (j = <span class="number">0</span>; j &lt; numevents; j++) &#123;</span><br><span class="line">            <span class="keyword">int</span> mask = <span class="number">0</span>;</span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> *<span class="title">e</span> = <span class="title">state</span>-&gt;<span class="title">events</span>+<span class="title">j</span>;</span></span><br><span class="line">            <span class="comment">// 识别事件类型</span></span><br><span class="line">            <span class="keyword">if</span> (e-&gt;events &amp; EPOLLIN) mask |= AE_READABLE;</span><br><span class="line">            <span class="keyword">if</span> (e-&gt;events &amp; EPOLLOUT) mask |= AE_WRITABLE;</span><br><span class="line">            <span class="keyword">if</span> (e-&gt;events &amp; EPOLLERR) mask |= AE_WRITABLE;</span><br><span class="line">            <span class="keyword">if</span> (e-&gt;events &amp; EPOLLHUP) mask |= AE_WRITABLE;</span><br><span class="line">            <span class="comment">// 设置链接的文件句柄</span></span><br><span class="line">            eventLoop-&gt;fired[j].fd = e-&gt;data.fd;</span><br><span class="line">            <span class="comment">// 设置事件类型</span></span><br><span class="line">            eventLoop-&gt;fired[j].mask = mask;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> numevents;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h6 id="事件处理-1"><a href="#事件处理-1" class="headerlink" title="事件处理"></a>事件处理</h6><p><strong>读取</strong></p><p><img src="/images/redis-ae-5.png" alt=""></p><p><code>readQueryFromClient</code>是从客户端读取请求数据的处理函数，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 每次读取Buffer的默认大小：16K</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> PROTO_IOBUF_LEN         (1024*16)</span></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">void</span> <span class="title">readQueryFromClient</span><span class="params">(aeEventLoop *el, <span class="keyword">int</span> fd, <span class="keyword">void</span> *privdata, <span class="keyword">int</span> mask)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 客户端连接的配置信息</span></span><br><span class="line">    client *c = (client*) privdata;</span><br><span class="line">    <span class="comment">// 设置读取大小</span></span><br><span class="line">    readlen = PROTO_IOBUF_LEN;</span><br><span class="line">    <span class="comment">// 获取客户端已经读取过的大小</span></span><br><span class="line">    qblen = sdslen(c-&gt;querybuf);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 扩展客户端请求buffer的大小</span></span><br><span class="line">    c-&gt;querybuf = sdsMakeRoomFor(c-&gt;querybuf, readlen);</span><br><span class="line">    <span class="comment">// 读取数据，追加在原有querybuf内容的后面</span></span><br><span class="line">    nread = read(fd, c-&gt;querybuf+qblen, readlen);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 处理客户端请求buffer中的数据</span></span><br><span class="line">    processInputBufferAndReplicate(c);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理Buffer</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">processInputBufferAndReplicate</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!(c-&gt;flags &amp; CLIENT_MASTER)) &#123;</span><br><span class="line">        processInputBuffer(c);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">       ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>执行</strong></p><p><img src="/images/redis-ae-6.png" alt=""></p><p><code>processInputBuffer</code>是用于解析客户端请求的Buffer并调用<code>processCommand</code>执行对应的操作，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 处理缓存内的请求数据 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">processInputBuffer</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 设置当前处理的客户端</span></span><br><span class="line">    server.current_client = c;</span><br><span class="line">    <span class="comment">// 循环读取Buffer并处理</span></span><br><span class="line">    <span class="keyword">while</span>(c-&gt;qb_pos &lt; sdslen(c-&gt;querybuf)) &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">// 根据不同的请求类型，尝试解析请求命令（数据完整），否则，进入下一次循环</span></span><br><span class="line">        <span class="keyword">if</span> (c-&gt;reqtype == PROTO_REQ_INLINE) &#123;</span><br><span class="line">            <span class="keyword">if</span> (processInlineBuffer(c) != C_OK) <span class="keyword">break</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (c-&gt;reqtype == PROTO_REQ_MULTIBULK) &#123;</span><br><span class="line">            <span class="keyword">if</span> (processMultibulkBuffer(c) != C_OK) <span class="keyword">break</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            serverPanic(<span class="string">"Unknown request type"</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="keyword">if</span> (c-&gt;argc == <span class="number">0</span>) &#123;</span><br><span class="line">            resetClient(c);</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">if</span> (processCommand(c) == C_OK) &#123;</span><br><span class="line">                ...</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">    server.current_client = <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><code>processInlineBuffer</code>与<code>processMultibulkBuffer</code>是用于命令解析的，如果解析成功则执行命令，否则，进入下一轮循环读取剩余Buffer。</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 执行命令 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">processCommand</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 处理quit命令</span></span><br><span class="line">    <span class="keyword">if</span> (!strcasecmp(c-&gt;argv[<span class="number">0</span>]-&gt;ptr,<span class="string">"quit"</span>)) &#123;</span><br><span class="line">        addReply(c,shared.ok);</span><br><span class="line">        c-&gt;flags |= CLIENT_CLOSE_AFTER_REPLY;</span><br><span class="line">        <span class="keyword">return</span> C_ERR;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 查询argv[0]对应的命令</span></span><br><span class="line">    c-&gt;cmd = c-&gt;lastcmd = lookupCommand(c-&gt;argv[<span class="number">0</span>]-&gt;ptr);</span><br><span class="line">    ...</span><br><span class="line">    <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="keyword">if</span> (c-&gt;flags &amp; CLIENT_MULTI &amp;&amp;</span><br><span class="line">        c-&gt;cmd-&gt;proc != execCommand &amp;&amp; c-&gt;cmd-&gt;proc != discardCommand &amp;&amp;</span><br><span class="line">        c-&gt;cmd-&gt;proc != multiCommand &amp;&amp; c-&gt;cmd-&gt;proc != watchCommand)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// MULTI 开启事务，命令入队列</span></span><br><span class="line">        queueMultiCommand(c);</span><br><span class="line">        addReply(c,shared.queued);</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">        call(c,CMD_CALL_FULL);</span><br><span class="line">        c-&gt;woff = server.master_repl_offset;</span><br><span class="line">        <span class="keyword">if</span> (listLength(server.ready_keys))</span><br><span class="line">            handleClientsBlockedOnKeys();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> C_OK;</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">void</span> <span class="title">queueMultiCommand</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 申请空间</span></span><br><span class="line">    c-&gt;mstate.commands = zrealloc(c-&gt;mstate.commands,</span><br><span class="line">            <span class="keyword">sizeof</span>(multiCmd)*(c-&gt;mstate.count+<span class="number">1</span>));</span><br><span class="line">    <span class="comment">// 设置命令</span></span><br><span class="line">    mc = c-&gt;mstate.commands+c-&gt;mstate.count;</span><br><span class="line">    mc-&gt;cmd = c-&gt;cmd;</span><br><span class="line">    mc-&gt;argc = c-&gt;argc;</span><br><span class="line">    mc-&gt;argv = zmalloc(<span class="keyword">sizeof</span>(robj*)*c-&gt;argc);</span><br><span class="line">    <span class="built_in">memcpy</span>(mc-&gt;argv,c-&gt;argv,<span class="keyword">sizeof</span>(robj*)*c-&gt;argc);</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>call</code>是在没有事务的情况下，直接执行，</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><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 执行命令 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">call</span><span class="params">(client *c, <span class="keyword">int</span> flags)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 通知监控</span></span><br><span class="line">    <span class="keyword">if</span> (listLength(server.monitors) &amp;&amp;</span><br><span class="line">        !server.loading &amp;&amp;</span><br><span class="line">        !(c-&gt;cmd-&gt;flags &amp; (CMD_SKIP_MONITOR|CMD_ADMIN)))</span><br><span class="line">    &#123;</span><br><span class="line">        replicationFeedMonitors(c,server.monitors,c-&gt;db-&gt;id,c-&gt;argv,c-&gt;argc);</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">    c-&gt;cmd-&gt;proc(c);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 记录慢日志</span></span><br><span class="line">    <span class="keyword">if</span> (flags &amp; CMD_CALL_SLOWLOG &amp;&amp; c-&gt;cmd-&gt;proc != execCommand) &#123;</span><br><span class="line">        <span class="keyword">char</span> *latency_event = (c-&gt;cmd-&gt;flags &amp; CMD_FAST) ?</span><br><span class="line">                              <span class="string">"fast-command"</span> : <span class="string">"command"</span>;</span><br><span class="line">        latencyAddSampleIfNeeded(latency_event,duration/<span class="number">1000</span>);</span><br><span class="line">        slowlogPushEntryIfNeeded(c,c-&gt;argv,c-&gt;argc,duration);</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="keyword">if</span> (flags &amp; CMD_CALL_PROPAGATE &amp;&amp;</span><br><span class="line">        (c-&gt;flags &amp; CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">int</span> propagate_flags = PROPAGATE_NONE;</span><br><span class="line">        <span class="comment">// 传播命令到Slave和AOF</span></span><br><span class="line">        <span class="keyword">if</span> (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);</span><br><span class="line">        <span class="comment">// 强制传播命令到Slaver和AOF</span></span><br><span class="line">        <span class="keyword">if</span> (c-&gt;flags &amp; CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;</span><br><span class="line">        <span class="keyword">if</span> (c-&gt;flags &amp; CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;</span><br><span class="line">        ...</span><br><span class="line">        <span class="keyword">if</span> (propagate_flags != PROPAGATE_NONE &amp;&amp; !(c-&gt;cmd-&gt;flags &amp; CMD_MODULE))</span><br><span class="line">            <span class="comment">// 传播命令</span></span><br><span class="line">            propagate(c-&gt;cmd,c-&gt;db-&gt;id,c-&gt;argv,c-&gt;argc,propagate_flags);</span><br><span class="line">    &#125;</span><br><span class="line">    ...</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">void</span> <span class="title">propagate</span><span class="params">(struct redisCommand *cmd, <span class="keyword">int</span> dbid, robj **argv, <span class="keyword">int</span> argc,</span></span></span><br><span class="line"><span class="function"><span class="params">               <span class="keyword">int</span> flags)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 传播命令给AOF和Slaver</span></span><br><span class="line">    <span class="keyword">if</span> (server.aof_state != AOF_OFF &amp;&amp; flags &amp; PROPAGATE_AOF)</span><br><span class="line">        feedAppendOnlyFile(cmd,dbid,argv,argc);</span><br><span class="line">    <span class="keyword">if</span> (flags &amp; PROPAGATE_REPL)</span><br><span class="line">        replicationFeedSlaves(server.slaves,dbid,argv,argc);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>写入</strong></p><p><img src="/images/redis-ae-7.png" alt=""></p><p><code>addReply</code>是用于给客户端返回执行结果，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 请求返回 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">addReply</span><span class="params">(client *c, robj *obj)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 判断是否需要给客户端返回数据</span></span><br><span class="line">    <span class="keyword">if</span> (prepareClientToWrite(c) != C_OK) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (sdsEncodedObject(obj)) &#123;</span><br><span class="line">        <span class="comment">// 尝试写入响应Buffer，Buffer无法存储的话写入响应链表</span></span><br><span class="line">        <span class="keyword">if</span> (_addReplyToBuffer(c,obj-&gt;ptr,sdslen(obj-&gt;ptr)) != C_OK)</span><br><span class="line">            _addReplyStringToList(c,obj-&gt;ptr,sdslen(obj-&gt;ptr));</span><br><span class="line">    &#125;</span><br><span class="line">    ...</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">int</span> <span class="title">prepareClientToWrite</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 过滤lua脚本</span></span><br><span class="line">    <span class="keyword">if</span> (c-&gt;flags &amp; (CLIENT_LUA|CLIENT_MODULE)) <span class="keyword">return</span> C_OK;</span><br><span class="line">    <span class="comment">// 过滤不需要返回</span></span><br><span class="line">    <span class="keyword">if</span> (c-&gt;flags &amp; (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) <span class="keyword">return</span> C_ERR;</span><br><span class="line">    <span class="comment">// 过滤主线程</span></span><br><span class="line">    <span class="keyword">if</span> ((c-&gt;flags &amp; CLIENT_MASTER) &amp;&amp;</span><br><span class="line">        !(c-&gt;flags &amp; CLIENT_MASTER_FORCE_REPLY)) <span class="keyword">return</span> C_ERR;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* client加入回写队列*/</span></span><br><span class="line">    <span class="keyword">if</span> (!clientHasPendingReplies(c)) clientInstallWriteHandler(c);</span><br><span class="line">    <span class="keyword">return</span> C_OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>clientInstallWriteHandler</code>是客户端回写处理器，它仅会把<code>写入命令缓存到写入队列</code>中，并<code>不会直接返回结果</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 把客户端加入等待回写队列 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">clientInstallWriteHandler</span><span class="params">(client *c)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    c-&gt;flags |= CLIENT_PENDING_WRITE;</span><br><span class="line">    <span class="comment">// 加入到等待回写队列</span></span><br><span class="line">    listAddNodeHead(server.clients_pending_write,c);</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此时，Redis内部对客户端的请求处理完毕，还没有发现哪里完成了最后结果的写入。</p><blockquote><p>再回头看一下<code>beforeSleep</code>到底做了什么？</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 每次循环一次执行一次 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">beforeSleep</span><span class="params">(struct aeEventLoop *eventLoop)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// flush到AOF</span></span><br><span class="line">    flushAppendOnlyFile(<span class="number">0</span>);</span><br><span class="line">    <span class="comment">// 处理等待回写的客户端</span></span><br><span class="line">    handleClientsWithPendingWrites();</span><br><span class="line">    ...</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">int</span> <span class="title">handleClientsWithPendingWrites</span><span class="params">(<span class="keyword">void</span>)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 循环遍历回写队列</span></span><br><span class="line">    listRewind(server.clients_pending_write,&amp;li);</span><br><span class="line">    <span class="keyword">while</span>((ln = listNext(&amp;li))) &#123;</span><br><span class="line">        <span class="comment">// 获取并移除客户端</span></span><br><span class="line">        client *c = listNodeValue(ln);</span><br><span class="line">        c-&gt;flags &amp;= ~CLIENT_PENDING_WRITE;</span><br><span class="line">        listDelNode(server.clients_pending_write,ln);</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">// 尝试向客户端回写数据</span></span><br><span class="line">        <span class="keyword">if</span> (writeToClient(c-&gt;fd,c,<span class="number">0</span>) == C_ERR) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="comment">// 如果未完成回写数据，则注册异步回写事件</span></span><br><span class="line">        <span class="keyword">if</span> (clientHasPendingReplies(c)) &#123;</span><br><span class="line">            ...</span><br><span class="line">            <span class="comment">// 创建写入事件</span></span><br><span class="line">            <span class="keyword">if</span> (aeCreateFileEvent(server.el, c-&gt;fd, ae_flags,</span><br><span class="line">                <span class="comment">/* 回写客户端处理器 */</span></span><br><span class="line">                sendReplyToClient, c) == AE_ERR)</span><br><span class="line">    ...</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">void</span> <span class="title">sendReplyToClient</span><span class="params">(aeEventLoop *el, <span class="keyword">int</span> fd, <span class="keyword">void</span> *privdata, <span class="keyword">int</span> mask)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">/* 尝试向客户端回写数据 */</span></span><br><span class="line">    writeToClient(fd,privdata,<span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/images/redis-ae-8.png" alt=""></p><p><code>writeToClient</code>是用于回写数据的处理函数，回写数据位于<code>client-&gt;bufpos</code>与<code>client-&gt;reply</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 回写数据 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">writeToClient</span><span class="params">(<span class="keyword">int</span> fd, client *c, <span class="keyword">int</span> handler_installed)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 循环读取并回写数据</span></span><br><span class="line">    <span class="keyword">while</span>(clientHasPendingReplies(c)) &#123;</span><br><span class="line">        <span class="comment">// 1. 检查缓冲区并回写</span></span><br><span class="line">        <span class="keyword">if</span> (c-&gt;bufpos &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 调用socket.write来写入数据</span></span><br><span class="line">            nwritten = write(fd,c-&gt;buf+c-&gt;sentlen,c-&gt;bufpos-c-&gt;sentlen);</span><br><span class="line">            ...</span><br><span class="line">        <span class="comment">// 2. 检查reply并回写</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            o = listNodeValue(listFirst(c-&gt;reply));</span><br><span class="line">            ...</span><br><span class="line">            <span class="comment">// 同上，写入数据</span></span><br><span class="line">            nwritten = write(fd, o-&gt;buf + c-&gt;sentlen, objlen - c-&gt;sentlen);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 当前节点写入完成后删除</span></span><br><span class="line">            <span class="keyword">if</span> (c-&gt;sentlen == objlen) &#123;</span><br><span class="line">                c-&gt;reply_bytes -= o-&gt;size;</span><br><span class="line">                listDelNode(c-&gt;reply,listFirst(c-&gt;reply));</span><br><span class="line">                ...</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 当回写字节数超过限制，则暂停回写退出（注册回写事件异步触发）</span></span><br><span class="line">        <span class="keyword">if</span> (totwritten &gt; NET_MAX_WRITES_PER_EVENT &amp;&amp;</span><br><span class="line">            (server.maxmemory == <span class="number">0</span> ||</span><br><span class="line">             zmalloc_used_memory() &lt; server.maxmemory) &amp;&amp;</span><br><span class="line">            !(c-&gt;flags &amp; CLIENT_SLAVE)) <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">return</span> C_OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在看下afterloop到底做了什么，</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">afterSleep</span><span class="params">(struct aeEventLoop *eventLoop)</span> </span>&#123;</span><br><span class="line">    UNUSED(eventLoop);</span><br><span class="line">    <span class="comment">// 如果存在模块，则加全局锁</span></span><br><span class="line">    <span class="keyword">if</span> (moduleCount()) moduleAcquireGIL();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">moduleAcquireGIL</span><span class="params">(<span class="keyword">void</span>)</span> </span>&#123;</span><br><span class="line">    pthread_mutex_lock(&amp;moduleGIL);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>加全局锁的目的是<code>为了避免模块与Redis框架并发读写数据的问题</code>。</p><blockquote><p><code>call</code>命令会自动调用<code>addReply</code>进行数据的回写，</p></blockquote><p>以<code>set</code>命令为例，对应的函数为<code>setGenericCommand</code>，</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">setGenericCommand</span><span class="params">(redisClient *c, <span class="keyword">int</span> flags, robj *key,</span></span></span><br><span class="line"><span class="function"><span class="params">        robj *val, robj *expire, <span class="keyword">int</span> unit, robj *ok_reply,</span></span></span><br><span class="line"><span class="function"><span class="params">        robj *abort_reply)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">//将键值关联到数据库</span></span><br><span class="line">    setKey(c-&gt;db,key,val);</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 回写结果</span></span><br><span class="line">    addReply(c, ok_reply ? ok_reply : shared.ok);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;AE事件驱动是Redis命令解析处理的基础。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="缓存" scheme="http://simyy.cn/tags/%E7%BC%93%E5%AD%98/"/>
    
      <category term="Redis" scheme="http://simyy.cn/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>领域驱动设计的理解与实践</title>
    <link href="http://simyy.cn/2020/05/30/domain-driven-design/"/>
    <id>http://simyy.cn/2020/05/30/domain-driven-design/</id>
    <published>2020-05-30T14:33:56.000Z</published>
    <updated>2020-05-30T14:56:10.000Z</updated>
    
    <content type="html"><![CDATA[<p>记录领域驱动设计的理解与实践。</p><a id="more"></a><h3 id="DDD-amp-架构"><a href="#DDD-amp-架构" class="headerlink" title="DDD &amp; 架构"></a>DDD &amp; 架构</h3><p><code>DDD（Domain Driven Design，领域驱动设计）</code>是基于业务领域实现的一种软件架构方法。</p><blockquote><p><code>DDD</code>的作用是将<code>业务架构</code>转变为<code>系统架构和技术架构</code>。</p></blockquote><p>按照架构角度的不同，可分为如下三类：</p><ul><li><code>业务架构</code>：业务视角，描述业务模块以及关系；</li></ul><p>业务架构的原型往往是业务方最先提出的，包含大体的业务需求。</p><p>完整的业务架构图是由业务、产品、技术在不断的需求迭代与架构演进不断完善的。</p><p><img src="/images/domain-driven-design-01.png" alt=""></p><ul><li><code>应用架构</code>：应用（系统）视角，描述应用系统及其依赖关系；</li></ul><p>应用架构图是架构演进的结果。</p><p>随着业务的发展，会带来组织划分与应用拆分，应用架构也在不断演进。</p><p><img src="/images/domain-driven-design-02.png" alt=""></p><ul><li><code>技术架构</code>：技术视角，描述应用技术选型及其功能作用。</li></ul><p>技术架构是当前系统（应用）的技术细节的描述，包括技术选型以及其应用场景。</p><p>技术架构是面向技术人员的，用于了解软件设计的细节。</p><p><img src="/images/domain-driven-design-03.png" alt=""></p><h3 id="Domain-Driven-Design"><a href="#Domain-Driven-Design" class="headerlink" title="Domain Driven Design"></a>Domain Driven Design</h3><p>开发视角的转变：从单纯的<code>技术视角</code>转变为<code>业务视角</code>。</p><p>开发人员往往从技术角度来剖析需求，大部分情况缺少从业务角度的全局思考，对业务与架构的演进缺少前瞻性导致架构的局限性。</p><blockquote><p>尤其在业务开展的初级，开发人员对业务理解不够深入。</p></blockquote><h4 id="领域专家"><a href="#领域专家" class="headerlink" title="领域专家"></a>领域专家</h4><p><code>领域专家</code>是指那些掌握领域内知识的人，对该领域有很深入的理解。</p><p><code>产品</code>、<code>技术</code>、<code>业务</code>都可以是领域专家，其中，<code>业务</code>一般掌握着最真实的领域知识。</p><p>与<code>领域专家</code>沟通学习是架构设计的基础，无论是产品、技术、架构师，都需要与领域专家深入的学习领域知识。</p><h4 id="通用语言"><a href="#通用语言" class="headerlink" title="通用语言"></a>通用语言</h4><p>由于领域专家并不一定是产品、技术，因此与领域专家的沟通必然存在一定问题（行业不同）。</p><p><code>通用语言</code>是解决领域专家与产品开发沟通的关键。</p><blockquote><p>让大家都能听懂的专业名词，例如，SPU是商品，SKU是某种规格的商品。</p></blockquote><h4 id="领域建模"><a href="#领域建模" class="headerlink" title="领域建模"></a>领域建模</h4><p>基于通用语言，与领域专家沟通学习领域知识，抽象出一套领域知识模型。</p><h5 id="领域-amp-限界上下文"><a href="#领域-amp-限界上下文" class="headerlink" title="领域 &amp; 限界上下文"></a>领域 &amp; 限界上下文</h5><p><code>限界上下文</code>是领域的边界，也是领域模型的边界。</p><blockquote><p><code>康威定律</code>可以作为领域划分的一种规则。</p><p>组织与领域按照一一对应的关系来划分，实现领域的高内聚。</p></blockquote><p>领域内可以包含多个<code>限界上下文</code>，此时，可以划分为多个<code>细分子域</code>。</p><p><img src="/images/domain-driven-design-04.png" alt=""></p><p>根据细分子域的不同性质，又可以划分为：</p><ul><li><code>核心子域</code>：领域的核心，包含该领域内的核心领域知识；</li><li><code>支撑子域</code>：用于支撑核心子域；</li><li><code>通用子域</code>：公共的功能模块。</li></ul><p><img src="/images/domain-driven-design-05.png" alt=""></p><h5 id="架构演进"><a href="#架构演进" class="headerlink" title="架构演进"></a>架构演进</h5><p>领域模型需要不断演进来满足业务的迭代。</p><p>业务迭代会造成领域模型的拆分、增加，都会对原有架构产生巨大的挑战。</p><p><img src="/images/domain-driven-design-06.png" alt=""></p><h5 id="细分子域"><a href="#细分子域" class="headerlink" title="细分子域"></a>细分子域</h5><p>领域划分是从全局来考虑架构结构的划分，属于架构顶层功能模块的划分。</p><p>对于开发人员来说，<code>细分子域的领域模型的构建</code>才是软件实现的关键。</p><blockquote><p>开发人员不仅仅不要掌握细分子域，也需要了解和掌握整理领域的划分。</p></blockquote><h5 id="实体-amp-值对象"><a href="#实体-amp-值对象" class="headerlink" title="实体 &amp; 值对象"></a>实体 &amp; 值对象</h5><p><code>实体</code>是指<code>具有唯一标识</code>的对象，例如，User（实体）包含唯一的用户ID。</p><p><code>值对象</code>是指<code>不包含唯一标识</code>的对象且<code>不可修改</code>，例如，Address对于其他用户来说，只是用户的一个属性，不可修改。</p><p>实体与值对象的区别在于<code>唯一性</code>。</p><p>例如，不同规格的商品属于不同实体；规格只是一种属性，对于商品来说属于值对象。</p><h5 id="聚合-amp-聚合根"><a href="#聚合-amp-聚合根" class="headerlink" title="聚合 &amp; 聚合根"></a>聚合 &amp; 聚合根</h5><p><code>聚合</code>是指一组相关对象的集合，<code>聚合根</code>作为聚合根节点，并由<code>实体</code>与<code>值对象</code>组成。</p><p>聚合属于<code>领域内知识的子集</code>，可包含多个子域知识，用于对外提供领域服务。</p><p><img src="/images/domain-driven-design-07.png" alt=""></p><h5 id="领域模型-amp-领域事件-amp-领域服务"><a href="#领域模型-amp-领域事件-amp-领域服务" class="headerlink" title="领域模型 &amp; 领域事件 &amp; 领域服务"></a>领域模型 &amp; 领域事件 &amp; 领域服务</h5><p><code>领域模型</code>是指<code>领域知识的抽象描述</code>，用于指导软件架构设计。</p><p><code>领域事件</code>是指<code>领域内发生事件的描述</code>，用于驱动关联领域的变更，可采用发布订阅模式实现。</p><p><code>领域服务</code>是指<code>领域对外提供的查询或更新服务</code>，用于外部调用。</p><p><strong>领域建模的步骤</strong></p><ul><li>领域边界、界限上下文的划分；</li><li>确认限界上下文中的核心子域、支撑子域、通用子域；</li><li>确定子域内部的实体、值对象；</li><li>划分聚合，确定聚合根；</li><li>确认限界上下文之间的关系。</li></ul><p><strong>康威定律</strong></p><p><code>康威定律</code>是指系统结构应尽量的与组织结构保持一致。</p><p>对于多个团队来说，按照团队划分顶层领域边界；</p><p>对于团队内部来说，按照小组来划分细分子域的边界。</p><h5 id="架构分类"><a href="#架构分类" class="headerlink" title="架构分类"></a>架构分类</h5><p>软件架构上一般包括为<code>CQRS</code>、<code>六边形架构（适配器架构）</code>、<code>分层架构</code>。</p><p><strong>CQRS</strong></p><p><code>CQRS</code>被称为<code>命令和查询责任分离</code>，通过拆分读写模型来实现避免资源竞争。</p><p>CQRS按照<code>读写</code>把模型拆分为：</p><ul><li><code>命令 - 领域模型</code>：用于持久化，包括领域行为，结构相对简单，事务支持；</li><li><code>查询 - 视图模型</code>：用于展示，属于非规范的模型，不包括领域行为；</li><li><code>通知 - 领域事件</code>：用于发布领域变更事件，包含领域行为。</li></ul><p><strong>六边形架构（适配器架构）</strong></p><p><code>六边形架构（适配器架构）</code>是基于适配器的设计模式的架构模型，</p><p>其主要特性如下：</p><ul><li>利用<code>依赖倒置</code>原则来分离技术框架</li><li>功能模块均采用<code>接口来实现相互依赖</code></li><li>用<code>防腐层与适配器</code>来实现外部技术框架的依赖</li></ul><p>六边形架构屏蔽了外部技术框架的细节，<code>内部功能高内聚，外部依赖低耦合</code>。</p><p><strong>分层架构</strong></p><p><code>分层架构</code>是最常见的架构模型，按照功能层级拆分为：</p><ul><li><code>用户层</code>：聚合多个顶层领域的数据，用于用户展示；</li><li><code>应用层</code>：聚合多个细分子域的数据，用于对外提供服务；</li><li><code>领域层</code>：细分子域的领域模型，可发布领域事件、执行领域行为，包括实体阈值对象的定义；</li><li><code>基础设施层</code>：提供持久化、防腐层等基础中间件的功能。</li></ul><p><img src="/images/domain-driven-design-08.png" alt=""></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;记录领域驱动设计的理解与实践。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="架构" scheme="http://simyy.cn/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="领域建模" scheme="http://simyy.cn/tags/%E9%A2%86%E5%9F%9F%E5%BB%BA%E6%A8%A1/"/>
    
      <category term="DDD" scheme="http://simyy.cn/tags/DDD/"/>
    
  </entry>
  
  <entry>
    <title>架构设计与演进</title>
    <link href="http://simyy.cn/2020/05/27/architecture-evolution/"/>
    <id>http://simyy.cn/2020/05/27/architecture-evolution/</id>
    <published>2020-05-27T11:37:57.000Z</published>
    <updated>2020-05-27T06:41:30.000Z</updated>
    
    <content type="html"><![CDATA[<p>架构设计与演进的思考。</p><a id="more"></a><h6 id="架构到底是什么"><a href="#架构到底是什么" class="headerlink" title="架构到底是什么"></a>架构到底是什么</h6><p>架构也被称为<code>软件架构</code>，是<code>软件结构的抽象描述</code>。</p><p>架构是由<code>架构元素</code>与<code>元素之间的关系</code>组成，<code>元素的划分、选型、交互</code>是架构的关键。</p><blockquote><p>架构元素：组件/服务的划分与技术选型。<br>元素之间的关系：组件/服务之间的关联关系与交互方式。</p></blockquote><h6 id="架构的目标"><a href="#架构的目标" class="headerlink" title="架构的目标"></a>架构的目标</h6><p>架构的目标是<code>解决利益相关者的关注点</code>（<a href="https://www.infoq.cn/article/every-architect-should-study-conway-law" target="_blank" rel="noopener">参考</a>）。</p><blockquote><p><code>利益相关者</code>是与当前架构有之间或间接关系的人，也就是当前架构的目标用户，例如，业务方、产品、开发、测试等等。<br><code>关注点</code>是<code>利益相关者</code>对于当前软件的认知及痛点。</p></blockquote><p>解决不仅仅是解决当前的问题，还要有<code>前瞻性（对未来业务变化的考量</code>）。</p><p>这也就是保证架构的可迭代、可演进。</p><blockquote><p><code>架构是有生命的</code>。</p></blockquote><p>好的架构生命周期很长，支持业务的快速迭代，不断演进、进化，</p><p>相反，不好的架构没办法支持业务的迭代，不得不得对架构进行重构、重写。</p><h6 id="架构的基础"><a href="#架构的基础" class="headerlink" title="架构的基础"></a>架构的基础</h6><p><strong>领域建模</strong></p><blockquote><p><code>好的架构离不开好的领域建模</code>。</p></blockquote><p><code>领域建模</code>是架构设计的基础，它确保了架构设计的边界。</p><p>通过领域建模将领域知识转化为软件架构，这里离不开<code>领域专家</code>的配合。</p><blockquote><p>架构不是凭空想象的，是基于<code>对领域知识（领域专家提供）建模</code>的基础之上来设计的。</p></blockquote><p>对于大部分人来说，当提及领域建模时往往觉得时高大上、遥不可及的事情，事实并非如此。</p><p>领域建模是业务高度抽象的产物，范围可大可小，但需要建模者充分理解并调研领域知识。</p><blockquote><p>对领域知识理解不足，造成的建模不准确？</p><p>不要怕，最好的建模通过演进实现的，试错修正、试错优化。</p></blockquote><p><strong>真正的需求</strong></p><p>架构设计的目标是<code>通过构建合理的元素关系来满足用户的需求</code>。</p><p>由于视角的不同，利益相关者的关注点可能各不相同。</p><p>那么，好的架构一定是基于以下两点：</p><p>① 发现所有直接或间接的利益相关者；<br>② 沟通与理解利益相关者的关注点。</p><p><code>尽可能收集利益相关者的关注点</code>作为架构设计的基础。</p><p><code>如何把关注点转换为领域知识</code>是架构设计的必经之路。</p><p><strong>最小化利益冲突</strong></p><p>在沟通过程中，利益关注者之间往往会出现利益冲突的情况。</p><p>如何最小化或避免利益冲突也属于架构设计的一部分。</p><p>好的架构是<code>基于充分的调研与思考，保持利益相关者之间的利益平衡</code>。</p><blockquote><p><code>大中台小前台</code>的架构设计上是存在<code>业务方与技术方的利益冲突</code>。</p><p>前台业务方考虑的是<code>快速迭代试错</code>，而中台技术方考虑的是<code>平台的稳定性与通用性</code>。</p></blockquote><h6 id="合理架构与过渡设计"><a href="#合理架构与过渡设计" class="headerlink" title="合理架构与过渡设计"></a>合理架构与过渡设计</h6><p>什么是<code>合理架构</code>？</p><p>① 满足<code>大多数利益相关者的关注点</code>（功能点）；</p><blockquote><p>大多数意味着存在取舍，<code>合理的取舍</code>也是架构设计的一部分。</p></blockquote><p>② <code>最少的开发成本、最快的上线速度</code>，满足业务方快速迭代试错；</p><blockquote><p>快与质量差没有任何关系，<code>不要以快为不合格的架构设计而找理由</code>。</p></blockquote><p>③ 可<code>持续演进</code>的架构，需要经得起业务的不断演进。</p><p><code>刚好够用且可持续优化</code>即为合理，一切不以现状为基础的设计均属于<code>过渡设计</code>。</p><blockquote><p>开发人员，往往会把追求极致挂在嘴边，但是，<code>追求极致</code>实际上也是一种过渡设计。<br>追求极致必然会增加功能的复杂度，无论是人力成本还是时间成本都是无法容忍的。</p></blockquote><p>架构设计需要<code>从公司当前的业务、人员、成本等多方面考虑</code>，避免吹毛求疵、过渡设计。</p><blockquote><p><code>&quot;技术债&quot;</code>这个词很流行，但不是所有的技术债都是无法避免的。</p><p>架构的演进伴随着技术债的填补，也伴随着新的技术债的产生，但要<code>时刻警惕非必要技术债的产生</code>。</p></blockquote><h6 id="理想架构与架构演进"><a href="#理想架构与架构演进" class="headerlink" title="理想架构与架构演进"></a>理想架构与架构演进</h6><blockquote><p><code>没有理想的架构，只有最合适的架构</code>。</p></blockquote><p>无论哪个公司都会经历<code>从简到繁的架构演变</code>的过程。</p><p><code>架构的演进</code>是建立在合理架构的基础之上。</p><blockquote><p><a href="/2019/12/21/solid-priciple/">SOLID设计原则</a>主要用于解决如何构建可持续性的软件架构。</p></blockquote><p>架构的演进不仅包含<code>业务逻辑上的演进</code>，也包括<code>领域模型的演进</code>。</p><p><code>业务逻辑上的演进</code>是指随着需求的不断迭代对原有业务逻辑的影响。</p><p><code>领域模型的演进</code>是指随着需求的不断迭代对原有领域模型有了更加清晰的定义，它是构建<code>可持续演进架构</code>的基础。</p><p>架构设计的关键问题是<code>划清边界</code>，而架构演进的关键问题是<code>领域模型的不断演进</code>。</p><p><strong>领域演进</strong></p><p>随着业务的演进，领域模型会面临拆分的问题，同时也会产生各个细分领域的领域专家。</p><p><code>细分领域的领域专家</code>是细分领域建模的关键，也是细分领域架构设计的关键。</p><p>与此同时，架构也会面临一次拆分，不同的细分领域负责各自的架构设计，架构设计更加聚焦，这也是架构演进的必然结果。</p><h6 id="架构闭环与优化"><a href="#架构闭环与优化" class="headerlink" title="架构闭环与优化"></a>架构闭环与优化</h6><p><code>闭环</code>是指拥有一套完整的反馈机制，<code>架构闭环</code>是指拥有一套完整的系统监控与预警机制。</p><p>架构演进包括两部分：<code>性能演进</code>与<code>逻辑演进</code>。</p><blockquote><p>逻辑演进与性能演进是不和分割的。</p><p>逻辑的演进会推动性能的演进，性能演进是为了保证逻辑的演进。</p></blockquote><p>架构闭环是性能演进的必要条件。</p><p>评估架构性能瓶颈、监测系统运行情况是<code>性能演进</code>的关键。</p><h6 id="架构与组织"><a href="#架构与组织" class="headerlink" title="架构与组织"></a>架构与组织</h6><blockquote><p><code>康伟定律</code>：架构与组织是一一对应的关系。</p></blockquote><p>好的架构是<code>依赖优秀的人来实现的</code>，而优秀的人是<code>依赖好的文化来吸引的</code>。</p><hr><p>公司中总能听到一些提效的要素，例如，工具、技术、流程。</p><blockquote><p>我们要使用最先进的工具；<br>我们要使用最牛逼的技术；<br>我们要推广最高效的流程。</p><p>真的是这样吗？</p></blockquote><hr><p>架构设计时需要考虑架构维护成本，一般需要按照组织来拆解架构（组织 : 架构 = 1 : 1）。</p><p>按组织拆解架构的好处在于<code>架构的高内聚</code>，也就是说<code>以组织为单位划分架构设计与演进的职责</code>。</p><p>组织内部的架构设计更加聚焦，领域知识也更加聚焦，</p><p>从而，领域模型更加清晰，架构设计也会相对轻松（由于目标更聚焦了）。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;架构设计与演进的思考。&lt;/p&gt;
    
    </summary>
    
      <category term="架构" scheme="http://simyy.cn/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
      <category term="架构" scheme="http://simyy.cn/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="领域建模" scheme="http://simyy.cn/tags/%E9%A2%86%E5%9F%9F%E5%BB%BA%E6%A8%A1/"/>
    
  </entry>
  
  <entry>
    <title>MySQL 读写分离</title>
    <link href="http://simyy.cn/2020/05/24/mysql-replication-rw-router/"/>
    <id>http://simyy.cn/2020/05/24/mysql-replication-rw-router/</id>
    <published>2020-05-24T01:46:39.000Z</published>
    <updated>2020-05-24T02:31:16.000Z</updated>
    
    <content type="html"><![CDATA[<p>记录下读写分离的使用方式与常见问题。</p><a id="more"></a><h5 id="读写分离"><a href="#读写分离" class="headerlink" title="读写分离"></a>读写分离</h5><p><code>读写分离</code>是通过<code>分离数据库的读写操作</code>，通过横向扩展的能力来<code>提高读性能</code>。</p><p>如图所示，<code>Master</code>称为主库，仅处理数据库的<code>写操作</code>；<code>Slaver</code>称为从库，仅处理数据库的<code>读操作</code>。</p><p>读写分离的实现可划分为两类：<code>基于客户端实现</code>与<code>基于中间件实现</code>。</p><blockquote><p>与分库分表的实现思路相似。</p></blockquote><p><img src="/images/mysql-replication-rw-router-1.png" alt=""></p><p>两种实现的原理都在于<code>请求的动态路由</code>，根据<code>请求的分类&quot;读、写或事务&quot;</code>来动态的路由到指定的数据库实例。</p><p><strong>基于客户端实现</strong></p><p><code>基于客户端实现</code>是通过嵌入业务层来实现请求路由的功能，</p><p>优点是<code>性能好</code>，</p><p>缺点是<code>升级困难</code>、<code>问题排查难</code>、<code>客户端复杂</code>。</p><blockquote><p>由于路由功能完全内嵌在业务应用，日志也会分散在不同的业务应用，因此，问题的上报与排查都需要业务方深入合作。</p></blockquote><p><strong>基于中间件实现</strong></p><p><code>基于中间件实现</code>是通过<code>中间件拦截请求转发（动态路由）</code>到指定的数据库实例，</p><p>与基于客户端实现相反，</p><p>优点是<code>升级方便</code>、<code>问题排查容易</code>、<code>客户端无感知</code>，</p><p>缺点是<code>性能损耗大</code>、<code>业务方可能存在读异常</code>、<code>引入新的单点问题</code>。</p><blockquote><p>路由功能的实现与业务应用无关，中间件可以做到无感知的升级，而且由于路由日志集中在中间件，排查问题更加容易。</p></blockquote><h4 id="MySQL主从同步"><a href="#MySQL主从同步" class="headerlink" title="MySQL主从同步"></a>MySQL主从同步</h4><p><code>MySQL主从同步</code>是利用<code>同步binlog日志同步以及操作重放实现</code>的数据同步。</p><p><img src="/images/mysql-replication-rw-router-2.png" alt=""></p><blockquote><p><code>binlog</code>：MySQL数据库的二进制日志，用于记录用户对数据库<code>变更操作的SQL语句</code>。</p></blockquote><p><strong>步骤</strong>：</p><p>① 当Slaver连接到Master时，Master会为Slaver开启<code>binlog dump线程</code>；</p><blockquote><p><code>binlog dump线程</code>用于读取binlog信息同步到<code>Slaver</code>。</p></blockquote><p>② Slaver会创建<code>I/O 线程</code>以及来处理<code>binlog dump线程</code>的数据，写入<code>relay log</code>；</p><blockquote><p><code>relay log</code>是为了避免同步数据过程中的异常，导致数据的丢失，在<code>半同步复制</code>中有介绍。</p></blockquote><p>③ Slaver还会创建<code>SQL 线程</code>用来解析<code>relay log</code>重放操作写入数据，完成数据同步。</p><blockquote><p>同步过程中，<code>SQL解析执行一定是单线程的</code>，否则，会造成执行顺序错乱影响数据一致性。</p></blockquote><hr><p>主从同步是通过异步线程同步数据，属于<code>最终一致性</code>的实现方案，因此，必然<code>存在主从不一致的</code>问题。</p><p><strong>异步模式</strong></p><p><code>异步模式</code>是最基础的同步方式，必然存在延迟。</p><p>为了解决<code>异步模式</code>的延时问题，MySQL提出了下面几种方法来解决这个问题。</p><p><strong>半同步模式（semi-sync）</strong></p><p><code>半同步模式</code>是通过<code>强制写入relay log</code>来保证<code>至少有一台从库完成了数据同步</code>。</p><blockquote><p>仅保证写入<code>relay log</code>的延迟，无法保证写入数据的延迟，因此，半同步模式不能彻底解决问题。</p></blockquote><p>对于一主一从的读写分离的情况下，此方法可以使同步延迟的问题忽略不记。</p><p>但在一主多从的读写分离的情况下，此方法就不一定会生效了，可能存在某个从库没有即使同步数据。</p><p><strong>全同步模式</strong></p><p><code>全同步模式</code>是在半同步的基础之上保证全部从库同步数据。</p><blockquote><p>这里的保证是relay log的写入还是数据的写入？</p><p>仍然是relay log（I/O线程与SQL线程不应该再次通信），只要日志flush到磁盘就不会出现数据丢失的情况。</p></blockquote><p><strong>并行模式</strong></p><p><code>并行模式</code>是通过增加<code>SQL线程</code>来实现并行读取<code>relay log</code>实现<code>库级别的并行</code>。</p><h4 id="主从延迟解决方案"><a href="#主从延迟解决方案" class="headerlink" title="主从延迟解决方案"></a>主从延迟解决方案</h4><p><strong>背景</strong></p><p>并发写入数据可能造成主从延迟增大。</p><p><strong>解决</strong></p><p>① 增加<code>数据缓存</code>，更新数据的同时写入缓存（读全部从缓存取数据）。</p><p>② 开启<code>半同步、同步复制</code>，降低时延问题，但会影响写入性能。</p><p>③ 强制<code>路由到主库</code>。</p><p>④ 优化业务逻辑，对于异步操作通过使用<code>延迟队列</code>、<code>重试</code>的机制来解决延迟问题。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;记录下读写分离的使用方式与常见问题。&lt;/p&gt;
    
    </summary>
    
      <category term="中间件" scheme="http://simyy.cn/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
    
      <category term="MySQL" scheme="http://simyy.cn/tags/MySQL/"/>
    
      <category term="数据库" scheme="http://simyy.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="中间件" scheme="http://simyy.cn/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
    
      <category term="读写分离" scheme="http://simyy.cn/tags/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/"/>
    
  </entry>
  
</feed>
