<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>martin91 (Martin)</title>
    <link>https://ruby-china.org/martin91</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>[广州] 小红花技术沙龙（第 5 期）暨 TechParty AI 技术专场沙龙</title>
      <description>&lt;p&gt;&lt;strong&gt;时间：&lt;/strong&gt; 2024 年 12 月 28 日 14:00-18:00&lt;br&gt;
&lt;strong&gt;地点：&lt;/strong&gt; 广州市番禺区番禺大道北 555 号天安总部中心 22 号楼一楼会议室&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;主题：&lt;/strong&gt; AI 技术深度探讨，高效编程新范式&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;议程：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;14:00-14:30 签到&lt;/li&gt;
&lt;li&gt;14:30 主持人开场&lt;/li&gt;
&lt;li&gt;14:35-15:15 陈国威 - 驾驭 AI 的力量，解锁高效编程新范式&lt;/li&gt;
&lt;li&gt;15:15-15:55 林聪 - Stable Diffusion 性能优化前沿：算法创新与工程优化&lt;/li&gt;
&lt;li&gt;15:55-16:15 合影&amp;amp;茶歇&lt;/li&gt;
&lt;li&gt;16:15-16:55 Leif - 重塑 AI 应用开发范式：基于 Dify.ai 从理念到落地实践&lt;/li&gt;
&lt;li&gt;16:55-17:35 朱顺濠 - 扣子，新一代 AI 应用开发平台&lt;/li&gt;
&lt;li&gt;17:35-18:00 自由交流&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;讲师阵容：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;陈国威 | 塞讯科技 - 研发经理&lt;/li&gt;
&lt;li&gt;张建红 | 塞讯科技 - 研发工程师&lt;/li&gt;
&lt;li&gt;林聪 | UCloud - 大模型算法专家&lt;/li&gt;
&lt;li&gt;Leif | BeanSmile - 技术总监&lt;/li&gt;
&lt;li&gt;朱顺濠 | 火山引擎 - 解决方案架构师&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;主办方：&lt;/strong&gt; 小红花技术领袖社区，TechParty&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;报名方式：&lt;/strong&gt; 前往&lt;a href="https://www.huodongxing.com/event/8787479334800" rel="nofollow" target="_blank" title=""&gt;活动行&lt;/a&gt;免费报名！&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/47c0e520-20d1-4299-927e-b47826ff5e34.jpeg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Wed, 25 Dec 2024 16:06:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/43989</link>
      <guid>https://ruby-china.org/topics/43989</guid>
    </item>
    <item>
      <title>PostgreSQL 数据库删除外键约束引发的死锁问题</title>
      <description>&lt;p&gt;上周将一个大功能发布到了线上环境，但是在半个多小时后收到排障要求（我们使用的是 PostgreSQL 数据库），因为 SRE 发现我们的其中一个从库的 CPU 使用率接近 100% 的告警，同时 APM 监控表明部分 API 响应时间明显变长，并且相关 API 的错误追踪里会看到 Statement Timeout 错误。经过版本回滚后，数据库和服务恢复正常。后面是漫长的根因分析的过程，而最后我们确定的原因，竟是一个 Rails migration 触发（注意不是导致，因为死锁还得找到和它形成相互等待的另一方，双方同时运行才能导致死锁）的从库死锁，比较有意思，总结一下，跟各位分享。&lt;/p&gt;
&lt;h2 id="为什么从库也有死锁？"&gt;为什么从库也有死锁？&lt;/h2&gt;
&lt;p&gt;按照过去仅有的认知，我只知道死锁在主库中更为常见，因为死锁通常发生在两个冲突的事务之间，而一般只有主库才会有大量事务同时运行，而从库由于只需要同步主库的日志进行顺序回放即可，理论上不存在并发的事务。&lt;/p&gt;

&lt;p&gt;但是我们的死锁发生在了从库，这是一个让人感觉有点匪夷所思的问题。&lt;/p&gt;
&lt;h2 id="根因：DDL 与高并发的 SELECT ... FROM A JOIN B ... 查询形成死锁"&gt;根因：DDL 与高并发的 &lt;code&gt;SELECT ... FROM A JOIN B ...&lt;/code&gt; 查询形成死锁&lt;/h2&gt;
&lt;p&gt;经过死锁日志的分析，我们发现每次死锁出现时，都有类似以下模式的联表查询语句在等待同一个线程锁定的资源：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;cars&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（以上 SQL 中的表名和列名为示例，非真实表名列名）&lt;/p&gt;

&lt;p&gt;完整的日志类似：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[12345]:ERROR:  deadlock detected at character 234
Process 12345 waits for AccessShareLock on relation 2 of database 789; blocked by process 911.
    Process 911 waits for AccessExclusiveLock on relation 1 of database 789; blocked by process 12345.
    Process 12345: SELECT users.id, users.name, cars.brand, ... FROM users LEFT OUTER JOIN cars ON cars.owner_id = users.id WHERE ...
    Process 911: &amp;lt;backend information not available&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过查询 PostgreSQL 数据库的元数据表，还原出 relation=1 对应表名是 &lt;code&gt;users&lt;/code&gt;，而 relation=2 对应表名是 &lt;code&gt;cars&lt;/code&gt;。
所以：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;这里的 SELECT 联表查询，正在等待 &lt;code&gt;cars&lt;/code&gt; 的 &lt;code&gt;AccessShareLock&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;而线程 911 在等待 &lt;code&gt;users&lt;/code&gt; 的 &lt;code&gt;AccessExclusiveLock&lt;/code&gt; 锁。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通过 PostgreSQL 官方文档，了解到这两个锁的求锁场景以及互斥性：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AccessShareLock&lt;/code&gt;: 仅在使用 SELECT 查询表时，对表进行加锁，并且 *&lt;em&gt;只和 &lt;code&gt;AccessExclusiveLock&lt;/code&gt; *&lt;/em&gt; 互斥；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AccessExclusiveLock&lt;/code&gt;: 和其它所有模式的锁互斥，这个锁保证同一时间只有持有这个锁的事务是唯一可访问目标表的事务。&lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;REINDEX&lt;/code&gt;, &lt;code&gt;CLUSTER&lt;/code&gt;, &lt;code&gt;VACUUM FULL&lt;/code&gt;，&lt;code&gt;REFRESH MATERIALIZED VIEW&lt;/code&gt;（不使用 &lt;code&gt;CONCURRENTLY&lt;/code&gt; 前提下）以及大多数的 &lt;code&gt;ALTER INDEX&lt;/code&gt; 和 &lt;code&gt;ALTER TABLE&lt;/code&gt; 操作都会尝试获取这个锁。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;从以上获取到的信息来看，那么死锁的形成，很有可能是有一个包含 DDL 的事务，并且整个事务里先后对 &lt;code&gt;cars&lt;/code&gt; 和 &lt;code&gt;users&lt;/code&gt; 加上了 &lt;code&gt;AccessExclusiveLock&lt;/code&gt; 锁。&lt;/p&gt;

&lt;p&gt;于是，我开始回顾这次版本里所有的数据库改动，也就是我们的 &lt;code&gt;db/migrations/&lt;/code&gt; 里的新增的文件，但是一开始，我没有发现任何问题，因为所有的 migration，看起来都是只修改了一张表。直到我冷静下来思考：&lt;strong&gt;如果显式的 DDL 没有相关的，但是 DDL 又确实存在，那么一定是某个我们不熟悉的潜在代码引发了问题&lt;/strong&gt;，进一步扩大了 review 的范围，我尝试找出 migration 中同时涉及这两个表的表名或者列名的，直到发现一个可疑对象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db/migrate/20241212010203_add_repaired_at_to_and_remove_fk_constraint_from_cars.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddReparedAtToAndRemoveFkConstraintFromCars&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:cars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:repaired_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:datetime&lt;/span&gt;
    &lt;span class="n"&gt;remove_foreign_key&lt;/span&gt; &lt;span class="ss"&gt;:remove_foreign_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:cars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:owner_id&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;难道是&lt;strong&gt;这个 &lt;code&gt;remove_foreign_key&lt;/code&gt; 会导致在 &lt;code&gt;users&lt;/code&gt; 表上加锁&lt;/strong&gt;？尝试 Google 搜索关键词“Pg remove foreign key constraint lock on table”，找到一篇讨论，标题是“&lt;a href="https://postgrespro.com/list/thread-id/2403559" rel="nofollow" target="_blank" title=""&gt;Thread: Full table lock dropping a foreign key&lt;/a&gt;”，里边提到帖子作者通过 &lt;code&gt;pg_locks&lt;/code&gt; 观察到当 pg 在执行外键约束删除时，会对被引用的表加上 &lt;code&gt;AccessExclusiveLock&lt;/code&gt; 锁，底层原因是 PostgreSQL 数据库内部会在此过程中移除响应的 trigger，但是由于对数据库底层我不精通，到此打住不深究，但是已经足以我找出此次死锁的根因了。&lt;/p&gt;

&lt;p&gt;所以，从始至终，包括我自己在内的开发人员，都没有意识到原来删除外键约束，会对外键引用的表进行加锁操作，这是我们写出这个风险代码，并且没有在死锁出现后第一时间怀疑到这里的原因，甚至我们一大帮人在分析根因的时候，虽然共享屏幕上正对着这行代码，但是我们都以为这行代码对于死锁原因分析无关紧要。&lt;/p&gt;
&lt;h2 id="根因定位过程的一些有趣思考"&gt;根因定位过程的一些有趣思考&lt;/h2&gt;&lt;h3 id="区分症结和现象"&gt;区分症结和现象&lt;/h3&gt;
&lt;p&gt;我们一开始引入 DBA 同事参与排查，DBA 同事将原因归为高并发，原因是当时并没有立马意识到死锁的存在以及监控显示数据库连接数在问题期间翻了一番。事实上，如我们一位 SRE 同事所怀疑的一致，连接数高不是因，而是果，是因为查询请求时延过高，导致连接池连接复用率下降，业务必须建立更多的数据库连接来完成数据查询。&lt;/p&gt;

&lt;p&gt;事后冷静思考，我才想起去看一下当时其他的指标，如果是业务量临时变大导致，那请求 QPS 以及数据库 IO 应该明显变高，但是事实上是当时的 API QPS 和数据库 IO 并没有明显变化。如果当时把这些观察纳入推理，那我们会更快排除这个错误的原因。&lt;/p&gt;

&lt;p&gt;所以，分析问题，得先确定问题是什么，避免把问题导致的连锁反应当成问题本身。&lt;/p&gt;
&lt;h3 id="为什么主库没有出现死锁？"&gt;为什么主库没有出现死锁？&lt;/h3&gt;
&lt;p&gt;因为我们之前做了主从读写分离，主库的查询请求非常少，从概率的角度来看，这降低了这次死锁在主库出现的概率，但也不是说主库一定高枕无忧。&lt;/p&gt;
&lt;h3 id="为什么另一个从库没有问题？"&gt;为什么另一个从库没有问题？&lt;/h3&gt;
&lt;p&gt;我们有 2 个从库实例，事实上另一个从库也不是独善其身，事后分析日志，仍然发现另一个从库上出现了 8 次死锁日志，说明这个从库在重放的过程中，还是遇到了 8 次死锁，只不过第 9 次重试时，它很幸运，抢先一步完成所有 DDL 并提交了事务，之后正常提供查询服务。&lt;/p&gt;

&lt;p&gt;而那个有问题的从库，则是很不幸地一直抢不到所有的锁，数据库一直重试，一直死锁，这点从事后回顾从库的复制的 lag 监控也能看出来，问题期间，数据库从库复制的 lag 持续拉大，表明数据库当时卡在了事务重放了，但是我们当时没有很快去排查这个监控，也是一个教训。&lt;/p&gt;
&lt;h3 id="为什么请求不是全部失败?"&gt;为什么请求不是全部失败？&lt;/h3&gt;
&lt;p&gt;PostgreSQL 数据库默认的死锁检测等待时间是 1s，之后启动死锁检测并且回滚导致死锁的事务。所以大量请求更多的影响是时延变高（等待 1 秒后拿到了共享锁，完成查询），并不会失败。但是也会有一些线程的查询很不幸，虽然 DDL 事务已经回滚，但是回滚同时这个线程可能在休眠中，等到线程调度进来的时候，新的 DDL 事务又已经开启并且进入等待了，所以这些不幸的线程再次等待。从事后监控看，问题期间，整体的 API 时延升高，P99 到达惊人的 1 分钟，说明极少数请求很不幸地一直在等待锁，直到 STATEMENT 超时（默认 60 秒）。&lt;/p&gt;
&lt;h2 id="最佳实践建议"&gt;最佳实践建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;慎重使用数据库层面的 constraint&lt;/strong&gt;
恰当使用数据库 constraint 可以简化应用层逻辑，保证数据完整性，但是对其不熟悉的前提下使用，容易踩坑。理想状况下，数据库只做数据存取这一件事，其他的事情交给应用层。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;避免多个表在同一个 DDL 事务里&lt;/strong&gt;
需要特别警醒的是，Rails migration 会将每个 migration 包装成一个事务，为了防止死锁，一个事务，也就是一个 migration，只能操作一个表，如果你不确定，可以将操作分散到多个 migration 里，或者干脆关闭这个 migration 的事务，即调用 &lt;code&gt;disable_ddl_transaction!&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;本文首发于个人博客：&lt;a href="https://blog.hackerpie.com/posts/postgresql/deadlock-due-to-remove-constrait/" rel="nofollow" target="_blank"&gt;https://blog.hackerpie.com/posts/postgresql/deadlock-due-to-remove-constrait/&lt;/a&gt; &lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Fri, 13 Dec 2024 17:57:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/43982</link>
      <guid>https://ruby-china.org/topics/43982</guid>
    </item>
    <item>
      <title>【广州】TechParty 基础软件专场沙龙暨小红花技术沙龙（第 4 期）11-25 下午</title>
      <description>&lt;h2 id="活动背景"&gt;活动背景&lt;/h2&gt;
&lt;p&gt;谁为基础软件且歌且舞，谁为基础软件小锤一百大锤八十，精心雕琢。&lt;/p&gt;

&lt;p&gt;不应埋没。&lt;/p&gt;

&lt;p&gt;小红花技术沙龙（第 4 期）暨 TechParty 基础软件专场沙龙，将于 &lt;strong&gt;11 月 25 日（星期六）下午 2:00-6:00&lt;/strong&gt; 在&lt;strong&gt;广州天河科韵路 12 号方圆 E 时光西座 6 楼微谷众创社区&lt;/strong&gt;举行，本次活动我们邀请了来自 OceanBase、Vika 维格云、华南师范大学和同创永益的嘉宾给大家分享 4 个主题，内容涵盖数据库、操作系统、混沌工程和 SaaS 开源软件等多个领域，相信参会的您将会满载而归。&lt;/p&gt;
&lt;h2 id="演讲主题介绍"&gt;演讲主题介绍&lt;/h2&gt;&lt;h2 id="《OB Cloud 云数据库的技术原理与最佳应用实践》"&gt;《OB Cloud 云数据库的技术原理与最佳应用实践》&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/4c16691f-621c-4db0-99bd-99eb1d1fa4f0.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;嘉宾：陈畅亮&lt;br&gt;
职务：OceanBase 解决方案架构师&lt;br&gt;
内容简介：一站式带你了解分布式数据库，介绍 OceanBase 的特性、典型应用场景及用户实践分享，帮助开发者与技术人员解决核心痛点。&lt;/p&gt;
&lt;h2 id="《从vika维格云到开源APITable：程序员如何打造一个赚钱的开源项目？》"&gt;《从 vika 维格云到开源 APITable：程序员如何打造一个赚钱的开源项目？》&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/4338332c-963b-4e95-bb82-6ff79c3024d2.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;嘉宾：蓝梓文&lt;br&gt;
职务：vika 维格云全栈工程师&lt;br&gt;
内容简介：您将亲眼目睹开源 APITable 的发展历程，看其如何上线 3 个月内斩获 1w+ stars。我们将探讨团队如何克服各种挑战，不断锻炼技能，打造商业化开源项目的故事。&lt;/p&gt;
&lt;h2 id="《用Rust开发操作系统：以ArceOS为例》"&gt;《用 Rust 开发操作系统：以 ArceOS 为例》&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/da1b4650-72d4-4c6f-b1ae-909e13c29607.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;嘉宾：刘逸珑&lt;br&gt;
职务：华南师范大学 2020 级（大四）学生&lt;br&gt;
内容简介：从操作系统外核，库操作系统迭代到 ArceOS 回顾开发演化历史。分享 ArceOS 这种基于组件化设计，致力于使用户像开发一般应用一样开发的操作系统。&lt;/p&gt;
&lt;h2 id="《混沌工程的应用场景与实践》"&gt;《混沌工程的应用场景与实践》&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/c314f22f-978d-4e5c-aba9-3149ff33d9ec.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;嘉宾：谭德辉&lt;br&gt;
职务：同创永益产品解决方案工程师&lt;br&gt;
内容简介：以实际落地应用场景切入，介绍混沌工程的场景设计、实验及用户实践分享，帮助开发、测试及运维人员解决核心痛点。&lt;/p&gt;
&lt;h2 id="参会流程"&gt;参会流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;14:00-14:30 签到、自由社交&lt;/li&gt;
&lt;li&gt;14:30-15:10 演讲主题：OB Cloud 云数据库的技术原理与最佳应用实践&lt;/li&gt;
&lt;li&gt;15:10-15:50 演讲主题：从 vika 维格云到开源 APITable：程序员如何打造一个赚钱的开源项目？&lt;/li&gt;
&lt;li&gt;15:50-16:10 大合照、茶歇、自由社交&lt;/li&gt;
&lt;li&gt;16:10-16:50 演讲主题：用 Rust 开发操作系统：以 ArceOS 为例&lt;/li&gt;
&lt;li&gt;16:50-17:30 演讲主题：混沌工程的应用场景与实践&lt;/li&gt;
&lt;li&gt;17:30-18:00 自由社交&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="报名链接"&gt;报名链接&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.huodongxing.com/event/1730609955300" rel="nofollow" target="_blank"&gt;https://www.huodongxing.com/event/1730609955300&lt;/a&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Thu, 23 Nov 2023 14:06:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/43483</link>
      <guid>https://ruby-china.org/topics/43483</guid>
    </item>
    <item>
      <title>stimulus.js 初体验</title>
      <description>&lt;p&gt;&lt;code&gt;stimulus.js&lt;/code&gt; 框架是一个轻量的 JavaScript 框架，由大名鼎鼎的 Basecamp 公司开发，也就是 Ruby on Rails 框架核心开发团队所在的公司。老早就听说了 stimulus.js 框架，但是没有实际使用过。最近刚好在自己的一个小项目中有了实践的机会，有了一些心得体会，总结分享一下。&lt;/p&gt;

&lt;p&gt;提醒：如果想快速体验 stimulus.js 做出来的 demo，可以看看这个 &lt;a href="https://github.com/chloerei/todomvc-stimulus" rel="nofollow" target="_blank" title=""&gt;todomvc-stimulus&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="一个克制的前端 JavaScript 框架"&gt;一个克制的前端 JavaScript 框架&lt;/h2&gt;
&lt;p&gt;谈起对 &lt;code&gt;stimulus.js&lt;/code&gt; 框架总的印象，我觉得这是一个非常克制的前端 JavaScript 框架。它聚焦于&lt;strong&gt;在 HTML 元素与 JavaScript 对象的绑定&lt;/strong&gt;这件事情上，并且这种绑定是单向的，不是前端开发中早已非常普遍的双向绑定。除此之外，它没有提供其他额外的功能。&lt;/p&gt;

&lt;p&gt;由于它的克制，&lt;strong&gt;轻量&lt;/strong&gt;是它必然而然的第一个优点。其次，配合其所设计的 &lt;code&gt;controller&lt;/code&gt; 的概念，可以实现交互逻辑里状态的隔离与解耦。最后，它在 &lt;code&gt;controller&lt;/code&gt; 代码的组织上，也让熟悉 Rails 开发的人感到亲切：&lt;strong&gt;约定大于配置&lt;/strong&gt;。每个 &lt;code&gt;controller&lt;/code&gt; 的定义，都需要按照约定，一个 &lt;code&gt;controller&lt;/code&gt; 对应一个文件，放在 &lt;code&gt;controllers&lt;/code&gt; 目录下，且文件名与 &lt;code&gt;controller&lt;/code&gt; 的名字一致。&lt;/p&gt;
&lt;h2 id="Stimulus.js 的轻量"&gt;
&lt;code&gt;Stimulus.js&lt;/code&gt; 的轻量&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Stimulus.js&lt;/code&gt; 的核心概念非常少，想要上手 &lt;code&gt;stimulus.js&lt;/code&gt; 框架的使用，只有 4 个核心概念是需要了解的。&lt;/p&gt;
&lt;h3 id="Controllers"&gt;&lt;code&gt;Controllers&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Controllers&lt;/code&gt; 是声明了诸如 &lt;code&gt;data-controller="todos"&lt;/code&gt; 这样的 data 属性的 HTML 元素所绑定的 JavaScript 对象：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stimulus.js&lt;/code&gt; 会自动为所有此类元素实例化对应的 controller，每个此类元素各自绑定一个实例。以上述例子来说，&lt;code&gt;stimulus.js&lt;/code&gt; 会自动查找位于 &lt;code&gt;app/javascript/controllers/todos_controller.js&lt;/code&gt; 的文件，并且导入其中导出的默认类，这是一个经典的&lt;strong&gt;约定大于配置&lt;/strong&gt;的做法：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/todos_controller.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，如果不想使用或者无法使用约定的形式，也可以通过 &lt;code&gt;stimulus.js&lt;/code&gt; 提供的函数进行 controller 的显式注册：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TodosController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这类元素以及其子孙元素，都是元素绑定的 controller 的可见范围。也就是说，在 &lt;code&gt;stimulus.js&lt;/code&gt; 框架中，controller 的各种操作，只能作用于 controller 绑定的元素以及此元素的子孙元素。这个原则同样适用于嵌套 Controllers 的情况下。&lt;/p&gt;

&lt;p&gt;Controllers 之间可以通过事件的方式相互协作，这个会在后面讲 Actions 的时候再补充讲一下。&lt;/p&gt;
&lt;h3 id="Targets"&gt;&lt;code&gt;Targets&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Targets&lt;/code&gt; 是另一种在 HTML 元素与 JavaScript 对象之间实现绑定的方法，但是它作用于具体的 Controller 之下：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-todos-target=&lt;/span&gt;&lt;span class="s"&gt;"addBtn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明 target 的规则是 &lt;code&gt;data-&amp;lt;controller&amp;gt;-target=&amp;lt;target-name&amp;gt;&lt;/code&gt;，相应的，需要在 controller 声明绑定的对象：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/todos_controller.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;addBtn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成了 target 的绑定之后，就可以按照 stimulus.js 对 targets 的约定，在 controllers 方法中使用类似 &lt;code&gt;this.addBtnTarget&lt;/code&gt; 或者 &lt;code&gt;this.addBtnTargets&lt;/code&gt; （针对有多个 HTML 元素绑定了同一个 Target 的情况）来访问这些绑定的 HTML 元素了。&lt;/p&gt;
&lt;h3 id="Controllers 和 Targets 的生命周期回调"&gt;&lt;code&gt;Controllers 和 Targets 的生命周期回调&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;上面说的 Controllers 和 Targets，都是 HTML 元素和 JavaScript 对象之间的绑定功能，因为 HTML 元素随着浏览器的加载以及后续的 DOM 操作，就带来了一个问题，这些对象的生命周期是怎样的？&lt;/p&gt;

&lt;p&gt;&lt;img src="http://www.plantuml.com/plantuml/svg/~1UDfpA2v9B2efpStXqj3ILD3LjLDGoipBBCbCpCciIjNGLE822qNd9UQcneOa5gMdbZZdv-Mb9fUafcYKWFbM2guvgRcfUIKmnSaG1LWrksGM9nFIdm9N0pB28JKl1UGy0FF6DcO0" title="" alt=""&gt;
这几个生命周期的回调函数都与 DOM 的变化紧密相关，一般来说看这几个条件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;元素是否存在？&lt;/li&gt;
&lt;li&gt;绑定的标识是否存在元素的属性列表中，比如 &lt;code&gt;data-controller&lt;/code&gt; 或者 &lt;code&gt;data-&amp;lt;controller&amp;gt;-target&lt;/code&gt;？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当条件从部分或者全部不满足变为满足时，则 connect 类型的回调函数被调用；相反，如果由于 DOM 的一些操作导致不再满足全部条件时，disconnect 类事件的回调函数被调用。&lt;/p&gt;

&lt;p&gt;controllers 可以在 &lt;code&gt;connect()&lt;/code&gt; 回调中定义初始化的工作，比如 controller 的一些状态的初始化，相应的，&lt;code&gt;[name]TargetConnected&lt;/code&gt; 也可以用于某个 target 的初始化。&lt;/p&gt;
&lt;h3 id="Actions"&gt;&lt;code&gt;Actions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Actions 是 &lt;code&gt;stimulus.js&lt;/code&gt; 中的事件回调机制，类似 HTML 中 &lt;code&gt;onclick&lt;/code&gt;、&lt;code&gt;onchange&lt;/code&gt; 一类的语法。&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-todos-target=&lt;/span&gt;&lt;span class="s"&gt;"addBtn"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;todos#add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actions 支持多个事件回调声明，这样同时也方便了实现 Controllers 之间的协作：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"todos submitter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- 注意，这里使用了多个 controller 绑定 --&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-todos-target=&lt;/span&gt;&lt;span class="s"&gt;"addBtn"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;todos#add todos:added-&amp;gt;submitter#submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/todos_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// do something to maintain the status of todos controller&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;added&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;xxxx&lt;/span&gt;&lt;span class="p"&gt;]}}}})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app/javascript/controllers/submitter_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// extract event data&lt;/span&gt;
    &lt;span class="c1"&gt;// do something else&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以这个例子来说，程序执行的流程是这样的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用户点击 Add 按钮后，按钮触发的 &lt;code&gt;click&lt;/code&gt; 事件触发了对 &lt;code&gt;todos&lt;/code&gt; controller 的 &lt;code&gt;added&lt;/code&gt; 的方法的回调；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;added&lt;/code&gt; 方法执行完自身的核心逻辑后，通过调用 &lt;code&gt;this.dispatch&lt;/code&gt; 方法触发 &lt;code&gt;todos:added&lt;/code&gt; 事件，注意这里的 &lt;code&gt;todos:&lt;/code&gt; 前缀是框架自动加上的；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;todos:added&lt;/code&gt; 事件的产生，触发了对 &lt;code&gt;submitter&lt;/code&gt; controller 的 &lt;code&gt;submit&lt;/code&gt; 方法的回调。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;就这样，通过抽象出不同的 controllers，实现逻辑的分离和解耦，再通过事件机制，将逻辑实现拼装和编排。&lt;/p&gt;

