<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>张天赐的小世界</title>
    <link>https://www.zhangtianci.cn/</link>
    <description>Recent content on 张天赐的小世界</description>
    <generator>Hugo -- 0.151.0</generator>
    <language>zh-cn</language>
    <copyright>本博客的作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可</copyright>
    <lastBuildDate>Sun, 12 Oct 2025 15:12:31 +0800</lastBuildDate>
    <atom:link href="https://www.zhangtianci.cn/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>JDK 17 营销初体验 —— 亚毫秒停顿 ZGC 落地实践</title>
      <link>https://www.zhangtianci.cn/posts/jdk-17-%E8%90%A5%E9%94%80%E5%88%9D%E4%BD%93%E9%AA%8C--%E4%BA%9A%E6%AF%AB%E7%A7%92%E5%81%9C%E9%A1%BF-zgc-%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/</link>
      <pubDate>Tue, 30 Apr 2024 15:51:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/jdk-17-%E8%90%A5%E9%94%80%E5%88%9D%E4%BD%93%E9%AA%8C--%E4%BA%9A%E6%AF%AB%E7%A7%92%E5%81%9C%E9%A1%BF-zgc-%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;自 2014 年发布以来， JDK 8 一直都是相当热门的 JDK 版本。其原因就是对底层数据结构、JVM 性能以及开发体验做了重大升级，得到了开发人员的认可。但距离 JDK 8 发布已经过去了 9 年，那么这 9 年的时间，JDK 做了哪些升级？是否有新的重大特性值得我们尝试？能否解决一些我们现在苦恼的问题？带着这份疑问，我们进行了 JDK 版本的调研与尝试。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>自 2014 年发布以来， JDK 8 一直都是相当热门的 JDK 版本。其原因就是对底层数据结构、JVM 性能以及开发体验做了重大升级，得到了开发人员的认可。但距离 JDK 8 发布已经过去了 9 年，那么这 9 年的时间，JDK 做了哪些升级？是否有新的重大特性值得我们尝试？能否解决一些我们现在苦恼的问题？带着这份疑问，我们进行了 JDK 版本的调研与尝试。</p>
<h1 id="新特性一览">新特性一览</h1>
<p>现如今的 JDK 发布节奏变快，每次新出一个版本，我们就会感叹一下：我还在用 JDK 8，现在都 JDK 9、10、11 …… 21 了？然后就会瞅瞅又多了哪些新特性。有一些新特性很香，但考虑一番还是决定放弃升级。主要原因除了新增特性对我们来说改变不大以外，最重要的就是 JDK 9 带来的模块化（JEP 200），导致我们升级十分困难。</p>
<p>模块化的本意是将 JDK 划分为一组模块，这些模块可以在编译时、构建时和运行时组合成各种配置，主要目标是使实现更容易扩展到小型设备，提高安全性和可维护性，并提高应用程序性能。但付出的代价非常大，最直观的影响就是，一些 JDK 内部类不能访问了。</p>
<p>但是除此之外，并没有太多阻塞升级的问题，后续版本都是一些很香的特性：</p>
<ul>
<li>G1 （JEP 248、JEP 307、JEP 344、JEP 345、JEP 346），提供一个支持指定暂停时间、NUMA 感知内存分配的高性能垃圾回收器</li>
<li>ZGC （JEP 333、JEP 376、JEP 377），一个支持 NUMA，暂停时间不应超过 1ms 的垃圾回收器</li>
<li>并发 API 更新（JEP 266），提供 publish-subscribe 框架，支持响应式流发布 - 订阅框架的接口，以及 CompletableFuture 的进一步完善</li>
<li>集合工厂方法（JEP 269），类似 Guava，支持快速创建有初始元素的集合</li>
<li>新版 HTTP 客户端（JEP 321），一个现代化、支持异步、WebSocket、响应式流的 JDK 内置 API</li>
<li>空指针 NPE 直接给出异常方法位置（JEP 358），以前只给代码行数，不告诉哪个方法，一行多个方法的写法一但出现空指针，全靠程序员上下文分析推理</li>
<li>instanceof 的模式匹配（JEP 394），判断类型后再也不用强转了</li>
<li>数据记录类（JEP 395），一个标准的值聚合类，帮助程序员专注于对不可变数据进行建模，实现数据驱动</li>
<li>Switch 表达式语法改进（JEP 361），改变 Switch 又臭又长，易于出错的现状</li>
<li>文本块（JEP 378），支持二维文本块，而不是像现在一样通过 + 号自行拼接</li>
<li>密封类（JEP 409），提供一种限制进行扩展的语法，超类应该可以被广泛访问（因为它代表了用户的重要抽象），但不能广泛扩展（因为它的子类应该仅限于作者已知的子类）</li>
<li>以及一些未提到的底层数据结构优化，JVM 性能提升……</li>
</ul>
<p>这么多的优点，恰好能解决我们当前遇到的一些问题，因此我们决定进行 JDK 升级。</p>
<h1 id="升级">升级</h1>
<h2 id="升级应用评估">升级应用评估</h2>
<p>首先自然是要考虑要将哪些应用进行升级。我们根据以下条件进行应用筛选：</p>
<ol>
<li>第一，也是最重要的一点，此系统可以通过升级，解决现有问题与瓶颈</li>
<li>第二，有完备的机制能够进行快速回归与验证，如完备的单元测试，自动化测试覆盖能力，便捷的生产压测能力等，底层的升级一定要做好完备的验证</li>
<li>第三，技术债务一定要少，不至于在升级过程中遇到一些必须解决的技术债，给升级增加难度</li>
<li>第四，负责升级的人对这个系统都很了解，除核心业务逻辑外，还能够了解引入了哪些中间件与依赖，使用了中间件的哪些功能，中间件升级后，大量不兼容的改动是否对现有系统造成影响</li>
</ol>
<p>最终我们选取了一个结算页、收银台展示无券支付营销的应用进行升级。此应用特点如下：</p>
<ul>
<li>作为核心链路的应用之一，接口响应时间要求很高，GC 是其耗时抖动的瓶颈之一</li>
<li>业务正在进行快速迭代发展，随着降本增效策略的落地，营销策略进一步精细化，营销种类、数量、范围进一步增加，给系统性能带来更大的挑战</li>
<li>日常流量不低，整点存在突发流量，并且需要承接大促流量</li>
<li>核心链路覆盖了单元测试，测试环境具备自动化回归能力，预发、生产支持常态化压测与生产流量回放</li>
<li>非 Web 应用，仅使用各个中间件的基础功能，升级出现不兼容的问题小</li>
<li>维护了 3 年，经历过多次重构，历史问题较少，几乎没有技术债务</li>
</ul>
<p>针对以上特点，此应用很适合进行 JDK 17 升级。此应用基于 JDK 8，SpringBoot 2.0.8，除常见外部基础组件外，还使用以下公司内部中间件：UMP、SGM、DUCC、CDS、JMQ、JSF、R2M。</p>
<h2 id="升级效果">升级效果</h2>
<p>可以先看下我们升级后压测的效果：</p>
<p>纯计算代码不再受 GC 影响</p>
<p><img alt="纯计算方法耗时.webp" loading="lazy" src="../../images/%E7%BA%AF%E8%AE%A1%E7%AE%97%E6%96%B9%E6%B3%95%E8%80%97%E6%97%B6.webp"></p>
<p>升级前</p>
<p><img alt="升级前 GC.webp" loading="lazy" src="../../images/%E5%8D%87%E7%BA%A7%E5%89%8D%20GC.webp"></p>
<p>升级后</p>
<p><img alt="升级后 GC.webp" loading="lazy" src="../../images/%E5%8D%87%E7%BA%A7%E5%90%8E%20GC.webp"></p>
<p><img loading="lazy" src="https://pic3.zhimg.com/80/v2-a426eebd613c4798343f71727fb11ac2_720w.webp"></p>
<p>升级后吞吐量几乎不受影响（甚至提升 <strong>0.01%</strong>），GC 平均耗时下降 <strong>1405 倍</strong>，GC 最大耗时下降 <strong>1132 倍</strong></p>
<h2 id="升级步骤">升级步骤</h2>
<h3 id="升级-jdk-编译版本">升级 JDK 编译版本</h3>
<p>首先自然是修改 maven 中指定的 JDK 版本，可以先升级到 JDK 11，同时修改 maven 编译插件</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;java.version&gt;</span>11<span class="nt">&lt;/java.version&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;maven-compiler-plugin.version&gt;</span>3.8.1<span class="nt">&lt;/maven-compiler-plugin.version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;maven-source-plugin.version&gt;</span>3.2.1<span class="nt">&lt;/maven-source-plugin.version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;maven-javadoc-plugin.version&gt;</span>3.3.2<span class="nt">&lt;/maven-javadoc-plugin.version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;maven-surefire-plugin.version&gt;</span>2.22.2<span class="nt">&lt;/maven-surefire-plugin.version&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plugin&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>org.apache.maven.plugins<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>maven-compiler-plugin<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>${maven-compiler-plugin.version}<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;configuration&gt;</span>        
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;release&gt;</span>${java.version}<span class="nt">&lt;/release&gt;</span>  
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;encoding&gt;</span>${project.build.sourceEncoding}<span class="nt">&lt;/encoding&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/configuration&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plugin&gt;</span>
</span></span></code></pre></div><h3 id="引入缺少的依赖">引入缺少的依赖</h3>
<p>然后就可以进行本地编译了，此时会暴露一些很简单的问题，比如找不到包、类等等。原因就是 JDK 11 移除了 Java EE and CORBA 的模块，需要手动引入。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c">&lt;!-- JAVAX --&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>javax.annotation<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>javax.annotation-api<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>1.3.1<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>javax.xml.bind<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>jaxb-api<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>2.3.0<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>com.sun.xml.bind<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>jaxb-impl<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>2.3.0<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>com.sun.xml.bind<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>jaxb-core<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>2.3.0<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;groupId&gt;</span>javax.activation<span class="nt">&lt;/groupId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;artifactId&gt;</span>activation<span class="nt">&lt;/artifactId&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;version&gt;</span>1.0.2<span class="nt">&lt;/version&gt;</span>  
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>
</span></span></code></pre></div><h3 id="升级外部中间件">升级外部中间件</h3>
<p>解决了编译找不到类的问题，接下来就该升级依赖的外部中间件了。对于我们的应用来说，也就是升级 SpringBoot 的版本。支持 JDK 17 的版本是 Spring 5.3，对应 SpringBoot 2.5。</p>
<p>在这里我建议升级至 SpringBoot 2.7，从 2.5 升级至 2.7 几乎没有需要改动的地方，同时高版本的 SprngBoot 所约定的依赖，对 JDK 17 的支持也更好。</p>
<p>建议进行大版本逐个升级，比如我们从 2.0 升级至 2.1。每升一个版本，就要仔细观察依赖版本的变化，掌握每个依赖升级的情况。SpringBoot 的升级其实意味着把所有开源组件约定版本进行大版本升级，接口弃用，破坏性兼容更新较多，需要一一鉴别。</p>
<p>下面以升级 Spring Boot 2.1 为例，说明我们升级的步骤：</p>
<ol>
<li>首先阅读 Spring Boot 2.1 做了哪些和我们有关的配置改动</li>
<li>禁用了同 Bean 覆盖，开启需要指定 <code>spring.main.allow-bean-definition-overriding</code> 为 <code>true</code></li>
<li>然后阅读 Spring Boot 2.1 升级了哪些我们用到的依赖
<ul>
<li>Spring 升级至 5.1
<ul>
<li>首先阅读 Spring 5.1 做了哪些和我们有关的配置改动：无影响</li>
<li>然后阅读 Spring 5.1 升级了哪些我们用到的依赖
<ul>
<li>ASM 7.0：同理，阅读升级影响（这种底层依赖的底层依赖，如果仅 ASM 在使用，则无需关心）</li>
<li>CGLIB 3.2：同理，阅读升级影响（这种底层依赖的底层依赖，如果仅 ASM 在使用，则无需关心）</li>
</ul>
</li>
<li>最后阅读 Spring 5.1 弃用了哪些和我们有关的配置与依赖：无影响</li>
</ul>
</li>
<li>Lombok 升级至 1.18
<ul>
<li>阅读改动影响，1.18 Lombok 默认情况下将不再生成私有无参构造函数。可以通过在 <code>lombok.config</code> 配置文件中设置 <code>lombok.noArgsConstructor.extraPrivate=true</code> 来启用它</li>
</ul>
</li>
<li>Hibernate 升级至 5.3：阅读改动影响，对我们项目无影响</li>
<li>JUnit 升级至 5.2：阅读改动影响，需要 Surefire 插件升级至 <code>2.21.0</code> 及以上</li>
</ul>
</li>
<li>最后阅读 Spring Boot 2.1 弃用了哪些和我们有关的配置与依赖</li>
</ol>
<p>至此，Spring Boot 2.1 升级完毕。接下来分析一次依赖树变化，和升级前的依赖树进行比较，查看依赖变化范围是否全部已知可控。完成后进行 Spring Boot 2.2 的升级。</p>
<p>以下为我们需要注意的升级事项，仅供参考：</p>
<ul>
<li>可以先升级到 JDK 11，一边启动一边验证。但不要在 JDK 11 使用 ZGC，ZGC 的堆预留与可用堆的比例太大，有时会导致 OOM</li>
<li>代码中存在同 Bean，启动时 Springboot 2.0 会自动进行覆盖，高版本开启覆盖，需要指定 <code>spring.main.allow-bean-definition-overriding</code> 为 <code>true</code></li>
<li>Spring Boot 2.2 默认的单元测试 Junit 升级至 5，Junit 4 的单元测试建议进行升级，改动不大</li>
<li>Spring Boot 2.4 不再支持 Junit 4 的单元测试，如果需要可以手动引入 Vintage 引擎</li>
<li>Spring Boot 2.4 配置文件处理逻辑变更，注意阅读更新日志</li>
<li>Spring Boot 2.6 默认禁用 Bean 循环依赖，可以通过将 <code>spring.main.allow-circular-references</code> 设置为 <code>true</code> 开启</li>
<li>Spring Boot 2.7 自动配置注册文件变更，<code>spring.factories</code> 中的内容需要移动至 <code>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</code> 文件下</li>
<li><code>spring-boot-properties-migrator</code> 可以识别弃用的属性，可以考虑使用</li>
<li>Spring Framework 5.2 需要 Jackson 2.9.7+，注意阅读更新日志</li>
<li>Spring Framework 5.2 注解检索算法重构，所有自定义注释都必须使用 <code>@Retention(RetentionPolicy.RUNTIME)</code> 进行注释，以便 Spring 能够找到它们</li>
<li>Spring Framework 5.3 修改了很多东西，但都与我们的应用无关，请关注更新日志</li>
<li>ASM 仅单元测试 Mock 在使用，无需特殊关注，做好 JUnit 升级兼容即可</li>
<li>CGLIB 大版本升级以兼容字节码版本为主，关注好变更日志即可</li>
<li>Lombok 即使是小版本升级，也会有破坏性更新，需要仔细阅读每个版本的更新日志，建议少用 Lombok</li>
<li>Hibernate 没有太大的破坏性更新，关注好变更日志即可</li>
<li>JUnit 升级主要关注大版本变更，如 4 升 5，小版本没有特别大的破坏性更新，并且是单元测试使用的依赖，可以放心升级或者不升级</li>
<li>Jackson 2.11，对 <code>java.util.Date</code> 和 <code>java.util.Calendar</code> 默认格式进行了更改，注意查看更新日志进行兼容</li>
<li>注意字节码增强相关依赖的升级</li>
<li>注意本地缓存升级</li>
<li>注意 Netty 升级，关注更新日志</li>
</ul>
<h3 id="升级内部中间件">升级内部中间件</h3>
<p>内部中间件升级较为简单，主要是关注 JMQ、JSF 版本。其中 JSF 依赖的 Netty 和 Javassist 等都需要升级，Netty 版本较低会有内存泄漏问题。</p>
<h3 id="我们使用的依赖版本">我们使用的依赖版本</h3>
<p>给大家参考下我们升级后的依赖版本</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;properties&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- 基础组件版本 Start --&gt;</span>    
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;java.version&gt;</span>17<span class="nt">&lt;/java.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;project.build.sourceEncoding&gt;</span>UTF-8<span class="nt">&lt;/project.build.sourceEncoding&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;maven-compiler-plugin.version&gt;</span>3.11.0<span class="nt">&lt;/maven-compiler-plugin.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;maven-surefire-plugin.version&gt;</span>2.22.2<span class="nt">&lt;/maven-surefire-plugin.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jacoco-maven-plugin-version&gt;</span>0.8.10<span class="nt">&lt;/jacoco-maven-plugin-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;maven-assembly-plugin-version&gt;</span>2.4.1<span class="nt">&lt;/maven-assembly-plugin-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;maven-dependency-plugin-version&gt;</span>3.1.0<span class="nt">&lt;/maven-dependency-plugin-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;profiles.dir&gt;</span>src/main/profiles<span class="nt">&lt;/profiles.dir&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;springboot-version&gt;</span>2.7.13<span class="nt">&lt;/springboot-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;log4j2.version&gt;</span>2.18.0-jdsec.rc2<span class="nt">&lt;/log4j2.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;hibernate-validator.version&gt;</span>5.2.4.Final<span class="nt">&lt;/hibernate-validator.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;collections-version&gt;</span>3.2.2<span class="nt">&lt;/collections-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;collections4.version&gt;</span>4.4<span class="nt">&lt;/collections4.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;netty.old.version&gt;</span>3.9.0.Final<span class="nt">&lt;/netty.old.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;netty.version&gt;</span>4.1.36.Final<span class="nt">&lt;/netty.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;javassist-version&gt;</span>3.29.2-GA<span class="nt">&lt;/javassist-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;guava.version&gt;</span>23.0<span class="nt">&lt;/guava.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;mysql-connector-java.version&gt;</span>5.1.29<span class="nt">&lt;/mysql-connector-java.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jmh-version&gt;</span>1.36<span class="nt">&lt;/jmh-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;caffeine-version&gt;</span>3.1.6<span class="nt">&lt;/caffeine-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;fastjson-version&gt;</span>1.2.83-jdsec.rc1<span class="nt">&lt;/fastjson-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;fastjson2-version&gt;</span>2.0.35<span class="nt">&lt;/fastjson2-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;roaringBitmap.version&gt;</span>0.9.44<span class="nt">&lt;/roaringBitmap.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;disruptor.version&gt;</span>3.4.4<span class="nt">&lt;/disruptor.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jaxb-impl.version&gt;</span>2.3.8<span class="nt">&lt;/jaxb-impl.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jaxb-core.version&gt;</span>2.3.0.1<span class="nt">&lt;/jaxb-core.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;activation.version&gt;</span>1.1.1<span class="nt">&lt;/activation.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- 基础组件版本 End --&gt;</span>  
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- 京东中间件版本 Start --&gt;</span>    
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;ump-version&gt;</span>20221231.1<span class="nt">&lt;/ump-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;ducc.version&gt;</span>1.0.20<span class="nt">&lt;/ducc.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jdcds-driver-alg-version&gt;</span>2.21.1<span class="nt">&lt;/jdcds-driver-alg-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jdcds-driver-version&gt;</span>3.8.3<span class="nt">&lt;/jdcds-driver-version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jmq.version&gt;</span>2.3.3-RC2<span class="nt">&lt;/jmq.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;jsf.version&gt;</span>1.7.6-HOTFIX-T2<span class="nt">&lt;/jsf.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;r2m.version&gt;</span>3.3.4<span class="nt">&lt;/r2m.version&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- 京东中间件版本 End --&gt;</span>  
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/properties&gt;</span>
</span></span></code></pre></div><h3 id="jvm-启动参数升级">JVM 启动参数升级</h3>
<p>远程 DEBUG 参数有所变化：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">JAVA_DEBUG_OPTS</span><span class="o">=</span><span class="s2">&#34; -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 &#34;</span>
</span></span></code></pre></div><p>打印 GC 日志参数的变化，我们在预发环境开启了日志进行观察：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">JAVA_GC_LOG_OPTS</span><span class="o">=</span><span class="s2">&#34; -Xlog:gc*:file=/export/logs/gc.log:time,tid,tags:filecount=10:filesize=10m &#34;</span>
</span></span></code></pre></div><p>使用了 ZGC 的部分 JVM 参数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">JAVA_MEM_OPTS</span><span class="o">=</span><span class="s2">&#34; -server -Xmx12g -Xms12g -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=2048m -XX:+UseZGC -XX:ZAllocationSpikeTolerance=3 -XX:ParallelGCThreads=8 -XX:CICompilerCount=3 -XX:-RestrictContended -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/logs &#34;</span>
</span></span></code></pre></div><p>内部依赖需要访问 JDK 模块，如 UMP、JSF、虫洞、MyBatis、DUCC、R2M、SGM：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$JAVA_VERSION</span><span class="s2">&#34;</span> -ge <span class="m">11</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>  
</span></span><span class="line"><span class="cl">  <span class="nv">SGM_OPTS</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">SGM_OPTS</span><span class="si">}</span><span class="s2"> --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED &#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">UMP_OPT</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/sun.net.util=ALL-UNNAMED &#34;</span> 
</span></span><span class="line"><span class="cl">  <span class="nv">JSF_OPTS</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED&#34;</span>  
</span></span><span class="line"><span class="cl">  <span class="nv">WORMHOLE_OPT</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/sun.security.action=ALL-UNNAMED &#34;</span>  
</span></span><span class="line"><span class="cl">  <span class="nv">MB_OPTS</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/java.lang=ALL-UNNAMED &#34;</span>  
</span></span><span class="line"><span class="cl">  <span class="nv">DUC_OPT</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/java.net=ALL-UNNAMED &#34;</span>  
</span></span><span class="line"><span class="cl">  <span class="nv">R2M_OPT</span><span class="o">=</span><span class="s2">&#34; --add-opens java.base/java.time=ALL-UNNAMED &#34;</span>  
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>启动后完整的启动参数如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">-javaagent:/export/package/sgm-probe-java/sgm-probe-5.9.5-product/sgm-agent-5.9.5.jar -Dsgm.server.address<span class="o">=</span>http://sgm.jdfin.local -Dsgm.app.name<span class="o">=</span>market-reduction-center -Dsgm.agent.sink.http.connection.requestTimeout<span class="o">=</span><span class="m">2000</span> -Dsgm.agent.sink.http.connection.connectTimeout<span class="o">=</span><span class="m">2000</span> -Dsgm.agent.sink.http.minAlive<span class="o">=</span><span class="m">1</span> -Dsgm.agent.virgo.address<span class="o">=</span>10.24.216.198:8999,10.223.182.52:8999,10.25.217.95:8999 -Dsgm.agent.zone<span class="o">=</span>m6 -Dsgm.agent.group<span class="o">=</span>m6-discount -Dsgm.agent.tenant<span class="o">=</span>jdjr -Dsgm.deployment.platform<span class="o">=</span>jdt-jdos --add-opens<span class="o">=</span>jdk.management/com.sun.management.internal<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.management/sun.management<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.management/java.lang.management<span class="o">=</span>ALL-UNNAMED -DJDOS_DATACENTER<span class="o">=</span>JXQ -Ddeploy.app.name<span class="o">=</span>jdos_kj_market-reduction-center -Ddeploy.app.id<span class="o">=</span><span class="m">30005051</span> -Ddeploy.instance.id<span class="o">=</span><span class="m">0</span> -Ddeploy.instance.name<span class="o">=</span>server -Djava.awt.headless<span class="o">=</span><span class="nb">true</span> -Djava.net.preferIPv4Stack<span class="o">=</span><span class="nb">true</span> -Djava.util.Arrays.useLegacyMergeSort<span class="o">=</span><span class="nb">true</span> -Dog4j2.contextSelector<span class="o">=</span>org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.AsyncQueueFullPolicy<span class="o">=</span>Discard -Xmx12g -Xms12g -XX:MaxMetaspaceSize<span class="o">=</span>256m -XX:MetaspaceSize<span class="o">=</span>256m -XX:MaxDirectMemorySize<span class="o">=</span>2048m -XX:+UseZGC -XX:ZAllocationSpikeTolerance<span class="o">=</span><span class="m">3</span> -XX:ParallelGCThreads<span class="o">=</span><span class="m">8</span> -XX:CICompilerCount<span class="o">=</span><span class="m">3</span> -XX:-RestrictContended -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath<span class="o">=</span>/export/logs --add-opens<span class="o">=</span>java.base/sun.net.util<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/sun.util.calendar<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/java.util<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/java.math<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/sun.security.action<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/java.lang<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/java.net<span class="o">=</span>ALL-UNNAMED --add-opens<span class="o">=</span>java.base/java.time<span class="o">=</span>ALL-UNNAMED -Dloader.path<span class="o">=</span>/export/package/jdos_kj_market-reduction-center/conf
</span></span></code></pre></div><h3 id="系统验证">系统验证</h3>
<p>系统可以成功启动后，就可以进行功能验证。有几个验证重点与方法：</p>
<ul>
<li>首先可以通过单元测试快速进行系统全面回归，避免出现 JDK API、中间件 API 变更导致的业务异常</li>
<li>部署到测试环境，验证各个中间件是否正常，如 DUCC 开关下发，MQ 收发，JSF 接口调用等等，系统中所有用到的中间件都需要一一验证</li>
<li>然后可以开始进行核心业务的验证，这时候可以利用测试同学的测试自动化能力加人工补充场景，快速进行核心业务回归。其中研发需要观察系统被调用时的所有异常日志，包括警告，明确每条日志产生的原因</li>
<li>验证完成后，可以部署到联调环境，利用外部同事联调时的请求进一步进行验证</li>
<li>充分在测试环境观察后，部署至预发环境，利用外部同事联调时的请求进一步进行验证，并进行常态化压测，验证优化效果与瓶颈</li>
<li>经过预发长时间验证，没有问题后，部署一台生产，通过回放生产流量进一步进行验证</li>
<li>回放流量无异常后，开始承接生产流量，按接口开量，进行若干周的观察</li>
<li>逐步切量，直到全量上线</li>
</ul>
<h1 id="gc-调优">GC 调优</h1>
<h2 id="zgc-介绍">ZGC 介绍</h2>
<p><img loading="lazy" src="https://pic4.zhimg.com/80/v2-d49ad8b7fdf954aad204e1500743593b_720w.webp"></p>
<p><img loading="lazy" src="https://pic2.zhimg.com/80/v2-a4f600dcd600e397eea0af55f2763ca9_720w.webp"></p>
<p><img loading="lazy" src="https://pic4.zhimg.com/80/v2-af7db8c6efc82b503016a0a276e81737_720w.webp"></p>
<p>如图所示，ZGC 的定位是一个最大暂停时间小于 1ms，且能够处理大小从 8MB 到 16TB 的堆，并且易于调优的垃圾回收器。ZGC 只有三个 STW 阶段，具体流程网上有大量类似文章，这里不做详细介绍。</p>
<h2 id="优化方向">优化方向</h2>
<p>目前我们的应用日常使用 G1 约 30ms 的 GC 停顿时间，不到 1 分钟就会触发一次，大促时频率更高，暂停时间更长，导致接口性能波动较大。随着业务发展，为了优化系统我们大量应用了本地缓存，导致存活对象较多。ZGC 暂停时间不随堆、活动集或根集大小而增加，且极低的 GC 时间正是我们需要的特性，因此决定使用 ZGC。</p>
<p>ZGC 作为一个现代化 GC，没有必要做过多的优化，默认配置已经可以解决 99.9% 的场景。但是我们的应用会承接大促流量，根据观察，瞬时流量激增时 GC 时机较晚，因此应对突发流量是我们 ZGC 调优的一个目标，其他属性不做任何调整。</p>
<h2 id="优化措施">优化措施</h2>
<p>ZGC 的一个优化措施就是足够大的堆，一般来说，给 ZGC 的内存越多越好，但我们也没必要浪费，通过压测观察 GC 日志，取得一个合适的值即可。我们只要保证：</p>
<ol>
<li>堆可以容纳应用程序产生的实时垃圾</li>
<li>堆中有足够的空间，以便在 GC 运行时，为新的垃圾分配提供空间</li>
</ol>
<p>因此，我们将机器升级成 8C 16G 配置，观察 GC 日志根据应用情况调整内存占用配置，最终设定为 <code>-Xmx12g -Xms12g -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=2048m</code>，提升 ZGC 的效果。</p>
<p>剩下的其他优化措施则视情况而定，可以调整触发 GC 的时机，也可以改为基于固定时间间隔触发 GC。</p>
<p>我们略微提升了触发时机，<code>-XX:ZAllocationSpikeTolerance=3</code>（默认为 2）应对突发流量。</p>
<p><code>CICompilerCount ParallelGCThreads</code> 一个是提升 JIT 编译速度，一个是垃圾收集器并行阶段使用的线程数，根据实际情况略微增加，牺牲一点点 CPU 使用率，提升下效率。</p>
<p>另外还可以开启 <code>Large Pages</code> 进一步提升性能。这一步我们没有做，因为现在部署方式为一台物理机 Docker 混部署。开启需要修改内核，影响宿主机的其他镜像。</p>
<h1 id="总结">总结</h1>
<p>至此，调优完成，目前我们已在线上跑了一个多月，每周都有三次常态化压测，一切正常。</p>
<p>以上升级心得分享给大家，希望对各位有所帮助。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Java CompletableFuture 异步超时实现探索</title>
      <link>https://www.zhangtianci.cn/posts/java-completablefuture-%E5%BC%82%E6%AD%A5%E8%B6%85%E6%97%B6%E5%AE%9E%E7%8E%B0%E6%8E%A2%E7%B4%A2/</link>
      <pubDate>Mon, 22 Jan 2024 21:11:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/java-completablefuture-%E5%BC%82%E6%AD%A5%E8%B6%85%E6%97%B6%E5%AE%9E%E7%8E%B0%E6%8E%A2%E7%B4%A2/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;JDK 8 是一次重大的版本升级，新增了非常多的特性，其中之一便是 &lt;code&gt;CompletableFuture&lt;/code&gt;。自此从 JDK 层面真正意义上的支持了基于事件的异步编程范式，弥补了 &lt;code&gt;Future&lt;/code&gt; 的缺陷。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>JDK 8 是一次重大的版本升级，新增了非常多的特性，其中之一便是 <code>CompletableFuture</code>。自此从 JDK 层面真正意义上的支持了基于事件的异步编程范式，弥补了 <code>Future</code> 的缺陷。</p>
<p>在我们的日常优化中，最常用手段便是多线程并行执行。这时候就会涉及到 <code>CompletableFuture</code> 的使用。</p>
<h1 id="常见使用方式">常见使用方式</h1>
<p>下面举例一个常见场景。</p>
<p>假如我们有两个 RPC 远程调用服务，我们需要获取两个 RPC 的结果后，再进行后续逻辑处理。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 任务 A，耗时 2 秒</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">resultA</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">compute</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 任务 B，耗时 2 秒</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">resultB</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">compute</span><span class="p">(</span><span class="n">2</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 后续业务逻辑处理</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">resultA</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">resultB</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>可以预估到，串行执行最少耗时 4 秒，并且 B 任务并不依赖 A 任务结果。</p>
<p>对于这种场景，我们通常会选择并行的方式优化，Demo 代码如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 仅简单举例，在生产代码中可别这么写！</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 统计耗时的函数</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">time</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">Integer</span><span class="o">&gt;</span><span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Stream</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                  </span><span class="c1">// 创建异步任务</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                  </span><span class="p">.</span><span class="na">map</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">supplyAsync</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">compute</span><span class="p">(</span><span class="n">x</span><span class="p">),</span><span class="w"> </span><span class="n">executor</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                  </span><span class="c1">// 聚合</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                  </span><span class="p">.</span><span class="na">reduce</span><span class="p">(</span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">completedFuture</span><span class="p">(</span><span class="n">0</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="na">thenCombineAsync</span><span class="p">(</span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">Integer</span><span class="p">::</span><span class="n">sum</span><span class="p">,</span><span class="w"> </span><span class="n">executor</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 等待结果</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;结果：&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">result</span><span class="p">.</span><span class="na">get</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">ExecutionException</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">System</span><span class="p">.</span><span class="na">err</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;任务执行异常&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>输出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[async-1]: 任务执行开始：1
</span></span><span class="line"><span class="cl">[async-2]: 任务执行开始：2
</span></span><span class="line"><span class="cl">[async-1]: 任务执行完成：1
</span></span><span class="line"><span class="cl">[async-2]: 任务执行完成：2
</span></span><span class="line"><span class="cl">结果：3
</span></span><span class="line"><span class="cl">耗时：2 秒
</span></span></code></pre></div><p>可以看到耗时变成了 2 秒。</p>
<h1 id="存在的问题">存在的问题</h1>
<h2 id="分析">分析</h2>
<p>看上去 <code>CompletableFuture</code> 现有功能可以满足我们诉求。但当我们引入一些现实常见情况时，一些潜在的不足便暴露出来了。</p>
<p><code>compute(x)</code> 如果是一个根据入参查询用户某类型优惠券列表的任务，我们需要查询两种优惠券并组合在一起返回给上游。假如上游要求我们 2 秒内处理完毕并返回结果，但 <code>compute(x)</code> 耗时却在 0.5 秒 ~ 无穷大波动。这时候我们就需要把耗时过长的 <code>compute(x)</code> 任务结果放弃，仅处理在指定时间内完成的任务，尽可能保证服务可用。</p>
<p>那么以上代码的耗时由耗时最长的服务决定，无法满足现有诉求。通常我们会使用 <code>get(long timeout, TimeUnit unit)</code> 来指定获取结果的超时时间，并且我们会给 <code>compute(x)</code> 设置一个超时时间，达到后自动抛异常来中断任务。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 仅简单举例，在生产代码中可别这么写！</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 统计耗时的函数</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">time</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">List</span><span class="o">&lt;</span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">Integer</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Stream</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                        </span><span class="c1">// 创建异步任务，compute(x) 超时抛出异常</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                        </span><span class="p">.</span><span class="na">map</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">supplyAsync</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">compute</span><span class="p">(</span><span class="n">x</span><span class="p">),</span><span class="w"> </span><span class="n">executor</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                                        </span><span class="p">.</span><span class="na">toList</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 等待结果</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kt">int</span><span class="w"> </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">Integer</span><span class="o">&gt;</span><span class="w"> </span><span class="n">future</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">result</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">res</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">future</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">2</span><span class="p">,</span><span class="w"> </span><span class="n">SECONDS</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">ExecutionException</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">TimeoutException</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">System</span><span class="p">.</span><span class="na">err</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;任务执行异常或超时&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;结果：&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">res</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>输出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[async-2]: 任务执行开始：2
</span></span><span class="line"><span class="cl">[async-1]: 任务执行开始：1
</span></span><span class="line"><span class="cl">[async-1]: 任务执行完成：1
</span></span><span class="line"><span class="cl">任务执行异常或超时
</span></span><span class="line"><span class="cl">结果：1
</span></span><span class="line"><span class="cl">耗时：2 秒
</span></span></code></pre></div><p>可以看到，只要我们能够给 <code>compute(x)</code> 设置一个超时时间将任务中断，结合 <code>get</code>、<code>getNow</code> 等获取结果的方式，就可以很好地管理整体耗时。</p>
<p>那么问题也就转变成了，如何给任务设置异步超时时间呢？</p>
<h2 id="现有做法">现有做法</h2>
<p>当异步任务是一个 RPC 请求时，我们可以设置一个 JSF（类似 Dubbo 的中间件） 超时，以达到异步超时效果。</p>
<p>当请求是一个 R2M（类似 Jedis 的中间件） 请求时，我们也可以控制 R2M 连接的最大超时时间来达到效果。</p>
<p>这么看好像我们都是在依赖三方中间件的能力来管理任务超时时间？那么就存在一个问题，中间件超时控制能力有限，如果异步任务是中间件 IO 操作 + 本地计算操作怎么办？</p>
<p>用 JSF 超时举一个具体的例子，反编译 JSF 的获取结果代码如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">V</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="kt">long</span><span class="w"> </span><span class="n">timeout</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 配置的超时时间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">unit</span><span class="p">.</span><span class="na">toMillis</span><span class="p">(</span><span class="n">timeout</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 剩余等待时间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">long</span><span class="w"> </span><span class="n">remaintime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">timeout</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">sentTime</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">genTime</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">remaintime</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="n">0L</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// 反序列化获取结果</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">getNow</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="na">await</span><span class="p">(</span><span class="n">remaintime</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">MILLISECONDS</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 等待时间内任务完成，反序列化获取结果</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">getNow</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="na">setDoneTime</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 超时抛出异常</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">throw</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">clientTimeoutException</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>当这个任务刚好卡在超时边缘完成时，这个任务的耗时时间就变成了超时时间 + 获取结果时间。而获取结果（反序列化）作为纯本地计算操作，耗时长短受 CPU 影响较大。</p>
<p>某些 CPU 使用率高的情况下，就会出现异步任务没能触发抛出异常中断，导致我们无法准确控制超时时间。对上游来说，本次请求全部失败。</p>
<h1 id="解决方式">解决方式</h1>
<h2 id="jdk-9">JDK 9</h2>
<p>这类问题非常常见，如大促场景，服务器 CPU 瞬间升高就会出现以上问题。</p>
<p>那么如何解决呢？其实 JDK 的开发大佬们早有研究。在 JDK 9，<code>CompletableFuture</code> 正式提供了 <code>orTimeout</code>、<code>completeTimeout</code> 方法，来准确实现异步超时控制。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">orTimeout</span><span class="p">(</span><span class="kt">long</span><span class="w"> </span><span class="n">timeout</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">unit</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">NullPointerException</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">result</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">whenComplete</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Canceller</span><span class="p">(</span><span class="n">Delayer</span><span class="p">.</span><span class="na">delay</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Timeout</span><span class="p">(</span><span class="k">this</span><span class="p">),</span><span class="w"> </span><span class="n">timeout</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">)));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>JDK 9 <code>orTimeout</code> 其实现原理是通过一个定时任务，在给定时间之后抛出异常。如果任务在指定时间内完成，则取消抛异常的操作。</p>
<p>以上代码我们按执行顺序来看下：</p>
<p>首先执行 <code>new Timeout(this)</code>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Timeout</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">Runnable</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">f</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Timeout</span><span class="p">(</span><span class="n">CompletableFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">f</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">f</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">f</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">run</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">f</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="n">f</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// 抛出超时异常</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">f</span><span class="p">.</span><span class="na">completeExceptionally</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">TimeoutException</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>通过源码可以看到，<code>Timeout</code> 是一个实现 Runnable 的类，<code>run()</code> 方法负责给传入的异步任务通过 <code>completeExceptionally</code> CAS 赋值异常，将任务标记为异常完成。</p>
<p>那么谁来触发这个 <code>run()</code> 方法呢？我们看下 <code>Delayer</code> 的实现。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Delayer</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="n">ScheduledFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">delay</span><span class="p">(</span><span class="n">Runnable</span><span class="w"> </span><span class="n">command</span><span class="p">,</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">delay</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                    </span><span class="n">TimeUnit</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 到时间触发 command 任务</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">delayer</span><span class="p">.</span><span class="na">schedule</span><span class="p">(</span><span class="n">command</span><span class="p">,</span><span class="w"> </span><span class="n">delay</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">DaemonThreadFactory</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">ThreadFactory</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">public</span><span class="w"> </span><span class="n">Thread</span><span class="w"> </span><span class="nf">newThread</span><span class="p">(</span><span class="n">Runnable</span><span class="w"> </span><span class="n">r</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">Thread</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(</span><span class="n">r</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">t</span><span class="p">.</span><span class="na">setDaemon</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">t</span><span class="p">.</span><span class="na">setName</span><span class="p">(</span><span class="s">&#34;CompletableFutureDelayScheduler&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">ScheduledThreadPoolExecutor</span><span class="w"> </span><span class="n">delayer</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">(</span><span class="n">delayer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ScheduledThreadPoolExecutor</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">DaemonThreadFactory</span><span class="p">())).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">setRemoveOnCancelPolicy</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><code>Delayer</code> 其实就是一个单例定时调度器，<code>Delayer.delay(new Timeout(this), timeout, unit)</code> 通过 <code>ScheduledThreadPoolExecutor</code> 实现指定时间后触发 <code>Timeout</code> 的 <code>run()</code> 方法。</p>
<p>到这里就已经实现了超时抛出异常的操作。但当任务完成时，就没必要触发 <code>Timeout</code> 了。因此我们还需要实现一个取消逻辑。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Canceller</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">BiConsumer</span><span class="o">&lt;</span><span class="n">Object</span><span class="p">,</span><span class="w"> </span><span class="n">Throwable</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="n">Future</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">f</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Canceller</span><span class="p">(</span><span class="n">Future</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">f</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">f</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">f</span><span class="p">;</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">accept</span><span class="p">(</span><span class="n">Object</span><span class="w"> </span><span class="n">ignore</span><span class="p">,</span><span class="w"> </span><span class="n">Throwable</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">ex</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">f</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="n">f</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 3 未触发抛异常任务则取消</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">f</span><span class="p">.</span><span class="na">cancel</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>当任务执行完成，或者任务执行异常时，我们也就没必要抛出超时异常了。因此我们可以把 <code>delayer.schedule(command, delay, unit)</code> 返回的定时超时任务取消，不再触发 <code>Timeout</code>。 当我们的异步任务完成，并且定时超时任务未完成的时候，就是我们取消的时机。因此我们可以通过 <code>whenComplete(BiConsumer&lt;? super T, ? super Throwable&gt; action)</code> 来完成。</p>
<p><code>Canceller</code> 就是一个 <code>BiConsumer</code> 的实现。其持有了 <code>delayer.schedule(command, delay, unit)</code> 返回的定时超时任务，<code>accept(Object ignore, Throwable ex)</code> 实现了定时超时任务未完成后，执行 <code>cancel(boolean mayInterruptIfRunning)</code> 取消任务的操作。</p>
<h2 id="jdk-8">JDK 8</h2>
<p>如果我们使用的是 JDK 9 或以上，我们可以直接用 JDK 的实现来完成异步超时操作。那么 JDK 8 怎么办呢？</p>
<p>其实我们也可以根据上述逻辑简单实现一个工具类来辅助。</p>
<p>以下是我们营销自己的工具类以及用法，贴出来给大家作为参考，大家也可以自己写的更优雅一些~</p>
<p>调用方式：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">CompletableFutureExpandUtils</span><span class="p">.</span><span class="na">orTimeout</span><span class="p">(</span><span class="n">异步任务</span><span class="p">,</span><span class="w"> </span><span class="n">超时时间</span><span class="p">,</span><span class="w"> </span><span class="n">时间单位</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>工具类源码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nn">com.jd.jr.market.reduction.util</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">com.jdpay.market.common.exception.UncheckedException</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">java.util.concurrent.*</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">java.util.function.BiConsumer</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * CompletableFuture 扩展工具
</span></span></span><span class="line"><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @author zhangtianci7
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">CompletableFutureExpandUtils</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm">     * 如果在给定超时之前未完成，则异常完成此 CompletableFuture 并抛出 {@link TimeoutException} 。
</span></span></span><span class="line"><span class="cl"><span class="cm">     *
</span></span></span><span class="line"><span class="cl"><span class="cm">     * @param timeout 在出现 TimeoutException 异常完成之前等待多长时间，以 {@code unit} 为单位
</span></span></span><span class="line"><span class="cl"><span class="cm">     * @param unit    一个 {@link TimeUnit}，结合 {@code timeout} 参数，表示给定粒度单位的持续时间
</span></span></span><span class="line"><span class="cl"><span class="cm">     * @return 入参的 CompletableFuture
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">orTimeout</span><span class="p">(</span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="w"> </span><span class="n">future</span><span class="p">,</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">timeout</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kc">null</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">UncheckedException</span><span class="p">(</span><span class="s">&#34;时间的给定粒度不能为空&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kc">null</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">future</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">UncheckedException</span><span class="p">(</span><span class="s">&#34;异步任务不能为空&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">future</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">future</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">future</span><span class="p">.</span><span class="na">whenComplete</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Canceller</span><span class="p">(</span><span class="n">Delayer</span><span class="p">.</span><span class="na">delay</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Timeout</span><span class="p">(</span><span class="n">future</span><span class="p">),</span><span class="w"> </span><span class="n">timeout</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">)));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm">     * 超时时异常完成的操作
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Timeout</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">Runnable</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">final</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">future</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Timeout</span><span class="p">(</span><span class="n">CompletableFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">future</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">this</span><span class="p">.</span><span class="na">future</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">future</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">run</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kc">null</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="n">future</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="n">future</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">future</span><span class="p">.</span><span class="na">completeExceptionally</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">TimeoutException</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm">     * 取消不需要的超时的操作
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Canceller</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">BiConsumer</span><span class="o">&lt;</span><span class="n">Object</span><span class="p">,</span><span class="w"> </span><span class="n">Throwable</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">final</span><span class="w"> </span><span class="n">Future</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">future</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Canceller</span><span class="p">(</span><span class="n">Future</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">future</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">this</span><span class="p">.</span><span class="na">future</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">future</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">accept</span><span class="p">(</span><span class="n">Object</span><span class="w"> </span><span class="n">ignore</span><span class="p">,</span><span class="w"> </span><span class="n">Throwable</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kc">null</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">ex</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="n">future</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="n">future</span><span class="p">.</span><span class="na">isDone</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">future</span><span class="p">.</span><span class="na">cancel</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm">     * 单例延迟调度器，仅用于启动和取消任务，一个线程就足够
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Delayer</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">static</span><span class="w"> </span><span class="n">ScheduledFuture</span><span class="o">&lt;?&gt;</span><span class="w"> </span><span class="n">delay</span><span class="p">(</span><span class="n">Runnable</span><span class="w"> </span><span class="n">command</span><span class="p">,</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">delay</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="w"> </span><span class="n">unit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">delayer</span><span class="p">.</span><span class="na">schedule</span><span class="p">(</span><span class="n">command</span><span class="p">,</span><span class="w"> </span><span class="n">delay</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">DaemonThreadFactory</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">ThreadFactory</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kd">public</span><span class="w"> </span><span class="n">Thread</span><span class="w"> </span><span class="nf">newThread</span><span class="p">(</span><span class="n">Runnable</span><span class="w"> </span><span class="n">r</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">Thread</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(</span><span class="n">r</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">t</span><span class="p">.</span><span class="na">setDaemon</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">t</span><span class="p">.</span><span class="na">setName</span><span class="p">(</span><span class="s">&#34;CompletableFutureExpandUtilsDelayScheduler&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">return</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">ScheduledThreadPoolExecutor</span><span class="w"> </span><span class="n">delayer</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">static</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">delayer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ScheduledThreadPoolExecutor</span><span class="p">(</span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">DaemonThreadFactory</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">delayer</span><span class="p">.</span><span class="na">setRemoveOnCancelPolicy</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="参考资料">参考资料</h2>
<ol>
<li><a href="https://link.zhihu.com/?target=https://bugs.openjdk.org/browse/JDK-8132960">JEP 266: JDK 9 并发包更新提案</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>大学毕业租房攻略</title>
      <link>https://www.zhangtianci.cn/posts/%E5%A4%A7%E5%AD%A6%E6%AF%95%E4%B8%9A%E7%A7%9F%E6%88%BF%E6%94%BB%E7%95%A5/</link>
      <pubDate>Sun, 26 Sep 2021 22:34:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/%E5%A4%A7%E5%AD%A6%E6%AF%95%E4%B8%9A%E7%A7%9F%E6%88%BF%E6%94%BB%E7%95%A5/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;来北京快要一年了，现在回想起这一年来还是有点感慨。其中比较遗憾的是房子租的不太满意，踩了不少坑。&lt;/p&gt;
&lt;p&gt;最近决定房子到期后不再续租，租一个新的房子。正好整理一下租房时需要注意的坑。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>来北京快要一年了，现在回想起这一年来还是有点感慨。其中比较遗憾的是房子租的不太满意，踩了不少坑。</p>
<p>最近决定房子到期后不再续租，租一个新的房子。正好整理一下租房时需要注意的坑。</p>
<h1 id="租房注意事项">租房注意事项</h1>
<h2 id="合租室友">合租室友</h2>
<p>为什么这一条单独放在第一个说？因为一个不好的室友，再好的房子也会让你痛苦不堪。</p>
<p>一定要确认以下内容：</p>
<ol>
<li>室友不是无业游民；</li>
<li>室友作息时间基本和你一致；</li>
<li>室友卫生情况你能不能接受（可以观察她用的卫生间、厨房及其房间的卫生情况判断）；</li>
<li>性别最好是同性；</li>
</ol>
<p>这里说一下第四条，生活习惯、生理特性的不同会让上班本来就很疲惫的你雪上加霜。</p>
<p>女生头发较长，掉在地上很明显。那就意味着想和男生保持一样的整洁度，女生要更勤快一点（男的正在秃顶过程中除外）。卫生间女生需要摆一个垃圾桶，男生不用。女生喜欢在房间弄些香香的东西，男生习惯保持原味。</p>
<h2 id="费用">费用</h2>
<h3 id="房租">房租</h3>
<p>首先当然要考虑清楚自己能有多少预算用来租房子。</p>
<p>我认为，房租占工资的 20% ~ 35% 左右都可接受。比如刚毕业的大学生来北漂，假如一个月 6000 的实习工资，那可以考虑租 1200 ~ 2100 的房子。房租高过这个范围那就有点超过你的承受能力了，要慎重考虑。要给自己留下吃饭、生病、同事聚会、出去玩耍和买买买等等的预算，小心变成一个月光族。</p>
<h3 id="生活费用">生活费用</h3>
<p>除房租外，还要考虑水、电、网、燃气、暖气和物业费。</p>
<p>其中常见的是水、电、网和燃气自付，暖气和物业费房东出。要注意商水商电可比你想象的贵。尤其是夏天开空调，舍友是个无业游民并且半夜不睡觉，会比民水民电贵 3 ~ 4 倍。</p>
<h3 id="中介费">中介费</h3>
<p>这个费用的坑就比较多了，大多数中介是收一个月房租作为中介费。</p>
<p>万万不可在签合同前交出任何看房、中介等等各种费用。万一在看房途中觉得房租不好，钱大概率是要不回来的。</p>
<p>建议在来北京前，可以问问公司同事房源、转租信息。这是免中介费的好方法。</p>
<h2 id="设施">设施</h2>
<p>看房时，讲究就比较多了，要留意的地方也比较多。</p>
<h3 id="房屋结构">房屋结构</h3>
<p>需确认是否朝阳，阳台是否方便晒洗衣服，隔音情况如何，通风情况如何，插座有几个，插座位置是否合适。</p>
<p>窗户朝北冬天较冷，朝西夏天太晒，朝东下午太暗，结合上班时间考虑，需要注意自己能接受哪种。</p>
<h3 id="电器">电器</h3>
<p>是否有空调、洗衣机、冰箱等必备电器，是否有宽带，要检查上述电器及网络能否正常使用。</p>
<h3 id="屋内设施">屋内设施</h3>
<p>衣柜，桌椅，窗户，暖气片，沙发，床垫，床板，地砖，门是否正常和老旧。</p>
<p>要重点留意卧室窗户是否漏风，不严实会导致冬冷夏热，空调会更费电。</p>
<h3 id="洗漱间">洗漱间</h3>
<p>卫浴太阳能、热水 24 h，检查是否老旧，淋浴头和水龙头等等会不会漏水，地漏是否可以正常排水，马桶水压、流速和流量足不足。</p>
<h3 id="其他">其他</h3>
<p>在室内的手机信号如何，宽带网速快不快，稳定不稳定，如果是顶楼房顶会不会漏水，5 楼及以上的楼层有没有电梯。</p>
<h2 id="周边环境">周边环境</h2>
<p>小区周围有没有地铁、公交、饭店、药店、商场超市、菜市场、公园等等。</p>
<h2 id="看房">看房</h2>
<p>再次注意，没有签合同不交任何费用，不要隔断间。</p>
<p>最好看两次房子，白天看采光和交通，晚上看隔音、照明和小区安全性。</p>
<h2 id="合同">合同</h2>
<p>注意，签合同是最后一步，要确保上面得内容全部一一确认自己可以接受。</p>
<p>合同一定要仔细看，别不好意思。</p>
<p>下面事项请考虑周全：</p>
<ol>
<li>合约一年还是半年，整租还是合租；</li>
<li>合同上写明房东、二房东、承租人的姓名和身份证号码；</li>
<li>要写明房屋的地址，面积，房产证编号（签约时务必要求出示房产证）；</li>
<li>要查看房东房产证，身份证复印件，二房东身份证复印件，与房东签订的委托书或租房合同复印件和营业执照复印件并留存；（要确认是否允许二次出租转租分租）</li>
<li>签约后再交钱，并要开收据，允许以后用公积金租房；</li>
<li>清点好家具电器板凳数量，哪里损坏，新旧程度等，并签字；</li>
<li>写明水电天然气数字，避免争议；</li>
<li>电器非人为故障谁负责修，明确后续老化等问题的责任与维修期限；</li>
<li>提前终止合同如何赔偿；</li>
<li>押金退还日期，扣除事项；</li>
<li>是否可以贴墙纸，钉钉子；</li>
<li>所有姓名全部都为真名，如果房产证上有多人，那么签约时尽量要求全部到场，如不能到场，需要房产所有人出示授权书；</li>
<li>审核房产证明，并要求房东身份证复印件一份供租客留底。基于公平，租客也可给房东留底身份证复印件一份。因为缺少了中介环节，所以一定要房东将身份证复印件留下来，以防万一。除上述之外，无论是哪种签约方式，还要注意两个共同的问题：不要向没有房东转租许可的二房东租房。没签约之前，绝不能交房租。交押金之后，一定要房东开具收据。</li>
<li>谈租房条件的时候，别忘了说清楚电器非人为故障由谁负责维修，一般房东都会答应负责维修费，谈好请写进合同。明确后续老化等问题的责任，必要时录像、录音。</li>
</ol>
<h1 id="最后">最后</h1>
<p>以上内容其实难以一一做到，能做到一大半就可以租一个满意的房子了。</p>
<p>普通人没那么坏，房子的优缺点不会特意瞒着你，欺骗你。只是自己要留一个心眼，我们不去害人，但也要保证自己的利益。</p>
]]></content:encoded>
    </item>
    <item>
      <title>SQL 语句执行缓慢原因分析</title>
      <link>https://www.zhangtianci.cn/posts/sql-%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E7%BC%93%E6%85%A2%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90/</link>
      <pubDate>Sun, 05 Sep 2021 16:17:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/sql-%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E7%BC%93%E6%85%A2%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90/</guid>
      <description>&lt;h1 id=&#34;概述&#34;&gt;概述&lt;/h1&gt;
&lt;p&gt;SQL 查询性能优化是软件开发的核心技能之一。本文系统性地分析了 SQL 执行缓慢的各种原因，并提供了相应的诊断方法和解决方案。&lt;/p&gt;
&lt;h1 id=&#34;分类讨论&#34;&gt;分类讨论&lt;/h1&gt;
&lt;p&gt;SQL 执行缓慢可分为两种基本情况：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="概述">概述</h1>
<p>SQL 查询性能优化是软件开发的核心技能之一。本文系统性地分析了 SQL 执行缓慢的各种原因，并提供了相应的诊断方法和解决方案。</p>
<h1 id="分类讨论">分类讨论</h1>
<p>SQL 执行缓慢可分为两种基本情况：</p>
<ol>
<li>偶发性缓慢：大多数情况正常，偶尔出现性能问题</li>
<li>持续性缓慢：在数据量不变的情况下，一直执行缓慢</li>
</ol>
<p>针对这两种情况，我们来分析下可能是哪些原因导致的。</p>
<h1 id="针对偶尔很慢的情况">针对偶尔很慢的情况</h1>
<p>一条 SQL 大多数情况正常，偶尔才能出现很慢的情况，我觉得这条 SQL 语句的书写本身是没什么问题，而是其他原因导致。</p>
<h2 id="数据库在刷新脏页">数据库在刷新脏页</h2>
<p>当我们要往数据库插入一条数据、或者要更新一条数据的时候，我们知道数据库会在<strong>缓冲池</strong>中把对应字段的数据更新，但是更新之后，这些更新的字段并不会马上同步持久化到<strong>磁盘</strong>中去，而是把这些更新的记录写入到 <code>redo log</code> 中去。等到空闲的时候，在通过 <code>redo log</code> 里的记录把最新的数据同步到<strong>磁盘</strong>中去。</p>
<p>不过，<code>redo log</code> 里的容量是有限的。如果数据库一直很忙，更新又很频繁，这个时候 <code>redo log</code> 很快就会被写满，就没办法等到空闲的时候再把数据同步到磁盘，只能暂停其他操作，全身心来把数据同步到磁盘中去。而这个时候，<strong>就会导致我们平时正常的 SQL 语句突然执行的很慢</strong>。</p>
<h3 id="机制说明">机制说明</h3>
<ul>
<li>数据库使用缓冲池（Buffer Pool）在内存中缓存数据页</li>
<li>数据修改先在内存中完成，异步写入磁盘（Write-Ahead Logging）</li>
<li><code>Redo Log</code> 确保事务持久性，但容量有限</li>
</ul>
<h3 id="性能影响场景">性能影响场景</h3>
<ul>
<li><code>Redo Log</code> 写满时，必须强制刷脏页到磁盘</li>
<li>缓冲池空间不足，需要淘汰脏页</li>
<li>数据库正常关闭或检查点（Checkpoint）触发</li>
</ul>
<h3 id="监控与诊断">监控与诊断</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 查看InnoDB状态（包含缓冲池信息）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">ENGINE</span><span class="w"> </span><span class="n">INNODB</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 监控脏页比例
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Innodb_buffer_pool_pages_dirty&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="锁竞争问题">锁竞争问题</h2>
<p>这个就比较容易想到了。我们要执行的这条语句，刚好这条语句涉及到的表，别人在用，并且加锁。或者表没有加锁，但要使用到的某个一行被加锁。<strong>我们拿不到锁，只能慢慢等待别人释放锁</strong>。</p>
<p>如果要判断是否真的在等待锁，我们可以用 <code>show processlist</code> 这个命令来查看当前的状态。</p>
<h3 id="锁类型分析">锁类型分析</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>锁类型</strong></th>
          <th style="text-align: center"><strong>范围</strong></th>
          <th style="text-align: center"><strong>影响</strong></th>
          <th style="text-align: center"><strong>诊断方法</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">表级锁</td>
          <td style="text-align: center">整个表</td>
          <td style="text-align: center">高并发下严重影响性能</td>
          <td style="text-align: center"><code>SHOW PROCESSLIST</code></td>
      </tr>
      <tr>
          <td style="text-align: center">行级锁</td>
          <td style="text-align: center">单行记录</td>
          <td style="text-align: center">影响特定数据操作</td>
          <td style="text-align: center"><code>SHOW ENGINE INNODB STATUS</code></td>
      </tr>
      <tr>
          <td style="text-align: center">元数据锁</td>
          <td style="text-align: center">表结构变更</td>
          <td style="text-align: center">DDL 操作阻塞查询</td>
          <td style="text-align: center"><code>performance_schema.metadata_locks</code></td>
      </tr>
      <tr>
          <td style="text-align: center">间隙锁</td>
          <td style="text-align: center">索引范围</td>
          <td style="text-align: center">防止幻读，可能过度锁定</td>
          <td style="text-align: center">分析事务隔离级别</td>
      </tr>
  </tbody>
</table>
<h3 id="监控与诊断-1">监控与诊断</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 查看当前连接和锁状态
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">PROCESSLIST</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看InnoDB锁信息（MySQL 5.7+）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">data_locks</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">data_lock_waits</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看等待锁的线程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">innodb_lock_waits</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="其他偶发因素">其他偶发因素</h2>
<p>还有一些其他原因和数据库内部机制，以及所在的的网络、物理机有关，本文不做详细讨论。</p>
<h3 id="系统资源瓶颈">系统资源瓶颈</h3>
<ul>
<li>CPU 瞬时峰值</li>
<li>内存交换 (SWAP) 发生</li>
<li>磁盘 I/O 瓶颈</li>
<li>网络波动</li>
</ul>
<h3 id="数据库内部机制">数据库内部机制</h3>
<ul>
<li>自适应哈希索引 (Adaptive Hash Index) 重建</li>
<li>变更缓冲区 (Change Buffer) 合并</li>
<li>统计信息自动更新</li>
</ul>
<h1 id="针对一直都这么慢的情况">针对一直都这么慢的情况</h1>
<p>下来我们来访分析下第二种情况，我觉得第二种情况的分析才是最重要的。</p>
<p>如果在数据量一样大的情况下，这条 SQL 语句每次都执行的这么慢，那就就要好好考虑下你的 SQL 书写了，下面我们来分析下哪些原因会导致我们的 SQL 语句执行的很不理想。</p>
<p>我们先来假设我们有一个表，表里有下面两个字段,分别是主键 id，和两个普通字段 c 和 d。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">mysql</span><span class="o">&gt;</span><span class="w"> </span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="o">`</span><span class="n">t</span><span class="o">`</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="k">c</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">d</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="没用到索引">没用到索引</h2>
<p>没有用上索引，我觉得这个原因是很多人都能想到的，例如你要查询这条语句</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="字段没有索引">字段没有索引</h3>
<p>刚好你的 c 字段上没有索引，那么抱歉，只能走全表扫描了，你就体验不会索引带来的乐趣了，所以，这回导致这条查询语句很慢。</p>
<p>解决方案：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 分析查询模式后添加索引
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_c</span><span class="w"> </span><span class="p">(</span><span class="k">c</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_c_d</span><span class="w"> </span><span class="p">(</span><span class="k">c</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="p">);</span><span class="w"> </span><span class="c1">-- 复合索引
</span></span></span></code></pre></div><h3 id="没有用索引">没有用索引</h3>
<p>好吧，这个时候你给 c 这个字段加上了索引，然后又查询了一条语句</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我想问这样在查询的时候会用索引查询吗？</p>
<p>答是不会，如果我们在字段的左边做了运算，那么很抱歉，在查询的时候，就不会用上索引了，所以要注意这种<strong>字段上有索引，但由于自己的疏忽，导致系统没有使用索引</strong>的情况。</p>
<p>正确的查询应该如下</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>有人可能会说，右边有运算就能用上索引？难道数据库就不会自动帮我们优化一下，自动把 c - 1=1000 自动转换为 c = 1000+1。</p>
<p>不好意思，确实不会帮你，所以，你要注意了。</p>
<p>如果我们在查询的时候，对字段进行了函数操作，也是会导致没有用上索引的，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="n">pow</span><span class="p">(</span><span class="k">c</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>这里我只是做一个例子，实际上可能并没有 <code>pow(c)</code> 这个函数。其实这个和上面在左边做运算也是很类似的。</p>
<p>所以呢，一条语句执行都很慢的时候，可能是该语句没有用上索引了，不过具体是啥原因导致没有用上索引的呢，你就要会分析了，我上面列举的三个原因，应该是出现的比较多的。</p>
<h3 id="常见索引失效情况">常见索引失效情况</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>失效模式</strong></th>
          <th style="text-align: center"><strong>示例</strong></th>
          <th style="text-align: center"><strong>解决方案</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">左侧运算</td>
          <td style="text-align: center"><code>WHERE c - 1 = 1000</code></td>
          <td style="text-align: center">重写为 <code>WHERE c = 1000 + 1</code></td>
      </tr>
      <tr>
          <td style="text-align: center">函数操作</td>
          <td style="text-align: center"><code>WHERE DATE(create_time) = '2023-01-01'</code></td>
          <td style="text-align: center">使用范围查询 <code>WHERE create_time &gt;= '2023-01-01' AND create_time &lt; '2023-01-02'</code></td>
      </tr>
      <tr>
          <td style="text-align: center">隐式类型转换</td>
          <td style="text-align: center"><code>WHERE string_col = 123</code></td>
          <td style="text-align: center">保持类型一致 <code>WHERE string_col = '123'</code></td>
      </tr>
      <tr>
          <td style="text-align: center">OR 条件不当</td>
          <td style="text-align: center"><code>WHERE c = 100 OR d = 200</code></td>
          <td style="text-align: center">使用 UNION 或分别索引</td>
      </tr>
      <tr>
          <td style="text-align: center">模糊查询前缀</td>
          <td style="text-align: center"><code>WHERE name LIKE '%abc'</code></td>
          <td style="text-align: center">避免前导通配符</td>
      </tr>
  </tbody>
</table>
<h3 id="索引设计原则">索引设计原则</h3>
<ul>
<li>高选择性字段优先建索引</li>
<li>考虑复合索引的字段顺序</li>
<li>避免过度索引（写操作开销）</li>
<li>覆盖索引减少回表</li>
</ul>
<h2 id="数据库选错索引">数据库选错索引</h2>
<p>我们在进行查询操作的时候，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-mysql" data-lang="mysql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">c</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我们知道，主键索引和非主键索引是有区别的，主键索引存放的值是<strong>整行字段的数据</strong>，而非主键索引上存放的值不是整行字段的数据，而且存放<strong>主键字段的值</strong>。</p>
<p>也就是说，我们如果走 c 这个字段的索引的话，最后会查询到对应主键的值，然后，再根据主键的值走主键索引，查询到整行数据返回。</p>
<p>就算你在 c 字段上有索引，系统也并不一定会走 c 这个字段上的索引，而是有可能会直接扫描扫描全表，找出所有符合 100 &lt; c and c &lt; 100000 的数据。</p>
<p>系统在执行这条语句的时候，会进行预测：究竟是走 c 索引扫描的行数少，还是直接扫描全表扫描的行数少呢？显然，扫描行数越少当然越好了，因为扫描行数越少，意味着 I/O 操作的次数越少。</p>
<p>如果是扫描全表的话，那么扫描的次数就是这个表的总行数了，假设为 n；而如果走索引 c 的话，我们通过索引 c 找到主键之后，还得再通过主键索引来找我们整行的数据，也就是说，需要走两次索引。而且，我们也不知道符合 100 c &lt; and c &lt; 10000 这个条件的数据有多少行，万一这个表是全部数据都符合呢？这个时候意味着，走 c 索引不仅扫描的行数是 n，同时还得每行数据走两次索引。</p>
<p><strong>所以系统是有可能走全表扫描而不走索引的。那系统是怎么判断呢？</strong></p>
<h3 id="索引判断原则">索引判断原则</h3>
<p>判断来源于系统的预测，也就是说，如果要走 c 字段索引的话，系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多，它可能就不走索引而直接扫描全表了。</p>
<p>系统是通过<strong>索引的区分度</strong>来判断的，一个索引上不同的值越多，意味着出现相同数值的索引越少，意味着索引的区分度越高。我们也把区分度称之为<strong>基数</strong>，即区分度越高，基数越大。所以呢，基数越大，意味着符合 100 &lt; c and c &lt; 10000 这个条件的行数越少。</p>
<p>所以呢，一个索引的基数越大，意味着走索引查询越有优势。</p>
<p><strong>那么问题来了，怎么知道这个索引的基数呢？</strong></p>
<p>系统当然是不会遍历全部来获得一个索引的基数的，代价太大了，索引系统是通过遍历部分数据，也就是通过<strong>采样</strong>的方式，来预测索引的基数的。</p>
<p><strong>扯了这么多，重点的来了</strong>，居然是采样，那就有可能出现<strong>失误</strong>的情况，也就是说，c 这个索引的基数实际上是很大的，但是采样的时候，却很不幸，把这个索引的基数预测成很小。例如你采样的那一部分数据刚好基数很小，然后就误以为索引的基数很小。<strong>然后就呵呵，系统就不走 c 索引了，直接走全部扫描</strong>。</p>
<p>所以呢，说了这么多，得出结论：<strong>由于统计的失误，导致系统没有走索引，而是走了全表扫描</strong>，而这，也是导致我们 SQL 语句执行的很慢的原因。</p>
<blockquote>
<p>这里我声明一下，系统判断是否走索引，扫描行数的预测其实只是原因之一，这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。</p></blockquote>
<p>影响因素：</p>
<ul>
<li>索引选择性</li>
<li>预计需要回表的次数</li>
<li>临时表、排序开销</li>
<li>历史执行统计（MySQL 8.0+）</li>
</ul>
<h3 id="解决方案">解决方案</h3>
<p>不过呢，我们有时候也可以通过强制走索引的方式来查询，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">FORCE</span><span class="w"> </span><span class="k">INDEX</span><span class="p">(</span><span class="n">idx_c</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我们也可以通过</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>来查询索引的基数和实际是否符合，如果和实际很不符合的话，我们可以重新来统计索引的基数，可以用这条命令</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ANALYZE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>来重新统计分析。</p>
<p><strong>既然会预测错索引的基数，这也意味着，当我们的查询语句有多个索引的时候，系统有可能也会选错索引</strong>，这也可能是 SQL 执行的很慢的一个原因。</p>
<h1 id="系统化诊断流程">系统化诊断流程</h1>
<p>上述问题应该怎么发现呢？我列举了一些常用的手段。</p>
<h2 id="性能分析工具">性能分析工具</h2>
<h3 id="explain">EXPLAIN</h3>
<p>通过查看执行计划，提前避免问题。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">FORMAT</span><span class="o">=</span><span class="n">JSON</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>关键指标关注：</p>
<ul>
<li><code>type</code>：访问类型（const, ref, range, index, ALL）</li>
<li><code>key</code>：实际使用的索引</li>
<li><code>rows</code>：预估扫描行数</li>
<li><code>Extra</code>：额外信息（Using where, Using temporary, Using filesort）</li>
</ul>
<h3 id="性能监控performance-schema">性能监控（Performance Schema）</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 开启语句监控
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">setup_consumers</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">ENABLED</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;YES&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">NAME</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;events_statements%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看慢查询统计
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">events_statements_summary_by_digest</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">SUM_TIMER_WAIT</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="慢查询日志分析">慢查询日志分析</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># my.cnf 配置</span>
</span></span><span class="line"><span class="cl"><span class="na">slow_query_log</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">slow_query_log_file</span> <span class="o">=</span> <span class="s">/var/log/mysql/slow.log</span>
</span></span><span class="line"><span class="cl"><span class="na">long_query_time</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">log_queries_not_using_indexes</span> <span class="o">=</span> <span class="s">1</span>
</span></span></code></pre></div><h2 id="高级优化策略">高级优化策略</h2>
<h3 id="架构层面优化">架构层面优化</h3>
<ul>
<li>读写分离</li>
<li>分库分表</li>
<li>缓存策略（Redis, Memcached）</li>
<li>数据归档和历史表</li>
</ul>
<h3 id="数据库参数调优">数据库参数调优</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># InnoDB缓冲池（通常设为物理内存的70-80%）</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">16G</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 日志文件大小</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">2G</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 并发连接控制</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">500</span>
</span></span><span class="line"><span class="cl"><span class="na">thread_cache_size</span> <span class="o">=</span> <span class="s">50</span>
</span></span></code></pre></div><h3 id="应用层优化">应用层优化</h3>
<ul>
<li>连接池配置</li>
<li>批量操作减少网络往返</li>
<li>预处理语句避免重复解析</li>
<li>适当的数据缓存</li>
</ul>
<h1 id="总结">总结</h1>
<p>SQL 性能优化是一个系统性的工程，需要从多个维度进行分析和解决。本文提供的分析框架和优化策略覆盖了从基础到高级的各个层面，可以作为日常性能优化的参考指南。</p>
<p><strong>核心要点</strong>：</p>
<ul>
<li>建立系统化的诊断流程</li>
<li>索引是最高效的优化手段</li>
<li>统计信息的准确性至关重要</li>
<li>监控和预防优于事后补救</li>
<li>优化需要综合考虑成本和收益</li>
</ul>
<p>建议定期进行数据库健康检查，建立性能基线，以便快速识别和解决性能问题。</p>
]]></content:encoded>
    </item>
    <item>
      <title>月度开销记录方式分享</title>
      <link>https://www.zhangtianci.cn/posts/%E6%9C%88%E5%BA%A6%E5%BC%80%E9%94%80%E8%AE%B0%E5%BD%95%E6%96%B9%E5%BC%8F%E5%88%86%E4%BA%AB/</link>
      <pubDate>Wed, 01 Sep 2021 19:23:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/%E6%9C%88%E5%BA%A6%E5%BC%80%E9%94%80%E8%AE%B0%E5%BD%95%E6%96%B9%E5%BC%8F%E5%88%86%E4%BA%AB/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;h1 id=&#34;简介&#34;&gt;简介&lt;/h1&gt;
&lt;p&gt;现在消费主义横行，随便刷刷新闻，看看短视频，就很容易产生消费冲动。为了避免开销过大，成为月光族，做好预算和养成记账习惯可以保证在不降低生活幸福感的前提下，实现理性消费，提高生活品质。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<h1 id="简介">简介</h1>
<p>现在消费主义横行，随便刷刷新闻，看看短视频，就很容易产生消费冲动。为了避免开销过大，成为月光族，做好预算和养成记账习惯可以保证在不降低生活幸福感的前提下，实现理性消费，提高生活品质。</p>
<p>下面分享一下我大学和现在用的记账模板。</p>
<h1 id="大学记账模板">大学记账模板</h1>
<h2 id="模板介绍">模板介绍</h2>
<p><img alt="收入和支出.webp" loading="lazy" src="../../images/%E6%94%B6%E5%85%A5%E5%92%8C%E6%94%AF%E5%87%BA.webp"></p>
<p>主要使用学期费用这一列按学期来做预算，如果这个月有其他无法预计的支出则记录在每月支出中。</p>
<p>每月收入则是父母给的生活费以及其他收入。</p>
<p><img alt="我的大学预算.webp" loading="lazy" src="../../images/%E6%88%91%E7%9A%84%E5%A4%A7%E5%AD%A6%E9%A2%84%E7%AE%97.webp"></p>
<p>根据以上数据生成一个简单的图表，主要看盈余来考虑下个月大概还能花多少钱。</p>
<p>我这个模板还算好用吧，按学期做规划比较方便。</p>
<h1 id="毕业后记账模板">毕业后记账模板</h1>
<h2 id="模板介绍-1">模板介绍</h2>
<p><img alt="交易.webp" loading="lazy" src="../../images/%E4%BA%A4%E6%98%93.webp"></p>
<p>因为毕业后消费较为复杂，所以做了自定义类别。</p>
<p><img alt="汇总.webp" loading="lazy" src="../../images/%E6%B1%87%E6%80%BB.webp"></p>
<p>这个模板主要是根据大学模板的缺点改良来的。</p>
<p>汇总这里没做可视化数据表，感觉没什么意义。只留下了我比较关心的几个数字，比如本月存款和存款增长百分比。</p>
<p>其他数据和可视化数据表等我认为需要用到的时候我再加上吧。目前我认为显示这些就足够了。</p>
<h1 id="目前在用的记账软件">目前在用的记账软件</h1>
<p>==请注意，我并不推荐任何记账软件。==</p>
<p>国家对个人信息这一块的监管和法律基本等于空白，因此软件方早晚有一天会拿你的信息通过各种途径去盈利，去做用户画像、个性化广告等等。</p>
<p>但是现在移动支付入口基本掌握在微信和支付宝手中，其实你的消费习惯、能力等信息早已经被泄露分析了。那么这类软件是否值得信赖，要不要用，如何使用，要慎重考虑。</p>
<h2 id="软件介绍">软件介绍</h2>
<p>之所以没有继续使用 Excel，是因为工作一天回到家，实在是不想开电脑进行记录了。移动端的 Excel 操作体验又差，因此需要一个移动端的记账软件。</p>
<p>现在市面上的记账软件或多或少都有社区、金融理财等和记账不相关的功能。为了商业化，可以理解，但是不能接受。其实我们只需要一个专注于记账功能的软件就够了。</p>
<p>其实我也有想过自己开发一个 Web 端的通用记账管理系统，但因为各种各样的原因（995、懒），实在是腾不出手来，因此直接买了一个记账软件——记账鸭。</p>
<p>这款软件也可以免费使用，缺点是账本数量有限制，所以我干脆买了一个终身会员（当时是 60 人民币左右入手的，最近涨价了，目前是 89 人民币）。</p>
<p><img alt="记账鸭.webp" loading="lazy" src="../../images/%E8%AE%B0%E8%B4%A6%E9%B8%AD.webp"></p>
<h2 id="下载链接">下载链接</h2>
<p>各大安卓、苹果应用商店都有，自行下载。</p>
<h1 id="最后">最后</h1>
<p>模板什么的其实能用就行，也不用太花哨。也没必要用某些记账软件来记录，做的再好能有 Excel 好吗？反而把自己的消费习惯暴露给软件厂商拿去牟利了。</p>
<p>记账一方面是需要一个可以用的模板，另一方面是要养成记账的习惯。不能今天想起来就记录一下，明天比较累就不记录了。还要记得对账，毕竟记录难免有遗漏或者错误。</p>
<p>理性消费，抵制消费主义，从记账做起 😎 。</p>
]]></content:encoded>
    </item>
    <item>
      <title>如何贡献有价值的反馈</title>
      <link>https://www.zhangtianci.cn/posts/%E5%A6%82%E4%BD%95%E8%B4%A1%E7%8C%AE%E6%9C%89%E4%BB%B7%E5%80%BC%E7%9A%84%E5%8F%8D%E9%A6%88/</link>
      <pubDate>Sat, 03 Oct 2020 11:14:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/%E5%A6%82%E4%BD%95%E8%B4%A1%E7%8C%AE%E6%9C%89%E4%BB%B7%E5%80%BC%E7%9A%84%E5%8F%8D%E9%A6%88/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;我在工作中总会有如下对话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;某测试：“张，XX 功能报错了！”