&lt;p&gt;理解了上面这 4 个核心概念，就足以使用 &lt;code&gt;stimulus.js&lt;/code&gt; 开发出一个交互相对简单的前端逻辑了。当然，&lt;code&gt;stimulus.js&lt;/code&gt; 还有其他几个概念，但是在我看来只是一些锦上添花的功能，这里就没必要赘述了。&lt;/p&gt;
&lt;h2 id="也谈谈 stimulus.js 不适合的场景"&gt;也谈谈 &lt;code&gt;stimulus.js&lt;/code&gt; 不适合的场景&lt;/h2&gt;
&lt;p&gt;尽管是一个小项目，但是在使用 &lt;code&gt;stimulus.js&lt;/code&gt; 的过程中，也遇到了一些觉得比较繁琐的问题，这些问题体现在：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;缺乏 DOM 操作的封装&lt;/strong&gt;：因为 &lt;code&gt;stimulus.js&lt;/code&gt; 只提供 HTML 元素与 JavaScript 对象之间的绑定，并没有提供对 DOM 操作的封装，所以在需要操作 DOM 的时候，就会经常需要直接使用原生 DOM 对象的操作，比如 &lt;code&gt;Element.classList.add()&lt;/code&gt; 一类，如果是在早期的浏览器中，还需要担心兼容性问题等，但是好在现在的浏览器兼容性问题已经少了很多，这倒不是太大的问题；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;缺乏前端渲染的支持&lt;/strong&gt;：因为 &lt;code&gt;stimulus.js&lt;/code&gt; 中的绑定并非双向绑定，在一些需要根据 JavaScript 对象渲染不同页面内容或者视觉效果的情况下，如果不借助其他框架的支持，就只能编写各种字符串插值，以及 &lt;code&gt;Element.innerHTML = xxxx&lt;/code&gt; 的代码，同样效率比较低。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以，总结来说，如果你的前端页面是一个重交互的页面，可能只使用 &lt;code&gt;stimulus.js&lt;/code&gt; 并不是一个明智之选。以我自己来选择的话，如果是一些内容类的轻交互场景，比如博客或者论坛，一般需要交互的就是评论区，简单的文本输入以及追加展示等，我觉得用 &lt;code&gt;stimulus.js&lt;/code&gt; 会比较舒服，轻量，又是原汁原味的 DOM；但是其他情况下，我可能会直接上 &lt;code&gt;vue.js&lt;/code&gt; 之类功能更全面的框架，最大程度减少在页面与逻辑之间状态同步的代码。&lt;/p&gt;
&lt;h2 id="使用 stimulus.js 踩过的坑"&gt;使用 &lt;code&gt;stimulus.js&lt;/code&gt; 踩过的坑&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;在 Controllers scope 之外的 action 无法回调到 Controller 的方法&lt;/strong&gt;&lt;br&gt;
这个问题最开始排查了一些时间，一直没想明白为什么，后来才顿悟，原来是因为踩了 Controller scope 的坑。因为我的 action 声明需要回调的 controller 在当前 DOM 中不在可见范围，于是触发回调失败。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;先于 controller 初始化前触发的 action 无法回调到 Controller 的方法&lt;/strong&gt;
这个问题是因为我在代码中声明了一个 action，并且在 controller 中也执行了 &lt;code&gt;dispatch&lt;/code&gt;，但是此时因为目标的 controller 还没有初始化，导致看似代码没有任何语法或者使用错误，但是 action 无可能触发回调成功。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="相关资料链接"&gt;相关资料链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stimulus.hotwired.dev/" rel="nofollow" target="_blank" title=""&gt;stimulus.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/chloerei/todomvc-stimulus" rel="nofollow" target="_blank" title=""&gt;todomvc-stimulus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bundlephobia.com/package/stimulus@3.1.0" rel="nofollow" target="_blank" title=""&gt;bundlephobia:stimulus@3.1.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>martin91</author>
      <pubDate>Sun, 31 Jul 2022 15:35:35 +0800</pubDate>
      <link>https://ruby-china.org/topics/42566</link>
      <guid>https://ruby-china.org/topics/42566</guid>
    </item>
    <item>
      <title>Kafka 核心设计思考总结</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在学习 Kafka 的一些设计原理，偶然间发现 Kafka 官方文档中独列了 &lt;a href="https://kafka.apache.org/documentation/#design" rel="nofollow" target="_blank" title=""&gt;Design&lt;/a&gt; 一章。两天看完后觉得很兴奋，因为文档中很详细地从各方面阐述了 Kafka 官方对于 Kafka 设计的目标以及设计权衡等，让我恍然大悟 Kafka 的独特与简洁。这种快乐是阅读网上各种零散的博客文章无法比拟的。我此处总结更多是为了提升自己的领悟和理解程度，行文之中会夹杂个人主观理解，我建议大家抽出时间阅读原汁原味的&lt;a href="https://kafka.apache.org/documentation/#design" rel="nofollow" target="_blank" title=""&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="Kafka 设计目标与设计概述"&gt;Kafka 设计目标与设计概述&lt;/h2&gt;
&lt;p&gt;设计一个系统，精准的目标是第一步。Kafka 官方在最开始的时候，对 Kafka 的设计理想是将其做成一个可以帮助大型公司应对各种可能的实时数据流处理的通用平台。这句话里边有几个重点：“大型公司”、“实时”、“通用”，对应到系统设计上，就是需要支持大量数据的低延迟处理，并且需要考虑各种不同的数据处理场景。在官方阐述中，Kafka 着眼于以下几个核心指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;高吞吐量&lt;/strong&gt;：因为 Kafka 需要处理大量的消息；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;低延迟&lt;/strong&gt;：消息系统的关键设计指标；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;支持加载离线数据&lt;/strong&gt;：这是 Kafka 考虑的所谓“各种可能的”数据处理场景，支持从离线系统中加载数据，或者将数据加载到离线系统中，都是无法逃避的；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;支持分区的、分布式的、实时的数据流处理以产生新的、派生的数据流&lt;/strong&gt;：这个指导了 Kafka 里 topic 分区模型以及消费者模型的设计；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;容错与可靠性&lt;/strong&gt;：Kafka 作为消息中间件，核心场景之一就是作为系统间的连接器，需要保证整体业务的正常运作，可靠的消息投递机制以及应对节点故障的高可用设计等，必不可少。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;理解了 Kafka 的设计目标以及核心指标，后续对 Kafka 的整体架构设计就会有一个方向了，因为 Kafka 的整体设计细节还算比较多，但是归根结底都是围绕这几个核心指标去做的设计，我尝试分门别类先汇总一下，可能不是很准确，希望请大家看的时候顺便赐教：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;核心指标&lt;/th&gt;
&lt;th&gt;实现的角度&lt;/th&gt;
&lt;th&gt;具体设计手段&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐量&lt;/td&gt;
&lt;td&gt;读写缓存&lt;/td&gt;
&lt;td&gt;依赖文件系统自身的 Page Cache，而不是自己实现内存缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐量&lt;/td&gt;
&lt;td&gt;高效的数据结构&lt;/td&gt;
&lt;td&gt;采用顺序读写的结构，而不是 B 树等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐量&lt;/td&gt;
&lt;td&gt;降低大量小的 I/O&lt;/td&gt;
&lt;td&gt;消息分批发布，按批投递&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐量&lt;/td&gt;
&lt;td&gt;提高消息投递吞吐量&lt;/td&gt;
&lt;td&gt;由消费者批量拉取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐量&lt;/td&gt;
&lt;td&gt;支持分批消息&lt;/td&gt;
&lt;td&gt;支持异步发送消息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;低延迟&lt;/td&gt;
&lt;td&gt;避免昂贵的字节拷贝&lt;/td&gt;
&lt;td&gt;统一的消息格式，零拷贝技术&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;低延迟&lt;/td&gt;
&lt;td&gt;优化传输性能&lt;/td&gt;
&lt;td&gt;通过批量消息压缩减小传输数据量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;低延迟&lt;/td&gt;
&lt;td&gt;提升读取性能&lt;/td&gt;
&lt;td&gt;顺序读，日志文件分段存储，应用二分查找&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;低延迟&lt;/td&gt;
&lt;td&gt;降低负载均衡延迟&lt;/td&gt;
&lt;td&gt;producer 直连 broker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;离线数据加载&lt;/td&gt;
&lt;td&gt;支持周期性大量数据加载&lt;/td&gt;
&lt;td&gt;依赖存储层顺序读写的常量时间复杂度的访问优势以及低廉的磁盘成本要求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;离线数据处理&lt;/td&gt;
&lt;td&gt;支持并行处理&lt;/td&gt;
&lt;td&gt;通过分区设计以及 consumer 的 offset，支持 Hadoop 一类的并行作业以及断点作业&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可靠性&lt;/td&gt;
&lt;td&gt;支持“有且仅有一次”的消息投递语义&lt;/td&gt;
&lt;td&gt;producer 的 ID 与消息 Sequence Number，类事务提交语义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可靠性&lt;/td&gt;
&lt;td&gt;容错处理与高可用&lt;/td&gt;
&lt;td&gt;ISR 机制与 Leader 均匀分布设计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;除了上表所列内容，还有少量设计思考暂时不好归类，比如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;消息消费进度的存储设计思考&lt;/li&gt;
&lt;li&gt;日志压缩（Log Compaction）的设计&lt;/li&gt;
&lt;li&gt;其他……&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以上的总体设计，让 Kafka 看起来也更像是一个日志型系统，而不仅仅是传统意义上的消息队列。&lt;/p&gt;
&lt;h2 id="高吞吐量的设计思考"&gt;高吞吐量的设计思考&lt;/h2&gt;&lt;h3 id="读写缓存的利用"&gt;读写缓存的利用&lt;/h3&gt;
&lt;p&gt;Kafka 的设计中，存储层直接基于文件系统实现，而不是额外实现复杂的存储层抽象，比如引入缓存和缓冲等。&lt;/p&gt;

&lt;p&gt;一般提到文件系统或者磁盘存储，大家第一反应就是“这东西不是很慢吗”？是的，一般来说，磁盘的读写速度是很慢，但也限于随机访问的前提下，而事实上，特定条件下，磁盘的顺序读写性能堪比内存的随机访问性能！是不是很出乎意料？&lt;/p&gt;

&lt;p&gt;另外，现代操作系统内部都已经实现对底层文件系统的统一抽象，特别是对读写性能的优化，大家可能了解的是预读（Read-Ahead）和后写（Write-Behind）。结合顺序读写的特性，这种操作系统的优化能够被发挥到极致。&lt;/p&gt;

&lt;p&gt;如果考虑应用层的缓存设计方案，就会考虑到 Kafka 运行于 JVM 之上，JVM 中对象的封装表示都会有额外的内存开销，这种额外开销与对象本身数据的大小相当。所以，如果是在应用层自行实现缓存层，则意味着会有额外的大致两倍于消息体积的内存开销。这个成本对于大数据处理场景来说，可不是闹着玩的。开销也不仅限于内存开销，Java 本身的 GC 算法会随着应用堆内存的增加而愈加频繁且迟钝。&lt;/p&gt;

&lt;p&gt;最后，缓存的设计还绕不开缓存预热的思考，由于操作系统本身对于读写性能优化的设计，可以认为预读和后写等特性已经帮助应用透明地实现了缓存的预热和落盘。而如果是在应用层面，则不得不重复造轮子，且需要考虑的细节很多。&lt;/p&gt;

&lt;p&gt;综上，Kafka 官方认为直接基于文件系统实现存储，是一个非常明智的决定。&lt;/p&gt;
&lt;h3 id="顺序数据结构的妙处"&gt;顺序数据结构的妙处&lt;/h3&gt;
&lt;p&gt;众所周知，Kafka 采用了追加写也就是顺序写的方式来完成数据持久化，消息投递过程中也是按照顺序读的方式实现。在 Kafka 看来，顺序读写带来了诸多好处。&lt;/p&gt;

&lt;p&gt;在 B 树等数据结构上操作的时间复杂度是 log(n)，一种一般看来近似于常量时间复杂度的算法。但是实际上，考虑到磁盘的特殊结构以及额外的磁盘定位（事实上，定位不是一步到位的，分为寻道和旋转两个阶段，感兴趣的可以阅读《&lt;a href="https://tech.meituan.com/2017/05/19/about-desk-io.html" rel="nofollow" target="_blank" title=""&gt;磁盘 I/O 那些事&lt;/a&gt;》）等，这种数据结构的操作性能的下降速度，其实是高于数据本身体积的增长的，也就是随着数据越来越大，这种数据结构的性能下降越来越快。&lt;/p&gt;

&lt;p&gt;而采用顺序读写，由于只需要一次磁盘定位，可以认为其操作时间复杂度为 O(1)。因为一般而言，一次 I/O 操作的总体延迟，主要是磁盘定位的延迟，而数据传输的延迟与之相比不值一提。所以这种常量时间复杂度的访问操作，天然的好处是我们可以不用担心访问数据的大小。因此，这种数据结构在面对大量数据的读写时，会有更加稳定的性能表现。在 Kafka 团队看来，Kafka 可以放心地以更低成本实现存储，特别是可以以磁盘转速换取空间，这也是 Kafka 可以放心地保留历史消息而不做即刻清除的原因。&lt;/p&gt;

&lt;p&gt;这里补充一点来自《&lt;a href="https://tech.meituan.com/2017/05/19/about-desk-io.html" rel="nofollow" target="_blank" title=""&gt;磁盘 I/O 那些事&lt;/a&gt;》）的参考信息：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;目前磁盘的平均寻道时间一般在 3-15ms&lt;br&gt;
7200rpm 的磁盘平均旋转延迟大约为 60*1000/7200/2 = 4.17ms&lt;br&gt;
目前 IDE/ATA 能达到 133MB/s，SATA II 可达到 300MB/s 的接口数据传输率，数据传输时间通常远小于前两部分消耗时间&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;除此之外，由于是追加写顺序读，还可以简化读写操作并发的问题。我们不需要担心各种锁或者阻塞问题，读写互不干扰。&lt;/p&gt;
&lt;h3 id="避免过多的小 I/O 操作"&gt;避免过多的小 I/O 操作&lt;/h3&gt;
&lt;p&gt;Kafka 中的 I/O 操作主要是两个环节：客户端和服务器端之间的网络 I/O，以及服务器内部持久化操作中的磁盘 I/O。在 Kafka 的整体设计里，大的思路就是降 I/O，增吞吐。&lt;/p&gt;

&lt;p&gt;Kafka 在设计上支持消息分批投递，并且在持久化存储上原样保存，最后也是按批交付给消费者，全程不会对此批数据进行分解或者合并。这种设计有几个好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;足够大的网络分包&lt;/li&gt;
&lt;li&gt;足够大的磁盘顺序操作&lt;/li&gt;
&lt;li&gt;毗邻的内存空间等&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里消息原样存储和投递还有一些零拷贝以及消息压缩方面的考虑，稍后也会聊到。&lt;/p&gt;

&lt;p&gt;这里刚好由消息分批就想到了发布者的异步消息发送，这是由客户端 SDK 完成的功能，其可以配置在超过指定时间或者超出指定消息量的情况下触发消息投递到 broker，虽然会牺牲一些投递时机的延迟，但是赢取了分批投递所带来的吞吐量的提升。&lt;/p&gt;

&lt;p&gt;目前为止，关于提高吞吐量的设计，画了个图，以助加深印象：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/9769f7db-a2a5-4d9e-8595-3112a31c598e.jpg!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="低延迟的设计思考"&gt;低延迟的设计思考&lt;/h2&gt;&lt;h3 id="避免昂贵的字节拷贝等操作"&gt;避免昂贵的字节拷贝等操作&lt;/h3&gt;
&lt;p&gt;为了降低延迟，broker 最好是越少干预消息约好。为此，Kafka 设计了统一的二进制消息格式，而且在消息投递的全过程中，都需要修改消息内容，带来的好处是二进制消息无需经过 broker 的任何转化处理，原样落盘。更重要的是，由于消息原样投递给消费者，可以方便结合零拷贝技术实现消息在网络的快速传输。特别是对于多消费者组的场景，消息的投递直接从 Page Cache 读取，不用担心广播带来线性的访问开销。最后通过网络传输，理论上消息投递的速率可以逼近网络连接传输速率的上限。&lt;/p&gt;
&lt;h3 id="端到端消息压缩"&gt;端到端消息压缩&lt;/h3&gt;
&lt;p&gt;如果说零拷贝是为了避免无谓的开销，那将消息体进行压缩，则是为了降低数据传输的体积。Kafka 使用了端到端的分批消息压缩协议，至于为什么是分批呢？因为一般来说，在同个 topic 里，我们倾向于传输同类或者相似的消息类型，这些类型的消息会有大量重复的字段名，如果按批压缩，能够获得远比单条消息大的压缩率。由于是端到端压缩解压，Kafka broker 也就无需考虑消息本身实际使用的压缩格式，这也符合上面说的二进制消息格式中，broker 不参与消息转换的设计思想。目前，Kafka 支持的压缩协议有 GZIP、Snappy、LZ4 以及 ZStandard。&lt;/p&gt;
&lt;h3 id="发布者的低延迟设计"&gt;发布者的低延迟设计&lt;/h3&gt;
&lt;p&gt;发布者的低延迟设计主要是降低负载均衡的延迟。Kafka 采用了 producer 直连 broker 的设计，而不依赖其他任何中间的路由层，好处是直接高效，减少了一层就是去除了一个环节的回路，同时降低了系统的复杂度，无需额外考虑路由层的高可用问题。但是就要求所有 broker 节点都能够获知集群的节点分布以及每个分区的 leader 所在节点等信息，这些信息由 ZooKeeper 管理。&lt;/p&gt;

&lt;p&gt;另外，消息投递分区由客户端也就是 producer 决定，既支持随机或者轮询等简单的均衡算法，也支持按 Key 哈希的分区算法等，这些在 producer 上完成。&lt;/p&gt;
&lt;h3 id="消费者的低延迟设计"&gt;消费者的低延迟设计&lt;/h3&gt;
&lt;p&gt;消费者的低延迟，一方面是依赖前面讲的零拷贝技术的应用，另一方面是结合批量拉取消息，由于前面都有介绍，这里只是带过。&lt;/p&gt;
&lt;h2 id="可靠性的设计思考"&gt;可靠性的设计思考&lt;/h2&gt;&lt;h3 id="实现“有且仅有一次”的消息投递语义"&gt;实现“有且仅有一次”的消息投递语义&lt;/h3&gt;
&lt;p&gt;想要实现刚好一次的消息投递，需要分开从发布端和消费端来看。&lt;/p&gt;

&lt;p&gt;在发布端，每个发布者都会获得 broker 授予的一个唯一的 ID，结合消息本身隐含的顺序的序号，可以方便 broker 识别重复投递的消息。其次，考虑到在一次事务型操作中可能会有多个消息同时发布到多个分区的需求，Kafka 也提供了类似事务的语义，具体大家可以搜索了解一下。&lt;/p&gt;

&lt;p&gt;来到消费端，实现刚好一次的消息投递也相对简单。由于消息拉取起点由消费者控制，所以只需要思考消费者如何避免重复拉取就好了。在官方文档中，建议的方式是消费者将已消费的消息偏移量一同记录到消费消费处理结果的输出中，这样可以保证消费者（可能是原来的消费者重启了，也可能是消费者挂了后有其他消费者分担了此消费者原来的分区）在开始拉取之前确认最后消费进度。&lt;/p&gt;
&lt;h3 id="高可用的设计思考"&gt;高可用的设计思考&lt;/h3&gt;
&lt;p&gt;高可用的设计主要涉及两个内容：复制和容灾选主。&lt;/p&gt;

&lt;p&gt;复制上，Kafka 的每个分区都可以配置 0 个或多个副本数量，也就是每个分区对应 1 个或多个 broker 节点。follower 使用和消费者一致的批量拉取机制来同步 leader 节点的日志。&lt;/p&gt;

&lt;p&gt;在节点活性方面，Kafka 认为如果一个节点满足以下两点，即可称为 &lt;code&gt;In-Sync&lt;/code&gt; 节点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;节点保持了到 ZooKeeper 的心跳&lt;/li&gt;
&lt;li&gt;节点紧跟 Leader 的日志复制，没有“明显落后”Leader 节点的日志&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在考虑 Leader 故障上，Kafka 放弃了大多数选举的分布式一致性方案，而是采用名为 ISR（In-Sync Replica）的方案。因为传统的大多数选举，为了容忍 &lt;code&gt;n&lt;/code&gt; 次 leader 故障，必须部署 &lt;code&gt;2n+1&lt;/code&gt; 个节点，对于需要存储大量数据的 Kafka 来说，这个成本显然过大。而采用 ISR 的方案，只需要 &lt;code&gt;n+1&lt;/code&gt; 个节点，就可以做到容忍 &lt;code&gt;n&lt;/code&gt; 次故障的情况，成本相比而言降低了接近一半。&lt;/p&gt;

&lt;p&gt;在 ISR 的方案下，消息被成功提交的判断就是 In-Sync 集合中的所有节点返回确认成功。一个成功提交的消息可以保证不会丢失。&lt;/p&gt;

&lt;p&gt;但是 ISR 的方案还需要考虑一种极端场景：如果所有 In-Sync 节点都故障了，怎样选取新的 Leader？有两种不同的取舍：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;牺牲可用性：坚决等待 In-Sync 机器恢复，不可用的时间可能更长&lt;/li&gt;
&lt;li&gt;牺牲一致性：选取任意一台可用的机器作为 Leader，这个机器可能是 In-Sync，也可能不是&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Kafka 默认选项中，使用了前面的方案，就是 Kafka 认为一般来说一致性更重要。&lt;/p&gt;

&lt;p&gt;另外，Kafka 还会尽可能将所有分区的 Leader 均匀分散到不同的 broker 上。&lt;/p&gt;
&lt;h2 id="其他设计思考"&gt;其他设计思考&lt;/h2&gt;&lt;h3 id="分段存储提升查找效率"&gt;分段存储提升查找效率&lt;/h3&gt;
&lt;p&gt;熟悉 Kafka 的同学也都知道，尽管 Kafka 的 topic 会进一步分为多个分区（partition），分区也是备份的最小单元，但是单个分区的日志在磁盘上还会进一步分解为多个段，也就是多个独立的文件，逻辑上可以见下面这个官方文档的图：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/026672f7-b786-4eba-aca4-d9950d05ee0c.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;好处是什么呢？当然是方便查找了，你想想，既然消息的日志是顺序存储的，那我结合二分查找算法，不就可以支持快速定位到指定的消息了吗？&lt;/p&gt;
&lt;h3 id="消息消费进度标记——consumer offset"&gt;消息消费进度标记——consumer offset&lt;/h3&gt;
&lt;p&gt;作为消息队列，broker 都需要考虑一种功能：记录消息被消费的状态。一种经典的思路是标记每个消息的状态：已投递、已确认。但是这种方案有几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;可能重复投递消息：对于 broker 来说，等待确认消息的过程中有很多未知因素，可能导致消息未能被正确确认，broker 可能会被设计成再次投递未确认消息；&lt;/li&gt;
&lt;li&gt;额外的存储空间开销：对于每个消息，都需要额外的存储空间用于标记信息；&lt;/li&gt;
&lt;li&gt;需要考虑极端场景：大量消息发送后未被确认。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka 的做法比较简单粗暴：限定每个分区一个消费者。这样一来，由于一个分区只能被一个消费者消费，而且消息顺序投递，这样就可以用一个简单的整数表示一个消费者组在一个分区上的消费进度，而不是记录每个消息的消费状态，这是一个极低的 O(1) 的常量空间开销。另外消息消费进度可以周期性更新，而且只需要更新 offset 信息，整体维护消息确认进度的成本显然更低。&lt;/p&gt;

&lt;p&gt;最后，Kafka 由于保留了历史消息，配合前面说的分段存储和查找，所以 Kafka 可以方便地支持回退 offset 的场景，以便重放消息。&lt;/p&gt;
&lt;h3 id="日志压缩（Log Compaction）"&gt;日志压缩（Log Compaction）&lt;/h3&gt;
&lt;p&gt;这里的日志压缩不同于前面提到的消息压缩，这里特指对日志进行合并重写，以只保留同个 key 的消息的最新版本。经过日志压缩后，保留下来的消息仍旧保持时序性不变，offset 也不变，但是整个分区内的消息的 offset 不再连续。&lt;/p&gt;

&lt;p&gt;至于日志压缩的作用，应该类似 Redis 的 AOF 重写，更多是为了减小存储空间的占用吧。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文以走马观花的方式介绍了 Kafka 官方对于 Kafka 设计思考以及诸多权衡，以便我自己能够快速理解 Kafka 中的很多设计的出发点，进而能够更好地理解 Kafka 的很多底层设计思路。此前我对于 Kafka 的认识仅限于它的分区设计以及 offset，特别是消费者组的设计等等，但是只是知其然，官方文档的设计思考内容帮我自己补全了对于 Kafka 知其所以然的认识。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kafka.apache.org/documentation/#design" rel="nofollow" target="_blank" title=""&gt;Kafka Documentation: 4. Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tech.meituan.com/2017/05/19/about-desk-io.html" rel="nofollow" target="_blank" title=""&gt;美团技术团队博客：磁盘 I/O 那些事&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>martin91</author>
      <pubDate>Thu, 09 Dec 2021 18:04:08 +0800</pubDate>
      <link>https://ruby-china.org/topics/41967</link>
      <guid>https://ruby-china.org/topics/41967</guid>
    </item>
    <item>
      <title>【深圳】腾讯在线教育部 ABCmouse 团队诚聘高级后台开发工程师</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/martin91/48707786-91af-4ac4-9942-d9cfa7556e33.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="产品介绍"&gt;产品介绍&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://abcmouse.qq.com/" rel="nofollow" target="_blank" title=""&gt;腾讯 ABCmouse&lt;/a&gt;，是腾讯出品，针对 3-8 岁儿童的英语启蒙互动课程。ABCMouse 品牌源自美国，是美国本土家喻户晓的启蒙教育品牌。课程内容由美国本土教研团队制作，腾讯联合研发，适合中国孩子学习。独创的激励式教学理念，激发孩子学习兴趣。8000 多个美国原版儿歌、动画、游戏，外教趣味动画课，打造沉浸式学习场景，让孩子主动要学。&lt;/p&gt;
&lt;h3 id="岗位说明"&gt;岗位说明&lt;/h3&gt;
&lt;p&gt;本次招聘岗位的是我所在的团队，主要面向 3 年以上工作经验（仅供参考，优秀者肯定可以放宽要求）的后台开发人员。我们的项目经过迁移已经全面使用 &lt;strong&gt;Golang&lt;/strong&gt; 编程语言，系统在&lt;strong&gt;微服务架构&lt;/strong&gt;之上构筑服务，项目基于内部自研开发框架（自认为很称手灵活的框架，框架本身设计于众多流行开源组件之上）以及一系列配套的服务治理组件开发。&lt;/p&gt;

&lt;p&gt;欢迎大家加我微信（扫描下方二维码）了解岗位具体信息以及内推，我会尽量解答你们的疑问，并且提供必要的面试指导。期待我们一起共事！&lt;br&gt;
&lt;img src="https://l.ruby-china.com/photo/martin91/63c65c38-eae0-404f-8eca-c9521457c98f.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="薪酬范围"&gt;薪酬范围&lt;/h2&gt;
&lt;p&gt;25k - 50k&lt;/p&gt;
&lt;h2 id="岗位要求"&gt;岗位要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;3 年以上相关工作经验，本科以上学历，计算机或相关专业；&lt;/li&gt;
&lt;li&gt;精通后台开发语言至少一种（C++、golang 等），有很好的架构理解力和良好代码规范，Golang 优先；&lt;/li&gt;
&lt;li&gt;熟悉 linux/unix 系统与开发环境，熟悉 TCP/IP 协议，socket 编程；熟悉 mysql、mongodb 等主流数据库操作以及 SQL 语言；&lt;/li&gt;
&lt;li&gt;精通多种主流自动化工具的技术原理，并有丰富自动化框架设计与开发经验，在大型项目中落地并取得效果；&lt;/li&gt;
&lt;li&gt;责任感强、有较强的逻辑思维能力、沟通能力和抗压能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="岗位职责"&gt;岗位职责&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;负责在线教育部创新产品中心后台模块开发和维护；&lt;/li&gt;
&lt;li&gt;参与业务产品功能方案讨论及开发；&lt;/li&gt;
&lt;li&gt;参与部分服务进行服务重构和技术攻关；&lt;/li&gt;
&lt;li&gt;参与教育部基础中台服务的设计与开发。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="福利"&gt;福利&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;免费班车接送上下班&lt;/li&gt;
&lt;li&gt;免费早餐，晚餐可凭夜宵券抵消（相当于一日有两餐免费）&lt;/li&gt;
&lt;li&gt;办公楼配备健身房&lt;/li&gt;
&lt;li&gt;每月 Q 币发放与 Q 米（内部福利，可用于换购百果园水果卡等各类购物卡或者各类商品）&lt;/li&gt;
&lt;li&gt;12% 公积金缴存比例&lt;/li&gt;
&lt;li&gt;员工补充商业保险福利&lt;/li&gt;
&lt;li&gt;每年春节额外 2 天假期&lt;/li&gt;
&lt;li&gt;各类培训补贴政策&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>martin91</author>
      <pubDate>Mon, 26 Apr 2021 10:16:15 +0800</pubDate>
      <link>https://ruby-china.org/topics/41194</link>
      <guid>https://ruby-china.org/topics/41194</guid>
    </item>
    <item>
      <title>[深圳] 2017-06-24 Techparty 互联网安全专题沙龙</title>
      <description>&lt;h2 id="About 珠三角技术沙龙"&gt;About 珠三角技术沙龙&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/6ac52f76-82ee-4c25-838c-c9918a7c65f8.!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://techparty.org" rel="nofollow" target="_blank" title=""&gt;&lt;strong&gt;珠三角技术沙龙&lt;/strong&gt;&lt;/a&gt; 创办于 2009 年，是一个公益性质的草根组织，每月在广州 /深圳 /珠海发起一次不同主题的技术沙龙，所有沙龙活动都由珠三角地区技术爱好者自发完成。创办至今，已经累计举办超过 100 场大大小小的技术沙龙。&lt;/p&gt;
&lt;h2 id="主题背景"&gt;主题背景&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;在每年各家安全机构出具的安全年报中，弱口令、SQL 注入、跨站脚本、不安全的通信以及不加密的存储设备等，依然是导致各网络安全漏洞爆发的主要原因。&lt;br&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;随着大数据及人工智能的应用场景增加，网络安全不仅保障了数据的安全，同时相关技术的深度挖掘也在帮网络安全提升防护能力。&lt;br&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;本期沙龙的几位讲师就将给大家分享当下安全领域中的相爱相杀。&lt;/p&gt;
&lt;h2 id="主题介绍"&gt;主题介绍&lt;/h2&gt;&lt;h3 id="1. 看黑客如何轻易入侵大型企业"&gt;1. 看黑客如何轻易入侵大型企业&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;尹毅（网名 Seay、法师）&lt;/strong&gt; Sobug 合伙人，《代码审计：企业级 web 代码安全架构》一书作者，知名安全博客 cnseay.com 博主，提出安全以人为中心，人是一切问题的根本。&lt;br&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;在早几年的入侵中，大多数黑客的目标都是放在寻找主机和应用的漏洞上，而近几年企业数据大规模泄露，地下黑客手中掌握上百亿条个人隐私数据，而利用这些数据对企业员工邮箱、内部系统、云存储等账户入侵，只需要分分钟的时间。更有大量员工主动将服务器私钥、密码等大量内部敏感信息直接存放到外部云笔记和网盘等。这些员工行为很难管控。修复掉一个 SQL 漏洞没几天同样一个研发还会写出同样的漏洞，因为修复掉的是程序漏洞而不是人，所以漏洞永远不减少。种种攻击手段让人员安全成为最难防御的威胁，本次将揭露黑客对员工的种种猥琐入侵手段。&lt;/p&gt;
&lt;h3 id="2. 保障数据安全的一些法则"&gt;2. 保障数据安全的一些法则&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;曾德康&lt;/strong&gt; 腾讯社交网络事业群安全工程师，毕业于南京大学计算机系，6 年后台业务安全经历。&lt;br&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;在开发&amp;amp;运营过程中保障数据安全的一些法则，以及数据安全体系建设的一些参考&lt;/p&gt;
&lt;h3 id="3. 从真实案例剖析企业安全"&gt;3. 从真实案例剖析企业安全&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;小黑&lt;/strong&gt; 非安全中国网核心成员，网络安全爱好者。&lt;br&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;介绍如何模拟黑客常用的工具和方法入侵企业内部，剖析企业安全现状普遍存在的安全风险。企业的信息安全部门应该如何加强防范，知己知彼，将风险值降低。&lt;/p&gt;
&lt;h2 id="时间地点"&gt;时间地点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;主办方&lt;/strong&gt;：珠三角技术沙龙&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;负责组委&lt;/strong&gt;：Martin（微信号 hong_zeqin）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;报名方式&lt;/strong&gt;：前往&lt;a href="http://www.huodongxing.com/event/4392402828000" rel="nofollow" target="_blank" title=""&gt;活动行&lt;/a&gt;报名&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;时间&lt;/strong&gt;：2017 年 6 月 24 号 14:00 - 17:30&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;地点&lt;/strong&gt;：深圳市南山区怡化金融科技大厦 18 楼（后海大道 2388 号，软件产业基地芒果大厦旁）&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>martin91</author>
      <pubDate>Tue, 20 Jun 2017 13:35:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/33266</link>
      <guid>https://ruby-china.org/topics/33266</guid>
    </item>
    <item>
      <title>动态密码生成算法介绍与实现</title>
      <description>&lt;p&gt;动态密码，亦称一次性密码（One Time Password, 简称 OTP），是一种高效简单又比较安全的密码生成算法，在我们的生活以及工作中随处可见，身为开发者，也或多或少在自己的业务系统中集成了二步验证机制，那么，技术运用，既要知其然，更要知其所以然，动态密码算法是怎样的？&lt;/p&gt;
&lt;h2 id="内容"&gt;内容&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/0c1902e75329eacde4b062973c4e1137.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="读前指引"&gt;读前指引&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;通过这篇文章，你可以了解以下知识：

&lt;ul&gt;
&lt;li&gt;动态密码的背景知识&lt;/li&gt;
&lt;li&gt;动态密码的分类&lt;/li&gt;
&lt;li&gt;不同动态密码的生成算法，HOTP 以及 TOTP&lt;/li&gt;
&lt;li&gt;HOTP 以及 TOTP 的简单的 Ruby 编程语言的实现&lt;/li&gt;
&lt;li&gt;两类算法各自注意事项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;限于篇幅，我不会讨论以下几点，有兴趣的同学可以参考我文章末尾给出的参考资料了解：

&lt;ul&gt;
&lt;li&gt;不同动态密码的安全性分析&lt;/li&gt;
&lt;li&gt;计时动态密码如何确保有效期间内，密码不被二次使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="动态密码背景介绍"&gt;动态密码背景介绍&lt;/h2&gt;
&lt;p&gt;从我的角度理解，动态密码是指随着某一事件（密码被使用、一定的时间流逝等）的发生而重新生成的密码，因为动态密码本身最大优点是防重复执行攻击 (replay attack)，它能很好地避免类似静态密码可能被暴力破解等的缺陷，现实运用中，一般采用“静态密码 + 动态密码”相结合的双因素认证，我们也称二步验证。&lt;/p&gt;

&lt;p&gt;而动态密码其实很早就出现在我们的生活里了，在移动支付发展起来之前，网银是当时最为流行的在线支付渠道，当时银行为了确保大家的网银账号支付安全，都会给网银客户配发动态密码卡，比如中国银行电子口令卡（按时间差定时生成新密码，口令卡自带电池，可保证连续使用几年），或者工商银行的电子银行口令卡（网银支付网页每次生成不同的行列序号，用户根据指定行列组合刮开密码卡上的涂层获取密码，密码使用后失效），又或者银行强制要求的短信验证码，这些都可以纳入动态密码的范畴。
&lt;img src="https://l.ruby-china.com/photo/2017/8e56e01b39c9a6491e8eb72689ce6ed5.jpeg!large" title="" alt="中行电子口令卡"&gt;
&lt;img src="https://l.ruby-china.com/photo/2017/1be8c3ed93aa8103ff010ba192232565.jpg!large" title="" alt="工行电子银行口令卡"&gt;&lt;/p&gt;

&lt;p&gt;而随着移动互联网的发展以及移动设备的智能化的不断提高，设备间的同步能力大幅提升，以前依赖独立设备的动态密码生成技术很快演变成了手机上的动态密码生成软件，以手机软件的形式生成动态密码的方式极大提高了动态密码的便携性，一个用户一个手机就可以管理任意多个动态密码的生成，这也使得在网站上推动二步验证减少了很多阻力，因为以往客户可能因为使用口令卡太麻烦，而拒绝打开二步验证机制，从而让自己的账号暴露在风险之下。最为知名的动态密码生成软件，当属 Google 的 Authenticator APP。&lt;br&gt;
&lt;img src="https://l.ruby-china.com/photo/2017/370b44a18e2e6d1454c59be2d13b8241.jpeg!large" title="" alt="Google Authenticator"&gt;&lt;/p&gt;
&lt;h2 id="动态密码算法探索之旅"&gt;动态密码算法探索之旅&lt;/h2&gt;&lt;h3 id="动态密码的分类"&gt;动态密码的分类&lt;/h3&gt;
&lt;p&gt;一般来说，常见的动态密码有两类：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;计次使用：&lt;/strong&gt;计次使用的 OTP 产出后，可在不限时间内使用，知道下次成功使用后，计数器加 1，生成新的密码。用于实现计次使用动态密码的算法叫 HOTP，接下来会对这个算法展开介绍，实际例子就是上面工行的电子银行口令卡；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;计时使用：&lt;/strong&gt;计时使用的 OTP 则可设定密码有效时间，从 30 秒到两分钟不等，而 OTP 在进行认证之后即废弃不用，下次认证必须使用新的密码。用于实现计时使用动态密码的算法叫 TOTP，接下来会对这个算法展开介绍，实际例子就是上面中行的电子口令卡。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在真正开展算法介绍之前，需要补充介绍的是：动态密码的基本认证原理是在认证双方共享密钥，也称种子密钥，并使用的同一个种子密钥对某一个事件计数、或时间值进行密码算法计算，使用的算法有对称算法、HASH、HMAC 等。记住这一点，这个是所有动态密码算法实现的基础。&lt;/p&gt;
&lt;h3 id="HOTP"&gt;HOTP&lt;/h3&gt;
&lt;p&gt;HOTP 算法，全称是“An HMAC-Based One-Time Password Algorithm”，是一种基于事件计数的一次性密码生成算法，详细的算法介绍可以查看 &lt;a href="https://tools.ietf.org/html/rfc4226" rel="nofollow" target="_blank" title=""&gt;RFC 4226&lt;/a&gt;。其实算法本身非常简单，算法本身可以用两条简短的表达式描述：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))&lt;br&gt;
PWD(K,C,digit) = HOTP(K,C) mod 10^Digit&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;上式中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;K 代表我们在认证服务器端以及密码生成端（客户设备）之间共享的密钥，在 RFC 4226 中，作者要求共享密钥最小长度是 128 位，而作者本身推荐使用 160 位长度的密钥&lt;/li&gt;
&lt;li&gt;C 表示事件计数的值，8 字节的整数，称为移动因子（moving factor），需要注意的是，这里的 C 的整数值需要用二进制的字符串表达，比如某个事件计数为 3，则 C 是 &lt;code&gt;"11"&lt;/code&gt;（此处省略了前面的二进制的数字 0）&lt;/li&gt;
&lt;li&gt;HMAC-SHA-1 表示对共享密钥以及移动因子进行 HMAC 的 SHA1 算法加密，得到 160 位长度（20 字节）的加密结果&lt;/li&gt;
&lt;li&gt;Truncate 即截断函数，后面会详述&lt;/li&gt;
&lt;li&gt;digit 指定动态密码长度，比如我们常见的都是 6 位长度的动态密码&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="Truncate 截断函数"&gt;Truncate 截断函数&lt;/h4&gt;
&lt;p&gt;由于 HMAC SHA-1 算法是既有算法，不是我们讨论重点，故而 Truncate 函数就是整个算法中最为关键的部分了。以下引用 Truncate 函数的步骤说明：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;DT(String) // String = String[0]...String[19]&lt;br&gt;
    Let OffsetBits be the low-order 4 bits of String[19]&lt;br&gt;
    Offset = StToNum(OffsetBits) // 0 &amp;lt;= OffSet &amp;lt;= 15&lt;br&gt;
    Let P = String[OffSet]...String[OffSet+3]&lt;br&gt;
    Return the Last 31 bits of P  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;结合上面的公式理解，大概的描述就是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先从第一步通过 SHA-1 算法加密得到的 20 字节长度的结果中选取最后一个字节的低字节位的 4 位（注意：动态密码算法中采用的大端 (big-endian) 存储）；&lt;/li&gt;
&lt;li&gt;将这 4 位的二进制值转换为无标点数的整数值，得到 0 到 15（包含 0 和 15）之间的一个数，这个数字作为 20 个字节中从 0 开始的偏移量；&lt;/li&gt;
&lt;li&gt;接着从指定偏移位开始，连续截取 4 个字节（32 位），最后返回 32 位中的后面 31 位。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;回到算法本身，在获得 31 位的截断结果之后，我们将其又转换为无标点的大端表示的整数值，这个值的取值范围是 0 ~ 2^31，也即 0 ~ 2.147483648E9，最后我们将这个数对 10 的乘方（digit 指数范围 1-10）取模，得到一个余值，对其前面补 0 得到指定位数的字符串。&lt;/p&gt;
&lt;h4 id="代码示例"&gt;代码示例&lt;/h4&gt;
&lt;p&gt;以下代码示例也可访问 &lt;a href="https://gist.github.com/Martin91/15a3a29acd9d138b0d6e125d8fbd5ab0" rel="nofollow" target="_blank" title=""&gt;Gist&lt;/a&gt; 获取。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'openssl'&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hotp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HMAC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int_to_bytestring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# SHA-1 算法加密&lt;/span&gt;
  &lt;span class="s2"&gt;"%0&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;i"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 取模获取指定长度数字密码&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xf&lt;/span&gt;           &lt;span class="c1"&gt;# 取最后一个字节&lt;/span&gt;
  &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# 从偏移量开始，连续取 4 个字节&lt;/span&gt;
  &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"C*"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7fffffff&lt;/span&gt;    &lt;span class="c1"&gt;# 取后面 31 位结果后得到整数&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;int_to_bytestring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;chr&lt;/span&gt;
    &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的算法实现代码量很少，核心都是按照算法描述进行多个掩码运算跟位操作而已。&lt;/p&gt;
&lt;h4 id="密码失效机制"&gt;密码失效机制&lt;/h4&gt;
&lt;p&gt;从上面的分析可以看到，一个动态密码的生成，取决于共享密钥以及移动因子的值，而共享密钥是保持不变的，最终就只有移动因子决定了密码的生成结果。所以在 HOTP 算法中，要求每次密码验证成功后，认证服务器端以及密码生成器（客户端）都要将计数器的值加 1，已确保得到新的密码。&lt;/p&gt;

&lt;p&gt;但是在这里就会引入一个问题，假如认证服务器端与密码生成器之间由于通信故障或者其他意外情况，导致两边计数器的值不同步了，那么就会导致两边生成的密码无法正确匹配。为了解决这个问题，算法在分析中建议认证服务器端在验证密码失败后，可以主动尝试计数器减 1 之后重新生成的新密码是否与客户端提交密码一致，如果是，则可以认定是客户端计数器未同步导致，这种情况下可以通过验证，并且要求客户端重新同步计数器的值。&lt;/p&gt;

&lt;p&gt;出了上面提到的计数器不同步的问题，我另外想的是，如果客户有多个密码生成器（假设 iPad 和 iPhone）为同个账号生成密码，那么计数器在多个设备间的同步可能就需要另外考虑的方案了。&lt;/p&gt;
&lt;h4 id="小结"&gt;小结&lt;/h4&gt;
&lt;p&gt;其实 HOTP 的算法比我在阅读算法前所想象的要简洁得多，而且仍然足够强健。算法本身巧妙利用了加密算法对共享密钥和计数器进行加密，确保这两个动态密码生成因子不被篡改，接着通过一个 truncate 函数随机得到一个最长 10 位的 10 进制整数，最终实现对 1 - 10 位长度动态密码的支持。算法本身的简洁也确保了算法本身可以在各种设备上实现。&lt;/p&gt;
&lt;h3 id="TOTP"&gt;TOTP&lt;/h3&gt;
&lt;p&gt;TOTP 算法，全称是 TOTP: Time-Based One-Time Password Algorithm，其基于 HOTP 算法实现，核心是将移动因子从 HOTP 中的事件计数改为时间差。完整的 TOTP 算法的说明可以查看 &lt;a href="https://tools.ietf.org/html/rfc6238" rel="nofollow" target="_blank" title=""&gt;RFC 6238&lt;/a&gt;，其公式描述也非常简单：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TOTP = HOTP(K, T)   // T is an integer
  and represents the number of time steps between the initial counter
  time T0 and the current Unix time  &lt;/p&gt;

&lt;p&gt;More specifically, T = (Current Unix time - T0) / X, where the
  default floor function is used in the computation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;通常来说，TOTP 中所使用的时间差都是当前时间戳，TOTP 将时间差除以时间窗口（密码有效期，默认 30 秒）得到时间窗口计数，以此作为动态密码算法的移动因子，这样基于 HOTP 算法就能方便得到基于时间的动态密码了。&lt;br&gt;
&lt;strong&gt;注意：&lt;/strong&gt; RFC 6238 提到，在 TOTP 算法中，可以指定不同的键控哈希算法，比如在 HOTP 中使用的是 HMAC-SHA1 算法，而在 TOTP，除此之外，还可以使用 HMAC-SHA256 或者 HMAC-SHA512。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TOTP implementations MAY use HMAC-SHA-256 or HMAC-SHA-512 functions,
  based on SHA-256 or SHA-512 [SHA2] hash functions, instead of the
  HMAC-SHA-1 function that has been specified for the HOTP computation
  in [RFC4226].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id="代码示例"&gt;代码示例&lt;/h4&gt;
&lt;p&gt;以下代码示例也可访问 &lt;a href="https://gist.github.com/Martin91/15a3a29acd9d138b0d6e125d8fbd5ab0" rel="nofollow" target="_blank" title=""&gt;Gist&lt;/a&gt; 获取。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'hotp'&lt;/span&gt;

&lt;span class="c1"&gt;# 仅为示例方便，复用了前述代码，如果需要使用 HMAC-SHA256 或者 HMAC-SHA512，需要另外实现&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;totp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;initial_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;

  &lt;span class="n"&gt;hotp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到了吧，极其简短的实现代码！一个时间计数的动态密码算法就此诞生，如此简单的算法，却是支撑多少业务系统安全运作的基石，颇有四两拨千斤的快感！&lt;/p&gt;
&lt;h4 id="问题探讨"&gt;问题探讨&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;ROTP 算法中的主要问题是计数器的同步，而 TOTP 也不例外，只是问题在于服务器端与客户端之间时间的同步，由于现在互联网的发达，加上移动设备一般都会按照网络时间设置设备时间，基本上时间的相对同步都不是问题；&lt;/li&gt;
&lt;li&gt;时间同步的另一个问题其实是边界问题，假如客户端生成密码的时间刚好是第 29 秒，而由于网络延迟等原因，服务器受理验证时刚好是下一个时间窗口的第 1 秒，这个时候会导致密码验证失效。于是，TOTP 算法在其算法讨论中，也建议服务器在验证密码失败之后，可以尝试将自身的时间窗口值减 1 之后重新生成密码比对，如果验证通过，说明验证不通过是时间窗口的边界问题导致，这个时候可以认为密码验证通过。&lt;/li&gt;
&lt;li&gt;基于时间的动态密码的另一个好处是避免了基于计数器的多设备间的计数器同步问题，因为每台设备以及服务端都可以自行与网络时间（共同时间标准）校准，而无需依赖服务端的时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="Google Authenticator"&gt;Google Authenticator&lt;/h2&gt;
&lt;p&gt;在 Google Authenticator 的&lt;a href="https://github.com/google/google-authenticator" rel="nofollow" target="_blank" title=""&gt;开源项目&lt;/a&gt;的 README 里有明确提到：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These implementations support the HMAC-Based One-time Password (HOTP) algorithm specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm specified in RFC 6238.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;也就是说，至此，我们也明白了，其实 Google Authenticator 算法核心也是 HOTP 以及 TOTP，在明白了整个动态密码算法的核心之后，有没有一种豁然开朗的感觉呢？既知其然，又知其所以然了，对吧？每次看着应用定时生成密码，&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章简单介绍了两类常见的动态密码的生成算法，算法本身简洁不复杂，效率并且足够强健。这篇文章的目的是方便跟我一样希望了解算法核心的小伙伴，而在 RFC 文档中，仍有大量关于算法本身的安全性方面的探讨，有兴趣的小伙伴可以去看一下。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://zh.wikipedia.org/zh/%E4%B8%80%E6%AC%A1%E6%80%A7%E5%AF%86%E7%A2%BC" rel="nofollow" target="_blank" title=""&gt;Wikipedia: 一次性密码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mdp/rotp" rel="nofollow" target="_blank" title=""&gt;github: rotp&lt;/a&gt;，HOTP 以及 TOTP 的算法实现以及其他封装&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.csdn.net/goldboar/article/details/7065948" rel="nofollow" target="_blank" title=""&gt;动态口令（OTP）认证技术概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tools.ietf.org/html/rfc4226" rel="nofollow" target="_blank" title=""&gt;RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tools.ietf.org/html/rfc6238" rel="nofollow" target="_blank" title=""&gt;RFC 6238 - TOTP: Time-Based One-Time Password Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.csdn.net/article/2014-09-23/2821808-Google-Authenticator" rel="nofollow" target="_blank" title=""&gt;详解 Google Authenticator 工作原理&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="示例源码"&gt;示例源码&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/Martin91/15a3a29acd9d138b0d6e125d8fbd5ab0" rel="nofollow" target="_blank" title=""&gt;Gist: OTP algorithms in Ruby&lt;/a&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sat, 18 Feb 2017 12:53:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/32333</link>
      <guid>https://ruby-china.org/topics/32333</guid>
    </item>
    <item>
      <title>周末到了，来段代码压压惊</title>
      <description>&lt;p&gt;最近一段时间，写了两篇关于 sidekiq 的源码分析，但是一直想要补充的一段 sidekiq 里边的代码其实是挺有趣也挺逗的，所以这个星期就不要长篇大论的源码分析，来点轻松点的吧。&lt;/p&gt;



&lt;p&gt;这个代码是这样的 o(╯□╰)o：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq.rb#L51-L53&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Sidekiq&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;❨╯°□°❩╯︵┻━┻&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Calm down, yo."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Haha, are you kidding me? 见过用特殊字符或者特殊语言文字做方法名的，但是用颜文字，我还是第一次见。但是别笑，本着工科男严谨与求知的精神，我全局搜索了下这个方法的调用，结果更搞笑的结果来了，这个方法根本就没有真实调用，但是相应的测试用例同样非常逗 2333333333！！！&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"❨╯°□°❩╯︵┻━┻"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;after&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;STDOUT&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"allows angry developers to express their emotional constitution and remedies it"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;❨╯°□°❩╯︵┻━┻&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="s2"&gt;"Calm down, yo.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈哈，这只是难道为了方便程序员怒火中烧的时候表达想掀桌的内心吗？(PS: 其实最开始那个方法的原意是为了测试文件的 UTF-8 编码，只是后来成了偶尔被拿起来讨论的段子)&lt;/p&gt;

&lt;p&gt;当然，这个问题其实早就有很多人发现了，Ruby China 上也有好多的讨论了。今天是个快乐周六，让我再从网络上搜罗多一些搞笑的代码吧，哈哈~~~&lt;/p&gt;
&lt;h2 id="精彩段子时间"&gt;精彩段子时间&lt;/h2&gt;
&lt;p&gt;每一个在注释或者代码里藏段子的程序员上辈子都是折翼的逗逼，不信，你看！&lt;/p&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Something is really wrong."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;//ha ha&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个程序猿写代码时到底什么心态啊，故意抛个异常，还在注释里如此狂妄？坟头草可除了？&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;//When I wrote this, only God and I understood what I was doing&lt;/span&gt;
&lt;span class="c1"&gt;//Now, God only knows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈哈，这个是我看的时候感觉比较搞笑的了，有种代码叫做天知地知我知，后来变成只有天知道了。。。&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// drunk, fix later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好自觉的程序猿啊，酒后不宜改代码，多提倡，建议立法机关考虑加条规定，凡是酒后写代码的，一律立案侦办！&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define TRUE FALSE
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以想象当这个 commit 被 merge 进生产环境之后。。。哈哈，整个世界黑白颠倒！对的就是错的，错的就是对的！！！&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# returns 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这。。。不。。。是。。。废。。。话。。。吗。。。？！&lt;/p&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;//who cares?&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈哈，我知道系统有异常啊，但是我才不管呢，哼~~~ ╭(╯^╰)╮&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// I am not responsible of this code.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;o(╯□╰)o 这个。。。不是我干的~~~真的！！！&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// it was hard to write&lt;/span&gt;
&lt;span class="c1"&gt;// so it should be hard to read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以不能怪我咯，代码本来就不好写，你还想我让你好读？？？ &lt;strong&gt;╭(╯^╰)╮&lt;/strong&gt; 来啊，互相伤害啊！&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// I have to find a better job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这位哥意识到自己职业生涯的终结了吗？&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If this code works, it was written by Paul DiLascia. If not, I don't know&lt;/span&gt;
&lt;span class="c1"&gt;// who wrote it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反正我不管，好的代码跟我有关，不好的代码肯定不是我写的！！！&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux Sex&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; unzip &lt;span class="p"&gt;;&lt;/span&gt; strip &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; finger &lt;span class="p"&gt;;&lt;/span&gt; mount &lt;span class="p"&gt;;&lt;/span&gt; fsck &lt;span class="p"&gt;;&lt;/span&gt; more &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; umount &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好污的一段代码！！！天哪，我的眼睛！&lt;code&gt;(*/ω╲*)&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* in a galaxy far far away */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我有故事你有酒，我来给你讲一宿！O(∩_∩)O&lt;/p&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Always returns true.
 */&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我读书少，你别骗我！（这段代码据说是真的跟注释说的一样的~~~）&lt;/p&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Dear maintainer:&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Once you are done trying to 'optimize' this routine,&lt;/span&gt;