我：“报什么错？”
某测试：“我用了下这个功能就报错了！”
我：“……（可能对方没理解我的意思？）有什么提示吗？怎么操作的？日志发来我瞧瞧。”
某测试：“我就是调用了一下就报错了，没看日志！张你研究下！”
我：……&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>我在工作中总会有如下对话：</p>
<blockquote>
<p>某测试：“张，XX 功能报错了！”
我：“报什么错？”
某测试：“我用了下这个功能就报错了！”
我：“……（可能对方没理解我的意思？）有什么提示吗？怎么操作的？日志发来我瞧瞧。”
某测试：“我就是调用了一下就报错了，没看日志！张你研究下！”
我：……</p></blockquote>
<p>这种对话不仅浪费时间，而且往往最后发现是对方没有仔细阅读文档导致的误用。无论是技术人员还是非技术人员，掌握如何提出有效反馈都能大大节约双方的时间。</p>
<p>当你决定反馈问题时（既然已经这么做了），肯定是希望问题能尽快解决。此时任何过激言语都无济于事——因为这可能是程序员的错误，也可能是你的错误。也许你有权对他们发火，但是如果你能提供有用的信息而非情绪化表达，问题往往会更快得到解决。</p>
<p>基于这些经验，我整理了以下建议，希望能帮助大家提出更有价值的反馈。</p>
<h1 id="提问前">提问前</h1>
<p>遇到问题时，心里一定很着急，可以理解。但是在决定向开源社区或开发人员提交问题前，建议先尝试以下步骤：</p>
<h2 id="阅读文档">阅读文档</h2>
<p>查阅相关功能的官方文档，利用搜索功能查找问题关键词，很多时候能快速找到解决方案。</p>
<h2 id="搜索引擎">搜索引擎</h2>
<p>90% 你遇到的问题，很可能别人也遇到过。通过 Google、StackOverflow 等技术社区搜索（建议避开 CSDN 等低质量站点），往往能快速定位解决方法。</p>
<p>永远记住，地球上的你并不孤单，包括你遇到的问题。</p>
<h2 id="问问身边的人">问问身边的人</h2>
<p>如果你身边的人也曾用过同一个功能，可以礼貌地请教。善意的技术探讨很少有人会拒绝。</p>
<p>如果以上三步都无法解决问题，就不要犹豫，立马向开源社区或开发人员提交问题就好。</p>
<h1 id="提问时">提问时</h1>
<p>提问有很多种，比如你认识作者，直接面对面请教就行。下面探讨的是如何通过互联网的方式来问问题。</p>
<h2 id="平和对等的心态">平和对等的心态</h2>
<p>提问时，不要把自己摆在顾客的位置，比如：</p>
<blockquote>
<p>项目马上要上线了，请务必解决！</p></blockquote>
<p>另外，也不要把自己摆在乞食者的位置，比如：</p>
<blockquote>
<p>救命啊，这个功能我今天急用！</p></blockquote>
<p>不论是在开源社区，还是公司部门之间，一切皆是朋友。无论对方是 Linux 内核的作者，还是新来的实习生，你们都是对等的。你的提问是在帮助完善软件。这种心态可以让你的问题赢得更多人的阅读和思考。</p>
<h2 id="通过正确的途径提交">通过正确的途径提交</h2>
<p>如果有专门的反馈邮箱或 Bug 管理系统，一定要使用这些指定渠道。</p>
<p>最不好的途径是：</p>
<ul>
<li>QQ 、微信等群组。这些群组主要是用来休闲的。对开源项目来说，在这些地方提问，作者一般不会关注，效率非常低。</li>
<li>微博、Facebook 等社交网络。不少人在微博上通过 @ 或私信询问问题，这些我经常看不到。看到了，也不情愿回复。微博是扯淡、交流情感的地方，一般是写代码写累了，才去逛逛，很少会有在社交网络上回答技术问题的心情。</li>
</ul>
<p>通过正确的途径提交问题，一般可以让你的问题得到及时准确的回复。</p>
<h2 id="使用明确有意义的标题">使用明确、有意义的标题</h2>
<p>抱着平和对等的心态，找到合适的途径后，就得静下心来将遇到的问题写成文字。书写文字不是一件简单的事情，我们可以从遵循一些简单的规则开始。</p>
<p>首先是提问标题要简洁清晰，要言之有物。比如：</p>
<blockquote>
<p>XX 功能出错，求大神指点</p>
<p>XX 功能在我的浏览器上运行不了，别人的可以</p></blockquote>
<p>上面举例的标题很糟糕，光看标题作者无法知道发生了什么事。当开源社区的问题很多时，上面这类标题，经常会让作者直接忽视或将优先级降到很低。更妥当的标题是：</p>
<blockquote>
<p>XX 功能提示 XXXXX（错误码）</p>
<p>XX 功能在 Chrome 69 无法使用</p></blockquote>
<p>明确、有意义的标题，可以帮助作者确定问题具体是什么类型、预估需要多少时间解决、是否现在马上解决等。一个好的标题，也有利于社区知识的沉淀和后期搜索。标题有如一个人的颜面衣着，虽然不是关键，但在嘈杂的信息社区中，这很重要。</p>
<h2 id="遵循良好的模板">遵循良好的模板</h2>
<p>如果社区提供了问题模板，一定要仔细看下。比如我的开源项目，当你创建一个问题时，会自动提供以下模板：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># 前言
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">请确认你已经做了下面这些事情，若 bug 还是未解决，那么请尽可详细地描述你的问题。
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">- [ ]</span> 我已经安装了最新版的 Hobby
</span></span><span class="line"><span class="cl">  <span class="k">- [ ]</span> 我已经搜索了已有的 Issues 列表中有关的信息
</span></span><span class="line"><span class="cl">  <span class="k">- [ ]</span> 我已经阅读了 Hobby 的 相关文档
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 描述错误
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--简洁而清晰的描述这个错误。--&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 复现方式
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--复现该行为的步骤。--&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 预期行为
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--对你期望发生的事情进行简洁而清晰的描述。--&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 截图
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--如果可以的话，请添加一些截图以帮助描述你的问题。--&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 系统版本和运行环境
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--请描述你的版本以及运行环境。--&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># 其他
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="cl">&lt;!--在此处添加有关该问题的其他内容。--&gt;
</span></span></code></pre></div><p>遵循这个模板去描述问题，经常能省很多事。作者一般也非常欢迎通过模板提交的问题。如果社区没有提供模板，也可以自己遵循以上模板来提交。</p>
<p>下面针对问题内容，具体说说一些需要注意的点。</p>
<h2 id="注意语法和格式">注意语法和格式</h2>
<p>虽然我们不是作家，但正确的语法、清晰的格式，可以让读者赏心悦目，也就更有心情帮你一起思考解决问题。</p>
<p>对于很多需要代码来描述的问题，要尤其注意格式，比如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">List</span><span class="o">&lt;</span><span class="n">Category</span><span class="o">&gt;</span><span class="w"> </span><span class="n">children</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">categories</span><span class="p">.</span><span class="na">stream</span><span class="p">().</span><span class="na">filter</span><span class="p">(</span><span class="n">category</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">Objects</span><span class="p">.</span><span class="na">equal</span><span class="p">(</span><span class="n">parentCategory</span><span class="p">.</span><span class="na">getId</span><span class="p">(),</span><span class="w"> </span><span class="n">category</span><span class="p">.</span><span class="na">getParentId</span><span class="p">())).</span><span class="na">collect</span><span class="p">(</span><span class="n">Collectors</span><span class="p">.</span><span class="na">toList</span><span class="p">());</span><span class="w">
</span></span></span></code></pre></div><p>可读性不如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">List</span><span class="o">&lt;</span><span class="n">Category</span><span class="o">&gt;</span><span class="w"> </span><span class="n">children</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">categories</span><span class="p">.</span><span class="na">stream</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">filter</span><span class="p">(</span><span class="n">category</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">Objects</span><span class="p">.</span><span class="na">equal</span><span class="p">(</span><span class="n">parentCategory</span><span class="p">.</span><span class="na">getId</span><span class="p">(),</span><span class="w"> </span><span class="n">category</span><span class="p">.</span><span class="na">getParentId</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">collect</span><span class="p">(</span><span class="n">Collectors</span><span class="p">.</span><span class="na">toList</span><span class="p">());</span><span class="w">
</span></span></span></code></pre></div><p>良好的代码排版能让你的内容看起来很专业，大家也就更有意愿会去帮助你，否则糟糕的排版，经常带来的是提问之后的石沉大海。</p>
<h2 id="描述事实而不是猜测">描述事实而不是猜测</h2>
<p>事实是指，依次进行了哪些操作、产生了怎样的结果。比如：</p>
<blockquote>
<p>我在 XX 环境下，通过 XX 操作后，点击 XX，这时浏览器弹出 XX 错误提示。</p></blockquote>
<p>上面是一段比较好的事实描述（更好的是把错误提示也截图上来），而不要像下面这样猜测：</p>
<blockquote>
<p>XX 功能在 XX 环境下运行不正常，我怀疑是源码第 213 行有问题。</p></blockquote>
<p>上面的描述，会让作者一头雾水、甚至很恼火。尽量避免猜测性描述，除非你能先描述事实，在事实描述清楚之后，再给出合理的猜测是欢迎的。</p>
<p>如果能提供可重现错误的在线可访问代码，那是最好不过。一旦你这么用心去做了，作者往往也会很用心地立马帮你解决。</p>
<h2 id="描述目标而非过程">描述目标而非过程</h2>
<p>经常会有这种情况，提问者在脑袋里有个更高层次的目标，他们在自以为能达到目标的特定道路上卡住了，然后跑来问该怎么走。比如：</p>
<blockquote>
<p>XX 应用如何手动指定 JDK 路径？</p></blockquote>
<p>上面这个问题的背后，提问者实际上想解决的是如何通过手动指定 JDK 路径来实现单机安装多个 JDK 的问题。提问者选择了手动指定 JDK 的方式来实现，但这过程中遇到了问题，因此跑过来继续怎么走。然而，如果只是描述过程，往往会把作者也绕进去。</p>
<p>实际情况却是，提问者选择的路本身就是一条崎岖之路，对于要解决的问题，实际上有更好的方式。这种情况下，描述清楚目标，讲清楚要干什么非常重要。</p>
<p>在描述自己是怎么做之前，一定要先描述要做什么。提问题时，What 往往比 How 更重要。</p>
<h2 id="要有具体场景">要有具体场景</h2>
<p>无论在开源社区，还是微博、知乎等平台上，有一种非常常见的问题：</p>
<blockquote>
<p>如何优化 Java 代码性能？</p>
<p>如何使用 maven 进行模块化开发？</p></blockquote>
<p>这类问题还有很多，每每遇到，只能笑笑，然后悄悄地忽略掉。因此这类问题很难回答，就如下面这些问题一样：</p>
<blockquote>
<p>如何才能让生命有意义？</p>
<p>如何打败阿里？</p></blockquote>
<p>这类提问者，一般比较浮躁，经常对问题本身也没有经过思考。踏实的提问者，不会让问题浮在空中无法回答，而会在具体场景中让问题落地：</p>
<blockquote>
<p>我的项目有 20 多个 Java 模块，接下来还会急剧增加。目前遇到以下问题……（省略五百字）…… 请问如何维护？</p></blockquote>
<h2 id="正确描述问题">正确描述问题</h2>
<h3 id="记录问题">记录问题</h3>
<p>当程序出毛病的时候，立刻停止正在做的任何操作。不要按任何健。仔细地看一下屏幕，注意那些不正常的地方，记住它或者写下来。然后慎重地点击“确定” 或“取消”，选择一个最安全的。学着养成一种条件反射——一旦电脑出了问题，先不要动。要想摆脱这个问题，关掉受影响的程序或者重新启动计算机都不好，一个解决问题的好办法是让问题再次产生。</p>
<h3 id="演示问题">演示问题</h3>
<p>报告问题的最好的方法之一是“演示”给程序员看。让程序员站在电脑前，运行他们的程序，指出程序的错误。让他们看着你启动电脑、运行程序、如何进行操作以及程序对你的输入有何反应。</p>
<h3 id="口述问题">口述问题</h3>
<p>“演示”是很好的办法，但是常常做不到。那我们应该尽可能确切地告诉程序员你做了些什么。如果是一个图形界面程序，告诉他们你按了哪个按钮，依照什么顺序按的。如果是一个命令行程序，精确的告诉他们你输入了什么命令。你应该尽可能详细地提供所输入的命令和程序的反应。</p>
<p>把你能想到的所有的输入方式都告诉程序员，如果程序要读取一个文件，你可能需要把这个文件发给他们。如果程序需要通过网络与另一台电脑通讯，你或许不能把那台电脑复制过去，但至少可以说一下电脑的类型和安装了哪些软件（如果可以的话）。</p>
<p>如果你给了程序员一长串输入和指令，他们执行以后没有出现错误，那是因为你没有给他们足够的信息，可能错误不是在每台计算机上都出现，你的系统可能和他们的在某些地方不一样。有时候程序的行为可能和你预想的不一样，这也许是误会，但是你会认为程序出错了，程序员却认为这是对的。</p>
<p>同样也要描述发生了什么。精确的描述你看到了什么。告诉他们为什么你觉得自己所看到的是错误的，最好再告诉他们，你认为自己应该看到什么。如果你只是说：“程序出错了”，那你很可能漏掉了非常重要的信息。</p>
<p>如果你看到了错误消息，一定要仔细、准确的告诉程序员，这 <em>确实</em> 很重要。在这种情况下，程序员只要修正错误，而不用去找错误。他们需要知道是什么出问题了，系统所报的错误消息正好帮助了他们。如果你没有更好的方法记住这些消息，就把它们写下来。只报告“程序出了一个错”是毫无意义的，除非你把错误消息一块报上来。</p>
<p>特殊情况下，如果有错误消息号，<em>一定</em> 要把这些号码告诉程序员。不要以为你看不出任何意义，它就没有意义。错误消息号包含了能被程序员读懂的各种信息，并且很有可能包含重要的线索。给错误消息编号是因为用语言描述计算机错误常常令人费解。用这种方式告诉你错误的所在是一个最好的办法。</p>
<p>在这种情形下，程序员的排错工作会十分高效。他们不知道发生了什么，也不可能到现场去观察，所以他们一直在搜寻有价值的线索。错误消息、错误消息号以及一些莫名其妙的延迟，都是很重要的线索，就像办案时的指纹一样重要，保存好。</p>
<h3 id="间歇性问题">间歇性问题</h3>
<p>“间歇性错误”着实让程序员发愁。相比之下，进行一系列简单的操作便能导致错误发生的问题是简单的。程序员可以在一个便于观察的条件下重复那些操作，观察每一个细节。太多的问题在这种情况下不能解决，例如：程序每星期出一次错，或者偶然出一次错，或者在程序员面前从不出错。当然还有就是程序的截止日期到了，那肯定要出错。</p>
<p>大多数“间歇性错误”并不是真正的“间歇”。其中的大多数错误与某些地方是有联系的。有一些错误可能是内存泄漏产生的，有一些可能是别的程序在不恰当的时候修改某个重要文件造成的，还有一些可能发生在某一特定时间。</p>
<p>程序员想要了解任何与你发现的问题相关的事情。有可能的话你到另一台机器上试试，多试几次，两次，三次，看看问题是不是经常发生。如果问题出现在你进行了一系列操作之后，不是你想让它出现它就会出现，这就有可能是长时间的运行或处理大文件所导致的错误。程序崩溃的时候，你要尽可能的记住你都做了些什么，并且如果你看到任何图形,也别忘了提一下。你提供的任何事情都是有帮助的。即使只是概括性的描述。这虽然不能提供导致问题的直接线索，但是可能帮助程序员重现问题。</p>
<p>最重要的是：程序员想要确定他们正在处理的是一个真正的“间歇性错误”呢，还是一个在另一类特定的计算机上才出现的错误。他们想知道有关你计算机的许多细节，以便了解你的机器与他们的有什么不同。有许多细节都依仗特定的程序，但是有一件东西你一定要提供——版本号。程序的版本、操作系统的版本以及与问题有关的程序的版本。</p>
<h2 id="仔细检查确保准确">仔细检查、确保准确</h2>
<p>是人都会犯错误，特别是在如此快节奏的互联网环境下。好不容易把问题描述清楚时，不要急着立刻提交。在提交前，至少保证从头到尾再仔细阅读一遍，比如语法错误、错别字、标点符号、排版等等。做到这些，不光是尊重别人，也是尊重自己。</p>
<h1 id="提问后">提问后</h1>
<p>提交问题后，建议通过邮件等方式订阅回复。互联网上最有效的沟通方式是异步沟通，不要期待作者马上回复，也不要心烦意乱着急地等待。</p>
<h2 id="尽可能补充信息">尽可能补充信息</h2>
<p>在接收到回复时，仔细阅读。最经常的情况是，社区回复的，经常不是你想要的。比如：</p>
<blockquote>
<p>根据你的描述，问题无法重现。能否提供具体使用环境和重现步骤？</p></blockquote>
<p>这时要淡定。仔细看看自己提交的问题描述是否足够清晰，如果有可补充的信息，尽量补充，以帮助作者能尽快定位问题。比如：</p>
<blockquote>
<p>很抱歉，我前面有一步描述不正确，实际情况是我是在 Firefox 中运行的……</p></blockquote>
<p>谦和淡定的交流，不光能帮助你解决问题，还有助于你结交更多朋友。</p>
<h2 id="适当的总结">适当的总结</h2>
<p>当问题终于解决时，建议对问题进行总结。可以编辑原帖，也可以通过博客等方式总结。你的总结，会让遇到同样问题的朋友们受益，并且对自己的技能也是一种提高。无论国内还是国外，有很多牛人之所以成为牛人，很大程度上都是因为有总结思考的好习惯。</p>
<h2 id="不要忘记感谢">不要忘记感谢</h2>
<p>最后，记得感谢。很多开源软件的作者，都是利用业余时间在创作代码。你的感谢，会让开源社区充满爱与力量。</p>
]]></content:encoded>
    </item>
    <item>
      <title>学科竞赛训练管理系统设计与实现</title>
      <link>https://www.zhangtianci.cn/posts/%E5%AD%A6%E7%A7%91%E7%AB%9E%E8%B5%9B%E8%AE%AD%E7%BB%83%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/</link>
      <pubDate>Mon, 01 Jun 2020 14:20:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/%E5%AD%A6%E7%A7%91%E7%AB%9E%E8%B5%9B%E8%AE%AD%E7%BB%83%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;学科竞赛训练管理系统的前端使用 React 创建组件，使用 Nginx 解决跨域问题；后端使用 Spring Boot 搭建项目，使用 Spring Security 保证安全，使用 Spring Data JPA 访问 PostgreSQL，使用 Redis 作为系统缓存。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>学科竞赛训练管理系统的前端使用 React 创建组件，使用 Nginx 解决跨域问题；后端使用 Spring Boot 搭建项目，使用 Spring Security 保证安全，使用 Spring Data JPA 访问 PostgreSQL，使用 Redis 作为系统缓存。</p>