&lt;span class="c1"&gt;// and have realized what a terrible mistake that was,&lt;/span&gt;
&lt;span class="c1"&gt;// please increment the following counter as a warning&lt;/span&gt;
&lt;span class="c1"&gt;// to the next guy:&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// total_hours_wasted_here = 42&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈哈，这是受害者联盟吗？来来来，你掉坑里了吗？签个字登记一下吧！&lt;/p&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;//true my ass! this doesn't work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈哈，童话里都是骗人的 o(╯□╰)o&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Peter wrote this, nobody knows what it does, don't change it!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编程界网红&lt;strong&gt;Peter&lt;/strong&gt;又中招……&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;上面所有有趣的代码片段跟注释都是从以下帖子或者讨论中摘录，欢迎点击链接阅读原文：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="http://fuzzzyblog.blogspot.hk/2014/09/40-most-funny-code-comments.html" rel="nofollow" target="_blank" title=""&gt;Fuzzzy blog: 40 most funny code comments ever&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.quora.com/What-are-some-of-the-funniest-comments-in-source-code" rel="nofollow" target="_blank" title=""&gt;What are some of the funniest comments in source code?&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;大多数程序猿的日常工作繁重辛苦，加班跟高度的精神压力都是家常便饭，如果你的身边有这样的程序猿，请一定要多多珍惜他们！也祝愿看到这篇帖子的程序猿们开怀一笑，生活已经如此多艰，快快休息放松一下吧！&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sat, 26 Nov 2016 22:46:08 +0800</pubDate>
      <link>https://ruby-china.org/topics/31706</link>
      <guid>https://ruby-china.org/topics/31706</guid>
    </item>
    <item>
      <title>Sidekiq 系统信号处理源码分析</title>
      <description>&lt;h3 id="引言"&gt;引言&lt;/h3&gt;
&lt;p&gt;在之前的文章&lt;a href="/topics/31470" title=""&gt;《Sidekiq 任务调度流程分析》&lt;/a&gt;中，我们一起仔细分析了 Sidekiq 是如何基于多线程完成队列任务处理以及调度的。我们在之前的分析里，看到了不管是 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 还是 &lt;code&gt;Sidekiq::Processor&lt;/code&gt; 的核心代码里，都会有一个由 &lt;code&gt;@done&lt;/code&gt; 实例变量控制的循环体：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L63-L73&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scheduler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;initial_wait&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;           &lt;span class="c1"&gt;# 这是 poller 的循环控制&lt;/span&gt;
      &lt;span class="n"&gt;enqueue&lt;/span&gt;
      &lt;span class="n"&gt;wait&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Scheduler exiting..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L66-L77&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;           &lt;span class="c1"&gt;# 这是我们常说的 worker 循环控制&lt;/span&gt;
      &lt;span class="n"&gt;process_one&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_died&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，这些 &lt;code&gt;@done&lt;/code&gt; 实例变量决定了 &lt;code&gt;poller&lt;/code&gt; 线程跟 &lt;code&gt;worker&lt;/code&gt; 线程是否循环执行？一旦 &lt;code&gt;@done&lt;/code&gt; 被改为 &lt;code&gt;true&lt;/code&gt;，那循环体就不再执行，线程自然也就是退出了。于是，单从这些代码，我们可以断定，Sidekiq 就是通过设置 &lt;code&gt;@done&lt;/code&gt; 的值来通知一个线程安全退出（graceful exit）的。我们也知道，生产环境中，我们是通过发送信号的方式来告诉 sidekiq 退出或者进入静默 (quiet) 状态的，那么，这里的 &lt;code&gt;@done&lt;/code&gt; 是怎么跟信号处理联系起来的呢？这些就是今天这篇文章的重点了！&lt;/p&gt;
&lt;h3 id="注意"&gt;注意&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;今天的分析所参考的 sidekiq 的源码对应版本是 4.2.3；&lt;/li&gt;
&lt;li&gt;今天所讨论的内容，将主要围绕系统信号处理进行分析，无关细节将不赘述，如有需要，请自行翻阅 sidekiq 源码；&lt;/li&gt;
&lt;li&gt;今天的文章跟上篇的《Sidekiq 任务调度流程分析》紧密相关，上篇文章介绍的启动过程跟任务调度会帮助这篇文章的理解，如果还没有阅读上篇文章的，建议先阅读后再来阅读这一篇信号处理的文章。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="你将了解到什么？"&gt;你将了解到什么？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Sidekiq 信号处理机制；&lt;/li&gt;
&lt;li&gt;为什么重启 Sidekiq 时，&lt;code&gt;USR1&lt;/code&gt; 信号（即进入 &lt;code&gt;quiet&lt;/code&gt; 模式）需要尽可能早，而进程的退出重启需要尽可能晚。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="从头再来"&gt;从头再来&lt;/h3&gt;
&lt;p&gt;因为前一篇文章着眼于任务调度，所以略过了其他无关细节，包括信号处理，这篇文章则将镜头对准信号处理，所以让我们从头再来一遍，只是这一次，我们只关心与信号处理有关的代码。&lt;/p&gt;

&lt;p&gt;依旧是从 &lt;code&gt;cli.rb&lt;/code&gt; 文件开始，它是 Sidekiq 核心代码的生命起点，因为 Sidekiq 命令行启动后，它是第一个被执行的代码，Sidekiq 启动过程中调用了 &lt;code&gt;Sidekiq::CLI#run&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/cli.rb#L49-L106&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="n"&gt;boot_system&lt;/span&gt;
  &lt;span class="n"&gt;print_banner&lt;/span&gt;

  &lt;span class="n"&gt;self_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self_write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;

  &lt;span class="sx"&gt;%w(INT TERM USR1 USR2 TTIN)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;begin&lt;/span&gt;
      &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;self_write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Signal &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not supported"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ... other codes&lt;/span&gt;

  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self_read&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;gets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
      &lt;span class="n"&gt;handle_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Interrupt&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&gt;'Shutting down'&lt;/span&gt;
    &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;
    &lt;span class="c1"&gt;# Explicitly exit so busy Processor threads can't block&lt;/span&gt;
    &lt;span class="c1"&gt;# process shutdown.&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Bye!"&lt;/span&gt;
    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上的代码就是整个 Sidekiq 最顶层的信号处理的核心代码了，让我们慢慢分析！
首先，&lt;code&gt;self_read, self_write = IO.pipe&lt;/code&gt; 创建了一个模拟管道的 IO 对象，并且同时返回这个 管道的一个写端以及一个读端，通过这两端，就可以实现对管道的读写了。需要注意的是，&lt;code&gt;IO.pipe&lt;/code&gt; 创建的读端在读的时候不会自动生成 &lt;code&gt;EOF&lt;/code&gt; 符，所以这就要求读时，写端是关闭的，而写时，读端是关闭的，一句话说，就是这样的管道不允许读写端同时打开。关于 &lt;code&gt;IO.pipe&lt;/code&gt; 还有挺多细节跟需要注意的点，如果还需要了解，请阅读&lt;a href="https://ruby-doc.org/core-2.3.1/IO.html#method-c-pipe" rel="nofollow" target="_blank" title=""&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;上面说的管道本质上只是一个 IO 对象而已，暂时不用纠结太多，让我们接着往下读：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="sx"&gt;%w(INT TERM USR1 USR2 TTIN)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;self_write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Signal &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not supported"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码就比较有意思了，最外层遍历了一个系统信号的数组，然后逐个信号进行监听（trap，或者叫捕捉？）。让我们聚焦在 &lt;code&gt;trap&lt;/code&gt; 方法的调用跟其 block 上，查阅 &lt;a href="https://ruby-doc.org/core-2.2.0/Signal.html#method-c-trap" rel="nofollow" target="_blank" title=""&gt;Ruby 文档&lt;/a&gt;，发现 &lt;code&gt;trap&lt;/code&gt; 是 &lt;code&gt;Signal&lt;/code&gt; 模块下的一个方法，&lt;code&gt;Signal&lt;/code&gt; 主要是处理与系统信号有关的任务，然后 &lt;code&gt;trap&lt;/code&gt; 的作用是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Specifies the handling of signals. The first parameter is a signal name (a string such as “SIGALRM”, “SIGUSR1”, and so on) or a signal number...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以，前面的那段代码的意思就很容易理解了，Sidekiq 注册了对 &lt;code&gt;INT&lt;/code&gt;、&lt;code&gt;TERM&lt;/code&gt;、&lt;code&gt;USR1&lt;/code&gt;、&lt;code&gt;USR2&lt;/code&gt;以及&lt;code&gt;TTIN&lt;/code&gt;等系统信号的处理，而在进程收到这些信号时，就会执行 &lt;code&gt;self_write.puts(sig)&lt;/code&gt;，也就是将收到的信号通过之前介绍的管道写端 &lt;code&gt;self_write&lt;/code&gt; 记录下来。什么？只记录下来，那还得处理啊？！&lt;/p&gt;

&lt;p&gt;稍安勿躁，让我们接着往下分析 &lt;code&gt;Sidekiq::CLI#run&lt;/code&gt; 方法末尾的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self_read&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;gets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
    &lt;span class="n"&gt;handle_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Interrupt&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&gt;'Shutting down'&lt;/span&gt;
  &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;
  &lt;span class="c1"&gt;# Explicitly exit so busy Processor threads can't block&lt;/span&gt;
  &lt;span class="c1"&gt;# process shutdown.&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Bye!"&lt;/span&gt;
  &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到没有，这里有个循环，循环控制条件里，&lt;code&gt;readable_io = IO.select([self_read])&lt;/code&gt; 是从前面的管道的读端 &lt;code&gt;self_read&lt;/code&gt; 阻塞地等待信号的到达。对于 &lt;code&gt;IO.select&lt;/code&gt;，&lt;a href="https://ruby-doc.org/core-2.3.1/IO.html#method-c-select" rel="nofollow" target="_blank" title=""&gt;Ruby 官方文档&lt;/a&gt;介绍如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Calls select(2) system call. It monitors given arrays of IO objects, waits until one or more of IO objects are ready for reading, are ready for writing, and have pending exceptions respectively, and returns an array that contains arrays of those IO objects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以这里就是说 Sidekiq 主线程首先负责执行完其他初始化工作，最后阻塞在信号等待以及处理。在其等到新的信号之后，进入上面代码展示的循环体：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;gets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
&lt;span class="n"&gt;handle_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里语法细节先不深究，我们看下这两行代码第一行是从前面说的管道中读取信号，并且将信号传递给 &lt;code&gt;handle_signal&lt;/code&gt; 方法，让我们接着往下看 &lt;code&gt;handle_signal&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/cli.rb#L125-L153&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="s2"&gt;"Got &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; signal"&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'INT'&lt;/span&gt;
    &lt;span class="c1"&gt;# Handle Ctrl-C in JRuby like MRI&lt;/span&gt;
    &lt;span class="c1"&gt;# http://jira.codehaus.org/browse/JRUBY-4637&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Interrupt&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'TERM'&lt;/span&gt;
    &lt;span class="c1"&gt;# Heroku sends TERM and then waits 10 seconds for process to exit.&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Interrupt&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'USR1'&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Received USR1, no longer accepting new work"&lt;/span&gt;
    &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quiet&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'USR2'&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:logfile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Received USR2, reopening log file"&lt;/span&gt;
      &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reopen_logs&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'TTIN'&lt;/span&gt;
    &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt; &lt;span class="s2"&gt;"Thread TID-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'label'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt;
        &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;no backtrace available&amp;gt;"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的代码挺长，但是一点都不难理解，我简单解释下就够了。当进程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;收到 &lt;code&gt;TERM&lt;/code&gt; 或者 &lt;code&gt;INT&lt;/code&gt;信号时，直接抛出 &lt;code&gt;Interrupt&lt;/code&gt; 中断；&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;USR1&lt;/code&gt; 信号时，则通知 &lt;code&gt;launcher&lt;/code&gt; 执行 &lt;code&gt;.quiet&lt;/code&gt; 方法，Sidekiq 在这里进入 Quiet 模式（怎么进入？）；&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;USR2&lt;/code&gt; 信号时，重新打开日志；&lt;/li&gt;
&lt;li&gt;收到 &lt;code&gt;TTIN&lt;/code&gt; 信号时，打印所有线程当前正在执行的代码列表。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;到此，一个信号从收到被存下，到被取出处理的大致过程就是这样的，至于具体的处理方式，我们下个章节详细展开。现在有一点需要补充的是，上面讲当 Sidekiq 收到 &lt;code&gt;TERM&lt;/code&gt; 或者 &lt;code&gt;INT&lt;/code&gt; 信号时，都会抛出 &lt;code&gt;Interrupt&lt;/code&gt; 中断异常，那这个异常又是如何处理的呢？我们回过头去看刚才最开始的 &lt;code&gt;Sidekiq::CLI#run&lt;/code&gt; 方法末尾的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self_read&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readable_io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;gets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
    &lt;span class="n"&gt;handle_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Interrupt&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&gt;'Shutting down'&lt;/span&gt;
  &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;
  &lt;span class="c1"&gt;# Explicitly exit so busy Processor threads can't block&lt;/span&gt;
  &lt;span class="c1"&gt;# process shutdown.&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Bye!"&lt;/span&gt;
  &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来是 &lt;code&gt;run&lt;/code&gt; 方法在处理信号时，声明了 &lt;code&gt;rescue Interrupt&lt;/code&gt;，捕捉了 &lt;code&gt;Interrupt&lt;/code&gt; 中断异常，并且在异常处理时打印必要日志，同时执行 &lt;code&gt;launcher.stop&lt;/code&gt; 通知各个线程停止工作，最后调用 &lt;code&gt;exit&lt;/code&gt; 方法强制退出进程，到此，一个 Sidekiq 进程就彻底退出了。
但是问题又来了，信号处理的大致过程我是知道了，但是具体的 &lt;code&gt;launcher.quiet&lt;/code&gt; 跟 &lt;code&gt;launcher.stop&lt;/code&gt; 都干了些什么呢？&lt;/p&gt;
&lt;h3 id="Sidekiq::Launcher#quiet 源码探索"&gt;Sidekiq::Launcher#quiet 源码探索&lt;/h3&gt;
&lt;p&gt;老规矩，先上代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L32-L36&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quiet&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="vi"&gt;@manager.quiet&lt;/span&gt;
  &lt;span class="vi"&gt;@poller.terminate&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码只有短短三行。Launcher 对象首先设置自己的实例变量 &lt;code&gt;@done&lt;/code&gt; 的值为 &lt;code&gt;true&lt;/code&gt;，接着执行 &lt;code&gt;@manager.quiet&lt;/code&gt; 以及 &lt;code&gt;@poller.terminate&lt;/code&gt;。看方法命名上理解，应该是 Luancher 对象又将 quiet 的消息传递给了 &lt;code&gt;@manager&lt;/code&gt; 即 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 对象，同时通知 &lt;code&gt;@poller&lt;/code&gt; 即 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 对象结束工作。那到底是不是真的这样呢？让我们继续深挖！&lt;/p&gt;
&lt;h4 id="Sidekiq::Manager#quiet"&gt;Sidekiq::Manager#quiet&lt;/h4&gt;
&lt;p&gt;让我们来看看 &lt;code&gt;Sidekiq::Manager#quiet&lt;/code&gt; 方法的代码&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L51-L58&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quiet&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@done&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Terminating quiet workers"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="vi"&gt;@workers.each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;fire_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:quiet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码也很短，首先将 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 对象自身的 &lt;code&gt;@done&lt;/code&gt; 实例变量的值设置为 &lt;code&gt;true&lt;/code&gt;，接着对其所管理的每一个 worker，都发出一个 &lt;code&gt;terminate&lt;/code&gt; 消息。让我们接着往下看 worker 对象（&lt;code&gt;Sidekiq::Processor&lt;/code&gt; 对象）的 &lt;code&gt;#terminate&lt;/code&gt; 方法定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L42-L46&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@thread&lt;/span&gt;
  &lt;span class="vi"&gt;@thread.value&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的代码依然保持了精短的特点！跟上一层逻辑一样，worker 在处理 &lt;code&gt;terminate&lt;/code&gt; 时，同样设置自己的 &lt;code&gt;@done&lt;/code&gt; 实例变量为 &lt;code&gt;true&lt;/code&gt; 后返回，但是，如果其参数 &lt;code&gt;wait&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，则会保持主线程等待，直到 &lt;code&gt;@thread&lt;/code&gt; 线程退出（&lt;code&gt;@thread.value&lt;/code&gt; 相当于执行 &lt;code&gt;@thread.join&lt;/code&gt;并且返回线程的返回值，可参考 &lt;a href="https://ruby-doc.org/core-2.2.0/Thread.html#method-i-value" rel="nofollow" target="_blank" title=""&gt;Ruby 文档&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;那么，这里就要问了，worker 设置 &lt;code&gt;@done&lt;/code&gt; 为 true 是要干嘛？这里好像也没有做什么特别的事啊？！勿急，还记得上篇文章介绍 worker 运行时的核心代码吗？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L66-L77&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;process_one&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_died&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到了吧，&lt;code&gt;@done&lt;/code&gt; 变量可是一个重要的开关，当 &lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 时，worker 一直周而复始地从队列中取任务并且老老实实干活；而当 &lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 时，worker 在处理完当前的任务之后，便不再执行新的任务，执行 &lt;code&gt;@msg.processor_stopped(self)&lt;/code&gt; 通知 worker 管理器自己已经退出工作，最终 &lt;code&gt;#run&lt;/code&gt; 方法返回。由于 &lt;code&gt;#run&lt;/code&gt; 方法是在独立线程里执行的，所以当 &lt;code&gt;#run&lt;/code&gt; 方法返回时，其所在的线程自然也就退出了。&lt;/p&gt;

&lt;p&gt;那关于 worker 的 quiet 模式进入过程就是这么简单，通过一个共享变量 &lt;code&gt;@done&lt;/code&gt; 便实现了对工作线程的控制。&lt;/p&gt;
&lt;h4 id="Sidekiq::Scheduled::Poller#terminate"&gt;Sidekiq::Scheduled::Poller#terminate&lt;/h4&gt;
&lt;p&gt;前面说到 &lt;code&gt;Sidekiq::Launcher#quiet&lt;/code&gt; 执行时，先将消息传递给了 worker 管理器，随后执行了 &lt;code&gt;@poller.terminate&lt;/code&gt;，那我们来看看 &lt;code&gt;#terminate&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L53-L61&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;terminate&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@thread&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@thread&lt;/span&gt;
    &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
    &lt;span class="vi"&gt;@sleeper&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又是如此简短的代码。poller 退出的逻辑跟 worker 退出的逻辑非常一致，都是同样先设置自己的 &lt;code&gt;@done&lt;/code&gt; 实例变量的值为 &lt;code&gt;true&lt;/code&gt;，接着等待线程 &lt;code&gt;@thread&lt;/code&gt; 退出，最后 poller 返回。&lt;/p&gt;

&lt;p&gt;那么，poller 的 &lt;code&gt;@done&lt;/code&gt; 是不是也是用来控制线程退出呢？答案是肯定的！&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L63-L73&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scheduler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;initial_wait&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;enqueue&lt;/span&gt;
      &lt;span class="n"&gt;wait&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Scheduler exiting..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还记得上面这段代码吗？poller 在每次将定时任务压回任务队列之后，等待一定时间，然后重新检查 &lt;code&gt;@done&lt;/code&gt; 的值，如果为 &lt;code&gt;true&lt;/code&gt;，则 poller 直接返回退出，因为 &lt;code&gt;#start&lt;/code&gt; 方法里的循环体在新线程中执行，当循环结束时，线程自然也退出了。&lt;/p&gt;
&lt;h4 id="小结"&gt;小结&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;当 Sidekiq 收到 &lt;code&gt;USR1&lt;/code&gt; 系统信号时，Sidekiq 主线程向 &lt;code&gt;@launcher&lt;/code&gt; 发送 &lt;code&gt;quiet&lt;/code&gt; 消息，&lt;code&gt;@launcher&lt;/code&gt; 又将消息传递给 &lt;code&gt;@manager&lt;/code&gt; ，同时向 &lt;code&gt;@poller&lt;/code&gt; 发出 &lt;code&gt;terminate&lt;/code&gt; 消息；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@manager&lt;/code&gt; 在收到 &lt;code&gt;quiet&lt;/code&gt; 消息时，逐一对运行中的 worker 发送 &lt;code&gt;terminate&lt;/code&gt; 消息，worker 收到消息后，设置自己的 &lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，标识不再处理新任务，当前任务处理完成后退出线程；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@poller&lt;/code&gt; 在收到 &lt;code&gt;terminate&lt;/code&gt; 消息后，也是设置自己的 &lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，在本次任务执行完毕后，线程也退出；&lt;/li&gt;
&lt;li&gt;Sidekiq 进入 quiet 模式之后，所有未处理任务以及新任务都不再处理，直到 sidekiq 的下一次重启。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="Sidekiq::Launcher#stop 源码探索"&gt;Sidekiq::Launcher#stop 源码探索&lt;/h3&gt;
&lt;p&gt;前面介绍的是 Sidekiq 进入 quiet 模式的过程，那 Sidekiq 的停止过程又是怎样的呢？&lt;/p&gt;

&lt;p&gt;让我们从 &lt;code&gt;Sidekiq::Launcher#stop&lt;/code&gt; 方法开始寻找答案：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L41-L56&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;
  &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="vi"&gt;@options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:timeout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="vi"&gt;@manager.quiet&lt;/span&gt;
  &lt;span class="vi"&gt;@poller.terminate&lt;/span&gt;

  &lt;span class="vi"&gt;@manager.stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Requeue everything in case there was a worker who grabbed work while stopped&lt;/span&gt;
  &lt;span class="c1"&gt;# This call is a no-op in Sidekiq but necessary for Sidekiq Pro.&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fetch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BasicFetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulk_requeue&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="vi"&gt;@options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;clear_heartbeat&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，&lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 对象设定了一个强制退出的 &lt;code&gt;deadline&lt;/code&gt;，时间是以当前时间加上配置的 &lt;code&gt;timeout&lt;/code&gt;，这个时间&lt;a href="https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq.rb#L23" rel="nofollow" target="_blank" title=""&gt;默认是 8 秒&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;接着，设定对象本身的 &lt;code&gt;@done&lt;/code&gt; 变量的值为 &lt;code&gt;true&lt;/code&gt;，然后分别对 &lt;code&gt;@manager&lt;/code&gt; 和 &lt;code&gt;@poller&lt;/code&gt; 发送 &lt;code&gt;quiet&lt;/code&gt; 和 &lt;code&gt;terminate&lt;/code&gt; 消息，这个过程就是我们上面说的 &lt;code&gt;Sidekiq::Launcher#quiet&lt;/code&gt; 的过程，所以，这里的代码主要是 Sidekiq 要确保退出前已经通知各个线程准备退出。&lt;/p&gt;

&lt;p&gt;接下来的代码就比较重要了，我们先看这一行：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@manager.stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在通知完 &lt;code&gt;@manager&lt;/code&gt; 进入 quiet 模式之后，launcher 向 &lt;code&gt;@manager&lt;/code&gt; 发送了 &lt;code&gt;stop&lt;/code&gt; 消息，并且同时传递了 &lt;code&gt;deadline&lt;/code&gt; 参数。让我们接着继续往下看：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L61-L83&lt;/span&gt;
&lt;span class="no"&gt;PAUSE_TIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;STDOUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tty?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;quiet&lt;/span&gt;
  &lt;span class="n"&gt;fire_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:shutdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# some of the shutdown events can be async,&lt;/span&gt;
  &lt;span class="c1"&gt;# we don't have any way to know when they're done but&lt;/span&gt;
  &lt;span class="c1"&gt;# give them a little time to take effect&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="no"&gt;PAUSE_TIME&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@workers.empty&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Pausing to allow workers to finish..."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;PAUSE_TIME&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@workers.empty&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="no"&gt;PAUSE_TIME&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@workers.empty&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="n"&gt;hard_shutdown&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码，manager 首先调用了自身的 &lt;code&gt;quiet&lt;/code&gt; 方法（这里就真的多此一举了，因为外层的 launcher 已经调用过一次了），然后 manager 执行 &lt;code&gt;sleep&lt;/code&gt; 系统调用进入休眠，持续时间为 0.5 秒，休眠结束后检查所有 worker 是否已经都退出，如果退出，则直接返回，任务提前结束；如果仍有 worker 未退出，则检查当前时间是否接近强制退出的 deadline，如果不是，则重复“检查所有 worker 退出 - 休眠”的过程，直到 deadline 来临，或者 worker 线程都已经全部退出。如果最后到达 deadline，仍有 worker 线程未退出，则最后执行 &lt;code&gt;hard_shutdown&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L108-L135&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hard_shutdown&lt;/span&gt;
  &lt;span class="n"&gt;cleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="vi"&gt;@plock.synchronize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;cleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@workers.dup&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;job&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;compact&lt;/span&gt;

    &lt;span class="c1"&gt;# ... other codes&lt;/span&gt;

    &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fetch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BasicFetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulk_requeue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;hard_shutdown&lt;/code&gt; 方法在执行时，首先克隆了当前仍未退出的 &lt;code&gt;@workers&lt;/code&gt; 列表，接着获取每个 worker 当前正在处理的任务，将这些正在执行中的任务数据通过 &lt;code&gt;strategy.bulk_requeue(jobs, @options)&lt;/code&gt; 重新写回队列，而最后对每一个 worker 发送 &lt;code&gt;kill&lt;/code&gt; 消息：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L48-L58&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@thread&lt;/span&gt;

  &lt;span class="vi"&gt;@thread.raise&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
  &lt;span class="vi"&gt;@thread.value&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;worker 在收到 &lt;code&gt;kill&lt;/code&gt; 消息时，首先设置自己的 &lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，最后向 worker 所关联的线程抛出 &lt;code&gt;::Sidekiq::Shutdown&lt;/code&gt; 异常。让我们看看 worker 的线程又是如何处理异常的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L66-L77&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;process_one&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_died&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又回到 worker 的 &lt;code&gt;run&lt;/code&gt; 方法这里，可以看到，&lt;code&gt;run&lt;/code&gt; 方法捕捉了 &lt;code&gt;Sidekiq::Shutdown&lt;/code&gt; 异常，并且在处理异常时，只是执行 &lt;code&gt;@mgr.processor_stopped(self)&lt;/code&gt;，通知 manager 自己已经退出，由于已经跳出正常流程，worker 的 &lt;code&gt;run&lt;/code&gt; 方法返回，线程也因此得以退出。至此，worker 也都正常退出了。&lt;/p&gt;
&lt;h4 id="小结"&gt;小结&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;launcher 在执行退出时，首先按照 quiet 的流程先通知各个线程准备退出；&lt;/li&gt;
&lt;li&gt;接着 launcher 向 manager 下达 &lt;code&gt;stop&lt;/code&gt; 指令，并且给出最后期限（&lt;code&gt;deadline&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;manager 在给定的限时内，尽可能等待所有 worker 执行完自己退出，对于到达限时仍未退出的 worker，manager 备份了每个 worker 的当前任务，重新加入队列，确保任务至少完整执行一次，然后通过向线程抛出异常的方式，迫使 worker 的线程被动退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Sidekiq 简单高效利用了系统信号，并且有比较清晰明了的信号处理过程；&lt;/li&gt;
&lt;li&gt;Sidekiq 在信号处理的过程中，各个组件协调很有条理，消息逐级传递，而且对被强制停止的任务也有备份方案；&lt;/li&gt;
&lt;li&gt;我们可以从 Sidekiq 的系统信号处理机制上借鉴不少东西，比如常用系统信号的分类处理等；&lt;/li&gt;
&lt;li&gt;对于多线程的控制，通过共享变量以及异常的方式做到 &lt;code&gt;graceful&lt;/code&gt; 以及 &lt;code&gt;hard&lt;/code&gt; 两种方式的退出处理。&lt;/li&gt;
&lt;li&gt;还有很多，一百个人心中有一百个哈姆莱特，同样一份代码，不同的人学习阅读，肯定收获不同，你可以在评论区留下你的感悟，跟看到这篇文章的人一起分享！&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="问题思考"&gt;问题思考&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;为了尽可能确保所有 Sidekiq 的任务能够正常主动退出，所以在部署脚本中，都会尽可能早地让 Sidekiq 进入 quiet 模式，但是 Sidekiq 的 quiet 是不可逆的，所以一旦部署脚本中途失败，Sidekiq 得不到重启，将会一直保持 quiet 状态，如果长时间未重启，任务就会积压。所以，一般我都会在部署脚本中，额外捕捉部署脚本失败异常，然后主动执行 sidekiq 的重启。&lt;strong&gt;如果你的部署脚本中有涉及 Sidekiq 的，一定要注意检查部署失败是否会影响 Sidekiq 的状态&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;虽然 Sidekiq 在强制退出当前长时间未退出的任务时，会将 job 的数据写回队列，等待重启后重新执行，那么这里就有个细节需要注意了，就是你的 job 必须是幂等的，否则就不能允许重新执行了。所以，请注意，&lt;strong&gt;如果你有需要长时间运行的 job，请注意检查其幂等性&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;好了，今天就写到这吧！仍然挺长一篇，啰嗦了。感谢看到这里！&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sun, 20 Nov 2016 13:26:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/31642</link>
      <guid>https://ruby-china.org/topics/31642</guid>
    </item>
    <item>
      <title>融云服务器端 API Ruby SDK</title>
      <description>&lt;h2 id="简介"&gt;简介&lt;/h2&gt;&lt;h3 id="这个 gem 是什么？"&gt;这个 gem 是什么？&lt;/h3&gt;
&lt;p&gt;这个 gem 是我利用业余时间开发完成的融云 IM 功能的 &lt;a href="http://www.rongcloud.cn/docs/server.html" rel="nofollow" target="_blank" title=""&gt;Server API&lt;/a&gt; 的 Ruby gem，实现了 Server API 的绝大部分接口（实时消息路由、消息历史记录以及在线状态订阅这三个付费服务接口未实现）的 Ruby 对接。这个 gem 已经在我自己的项目中正常运行，大家可以放心使用。&lt;/p&gt;
&lt;h3 id="安装"&gt;安装&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="s1"&gt;'rong_cloud_server'&lt;/span&gt;, &lt;span class="s1"&gt;'~&amp;gt; 0.0.2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码请看 &lt;a href="https://github.com/Martin91/rong_cloud" rel="nofollow" target="_blank" title=""&gt;martin91/rong_cloud&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="这个 gem 有什么特点？"&gt;这个 gem 有什么特点？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;轻量&lt;/strong&gt;，没有任何其他依赖，所有 HTTP 请求直接基于 net/http 实现，我可不想你用的时候发现我又替你的项目引入了一个不必要的 HTTP Client；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;简单直观&lt;/strong&gt;，所有方法名跟参数名基本参照 API 文档，不封装任何 DSL。因为我觉得作为一个 SDK，只需要做好最简单的请求封装就够了，使用者总免不了要去了解 API 本身细节，所以有必要尽可能保持 API 文档跟 SDK 代码的一致性。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果你在用的过程中，有其他需求或者问题，欢迎在 github 上给我提交 issue，我会仔细考虑提供更多开发。&lt;/p&gt;
&lt;h2 id="TL;DR"&gt;TL;DR&lt;/h2&gt;&lt;h3 id="源起"&gt;源起&lt;/h3&gt;
&lt;p&gt;之前的开发中需要融云 IM 的功能，但是在集成的过程中，都没有找到特别舒服的 gem，大部分的 gem 都已经太长时间没有维护了，很多也缺失测试，而官方的所谓 SDK 就不敢恭维了，与其说是一个 SDK，还不如说是一堆代码片段，根本没法开箱即用，大家先感受一下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;看看官方库的 issues
&lt;img src="https://l.ruby-china.com/photo/2016/f8bdc5b72261349f39c82a2a97b60831.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;其中一个 issue 的讨论
&lt;img src="https://l.ruby-china.com/photo/2016/f30b465e011d350ec014cc706ec0e471.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;文件命名
&lt;img src="https://l.ruby-china.com/photo/2016/4bcd87cddefba05b4f7e519d1500c798.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;额。。。
&lt;img src="https://l.ruby-china.com/photo/2016/1211e779d39d749c737530edab932f3b.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;后来使用的是 &lt;a href="https://github.com/FlowerWrong/rongcloud-ruby" rel="nofollow" target="_blank"&gt;https://github.com/FlowerWrong/rongcloud-ruby&lt;/a&gt; 这个 gem（不知道这个 FlowerWrong 在社区的 id，先表示一下真诚的感谢，因为从最开始的项目集成还有后来的自己的 gem 的开发，都从这个库里受益良多），但是用的过程中依然发现一些问题，比如在发系统消息的时候，由于融云要求数组型参数必须采用 &lt;code&gt;param=value1&amp;amp;param=value2&lt;/code&gt; 的方式，而 &lt;code&gt;rongcloud-ruby&lt;/code&gt; 使用的 rest_client 则会将数组参数自动转换为 &lt;code&gt;param[]=value1&amp;amp;param[]=value2&lt;/code&gt;的方式，导致融云服务器端返回参数错误警告。而且由于这个 ruby gem 久未更新，出错的地方也是没有测试覆盖的，就只能自己先临时 fork 并且修复。&lt;/p&gt;

&lt;p&gt;由于自己项目中还用了 httparty 作为 http client，加上 rongcloud-ruby 依赖的 rest client，就导致安装完，项目中有多个 http client，这点就真的好烦了，所以一心想着回头利用业余时间解决这个问题。&lt;/p&gt;

&lt;p&gt;本来计划是对 rongcloud-ruby 库进行测试补全跟重构，但是后来评估了这些工作量，觉得还不如重新写一个简洁一点的 gem 就好了，所以就放弃了对 fork 过来的 gem 的继续维护，对此也对 FlowerWrong 表示一下抱歉。&lt;/p&gt;

&lt;p&gt;然后，就是开头的那个 gem 的诞生了。。。。。。&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Thu, 03 Nov 2016 02:41:27 +0800</pubDate>
      <link>https://ruby-china.org/topics/31513</link>
      <guid>https://ruby-china.org/topics/31513</guid>
    </item>
    <item>
      <title>Sidekiq 任务调度流程分析</title>
      <description>&lt;p&gt;&lt;a href="http://sidekiq.org/" rel="nofollow" target="_blank" title=""&gt;sidekiq&lt;/a&gt;是 Ruby 中一个非常优秀而且可靠的后台任务处理软件，其依赖 Redis 实现队列任务的增加、重试以及调度等。而 sidekiq 从启动到开始不断处理任务、定时任务以及失败任务的重试，都是如何调度的呢？遇到问题的时候，又该如何调优呢？&lt;/p&gt;
&lt;h3 id="注意"&gt;注意&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;今天的分析所参考的 sidekiq 的源码对应版本是 4.2.3；&lt;/li&gt;
&lt;li&gt;今天所讨论的内容，将主要围绕任务调度过程进行分析，无关细节将不赘述，如有需要，请自行翻阅 sidekiq 源码；&lt;/li&gt;
&lt;li&gt;文章内容真的很长，请做好心理准备（借用下我在 SegmentFault 博客自动生成的目录树，话说好想给社区也实现一个，很贴心很方便）。
&lt;img src="https://l.ruby-china.com/photo/2016/0fc8f347a4c7cba67066b2527bdb8f94.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="你将了解到什么？"&gt;你将了解到什么？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;sidekiq 的任务调度机制：定时任务、重试任务的检查机制，队列任务的排队以及队列权重对处理优先级的影响；&lt;/li&gt;
&lt;li&gt;sidekiq 的中间件机制以及在此基础上实现的任务重试机制。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="先抛结论"&gt;先抛结论&lt;/h2&gt;&lt;h3 id="时序图"&gt;时序图&lt;/h3&gt;
&lt;p&gt;对于复杂的调用关系，我习惯用时序图帮助我理解其中各部分代码之间相互协作的关系（注意：为了避免太多细节造成阅读负担，我将参数传递以及返回值等冗杂过程去除了，只保留与任务调度相关的关键调用）：
&lt;img src="https://l.ruby-china.com/photo/2016/0856f92ffb106db71b9a5d6cfefcf2bf.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="人话"&gt;人话&lt;/h3&gt;
&lt;p&gt;Sidekiq 整个任务调度过程中依赖几个不同角色的代码共同协作，其分工如下：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/fe43bace416ba3bb7f7d77b397683bf4.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="源码之旅 —— 启动"&gt;源码之旅 —— 启动&lt;/h2&gt;
&lt;p&gt;当我们在执行 &lt;code&gt;sidekiq&lt;/code&gt; 时，源码中的 &lt;code&gt;bin/sidekiq.rb&lt;/code&gt; 文件便是第一个开始执行的文件，让我们看看&lt;a href="https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/bin/sidekiq#L9-L12" rel="nofollow" target="_blank" title=""&gt;里边的主要代码&lt;/a&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/bin/sidekiq#L9-L12&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;cli&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CLI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;
  &lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;
  &lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;===== 这边走&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紧靠 &lt;code&gt;begin&lt;/code&gt; 后边的两行代码首先创建 &lt;code&gt;Sidekiq::CLI&lt;/code&gt; 类的一个实例，接着调用实例方法 &lt;code&gt;#parse&lt;/code&gt; 解析 sidekiq 的配置参数，其中包括队列的配置、worker 数量的配置等，在此不展开了。接着实例方法 &lt;code&gt;#run&lt;/code&gt; 将带着我们继续往下走，让我们继续看 &lt;code&gt;lib/sidekiq/cli.rb&lt;/code&gt; 里边的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/cli.rb#L46-L106&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="c1"&gt;# 这里打印控制台欢迎信息、打印日志以及运行环境（不同 Rails 版本）加载等&lt;/span&gt;

  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'sidekiq/launcher'&lt;/span&gt;
  &lt;span class="vi"&gt;@launcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;===== 这边走&lt;/span&gt;

  &lt;span class="c1"&gt;# 进程接收到的信号处理以及退出处理&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码主要是实例化了一个 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 的对象，紧随其后又调用了实例方法 &lt;code&gt;#run&lt;/code&gt;，所以让我们继续顺藤摸瓜，看看 &lt;code&gt;Sidekiq::Launcher#run&lt;/code&gt; 方法到底做了哪些事情？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L24-L28&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:start_heartbeat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="vi"&gt;@poller.start&lt;/span&gt;
  &lt;span class="vi"&gt;@manager.start&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;#run&lt;/code&gt; 方法首先通过 &lt;code&gt;safe_thread&lt;/code&gt; 创建了一个新的线程，线程主要负责执行 &lt;code&gt;start_heartbeat&lt;/code&gt; 方法的代码，从方法名称上，我们猜测其主要是心跳代码，负责定时检查 sidekiq 健康状态，跟之前一样，这里不往下挖，我们继续看后边的两行代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@poller.start&lt;/span&gt;
&lt;span class="vi"&gt;@manager.start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;@poller&lt;/code&gt; 跟 &lt;code&gt;@manager&lt;/code&gt; 都是什么呢？让我们回头看一下，前面讲到 &lt;code&gt;lib/cli.rb&lt;/code&gt; 的 &lt;code&gt;#run&lt;/code&gt; 方法会负责创建 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 的实例，那让我们看下后者的 &lt;code&gt;initialize&lt;/code&gt; 方法定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/launcher.rb#L17-L22&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@poller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Scheduled&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Poller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="vi"&gt;@options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，实际上，&lt;code&gt;@manager&lt;/code&gt;是在创建 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 实例的过程中同步创建的 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例，同理，&lt;code&gt;@poller&lt;/code&gt; 是同步创建的 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt;的实例。那我们按照代码执行顺序，先看下 &lt;code&gt;@poller.start&lt;/code&gt; 也就是 &lt;code&gt;Sidekiq::Scheduled::Poller#start&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L63-L73&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scheduler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;initial_wait&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;enqueue&lt;/span&gt;
      &lt;span class="n"&gt;wait&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Scheduler exiting..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里看到，&lt;code&gt;#start&lt;/code&gt;方法也创建了一个线程，在线程里执行了两个部分代码：1. 初始化等待；2. 循环里的 &lt;code&gt;enqueue&lt;/code&gt; 与 &lt;code&gt;wait&lt;/code&gt;。这都是什么呢？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;: &lt;code&gt;#start&lt;/code&gt; 方法在线程创建完成后就立刻返回了，至于 &lt;code&gt;#start&lt;/code&gt; 方法里的逻辑，请移步后面章节“继续深挖 Sidekiq::Scheduled::Poller#start”作更深一步分析。这里，我们先继续接着看看 &lt;code&gt;#start&lt;/code&gt; 方法返回后接下来执行的 &lt;code&gt;@manager.start&lt;/code&gt; 方法又做了什么：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L45-L49&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@workers.each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;@workers&lt;/code&gt; 又是什么？一个数组？怎样的数组？我们回顾下，前面说在创建 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 实例的过程中同步创建了 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例，让我们就看看 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的 &lt;code&gt;#initialize&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L31-L43&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="vi"&gt;@options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
  &lt;span class="vi"&gt;@count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Concurrency of &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not supported"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="vi"&gt;@done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="vi"&gt;@workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@count.times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="vi"&gt;@workers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="vi"&gt;@plock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在创建了 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例之后，又同步创建了多个 &lt;code&gt;Sidekiq::Processor&lt;/code&gt; 的实例，实例的个数取决于 &lt;code&gt;options[:concurrency] || 25&lt;/code&gt;，也就是配置的 &lt;code&gt;:concurrency&lt;/code&gt; 的值，缺省值为 &lt;code&gt;25&lt;/code&gt;。至此，我们知道，sidekiq 中的 worker 的数量就是在此其作用的，&lt;code&gt;Sidekiq::Manager&lt;/code&gt; 按照配置的数量创建指定数量的 worker。
往回看刚才的 &lt;code&gt;#start&lt;/code&gt; 方法中：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/manager.rb#L46-L48&lt;/span&gt;
&lt;span class="vi"&gt;@workers.each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简言之，就是 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 在 &lt;code&gt;start&lt;/code&gt; 的时候只做一件事：分别调用其管理的所有 worker 的 &lt;code&gt;#start&lt;/code&gt; 方法，也就是 &lt;code&gt;Sidekiq::Processor#start&lt;/code&gt;。继续往下走：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L60-L62&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"processor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又是我们熟悉的 &lt;code&gt;safe_thread&lt;/code&gt; 方法，同样是创建了一个新的线程，意味着每一个 worker 都是基于自己的一个新线程的，而这个线程里执行的代码是私有方法 &lt;code&gt;#run&lt;/code&gt; 里的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L66-L77&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;process_one&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_stopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="vi"&gt;@mgr.processor_died&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，又是一个 while 循环！而这个循环体里只调用了一个 &lt;code&gt;#process_one&lt;/code&gt; 实例方法，顾名思义，这里是说每个 worker 在没被结束之前，都重复每次处理一个新的任务，那这个 &lt;code&gt;#process_one&lt;/code&gt; 里又做了什么呢？怎么决定该先做哪个任务呢？别急，请看后面章节“继续深挖 Sidekiq::Processor#process_one”。&lt;/p&gt;
&lt;h3 id="小结"&gt;小结&lt;/h3&gt;
&lt;p&gt;sidekiq 在启动后（此处可借文章开头的时序图辅助理解）：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;首先创建了 &lt;code&gt;Sidekiq::CLI&lt;/code&gt; 的实例，并调用其 &lt;code&gt;run&lt;/code&gt; 方法；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::CLI&lt;/code&gt; 的实例在 &lt;code&gt;#run&lt;/code&gt; 的过程中，创建了 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 的实例，并调用其 &lt;code&gt;run&lt;/code&gt; 方法；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 的实例在创建后，同步创建了一个 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 的实例以及 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例，而在其执行 &lt;code&gt;#run&lt;/code&gt; 的过程中，则分别调用了这两个实例的 &lt;code&gt;start&lt;/code&gt; 方法；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 的实例在执行 &lt;code&gt;start&lt;/code&gt; 过程中，创建了一个内部循环执行的线程，周而复始地执行 &lt;code&gt;enqueue&lt;/code&gt; -&amp;gt; &lt;code&gt;wait&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例在创建后，同步创建若干个指定的 worker，也就是 &lt;code&gt;Sidekiq::Processor&lt;/code&gt; 的实例，并在执行 &lt;code&gt;start&lt;/code&gt; 方法的过程中对每一个 worker 发起 &lt;code&gt;start&lt;/code&gt; 调用；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::Processor&lt;/code&gt; 实例在执行 &lt;code&gt;start&lt;/code&gt; 方法的过程中创建了一个新的线程，新的线程里同样有一个 &lt;code&gt;while&lt;/code&gt; 循环，反复执行 &lt;code&gt;process_one&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上就是 Sidekiq 的主要启动过程，以下分别针对 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 以及 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 展开源码分析。&lt;/p&gt;
&lt;h2 id="定时任务拉取器的工作 Sidekiq::Scheduled::Poller#start"&gt;定时任务拉取器的工作 Sidekiq::Scheduled::Poller#start&lt;/h2&gt;
&lt;p&gt;经过前面较表层的代码分析，我们接下来继续展开 &lt;code&gt;Sidekiq::Scheduled::Poller#start&lt;/code&gt; 方法的探索之旅，首先重温下其代码定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L63-L73&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;
  &lt;span class="vi"&gt;@thread&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;safe_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scheduler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;initial_wait&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="vi"&gt;@done&lt;/span&gt;
      &lt;span class="n"&gt;enqueue&lt;/span&gt;
      &lt;span class="n"&gt;wait&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Scheduler exiting..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;code&gt;#start&lt;/code&gt; 方法的核心就是中间的 &lt;code&gt;while&lt;/code&gt; 循环，在循环前面，执行了 &lt;code&gt;#initial_wait&lt;/code&gt; 方法，让我们先看看这个方法到底是干些什么的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L133-L143&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initial_wait&lt;/span&gt;
  &lt;span class="c1"&gt;# Have all processes sleep between 5-15 seconds.  10 seconds&lt;/span&gt;
  &lt;span class="c1"&gt;# to give time for the heartbeat to register (if the poll interval is going to be calculated by the number&lt;/span&gt;
  &lt;span class="c1"&gt;# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="no"&gt;INITIAL_WAIT&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:poll_interval_average&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="vi"&gt;@sleeper.pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合注释理解，原来私有方法 &lt;code&gt;#initial_wait&lt;/code&gt; 只是为了避免所有进程在后续逻辑中同时触发 Redis IO 而做的设计，如果对大型系统有过架构经验的童鞋就会明白，这里其实就是为了防止类似雪崩之类的系统故障出现。让当前进程随机等待一定范围的时间，从而就可以跟其他进程错开了。&lt;/p&gt;

&lt;p&gt;在理解完 &lt;code&gt;initial_wait&lt;/code&gt; 之后，我们接着看到循环体里的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L68-L69&lt;/span&gt;
&lt;span class="n"&gt;enqueue&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;enqueue&lt;/code&gt;？干嘛呢？为什么是入队列呢？带着疑问往下看：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L75-L86&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="vi"&gt;@enq.enqueue_jobs&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里看到 &lt;code&gt;#enqueue&lt;/code&gt; 代码非常简单，只是调用了实例变量 &lt;code&gt;@enq&lt;/code&gt; 的 &lt;code&gt;#enqueue_jobs&lt;/code&gt; 方法而已，那么，&lt;code&gt;@enq&lt;/code&gt; 是什么类型的实例呢？它的 &lt;code&gt;#enqueue_jobs&lt;/code&gt; 方法又做了什么呢？让我们回过头来看一遍 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 的 &lt;code&gt;#initialize&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L45-L50&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
  &lt;span class="vi"&gt;@enq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:scheduled_enq&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Scheduled&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Enq&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@sleeper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ConnectionPool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TimedStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来缺省情况下，&lt;code&gt;@enq&lt;/code&gt; 就是 &lt;code&gt;Sidekiq::Scheduled::Enq&lt;/code&gt; 的实例。而代码上看的话，sidekiq 支持用户通过 &lt;code&gt;:scheduled_enq&lt;/code&gt; 配置项自定义 &lt;code&gt;@enq&lt;/code&gt; 的类型，但是官方文档未对此参数提及以及说明，这里其实是一种策略模式的实现，用户自定义的类型必须实现 &lt;code&gt;enqueue_jobs&lt;/code&gt; 方法。我估计，是 sidekiq pro 里边才会用到的配置项吧。&lt;/p&gt;

&lt;p&gt;知道了 &lt;code&gt;@enq&lt;/code&gt; 的类型后，让我们继续看下 &lt;code&gt;Sidekiq::Scheduled::Enq#enqueue_jobs&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L11-L33&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sorted_sets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;SETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# A job's "score" in Redis is the time at which it should be processed.&lt;/span&gt;
  &lt;span class="c1"&gt;# Just check Redis for the set of jobs with a timestamp before now.&lt;/span&gt;
  &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;sorted_sets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="c1"&gt;# Get the next item in the queue if it's score (time to execute) is &amp;lt;= now.&lt;/span&gt;
      &lt;span class="c1"&gt;# We need to go through the list one at a time to reduce the risk of something&lt;/span&gt;
      &lt;span class="c1"&gt;# going wrong between the time jobs are popped from the scheduled queue and when&lt;/span&gt;
      &lt;span class="c1"&gt;# they are pushed onto a work queue and losing the jobs.&lt;/span&gt;
      &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zrangebyscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-inf'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:limit&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

        &lt;span class="c1"&gt;# Pop item off the queue and add it to the work queue. If the job can't be popped from&lt;/span&gt;
        &lt;span class="c1"&gt;# the queue, it's because another process already popped it so we can move on to the&lt;/span&gt;
        &lt;span class="c1"&gt;# next one.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zrem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"enqueued &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这里这个方法的寓意，通过代码里的注释都已经很明晰了，不过我觉得还是有几个点需要强调下。
首先，在无参数调用 &lt;code&gt;#enqueue_jobs&lt;/code&gt; 方法时，定义中的参数 &lt;code&gt;now&lt;/code&gt; 缺省为当前时间，而 &lt;code&gt;sorted_sets&lt;/code&gt; 缺省为 &lt;code&gt;Sidekiq::Scheduled::SETS&lt;/code&gt; 的值，其值定义为：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L8&lt;/span&gt;
&lt;span class="no"&gt;SETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(retry schedule)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是数组 &lt;code&gt;["retry", "schedule"]&lt;/code&gt;，而这两个队列名称所对应的队列就是 sidekiq 的重试以及定时任务队列，在 sidekiq 里边，重试任务以及定时任务本质上都是 scheduled jobs，这两个队列使用了特殊的 Redis 的数据结构，进入队列的任务以其执行时间作为数据的 score，写入 Redis 之后按照 score 排序，也就是按任务的计划时间排序。&lt;/p&gt;

&lt;p&gt;接着往下看：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L14-L30&lt;/span&gt;
&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;sorted_sets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zrangebyscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-inf'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:limit&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zrem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"enqueued &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sorted_set&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，sidekiq 分别针对 &lt;code&gt;"retry"&lt;/code&gt; 和 &lt;code&gt;"schedule"&lt;/code&gt; 队列做了一个循环，循环体里每次通过 Redis 的 &lt;a href="http://redis.io/commands/ZRANGEBYSCORE" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;ZRANGEBYSCORE&lt;/code&gt;&lt;/a&gt;命令取出一个计划时间小于等于当前时间的任务，并且调用 &lt;code&gt;Sidekiq::Client&lt;/code&gt; 的 &lt;code&gt;.push&lt;/code&gt; 方法将此任务加到指定队列中（job 中包含队列名称等信息，在此不展开，有兴趣的同学请自行阅读 &lt;code&gt;Sidekiq::Client&lt;/code&gt; 的代码）。&lt;/p&gt;

&lt;p&gt;至此，可以明白，&lt;code&gt;enqueue_jobs&lt;/code&gt; 就是分别从 &lt;code&gt;"retry"&lt;/code&gt; 和 &lt;code&gt;"schedule"&lt;/code&gt; 队列中取出已经到达计划时间的任务，将其一一加入原来队列。注意，定时任务以及重试任务的计划时间只是计划加进执行中队列的时间，并非执行时间，执行的时间就只能取决于队列的长度以及队列执行速度了。&lt;/p&gt;

&lt;p&gt;接着往回点，继续看 &lt;code&gt;enqueue_jobs&lt;/code&gt; 之后的 &lt;code&gt;wait&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L90-L100&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait&lt;/span&gt;
  &lt;span class="vi"&gt;@sleeper.pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random_poll_interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;
  &lt;span class="c1"&gt;# expected&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;wait&lt;/code&gt; 方法只是做一个休眠，休眠的实现依赖于 &lt;code&gt;@sleeper&lt;/code&gt; 的 &lt;code&gt;#pop&lt;/code&gt; 方法调用，回顾 &lt;code&gt;Sidekiq::Scheduled::Poller&lt;/code&gt; 的 &lt;code&gt;#initialize&lt;/code&gt; 方法的实现可以确认 &lt;code&gt;@sleeper&lt;/code&gt; 是 &lt;code&gt;ConnectionPool::TimedStack&lt;/code&gt; 的实例，而后者是 Ruby gem &lt;a href="https://github.com/mperham/connection_pool/blob/master/lib/connection_pool/timed_stack.rb" rel="nofollow" target="_blank" title=""&gt;connection_pool&lt;/a&gt; 里的实现，其 &lt;code&gt;pop&lt;/code&gt; 方法会阻塞当前代码的执行，直到有值返回或者到达指定的超时时间，这里 sidekiq 利用了其阻塞的特性，作为 &lt;code&gt;wait&lt;/code&gt; 方法休眠器的实现。&lt;/p&gt;

&lt;p&gt;而代码里的休眠时间则不是固定的，依赖 &lt;code&gt;#random_poll_interval&lt;/code&gt; 方法的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L103-L105&lt;/span&gt;
&lt;span class="c1"&gt;# Calculates a random interval that is ±50% the desired average.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;random_poll_interval&lt;/span&gt;
  &lt;span class="n"&gt;poll_interval_average&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;poll_interval_average&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实现依赖一个 &lt;code&gt;#poll_interval_average&lt;/code&gt; 方法的返回值，顾名思义，这个方法将决定定时任务定期检查的平均时间周期。让我们继续深挖下去：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L107-L122&lt;/span&gt;