<p>学科竞赛训练管理系统是我的毕业设计。本文内容因为删掉了与我个人和学校有关的隐私内容，测试和系统设计可能描述的不是很完整。</p>
<p>因为是用 3 天就完成了 90% 前后端开发工作，因此代码不是很简洁，出现了大量重复、冗余代码，尽情谅解。</p>
<p>虽然开发时间很充裕，但我并不想在毕业设计上花太多的时间（打游戏和学习不香吗），目标就是毕业即可。</p>
<p>懒得写针对性的代码，直接写了通用代码一把梭。</p>
<h2 id="系统地址">系统地址</h2>
<p><a href="https://github.com/dormirr/galop-web">前端地址</a></p>
<p><a href="https://github.com/dormirr/galop-server">后端地址</a></p>
<h1 id="系统介绍">系统介绍</h1>
<p>针对目前高校举办学科竞赛所遇到的问题，本系统主要设计六个模块，分别是通知模块、战斗力模块、比赛模块、报名模块、角色模块和团队模块。</p>
<p><img alt="系统架构图" loading="lazy" src="../../images/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E5%9B%BE.webp"></p>
<h1 id="安装与使用教程">安装与使用教程</h1>
<h2 id="安装">安装</h2>
<h3 id="服务器环境">服务器环境</h3>
<p>服务器需要安装如表所示的软件。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>名称</strong></th>
          <th style="text-align: center"><strong>版本</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Nginx</td>
          <td style="text-align: center">1.16</td>
      </tr>
      <tr>
          <td style="text-align: center">OpenJDK</td>
          <td style="text-align: center">11</td>
      </tr>
      <tr>
          <td style="text-align: center">数据库</td>
          <td style="text-align: center">Redis 5 PostgreSQL 12</td>
      </tr>
      <tr>
          <td style="text-align: center">Node.js</td>
          <td style="text-align: center">12</td>
      </tr>
  </tbody>