&lt;span class="c1"&gt;# We do our best to tune the poll interval to the size of the active Sidekiq&lt;/span&gt;
&lt;span class="c1"&gt;# cluster.  If you have 30 processes and poll every 15 seconds, that means one&lt;/span&gt;
&lt;span class="c1"&gt;# Sidekiq is checking Redis every 0.5 seconds - way too often for most people&lt;/span&gt;
&lt;span class="c1"&gt;# and really bad if the retry or scheduled sets are large.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Instead try to avoid polling more than once every 15 seconds.  If you have&lt;/span&gt;
&lt;span class="c1"&gt;# 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds.&lt;/span&gt;
&lt;span class="c1"&gt;# To keep things statistically random, we'll sleep a random amount between&lt;/span&gt;
&lt;span class="c1"&gt;# 225 and 675 seconds for each poll or 450 seconds on average.  Otherwise restarting&lt;/span&gt;
&lt;span class="c1"&gt;# all your Sidekiq processes at the same time will lead to them all polling at&lt;/span&gt;
&lt;span class="c1"&gt;# the same time: the thundering herd problem.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# We only do this if poll_interval_average is unset (the default).&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_interval_average&lt;/span&gt;
  &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:poll_interval_average&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;scaled_poll_interval&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法的重要性通过其几倍于代码的注释就可以看出来，大概意思是，sidekiq 为了避免在进程重启后，有大量的进程同时密集地访问 redis，所以设计了这个机制，就是每个进程对定时任务的检查都是按照一个公式来计算的，保证每个进程两次检查之间的平均休眠时间能够在一个范围内动态变化，从而将所有进程的 Redis IO 均匀错开。
从代码上看，sidekiq 的这个平均拉取时间支持配置项配置，但是目前也并没有在 wiki 上有所提及。而缺省情况下，其值由方法 &lt;code&gt;#scaled_poll_interval&lt;/code&gt; 决定：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/scheduled.rb#L124-L131&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scaled_poll_interval&lt;/span&gt;
  &lt;span class="n"&gt;pcount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProcessSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;
  &lt;span class="n"&gt;pcount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pcount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="n"&gt;pcount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:average_scheduled_poll_interval&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正如前面一段代码的注释所说，缺省情况下，sidekiq 认为定时任务拉取器的平均休眠时间正是：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sidekiq 进程数量 x 平均拉取时间 average_scheduled_poll_interval
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 &lt;code&gt;:average_scheduled_poll_interval&lt;/code&gt; 的缺省配置是 15 秒：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/master/lib/sidekiq.rb#L25&lt;/span&gt;
&lt;span class="no"&gt;DEFAULTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="ss"&gt;average_scheduled_poll_interval: &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以回过头来，在没有相关自定义配置的情况下，假设你只开启了一个 sidekiq 进程，那么 sidekiq 的定时任务拉取器的拉取时间平均间隔为 1 x 15 = 15 秒，那按照上面的 &lt;code&gt;#random_poll_interval&lt;/code&gt; 方法的定义，则实际每次拉取的时间间隔则是在 7.5 秒到 22.5 秒之间！&lt;/p&gt;
&lt;h3 id="小结"&gt;小结&lt;/h3&gt;
&lt;p&gt;从这个章节的分析，我们可以明白 Sidekiq 对定时任务和重试任务是一视同仁的，其处理流程都是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;所有定时任务（包括重试任务，本质上重试任务也是定时的，后边会单独讲解）以其计划时间为 score，加入特殊的 &lt;code&gt;"retry"&lt;/code&gt; 或 &lt;code&gt;"schedule"&lt;/code&gt; 有序队列中；&lt;/li&gt;
&lt;li&gt;sidekiq 的定时任务拉取器从 &lt;code&gt;"retry"&lt;/code&gt; 和 &lt;code&gt;"schedule"&lt;/code&gt; 队列中一一取出已到达计划时间的任务，将其加入该任务计划的队列中，后续的执行则跟其他普通队列中的任务一致；&lt;/li&gt;
&lt;li&gt;拉取器休眠一定时间（&lt;code&gt;random_poll_interval&lt;/code&gt;）后，从步骤 2 重新开始，周而复始。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以，定时任务的计划时间不是确切的任务时间！只是允许加回队列的时间，具体执行时间还得另外看队列长度以及队列处理速度！&lt;/p&gt;
&lt;h2 id="Sidekie worker 的秘密： Sidekiq::Processor#process_one"&gt;Sidekie worker 的秘密：Sidekiq::Processor#process_one&lt;/h2&gt;
&lt;p&gt;前面我们分析过 sidekiq 的 worker 的核心代码就是在线程里循环执行 &lt;code&gt;#process_one&lt;/code&gt; 方法，那么这个方法到底做了些什么啊？别急，现在就来一探究竟：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L79-L83&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_one&lt;/span&gt;
  &lt;span class="vi"&gt;@job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@job&lt;/span&gt;
  &lt;span class="vi"&gt;@job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中，&lt;code&gt;#process_one&lt;/code&gt; 先通过 &lt;code&gt;#fetch&lt;/code&gt; 方法获取一个任务，当任务获取成功后，就将其作为参数调用 &lt;code&gt;#process&lt;/code&gt; 方法，完成对任务的处理；如果没有获取到任务，则直接重新尝试获取新的任务。&lt;/p&gt;

&lt;p&gt;首先让我们看看 &lt;code&gt;#fetch&lt;/code&gt; 方法的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L96-L104&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;
  &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_one&lt;/span&gt;         &lt;span class="c1"&gt;# 吐槽一下这个 `j` 变量，命名真的不敢恭维，这个库就这里写得不雅&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="vi"&gt;@done&lt;/span&gt;
    &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requeue&lt;/span&gt;
    &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;j&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;#fetch&lt;/code&gt; 方法通过 &lt;code&gt;#get_one&lt;/code&gt; 方法从队列中获取任务，当获取到任务后，判断当前 worker 是否已经停止 (&lt;code&gt;@done&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;)，是则将任务重新压回队列。&lt;/p&gt;

&lt;p&gt;让我们接着看 &lt;code&gt;#get_one&lt;/code&gt; 方法的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L85-L94&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_one&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@strategy.retrieve_work&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Redis is online, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="vi"&gt;@down&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; sec downtime"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="vi"&gt;@down&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@down&lt;/span&gt;
    &lt;span class="n"&gt;work&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="n"&gt;handle_fetch_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码则是 &lt;code&gt;work = @strategy.retrieve_work&lt;/code&gt;，为了了解 &lt;code&gt;@strategy&lt;/code&gt;，我们仍旧往回看&lt;code&gt;#initialize&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L32-L40&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="vi"&gt;@strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fetch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BasicFetch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又是一个策略模式，缺省下，使用了 &lt;code&gt;Sidekiq::BasicFetch&lt;/code&gt; 生成实例，并且通过实例变量 &lt;code&gt;@strategy&lt;/code&gt; 引用。&lt;/p&gt;

&lt;p&gt;回到前面的 &lt;code&gt;@strategy.retrieve_work&lt;/code&gt;，让我们继续看看 &lt;code&gt;Sidekiq::BasicFetch#retrieve_work&lt;/code&gt; 的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/fetch.rb#L35-L38&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_work&lt;/span&gt;
  &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brpop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;queues_cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="no"&gt;UnitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码，可以知道 &lt;code&gt;Sidekiq::BasicFetch&lt;/code&gt; 的取任务逻辑比较直接，是通过 Redis 的 &lt;a href="http://redis.io/commands/brpop" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;BRPOP&lt;/code&gt; 命令&lt;/a&gt;从“所有队列”中阻塞地取出第一个任务：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BRPOP is a blocking list pop primitive. It is the blocking version of RPOP because it blocks the connection when there are no elements to pop from any of the given lists. An element is popped from the tail of the first list that is non-empty, with the given keys being checked in the order that they are given.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以，理解了 &lt;code&gt;BRPOP&lt;/code&gt; 命令的工作细节之后，我们把注意力缩放到 &lt;code&gt;#queues_cmd&lt;/code&gt; 方法上：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/fetch.rb#L40-L53&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;queues_cmd&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@strictly_ordered_queues&lt;/span&gt;
    &lt;span class="vi"&gt;@queues&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@queues.shuffle.uniq&lt;/span&gt;
    &lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;TIMEOUT&lt;/span&gt;
    &lt;span class="n"&gt;queues&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，代码中检查了 &lt;code&gt;@strictly_ordered_queues&lt;/code&gt; 这个实例变量的值，让我们回头看下这个变量的值的来源，也就是 &lt;code&gt;#initialize&lt;/code&gt; 方法的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/d8f11c26518dbe967880f76fd23bb99e9d2411d5/lib/sidekiq/fetch.rb#L26-L33&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@strictly_ordered_queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:strict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="vi"&gt;@queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:queues&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"queue:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@strictly_ordered_queues&lt;/span&gt;
      &lt;span class="vi"&gt;@queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@queues.uniq&lt;/span&gt;
      &lt;span class="vi"&gt;@queues&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;TIMEOUT&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺省情况下，此值为 &lt;code&gt;false&lt;/code&gt;。所以让我们看 &lt;code&gt;#queues_cmd&lt;/code&gt; 方法的 &lt;code&gt;else&lt;/code&gt; 分支里的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@queues.shuffle.uniq&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这里的 &lt;code&gt;@queues&lt;/code&gt; 就是来自 &lt;code&gt;options[:queues]&lt;/code&gt; 中的配置： &lt;code&gt;options[:queues].map { |q| "queue:#{q}" }&lt;/code&gt;。那么，这个 &lt;code&gt;options[:queues]&lt;/code&gt; 的值又是什么呢？
让我们一步一步沿着调用链上参数往回走：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Sidekiq::BasicFetch.new&lt;/code&gt; 的参数 &lt;code&gt;options&lt;/code&gt; 来自 worker 在 &lt;code&gt;Sidekiq::Processor#initialize&lt;/code&gt; 方法中的参数 &lt;code&gt;mgr&lt;/code&gt; 的 &lt;code&gt;options&lt;/code&gt; 属性；&lt;/li&gt;
&lt;li&gt;worker 的 mgr 参数正是 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 的实例，其 &lt;code&gt;options&lt;/code&gt; 属性则是 &lt;code&gt;Sidekiq::Launcher&lt;/code&gt; 创建 &lt;code&gt;Sidekiq::Manager&lt;/code&gt; 实例时传入的 &lt;code&gt;options&lt;/code&gt; 变量；&lt;/li&gt;
&lt;li&gt;而 &lt;code&gt;Sidekiq::Launcher#initialize&lt;/code&gt; 接收到的 &lt;code&gt;options&lt;/code&gt; 变量则是更外层的 &lt;code&gt;Sidekiq::CLI&lt;/code&gt; 的实例方法 &lt;code&gt;options&lt;/code&gt; 的值；&lt;/li&gt;
&lt;li&gt;而 &lt;code&gt;Sidekiq::CLI&lt;/code&gt; 的实例的 &lt;code&gt;options&lt;/code&gt; 则是在其接收到 &lt;code&gt;#parse&lt;/code&gt; 调用时设置的。
为了节省篇幅，省略这里其中的太多调用栈，我们直接看最根源代码：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/cli.rb#L389-L399&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_queues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queues_and_weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;queues_and_weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;queue_and_weight&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;parse_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;queue_and_weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:queues&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:strict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，sidekiq 在解析 &lt;code&gt;:queues&lt;/code&gt; 的相关配置时，按照每个队列以及其权重，生成了一个重复次数等于队列权重的队列的新数组，假设用户提供如下配置：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;:queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;myqueue&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则此处生成的 &lt;code&gt;options[:queues]&lt;/code&gt; 则为 &lt;code&gt;["default", "myqueue", "myqueue"]&lt;/code&gt;。所以，这里权重主要用于后边确定各个不同队列被处理到的优先权的比重。&lt;/p&gt;

&lt;p&gt;了解了 &lt;code&gt;@queues&lt;/code&gt; 的来源之后，我们回到最开始讨论的地方：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@queues.shuffle.uniq&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，每次 worker 在请求新的任务时，sidekiq 都按照原来的 &lt;code&gt;@queues&lt;/code&gt; 执行 &lt;code&gt;shuffle&lt;/code&gt; 方法，而 &lt;code&gt;shuffle&lt;/code&gt; 则表示将数组元素重新随机排序，亦即“洗牌”。结合前面的权重，那么每个队列洗牌后排在第一位的概率与其权重挂钩。最后的 &lt;code&gt;#uniq&lt;/code&gt; 方法确保队列名称没有重复，避免 Redis 在执行 &lt;code&gt;BRPOP&lt;/code&gt; 命令时重复检查同一队列。这里使用 &lt;code&gt;BRPOP&lt;/code&gt; 还有个好处就是，加入当前面优先的队列里边没有任务时，可以依次将机会让给后面的队列。&lt;/p&gt;

&lt;p&gt;而后边的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;TIMEOUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则是在命令末尾追加超时设定，即 Redis 的 &lt;code&gt;BRPOP&lt;/code&gt; 命令最多阻塞 2 秒，超时则直接放弃。&lt;/p&gt;

&lt;p&gt;了解了任务的获取之后，我们接着看 sidekiq 如何处理获取到的任务，回到 &lt;code&gt;retrieve_work&lt;/code&gt; 的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/fetch.rb#L36-L37&lt;/span&gt;
&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brpop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;queues_cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;UnitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到在获取到任务之后，任务通过 &lt;code&gt;Sidekiq::BasicFetch::UnitOfWork&lt;/code&gt; 结构体实例化后返回给调用方。&lt;/p&gt;

&lt;p&gt;直接回到 &lt;code&gt;Sidekiq::Processor#process_one&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L79-L83&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_one&lt;/span&gt;
  &lt;span class="vi"&gt;@job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@job&lt;/span&gt;
  &lt;span class="vi"&gt;@job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以明白，&lt;a href="/job" class="user-mention" title="@job"&gt;&lt;i&gt;@&lt;/i&gt;job&lt;/a&gt; 就是返回的 &lt;code&gt;UnitOfWork&lt;/code&gt; 实例，那么 &lt;code&gt;process(@job)&lt;/code&gt; 会做些什么呢？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L118-L152&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;jobstr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;job&lt;/span&gt;
  &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_name&lt;/span&gt;

  &lt;span class="vi"&gt;@reloader.call&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;ack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;begin&lt;/span&gt;
      &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobstr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;klass&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;constantize&lt;/span&gt;
      &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
      &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jid'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

      &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="c1"&gt;# Only ack if we either attempted to start this job or&lt;/span&gt;
          &lt;span class="c1"&gt;# successfully completed it. This prevents us from&lt;/span&gt;
          &lt;span class="c1"&gt;# losing jobs if a middleware raises an exception before yielding&lt;/span&gt;
          &lt;span class="n"&gt;ack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
          &lt;span class="n"&gt;execute_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cloned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'args'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，sidekiq 从 &lt;code&gt;work&lt;/code&gt; 中获取任务的相关信息，包括队列名称，任务对应的类型（&lt;code&gt;job['class'.freeze]&lt;/code&gt;）、任务调用所需的参数等，根据这些信息重新实例化任务对象，并且将实例化的任务对象 &lt;code&gt;worker&lt;/code&gt; 以及任务参数都传递给对 &lt;code&gt;execute_job&lt;/code&gt; 的调用。让我们看看 &lt;code&gt;#execute_job&lt;/code&gt; 的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L154-L156&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cloned_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cloned_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到了吧？我们最熟悉的 &lt;code&gt;#perform&lt;/code&gt; 方法！这下知道我们为什么需要在每个 sidekiq Worker 或者 ActiveJob 的 Job 类中定义这个方法了吧？因为这个方法就是最终任务执行时所需调用的方法，这就是约定！&lt;/p&gt;

&lt;p&gt;至此，任务的调度过程就到此为止了，剩下的就是周而复始的重复了。&lt;/p&gt;
&lt;h3 id="小结"&gt;小结&lt;/h3&gt;
&lt;p&gt;经过上面的分析，我们可以明白 sidekiq 中 worker 的工作流程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;按照所有队列以及其权重，每次重新排列待处理队列顺序，高权重的队列有更高的优先级；&lt;/li&gt;
&lt;li&gt;将重新排好的队列顺序传递给 Redis 的 BRPOP 命令，同时设置 2 秒超时；&lt;/li&gt;
&lt;li&gt;sidekiq 将从队列中获取到的任务实例化，并且根据携带的参数调用了任务的 &lt;code&gt;#perform&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;等等，上面都只是正常流程，那如果任务执行过程中出错了怎么办？？？重试的机制是如何运转的呢？&lt;/p&gt;
&lt;h2 id="重试机制：基于中间件的实现"&gt;重试机制：基于中间件的实现&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;阅读本章节前，建议先阅读官方 Wiki 的 &lt;a href="https://github.com/mperham/sidekiq/wiki/Error-Handling" rel="nofollow" target="_blank" title=""&gt;Error Handling&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;细心的童鞋肯定发现了上面的 &lt;code&gt;Sidekiq::Processor#process&lt;/code&gt; 方法中有个关键的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/processor.rb#L131-L137&lt;/span&gt;
&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;execute_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cloned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'args'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;server_middleware&lt;/code&gt; 是什么呢？让我们来简单过一下吧：&lt;/p&gt;

&lt;p&gt;全局搜索了代码，发现 &lt;code&gt;Sidekiq.server_middleware&lt;/code&gt; 的来源是：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq.rb#L140-L144&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_middleware&lt;/span&gt;
  &lt;span class="vi"&gt;@server_chain&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;default_server_middleware&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="vi"&gt;@server_chain&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
  &lt;span class="vi"&gt;@server_chain&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺省情况下，&lt;code&gt;.server_middleware&lt;/code&gt; 依赖 &lt;code&gt;.default_server_middleware&lt;/code&gt; 的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq.rb#L146-L154&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_server_middleware&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;

  &lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt; &lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Logging&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt; &lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RetryJobs&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以明白 &lt;code&gt;Sidekiq.default_server_middleware&lt;/code&gt; 返回一个 &lt;code&gt;Middleware::Chain&lt;/code&gt; 实例，并且调用了其 &lt;code&gt;#add&lt;/code&gt; 方法将 &lt;code&gt;Middleware::Server::Logging&lt;/code&gt; 以及 &lt;code&gt;Middleware::Server::RetryJobs&lt;/code&gt; 两个中间件加到中间件的 Chain 上。此中间件的实现以及实现类似 rackup，有兴趣的童鞋自行&lt;a href="https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/middleware/chain.rb" rel="nofollow" target="_blank" title=""&gt;阅读源码&lt;/a&gt;，在此不展开，让我们直接跳到 &lt;code&gt;Middleware::Server::RetryJobs&lt;/code&gt; 的 &lt;code&gt;call&lt;/code&gt; 方法中：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/middleware/server/retry_jobs.rb#L73-L84&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt;
  &lt;span class="c1"&gt;# ignore, will be pushed back onto queue during hard_shutdown&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="c1"&gt;# ignore, will be pushed back onto queue during hard_shutdown&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Shutdown&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception_caused_by_shutdown?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;attempt_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们聚焦方法的最后一行代码 &lt;code&gt;attempt_retry(worker, msg, queue, e)&lt;/code&gt;，此处表示当执行中的任务出现异常时，除去停机的因素以及禁用了重试机制后，尝试进行下次重试运行：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/middleware/server/retry_jobs.rb#L88-L137&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attempt_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;max_retry_attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retry_attempts_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="vi"&gt;@max_retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry_count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retried_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry_count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'failed_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry_count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_retry_attempts&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delay_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Failure! Retry &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'retry'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;# Goodbye dear message, you (re)tried your best I'm sure.&lt;/span&gt;
    &lt;span class="n"&gt;retries_exhausted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码中看出，sidekiq 在捕捉到异常后，首先检查此任务此前是否已经重试过，是的话，则在重试累计次数上加 1，更新最后重试时间；否则初始化重试累计次数为 0，设定初次失败时间。接着，sidekiq 检查重试累计次数是否超过限定最大重试次数，是的话则放弃重试，任务从此不再重试，进入 Dead 状态，sidekiq 抛出异常；否则计算任务下次重试时间，将任务按照计划的下次重试时间加到 &lt;code&gt;retry&lt;/code&gt; 有序队列中，最后抛出异常。关于重试任务的检查跟执行，请阅读前面的相关章节，接下来我们主要分析 sidekiq 如何计算任务的下次重试时间 &lt;code&gt;delay&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;让我们展开对 &lt;code&gt;#delay_for&lt;/code&gt; 方法的探索：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/middleware/server/retry_jobs.rb#L172-L174&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delay_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sidekiq_retry_in_block?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;retry_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seconds_to_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先了解下 &lt;code&gt;worker.sidekiq_retry_in_block?&lt;/code&gt; 的定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/worker.rb#L32&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class_attribute&lt;/span&gt; &lt;span class="ss"&gt;:sidekiq_retry_in_block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其定义了每个 Worker 类的 &lt;code&gt;sidekiq_retry_in_block&lt;/code&gt; 属性，而其又可以通过 Worker 类的 &lt;code&gt;#sidekiq_retry_in&lt;/code&gt; 方法完成赋值：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/worker.rb#L96-L98&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sidekiq_retry_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sidekiq_retry_in_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回过头来，前面的&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sidekiq_retry_in_block?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;retry_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seconds_to_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示当具体的 Worker 配置了 &lt;code&gt;:sidekiq_retry_in_block&lt;/code&gt; 时，则直接使用这个配置的 block 执行的值作为失败任务下次重试的时间间隔；否则使用缺省的计算公式：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/mperham/sidekiq/blob/5ebd857e3020d55f5c701037c2d7bedf9a18e897/lib/sidekiq/middleware/server/retry_jobs.rb#L177-L179&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seconds_to_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;count&lt;/code&gt; 为任务累计重试次数，从公式上看，随着失败重试次数的累计增加，任务的下次重试时间间隔也会指数式增长，按照官方文档说法：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sidekiq will retry failures with an exponential backoff using the formula (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1)) (i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time). It will perform 25 retries over approximately 21 days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;更多失败任务重试的相关配置请看文档：&lt;a href="https://github.com/mperham/sidekiq/wiki/Error-Handling#configuration" rel="nofollow" target="_blank" title=""&gt;Error Handling: Configuration&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="小结"&gt;小结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;sidekiq 在执行任务时，通过自行实现的中间件架构以及对应的简单的中间件，及时捕捉失败的任务，针对允许再次重试的任务，按失败次数计算新的重试时间，缺省为指数增长的时间间隔；&lt;/li&gt;
&lt;li&gt;用户可以通过配置修改缺省的公式，也可以指定最大重试次数等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：结合失败任务捕捉处理以及重试任务的检查，缺省情况下，一个首次失败任务下次重回队列（不是执行）的理论最大时间间隔大概是 67.5 秒！（固定的 15 秒 + 最大随机时间 30 秒 + 最大理论检查时间 22.5 秒）。所以，如果你的任务很重要，又需要尽快重试，就需要对几部分时间的相关配置参数进行调优了哦！在我自己的工作中，我针对某个队列任务设置的 &lt;code&gt;sidekiq_retry_in&lt;/code&gt; 公式为线性时间，即 1s、2s、...50s，然后在重试检查那里设置了 &lt;code&gt;:poll_interval_average&lt;/code&gt; 为 5 秒，新的下次执行时间理论最大时间间隔就是 8.5 秒！不过这些配置需要慎重调整，综合考虑业务以及业务量，既要尽可能保证任务尽早处理完，又得保证 Redis 没被 IO 压垮。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;&lt;h3 id="关于 sidekiq 项目代码"&gt;关于 sidekiq 项目代码&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;sidekiq 的源码比较简洁，很少看到长方法定义，大部分方法都在几行之内，读的过程中非常舒服；&lt;/li&gt;
&lt;li&gt;sidekiq 的注释也很充足，比较重要又比较核心的代码都有大量详细的注释跟例子，除此之外大部分重点在 Wiki 中都有提及，非常好的一份代码库；&lt;/li&gt;
&lt;li&gt;sidekiq 将 Redis 的各种数据结构用得都恰到好处，可以通过 sidekiq 加深对 Redis 的印象以及学习到如何恰当高效地结合 Redis 实现业务逻辑；&lt;/li&gt;
&lt;li&gt;正是因为 sidekiq 将 Redis 充分利用以及高度结合，我终于理解 sidekiq 的作者为什么表示 sidekiq 不考虑其他数据库了；&lt;/li&gt;
&lt;li&gt;sidekiq 的代码没有太多花俏的代码，非常推荐各位童鞋仔细研读。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="关于源码阅读"&gt;关于源码阅读&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;带着问题去阅读，效率通常很高；&lt;/li&gt;
&lt;li&gt;读的过程中适当放弃无关细节，只追击与问题相关的线索；&lt;/li&gt;
&lt;li&gt;有些文档中没有提及的配置项，往往都藏匿在代码之中；&lt;/li&gt;
&lt;li&gt;只有充分了解了工具的运行机制，在遇到问题调优的时候才能得心应手。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="最后"&gt;最后&lt;/h3&gt;
&lt;p&gt;如果你能从头看到这里，那么非常感谢你的时间，毕竟这篇文章确实不短，尽管我已经尽量去除无用的部分，一些代码也直接跳过了，但是系统得了解一个框架或者一个软件，确实也是很多细节。&lt;/p&gt;

&lt;p&gt;这是今年第二篇博客，今年的产出远不比去年，然而去年的产出远不比千年，所以，可能这篇也是今年最后一篇了。洋洋洒洒两三万字，从下午两三点写到现在，七个多小时，难得可以静下心来写这么多，哎，这两年心态太浮躁，技术路上，还是继续保持“stay foolish, stay hungry”的好。&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sat, 29 Oct 2016 22:41:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/31470</link>
      <guid>https://ruby-china.org/topics/31470</guid>
    </item>
    <item>
      <title>一起来发现这次 RubyConf 大会的最萌细节吧~~~</title>
      <description>&lt;p&gt;作为国内 Ruby 工程师一年一度的网友见面会，今年的设计风格极尽萌的路线，处处细节都是把人萌得不要不要的，用句话说，嗯，这很成都！！！&lt;/p&gt;
&lt;h4 id="熊猫排排坐之主人去吃饭，我为主人看着座："&gt;熊猫排排坐之主人去吃饭，我为主人看着座：&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/157dab73f82764f38fdff1e56543ce33.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;另外感叹下，弹指一挥间，一年又过……&lt;/p&gt;

&lt;p&gt;先放几张昨晚星巴克的图！
&lt;img src="https://l.ruby-china.com/photo/2016/98a2e5b3cc44fc5f841c290199f73a49.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2016/b7094791959ac3a0b96ccce4358ef94b.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2016/df7e48cefc30da885cd4db8932742677.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2016/7bcf7c7c8d9966b7b32b72eec4523d43.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2016/cd28bf1b2c92996ffd64741f2897a592.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2016/ef97f0d813a4f9a00c2d12ef3167c28b.jpg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Fri, 23 Sep 2016 23:48:15 +0800</pubDate>
      <link>https://ruby-china.org/topics/31154</link>
      <guid>https://ruby-china.org/topics/31154</guid>
    </item>
    <item>
      <title>这个城市名是数据错误了吗？</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/703bb0245d0301a6d9d7d084146f2ca8.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sun, 04 Sep 2016 22:16:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/30984</link>
      <guid>https://ruby-china.org/topics/30984</guid>
    </item>
    <item>
      <title>哈哈，这里需要考虑妹子们的感受吗？</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/afc1c36fec3f66a1f377c8de42194db5.png" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sat, 21 May 2016 11:23:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/30081</link>
      <guid>https://ruby-china.org/topics/30081</guid>
    </item>
    <item>
      <title>嘿，小心你的双等号==</title>
      <description>&lt;p&gt;前两天在写代码的时候，突然收到警告说项目代码中存在 XSS 漏洞，遂立即根据报告的 URL 排查页面代码，虽然很快就修复了，而且同样问题的讨论两年前就有了，看&lt;a href="https://ruby-china.org/topics/16633" title=""&gt;《别用 raw 和 html_safe》&lt;/a&gt;，一般来说相对有经验的同学也应该都知道这个点，但是还是觉得有必要写出来，再次提醒一下其他小伙伴，避免踩坑。&lt;/p&gt;