</table>
<h3 id="客户端环境">客户端环境</h3>
<p>安装 IE 11 或所有现代浏览器。</p>
<h3 id="nginx-配置">Nginx 配置</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">http</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">include</span>       <span class="s">mime.types</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">default_type</span>  <span class="s">application/octet-stream</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">sendfile</span>        <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">keepalive_timeout</span>  <span class="mi">65</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kn">gzip</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">gzip_min_length</span> <span class="mi">1k</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">gzip_comp_level</span> <span class="mi">9</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">gzip_types</span> <span class="s">text/plain</span> <span class="s">application/javascript</span> <span class="s">application/x-javascript</span> <span class="s">text/css</span> <span class="s">application/xml</span> <span class="s">text/javascript</span> <span class="s">application/x-httpd-php</span> <span class="s">image/jpeg</span> <span class="s">image/gif</span> <span class="s">image/png</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">gzip_vary</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">gzip_disable</span> <span class="s">&#34;MSIE</span> <span class="s">[1-6]\.&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kn">root</span> <span class="s">D:/AntDesignProjects/galop-web/dist</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="s">/index.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">rewrite</span> <span class="s">^/(.*)</span>$ <span class="s">https://localhost/</span><span class="nv">$1</span> <span class="s">permanent</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>        
</span></span><span class="line"><span class="cl">        <span class="kn">location</span> <span class="s">/api/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_pass</span> <span class="s">https://localhost:8080/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_set_header</span>   <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_set_header</span>   <span class="s">X-Real-IP</span>         <span class="nv">$remote_addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span> <span class="s">http2</span> <span class="s">default_server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_certificate</span> <span class="s">D:/IdeaProjects/galop-server/core-module/src/main/resources/galop.crt</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_certificate_key</span> <span class="s">D:/IdeaProjects/galop-server/core-module/src/main/resources/galop.key</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_session_timeout</span>  <span class="mi">5m</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_protocols</span> <span class="s">TLSv1</span> <span class="s">TLSv1.1</span> <span class="s">TLSv1.2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_ciphers</span> <span class="s">AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_prefer_server_ciphers</span>   <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="kn">root</span> <span class="s">D:/AntDesignProjects/galop-web/dist</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="s">/index.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="kn">location</span> <span class="s">/api/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_pass</span> <span class="s">https://localhost:8080/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_set_header</span>   <span class="s">X-Forwarded-Proto</span> <span class="nv">$scheme</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_set_header</span>   <span class="s">Host</span>              <span class="nv">$http_host</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="kn">proxy_set_header</span>   <span class="s">X-Real-IP</span>         <span class="nv">$remote_addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>其中前端文件位置可以在 <code>root</code> 处定义，域名可以在 <code>location</code> 处定义，证书位置可以在 <code>ssl\_certificate</code> 和 <code>ssl\_certificate\_key</code> 处定义，可自行修改。</p>
<h3 id="数据库配置">数据库配置</h3>
<p><strong>Redis 配置</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">spring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">repositories</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># 数据库索引</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="m">127.0.0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># 连接超时时间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span></code></pre></div><p>可自行修改数据库服务器 IP 地址、端口号和密码。</p>
<p><strong>PostgreSQL 配置</strong></p>
<p>首先需要使用 <code>galop\_20200502.sql</code> 文件创建数据库，创建后进行如下数据库配置。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">spring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">datasource</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">druid</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">jdbc:postgresql://localhost:5432/galop</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="m">123456</span><span class="w">
</span></span></span></code></pre></div><p>可自行修改数据库服务器 IP 地址、端口号、账号和密码。</p>
<h3 id="其他配置">其他配置</h3>
<p>可自行修改 <code>application.yml</code> 中如表所示配置。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>名称</strong></th>
          <th style="text-align: center"><strong>默认值</strong></th>
          <th style="text-align: center"><strong>备注</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Excel 模板存储位置</td>
          <td style="text-align: center"><code>D:\IdeaProjects\galop-server\file\</code></td>
          <td style="text-align: center">可选</td>
      </tr>
      <tr>
          <td style="text-align: center">邮箱配置</td>
          <td style="text-align: center"></td>
          <td style="text-align: center">默认为空，必填</td>
      </tr>
      <tr>
          <td style="text-align: center">头像存储位置</td>
          <td style="text-align: center"><code>D:\IdeaProjects\galop-server\avatar\</code></td>
          <td style="text-align: center">可选</td>
      </tr>
  </tbody>
</table>
<h3 id="建议配置">建议配置</h3>
<p>建议将前端代码放置在 <code>D:\AntDesignProjects</code> 文件夹下，将后端代码放置在 <code>D:\IdeaProjects</code> 文件夹下。这样可以直接使用自带的默认配置，仅需配置环境与邮箱。</p>
<p>配置完成后，前端需要执行 <code>npm install</code> 安装依赖，然后执行 <code>umi build</code> 编译。后端需要执行 <code>mvn clean install</code> 打包并发布到本地仓库。</p>
<h2 id="运行">运行</h2>
<h3 id="启动">启动</h3>
<p>运行 Nginx 服务器，Redis 和 PostgreSQL，然后运行编译后的 jar 包即可启动。</p>
<p>Windows 10 上具体操作如表所示。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>命令</strong></th>
          <th style="text-align: center"><strong>备注</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">启动 PowerShell</td>
          <td style="text-align: center"></td>
      </tr>
      <tr>
          <td style="text-align: center">执行 <code>cd D:\Nginx\nginx-1.16.1\</code></td>
          <td style="text-align: center">切换到 Nginx 安装目录</td>
      </tr>
      <tr>
          <td style="text-align: center"><code>start  ./nginx.exe</code></td>
          <td style="text-align: center">启动 Nginx 服务器</td>
      </tr>
      <tr>
          <td style="text-align: center">启动 WSL 2</td>
          <td style="text-align: center"></td>
      </tr>
      <tr>
          <td style="text-align: center">执行 <code>redis-server</code></td>
          <td style="text-align: center">启动 Redis</td>
      </tr>
      <tr>
          <td style="text-align: center">启动 PostgreSQL 服务</td>
          <td style="text-align: center">自行开启数据库服务</td>
      </tr>
      <tr>
          <td style="text-align: center">执行 <code>cd D:\IdeaProjects\galop-server\core-module\target\</code></td>
          <td style="text-align: center">切换到后端编译出包位置</td>
      </tr>
      <tr>
          <td style="text-align: center">执行 <code>java -jar .\core-module-1.0.0.jar</code></td>
          <td style="text-align: center">运行系统 jar 包</td>
      </tr>
  </tbody>