&lt;h2 id="问题根源"&gt;问题根源&lt;/h2&gt;
&lt;p&gt;其中，在找到的漏洞出现的地方，都存在类似以下这样的 slim 代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题就出在双等号 &lt;code&gt;==&lt;/code&gt; 上，因为在 &lt;strong&gt;slim&lt;/strong&gt; 跟 &lt;strong&gt;ERB&lt;/strong&gt; 模板（其他模板比如 HAML 之类的就不清楚了）中，双等号其实是 Rails 的 &lt;code&gt;raw&lt;/code&gt; 这个 helper 方法的缩写，&lt;a href="http://edgeguides.rubyonrails.org/active_support_core_extensions.html#output-safety" rel="nofollow" target="_blank" title=""&gt;参考链接&lt;/a&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To insert something verbatim use the raw helper rather than calling html_safe:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;
    &amp;lt;%= raw @cms.current_template %&amp;gt; &amp;lt;%# inserts @cms.current_template as is %&amp;gt;
&lt;/code&gt;
or, equivalently, use &lt;code&gt;&amp;lt;%==&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;
    &amp;lt;%== @cms.current_template %&amp;gt; &amp;lt;%# inserts @cms.current_template as is %&amp;gt;
&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;也就是说上面的代码等同于：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;raw&lt;/code&gt; 方法在 &lt;a href="http://api.rubyonrails.org/classes/ActionView/Helpers/OutputSafetyHelper.html#method-i-raw" rel="nofollow" target="_blank" title=""&gt;Rails 文档&lt;/a&gt;中的解释是这样子的：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This method outputs without escaping a string. Since escaping tags is now default, this can be used when you don't want Rails to automatically escape tags. This is not recommended if the data is coming from the user's input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;大概意思就是，这个方法将会跳过对传入的字符串进行标签过滤以及其他处理，直接将字符串输出到 HTML 中。&lt;br&gt;
所以到现在原因就很清晰了，因为不小心在代码里多加了一个等号，变成了双等号，导致将会直接把用户的输入输出到待渲染的 HTML 中，在不自知的情况下留下了 XSS 漏洞。于是乎，修复方案仅需去掉一个等号即可：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，Rails 就能继续自动过滤输入的 &lt;code&gt;:account&lt;/code&gt; 的参数并且自动过滤恶意内容了。&lt;/p&gt;
&lt;h2 id="raw、String#html_safe 以及 &lt;%== %&gt;"&gt;raw、String#html_safe 以及 &amp;lt;%== %&amp;gt;&lt;/h2&gt;
&lt;p&gt;在查看 &lt;code&gt;raw&lt;/code&gt; 方法的文档时，顺便看了其源码，极其简单，只有一行：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File actionview/lib/action_view/helpers/output_safety_helper.rb, line 16&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stringish&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;stringish&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;raw&lt;/code&gt; 只是先确保将 &lt;code&gt;stringish&lt;/code&gt; 参数转化为字符串，然后调用了 &lt;a href="http://api.rubyonrails.org/classes/String.html#method-i-html_safe" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;String#html_safe&lt;/code&gt;&lt;/a&gt; 方法而已。而且在 &lt;code&gt;String#html_safe&lt;/code&gt; 的文档中，同样反复强调慎重使用这两个方法：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It will be inserted into HTML with no additional escaping performed. It is your responsibilty to ensure that the string contains no malicious content. This method is equivalent to the &lt;code&gt;raw&lt;/code&gt; helper in views.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以，可以总结一下，以下三种写法的代码都是等价的，都是不安全的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'xxx'&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那在切实需要输出包含 HTML 内容比如富文本编辑器编辑的内容时，如何保证安全？&lt;br&gt;
方案很简单，只需要使用文档中推荐的 &lt;a href="http://api.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html#method-i-sanitize" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;sanitize&lt;/code&gt;&lt;/a&gt; helper 方法：&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;It is recommended that you use &lt;code&gt;sanitize&lt;/code&gt; instead of this method(html_safe).&lt;br&gt;
&lt;/li&gt;
&lt;li&gt;(#sanitize)Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;或者使用一些其他第三方的 gem 用来做过滤处理。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;不要使用双等号缩写的方式，以避免其他人（比如项目里的 Rails 新手）在不了解的情况下照着滥用；&lt;/li&gt;
&lt;li&gt;尽可能不用 &lt;code&gt;raw&lt;/code&gt; helper 或者 &lt;code&gt;String#html_safe&lt;/code&gt; 方法，尽可能使用 &lt;code&gt;#sanitize&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;多借助工具进行自动扫描，比如 &lt;a href="http://brakemanscanner.org/" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;brakeman&lt;/code&gt;&lt;/a&gt;，能够快速高效检测出包括 XSS 漏洞在内的多种安全隐患。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="参考链接"&gt;参考链接&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://ruby-china.org/topics/16633" title=""&gt;别用 raw 和 html_safe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://brakemanscanner.org/" rel="nofollow" target="_blank" title=""&gt;BrakemanScanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://guides.rubyonrails.org/active_support_core_extensions.html#safe-strings" rel="nofollow" target="_blank" title=""&gt;Rails Guides: Safe Strings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://wiki.open.qq.com/wiki/Web%E6%BC%8F%E6%B4%9E%E6%A3%80%E6%B5%8B%E5%8F%8A%E4%BF%AE%E5%A4%8D#1.2_XSS.E6.BC.8F.E6.B4.9E" rel="nofollow" target="_blank" title=""&gt;腾讯开放平台：XSS 漏洞&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Cross-site_scripting" rel="nofollow" target="_blank" title=""&gt;Wikipedia: Cross-site scripting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://api.rubyonrails.org/classes/ActionView/Helpers/OutputSafetyHelper.html#method-i-raw" rel="nofollow" target="_blank" title=""&gt;Rails API: #raw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://api.rubyonrails.org/classes/String.html#method-i-html_safe" rel="nofollow" target="_blank" title=""&gt;Rails API: String#html_safe&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="2016.1.17 补充"&gt;2016.1.17 补充&lt;/h2&gt;
&lt;p&gt;前面提到说不确定 HAML 里边是否有双等号的同样问题，经过查阅文档发现，HAML 从语法上规避了这个坑，比如 unescape 的话就得是 &lt;code&gt;!=&lt;/code&gt;，看文档 &lt;a href="http://haml.info/docs/yardoc/file.REFERENCE.html#unescaping_html" rel="nofollow" target="_blank"&gt;http://haml.info/docs/yardoc/file.REFERENCE.html#unescaping_html&lt;/a&gt; ，而且它的 &lt;code&gt;=&lt;/code&gt; 和 &lt;code&gt;==&lt;/code&gt; 则是一样的，它也强调了 HAML 是有刻意防止 XSS 的，看 &lt;a href="http://haml.info/docs/yardoc/file.REFERENCE.html#rails_xss_protection" rel="nofollow" target="_blank"&gt;http://haml.info/docs/yardoc/file.REFERENCE.html#rails_xss_protection&lt;/a&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sun, 17 Jan 2016 02:41:23 +0800</pubDate>
      <link>https://ruby-china.org/topics/28758</link>
      <guid>https://ruby-china.org/topics/28758</guid>
    </item>
    <item>
      <title>谨防 ActiveSupport::Cache::Store 缓存 nil 值</title>
      <description>&lt;p&gt;Rails 中的 &lt;strong&gt;&lt;a href="https://github.com/rails/rails/tree/master/activesupport" rel="nofollow" target="_blank" title=""&gt;active_support&lt;/a&gt;&lt;/strong&gt; 组件主要基于 Rails 需要提供了很多非常有用的基础工具以及对 Ruby 内置类进行扩展。其中的 cache 模块主要提供了 Rails 中底层缓存的定义以及简单实现。今天要跟大家探讨的是之前在使用此模块所遇到的一个坑，有兴趣学习其基本用法的可以点击以下两个链接：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-store" rel="nofollow" target="_blank" title=""&gt;Rails Guides: ActiveSupport::Cache::Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html" rel="nofollow" target="_blank" title=""&gt;Rails API: ActiveSupport::Cache::Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="从 ActiveSupport::Cache::Store#fetch 聊起"&gt;从 ActiveSupport::Cache::Store#fetch 聊起&lt;/h3&gt;
&lt;p&gt;之前在实现一个需要从外部服务请求数据的功能时，处于性能考虑，我在代码中使用了缓存，并且设置缓存失效时间为 7 天，示例代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_external_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# 这段代码稍微解释下：&lt;/span&gt;
  &lt;span class="c1"&gt;#   当缓存命中时，则直接读取缓存，如果无期待缓存，则通过 HTTP 向外请求结果，并且将结果&lt;/span&gt;
  &lt;span class="c1"&gt;#   缓存下来，这样子，当下次继续调用时，则可直接返回缓存内容，而无需重复向外请求&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="s1"&gt;'example_cache_key_here'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/example/request/path'&lt;/span&gt;
    &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码其实不复杂，核心代码就是使用了 &lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;ActiveSupport::Cache::Store#fetch&lt;/code&gt;&lt;/a&gt; 方法。&lt;/p&gt;

&lt;p&gt;一切都很正常地运行着，直到有一天，线上系统不断报警，出错原因就是这段代码总是返回 &lt;code&gt;nil&lt;/code&gt; ，而调用者又因为没有判断 &lt;code&gt;nil&lt;/code&gt; 值，就会出现 &lt;code&gt;undefined method 'xxx' for nil:NilClass&lt;/code&gt; 错误。在 debug 时，我尝试了直接调用外部服务接口，发现请求都有正确返回数据，不可能返回 &lt;code&gt;nil&lt;/code&gt; 啊，难道是缓存了 &lt;code&gt;nil&lt;/code&gt; 值？下面就直接通过代码验证一下！&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'active_support'&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="c1"&gt;#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt; &lt;span class="ss"&gt;:nil_value&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt; &lt;span class="ss"&gt;:nil_value&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="ss"&gt;:nil_value&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="kp"&gt;nil&lt;/span&gt;   &lt;span class="c1"&gt;# this `nil` value will be cached&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt; &lt;span class="ss"&gt;:nil_value&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt; &lt;span class="ss"&gt;:nil_value&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看吧， &lt;code&gt;fetch&lt;/code&gt; 方法确实会缓存 &lt;code&gt;nil&lt;/code&gt; 值（通过 &lt;code&gt;exist?&lt;/code&gt; 方法可以判断是否缓存了指定的 key），所以系统出错原因就清晰了：在某次代码执行中，我的缓存刚好失效了，所以系统向外部发送了请求，恰巧这时候外部系统因为故障或者其他可能原因，没有返回期待数据，导致代码中最终缓存了 &lt;code&gt;nil&lt;/code&gt; 值，在接下来的时间里，虽然外部系统可能恢复了正确服务，可是这时候因为我们的系统已经缓存了 &lt;code&gt;nil&lt;/code&gt;值，所以在每次调用时都返回缓存的 &lt;code&gt;nil&lt;/code&gt;，而不是重新请求正确结果，导致最后不停的报错告警。&lt;/p&gt;

&lt;p&gt;这里插播一句，通过后来仔细查阅文档，才发现文档里已经注明：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nil values can be cached.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;╮(╯▽╰)╭ 怪我咯~&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="解决方案"&gt;解决方案&lt;/h3&gt;
&lt;p&gt;意识到这个问题之后，解决思路简单粗暴，就是在可能返回 &lt;code&gt;nil&lt;/code&gt; 值的地方放弃写入缓存：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_external_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example_cache_key_here'&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# 缓存命中，且内容不为 nil ，直接返回缓存内容&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;

  &lt;span class="c1"&gt;# 缓存失效，只能重新请求了~&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/example/request/path'&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# 请求结果正确，写入缓存；否则，放弃之~~~&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;呃~~~虽然解决问题了，可是，就为了告诉系统不要相信 &lt;code&gt;nil&lt;/code&gt;，就写得这么繁琐，好么？好么？好么？&lt;/p&gt;
&lt;h3 id="踏上阅读源码之路"&gt;踏上阅读源码之路&lt;/h3&gt;
&lt;p&gt;我尝试搜索了 &lt;code&gt;#fetch&lt;/code&gt; 方法是否有支持比如 &lt;code&gt;reject_nil&lt;/code&gt; 这样的 option，可惜的是，没有！可是真的没有吗？我不信！看源码去！&lt;/p&gt;

&lt;p&gt;首先还是拜访下 &lt;a href="https://github.com/rails/rails/blob/edd33c08d98723ae9bb89cf7f019277117ed6414/activesupport/lib/active_support/cache.rb#L154" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;ActiveSupport::Cache::Store&lt;/code&gt;&lt;/a&gt; 这个类啦，它可是所有缓存实现类的抽象类，别问我抽象类是什么，就是它明明只说话不干活，但是其他干活的都得向它看齐！好啦，说人话，其实就是说，我们在调用 &lt;code&gt;Rails.cache.read&lt;/code&gt;、&lt;code&gt;Rails.cache.fetch&lt;/code&gt; 等读写方法时，这些方法都是在 &lt;code&gt;ActiveSupport::Cache::Store&lt;/code&gt; 中定义的，但是它只定义逻辑，而实际底层的读写实现，则都是交由其各种子类实现的，比如前面的 &lt;code&gt;ActiveSupport::Cache::MemoryStore&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;首先让我们来看看 &lt;a href="https://github.com/rails/rails/blob/edd33c08d98723ae9bb89cf7f019277117ed6414/activesupport/lib/active_support/cache.rb#L275" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/a&gt;方法的全部内容：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merged_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namespaced_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;cached_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:force&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:super_operation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:fetch&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
      &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle_expired_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached_entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:hit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
        &lt;span class="n"&gt;get_entry_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:hit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
        &lt;span class="n"&gt;save_block_result_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看到，当 &lt;code&gt;#fetch&lt;/code&gt; 方法调用时没有传递 block 的话，它本质上就是 &lt;code&gt;read&lt;/code&gt; 方法的别名而已。而当调用时传递了 block 的话，即如我前面的示例代码，让我们把代码分开看下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;cached_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:force&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:super_operation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:fetch&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle_expired_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached_entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它首先判断是否设置了 &lt;code&gt;force&lt;/code&gt; 选项，如果有，则不读取缓存，由此模拟缓存强制失效；如果未设置 &lt;code&gt;force&lt;/code&gt; 选项或者该选项不等于 true value，则尝试读取缓存，并且调用 &lt;a href="https://github.com/rails/rails/blob/edd33c08d98723ae9bb89cf7f019277117ed6414/activesupport/lib/active_support/cache.rb#L564-L578" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;handle_expired_entry&lt;/code&gt;&lt;/a&gt;判断缓存是否仍旧有效。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:hit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="n"&gt;get_entry_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三行代码，则是在缓存命中时，直接读取缓存内容并且返回。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:hit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="n"&gt;save_block_result_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt; 的代码则表示，在缓存无命中时， &lt;code&gt;#fetch&lt;/code&gt; 代码直接调用 &lt;a href="https://github.com/rails/rails/blob/edd33c08d98723ae9bb89cf7f019277117ed6414/activesupport/lib/active_support/cache.rb#L585-L592" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;#save_block_result_to_cache&lt;/code&gt;&lt;/a&gt; 方法，并且向其传递了一个 block，这个 block 没有干别的事情，它只会执行我们传递给 &lt;code&gt;#fetch&lt;/code&gt; 方法的 block，让我们接着往下看看相关的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_block_result_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;code&gt;#save_block_result_to_cache&lt;/code&gt; 方法首先执行传递进来的代码块，实际上也就是我们期待在缓存失效时执行的代码，而在获得执行结果 &lt;code&gt;result&lt;/code&gt; 后，方法通过调用 &lt;a href="https://github.com/rails/rails/blob/edd33c08d98723ae9bb89cf7f019277117ed6414/activesupport/lib/active_support/cache.rb#L384-L391" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;#write&lt;/code&gt;&lt;/a&gt; 方法将结果写入缓存，最后将 &lt;code&gt;result&lt;/code&gt; 返回。&lt;/p&gt;

&lt;p&gt;通过上面的源码分析，我们可以知道，当缓存失效时，&lt;code&gt;#fetch&lt;/code&gt; 方法会直接将其代码块中的代码的返回值&lt;strong&gt;不加判断&lt;/strong&gt;地写入缓存，并且返回该返回值。这里，或许我们可以做点什么，来实现我们想要支持 &lt;code&gt;:reject_nil&lt;/code&gt; 的需求？&lt;/p&gt;
&lt;h3 id="支持 :reject_nil option"&gt;支持 &lt;code&gt;:reject_nil&lt;/code&gt; option&lt;/h3&gt;
&lt;p&gt;为了支持 &lt;code&gt;:reject_nil&lt;/code&gt;，我们只需要在写入缓存前判断是否真的需要 &lt;code&gt;nil&lt;/code&gt; 值即可，于是我们只需要在 &lt;code&gt;#save_block_result_to_cache&lt;/code&gt; 中加入 &lt;code&gt;#write&lt;/code&gt; 的前置条件：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_block_result_to_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# options[:reject_nil] &amp;amp;&amp;amp; result.nil? 作为前置条件&lt;/span&gt;
  &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:reject_nil&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;话不多说，让我们来重新试验一番：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'active_support'&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="c1"&gt;#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="ss"&gt;:nil_key1&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt; &lt;span class="ss"&gt;:nil_key1&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="ss"&gt;:nil_key2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;reject_nil: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt; &lt;span class="ss"&gt;:nil_key2&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，当我们调用 &lt;code&gt;#fetch&lt;/code&gt; 方法时，如果没有传递 &lt;code&gt;reject_nil: true&lt;/code&gt;，则 &lt;code&gt;#fetch&lt;/code&gt; 方法会默认缓存 &lt;code&gt;nil&lt;/code&gt; 值；而如果我们设置 &lt;code&gt;reject_nil: true&lt;/code&gt; 的话，则 &lt;code&gt;#fetch&lt;/code&gt; 就会放弃写入 &lt;code&gt;nil&lt;/code&gt; 值到缓存中。试验成功！！！&lt;/p&gt;

&lt;p&gt;基于这样的实现，我的代码就又可以改为如下了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_external_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# 所有改动只是加了一个 `reject_nil: true`，多方便，妈妈再也不用担心我掉到坑里去了&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="s1"&gt;'example_cache_key_here'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;reject_nil: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/example/request/path'&lt;/span&gt;
    &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;待会去给 Rails 提交 Pull Request 去 &lt;strong&gt;O(∩_∩)O~~&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;缓存是好个东西，用得好能够让应用性能表现突飞猛进&lt;/li&gt;
&lt;li&gt;要注意缓存写入的边界条件，要注意避免缓存了空值，但也并非所有空值都不能缓存（比如有些接口确实就是有可能返回空值嘛），具体看业务，没有绝对的要与不要，反正 &lt;code&gt;:reject_nil&lt;/code&gt; 给你了，看你要不要&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>martin91</author>
      <pubDate>Fri, 30 Oct 2015 22:39:38 +0800</pubDate>
      <link>https://ruby-china.org/topics/27900</link>
      <guid>https://ruby-china.org/topics/27900</guid>
    </item>
    <item>
      <title>Apdex——衡量服务器性能的标准</title>
      <description>&lt;p&gt;&lt;strong&gt;前言：&lt;/strong&gt;不知道该放哪个节点，只能放 &lt;strong&gt;工具控&lt;/strong&gt; 了，最近才开始接触 Apdex，如有错误理解，烦请指正！&lt;/p&gt;

&lt;p&gt;日常工作中，我们总是习惯于通过量化的标准去衡量我们对事物的评价，比如美食点评的星级、酒店的星级、每个个人的信用评分等等。而作为一个 Web 工程师，我们也总是在意于我们网站的性能，因为网站的性能会最直接地影响用户的体验。今天要介绍的就是一种同样能够帮助工程师对应用性能进行量化评估的标准 —— Apdex。&lt;/p&gt;

&lt;p&gt;Apdex 全称是 Application Performance Index，是由 Apdex 联盟开放的用于评估应用性能的工业标准。Apdex 联盟起源于 2004 年，由 &lt;a href="http://apdex.org/bios.html" rel="nofollow" target="_blank" title=""&gt;Peter Sevcik&lt;/a&gt;发起。Apdex 标准从用户的角度出发，将对应用响应时间的表现，转为用户对于应用性能的可量化为范围为 0-1 的满意度评价。&lt;/p&gt;
&lt;h3 id="术语"&gt;术语&lt;/h3&gt;
&lt;p&gt;Apdex 定义了应用响应时间的最优门槛为 &lt;strong&gt;T&lt;/strong&gt;，另外根据应用响应时间结合 T 定义了三种不同的性能表现：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Satisfied（满意）&lt;/strong&gt;：应用响应时间低于或等于 T（T 由性能评估人员根据预期性能要求确定），比如 T 为 1.5s，则一个耗时 1s 的响应结果则可以认为是 satisfied 的。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tolerating（可容忍）&lt;/strong&gt;：应用响应时间大于 T，但同时小于或等于 4T。假设应用设定的 T 值为 1s，则 4 * 1 = 4 秒极为应用响应时间的容忍上限。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frustrated（烦躁期）&lt;/strong&gt;：应用响应时间大于 4T。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="公式"&gt;公式&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apdex&amp;lt;sub&amp;gt;t&amp;lt;/sub&amp;gt; = (Satisfied Count + Tolerating Count / 2) / Total Samples
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;Satisfied Count&lt;/code&gt; 就是指定采样时间内响应时间满足 &lt;code&gt;Satisfied&lt;/code&gt; 要求的应用响应次数；而 &lt;code&gt;Tolerating Count&lt;/code&gt; 就是指定采样时间内响应时间满足 &lt;code&gt;Tolerating&lt;/code&gt; 要求的应用响应次数；最后的 &lt;code&gt;Total Samples&lt;/code&gt; 就是总的采样次数总数。从公式可以看出，应用的 Apdex 得分与采样持续时间无关，与目标响应时间 T 相关（在采用总数固定的情况下，T 通过影响 &lt;code&gt;Satisfied Count&lt;/code&gt;以及 &lt;code&gt;Tolerating Count&lt;/code&gt;的值间接影响最终的得分）。&lt;/p&gt;

&lt;p&gt;举例来说，假设你的应用期待的响应时间能够在 1000 ms 内，在 100 次采样中，有 50 次应用响应时间低于 1000 ms，30 次应用响应时间处于 1000 ms 到 4000 ms（4 * 1000ms）之间，剩下 20 次响应时间长于 4000 ms，那么，该应用在 T = 1000ms 的情况下的 Apdex 值为：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="Apdex 与 New Relic"&gt;Apdex 与 New Relic&lt;/h3&gt;
&lt;p&gt;在 New Relic 的 APM（Application Performance Management）功能中，就提供了各个维度的 Apdex 统计结果，比如 Server Apdex(服务器性能评分) 以及 Browser Apdex(终端用户性能体验评分)，如图：
&lt;img src="http://7xj84e.com1.z0.glb.clouddn.com/blog/apdex%20overview.png" title="" alt="Apdex 统计报表(T=0.5s)"&gt;&lt;/p&gt;

&lt;p&gt;其中可以看到应用服务器在 &lt;code&gt;T=0.5s&lt;/code&gt; 的情况下得到的 Apdex 分数为 0.76，而 Browser（Browser 更多的是静态文件加载）在 &lt;code&gt;T=7s&lt;/code&gt;的情况下得到的 Apdex 分数为 0.94。结合两者可以判断，目前应用到达终端用户性能表现比较优秀（0.94，比较接近最大值 1），但是其中影响总体性能的瓶颈则在于服务器性能（仅仅只有 0.76 分），通过这样的数据，我们就能知道下一步性能优化的方向了——服务器端性能优化。&lt;/p&gt;

&lt;p&gt;实际上，上面展示的只是 New Relic 的一种粒度比较粗的针对整个应用的 Apdex 报表，New Relic 同样提供了很多细粒度的 Apdex 数据，比如下面展示的针对具体的请求入口的 Apdex 报表：
&lt;img src="http://7xj84e.com1.z0.glb.clouddn.com/blog/detailed_apdex.png" title="" alt="具体请求入口的 Apdex"&gt;
这样，通过逐步的细化，我们就可以进一步定位性能瓶颈，通过不断优化 Apdex 评分低的入口逐步提升应用整体性能体验。&lt;/p&gt;
&lt;h3 id="Apdex 与 T 值"&gt;Apdex 与 T 值&lt;/h3&gt;
&lt;p&gt;从公式中其实可以非常明显地看出来，T 值的选择对于最终的 Apdex 值也会有直接影响，越大的 T 值理论上来说会有更大的 Apdex 得分。。比如我们可以在 New Relic 中将应用的 Apdex T 值改为 1s，以下是设定过程中看到的原来的值是 0.5s：
&lt;img src="http://7xj84e.com1.z0.glb.clouddn.com/blog/apdex%20setting.png" title="" alt="T 值设定"&gt;
而改为 1s 后，跟上面同样的采样数据得到的新的平均 Apdex 值则高于原来的 0.76。
所以，在对应用性能进行评估的时候，首先需要确保结合应用具体情况设定一个相对合理的 T 值，太大的 T 值会导致过于乐观的 Apdex 值，但是太小的 T 值又会造成过于严苛的性能要求，最终可能导致过度的性能优化。
所以，总而言之，抛弃 T 值谈 Apdex 得分，都是耍流氓！&lt;/p&gt;
&lt;h3 id="Apdex 值一定要做到 1 吗？"&gt;Apdex 值一定要做到 1 吗？&lt;/h3&gt;
&lt;p&gt;Apdex 公式计算能够得到的最大值就是 1，表示应用“可能”能够令所有用户对应用性能感到满意。但是，Apdex = 1 可以只是一个不断优化的方向，却不一定是要成为优化的目标，具体根据项目实际情况确定，毕竟，优化本身也需要成本投入，不需要为了极致的性能而投入过多的成本。&lt;/p&gt;
&lt;h3 id="参考资料以及推荐链接"&gt;参考资料以及推荐链接&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Apdex" rel="nofollow" target="_blank" title=""&gt;Wikipedia: Apdex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.apdex.org/" rel="nofollow" target="_blank" title=""&gt;Apdex 官网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measuring-user-satisfaction#what-is" rel="nofollow" target="_blank" title=""&gt;Apdex: Measuring user satisfaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/change-your-apdex-settings" rel="nofollow" target="_blank" title=""&gt;New Relic: Change your Apdex settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/view-your-apdex-score" rel="nofollow" target="_blank" title=""&gt;New Relic: View your Apdex score&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;文章在我的 blog 上的地址是： &lt;a href="http://martin91.github.io/blog/2015/03/28/the-correct-way-to-metric-server-response-time/" rel="nofollow" target="_blank"&gt;http://martin91.github.io/blog/2015/03/28/the-correct-way-to-metric-server-response-time/&lt;/a&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Sun, 30 Aug 2015 17:00:55 +0800</pubDate>
      <link>https://ruby-china.org/topics/27121</link>
      <guid>https://ruby-china.org/topics/27121</guid>
    </item>
    <item>
      <title>解读 Rails - 适配器模式</title>
      <description>&lt;p&gt;看到最近翻译的文章比较热，我也来贡献一篇去年翻译的吧，本文翻译自&lt;a href="http://monkeyandcrow.com/blog/reading_rails_the_adapter_pattern/?utm_source=rubyweekly&amp;amp;utm_medium=email" rel="nofollow" target="_blank" title=""&gt;Reading Rails - The Adapter Pattern&lt;/a&gt;，限于本人水平有限，翻译不当之处，敬请指教！&lt;/p&gt;

&lt;p&gt;今天我们暂时先放下具体的代码片段，我们将要对 Rails 中所实现的一个比较常见的设计模式进行一番探索，这个模式就是&lt;a href="http://en.wikipedia.org/wiki/Adapter_pattern" rel="nofollow" target="_blank" title=""&gt;适配器模式（Adapter Pattern）&lt;/a&gt;。从一定的意义上来说，这次的探索并不全面，但是我希望能够突出一些实际的例子。&lt;/p&gt;

&lt;p&gt;为了跟随本文的步骤，请使用&lt;a href="https://github.com/adamsanderson/qwandry" rel="nofollow" target="_blank" title=""&gt;qwandry&lt;/a&gt;打开相关的代码库，或者直接在&lt;a href="https://github.com/rails/rails/tree/5505c1d700f17e2009e1189a7aa6dafafe7062a4" rel="nofollow" target="_blank" title=""&gt;Github&lt;/a&gt;上查看这些代码。&lt;/p&gt;
&lt;h3 id="适配器模式"&gt;适配器模式&lt;/h3&gt;
&lt;p&gt;&lt;a href="http://en.wikipedia.org/wiki/Adapter_pattern" rel="nofollow" target="_blank" title=""&gt;适配器模式&lt;/a&gt;可以用于对不同的接口进行包装以及提供统一的接口，或者是让某一个对象看起来像是另一个类型的对象。在静态类型的编程语言里，我们经常使用它去满足类型系统的特点，但是在类似 Ruby 这样的弱类型编程语言里，我们并不需要这么做。尽管如此，它对于我们来说还是有很多意义的。&lt;/p&gt;

&lt;p&gt;当使用第三方类或者库的时候，我们经常从这个例子开始（start out fine）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_nearest_restaurant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:restaurant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们假设有一个针对&lt;code&gt;locator&lt;/code&gt;的接口，但是如果我们想要&lt;code&gt;find_nearest_restaurant&lt;/code&gt;能够支持另一个库呢？这个时候我们可能就会去尝试添加新的特殊的场景的处理：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_nearest_restaurant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt; &lt;span class="no"&gt;GeoFish&lt;/span&gt;
    &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:restaurant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt; &lt;span class="no"&gt;ActsAsFound&lt;/span&gt;
    &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_food&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:lat&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:lon&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not supported."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个比较务实的解决方案。或许我们也不再需要考虑去支持另一个库了。也或许&lt;code&gt;find_nearest_restaurant&lt;/code&gt;就是我们使用&lt;code&gt;locator&lt;/code&gt;的唯一场景。&lt;/p&gt;

&lt;p&gt;那假如你真的需要去支持一个新的&lt;code&gt;locator&lt;/code&gt;，那又会是怎么样的呢？那就是你有三个特定的场景。再假如你需要实现&lt;code&gt;find_nearest_hospital&lt;/code&gt;方法呢？这样你就需要在维护这三种特定的场景时去兼顾两个不同的地方。当你觉得这种解决方案不再可行的时候，你就需要考虑适配器模式了。&lt;/p&gt;

&lt;p&gt;在这个例子中，我们可以为&lt;code&gt;GeoFish&lt;/code&gt;以及&lt;code&gt;ActsAsFound&lt;/code&gt;编写适配器，这样的话，在我们的其他代码中，我们就不需要了解我们当前正在使用的是哪个库了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_nearest_hospital&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="ss"&gt;:type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:hospital&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="ss"&gt;:lat&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="ss"&gt;:lon&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lon&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;locator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GeoFishAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geo_fish_locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;find_nearest_hospital&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特意假设的例子就到此为止，接下来让我们看看真实的代码。&lt;/p&gt;
&lt;h3 id="MultiJSON"&gt;MultiJSON&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ActiveSupport&lt;/code&gt;在做 JSON 格式的解码时，用到的是&lt;code&gt;MultiJSON&lt;/code&gt;，这是一个针对 JSON 库的适配器。每一个库都能够解析 JSON，但是做法却不尽相同。让我们分别看看针对&lt;a href="https://github.com/ohler55/oj" rel="nofollow" target="_blank" title=""&gt;oj&lt;/a&gt;和&lt;a href="https://github.com/brianmario/yajl-ruby" rel="nofollow" target="_blank" title=""&gt;yajl&lt;/a&gt;的适配器。
(&lt;strong&gt;提示&lt;/strong&gt;: 可在命令行中输入&lt;code&gt;qw multi_json&lt;/code&gt;查看源码。)&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MultiJson&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Adapters&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Oj&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Adapter&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:symbol_keys&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:symbolize_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Oj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oj 的适配器修改了&lt;code&gt;options&lt;/code&gt;哈希表，使用&lt;code&gt;Hash#delete&lt;/code&gt;将&lt;code&gt;:symbolize_keys&lt;/code&gt;项转换为 Oj 的&lt;code&gt;:symbol_keys&lt;/code&gt;项：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:symbolize_keys&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:symbol_keys&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:symbolize_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;                                                 &lt;span class="c1"&gt;# =&amp;gt; {:symbol_keys=&amp;gt;true}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来 MultiJSON 调用了&lt;code&gt;::Oj.load(string, options)&lt;/code&gt;。MultiJSON 适配后的 API 跟 Oj 原有的 API 非常相似，在此不必赘述。不过你是否注意到，Oj 是如何引用的呢？&lt;code&gt;::Oj&lt;/code&gt;引用了顶层的&lt;code&gt;Oj&lt;/code&gt;类，而不是&lt;code&gt;MultiJson::Adapters::Oj&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;现在让我们看看 MultiJSON 又是如何适配 Yajl 库的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MultiJson&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Adapters&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Yajl&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Adapter&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Yajl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:symbolize_keys&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:symbolize_keys&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个适配器从不同的方式实现了&lt;code&gt;load&lt;/code&gt;方法。Yajl 的方式是先创建一个解析器的实力，然后将传入的字符串&lt;code&gt;string&lt;/code&gt;作为参数调用&lt;code&gt;Yajl::Parser#parse&lt;/code&gt;方法。在&lt;code&gt;options&lt;/code&gt;哈希表上的处理也略有不同。只有&lt;code&gt;:symbolize_keys&lt;/code&gt;项被传递给了 Yajl。&lt;/p&gt;

&lt;p&gt;这些 JSON 的适配器看似微不足道，但是他们却可以让你随心所欲地在不同的库之间进行切换，而不需要在每一个解析 JSON 的地方更新代码。&lt;/p&gt;
&lt;h3 id="ActiveRecord"&gt;ActiveRecord&lt;/h3&gt;
&lt;p&gt;很多 JSON 库往往都遵从相似的模式，这让适配工作变得相当轻松。但是如果你是在处理一些更加复杂的情况时，结果会是怎样？ActiveRecord 包含了针对不同数据库的适配器。尽管 PostgreSQL 和 MySQL 都是 SQL 数据库，但是他们之间还是有很多不同之处，而 ActiveRecord 通过使用适配器模式屏蔽了这些不同。(&lt;strong&gt;提示&lt;/strong&gt;: 命令行中输入&lt;code&gt;qw activerecord&lt;/code&gt;查看 ActiveRecord 的代码)&lt;/p&gt;

&lt;p&gt;打开 ActiveRecord 代码库中的&lt;code&gt;lib/connection_adapters&lt;/code&gt;目录，里边会有针对 PostgreSQL,MySQL 以及 SQLite 的适配器。除此之外，还有一个名为&lt;code&gt;AbstractAdapter&lt;/code&gt;的适配器，它作为每一个具体的适配器的基类。&lt;code&gt;AbstractAdapter&lt;/code&gt;实现了在大部分数据库中常见的功能，这些功能在其子类比如&lt;code&gt;PostgreSQLAdapter&lt;/code&gt;以及&lt;code&gt;AbstractMysqlAdapter&lt;/code&gt;中被重新定制，而其中&lt;code&gt;AbstractMysqlAdapter&lt;/code&gt;则是另外两个不同的 MySQL 适配器——MysqlAdapter 以及 Mysql2Adapter——的父类。让我们通过一些真实世界中的例子来看看他们是如何一起工作的。&lt;/p&gt;

&lt;p&gt;PostgreSQL 和 MySQL 在 SQL 方言的实现稍有不同。查询语句&lt;code&gt;SELECT * FROM users&lt;/code&gt;在这两个数据库都可以正常执行，但是它们在一些类型的处理上会稍显不同。在 MySQL 和 PostgreSQL 中，时间格式就不尽相同。其中，PostgreSQL 支持微秒级别的时间，而 MySQL 只是到了最近的一个稳定发布的版本中才支持。那这两个适配器又是如何处理这种差异的呢？&lt;/p&gt;

&lt;p&gt;ActiveRecord 通过被混入到&lt;code&gt;AbstractAdapter&lt;/code&gt;的&lt;code&gt;ActiveRecord::ConnectionAdapters::Quoting&lt;/code&gt;中的&lt;code&gt;quoted_date&lt;/code&gt;引用日期。而&lt;code&gt;AbstractAdapter&lt;/code&gt;中的实现仅仅只是格式化了日期：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quoted_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
  &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rails 中的 ActiveSupport 扩展了&lt;code&gt;Time#to_s&lt;/code&gt;，使其能够接收一个代表格式名的符号类型参数。&lt;code&gt;:db&lt;/code&gt;所代表的格式就是&lt;code&gt;%Y-%m-%d %H:%M:%S&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Examples of common formats:&lt;/span&gt;
&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;#=&amp;gt; "2014-02-19 06:08:13"&lt;/span&gt;
&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:short&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;#=&amp;gt; "19 Feb 06:08"&lt;/span&gt;
&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:rfc822&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;#=&amp;gt; "Wed, 19 Feb 2014 06:08:13 +0000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 的适配器都没有重写&lt;code&gt;quoted_date&lt;/code&gt;方法，它们自然会继承这种行为。另一边，&lt;code&gt;PostgreSQLAdapter&lt;/code&gt;则对日期的处理做了两个修改：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quoted_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acts_like?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%06d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;year&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^-/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" BC"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它在一开始便调用&lt;code&gt;super&lt;/code&gt;方法，所以它也会得到一个类似 MySQL 中格式化后的日期。接下来，它检测&lt;code&gt;value&lt;/code&gt;是否像是一个具体时间。这是一个 ActiveSupport 中扩展的方法，当一个对象类似&lt;code&gt;Time&lt;/code&gt;类型的实例时，它会返回&lt;code&gt;true&lt;/code&gt;。这让它更容易表明各种对象已被假设为类似&lt;code&gt;Time&lt;/code&gt;的对象。（&lt;strong&gt;提示&lt;/strong&gt;: 对&lt;code&gt;acts_like?&lt;/code&gt;方法感兴趣？请在命令行中执行&lt;code&gt;qw activesupport&lt;/code&gt;，然后阅读&lt;code&gt;core_ext/object/acts_like.rb&lt;/code&gt;）&lt;/p&gt;

&lt;p&gt;第二部分的条件检查&lt;code&gt;value&lt;/code&gt;是否有用于返回毫秒的&lt;code&gt;usec&lt;/code&gt;方法。如果可以求得毫秒数，那么它将通过&lt;code&gt;sprintf&lt;/code&gt;方法被追加到&lt;code&gt;result&lt;/code&gt;字符串的末尾。跟很多时间格式一样，&lt;code&gt;sprintf&lt;/code&gt;也有很多不同的方式用于格式化数字：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%06d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#=&amp;gt; "000032"&lt;/span&gt;
&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%6d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#=&amp;gt; "    32"&lt;/span&gt;
&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#=&amp;gt; "32"&lt;/span&gt;
&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%.2f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#=&amp;gt; "32.00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，假如日期是一个负数，&lt;code&gt;PostgreSQLAdapter&lt;/code&gt;就会通过加上"BC"去重新格式化日期，这是 PostgreSQL 数据库的实际要求：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'2000-01-20'&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;
&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'2000-01-20 BC'&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt; &lt;span class="no"&gt;BC&lt;/span&gt;
&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'-2000-01-20'&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="no"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="n"&gt;displacement&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="ss"&gt;range: &lt;/span&gt;&lt;span class="s2"&gt;"-2000-01-20"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只是 ActiveRecord 适配多个 API 时的一个极小的方式，但它却能帮助你免除由于不同数据库的细节所带来的差异和烦恼。&lt;/p&gt;

&lt;p&gt;另一个体现 SQL 数据库的不同点是数据库表被创建的方式。MySQL 以及 PostgreSQL 中对主键的处理各不相同：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AbstractMysqlAdapter&lt;/span&gt;
&lt;span class="no"&gt;NATIVE_DATABASE_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;:primary_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"int(11) DEFAULT NULL auto_increment PRIMARY KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# PostgreSQLAdapter&lt;/span&gt;
&lt;span class="no"&gt;NATIVE_DATABASE_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="s2"&gt;"serial primary key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两种适配器都能够明白 ActiveRecord 中的主键的表示方式，但是它们会在创建新表的时候将此翻译为不同的 SQL 语句。当你下次在编写一个 migration 或者执行一个查询的时候，思考一下 ActiveRecord 的适配器以及它们为你做的所有微小的事情。&lt;/p&gt;
&lt;h3 id="DateTime和Time"&gt;DateTime 和 Time&lt;/h3&gt;
&lt;p&gt;当 MultiJson 以及 ActiveRecord 实现了传统的适配器的时候，Ruby 的灵活性使得另一种解决方案成为可能。&lt;code&gt;DateTime&lt;/code&gt;以及&lt;code&gt;Time&lt;/code&gt;都用于表示时间，但是它们在内部的处理上是不同的。虽然有着这些细微的差异，但是它们所暴露出来的 API 却是极其类似的（&lt;strong&gt;提示&lt;/strong&gt;：命令行中执行&lt;code&gt;qw activesupport&lt;/code&gt;查看此处相关代码）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;day&lt;/span&gt;     &lt;span class="c1"&gt;#=&amp;gt; 19         (Day of month)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wday&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; 3          (Day of week)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usec&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; 371552     (Microseconds)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; 1392871392 (Epoch secconds)&lt;/span&gt;

&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;day&lt;/span&gt;     &lt;span class="c1"&gt;#=&amp;gt; 19         (Day of month)&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wday&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; 3          (Day of week)&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usec&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; NoMethodError: undefined method `usec'&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;    &lt;span class="c1"&gt;#=&amp;gt; NoMethodError: undefined method `to_i'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ActiveSupport 通过添加缺失的方法来直接修改&lt;code&gt;DateTime&lt;/code&gt;和&lt;code&gt;Time&lt;/code&gt;，进而抹平了两者之间的差异。从实例上看，这里就有一个例子演示了 ActiveSupport 如何定义&lt;code&gt;DateTime#to_i&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_i&lt;/span&gt;
    &lt;span class="n"&gt;seconds_since_unix_epoch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seconds_since_unix_epoch&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2440588&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;offset_in_seconds&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;seconds_since_midnight&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;offset_in_seconds&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seconds_since_midnight&lt;/span&gt;
    &lt;span class="n"&gt;sec&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一个用于支持的方法，&lt;code&gt;seconds_since_unix_epoch&lt;/code&gt;，&lt;code&gt;offset_in_seconds&lt;/code&gt;，以及&lt;code&gt;seconds_since_midnight&lt;/code&gt;都使用或者扩展了&lt;code&gt;DateTime&lt;/code&gt;中已经存在的 API 去定义与&lt;code&gt;Time&lt;/code&gt;中匹配的方法。&lt;/p&gt;

&lt;p&gt;假如说我们前面所看到的适配器是相对于被适配对象的外部适配器，那么我们现在所看到的这个就可以被称之为内部适配器。与外部适配器不同的是，这种方法受限于已有的 API，并且可能导致一些麻烦的矛盾问题。举例来说，&lt;code&gt;DateTime&lt;/code&gt;和&lt;code&gt;Time&lt;/code&gt;在一些特殊的场景下就有可能出现不一样的行为：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="c1"&gt;#=&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;#=&amp;gt; 2014-02-26 07:32:39&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;         &lt;span class="c1"&gt;#=&amp;gt; 2014-02-25 07:32:40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当加上 1 的时候，&lt;code&gt;DateTime&lt;/code&gt;加上了一天，而&lt;code&gt;Time&lt;/code&gt;则是加上了一秒。当你需要使用它们的时候，你要记住 ActiveSupport 基于这些不同，提供了诸如&lt;code&gt;change&lt;/code&gt;和&lt;code&gt;Duration&lt;/code&gt;等保证一致行为的方法或类。&lt;/p&gt;

&lt;p&gt;这是一个好的模式吗？它理所当然是方便的，但是如你刚才所见，你仍旧需要注意其中的一些不同之处。&lt;/p&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;p&gt;设计模式不是只有 Java 才需要的。Rails 通过使用设计模式以提供用于 JSON 解析以及数据库维护的统一接口。由于 Ruby 的灵活性，类似&lt;code&gt;DateTime&lt;/code&gt;以及&lt;code&gt;Time&lt;/code&gt;这样的类可以被直接地修改而提供相似的接口。Rails 的源码就是一个可以让你挖掘真实世界中不同设计模式实例的天堂。&lt;/p&gt;

&lt;p&gt;在这次的实践中，我们同时也发掘了一些有趣的代码：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hash[:foo] = hash.delete(:bar)&lt;/code&gt;是一个用于重命名哈希表中某一项的巧妙方法。&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;::ClassName&lt;/code&gt;会调用顶层的类。&lt;/li&gt;
&lt;li&gt;ActiveSupport 为&lt;code&gt;Time&lt;/code&gt;、&lt;code&gt;Date&lt;/code&gt;以及其他的类添加了一个可选的代表格式的参数&lt;code&gt;format&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sprintf&lt;/code&gt;可以用于格式化数字。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;想要探索更多的知识？回去看看 MultiJson 是如何处理以及解析格式的。仔细阅读你在你的数据库中所使用到的 ActiveRecord 的适配器的代码。浏览 ActiveSupport 中用于 xml 适配器的&lt;code&gt;XmlMini&lt;/code&gt;，它跟 MultiJson 中的 JSON 适配器是类似的。在这些里面还会有很多可以学习的。&lt;/p&gt;

&lt;p&gt;喜欢这篇文章？
&lt;a href="http://martin91.github.io/blog/2014/03/02/jie-du-rails-xi-lie-fan-yi/" rel="nofollow" target="_blank" title=""&gt;阅读另外 8 篇&lt;/a&gt;“解读 Rails”中的文章。&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Tue, 26 May 2015 17:28:25 +0800</pubDate>
      <link>https://ruby-china.org/topics/25753</link>
      <guid>https://ruby-china.org/topics/25753</guid>
    </item>
    <item>
      <title>[2015年6月5-7日] 热爱创造的你快来一起 Change The World —— 第二届广州创客马拉松报名开始</title>
      <description>&lt;h3 id="活动主题解读"&gt;活动主题解读&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;在 21 世纪，我们的城市正在进行巨大的改变。新型的通讯技术及前所未有多层级的交互正在统一全球网络，并让我们能够以眼睛等各种感官与这个新的世界互动。这些软件被应用在各种各样的商业/创业项目上，改变了众多的传统行业和已有规则，进而让人们的生活方式也随之变化。&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;然而，在旧的生活场景和行业模式更新迭代时，许多公共应用场景仍然无法得到解决：在公共场所苦苦寻找一个洗手间；在需要出行时被堵在高速公路上，而实际上只需要拐入另一条小路即可畅通无阻；收拾装备准备去体育馆锻炼时，却发现找不到开放时间。这些都是亟待解决的问题，我们解决它的方法就是 hacking。&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;a href="http://gzhack.io/" rel="nofollow" target="_blank" title=""&gt;广州创客马拉松（GZHACK）&lt;/a&gt;将为你提供数据，让你在现场和各行各业的人一起，实现对开放数据的 hacking！来吧，赶紧加入我们，你的知识，你的技能，你的想法，等等一切就是你手中的武器，让我们一起致力于为我们所生活的城市以及环境贡献热血吧！你就是下一个城市英雄！&lt;/p&gt;
&lt;h3 id="活动主要内容"&gt;活动主要内容&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;48 小时极限开发，带上你的团队或现场组队（1-5 人），来一场开放数据的狂欢吧！你可以：对数据进行可视化，让大家更容易看懂数据；基于数据进行应用开发，让更多人享受到公共数据带来的益处；你也可以通过自己对于数据的理解，提出专业的意见和想法，组建团队将其实现。&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;大赛会根据既定的评分规则（见本文末尾）评出不同奖项，并且予以现金奖励。表现突出的优秀参赛者更有机会赢取前往韩国首尔参加国际创客马拉松的宝贵名额！&lt;/p&gt;
&lt;h3 id="报名方式"&gt;报名方式&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;填写&lt;a href="http://www.huodongxing.com/event/9280906940000" rel="nofollow" target="_blank" title=""&gt;报名表单&lt;/a&gt;，收到确认邮件即成功报名～&lt;/p&gt;
&lt;h3 id="活动地点"&gt;活动地点&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;a href="http://www.huodongxing.com/event/map/9280906940000" rel="nofollow" target="_blank" title=""&gt;广州市番禺区大学城青蓝街 22 号 6CIT 咖啡厅&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="大赛流程"&gt;大赛流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;6 月 5 日周五（第一日）&lt;/strong&gt;
2:00 p.m. –4:00 p.m.  报到与开幕
参赛者在主赛场周边报到，随后进入主赛场，大会将组织赛前活动与社交活动引导参赛者互相介绍交流。
相关人员进场完毕后举行开幕讲话及大赛规则讲解&lt;/p&gt;

&lt;p&gt;4:00 p.m. –5:00 p.m.  创意交流
自我介绍，每个参赛者有 1 分钟来阐述自己的想法 (非强制性)，自己的特长以及需要团队成员的类型，其余参赛者可以自由选择感兴趣的想法组成团队。
完成组队后，参赛队伍可以开始开发作品&lt;/p&gt;

&lt;p&gt;6:00 p.m. –12:00a.m.  开发阶段
竞赛时间，参赛队伍投入开发&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;6 月 6 日周六（第二日）&lt;/strong&gt;
全天 开发阶段
竞赛时间，参赛队伍投入开发&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;6 月 7 日周日（第三日）&lt;/strong&gt;
12:00 a.m. –3:00 p.m. 开发阶段
竞赛时间，参赛队伍投入开发&lt;/p&gt;

&lt;p&gt;8:00 a.m. – 3:00 p.m. 展示准备
建议参赛者利用这段时间开始准备展示，写作 PPT，拍摄微视频，练习演讲等&lt;/p&gt;

&lt;p&gt;3:30 p.m. –4:30 p.m. 提交作品
提交作品，上传相关作品名称，介绍，视频，PPT 到大赛网站。参赛队伍可以在比赛开始后 24 小时开始提交作品。提交截止后，初步评估每件作品，并安排符合要求的作品展示。 &lt;/p&gt;

&lt;p&gt;4:30 p.m. –6:30p.m. 作品展示
参赛队伍有三分钟时间展示每件作品，可以通过 PPT，演示等手段进行展示，评委和投资人将会有 2 分钟进行提问。&lt;/p&gt;

&lt;p&gt;6:45 p.m. –10:00 p.m. 颁奖典礼和晚宴
经过大赛评委的商议，评出各个奖项的获奖团队。现场主持人为引导各个获奖团队和评委嘉宾上台领/颁奖，合影。
结束之后开始集中聚餐，形式为自助餐。参赛者，嘉宾评委，投资人可以自由地进行赛后交流。&lt;/p&gt;

&lt;p&gt;10:00 p.m. 广州创客马拉松系列活动全部结束&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="评分规则"&gt;评分规则&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;评委嘉宾组成的评委会给个项目打分，以分数高低决出前十名，获得现金奖。评分项目有以下内容：
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;1. 完成度：&lt;/strong&gt;产品是基本完成还是只有几个截图和幻灯片？
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;2. 创意：&lt;/strong&gt;产品的新颖度。是否已有同类产品？差别在哪里？
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;3. 实用性：&lt;/strong&gt;产品在现实生活中的实用性
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;4. 展示的清晰度：&lt;/strong&gt;团队对产品展示的清晰度
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;5. 影响力：&lt;/strong&gt;产品所解决问题的影响规模
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;6. 技术难度：&lt;/strong&gt;产品所需技术难度
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;strong&gt;7. 外观设计：&lt;/strong&gt;产品外观、用户界面和用户体验&lt;/p&gt;
&lt;h3 id="关于我们"&gt;关于我们&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;广州创客马拉松组委会是由广州的创业、技术社区的志愿者自发组成的草根团队，主要执行每年一次的广州创客马拉松主赛事组织，以及开展每年活动之间的各个系列小型活动和开源活动。&lt;/p&gt;
&lt;h3 id="活动官网"&gt;活动官网&lt;/h3&gt;
&lt;p&gt;&lt;a href="http://gzhack.io/" rel="nofollow" target="_blank" title=""&gt;2015 第二届广州创客马拉松&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;更多实时资讯，同时推荐大家关注广州创客马拉松官方微信公众号：gz_hack。&lt;/p&gt;
&lt;h3 id="2014 第一届活动相关报道和成果"&gt;2014 第一届活动相关报道和成果&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1. &lt;a href="http://tech.southcn.com/t/2014-04/30/content_98687078.htm" rel="nofollow" target="_blank" title=""&gt;南方网：首届广州创客马拉松比赛在广东科学中心举行&lt;/a&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2. &lt;a href="http://news.xinhuanet.com/tech/2014-04/28/c_1110444545.htm" rel="nofollow" target="_blank" title=""&gt;新华网：百名“创客”广州大比拼 48 小时实现科技发明 21 项&lt;/a&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;3. &lt;a href="http://news.ycwb.com/2014-04/28/content_6630993.htm" rel="nofollow" target="_blank" title=""&gt;金羊网：广州首届创客马拉松赛落幕 诞生 21 个极具创意作品&lt;/a&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;4. &lt;a href="http://www.chinanews.com/tp/2014/04-28/6112993.shtml" rel="nofollow" target="_blank" title=""&gt;中新网：全球创客马拉松“智能垃圾桶”让生活更卫生&lt;/a&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;5. &lt;a href="http://gzdaily.dayoo.com/html/2014-05/17/content_2630593.htm" rel="nofollow" target="_blank" title=""&gt;广州日报：创客“马拉松”：“抱团式”头脑风暴&lt;/a&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;6. &lt;a href="http://informationtimes.dayoo.com/html/2014-05/18/content_2631557.htm" rel="nofollow" target="_blank" title=""&gt;信息时报专版：“广州像一盘菜，什么材料和佐料都有了，我希望帮忙把这盘菜炒熟”&lt;/a&gt;&lt;/p&gt;</description>
      <author>martin91</author>
      <pubDate>Fri, 22 May 2015 23:19:50 +0800</pubDate>
      <link>https://ruby-china.org/topics/25711</link>
      <guid>https://ruby-china.org/topics/25711</guid>
    </item>
  </channel>
</rss>