</table>
<h3 id="账号">账号</h3>
<p>系统初始内置一个管理员账号，账号为 <code>000000</code>，密码为 <code>123456</code>。</p>
<p>管理员可为其他人批量注册管理员或普通账号，注册后的默认密码为 <code>123456</code>。</p>
<h3 id="运行-1">运行</h3>
<p>成功启动后可以通过浏览器访问 <code>https://localhost/login</code> 进行登录，登录后即可使用学科竞赛训练管理系统。</p>
<h1 id="主要技术">主要技术</h1>
<h2 id="开发环境">开发环境</h2>
<p>采用前后端分离的方式进行开发，具体环境如表所示。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>名称</strong></th>
          <th style="text-align: center"><strong>版本</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">操作系统</td>
          <td style="text-align: center">Windows 10</td>
      </tr>
      <tr>
          <td style="text-align: center">版本控制工具</td>
          <td style="text-align: center">Git 2.26</td>
      </tr>
      <tr>
          <td style="text-align: center">前端开发环境</td>
          <td style="text-align: center"></td>
      </tr>
      <tr>
          <td style="text-align: center">开发工具</td>
          <td style="text-align: center">Visual Studio Code 1.44</td>
      </tr>
      <tr>
          <td style="text-align: center">Node.js</td>
          <td style="text-align: center">12.16</td>
      </tr>
      <tr>
          <td style="text-align: center">React</td>
          <td style="text-align: center">16.13</td>
      </tr>
      <tr>
          <td style="text-align: center">JavaScript</td>
          <td style="text-align: center">ECMAScript 6</td>
      </tr>
      <tr>
          <td style="text-align: center">Web 服务器</td>
          <td style="text-align: center">Nginx 1.16</td>
      </tr>
      <tr>
          <td style="text-align: center">后端开发环境</td>
          <td style="text-align: center"></td>
      </tr>
      <tr>
          <td style="text-align: center">开发工具</td>
          <td style="text-align: center">IntelliJ IDEA 2020</td>
      </tr>
      <tr>
          <td style="text-align: center">OpenJDK</td>
          <td style="text-align: center">11</td>
      </tr>
      <tr>
          <td style="text-align: center">Spring Boot</td>
          <td style="text-align: center">2.2</td>
      </tr>
      <tr>
          <td style="text-align: center">数据库</td>
          <td style="text-align: center">Redis 5 PostgreSQL 12</td>
      </tr>
      <tr>
          <td style="text-align: center">Web 服务器</td>
          <td style="text-align: center">Tomcat 9</td>
      </tr>
  </tbody>
</table>
<p>若干依赖不再详细列出。</p>
<h2 id="运行环境">运行环境</h2>
<p>前端部署在 Nginx 服务器上，通过 Nginx 将 80 端口的请求转向 443 端口，然后将 443 端口的请求反向代理到后端的服务器上。</p>
<p>后端部署在装有 OpenJDK、Redis 和 PostgreSQL 的服务器上。</p>
<p>系统支持 IE 11 和所有现代浏览器访问。</p>
<h1 id="系统设计">系统设计</h1>
<h2 id="功能介绍">功能介绍</h2>
<p>学科竞赛训练管理系统主要由通知模块、战斗力模块、比赛模块、报名模块、角色模块、团队模块以及工具模块组成。</p>
<h3 id="通知模块">通知模块</h3>
<p>通知模块主要完成创建、查询、修改和删除公告的功能，通过自动化的方式来解决比赛组织和宣传成本高的问题。其中创建、修改和删除功能仅对老师开放，查询功能则对老师和学生开放。</p>
<p>比赛创建和比赛结果上传后，系统也会自动创建新的公告，如图所示。</p>
<p><img alt="自动创建公告" loading="lazy" src="../../images/%E8%87%AA%E5%8A%A8%E5%88%9B%E5%BB%BA%E5%85%AC%E5%91%8A.webp"></p>
<p>老师和学生登录主页的最近十条公告功能也由通知模块提供。</p>
<h3 id="战斗力模块">战斗力模块</h3>
<p>战斗力模块主要负责提供创建和查询学生战斗力变化情况的功能，通过每场比赛的结果计算战斗力并统计比赛人数，以可视化的图表来展现学生训练成果和比赛参与度。其中创建战斗力记录由系统自动完成，查询功能仅对学生开放。</p>
<p>学生登录主页的战斗力可视化图表和老师登录主页的比赛人数可视化图表功能也由战斗力模块提供，如图所示。</p>
<p><img alt="比赛参与人数变化图" loading="lazy" src="../../images/%E6%AF%94%E8%B5%9B%E5%8F%82%E4%B8%8E%E4%BA%BA%E6%95%B0%E5%8F%98%E5%8C%96%E5%9B%BE.webp"></p>
<p><img alt="战斗力变化图" loading="lazy" src="../../images/%E6%88%98%E6%96%97%E5%8A%9B%E5%8F%98%E5%8C%96%E5%9B%BE.webp"></p>
<h3 id="比赛模块">比赛模块</h3>
<p>比赛模块主要完成创建比赛、查询和处理比赛结果的功能，通过自动化的方式来解决比赛结果处理繁琐以及训练效率低的问题。其中创建和上传比赛结果仅对老师开放，查询比赛功能对老师和学生开放，查询比赛结果功能仅对学生开放。</p>
<p>比赛创建和比赛结果上传后，系统会自动调用通知模块创建公告，如图所示。</p>
<p><img alt="比赛创建公告" loading="lazy" src="../../images/%E6%AF%94%E8%B5%9B%E5%88%9B%E5%BB%BA%E5%85%AC%E5%91%8A.webp"></p>
<p><img alt="比赛结果公告" loading="lazy" src="../../images/%E6%AF%94%E8%B5%9B%E7%BB%93%E6%9E%9C%E5%85%AC%E5%91%8A.webp"></p>
<p>尚未结束报名的比赛功能也由比赛模块提供，如图所示。</p>
<p><img alt="尚未结束的比赛" loading="lazy" src="../../images/%E5%B0%9A%E6%9C%AA%E7%BB%93%E6%9D%9F%E7%9A%84%E6%AF%94%E8%B5%9B.webp"></p>
<h3 id="报名模块">报名模块</h3>
<p>报名模块主要完成比赛报名、审核和导出报名表的功能，学生可以从我的报名来看曾参加过的比赛与报名状态。通过自动化的方式解决老师难以选拔合适的参赛学生问题。其中比赛报名仅对团队队长开放，报名审核和导出报名表仅对老师开放，我的报名仅对学生开放。</p>
<p>老师可以根据学生所在团队的战斗力决定是否允许这个队伍参赛，如图所示。</p>
<p><img alt="报名审核" loading="lazy" src="../../images/%E6%8A%A5%E5%90%8D%E5%AE%A1%E6%A0%B8.webp"></p>
<h3 id="角色模块">角色模块</h3>
<p>角色模块主要完成用户登录、查询、批量注册与注销、导出战斗力排名表、修改信息和重置密码的功能，通过自动化的方式解决注册、注销繁琐的问题。其中用户查询、批量注册与注销、导出战斗力排名和重置密码的功能仅对老师开放，登录与修改信息则对老师和学生开放。</p>
<p>批量注销后，角色模块会调用战斗力模块、比赛模块和团队模块删除信息。</p>
<p>用户忘记密码后，若注册时填写了邮箱，即可通过邮箱接收重置密码的邮件自己进行重置，如图 所示。</p>
<p><img alt="重置密码邮件" loading="lazy" src="../../images/%E9%87%8D%E7%BD%AE%E5%AF%86%E7%A0%81%E9%82%AE%E4%BB%B6.webp"></p>
<p>若没有填写邮箱则可以通过联系老师进行密码重置。</p>
<h3 id="团队模块">团队模块</h3>
<p>团队模块主要完成创建、查询、修改、发送加入申请和申请审核功能，用来解决学生组队困难的问题，如所示。</p>
<p>团队模块仅对学生开放。发送申请后，仅由团队的队长可见，如图所示。</p>
<p><img alt="团队申请审核" loading="lazy" src="../../images/%E5%9B%A2%E9%98%9F%E7%94%B3%E8%AF%B7%E5%AE%A1%E6%A0%B8.webp"></p>
<h3 id="工具模块">工具模块</h3>
<p>工具模块主要完成字符串处理、异常统一抛出、获取当前登录用户、Excel 读取与生成、Redis 配置、分页处理等功能。</p>
<h2 id="前端">前端</h2>
<p>以创建比赛为例。</p>
<p>通过封装后的单选框 Radio 组件接收比赛类型的输入；通过封装后的 Input 组件接收比赛标题的输入；通过封装后的范围选择器 Range Picker 组件接收比赛时间的输入；通过封装 Input 组件后的 Input Number 组件接收要求团队人数、冠军所得奖励、递减梯度三种数字类型的输入；通过封装 Button 组件进行数据提交。</p>
<p>同时需要为以上组件统一编写数据验证规则，如非空判定和数据长度判定，以及对应的提示，防止提交空数据到后端。因此需要再封装一个 Form 组件统一管理提交、数据验证与数据说明。完成以上内容后效果如图所示。</p>
<p><img alt="创建比赛页面" loading="lazy" src="../../images/%E5%88%9B%E5%BB%BA%E6%AF%94%E8%B5%9B%E9%A1%B5%E9%9D%A2.webp"></p>
<p>本系统前端使用的架构是基于 Flux 的 Redux 架构。Redux 架构遵循三大原则，单一数据源、只允许触发 <code>action</code> 改变 <code>state</code>、<code>reducer</code> 必须为纯函数。<code>action</code> 作为 <code>reducers</code> 和 <code>effects</code> 的触发器，一般由 <code>type</code> 属性和 <code>payload</code> 属性组成。通过 <code>type</code> 属性寻找对应的 <code>reducers</code> 或 <code>effects</code>，将 <code>payload</code> 传递给它们。</p>
<p>因此当老师点击提交时，以上组件的 <code>state</code> 作为 <code>payload</code>，通过 <code>payload</code> 属性和 <code>type</code> 属性来触发 <code>action</code> 调用指定的 <code>effect</code>s。当 <code>action</code> 触发 <code>effects</code> 之后，由 <code>effects</code> 将新增数据通过 HTTPS 请求发送给后端服务器，并接收后端服务器所返回的数据。根据返回数据判断是否成功，使用封装好的全局组件 Message 给出提示。完成以上内容后效果如图所示。</p>
<p><img alt="触发 effects" loading="lazy" src="../../images/%E8%A7%A6%E5%8F%91%20effects.webp"></p>
<p>之所以调用 <code>effects</code> 而不是 <code>reducer</code>，是因为 Redux 架构要求 <code>reducer</code> 必须为纯函数。纯函数有两个特点，一是在执行过程中不会对外界产生影响；二是函数的参数与返回值是一一对应的，同一参数所得到的返回结果应该一致。在向服务端请求数据时，并不能保证参数与返回值一一对应；强制延时也会对外界产生影响，以上操作统称为副作用。因此，存在副作用的逻辑并不能写在 <code>reducer</code> 中。为了保证 <code>reducer</code> 是一个纯函数，同时又需要处理存在副作用的逻辑，那就需要打破“直接”这一特性。而 <code>effect</code> 则用来作为中间层。</p>
<p><code>effects</code> 用于处理异步操作，例如将数据发送给后端服务器。但是根据 Redux 架构的约束，它不可以修改 <code>state</code>，必须再次触发 <code>action</code> 来调用 <code>reducers</code> 达成修改 <code>state</code> 的操作。从宏观角度看，<code>effect</code> 是一层中间件。从局部角度看，<code>effect</code> 就是一个 generator function。</p>
<p><code>effects</code> 向服务器发起异步请求，但整体的执行结果却像同步一样。这是因为使用了 generator function 来处理异步逻辑。异步本质是事件的触发会导致程序跳转。使用 <code>call</code> 只是描述跳转的方式，并没有改变异步的本质，让程序看起来是同步的。</p>
<p><code>effect</code> 的参数有两个，第一个对象可以获取 <code>action</code> 的 <code>payload</code> 字段，第二个对象则是 <code>effect</code> 的原语集，例如 <code>call</code> 和 <code>put</code> 就是原语集之一。其中 <code>yield</code> 和 <code>call</code> 配合可以处理异步逻辑，调用之后程序会进入阻塞状态，以此来等待服务器返回结果。得到结果后解除阻塞状态，继续执行。<code>yield</code> 和 <code>put</code> 配合可以再次触发一个 <code>action</code>，用来在 <code>effect</code> 中使用。</p>
<p>某一事件导致程序进行跳转，就是异步的本质。而 callback 可以看作一种描述异步的方法。generator function 通过修改这种描述的方法来让程序变成同步的样子。</p>
<p>在执行时 generator function 有两方。一方是本身，一方是句柄持有者，也就是框架。当框架调用这个句柄的 <code>next</code> 方法时，generator function 会在下一个 <code>yield</code> 前暂停，并把程序执行权和 <code>yield</code> 后面表达式的值交给框架。框架得到执行权和值后进行处理，结束后再次调用 <code>next</code> 方法，把值交还给 generator function，同时恢复运行。generator function 定义了程序执行流程，并在每一个 <code>yield</code> 告诉框架想要完成的任务。而异步任务真正的执行逻辑则交给了框架执行。</p>
<p>在向服务器发起请求时，对 fetch 进行了封装，便于做好统一异常处理。值得注意的是，在发送 HTTPS 请求时，请求的路径与后端服务器所在的路径并不相同。当请求协议不同、端口不同或路径不同时，我们就认为资源请求“跨域”了。而 HTTPS 的默认规则并不允许进行“跨域”请求。</p>
<p>本项目使用 Nginx 反向代理解决跨域的问题。Nginx 服务器监听来自前端的请求，然后将监听到的请求转发到后端服务器上。这样 Nginx 服务器与后端服务器就是服务器之间在进行通信，不存在跨域问题。</p>
<p>接下来继续看创建比赛的例子，<code>effect</code> 处理完毕后，再次触发 <code>action</code>，将服务器结果和 <code>state</code> 传递给 <code>reducers</code>，<code>reducers</code> 根据结果改变 <code>state</code>，然后通过新的 <code>state</code> 渲染页面。这样创建比赛的请求就完成了。</p>
<p>以多次触发 <code>effect</code> 为例，如图所示。</p>
<p><img alt="触发 reducers" loading="lazy" src="../../images/%E8%A7%A6%E5%8F%91%20reducers.webp"></p>
<p>当 <code>action</code> 被触发后，首先需要解决一些副作用，如请求数据。这些操作需要通过 <code>effect</code> 来完成。然后 <code>effect</code> 会再次触发一个新的 <code>action</code>。这个新的 <code>action</code> 根据开发者的需要来指定 <code>type</code>，既可以被另一个 <code>effect</code> 捕获，然后继续处理副作用，也可以选择由 <code>reducer</code> 捕获，然后结束。无论如何，最终都会来到 <code>reducer</code>。</p>
<p>对于视图层，并没有办法察觉到 <code>effect</code> 和 <code>reducer</code> 的差别。视图层仅描述我想要通过 <code>action</code> 执行的操作，并不关心 <code>action</code> 之后是由 <code>effect</code> 到 <code>reducer</code> 处理，还是直接由 <code>reducer</code> 处理。这样我们就能够将数据逻辑与试图逻辑分离了。</p>
<p><code>reducer</code> 的返回值会作为新的 <code>state</code>，重新渲染组件。而重新渲染组件是通过 <code>connect</code> 方法将新的 <code>state</code> 注入组件，进行渲染的。注入的本质是控制反转。有了 <code>connect</code>，组件不需要再管理数据，仅通过 <code>connect</code> 向框架描述所需的数据即可。React 也是如此，开发者并不需要直接操作 DOM，在组件中写一个返回视图的部分，这个部分就是向 React 描述我想要渲染的内容。React 负责将所描述的内容转换为 DOM，并决定渲染的时机。</p>
<h2 id="后端">后端</h2>
<p>以查询比赛为例。</p>
<p>Nginx 转发的 HTTPS 请求访问后端服务器，Spring Security 拦截请求，进行 Token 验证。如图 所示。</p>
<p><img alt="身份验证" loading="lazy" src="../../images/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81.webp"></p>
<p>Spring Security 框架是用来防御常见攻击的框架。它可以提供身份验证和授权机制，并为 Authentication 和 Authorization 提供了解决方案。另外 Spring Security 框架不需要额外配置如身份验证、JAAS 策略等任何文件，使用十分便捷。</p>
<p>可以通过 <code>SecurityContextHolder</code> 的 <code>getContext</code> 方法获取 <code>SecurityContext</code>。我们通常通过 <code>SecurityContext</code> 获取 <code>Authentication</code> 对象，再从 <code>Authentication</code> 对象中取得 <code>UserDetails</code>，以得到详细的用户信息。</p>
<p>在登录时，Spring Security 通过 <code>UserDetailsService</code> 接口的实现类来获取登录用户的账号密码，然后由 <code>AuthenticationManager</code> 和 <code>AuthenticationProvider</code> 负责进行密码比对。之所以使用这样一个抽象的接口，是因为这样做 Spring Security 可以与持久层完全解耦，因此我保存 <code>UserDetails</code> 的 DAO 层可以是任何东西，数据库、缓存、Properties 文件或内存中都可以。</p>
<p>学科竞赛训练管理系统采用 JWT 方式来处理用户密码认证。JWT 作为无状态的授权校验技术，可以让后端不必保存用户状态。只需要在登录认证通过后，由后端生成一个 Token，把 Token 存入 Redis 并将 Token 返回给前端即可。前端之后的请求都要在请求头中携带 Token，后端接收 Token 后会对 Token 的格式、签名、以及权限进行验证，并与 Redis 中的 Token 进行比对。解析出权限后，根据 PreAuthorize 注解的配置进行权限验证，验证通过后则可以访问 RESTful API。当 Redis 的 Token 过期后，登录失效。具体原理如图所示。</p>
<p><img alt="SpringSecurity 工作流程" loading="lazy" src="../../images/SpringSecurity%20%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.webp"></p>
<p>可见 Spring Security 是由各种各样的 Filter 组成的。<code>WebAsyncManagerIntegrationFilter</code> 负责把 <code>SecurityContext</code> 保存在异步线程中，使异步线程也可以获取到用户信息；<code>SecurityContextPersistenceFilter</code> 负责读取或创建 <code>SecurityContext</code>，并存入 <code>SecurityContextHolder</code> 中；<code>HeaderWriterFilter</code> 负责给 HTTPS 请求添加一些请求头，如 X-Content-<code>type</code>-Options 等；<code>LogoutFilter</code> 负责处理退出登录；<code>CorsFilter</code> 负责跨域问题；<code>TokenFilter</code> 是自定义 Filter，负责验证 Token；<code>RequestCacheAwareFilter</code> 负责缓存 request 请求；<code>SecurityContextHolderAwareRequestFilter</code> 负责对 request 进行包装；<code>AnonymousAuthenticationFilter</code> 负责处理匿名登录；<code>SessionManagementFilter</code> 负责限制用户发起多次会话；<code>ExceptionTranslationFilter</code> 负责异常拦截，只处理未登录状态下访问受保护资源和登录后访问权限不足资源的异常；<code>FilterSecurityInterceptor</code> 负责授权验证，<code>FilterSecurityInterceptor</code> 通过 <code>SecurityContextHolder</code> 来得到 <code>Authentication</code> 对象，然后从 <code>Authentication</code> 对象中得到用户权限，将权限与用户想要访问的资源权限进行比对。</p>
<p>通过 SpringSecurity 验证后，即可访问 RESTful API。如图所示。</p>
<p><img alt="访问 RESTfulAPI" loading="lazy" src="../../images/%E8%AE%BF%E9%97%AE%20RESTfulAPI.webp"></p>
<p>RESTful API 是一种遵守对方法、风格以及对体系结构的一些列约束的 API。一些后端服务可能是有着一个类似 <code>/getxxx/1</code>，使用 GET 或 POST 请求的服务，这样的可以称之为 RPC。因为无法知道应该如何与此服务进行交互，如果想要使用这样的项目那必须编写一个帮助文档来说明每一个服务的作用。很多人也将此称为 RESTful，但是它并不是。Roy Fielding 解释了 RESTful：只有由超文本驱动的 API，才能被称作是 RESTful API。由此可见，首先，要做到每个 URI 标识一种网络资源。资源是网络上的一个信息，如文本、图片或服务。其次，客户端与服务器之间以某种表现层传递这种资源。表现层是资源的表现形式，如图片可以使用 svg 格式，文本可以使用 json 格式表现出来。网址最后的 <code>.html</code> 实际是一种表现层，如果加上 <code>.html</code> 并不能说明这是资源的位置，只能说明这是资源的一种表现形式。最后，客户端通过 GET、POST 等原语操作对 URI 所代表的资源进行交互，以此将表现层的状态进行改变。HTTPS 协议是无状态协议，状态都会在服务端保存。因此想要改变表现层的状态，则只能使用 HTTPS 协议。只有做到以上三点，才可以被称为 RESTful API。</p>
<p>RESTful API 会对前端发来的数据进行验证，对于不需要返回数据的 API 来说，通过验证则将成功的信息返回，随后异步调用 Service 进行业务逻辑处理。若前端需要返回数据，如比赛查询需要返回查询结果，则同步调用 Service 处理，等待数据返回后将数据按前端需要的格式封装，将其返回。如图所示。</p>
<p><img alt="结束服务调用" loading="lazy" src="../../images/%E7%BB%93%E6%9D%9F%E6%9C%8D%E5%8A%A1%E8%B0%83%E7%94%A8.webp"></p>
<p>当数据通过验证后，则会传递给 Service 进行业务逻辑处理。在浏览比赛接口中，数据是查询条件和分页数据，Service 负责处理动态查询。</p>
<p>通过 Spring Data JPA 中的 <code>JpaSpecificationExecutor</code> 接口来实现动态查询。<code>JpaSpecificationExecutor</code> 接口提供了五种动态查询方法，分别是查询单条数据、查询多条数据、查询多条数据并排序、查询多条数据并分页与排序、查询数量。在浏览比赛接口中用到了分页并排序。</p>
<p>此方法定义如下：</p>
<p><code>Page&lt;T&gt; findAll(@Nullable Specification&lt;T&gt; var1, Pageable var2);</code></p>
<p>排序和分页只需要创建 <code>Pageable</code> 实例即可，如下所示：</p>
<p><code>PageRequest.of(当前页数, 每页展示数量, Sort.by(排序方式, 排序字段));</code></p>
<p>动态查询则创建 <code>Specification&lt;T&gt;</code> 实例即可，如下所示：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Specification</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="w"> </span><span class="n">specification</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Specification</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="n">root</span><span class="p">,</span><span class="w"> </span><span class="n">criteriaQuery</span><span class="p">,</span><span class="w"> </span><span class="n">criteriaBuilder</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ArrayList</span><span class="o">&lt;</span><span class="n">Predicate</span><span class="o">&gt;</span><span class="w"> </span><span class="n">andQuery</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">某查询条件</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Path</span><span class="o">&lt;</span><span class="n">字段类型</span><span class="o">&gt;</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">root</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">字段名</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Predicate</span><span class="w"> </span><span class="n">idEqual</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">criteriaBuilder</span><span class="p">.</span><span class="na">equal</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">查询条件</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">andQuery</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">idEqual</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err">……</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Predicate</span><span class="o">[]</span><span class="w"> </span><span class="n">andPredicates</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">andQuery</span><span class="p">.</span><span class="na">toArray</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Predicate</span><span class="o">[</span><span class="n">0</span><span class="o">]</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">criteriaBuilder</span><span class="p">.</span><span class="na">and</span><span class="p">(</span><span class="n">andPredicates</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">};</span><span class="w">
</span></span></span></code></pre></div><p>最后只需要将 <code>Specification&lt;T&gt;</code> 实例和 <code>Pageable</code> 实例作为参数即可实现动态查询。</p>
<p>以上是在缓存未命中的情况下才会执行，并且会将类地址、方法名、包名和参数作为键，将返回的结果作为值，存入缓存。若缓存存在，则无需执行以上代码，直接从缓存中取出数据即可。</p>
<p>当 Service 执行完毕后，会将返回的数据封装成前端所需的格式，存入请求体中返回。</p>
<h1 id="测试">测试</h1>
<h2 id="性能瓶颈">性能瓶颈</h2>
<h3 id="产生原因">产生原因</h3>
<p>因为本次测试以最坏情况为准，假设每人都会在查询团队之前创建新的团队，导致团队缓存失效，Redis 被穿透。因此涉及到团队查询的接口都会从数据库中进行查询，导致耗时较高。如表所示。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>接口名</strong></th>
          <th style="text-align: center"><strong>获取用户信息</strong></th>
          <th style="text-align: center"><strong>获取战斗力变化数据</strong></th>
          <th style="text-align: center"><strong>获取尚未结束的比赛</strong></th>
          <th style="text-align: center"><strong>获取前十条公告</strong></th>
          <th style="text-align: center"><strong>获取战斗力排行榜</strong></th>
          <th style="text-align: center"><strong>获取团队信息</strong></th>
          <th style="text-align: center"><strong>创建团队</strong></th>
          <th style="text-align: center"><strong>获取比赛信息</strong></th>
          <th style="text-align: center"><strong>比赛报名</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">平均值</td>
          <td style="text-align: center">65ms</td>
          <td style="text-align: center">34ms</td>
          <td style="text-align: center">33ms</td>
          <td style="text-align: center">31ms</td>
          <td style="text-align: center">29ms</td>
          <td style="text-align: center">1169ms</td>
          <td style="text-align: center">52ms</td>
          <td style="text-align: center">40ms</td>
          <td style="text-align: center">1094ms</td>
      </tr>
      <tr>
          <td style="text-align: center">90%</td>
          <td style="text-align: center">154ms</td>
          <td style="text-align: center">104ms</td>
          <td style="text-align: center">103ms</td>
          <td style="text-align: center">95ms</td>
          <td style="text-align: center">97ms</td>
          <td style="text-align: center">3146ms</td>
          <td style="text-align: center">122ms</td>
          <td style="text-align: center">126ms</td>
          <td style="text-align: center">1716ms</td>
      </tr>
  </tbody>
</table>
<p>因获取团队信息和比赛报名接口都涉及到了查询团队信息，因此在缓存被穿透的情况下耗时异常的高。</p>
<h3 id="优化方案">优化方案</h3>
<p>因本系统性能目前已达到预计要求，所以仅提出以下理论优化方案，不做具体实现。</p>
<p>因查询团队性能瓶颈是由 Redis 缓存失效导致，所以可以建立二级缓存。当缓存失效时，为数据表加锁，保证只有一个进程进行数据库的查询。其他进程发现加锁后返回二级缓存中的数据，查询完毕后再更新对应的缓存即可。尽管只有拿到锁的用户可以看到最新数据，一部分人没有及时看到最新的数据，但是这部分人只是少数，只需要刷新一下就可以看到最新数据了。</p>
<p>用以上方案缓解 Redis 失效问题后再次压测，系统指标一切正常，实际 TPS 大于预计的 275TPS，90% 的请求最大响应时间为 186ms，平均值为 75ms，可以满足学生报名需求。</p>
]]></content:encoded>
    </item>
    <item>
      <title>搜索引擎使用教程</title>
      <link>https://www.zhangtianci.cn/posts/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</link>
      <pubDate>Tue, 24 Dec 2019 13:03:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</guid>
      <description>&lt;h1 id=&#34;前言&#34;&gt;前言&lt;/h1&gt;
&lt;p&gt;尽管人们经常使用搜索引擎，但有时候对于一些问题可能无法高效搜索，会搜出来许多无用的干扰信息。&lt;/p&gt;
&lt;p&gt;对于知识工作者而言，搜索这个技能可以说是渗透到学习闭环的各个环节。因此学习一下搜索引擎的使用技巧，提高效率，可以避免时间浪费。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<p>尽管人们经常使用搜索引擎，但有时候对于一些问题可能无法高效搜索，会搜出来许多无用的干扰信息。</p>
<p>对于知识工作者而言，搜索这个技能可以说是渗透到学习闭环的各个环节。因此学习一下搜索引擎的使用技巧，提高效率，可以避免时间浪费。</p>
<h1 id="搜索引擎推荐">搜索引擎推荐</h1>
<h2 id="google">Google</h2>
<p>全球第一，即使搜索中文关键词也强于百度。</p>
<h2 id="百度">百度</h2>
<p>中国第一，搜索娱乐新闻等十分好用。</p>
<p>计算机专业不推荐使用，大多数技术网站都对百度做了反爬虫，百度是无法搜索到的。</p>
<h2 id="bing">Bing</h2>
<p>针对中文进行了优化，是无法访问 Google 时最好的替代品。</p>
<p>推荐国际版，无广告。</p>
<h1 id="搜索技巧">搜索技巧</h1>
<h2 id="google-1">Google</h2>
<p><img alt="Google 高级搜索.webp" loading="lazy" src="../../images/Google%20%E9%AB%98%E7%BA%A7%E6%90%9C%E7%B4%A2.webp"></p>
<p>如上，这是 Google 提供的高级搜索方式。</p>
<h2 id="百度-1">百度</h2>
<p><img alt="百度高级搜索.webp" loading="lazy" src="../../images/%E7%99%BE%E5%BA%A6%E9%AB%98%E7%BA%A7%E6%90%9C%E7%B4%A2.webp"></p>
<p>如上，这是百度提供的高级搜索方式。</p>
<h2 id="bing-1">Bing</h2>
<p><img alt="Bing" loading="lazy" src="../../images/Bing%20%E9%AB%98%E7%BA%A7%E6%90%9C%E7%B4%A2.webp"></p>
<p>Bing 并未提供可视化的高级搜索方式。</p>
<h2 id="搜索技巧-1">搜索技巧</h2>
<p>一般来说通过使用各搜索引擎提供的可视化高级搜索工具可以解决大多数需求。</p>
<p>但有一些好用的搜索引擎并没有提供可视化高级搜索工具，并且可视化高级搜索工具用起来也略微繁琐。</p>
<p>那么以下内容则是介绍不通过可视化的方式来进行高级搜索的方法。</p>
<h3 id="关键字选取">关键字选取</h3>
<p>搜索最重要的就是关键词。现在搜索引擎对于语义搜索、情景搜索、智能推荐等已经做得很好了，以前自行提取关键词的方式并不适用于现在。</p>
<p>因此我们在进行搜索的时候，既可以直接输入问题，也可以自行提取 1 - 3 个关键词进行搜索。</p>
<p>另外也可以用不同的语言来搜索。</p>
<p><img alt="英文关键字选取.webp" loading="lazy" src="../../images/%E8%8B%B1%E6%96%87%E5%85%B3%E9%94%AE%E5%AD%97%E9%80%89%E5%8F%96.webp"></p>
<p><img alt="中文关键字选取.webp" loading="lazy" src="../../images/%E4%B8%AD%E6%96%87%E5%85%B3%E9%94%AE%E5%AD%97%E9%80%89%E5%8F%96.webp"></p>
<h3 id="完全匹配">完全匹配</h3>
<p>用引号将需要完全匹配的字词引起：<code>&quot;可口可乐&quot;</code>，即可进行完全匹配，并不会搜到 <code>可乐</code> 这样的关键词。</p>
<p><img alt="完全匹配.webp" loading="lazy" src="../../images/%E5%AE%8C%E5%85%A8%E5%8C%B9%E9%85%8D.webp"></p>
<h3 id="任意匹配">任意匹配</h3>
<p>在所需字词之间添加 <code>OR</code>：<code>可口 OR 百事</code>。</p>
<p><img alt="任意匹配.webp" loading="lazy" src="../../images/%E4%BB%BB%E6%84%8F%E5%8C%B9%E9%85%8D.webp"></p>
<h3 id="不包含匹配">不包含匹配</h3>
<p>在不需要的字词前添加一个减号：<code>-可口</code>。</p>
<p><img alt="不包含匹配.webp" loading="lazy" src="../../images/%E4%B8%8D%E5%8C%85%E5%90%AB%E5%8C%B9%E9%85%8D.webp"></p>
<h3 id="数字范围">数字范围</h3>
<p>在数字之间加上两个句号并添加度量单位：<code>10..35 斤</code>、<code>300..500 元</code>、<code>2010..2011 年</code>。</p>
<p><img alt="数字匹配.webp" loading="lazy" src="../../images/%E6%95%B0%E5%AD%97%E5%8C%B9%E9%85%8D.webp"></p>
<h3 id="不常用操作">不常用操作</h3>
<p>以下操作较为繁琐，也很少用。</p>
<p><code>filetype</code> 搜索对应类型的文件。</p>
<p><code>site</code> 在某个网站内搜索。</p>
<p><code>allintitle</code> 搜索网页标题中包含关键词的结果。</p>
<p><code>allintext</code> 搜索网页文本中包含关键词的结果。</p>
<p><code>allinurl</code> 搜索网页网址中包含关键词的结果。</p>
<p><code>allinanchor</code> 搜索指向网页的链接中包含关键词的结果。</p>
<p><code>related</code> 搜索相关网站。</p>
<p><code>index of</code> 可以突破网站入口下载。</p>
<p><img alt="不常用操作.webp" loading="lazy" src="../../images/%E4%B8%8D%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C.webp"></p>
]]></content:encoded>
    </item>
    <item>
      <title>关于</title>
      <link>https://www.zhangtianci.cn/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://www.zhangtianci.cn/about/</guid>
      <description>about</description>
      <content:encoded><![CDATA[<p><img alt="关于" loading="lazy" src="../%E5%85%B3%E4%BA%8E.webp"></p>
<h1 id="介绍">介绍</h1>
<p>欢迎访问我的博客，我是一名 2020 年毕业的后端研发工程师，目前在北京工作，就职于京东。</p>
<p>工作中主要负责支付营销工具的开发与稳定性保障。在这期间，支撑过若干次 618、1111 大促，以及春晚亿级流量营销，有较为丰富的亿级流量高并发系统设计与实战经验。相信大家在京东购物时一定都用过我负责的系统服务。</p>
<p>平时爱好读书与思考，研究一些让我好奇的东西。偶尔会玩玩尤克里里与 Nintendo Switch，享受一个人的平静生活。</p>
<h1 id="技能清单">技能清单</h1>
<ul>
<li>语言: Java、Lua、Groovy、C、SQL、Shell、HTML、CSS、JavaScript 等</li>
<li>版本管理: Git</li>
<li>数据库: MySQL、CDS(自研分布式数据库)、PostgreSQL、JimDB(自研 NoSQL 数据库及缓存)、Redis、HBase、Elasticsearch</li>
<li>常用 Linux 发行版: CentOS、Debian 等</li>
<li>其他常用中间件、开发组件、工具: Spring Boot、MyBatis、Spring Data JPA、JSF(自研微服务平台，类似 Dubbo 等)、EasyJob(自研分布式调度平台，类似 ElasticJob)、Kafka、JMQ(自研分布式事件流平台，类似 Kafka)、Flink、NFS、HDFS、Docker、Caffeine Cache、Tomcat、Nginx、Netty 等</li>
</ul>
<h1 id="博客">博客</h1>
<p>我一直认为，定期记录自己的生活与工作，是一件很有意义的事。所以，不论是生活中的大太阳还是小乌云，我都会在博客中记录下来。若干年后回过头来看看自己的点点滴滴，一定会很有成就感。</p>
<p>除了分享自己的生活，我还会记录自己在职业发展、学习成长路上的经验与心得。学习路上应该做好复盘，时时刻刻整理自己的收获，才可以走得更远。</p>
<p>如果各位在浏览过程中，发现有些方面理解的有偏差，或者想要交流，可以直接留言或者发邮件。非常期待各位的指教。</p>
<h1 id="网站信息">网站信息</h1>
<ul>
<li>名称：张天赐的小世界</li>
<li>描述：一个记录自己生活历程的纸飞机</li>
<li>链接：https://www.zhangtianci.cn</li>
</ul>
<h1 id="排版">排版</h1>
<p>全博客使用 <a href="https://github.com/sparanoid/chinese-copywriting-guidelines/blob/master/README.zh-CN.md">中文文案排版指北</a> 进行排版。</p>
<h1 id="许可协议">许可协议</h1>
<p>本博客的作品采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议</a> 进行许可。</p>
<p>使用者可以对本创作进行转载、节选、混编、二次创作，但不得运用于商业目的，且使用时须进行署名，采用本创作的内容必须同样采用本协议进行授权。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
