This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -167,13 +167,13 @@ function hide_canvas() {
<p><strong>加载缓慢的网站,会受到搜索排名算法的惩罚,从而导致网站排名下降。</strong> 因此加载的快慢是性能优化是否合理的一个非常直观的判断因素,但性能指标不仅仅包括单次请求的速度,它还包含更多因素。</p>
<p>接下来看一下,都有哪些衡量指标能够帮我们进行决策。</p>
<h1>衡量指标有哪些?</h1>
<p><img src="assets/CgqCHl8L01eAWBW8AADH_LPiVzY445.png" alt="image" /></p>
<p><img src="assets/CgqCHl8L01eAWBW8AADH_LPiVzY445.png" alt="png" /></p>
<h2>1. 吞吐量和响应速度</h2>
<p>分布式的高并发应用并不能把单次请求作为判断依据,它往往是一个统计结果。其中最常用的衡量指标就是吞吐量和响应速度,而这两者也是考虑性能时非常重要的概念。要理解这两个指标的意义,我们可以类比为交通环境中的十字路口。</p>
<p>在交通非常繁忙的情况下,十字路口是典型的瓶颈点,当红绿灯放行时间非常长时,后面往往会排起长队。</p>
<p>从我们开车开始排队,到车经过红绿灯,这个过程所花费的时间,就是<strong>响应时间。</strong></p>
<p>当然,我们可以适当地调低红绿灯的间隔时间,这样对于某些车辆来说,通过时间可能会短一些。但是,如果信号灯频繁切换,反而会导致单位时间内通过的车辆减少,换一个角度,我们也可以认为这个十字路口的车辆吞吐量减少了。</p>
<p><img src="assets/CgqCHl8L02KAdjZ_AAB-AStGwkw402.png" alt="image" /></p>
<p><img src="assets/CgqCHl8L02KAdjZ_AAB-AStGwkw402.png" alt="png" /></p>
<p>像我们平常开发中经常提到的QPS 代表每秒查询的数量TPS 代表每秒事务的数量HPS 代表每秒的 HTTP 请求数量等,这都是常用的与吞吐量相关的量化指标。</p>
<p><strong>在性能优化的时候,我们要搞清楚优化的目标,到底是吞吐量还是响应速度。</strong> 有些时候,虽然响应速度比较慢,但整个吞吐量却非常高,比如一些数据库的批量操作、一些缓冲区的合并等。虽然信息的延迟增加了,但如果我们的目标就是吞吐量,那么这显然也可以算是比较大的性能提升。</p>
<p>一般情况下,我们认为:</p>
@@ -190,7 +190,7 @@ function hide_canvas() {
<p>除非服务在一段时间内出现了严重的问题,否则平均响应时间都会比较平缓。因为高并发应用请求量都特别大,所以长尾请求的影响会被很快平均,导致很多用户的请求变慢,但这不能体现在平均耗时指标中。</p>
<p>为了解决这个问题,另外一个比较常用的指标,就是<strong>百分位数(Percentile)</strong></p>
<p><strong>2百分位数</strong></p>
<p><img src="assets/Ciqc1F8L032AC_6sAABe7N44eqs490.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8L032AC_6sAABe7N44eqs490.png" alt="png" /></p>
<p>这个也比较好理解。我们圈定一个时间范围,把每次请求的耗时加入一个列表中,然后按照从小到大的顺序将这些时间进行排序。这样,我们取出特定百分位的耗时,这个数字就是 TP 值。可以看到TP 值(Top Percentile)和中位数、平均数等是类似的,都是一个统计学里的术语。</p>
<p>它的意义是,超过 N% 的请求都在 X 时间内返回。比如 TP90 = 50ms意思是超过 90th 的请求,都在 50ms 内返回。</p>
<p><strong>这个指标也是非常重要的,它能够反映出应用接口的整体响应情况</strong>。比如,某段时间若发生了长时间的 GC那它的某个时间段之上的指标就会产生严重的抖动但一些低百分位的数值却很少有变化。</p>
@@ -219,7 +219,7 @@ function hide_canvas() {
<p>基准测试(Benchmark)并不是简单的性能测试,是用来测试某个程序的最佳性能。</p>
<p>应用接口往往在刚启动后都有短暂的超时。在测试之前,我们需要对应用进行预热,消除 JIT 编译器等因素的影响。而在 Java 里就有一个组件,即 JMH就可以消除这些差异。</p>
<h1>注意点</h1>
<p><img src="assets/Ciqc1F8NLD2APjhfAAFDL-uVLMw32.jpeg" alt="1.jpeg" /></p>
<p><img src="assets/Ciqc1F8NLD2APjhfAAFDL-uVLMw32.jpeg" alt="png" /></p>
<h2>1. 依据数字而不是猜想</h2>
<p>有些同学对编程有很好的感觉,能够靠猜测列出系统的瓶颈点,这种情况固然存在,但却非常不可取。复杂的系统往往有多个影响因素,我们应将性能分析放在第一位,把性能优化放在次要位置,直觉只是我们的辅助,但不能作为下结论的工具。</p>
<p>进行性能优化时,我们一般会把分析后的结果排一个优先级(根据难度和影响程度),从大处着手,首先击破影响最大的点,然后将其他影响因素逐一击破。</p>

View File

@@ -165,7 +165,7 @@ function hide_canvas() {
<p>了解了优化目标后,那接下来应该从哪些方面入手呢?本课时主要侧重于理论分析,我们从整体上看一下 Java 性能优化都有哪些可以遵循的规律。本课主讲理论,关于实践,后面的课时会用较多的案例来细化本课时的知识点,适合反复思考和归纳。</p>
<h1>性能优化的 7 类技术手段</h1>
<p>性能优化根据优化的类别,分为<strong>业务优化</strong><strong>技术优化</strong>。业务优化产生的效果也是非常大的,但它属于产品和管理的范畴。同作为程序员,在平常工作中,我们面对的优化方式,主要是通过一系列的技术手段,来完成对既定的优化目标。这一系列的技术手段,我大体归纳为如图以下 7 类:</p>
<p><img src="assets/CgqCHl8RPjKAXfRcAABjINYVRzo486.png" alt="image" /></p>
<p><img src="assets/CgqCHl8RPjKAXfRcAABjINYVRzo486.png" alt="png" /></p>
<p>可以看到,优化方式集中在对计算资源和存储资源的规划上。优化方法中有多种用空间换时间的方式,但只照顾计算速度,而不考虑复杂性和空间问题,也是不可取的。我们要做的,就是在照顾性能的前提下,达到资源利用的最优状态。</p>
<p>接下来,我简要介绍一下这 7 种优化方式。如果你感觉比较枯燥,那也没关系,我们本课时的目的,就是让你的脑海里有一个总分的概念,以及对理论基础有一个整体的认识。</p>
<h2>1. 复用优化</h2>
@@ -177,7 +177,7 @@ function hide_canvas() {
<li>缓存Cache常见于对已读取数据的复用通过将它们缓存在相对高速的区域<strong>缓存主要针对的是读操作</strong></li>
</ul>
<p>与之类似的,是对于对象的池化操作,比如数据库连接池、线程池等,在 Java 中使用得非常频繁。由于这些对象的创建和销毁成本都比较大,我们在使用之后,也会将这部分对象暂时存储,下次用的时候,就不用再走一遍耗时的初始化操作了。</p>
<p><img src="assets/CgqCHl8RPjyAAIEGAABE6kBab04139.png" alt="image" /></p>
<p><img src="assets/CgqCHl8RPjyAAIEGAABE6kBab04139.png" alt="png" /></p>
<h2>2. 计算优化</h2>
<p>1并行执行</p>
<p>现在的 CPU 发展速度很快,绝大多数硬件,都是多核。要想加快某个任务的执行,最快最优的解决方式,就是让它并行执行。并行执行有以下三种模式。</p>
@@ -190,7 +190,7 @@ function hide_canvas() {
<p>异步操作可以方便地支持横向扩容,也可以缓解瞬时压力,使请求变得平滑。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性的,体验更加友好。</p>
<p>3惰性加载</p>
<p>最后一种,就是使用一些常见的设计模式来优化业务,提高体验,比如单例模式、代理模式等。举个例子,在绘制 Swing 窗口的时候,如果要显示比较多的图片,就可以先加载一个占位符,然后通过后台线程慢慢加载所需要的资源,这就可以避免窗口的僵死。</p>
<p><img src="assets/Ciqc1F8RQ6mACAPJAACrAhiYBdY56.jpeg" alt="Lark20200717-142148.jpeg" /></p>
<p><img src="assets/Ciqc1F8RQ6mACAPJAACrAhiYBdY56.jpeg" alt="png" /></p>
<h2>3. 结果集优化</h2>
<p>接下来介绍一下对结果集的优化。举个比较直观的例子,我们都知道 XML 的表现形式是非常好的,那为什么还有 JSON 呢?除了书写要简单一些,一个重要的原因就是它的体积变小了,传输效率和解析效率变高了,像 Google 的 Protobuf体积就更小了一些。虽然可读性降低但在一些高并发场景下如 RPC能够显著提高效率这是典型的对结果集的优化。</p>
<p>这是由于我们目前的 Web 服务,都是 C/S 模式。数据从服务器传输到客户端,需要分发多份,这个数据量是急剧膨胀的,每减少一小部分存储,都会有比较大的传输性能和成本提升。</p>
@@ -198,14 +198,14 @@ function hide_canvas() {
<p>了解了这个道理,我们就能看到对于结果集优化的一般思路,你要尽量保持返回数据的精简。一些客户端不需要的字段,那就在代码中,或者直接在 SQL 查询中,就把它去掉。</p>
<p>对于一些对时效性要求不高,但对处理能力有高要求的业务。我们要吸取缓冲区的经验,尽量减少网络连接的交互,采用批量处理的方式,增加处理速度。</p>
<p>结果集合很可能会有二次使用,你可能会把它加入缓存中,但依然在速度上有所欠缺。这个时候,就需要对数据集合进行处理优化,采用索引或者 Bitmap 位图等方式,加快数据访问速度。</p>
<p><img src="assets/CgqCHl8RPkyAUD8cAAAz42owPXs398.png" alt="image" /></p>
<p><img src="assets/CgqCHl8RPkyAUD8cAAAz42owPXs398.png" alt="png" /></p>
<h2>4. 资源冲突优化</h2>
<p>我们在平常的开发中,会涉及很多共享资源。这些共享资源,有的是单机的,比如一个 HashMap有的是外部存储比如一个数据库行有的是单个资源比如 Redis 某个 key 的Setnx有的是多个资源的协调比如事务、分布式事务等。</p>
<p>现实中的性能问题和锁相关的问题是非常多的。大多数我们会想到数据库的行锁、表锁、Java 中的各种锁等。在更底层,比如 CPU 命令级别的锁、JVM 指令级别的锁、操作系统内部锁等,可以说无处不在。</p>
<p>只有并发,才能产生资源冲突。也就是在同一时刻,只能有一个处理请求能够获取到共享资源。解决资源冲突的方式,就是加锁。再比如事务,在本质上也是一种锁。</p>
<p>按照锁级别,锁可分为乐观锁和悲观锁,乐观锁在效率上肯定是更高一些;按照锁类型,锁又分为公平锁和非公平锁,在对任务的调度上,有一些细微的差别。</p>
<p>对资源的争用,会造成严重的性能问题,所以会有一些针对无锁队列之类的研究,对性能的提升也是巨大的。</p>
<p><img src="assets/Ciqc1F8RPlSAMb5AAABe184UTQ0081.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8RPlSAMb5AAABe184UTQ0081.png" alt="png" /></p>
<h2>5. 算法优化</h2>
<p>算法能够显著提高复杂业务的性能,但在实际的业务中,往往都是变种。由于存储越来越便宜,在一些 CPU 非常紧张的业务中,往往采用空间换取时间的方式,来加快处理速度。</p>
<p>算法属于代码调优,代码调优涉及很多编码技巧,需要使用者对所使用语言的 API 也非常熟悉。有时候,对算法、数据结构的灵活使用,也是代码优化的一个重要内容。比如,常用的降低时间复杂度的方式,就有递归、二分、排序、动态规划等。</p>

View File

@@ -173,7 +173,7 @@ function hide_canvas() {
<p>具体情况如下。</p>
<h2>1.top 命令 —— CPU 性能</h2>
<p>如下图,当进入 top 命令后,按 1 键即可看到每核 CPU 的运行指标和详细性能。</p>
<p><img src="assets/Ciqc1F8VRhOAOZZkAAIR0PdGn-M708.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F8VRhOAOZZkAAIR0PdGn-M708.png" alt="png" /></p>
<p>CPU 的使用有多个维度的指标,下面分别说明:</p>
<ul>
<li>us 用户态所占用的 CPU 百分比,即引用程序所耗费的 CPU</li>
@@ -188,7 +188,7 @@ function hide_canvas() {
<p>一般地,我们比较关注空闲 CPU 的百分比,它可以从整体上体现 CPU 的利用情况。</p>
<h2>2.负载 —— CPU 任务排队情况</h2>
<p>如果我们评估 CPU 任务执行的排队情况那么需要通过负载load来完成。除了 top 命令,使用 uptime 命令也能够查看负载情况load 的效果是一样的,分别显示了最近 1min、5min、15min 的数值。</p>
<p><img src="assets/Ciqc1F8VRlWAKi0UAABUtUrc7Ec737.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F8VRlWAKi0UAABUtUrc7Ec737.png" alt="png" /></p>
<p>如上图所示,以单核操作系统为例,将 CPU 资源抽象成一条单向行驶的马路,则会发生以下三种情况:</p>
<ul>
<li>马路上的车只有 4 辆车辆畅通无阻load 大约是 0.5</li>
@@ -205,7 +205,7 @@ function hide_canvas() {
<p>所以,对于一个 load 到了 10却是 16 核的机器,你的系统还远没有达到负载极限。</p>
<h2>3.vmstat —— CPU 繁忙程度</h2>
<p>要看 CPU 的繁忙程度,可以通过 vmstat 命令,下图是 vmstat 命令的一些输出信息。</p>
<p><img src="assets/CgqCHl8VeDqAOXoUAAL5_mAD--A654.gif" alt="1111.gif" /></p>
<p><img src="assets/CgqCHl8VeDqAOXoUAAL5_mAD--A654.gif" alt="png" /></p>
<p>比较关注的有下面几列:</p>
<ul>
<li><strong>b</strong> 如果系统有负载问题,就可以看一下 b 列Uninterruptible Sleep它的意思是等待 I/O可能是读盘或者写盘动作比较多</li>
@@ -224,7 +224,7 @@ nonvoluntary_ctxt_switches: 171204
<p>我们在平常写完代码后,比如写了一个 C++ 程序,去查看它的汇编,如果看到其中的内存地址,并不是实际的物理内存地址,那么应用程序所使用的,就是<strong>逻辑内存</strong>。学过计算机组成结构的同学应该都有了解。</p>
<p>逻辑地址可以映射到两个内存段上:<strong>物理内存</strong><strong>虚拟内存</strong>,那么整个系统可用的内存就是两者之和。比如你的物理内存是 4GB分配了 8GB 的 SWAP 分区,那么应用可用的总内存就是 12GB。</p>
<h2>1. top 命令</h2>
<p><img src="assets/Ciqc1F8VRpyAJEDBAAGXn95jReA806.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8VRpyAJEDBAAGXn95jReA806.png" alt="png" /></p>
<p>如上图所示,我们看一下内存的几个参数,从 top 命令可以看到几列数据,注意方块框起来的三个区域,解释如下:</p>
<ul>
<li><strong>VIRT</strong> 这里是指虚拟内存,一般比较大,不用做过多关注;</li>
@@ -267,14 +267,14 @@ cache_alignment : 64
<p>这样,启动时虽然慢了些,但运行时的性能会增加。</p>
<h1>I/O</h1>
<p>I/O 设备可能是计算机里速度最慢的组件了,它指的不仅仅是硬盘,还包括外围的所有设备。那硬盘有多慢呢?我们不去探究不同设备的实现细节,直接看它的写入速度(数据未经过严格测试,仅作参考)。</p>
<p><img src="assets/Ciqc1F8VRxaAK34SAAHTZp7R44c733.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F8VRxaAK34SAAHTZp7R44c733.png" alt="png" /></p>
<p>如上图所示,可以看到普通磁盘的随机写与顺序写相差非常大,但顺序写与 CPU 内存依旧不在一个数量级上。</p>
<p><strong>缓冲区依然是解决速度差异的唯一工具</strong>,但在极端情况下,比如断电时,就产生了太多的不确定性,这时这些缓冲区,都容易丢。由于这部分内容的篇幅比较大,我将在第 06 课时专门讲解。</p>
<h2>1. iostat</h2>
<p>最能体现 I/O 繁忙程度的,就是 top 命令和 vmstat 命令中的 wa%。如果你的应用写了大量的日志I/O wait 就可能非常高。</p>
<p><img src="assets/CgqCHl8VRzqALt_DAASmJQkN7Ro492.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8VRzqALt_DAASmJQkN7Ro492.png" alt="png" /></p>
<p>很多同学反馈到,不知道有哪些便捷好用的查看磁盘 I/O 的工具,其实 iostat 就是。你可以通过 sysstat 包进行安装。</p>
<p><img src="assets/CgqCHl8VR0KARZVeAAWCFxfk75s510.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl8VR0KARZVeAAWCFxfk75s510.png" alt="png" /></p>
<p>上图中的指标详细介绍如下所示。</p>
<ul>
<li><strong>%util</strong>:我们非常关注这个数值,通常情况下,这个数字超过 80%,就证明 I/O 的负荷已经非常严重了。</li>

View File

@@ -167,7 +167,7 @@ function hide_canvas() {
<h1>nmon —— 获取系统性能数据</h1>
<p>除了在上一课时中介绍的 top、free 等命令,还有一些将资源整合在一起的监控工具,</p>
<p>nmon 便是一个老牌的 Linux 性能监控工具,它不仅有漂亮的监控界面(如下图所示),还能产出细致的监控报表。</p>
<p><img src="assets/CgqCHl8X2gWANM2wAAkEF7IjoMg031.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8X2gWANM2wAAkEF7IjoMg031.png" alt="png" /></p>
<p>我在对应用做性能评估时,通常会加上 nmon 的报告,这会让测试结果更加有说服力。你在平时工作中也可如此尝试。</p>
<p>上一课时介绍的一些操作系统性能指标,都可从 nmon 中获取。它的监控范围很广,包括 CPU、内存、网络、磁盘、文件系统、NFS、系统资源等信息。</p>
<p>nmon 在 sourceforge 发布,我已经下载下来并上传到了仓库中。比如我的是 CentOS 7 系统,选择对应的版本即可执行。</p>
@@ -182,12 +182,12 @@ function hide_canvas() {
root 2228 1 0 16:33 pts/0 00:00:00 ./nmon_x86_64_centos7 -f -s 5 -c 12 -m .
</code></pre>
<p>使用 nmonchart 工具(见仓库),即可生成 html 文件。下面是生成文件的截图。</p>
<p><img src="assets/Ciqc1F8X2m6ABh9lAAqiFOnIMT0061.png" alt="Drawing 1.png" />
<p><img src="assets/Ciqc1F8X2m6ABh9lAAqiFOnIMT0061.png" alt="png" />
nmonchart 报表</p>
<h1>jvisualvm —— 获取 JVM 性能数据</h1>
<p>jvisualvm 原是随着 JDK 发布的一个工具Java 9 之后开始单独发布。通过它,可以了解应用在运行中的内部情况。我们可以连接本地或者远程的服务器,监控大量的性能数据。</p>
<p>通过插件功能jvisualvm 能获得更强大的扩展。如下图所示,建议把所有的插件下载下来进行体验。</p>
<p><img src="assets/CgqCHl8X3PeAPufLAAPBFcBR8qY801.png" alt="Drawing 2.png" />
<p><img src="assets/CgqCHl8X3PeAPufLAAPBFcBR8qY801.png" alt="png" />
jvisualvm 插件安装</p>
<p>要想监控远程的应用,还需要在被监控的 App 上加入 jmx 参数。</p>
<pre><code>-Dcom.sun.management.jmxremote.port=14000
@@ -196,7 +196,7 @@ jvisualvm 插件安装</p>
</code></pre>
<p>上述配置的意义是开启 JMX 连接端口 14000同时配置不需要 SSL 安全认证方式连接。</p>
<p>对于性能优化来说,我们主要用到它的采样器。注意,由于抽样分析过程对程序运行性能有较大的影响,一般我们只在测试环境中使用此功能。</p>
<p><img src="assets/CgqCHl8X3QOANLEGAAaKW6xLOSg775.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl8X3QOANLEGAAaKW6xLOSg775.png" alt="png" /></p>
<p>jvisualvm CPU 性能采样图</p>
<p>对于一个 Java 应用来说,除了要关注它的 CPU 指标,垃圾回收方面也是不容忽视的性能点,我们主要关注以下三点。</p>
<ul>
@@ -215,48 +215,48 @@ jcmd &lt;pid&gt; JFR.stop
<p><strong>JMC 集成了 JFR 的功能</strong>,下面介绍一下 JMC 的使用。</p>
<h2>1.录制</h2>
<p>下图是录制了一个 Tomcat 一分钟之后的结果,从左边的菜单栏即可进入相应的性能界面。</p>
<p><img src="assets/CgqCHl8X3SyAbYa7AAfd6jZo6t4915.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8X3SyAbYa7AAfd6jZo6t4915.png" alt="png" /></p>
<p>JMC 录制结果主界面</p>
<p>通过录制数据,可以清晰了解到某一分钟内,操作系统资源,以及 JVM 内部的性能数据情况。</p>
<h2>2.线程</h2>
<p>选择相应的线程,即可了解线程的执行情况,比如 Wait、Idle 、Block 等状态和时序。</p>
<p>以 C2 编译器线程为例可以看到详细的热点类以及方法内联后的代码大小。如下图所示C2 此时正在疯狂运转。</p>
<p><img src="assets/Ciqc1F8X3TWASVq0AAY9V2QKEX8030.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F8X3TWASVq0AAY9V2QKEX8030.png" alt="png" /></p>
<p>JMC 录制结果 线程界面</p>
<h2>3.内存</h2>
<p>通过内存界面,可以看到每个时间段内内存的申请情况。在排查内存溢出、内存泄漏等情况时,这个功能非常有用。</p>
<p><img src="assets/Ciqc1F8X3T6AesX5AAcyVYacyeQ529.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8X3T6AesX5AAcyVYacyeQ529.png" alt="png" /></p>
<p>JMC 录制结果 内存界面</p>
<h2>4.锁</h2>
<p>一些竞争非常严重的锁信息,以及一些死锁信息,都可以在锁信息界面中找到。</p>
<p>可以看到,一些锁的具体 ID以及关联的线程信息都可以进行联动分析。</p>
<p><img src="assets/CgqCHl8X3UeAJ5L_AAQ7-kTs7YM289.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8X3UeAJ5L_AAQ7-kTs7YM289.png" alt="png" /></p>
<p>JMC 录制结果 锁信息界面</p>
<h2>5.文件和 Socket</h2>
<p>文件和 Socket 界面能够监控对 I/O 的读写,界面一目了然。如果你的应用 I/O 操作比较繁重,比如日志打印比较多、网络读写频繁,就可以在这里监控到相应的信息,并能够和执行栈关联起来。</p>
<p><img src="assets/Ciqc1F8X3VGABH4xAAfkaSBZDio750.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F8X3VGABH4xAAfkaSBZDio750.png" alt="png" /></p>
<p>JMC 录制结果 文件和 Socket 界面</p>
<h2>6.方法调用</h2>
<p>这个和 jvisualvm 的功能类似,展示的是方法调用信息和排行。从这里可以看到一些高耗时方法和热点方法。</p>
<p><img src="assets/CgqCHl8X3WOAYQSCAAVmKbHpuBQ717.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8X3WOAYQSCAAVmKbHpuBQ717.png" alt="png" /></p>
<p>JMC 录制结果 方法调用</p>
<h2>7.垃圾回收</h2>
<p>如果垃圾回收过于频繁就会影响应用的性能。JFR 对垃圾回收进行了详细的记录,比如什么时候发生了垃圾回收,用的什么垃圾回收器,每次垃圾回收的耗时,甚至是什么原因引起的等问题,都可以在这里看到。</p>
<p><img src="assets/Ciqc1F8X3X6ACtlVAAgwHnO3oHQ281.png" alt="Drawing 10.png" /></p>
<p><img src="assets/Ciqc1F8X3X6ACtlVAAgwHnO3oHQ281.png" alt="png" /></p>
<p>JMC 录制结果 垃圾回收</p>
<h2>8.JIT</h2>
<p>JIT 编译后的代码,执行速度会特别快,但它需要一个编译过程。编译界面显示了详细的 JIT 编译过程信息,包括生成后的 CodeCache 大小、方法内联信息等。</p>
<p><img src="assets/CgqCHl8X3Y2AWi8dAAZ8RGTPyoA991.png" alt="Drawing 11.png" /></p>
<p><img src="assets/CgqCHl8X3Y2AWi8dAAZ8RGTPyoA991.png" alt="png" /></p>
<p>JMC 录制结果 JIT 信息</p>
<h2>9.TLAB</h2>
<p>JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配,这就是 TLABThread Local Allocation Buffer的概念。这个 buffer就放在 Eden 区。</p>
<p>原理和 Java 语言中的 ThreadLocal 类似,能够避免对公共区的操作,可以减少一些锁竞争。如下图所示的界面,详细地显示了这个分配过程。</p>
<p><img src="assets/CgqCHl8X3baAW4VFAAaz04YR1w4277.png" alt="Drawing 12.png" /></p>
<p><img src="assets/CgqCHl8X3baAW4VFAAaz04YR1w4277.png" alt="png" /></p>
<p>JMC 录制结果 TLAB 信息</p>
<p>在后面的课时中,我们会有多个使用此工具的分析案例。</p>
<h1>Arthas —— 获取单个请求的调用链耗时</h1>
<p>Arthas 是一个 Java 诊断工具可以排查内存溢出、CPU 飙升、负载高等内容,可以说是一个 jstack、jmap 等命令的大集合。</p>
<p><img src="assets/CgqCHl8X3eSAP67rAANG-JDjv2E614.png" alt="Drawing 13.png" /></p>
<p><img src="assets/CgqCHl8X3eSAP67rAANG-JDjv2E614.png" alt="png" /></p>
<p>Arthas 启动界面</p>
<p>Arthas 支持很多命令,我们以 trace 命令为例。</p>
<p>有时候,我们统计到某个接口的耗时非常高,但又无法找到具体原因时,就可以使用这个 trace 命令。该命令会从方法执行开始记录整个链路上的执行情况,然后统计每个节点的性能开销,最终以树状打印,很多性能问题一眼就能看出来。</p>

View File

@@ -188,7 +188,7 @@ System.out.println(&quot;Logic cost : &quot; + cost);
</code></pre>
<p>下面,我们介绍一下这个工具的使用。</p>
<p>JMH 是一个 jar 包,它和单元测试框架 JUnit 非常像,可以通过注解进行一些基础配置。这部分配置有很多是可以通过 main 方法的 OptionsBuilder 进行设置的。</p>
<p><img src="assets/Ciqc1F8epk6ALUNZAABpIyGz37g324.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F8epk6ALUNZAABpIyGz37g324.png" alt="png" /></p>
<p>上图是一个典型的 JMH 程序执行的内容。通过开启多个进程,多个线程,先执行预热,然后执行迭代,最后汇总所有的测试数据进行分析。在执行前后,还可以根据粒度处理一些前置和后置操作。</p>
<p>一段简单的 JMH 代码如下所示:</p>
<pre><code>@BenchmarkMode(Mode.Throughput)
@@ -251,7 +251,7 @@ timeUnit = TimeUnit.SECONDS)
</code></pre>
<p>一般来说,基准测试都是针对比较小的、执行速度相对较快的代码块,这些代码有很大的可能性被 JIT 编译、内联,所以在编码时保持方法的精简,是一个好的习惯。具体优化过程,我们将在 18 课时介绍。</p>
<p>说到预热,就不得不提一下在分布式环境下的服务预热。在对服务节点进行发布的时候,通常也会有预热过程,逐步放量到相应的服务节点,直到服务达到最优状态。如下图所示,负载均衡负责这个放量过程,一般是根据百分比进行放量。</p>
<p><img src="assets/CgqCHl8epmWAWw_3AABS3CbQ8AE949.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl8epmWAWw_3AABS3CbQ8AE949.png" alt="png" /></p>
<h2>2. @Measurement</h2>
<p>样例如下:</p>
<pre><code>@Measurement(
@@ -312,7 +312,7 @@ BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms
<p>那么 fork 到底是在进程还是线程环境里运行呢?</p>
<p>我们追踪一下 JMH 的源码,发现每个 fork 进程是单独运行在 Proccess 进程里的,这样就可以做完全的环境隔离,避免交叉影响。</p>
<p>它的输入输出流,通过 Socket 连接的模式,发送到我们的执行终端。</p>
<p><img src="assets/Ciqc1F8epneAFThuAABRpqRrEUw322.png" alt="3.png" /></p>
<p><img src="assets/Ciqc1F8epneAFThuAABRpqRrEUw322.png" alt="png" /></p>
<p>在这里分享一个小技巧。其实 fork 注解有一个参数叫作 jvmArgsAppend我们可以通过它传递一些 JVM 的参数。</p>
<pre><code>@Fork(value = 3, jvmArgsAppend = {&quot;-Xmx2048m&quot;, &quot;-server&quot;, &quot;-XX:+AggressiveOpts&quot;})
</code></pre>
@@ -379,12 +379,12 @@ public class JMHSample_27_Params {
</code></pre>
<p>值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数 1 M 个,参数 2 N 个,那么总共要执行 M*N 次。</p>
<p>下面是一个执行结果的截图:</p>
<p><img src="assets/CgqCHl8ebZaAPtXOAAPe5vpFf_c784.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl8ebZaAPtXOAAPe5vpFf_c784.png" alt="png" /></p>
<h2>10. @CompilerControl</h2>
<p>这可以说是一个非常有用的功能了。</p>
<p>Java 中方法调用的开销是比较大的尤其是在调用量非常大的情况下。拿简单的getter/setter 方法来说,这种方法在 Java 代码中大量存在。我们在访问的时候,就需要创建相应的栈帧,访问到需要的字段后,再弹出栈帧,恢复原程序的执行。</p>
<p>如果能够把这些对象的访问和操作,纳入目标方法的调用范围之内,就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。如下图所示,代码经过 JIT 编译之后,效率会有大的提升。</p>
<p><img src="assets/Ciqc1F8epoqAI9u2AAB4h_ABJWE362.png" alt="4.png" /></p>
<p><img src="assets/Ciqc1F8epoqAI9u2AAB4h_ABJWE362.png" alt="png" /></p>
<p>这个注解可以用在类或者方法上,能够控制方法的编译行为,常用的有 3 种模式:</p>
<p>强制使用内联INLINE禁止使用内联DONT_INLINE甚至是禁止方法编译EXCLUDE等。</p>
<h1>将结果图形化</h1>
@@ -403,16 +403,16 @@ public class JMHSample_27_Params {
<li><strong>LATEX</strong> 导出到 latex一种基于 ΤΕΧ 的排版系统。</li>
</ul>
<p>一般来说,我们导出成 CSV 文件,直接在 Excel 中操作,生成如下相应的图形就可以了。</p>
<p><img src="assets/Ciqc1F8ebi2AdAAbAALlvsHgcKk925.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F8ebi2AdAAbAALlvsHgcKk925.png" alt="png" /></p>
<h2>2. 结果图形化制图工具</h2>
<p><strong>JMH Visualizer</strong></p>
<p>这里有一个开源的项目,通过导出 json 文件,上传至 <a href="https://jmh.morethan.io/">JMH Visualizer</a>(点击链接跳转),可得到简单的统计结果。由于很多操作需要鼠标悬浮在上面进行操作,所以个人认为它的展示方式并不是很好。</p>
<p><strong>JMH Visual Chart</strong></p>
<p>相比较而言, <a href="http://deepoove.com/jmh-visual-chart">JMH Visual Chart</a>(点击链接跳转)这个工具,就相对直观一些。</p>
<p><img src="assets/CgqCHl8ebkmAbujsAAHK-g94ooM905.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl8ebkmAbujsAAHK-g94ooM905.png" alt="png" /></p>
<p><strong>meta-chart</strong></p>
<p>一个通用的 <a href="https://www.meta-chart.com/">在线图表生成器</a>(点击链接跳转),导出 CSV 文件后,做适当处理,即可导出精美图像。</p>
<p><img src="assets/CgqCHl8eboKAHRe8AAGSfMVOXxw934.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8eboKAHRe8AAGSfMVOXxw934.png" alt="png" /></p>
<p>像 Jenkins 等一些持续集成工具,也提供了相应的插件,用来直接显示这些测试结果。</p>
<h1>小结</h1>
<p>本课时主要介绍了 基准测试工具— JMH官方的 JMH 有非常丰富的示例比如伪共享FalseSharing的影响等高级话题。我已经把它放在了 <a href="https://gitee.com/xjjdog/tuning-lagou-res">Gitee</a>(点击链接跳转)上,你可以将其导入至 Idea 编辑器进行测试。</p>

View File

@@ -173,11 +173,11 @@ function hide_canvas() {
<li>优化用户体验,比如常见的音频/视频缓冲加载,通过提前缓冲数据,达到流畅的播放效果。</li>
</ul>
<p>缓冲在 Java 语言中被广泛应用,在 IDEA 中搜索 Buffer可以看到长长的类列表其中最典型的就是<strong>文件读取和写入字符流</strong></p>
<p><img src="assets/Ciqc1F8hIpGANh2WAADfDKHvr7I288.jpg" alt="15932484217918.jpg" /></p>
<p><img src="assets/Ciqc1F8hIpGANh2WAADfDKHvr7I288.jpg" alt="png" /></p>
<h1>文件读写流</h1>
<p>接下来,我会以文件读取和写入字符流为例进行讲解。</p>
<p>Java 的 I/O 流设计,采用的是<strong>装饰器模式</strong>,当需要给类添加新的功能时,就可以将被装饰者通过参数传递到装饰者,封装成新的功能方法。下图是装饰器模式的典型示意图,就增加功能来说,装饰模式比生成子类更为灵活。</p>
<p><img src="assets/Ciqc1F8hIqCAXF-UAACYqHfWtqs495.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8hIqCAXF-UAACYqHfWtqs495.png" alt="png" /></p>
<p>在读取和写入流的 API 中BufferedInputStream 和 BufferedReader 可以加快读取字符的速度BufferedOutputStream 和 BufferedWriter 可以加快写入的速度。</p>
<p>下面是直接读取文件的代码实现:</p>
<pre><code>int result = 0;
@@ -259,7 +259,7 @@ private void fill() throws IOException {
<p>这就是一个权衡的问题,缓冲区开得太大,会增加单次读写的时间,同时内存价格很高,不能无限制使用,缓冲流的默认缓冲区大小是 8192 字节,也就是 8KB算是一个比较折中的值。</p>
<p>这好比搬砖,如果一块一块搬,时间便都耗费在往返路上了;但若给你一个小推车,往返的次数便会大大降低,效率自然会有所提升。</p>
<p>下图是使用 FileReader 和 BufferedReader 读取文件的 JMH 对比(相关代码见仓库),可以看到,使用了缓冲,读取效率有了很大的提升(暂未考虑系统文件缓存)。</p>
<p><img src="assets/Ciqc1F8hItGALSyZAAA6Yg6KI-Y263.jpg" alt="15933320286502.jpg" /></p>
<p><img src="assets/Ciqc1F8hItGALSyZAAA6Yg6KI-Y263.jpg" alt="png" /></p>
<h1>日志缓冲</h1>
<p>日志是程序员们最常打交道的地方。在高并发应用中,即使对日志进行了采样,日志数量依旧惊人,所以选择高速的日志组件至关重要。</p>
<p>SLF4J 是 Java 里标准的日志记录库,它是一个允许你使用任何 Java 日志记录库的抽象适配层,最常用的实现是 Logback支持修改后自动 reload它比 Java 自带的 JUL 还要流行。</p>
@@ -276,7 +276,7 @@ private void fill() throws IOException {
&lt;appender-ref ref =&quot;FILE&quot;/&gt;
&lt;/appender&gt;
</code></pre>
<p><img src="assets/Ciqc1F8hIt2AC_tUAACr0rch4PI971.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8hIt2AC_tUAACr0rch4PI971.png" alt="png" /></p>
<p>如上图,异步日志输出之后,日志信息将暂存在 ArrayBlockingQueue 列表中,后台会有一个 Worker 线程不断地获取缓冲区内容,然后写入磁盘中。</p>
<p>上图中有三个关键参数:</p>
<ul>
@@ -288,22 +288,22 @@ private void fill() throws IOException {
<p>毫无疑问缓冲区是可以提高性能的,<strong>但它通常会引入一个异步的问题</strong>,使得编程模型变复杂。</p>
<p>通过文件读写流和 Logback 两个例子,我们来看一下对于缓冲区设计的一些常规操作。</p>
<p>如下图所示,资源 A 读取或写入一些操作到资源 B这本是一个正常的操作流程但由于中间插入了一个额外的<strong>存储层</strong>,所以这个流程被生生截断了,这时就需要你手动处理被截断两方的资源协调问题。</p>
<p><img src="assets/Ciqc1F8hIuqATvhSAAB9F5pMiOE699.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8hIuqATvhSAAB9F5pMiOE699.png" alt="png" /></p>
<p>根据资源的不同,对正常业务进行截断后的操作,分为同步操作和异步操作。</p>
<h2>1.同步操作</h2>
<p>同步操作的编程模型相对简单,在一个线程中就可完成,你只需要控制缓冲区的大小,并把握处理的时机。比如,缓冲区<strong>大小达到阈值</strong>,或者缓冲区的元素在缓冲区的<strong>停留时间超时</strong>,这时就会触发<strong>批量操作</strong></p>
<p>由于所有的操作又都在<strong>单线程</strong>,或者同步方法块中完成,再加上资源 B 的处理能力有限,那么很多操作就会阻塞并等待在调用线程上。比如写文件时,需要等待前面的数据写入完毕,才能处理后面的请求。</p>
<p><img src="assets/CgqCHl8hIvuAILAKAABaDCSPRRw546.png" alt="image" /></p>
<p><img src="assets/CgqCHl8hIvuAILAKAABaDCSPRRw546.png" alt="png" /></p>
<h2>2.异步操作</h2>
<p>异步操作就复杂很多。</p>
<p>缓冲区的<strong>生产</strong>者一般是同步调用,但也可以采用异步方式进行填充,一旦采用异步操作,就涉及缓冲区满了以后,生产者的一些响应策略。</p>
<p>此时,应该将这些策略抽象出来,根据业务的属性选择,比如直接抛弃、抛出异常,或者直接在用户的线程进行等待。你会发现它与线程池的饱和策略是类似的,这部分的详细概念将在 12 课时讲解。</p>
<p>许多应用系统还会有更复杂的策略,比如在用户线程等待,设置一个超时时间,以及成功进入缓冲区之后的回调函数等。</p>
<p>对缓冲区的<strong>消费</strong>,一般采用开启线程的方式,如果有多个线程消费缓冲区,还会存在信息同步和顺序问题。</p>
<p><img src="assets/CgqCHl8hIwaAQl3SAACaljNt5Fs553.png" alt="image" /></p>
<p><img src="assets/CgqCHl8hIwaAQl3SAACaljNt5Fs553.png" alt="png" /></p>
<h2>3.Kafka缓冲区示例</h2>
<p><strong>这里以一个常见的面试题来讲解上面的知识点Kafka 的生产者,有可能会丢数据吗?</strong></p>
<p><img src="assets/CgqCHl8hIxKADeVuAACET9IDWMQ424.png" alt="image" /></p>
<p><img src="assets/CgqCHl8hIxKADeVuAACET9IDWMQ424.png" alt="png" /></p>
<p>如图,要想解答这个问题,需要先了解 Kafka 对生产者的一些封装,其中有一个对性能影响非常大的点,就是缓冲。</p>
<p>生产者会把发送到同一个 partition 的多条消息,封装在一个 batch缓冲区中。当 batch 满了(参数 batch.size或者消息达到了超时时间参数 linger.ms缓冲区中的消息就会被发送到 broker 上。</p>
<p>这个缓冲区默认是 16KB如果生产者的业务突然断电这 16KB 数据是没有机会发送出去的。此时,就造成了消息丢失。</p>

View File

@@ -164,7 +164,7 @@ function hide_canvas() {
<p>和缓冲类似,缓存可能是软件中使用最多的优化技术了,比如:在最核心的 CPU 中,就存在着多级缓存;为了消除内存和存储之间的差异,各种类似 Redis 的缓存框架更是层出不穷。</p>
<p>缓存的优化效果是非常好的,它既可以让原本载入非常缓慢的页面,瞬间秒开,也能让本是压力山大的数据库,瞬间清闲下来。</p>
<p><strong>缓存</strong><strong>本质</strong>上是为了协调两个速度差异非常大的组件,如下图所示,通过加入一个中间层,将常用的数据存放在相对高速的设备中。</p>
<p><img src="assets/CgqCHl8nuCKAad7oAAAk6v90xvo900.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl8nuCKAad7oAAAk6v90xvo900.png" alt="png" /></p>
<p>在我们平常的应用开发中,根据缓存所处的物理位置,一般分为<strong>进程内</strong>缓存和<strong>进程外</strong>缓存。</p>
<p>本课时我们主要聚焦在进程内缓存上,在 Java 中进程内缓存就是我们常说的堆内缓存。Spring 的默认实现里,就包含 Ehcache、JCache、Caffeine、Guava Cache 等。</p>
<h3>Guava 的 LoadingCache</h3>
@@ -182,7 +182,7 @@ function hide_canvas() {
&lt;/dependency&gt;
</code></pre>
<p>下面介绍一下 LC 的常用操作:</p>
<p><img src="assets/Ciqc1F8nuDmAJcstAABnG73x05M360.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F8nuDmAJcstAABnG73x05M360.png" alt="png" /></p>
<h4>1.缓存初始化</h4>
<p>首先,我们可以通过下面的参数设置一下 LC 的大小。一般,我们只需给缓存提供一个上限。</p>
<ul>
@@ -212,7 +212,7 @@ static String slowMethod(String key) throws Exception {
}
</code></pre>
<p>上面是主动触发的示例代码,你可以使用 <strong>get</strong> 方法<strong>获取</strong>缓存的值。比如,当我们执行 lc.get(&quot;a&quot;) 时,第一次会比较缓慢,因为它需要到数据源进行获取;第二次就瞬间返回了,也就是缓存命中了。具体时序可以参见下面这张图。</p>
<p><img src="assets/CgqCHl8nuFGACX8vAABYHt8o1wc201.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl8nuFGACX8vAABYHt8o1wc201.png" alt="png" /></p>
<p>除了靠 LC 自带的回收策略,我们也可以<strong>手动删除</strong>某一个元素,这就是 <strong>invalidate</strong> 方法。当然,数据的这些删除操作,也是可以监听到的,只需要设置一个监听器就可以了,代码如下:</p>
<pre><code>.removalListener(notification -&gt; System.out.println(notification))
</code></pre>
@@ -259,7 +259,7 @@ static String slowMethod(String key) throws Exception {
boolean accessOrder)
</code></pre>
<p>accessOrder 参数是实现 LRU 的关键。当 accessOrder 的值为 true 时,将按照对象的访问顺序排序;当 accessOrder 的值为 false 时,将按照对象的插入顺序排序。我们上面提到过,按照访问顺序排序,其实就是 LRU。</p>
<p><img src="assets/CgqCHl8nuJeAQCW4AABgBDKI74g880.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8nuJeAQCW4AABgBDKI74g880.png" alt="png" /></p>
<p>如上图,按照缓存的一般设计方式,和 LC 类似,当你向 LinkedHashMap 中添加新对象的时候,就会调用 removeEldestEntry 方法。这个方法默认返回 false表示永不过期。我们只需要覆盖这个方法当超出容量的时候返回 true触发移除动作就可以了。关键代码如下</p>
<pre><code>public class LRU extends LinkedHashMap {
int capacity;
@@ -276,7 +276,7 @@ static String slowMethod(String key) throws Exception {
<p>相比较 LC这段代码实现的功能是比较简陋的它甚至不是线程安全的但它体现了缓存设计的一般思路是 Java 中最简单的 LRU 实现方式。</p>
<h3>进一步加速</h3>
<p>在 Linux 系统中,通过 free 命令,能够看到系统内存的使用状态。其中,有一块叫作 <strong>cached</strong> 的区域,占用了大量的内存空间。</p>
<p><img src="assets/Ciqc1F8nuKqAJaGZAAF4FboRD9E367.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F8nuKqAJaGZAAF4FboRD9E367.png" alt="png" /></p>
<p>如图所示,这个区域,其实就是存放了操作系统的文件缓存,当应用再次用到它的时候,就不用再到磁盘里走一圈,能够从内存里快速载入。</p>
<p>在文件读取的缓存方面,操作系统做得更多。由于磁盘擅长顺序读写,在随机读写的时候,效率很低,所以,操作系统使用了智能的<strong>预读算法</strong>readahead将数据从硬盘中加载到缓存中。</p>
<p>预读算法有三个关键点:</p>
@@ -307,7 +307,7 @@ static String slowMethod(String key) throws Exception {
<p><strong>3缓存失效策略</strong></p>
<p>缓存算法也会影响命中率和性能,目前效率最高的算法是 Caffeine 使用的 <strong>W-TinyLFU 算法</strong>,它的命中率非常高,内存占用也更小。新版本的 spring-cache已经默认支持 Caffeine。</p>
<p>下图展示了这个算法的性能,<a href="https://github.com/ben-manes/caffeine">从官网的 github 仓库</a>就可以找到 JMH 的测试代码。</p>
<p><img src="assets/CgqCHl8nuQWAKsjIAAG1hzHS76Q255.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8nuQWAKsjIAAG1hzHS76Q255.png" alt="png" /></p>
<p>推荐使用 Guava Cache 或者 Caffeine 作为堆内缓存解决方案,然后通过它们提供的一系列监控指标,来调整缓存的大小和内容,一般来说:</p>
<ul>
<li>缓存命中率达到 50% 以上,作用就开始变得显著;</li>

View File

@@ -164,7 +164,7 @@ function hide_canvas() {
<p>那什么叫<strong>分布式缓存</strong>呢?它其实是一种<strong>集中管理</strong>的思想。如果我们的服务有多个节点,堆内缓存在每个节点上都会有一份;而分布式缓存,所有的节点,共用一份缓存,既节约了空间,又减少了管理成本。</p>
<p>在分布式缓存领域,使用最多的就是 Redis。<strong>Redis</strong> 支持非常丰富的数据类型包括字符串string、列表list、集合set、有序集合zset、哈希表hash等常用的数据结构。当然它也支持一些其他的比如位图bitmap一类的数据结构。</p>
<p>说到 Redis就不得不提一下另外一个分布式缓存 <strong>Memcached</strong>(以下简称 MC。MC 现在已经很少用了,但<strong>面试的时候经常会问到它们之间的区别</strong>,这里简单罗列一下:</p>
<p><img src="assets/CgqCHl8qaxiATTH1AAB10CrXXk8295.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8qaxiATTH1AAB10CrXXk8295.png" alt="png" /></p>
<p>Redis 在互联网中,几乎是标配。我们接下来,先简单看一下 Redis 在 Spring 中是如何使用的然后再介绍一下在秒杀业务中Redis是如何帮助我们承接瞬时流量的。</p>
<h3>SpringBoot 如何使用 Redis</h3>
<p>使用 SpringBoot 可以很容易地对 Redis 进行操作(<a href="https://gitee.com/xjjdog/tuning-lagou-res/tree/master/tuning-008/cache-redis">完整代码见仓库</a>。Java 的 Redis的客户端常用的有三个jedis、redisson 和 lettuceSpring 默认使用的是 lettuce。</p>
@@ -176,7 +176,7 @@ function hide_canvas() {
&lt;/dependency&gt;
</code></pre>
<p>上面这种方式,我们主要是使用 RedisTemplate 这个类。它针对不同的数据类型,抽象了相应的方法组。</p>
<p><img src="assets/CgqCHl8qa0CAF6eCAAHpRwXu93w738.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl8qa0CAF6eCAAHpRwXu93w738.png" alt="png" /></p>
<p>另外一种方式,就是使用 Spring 抽象的缓存包 spring-cache。它使用注解采用 AOP的方式对 Cache 层进行了抽象,可以在各种堆内缓存框架和分布式框架之间进行切换。这是它的 maven 坐标:</p>
<pre><code>&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
@@ -202,7 +202,7 @@ function hide_canvas() {
<p>对于秒杀系统来说,仅仅使用这三个注解是有局限性的,需要使用更加底层的 API比如 RedisTemplate来完成逻辑开发下面就来介绍一些比较重要的功能。</p>
<h3>秒杀业务介绍</h3>
<p>秒杀,是对正常业务流程的考验。因为它会产生突发流量,平常一天的请求,可能就集中在几秒内就要完成。比如,京东的某些抢购,可能库存就几百个,但是瞬时进入的流量可能是几十上百万。</p>
<p><img src="assets/CgqCHl8qa06AXwgiAABlE7P5SV4914.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl8qa06AXwgiAABlE7P5SV4914.png" alt="png" /></p>
<p>如果参与秒杀的人,等待很长时间,体验就非常差,想象一下拥堵的高速公路收费站,就能理解秒杀者的心情。同时,被秒杀的资源会成为热点,发生并发争抢的后果。比如 12306 的抢票,如果单纯使用数据库来接受这些请求,就会产生严重的锁冲突,这也是秒杀业务难的地方。</p>
<p>大家可以回忆一下上一课时的内容,此时,秒杀前端需求与数据库之间的速度是严重不匹配的,而且秒杀的资源是热点资源。这种场景下,采用缓存是非常合适的。</p>
<p><strong>处理秒杀业务有三个绝招:</strong></p>
@@ -219,7 +219,7 @@ function hide_canvas() {
<li><strong>抢购阶段</strong>,就是我们通常说的秒杀,会产生瞬时的高并发流量,对资源进行集中操作;</li>
<li><strong>结束清算</strong>,主要完成数据的一致性,处理一些异常情况和回仓操作。</li>
</ul>
<p><img src="assets/Ciqc1F8qa1eAfW9ZAADONsLsuh4160.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F8qa1eAfW9ZAADONsLsuh4160.png" alt="png" /></p>
<p>下面,我将介绍一下最重要的秒杀阶段。</p>
<p>我们可以设计一个 Hash 数据结构,来支持库存的扣减。</p>
<pre><code>seckill:goods:${goodsId}{
@@ -273,13 +273,13 @@ return falseRet
}
</code></pre>
<p>执行仓库里的 testSeckill 方法。启动 1000 个线程对 100 个资源进行模拟秒杀,可以看到生成了 100 条记录,同时其他的线程返回的是 0表示没有秒杀到。</p>
<p><img src="assets/CgqCHl8qa3KAHrjzAACfVjTuQ9c533.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl8qa3KAHrjzAACfVjTuQ9c533.png" alt="png" /></p>
<h3>缓存穿透、击穿和雪崩</h3>
<p>抛开秒杀场景,我们再来看一下分布式缓存系统会存在的三大问题: 缓存穿透、缓存击穿和缓存雪崩 。</p>
<h4>1.缓存穿透</h4>
<p>第一个比较大的问题就是缓存穿透。这个概念比较好理解,和我们上一课时提到的命中率有关。如果命中率很低,那么压力就会集中在数据库持久层。</p>
<p>假如能找到相关数据,我们就可以把它缓存起来。但问题是,<strong>本次请求,在缓存和持久层都没有命中,这种情况就叫缓存的穿透。</strong></p>
<p><img src="assets/CgqCHl8qa32AXy2GAACsgw1i8As520.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8qa32AXy2GAACsgw1i8As520.png" alt="png" /></p>
<p>举个例子,如上图,在一个登录系统中,有外部攻击,一直尝试使用不存在的用户进行登录,这些用户都是虚拟的,不能有效地被缓存起来,每次都会到数据库中查询一次,最后就会造成服务的性能故障。</p>
<p>解决这个问题有多种方案,我们来简单介绍一下。</p>
<p><strong>第一种</strong>就是把空对象缓存起来。不是持久层查不到数据吗?那么我们就可以把本次请求的结果设置为 null然后放入到缓存中。通过设置合理的过期时间就可以保证后端数据库的安全。</p>
@@ -292,7 +292,7 @@ return falseRet
<h4>3.缓存雪崩</h4>
<p>雪崩这个词看着可怕,实际情况也确实比较严重。缓存是用来对系统加速的,后端的数据库只是数据的备份,而不是作为高可用的备选方案。</p>
<p>当缓存系统出现故障,流量会瞬间转移到后端的数据库。过不了多久,数据库将会被大流量压垮挂掉,这种级联式的服务故障,可以形象地称为雪崩。</p>
<p><img src="assets/CgqCHl8qa5CAd61nAAG3-zdlhRw552.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8qa5CAd61nAAG3-zdlhRw552.png" alt="png" /></p>
<p>缓存的高可用建设是非常重要的。Redis 提供了主从和 Cluster 的模式,其中 Cluster 模式使用简单,每个分片也能单独做主从,可以保证极高的可用性。</p>
<p>另外,我们对数据库的性能瓶颈有一个大体的评估。如果缓存系统当掉,那么流向数据库的请求,就可以使用限流组件,将请求拦截在外面。</p>
<h3>缓存一致性</h3>
@@ -314,7 +314,7 @@ return falseRet
<p>但这样还是有问题。<strong>接下来介绍的场景,也是面试中经常提及的问题。</strong></p>
<p>我们上面提到的缓存删除动作,和数据库的更新动作,明显是不在一个事务里的。如果一个请求删除了缓存,同时有另外一个请求到来,此时发现没有相关的缓存项,就从数据库里加载了一份到缓存系统。接下来,数据库的更新操作也完成了,此时数据库的内容和缓存里的内容,就产生了不一致。</p>
<p>下面这张图,直观地解释了这种不一致的情况,此时,缓存读取 B 操作以及之后的读取操作,都会读到错误的缓存值。</p>
<p><img src="assets/CgqCHl8qa5-AWDbqAACK1Itu_Wc954.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl8qa5-AWDbqAACK1Itu_Wc954.png" alt="png" /></p>
<p><strong>在面试中,只要你把这个问题给点出来,面试官都会跷起大拇指。</strong></p>
<p>可以使用分布式锁来解决这个问题,将缓存操作和数据库删除操作,与其他的缓存读操作,使用锁进行资源隔离即可。一般来说,读操作是不需要加锁的,它会在遇到锁的时候,重试等待,直到超时。</p>
<h3>小结</h3>

View File

@@ -171,9 +171,9 @@ function hide_canvas() {
final GenericObjectPoolConfig&lt;T&gt; config)
</code></pre>
<p><strong>Redis 的常用客户端 Jedis</strong>,就是使用 Commons Pool 管理连接池的,可以说是一个最佳实践。下图是 Jedis 使用工厂<strong>创建对象</strong>的主要代码块。对象工厂类最主要的方法就是makeObject它的返回值是 PooledObject 类型,可以将对象使用 new DefaultPooledObject&lt;&gt;(obj) 进行简单包装返回。</p>
<p><img src="assets/CgqCHl8xKV-AHSvoAAX4BkEi8aQ783.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8xKV-AHSvoAAX4BkEi8aQ783.png" alt="png" /></p>
<p>我们再来介绍一下对象的生成过程,如下图,对象在进行<strong>获取</strong>时,将首先尝试从对象池里拿出一个,如果对象池中没有空闲的对象,就使用工厂类提供的方法,生成一个新的。</p>
<p><img src="assets/Ciqc1F8xKWWAfETQAAXjITHnnyY877.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F8xKWWAfETQAAXjITHnnyY877.png" alt="png" /></p>
<p>那对象是存在什么地方的呢?这个存储的职责,就是由一个叫作 LinkedBlockingDeque的结构来承担的它是一个双向的队列。</p>
<p>接下来看一下 GenericObjectPoolConfig 的主要属性:</p>
<pre><code>private int maxTotal = DEFAULT_MAX_TOTAL;
@@ -196,7 +196,7 @@ private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_
private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
</code></pre>
<p>参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期。如下图所示,池子的操作主要有两个:一个是<strong>业务线程</strong>,一个是<strong>检测线程</strong></p>
<p><img src="assets/CgqCHl8xKYKAdvm7AADGC-6LsfE257.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl8xKYKAdvm7AADGC-6LsfE257.png" alt="png" /></p>
<p>对象池在进行初始化时,要指定三个主要的参数:</p>
<ul>
<li><strong>maxTotal</strong> 对象池中管理的对象上限</li>
@@ -206,7 +206,7 @@ private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
<p>其中 <strong>maxTotal</strong> 和业务线程有关,当业务线程想要获取对象时,会首先检测是否有空闲的对象。如果有,则返回一个;否则进入创建逻辑。此时,如果池中个数已经达到了最大值,就会创建失败,返回空对象。</p>
<p>对象在获取的时候,有一个非常重要的参数,那就是<strong>最大等待时间maxWaitMillis</strong>,这个参数对应用方的性能影响是比较大的。该参数默认为 -1表示永不超时直到有对象空闲。</p>
<p>如下图,如果对象创建非常缓慢或者使用非常繁忙,业务线程会持续阻塞 blockWhenExhausted 默认为 true进而导致正常服务也不能运行。</p>
<p><img src="assets/CgqCHl8xKZGAbtiiAABfuEZ8gwQ793.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl8xKZGAbtiiAABfuEZ8gwQ793.png" alt="png" /></p>
<p>一般面试官会问:你会把超时参数设置成多大呢?</p>
<p>我一般都会把最大等待时间,设置成接口可以忍受的最大延迟。比如,一个正常服务响应时间 10ms 左右,达到 1 秒钟就会感觉到卡顿,那么这个参数设置成 500~1000ms 都是可以的。超时之后,会抛出 NoSuchElementException 异常,请求<strong>会快速失败</strong>,不会影响其他业务线程,这种 Fail Fast 的思想,在互联网应用非常广泛。</p>
<p>带有<strong>evcit</strong> 字样的参数,主要是处理对象逐出的。池化对象除了初始化和销毁的时候比较昂贵,在运行时也会占用系统资源。比如,<strong>连接池</strong>会占用多条连接,<strong>线程池</strong>会增加调度开销等。业务在突发流量下,会申请到超出正常情况的对象资源,放在池子中。等这些对象不再被使用,我们就需要把它清理掉。</p>
@@ -237,11 +237,11 @@ public class JedisPoolVSJedisBenchmark {
...
</code></pre>
<p>将测试结果使用 meta-chart 作图,展示结果如下图所示,可以看到使用了连接池的方式,它的吞吐量是未使用连接池方式的 5 倍!</p>
<p><img src="assets/Ciqc1F8xKaCAP0c2AADCCnpuRd0416.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8xKaCAP0c2AADCCnpuRd0416.png" alt="png" /></p>
<h3>数据库连接池 HikariCP</h3>
<p><strong>HikariCP</strong> 源于日语“光”的意思(和光速一样快),它是 SpringBoot 中默认的数据库连接池。数据库是我们工作中经常使用到的组件,针对数据库设计的客户端连接池是非常多的,它的设计原理与我们在本课时开头提到的基本一致,可以有效地减少数据库连接创建、销毁的资源消耗。</p>
<p>同是连接池,它们的性能也是有差别的,下图是 HikariCP 官方的一张测试图,可以看到它优异的性能,官方的 JMH 测试代码见 <a href="https://github.com/brettwooldridge/HikariCP-benchmark">Github</a>,我也已经拷贝了一份到仓库中。</p>
<p><img src="assets/Ciqc1F8xKamACdt4AAG6dLqMUDo898.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F8xKamACdt4AAG6dLqMUDo898.png" alt="png" /></p>
<p><strong>一般面试题是这么问的: HikariCP 为什么快呢?主要有三个方面:</strong></p>
<ul>
<li>它使用 FastList 替代 ArrayList通过初始化的默认值减少了越界检查的操作</li>
@@ -255,7 +255,7 @@ public class JedisPoolVSJedisBenchmark {
<p>另外,根据数据库查询和事务类型,一个应用中是可以配置多个数据库连接池的,这个优化技巧很少有人知道,在此简要描述一下。</p>
<p>业务类型通常有两种:一种需要快速的响应时间,把数据尽快返回给用户;另外一种是可以在后台慢慢执行,耗时比较长,对时效性要求不高。如果这两种业务类型,共用一个数据库连接池,就容易发生资源争抢,进而影响接口响应速度。虽然微服务能够解决这种情况,但大多数服务是没有这种条件的,这时就可以对连接池进行拆分。</p>
<p>如图,在同一个业务中,根据业务的属性,我们分了两个连接池,就是来处理这种情况的。</p>
<p><img src="assets/CgqCHl8xKb-AaPAfAABFiMwiWmM309.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8xKb-AaPAfAABFiMwiWmM309.png" alt="png" /></p>
<p>HikariCP 还提到了另外一个知识点,在 JDBC4 的协议中,通过 Connection.isValid() 就可以检测连接的有效性。这样,我们就不用设置一大堆的 test 参数了HikariCP 也没有提供这样的参数。</p>
<h3>结果缓存池</h3>
<p>到了这里你可能会发现池Pool与缓存Cache有许多相似之处。</p>
@@ -273,7 +273,7 @@ public class JedisPoolVSJedisBenchmark {
</ul>
<p>将对象池化之后,只是开启了第一步优化。要想达到最优性能,就不得不调整池的一些关键参数,合理的池大小加上合理的超时时间,就可以让池发挥更大的价值。和缓存的命中率类似,对池的监控也是非常重要的。</p>
<p>如下图,可以看到数据库连接池连接数长时间保持在高位不释放,同时等待的线程数急剧增加,这就能帮我们快速定位到数据库的事务问题。</p>
<p><img src="assets/CgqCHl8xKcyAVb5-AAHduSa-zPY995.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl8xKcyAVb5-AAHduSa-zPY995.png" alt="png" /></p>
<p>平常的编码中,有很多类似的场景。比如 Http 连接池Okhttp 和 Httpclient 就都提供了连接池的概念,你可以类比着去分析一下,关注点也是在连接大小和超时时间上;在底层的中间件,比如 RPC也通常使用连接池技术加速资源获取比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。</p>
<p>你会发现,在不同资源层面的池化设计也是类似的。比如<strong>线程池</strong>,通过队列对任务进行了二层缓冲,提供了多样的拒绝策略等,线程池我们将在 12 课时进行介绍。线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了,欢迎你在下方留言,与大家一起分享讨论,我也会针对你的思考进行一一点评。</p>
<p>但无论以何种方式处理对象,让对象保持精简,提高它的复用度,都是我们的目标,所以下一课时,我将系统讲解大对象的复用和注意点。</p>

View File

@@ -171,10 +171,10 @@ function hide_canvas() {
<h3>String 的 substring 方法</h3>
<p>我们都知道String 在 Java 中是不可变的,如果你改动了其中的内容,它就会生成一个新的字符串。</p>
<p>如果我们想要用到字符串中的一部分数据,就可以使用 substring 方法。</p>
<p><img src="assets/CgqCHl8zkSuAJiz1AAXioe0G9Vc058.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8zkSuAJiz1AAXioe0G9Vc058.png" alt="png" /></p>
<p>如上图所示当我们需要一个子字符串的时候substring 生成了一个新的字符串,这个字符串通过构造函数的 Arrays.copyOfRange 函数进行构造。</p>
<p>这个函数在 JDK7 之后是没有问题的,但在 JDK6 中,却有着内存泄漏的风险,我们可以学习一下这个案例,来看一下大对象复用可能会产生的问题。</p>
<p><img src="assets/CgqCHl8zkTWAcVQ4AAEesZzLTVo509.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl8zkTWAcVQ4AAEesZzLTVo509.png" alt="png" /></p>
<p>上图是我从 JDK 官方的一张截图。可以看到,它在创建子字符串的时候,并不只拷贝所需要的对象,而是把整个 value 引用了起来。如果原字符串比较大,即使不再使用,内存也不会释放。</p>
<p>比如,一篇文章内容可能有几兆,我们仅仅是需要其中的摘要信息,也不得不维持整个的大对象。</p>
<pre><code>String content = dao.getArticle(id);
@@ -227,7 +227,7 @@ articles.put(id,summary);
<h3>保持合适的对象粒度</h3>
<p>给你分享一个实际案例:我们有一个并发量非常高的业务系统,需要频繁使用到用户的基本数据。</p>
<p>如下图所示,由于用户的基本信息,都是存放在另外一个服务中,所以每次用到用户的基本信息,都需要有一次网络交互。更加让人无法接受的是,即使是只需要用户的性别属性,也需要把所有的用户信息查询,拉取一遍。</p>
<p><img src="assets/CgqCHl8zkWGABcIuAADyGLJaQ44758.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl8zkWGABcIuAADyGLJaQ44758.png" alt="png" /></p>
<p>为了加快数据的查询速度,根据我们之前 [《08 | 案例分析Redis 如何助力秒杀业务》]的描述,对数据进行了初步的缓存,放入到了 Redis 中,查询性能有了大的改善,但每次还是要查询很多冗余数据。</p>
<p>原始的 redis key 是这样设计的:</p>
<pre><code>type: string
@@ -271,7 +271,7 @@ String getSex(int userId) {
<p>这些数据放在堆内内存中还是过大了。幸运的是Redis 也支持 Bitmap 结构,如果内存有压力,我们可以把这个结构放到 Redis 中,判断逻辑也是类似的。</p>
<p><strong>再插一道面试算法题:给出一个 1GB 内存的机器,提供 60亿 int 数据,如何快速判断有哪些数据是重复的?</strong></p>
<p>大家可以类比思考一下。Bitmap 是一个比较底层的结构在它之上还有一个叫作布隆过滤器的结构Bloom Filter布隆过滤器可以判断一个值不存在或者可能存在。</p>
<p><img src="assets/Ciqc1F8zkZWAKUhuAACFphHz8XU285.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F8zkZWAKUhuAACFphHz8XU285.png" alt="png" /></p>
<p>如图,它相比较 Bitmap它多了一层 hash 算法。既然是 hash 算法,就会有冲突,所以有可能有多个值落在同一个 bit 上。它不像 HashMap一样使用链表或者红黑树来处理冲突而是直接将这个hash槽重复使用。从这个特性我们能够看出布隆过滤器能够明确表示一个值不在集合中但无法判断一个值确切的在集合中。</p>
<p>Guava 中有一个 BloomFilter 的类,可以方便地实现相关功能。</p>
<p>上面这种优化方式,<strong>本质上也是把大对象变成小对象的方式</strong>,在软件设计中有很多类似的思路。比如像一篇新发布的文章,频繁用到的是摘要数据,就不需要把整个文章内容都查询出来;用户的 feed 信息,也只需要保证可见信息的速度,而把完整信息存放在速度较慢的大型存储里。</p>
@@ -280,7 +280,7 @@ String getSex(int userId) {
<p>所谓<strong>热数据</strong>,就是靠近用户的,被频繁使用的数据;而<strong>冷数据</strong>是那些访问频率非常低,年代非常久远的数据。</p>
<p>同一句复杂的 SQL运行在几千万的数据表上和运行在几百万的数据表上前者的效果肯定是很差的。所以虽然你的系统刚开始上线时速度很快但随着时间的推移数据量的增加就会渐渐变得很慢。</p>
<p><strong>冷热分离</strong>是把数据分成两份,如下图,一般都会保持一份全量数据,用来做一些耗时的统计操作。</p>
<p><img src="assets/CgqCHl8zkaGALD0uAADj7bx0YMY053.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8zkaGALD0uAADj7bx0YMY053.png" alt="png" /></p>
<p><strong>由于冷热分离在工作中经常遇到,所以面试官会频繁问到数据冷热分离的方案。下面简单介绍三种:</strong></p>
<h4>1.数据双写</h4>
<p>把对冷热库的插入、更新、删除操作,全部放在一个统一的事务里面。由于热库(比如 MySQL和冷库比如 Hbase的类型不同这个事务大概率会是分布式事务。在项目初期这种方式是可行的但如果是改造一些遗留系统分布式事务基本上是改不动的我通常会把这种方案直接废弃掉。</p>

View File

@@ -212,11 +212,11 @@ public class AopController {
<pre><code>class com.github.xjjdog.spring.ABean$$EnhancerBySpringCGLIB$$a5d91535 | 1023
</code></pre>
<p>下面使用 arthas 分析这个执行过程,找出耗时最高的 AOP 方法。启动 arthas 后,可以从列表中看到我们的应用程序,在这里,输入 2 进入分析界面。</p>
<p><img src="assets/Ciqc1F86TXeAUqo-AADHJU0cEYI383.jpg" alt="15956792012866.jpg" /></p>
<p><img src="assets/Ciqc1F86TXeAUqo-AADHJU0cEYI383.jpg" alt="png" /></p>
<p>在终端输入 trace 命令,然后访问 /aop 接口,终端将打印出一些 debug 信息,可以发现耗时操作就是 Spring 的代理类。</p>
<pre><code>trace com.github.xjjdog.spring.ABean method
</code></pre>
<p><img src="assets/Ciqc1F86TX-Ad9lAAAEgaEWakik394.jpg" alt="15956796510862.jpg" /></p>
<p><img src="assets/Ciqc1F86TX-Ad9lAAAEgaEWakik394.jpg" alt="png" /></p>
<h3>代理模式</h3>
<p>代理模式Proxy可以通过一个代理类来控制对一个对象的访问。</p>
<p>Java 中实现动态代理主要有两种模式:一种是使用 JDK另外一种是使用 CGLib。</p>
@@ -242,7 +242,7 @@ ProxyCreateBenchmark.jdk thrpt 10 15612.467 ± 268.362 ops/ms
<p>当指定为单例时(默认行为),在 Spring 容器中,组件有且只有一份,当你注入相关组件的时候,获取的组件实例也是同一份。</p>
<p>如果是普通的单例类,我们通常将单例的构造方法设置成私有的,单例有懒汉加载和饿汉加载模式。</p>
<p>了解 JVM 类加载机制的同学都知道,一个类从加载到初始化,要经历 5 个步骤:加载、验证、准备、解析、初始化。</p>
<p><img src="assets/Ciqc1F86TV-AT0GiAABJVGTaUd8838.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F86TV-AT0GiAABJVGTaUd8838.png" alt="png" /></p>
<p>其中static 字段和 static 代码块,是属于类的,在类加载的初始化阶段就已经被执行。它在字节码中对应的是 方法,属于类的(构造方法)。因为类的初始化只有一次,所以它就能够保证这个加载动作是线程安全的。</p>
<p>根据以上原理,只要把单例的初始化动作,放在方法里,就能够实现<strong>饿汉模式</strong></p>
<pre><code>private static Singleton instace = new Singleton();
@@ -250,7 +250,7 @@ ProxyCreateBenchmark.jdk thrpt 10 15612.467 ± 268.362 ops/ms
<p>饿汉模式在代码里用的很少,它会造成资源的浪费,生成很多可能永远不会用到的对象。
而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,就是,用来初始化对象的属性。由于在同一时刻,多个线程可以同时调用函数,我们就需要使用 synchronized 关键字对生成过程进行同步。</p>
<p>目前,<strong>公认的兼顾线程安全和效率的单例模式,就是 double check。很多面试官会要求你手写并分析 double check 的原理。</strong></p>
<p><img src="assets/Ciqc1F86TauAd8scAAB4D1n3djc759.jpg" alt="15957667011612.jpg" /></p>
<p><img src="assets/Ciqc1F86TauAd8scAAB4D1n3djc759.jpg" alt="png" /></p>
<p>如上图,是 double check 的关键代码,我们介绍一下四个关键点:</p>
<ul>
<li>第一次检查,当 instance 为 null 的时候,进入对象实例化逻辑,否则直接返回。</li>

View File

@@ -165,7 +165,7 @@ function hide_canvas() {
<h3>并行获取数据</h3>
<p>考虑到下面一种场景。有一个用户数据接口,要求在 50ms 内返回数据。它的调用逻辑非常复杂,打交道的接口也非常多,需要从 20 多个接口汇总数据。这些接口,最小的耗时也要 20ms哪怕全部都是最优状态算下来也需要 20*20 = 400ms。</p>
<p>如下图,解决的方式只有并行,通过多线程同时去获取计算结果,最后进行结果拼接。</p>
<p><img src="assets/CgqCHl8856-AdSNPAACjNbY02o4445.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8856-AdSNPAACjNbY02o4445.png" alt="png" /></p>
<p>但这种编程模型太复杂了,如果使用原始的线程 API或者使用 wait、notify 等函数,代码的复杂度可以想象有多大。但幸运的是,现在 Java 中的大多数并发编程场景,都可以使用 concurrent 包的一些工具类来实现。</p>
<p>在这种场景中,我们就可以使用 <strong>CountDownLatch</strong> 完成操作。CountDownLatch 本质上是一个计数器,我们把它初始化为与执行任务相同的数量。当一个任务执行完时,就将计数器的值减 1直到计数器值达到 0 时,表示完成了所有的任务,在 await 上等待的线程就可以继续执行下去。</p>
<p>下面这段代码,是我专门为这个场景封装的一个工具类。它传入了两个参数:一个是要计算的 job 数量,另外一个是整个大任务超时的毫秒数。</p>
@@ -258,7 +258,7 @@ function hide_canvas() {
</code></pre>
<p>前几个参数没有什么好说的,相对于普通对象池而言,由于线程资源总是有效,它甚至少了非常多的 Idle 配置参数(与对象池比较),我们主要来看一下 workQueue 和 handler。</p>
<p>关于任务的创建过程可以说是多线程每次必问的问题了。如下图所示任务被提交后首先判断它是否达到了最小线程数coreSize如果达到了就将任务缓存在任务队列里。如果队列也满了会判断线程数量是否达到了最大线程数maximumPoolSize如果也达到了就会进入任务的拒绝策略handler</p>
<p><img src="assets/Ciqc1F8858qAEUHZAACDI8Y5Ehc385.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8858qAEUHZAACDI8Y5Ehc385.png" alt="png" /></p>
<p>我们来看一下 Executors 工厂类中默认的几个快捷线程池代码。</p>
<p><strong>1.固定大小线程池</strong></p>
<pre><code>public static ExecutorService newFixedThreadPool(int nThreads) {
@@ -290,7 +290,7 @@ function hide_canvas() {
<p>SpringBoot 中可以非常容易地实现异步任务。</p>
<p>首先,我们需要在启动类上加上 @EnableAsync 注解,然后在需要异步执行的方法上加上 @Async 注解。一般情况下,我们的任务直接在后台运行就可以,但有些任务需要返回一些数据,这个时候,就可以使用 Future 返回一个代理,供其他的代码使用。</p>
<p>关键代码如下:</p>
<p><img src="assets/CgqCHl88596AfI6BAAQTo7l7qJs148.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl88596AfI6BAAQTo7l7qJs148.png" alt="png" /></p>
<p>默认情况下Spring 将启动一个默认的线程池供异步任务使用。这个线程池也是无限大的,资源使用不可控,所以强烈建议你使用代码设置一个适合自己的。</p>
<pre><code>@Bean
public ThreadPoolTaskExecutor getThreadPoolTaskExecutor() {
@@ -317,7 +317,7 @@ public ThreadPoolTaskExecutor getThreadPoolTaskExecutor() {
</ul>
<p>下面以一个经常发生问题的案例,来说一下线程安全的重要性。</p>
<p>SimpleDateFormat 是我们经常用到的日期处理类,但它本身不是线程安全的,在多线程运行环境下,会产生很多问题,在以往的工作中,通过 sonar 扫描,我发现这种误用的情况特别的多。<strong>在面试中,我也会专门问到 SimpleDateFormat用来判断面试者是否具有基本的多线程编程意识。</strong></p>
<p><img src="assets/CgqCHl885-eAAm9sAACoGWQZ14E564.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl885-eAAm9sAACoGWQZ14E564.png" alt="png" /></p>
<p>执行上图的代码,可以看到,时间已经错乱了。</p>
<pre><code>Thu May 01 08:56:40 CST 618104
Thu May 01 08:56:40 CST 618104
@@ -329,7 +329,7 @@ Wed Dec 25 08:56:40 CST 2019
Sun Jul 13 01:55:40 CST 20220200
</code></pre>
<p>解决方式就是使用 ThreadLocal 局部变量,代码如下图所示,可以有效地解决线程安全问题。</p>
<p><img src="assets/CgqCHl885-6AEAKjAADZkiqqLtY077.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl885-6AEAKjAADZkiqqLtY077.png" alt="png" /></p>
<p><strong>2.线程的同步方式</strong></p>
<p>Java 中实现线程同步的方式有很多,大体可以分为以下 8 类。</p>
<ul>
@@ -343,7 +343,7 @@ Sun Jul 13 01:55:40 CST 20220200
<li>使用 Thread 类的 join 方法,可以让多线程按照指定的顺序执行。</li>
</ul>
<p>下面的截图,是使用 LinkedBlockingQueue 实现的一个简单生产者和消费者实例,<strong>在很多互联网的笔试环节,这个题目会经常出现。</strong> 可以看到,我们还使用了一个 volatile 修饰的变量,来决定程序是否继续运行,这也是 volatile 变量的常用场景。</p>
<p><img src="assets/Ciqc1F885_aAbF8qAAEC6dLMPo0828.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F885_aAbF8qAAEC6dLMPo0828.png" alt="png" /></p>
<h3>FastThreadLocal</h3>
<p>在我们平常的编程中,使用最多的就是 ThreadLocal 类了。拿最常用的 Spring 来说,它事务管理的传播机制,就是使用 ThreadLocal 实现的。因为 ThreadLocal 是线程私有的,所以 Spring 的事务传播机制是不能够跨线程的。<strong>在问到 Spring 事务管理是否包含子线程时,要能够想到面试官的真实意图。</strong></p>
<pre><code>/**
@@ -368,10 +368,10 @@ ThreadLocalMap getMap(Thread t) {
}
</code></pre>
<p>问题就出在 ThreadLocalMap 类上,它虽然叫 Map但却没有实现 Map 的接口。如下图ThreadLocalMap 在 rehash 的时候,并没有采用类似 HashMap 的数组+链表+红黑树的做法,它只使用了一个数组,使用<strong>开放寻址</strong>(遇到冲突,依次查找,直到空闲位置)的方法,这种方式是非常低效的。</p>
<p><img src="assets/CgqCHl886AKAV0BAAAOn46tA-8c161.png" alt="Drawing 8.png" /></p>
<p><img src="assets/CgqCHl886AKAV0BAAAOn46tA-8c161.png" alt="png" /></p>
<p>由于 Netty 对 ThreadLocal 的使用非常频繁Netty 对它进行了专项的优化。它之所以快是因为在底层数据结构上做了文章使用常量下标对元素进行定位而不是使用JDK 默认的探测性算法。</p>
<p>还记得《03 | 深入剖析:哪些资源,容易成为瓶颈?》提到的伪共享问题吗?底层的 InternalThreadLocalMap对cacheline 也做了相应的优化。</p>
<p><img src="assets/Ciqc1F886AqAGmwzAAJh0-ZJljI401.png" alt="Drawing 9.png" /></p>
<p><img src="assets/Ciqc1F886AqAGmwzAAJh0-ZJljI401.png" alt="png" /></p>
<h3>你在多线程使用中都遇到过哪些问题?</h3>
<p>通过上面的知识总结,可以看到多线程相关的编程,是属于比较高阶的技能。面试中,<strong>面试官会经常问你在多线程使用中遇到的一些问题,以此来判断你实际的应用情况。</strong></p>
<p>我们先总结一下文中已经给出的示例:</p>
@@ -398,7 +398,7 @@ executor.submit( ()-&gt; {
executor.shutdown();
</code></pre>
<p>我们跟踪任务的执行,在 ThreadPoolExecutor 类中可以找到任务发生异常时的方法,它是抛给了 afterExecute 方法进行处理。</p>
<p><img src="assets/Ciqc1F886BSASB_lAAC1lOQlBbE230.png" alt="Drawing 10.png" /></p>
<p><img src="assets/Ciqc1F886BSASB_lAAC1lOQlBbE230.png" alt="png" /></p>
<p>可惜的是ThreadPoolExecutor 中的 afterExecute 方法是没有任何实现的,它是个空方法。</p>
<pre><code>protected void afterExecute(Runnable r, Throwable t) { }
</code></pre>
@@ -409,7 +409,7 @@ executor.shutdown();
<p>其实这是部分同学对“异步作用”的错误理解。<strong>异步是一种编程模型,它通过将耗时的操作转移到后台线程运行,从而减少对主业务的堵塞,所以我们说异步让速度变快了</strong>。但如果你的系统资源使用已经到了极限,异步就不能产生任何效果了,它主要优化的是那些阻塞性的等待。</p>
<p>在我们前面的课程里,缓冲、缓存、池化等优化方法,都是用到了异步。它能够起到转移冲突,优化请求响应的作用。由于合理地利用了资源,我们的系统响应确实变快了, 之后的《15 | 案例分析:从 BIO 到 NIO再到 AIO》会对此有更多讲解。</p>
<p>异步还能够对业务进行解耦,如下图所示,它比较像是生产者消费者模型。主线程负责生产任务,并将它存放在待执行列表中;消费线程池负责任务的消费,进行真正的业务逻辑处理。</p>
<p><img src="assets/CgqCHl886B6ADAe8AAFW13_eF1Q541.png" alt="Drawing 11.png" /></p>
<p><img src="assets/CgqCHl886B6ADAe8AAFW13_eF1Q541.png" alt="png" /></p>
<h3>小结</h3>
<p>多线程的话题很大,本课时的内容稍微多,我们简单总结一下课时重点。</p>
<p>本课时默认你已经有了多线程的基础知识(否则看起来会比较吃力),所以我们从 CountDownLatch 的一个实际应用场景说起,谈到了线程池的两个重点:<strong>阻塞队列</strong><strong>拒绝策略</strong></p>

View File

@@ -161,7 +161,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>13 案例分析:多线程锁的优化</h1>
<p>我们在上一课时,了解到可以使用 ThreadLocal来避免 SimpleDateFormat 在并发环境下引起的时间错乱问题。其实还有一种解决方式,就是通过对<strong>parse 方法</strong>进行加锁,也能保证日期处理类的正确运行,代码如下图(可见<a href="https://gitee.com/xjjdog/tuning-lagou-res/tree/master/tuning-011/design-pattern">仓库</a></p>
<p><img src="assets/Ciqc1F9DbU6AeoPsAAC8Nn863qc911.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9DbU6AeoPsAAC8Nn863qc911.png" alt="png" /></p>
<p>其实锁对性能的影响,是非常大的。因为对资源加锁以后,资源就被加锁的线程独占,其他的线程就只能排队等待这个锁,此时程序由并行执行,变相地成了顺序执行,执行速度自然就降低了。</p>
<p>下面是开启了 50 个线程,使用 ThreadLocal 和同步锁方式性能的一个对比。</p>
<pre><code>Benchmark Mode Cnt Score Error Units
@@ -229,7 +229,7 @@ void syncBlock();
</code></pre>
<p>这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。我们可以通过下面这张图,来看一下 monitor 的原理。</p>
<p><strong>注意了,下面是面试题目高发地。比如,你能描述一下 monitor 锁的实现原理吗?</strong></p>
<p><img src="assets/CgqCHl9Dl-mAHYlWAACjjjqUdwE492.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl9Dl-mAHYlWAACjjjqUdwE492.png" alt="png" /></p>
<p>如上图所示我们可以把运行时的对象锁抽象地分成三部分。其中EntrySet 和 WaitSet 是两个队列,中间虚线部分是当前持有锁的线程,我们可以想象一下线程的执行过程。</p>
<p>当第一个线程到来时,发现并没有线程持有对象锁,它会直接成为活动线程,进入 RUNNING 状态。</p>
<p>接着又来了三个线程,要争抢对象锁。此时,这三个线程发现锁已经被占用了,就先进入 EntrySet 缓存起来,进入 BLOCKED 状态。此时,从 jstack 命令,可以看到他们展示的信息都是 waiting for monitor entry。</p>
@@ -271,7 +271,7 @@ void syncBlock();
<p>在 JDK 1.8 中synchronized 的速度已经有了显著的提升它都做了哪些优化呢答案就是分级锁。JVM 会根据使用情况,对 synchronized 的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁 — 轻量级锁 — 重量级锁。</p>
<p><strong>锁只能升级,不能降级</strong>,所以一旦升级为重量级锁,就只能依靠操作系统进行调度。</p>
<p>要想了解锁升级的过程,需要先看一下对象在内存里的结构。</p>
<p><img src="assets/Ciqc1F9Dl_uAUOqvAABFvlyPAbE897.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F9Dl_uAUOqvAABFvlyPAbE897.png" alt="png" /></p>
<p>如上图所示,对象分为 MarkWord、Class Pointer、Instance Data、Padding 四个部分。</p>
<p>和锁升级关系最大的就是 MarkWord它的长度是 24 位我们着重介绍一下。它包含Thread ID23bit、Age6bit、Biased1bit、Tag2bit 四个部分,锁升级就是靠判断 Thread Id、Biased、Tag 等三个变量值来进行的。</p>
<ul>
@@ -396,7 +396,7 @@ FairVSNoFairBenchmark.nofair thrpt 10 35195.649 ± 6503.375 ops/ms
<h4>2.优化技巧</h4>
<p>锁的优化理论其实很简单,那就是<strong>减少锁的冲突</strong>。无论是锁的读写分离,还是分段锁,本质上都是为了<strong>避免多个线程同时获取同一把锁</strong></p>
<p>所以我们可以总结一下优化的一般思路:减少锁的粒度、减少锁持有的时间、锁分级、锁分离 、锁消除、乐观锁、无锁等。</p>
<p><img src="assets/Ciqc1F9DmBqAEgcKAABk33fmf4k676.png" alt="3.png" /></p>
<p><img src="assets/Ciqc1F9DmBqAEgcKAABk33fmf4k676.png" alt="png" /></p>
<ul>
<li><strong>减少锁粒度</strong></li>
</ul>

View File

@@ -166,7 +166,7 @@ function hide_canvas() {
<h3>CAS</h3>
<p>CAS 是 Compare And Swap 的缩写,意思是<strong>比较并替换</strong></p>
<p>如下图CAS 机制当中使用了 3 个基本操作数内存地址V、期望值E、要修改的新值N。更新一个变量的时候只有当变量的预期值E 和内存地址V 的真正值相同时才会将内存地址V 对应的值修改为 N。</p>
<p><img src="assets/CgqCHl9GGnmABhbVAAB4RWCMsX0107.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl9GGnmABhbVAAB4RWCMsX0107.png" alt="png" /></p>
<p>如果本次修改不成功,怎么办?很多情况下,它将一直重试,直到修改为期望的值。</p>
<p>拿 AtomicInteger 类来说,相关的代码如下:</p>
<pre><code>public final boolean compareAndSet(int expectedValue, int newValue) {
@@ -201,7 +201,7 @@ inline T Atomic::PlatformCmpxchg&lt;4&gt;::operator()(T exchange_value,
</code></pre>
<p>那 CAS 实现的原子类,性能能提升多少呢?我们开启了 20 个线程,对共享变量进行自增操作。</p>
<p>从测试结果得知,针对频繁的写操作,原子类的性能是 synchronized 方式的 3 倍。</p>
<p><img src="assets/Ciqc1F9GGpOActO6AACqgh2NBj0192.png" alt="chart.png" /></p>
<p><img src="assets/Ciqc1F9GGpOActO6AACqgh2NBj0192.png" alt="png" /></p>
<p><strong>CAS 原理,在近几年面试中的考察率越来越高,主要是由于乐观锁在读多写少的互联网场景中,使用频率愈发频繁。</strong></p>
<p>你可能发现有一些乐观锁的变种,但最基础的思想是一样的,都是基于<strong>比较替换并替换</strong>的基本操作。</p>
<p>关于 Atomic 类,还有一个小细节,那就是它的主要变量,使用了 volatile 关键字进行修饰。代码如下,你知道它是用来干什么的吗?</p>
@@ -347,7 +347,7 @@ try {
}
</code></pre>
<p>使用 redis 的 monitor 命令,可以看到具体的执行步骤,这个过程还是比较复杂的。</p>
<p><img src="assets/CgqCHl9GGvuAY0otAAKk-rNnRjQ444.jpg" alt="15963703738863.jpg" /></p>
<p><img src="assets/CgqCHl9GGvuAY0otAAKk-rNnRjQ444.jpg" alt="png" /></p>
<h3>无锁</h3>
<p>无锁Lock-Free指的是在多线程环境下在访问共享资源的时候不会阻塞其他线程的执行。</p>
<p>在 Java 中,最典型的无锁队列实现,就是 ConcurrentLinkedQueue但它是无界的不能够指定它的大小。ConcurrentLinkedQueue 使用 CAS 来处理对数据的并发访问,这是无锁算法得以实现的基础。</p>

View File

@@ -163,7 +163,7 @@ function hide_canvas() {
<p>Netty 的高性能架构,是基于一个网络编程设计模式 Reactor 进行设计的。现在,大多数与 I/O 相关的组件,都会使用 Reactor 模型,比如 Tomcat、Redis、Nginx 等,可见 Reactor 应用的广泛性。</p>
<p>Reactor 是 NIO 的基础。为什么 NIO 的性能就能够比传统的阻塞 I/O 性能高呢?我们首先来看一下传统阻塞式 I/O 的一些特点。</p>
<h3>阻塞 I/O 模型</h3>
<p><img src="assets/CgqCHl9MynKADFW4AAB9PAD7ZA0902.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9MynKADFW4AAB9PAD7ZA0902.png" alt="png" /></p>
<p>如上图,是典型的<strong>BIO 模型</strong>,每当有一个连接到来,经过协调器的处理,就开启一个对应的线程进行接管。如果连接有 1000 条,那就需要 1000 个线程。</p>
<p>线程资源是非常昂贵的,除了占用大量的内存,还会占用非常多的 CPU 调度时间,所以 BIO 在连接非常多的情况下,效率会变得非常低。</p>
<p>下面的代码是使用 ServerSocket 实现的一个简单 Socket 服务器,监听在 8888 端口。</p>
@@ -207,7 +207,7 @@ nice
PONG:nice
</code></pre>
<p>使用 “04 | 工具实践:如何获取代码性能数据?”提到的 JMC 工具,在录制期间发起多个连接,能够发现有多个线程在运行,和连接数是一一对应的。</p>
<p><img src="assets/CgqCHl9MyoiAGgY5AAGbD3wkqUs988.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9MyoiAGgY5AAGbD3wkqUs988.png" alt="png" /></p>
<p>可以看到BIO 的读写操作是阻塞的,线程的整个生命周期和连接的生命周期是一样的,而且不能够被复用。</p>
<p>就单个阻塞 I/O 来说,它的效率并不比 NIO 慢。但是当服务的连接增多考虑到整个服务器的资源调度和资源利用率等因素NIO 就有了显著的效果NIO 非常适合高并发场景。</p>
<h3>非阻塞 I/O 模型</h3>
@@ -294,7 +294,7 @@ ssc.register(selector, ssc.validOps());
<li>写就绪事件OP_WRITE</li>
</ul>
<p>任何网络和文件操作,都可以抽象成这四个事件。</p>
<p><img src="assets/Ciqc1F9MyqmAdmlrAAMSNPAj_F4698.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9MyqmAdmlrAAMSNPAj_F4698.png" alt="png" /></p>
<p>接下来,在 while 循环里,使用 select 函数,阻塞在主线程里。所谓<strong>阻塞</strong>,就是操作系统不再分配 CPU 时间片到当前线程中,所以 select 函数是几乎不占用任何系统资源的。</p>
<pre><code>int num = selector.select();
</code></pre>
@@ -333,7 +333,7 @@ int size = sc.read(buf);
<h3>Reactor 模式</h3>
<p>了解了 BIO 和 NIO 的一些使用方式Reactor 模式就呼之欲出了。</p>
<p>NIO 是基于事件机制的,有一个叫作 Selector 的选择器,阻塞获取关注的事件列表。获取到事件列表后,可以通过分发器,进行真正的数据操作。</p>
<p><img src="assets/Ciqc1F9MysaAZw9aAADxUNI1q_I139.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9MysaAZw9aAADxUNI1q_I139.png" alt="png" /></p>
<blockquote>
<p>该图来自 Doug Lea 的<a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf"></a><a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf">Scalable IO in Java》</a>,该图指明了最简单的 Reactor 模型的基本元素。</p>
</blockquote>
@@ -346,7 +346,7 @@ int size = sc.read(buf);
<li><strong>Reactor</strong>将具体的事件分配dispatch给 Handler。</li>
</ul>
<p>我们可以对上面的模型进行进一步细化,如下图所示,将 Reactor 分为 mainReactor 和 subReactor 两部分。</p>
<p><img src="assets/Ciqc1F9MytSAebMfAAFlMTAoyJQ496.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F9MytSAebMfAAFlMTAoyJQ496.png" alt="png" /></p>
<blockquote>
<p>该图来自 Doug Lea 的 <a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf">《Scalable IO in Java》</a></p>
</blockquote>
@@ -413,7 +413,7 @@ int size = sc.read(buf);
<p>所以,市面上对 AIO 的实践并不多,在采用技术选型的时候,一定要谨慎。</p>
<h3>响应式编程</h3>
<p>你可能听说过 Spring 5.0 的 WebFluxWebFlux 是可以替代 Spring MVC 的一套解决方案,可以编写响应式的应用,两者之间的关系如下图所示:</p>
<p><img src="assets/Ciqc1F9My2WAeCGbAACrOS4gYGA066.png" alt="image.png" /></p>
<p><img src="assets/Ciqc1F9My2WAeCGbAACrOS4gYGA066.png" alt="png" /></p>
<p>Spring WebFlux 的底层使用的是 Netty所以操作是异步非阻塞的类似的组件还有 vert.x、akka、rxjava 等。</p>
<p>WebFlux 是运行在 project reactor 之上的一个封装,其根本特性是后者提供的,至于再底层的非阻塞模型,就是由 Netty 保证的了。</p>
<p>非阻塞的特性我们可以理解,那响应式又是什么概念呢?</p>

View File

@@ -441,7 +441,7 @@ RegexVsRagelBenchmark.regex thrpt 10 201.322 ± 47.056 ops/ms
<h4>案例 2HikariCP 的字节码修改</h4>
<p><strong>“09 | 案例分析:池化对象的应用场景”</strong> 中,我们提到了 HikariCP 对字节码的修改,这个职责是由 JavassistProxyFactory 类来管理的。Javassist 是一个字节码类库HikariCP 就是用它对字节码进行修改。</p>
<p>如下图所示,这是工厂类的主要方法。</p>
<p><img src="assets/CgqCHl9PEBqAUmucAABlRi1dKhM359.jpg" alt="15970255895249.jpg" /></p>
<p><img src="assets/CgqCHl9PEBqAUmucAABlRi1dKhM359.jpg" alt="png" /></p>
<p>它通过 generateProxyClass 生成代理类,主要是针对 Connection、Statement、ResultSet、DatabaseMetaData 等 jdbc 的核心接口。</p>
<p>右键运行这个类,可以看到代码生成了一堆 Class 文件。</p>
<pre><code>Generating com.zaxxer.hikari.pool.HikariProxyConnection
@@ -453,7 +453,7 @@ Generating com.zaxxer.hikari.pool.HikariProxyCallableStatement
Generating method bodies for com.zaxxer.hikari.proxy.ProxyFactory
</code></pre>
<p>对于这一部分的代码组织,使用了设计模式中的委托模式。我们发现 HikariCP 源码中的代理类,比如 ProxyConnection都是 abstract 的,它的具体实例就是使用 javassist 生成的 class 文件。反编译这些生成的 class 文件,可以看到它实际上是通过调用父类中的委托对象进行处理的。</p>
<p><img src="assets/CgqCHl9PECuAFz5zAAB0EwpHKE0091.jpg" alt="15970285875030.jpg" /></p>
<p><img src="assets/CgqCHl9PECuAFz5zAAB0EwpHKE0091.jpg" alt="png" /></p>
<p>这么做有两个好处:</p>
<ul>
<li>第一,在代码中只需要实现需要修改的 JDBC 接口方法,其他的交给代理类自动生成的代码,极大地减少了编码数量。</li>

View File

@@ -165,7 +165,7 @@ function hide_canvas() {
<p><strong>另外,本课时的知识点,全部是面试的高频题目,这也从侧面看出 JVM 理论知识的重要性。</strong></p>
<h3>JVM 内存区域划分</h3>
<p>学习 JVM内存区域划分是绕不过去的知识点这几乎是面试必考的题目。如下图所示内存区域划分主要包括堆、Java 虚拟机栈、程序计数器、本地方法栈、元空间和直接内存这五部分,我将逐一介绍。</p>
<p><img src="assets/Ciqc1F9V1s-ACFuJAACTNmJefTc741.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9V1s-ACFuJAACTNmJefTc741.png" alt="png" /></p>
<p>JVM 内存区域划分图</p>
<h4>1.堆</h4>
<p>如 JVM 内存区域划分图所示JVM 中占用内存最大的区域就是堆Heap我们平常编码创建的对象大多数是在这上面分配的也是垃圾回收器回收的主要目标区域。</p>
@@ -173,7 +173,7 @@ function hide_canvas() {
<p>JVM 的解释过程是基于栈的,程序的执行过程也就是入栈出栈的过程,这也是 Java 虚拟机栈这个名称的由来。</p>
<p>Java 虚拟机栈是和线程相关的。当你启动一个新的线程Java 就会为它分配一个虚拟机栈,之后所有这个线程的运行,都会在栈里进行。</p>
<p>Java 虚拟机栈,从方法入栈到具体的字节码执行,其实是一个双层的栈结构,也就是栈里面还包含栈。</p>
<p><img src="assets/CgqCHl9V1tiACYPGAACVVpU1HCY374.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl9V1tiACYPGAACVVpU1HCY374.png" alt="png" /></p>
<p>Java 虚拟机栈图</p>
<p>如上图Java 虚拟机栈里的每一个元素,叫作栈帧。每一个栈帧,包含四个区域: 局部变量表 、操作数栈、动态连接和返回地址。</p>
<p>其中,<strong>操作数栈</strong>就是具体的字节码指令所操作的栈区域,考虑到下面这段代码:</p>
@@ -256,7 +256,7 @@ function hide_canvas() {
</ul>
<p>这个假设我们称之为弱代假设weak generational hypothesis</p>
<p>如下图分代垃圾回收器会在逻辑上把堆空间分为两部分年轻代Young generation和老年代Old generation</p>
<p><img src="assets/CgqCHl9V1xiADnRJAAGDq44CQZc346.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9V1xiADnRJAAGDq44CQZc346.png" alt="png" /></p>
<p>堆空间划分图:年轻代和老年代</p>
<h4>1.年轻代</h4>
<p>年轻代中又分为一个伊甸园空间Eden两个幸存者空间Survivor。对象会首先在年轻代中的 Eden 区进行分配,当 Eden 区分配满的时候,就会触发年轻代的 GC。</p>
@@ -282,7 +282,7 @@ function hide_canvas() {
<p>有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如 G1通过 TargetSurvivorRatio 这个参数,动态更改对象提升的阈值。</p>
<p>老年代的空间一般比较大,回收的时间更长,当老年代的空间被占满了,将发生老年代垃圾回收。</p>
<p>目前,被广泛使用的是 G1 垃圾回收器。G1 的目标是用来干掉 CMS 的它同样有年轻代和老年代的概念。不过G1 把整个堆切成了很多份,把每一份当作一个小目标,部分上目标很容易达成。</p>
<p><img src="assets/CgqCHl9V1y-APldqAABR3cE-qV0698.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9V1y-APldqAABR3cE-qV0698.png" alt="png" /></p>
<p>如上图G1 也是有 Eden 区和 Survivor 区的概念的只不过它们在内存上不是连续的而是由一小份一小份组成的。G1 在进行垃圾回收的时候将会根据最大停顿时间MaxGCPauseMillis设置的值动态地选取部分小堆区进行垃圾回收。</p>
<p>G1 的配置非常简单,我们只需要配置三个参数,一般就可以获取优异的性能:</p>
<p>① MaxGCPauseMillis 设置最大停顿的预定目标G1 垃圾回收器会自动调整,选取特定的小堆区;</p>

View File

@@ -161,7 +161,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>18 高级进阶JIT 如何影响 JVM 的性能?</h1>
<p>我们在上一课时,我们了解到 Java 虚拟机栈,其实是一个双层的栈,如下图所示,第一层就是针对 method 的栈帧,第二层是针对字节码指令的操作数栈。</p>
<p><img src="assets/CgqCHl9YevCARjawAAC9Bvf_IoE321.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9YevCARjawAAC9Bvf_IoE321.png" alt="png" /></p>
<p>Java 虚拟机栈图</p>
<p>栈帧的创建是需要耗费资源的,尤其是对于 Java 中常见的 getter、setter 方法来说,这些代码通常只有一行,每次都创建栈帧的话就太浪费了。</p>
<p>另外Java 虚拟机栈对代码的执行,采用的是字节码解释的方式,考虑到下面这段代码,变量 a 声明之后,就再也不被使用,要是按照字节码指令解释执行的话,就要做很多无用功。</p>
@@ -194,7 +194,7 @@ function hide_canvas() {
<p>另外我们了解到垃圾回收器回收的目标区域主要是堆堆上创建的对象越多GC 的压力就越大。要是能把一些变量,直接在栈上分配,那 GC 的压力就会小一些。</p>
<p>其实我们说的这几个优化的可能性JVM 已经通过 JIT 编译器Just In Time Compiler去做了JIT 最主要的目标是把解释执行变成编译执行。</p>
<p>为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,这就是 JIT 编译器的功能。</p>
<p><img src="assets/CgqCHl9YeyCAWROAAABdUyAOP5E893.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9YeyCAWROAAABdUyAOP5E893.png" alt="png" /></p>
<p>如上图JVM 会将调用次数很高,或者在 for 循环里频繁被使用的代码,编译成机器码,然后缓存在 CodeCache 区域里,下次调用相同方法的时候,就可以直接使用。</p>
<p>那 JIT 编译都有哪些手段呢?接下来我们详细介绍。</p>
<h3>方法内联</h3>
@@ -230,7 +230,7 @@ JMHSample_16_CompilerControl.dontinline avgt 3 1.934 ± 3.112 ns/op
JMHSample_16_CompilerControl.exclude avgt 3 57.603 ± 4.435 ns/op
JMHSample_16_CompilerControl.inline avgt 3 0.483 ± 1.520 ns/op
</code></pre>
<p><img src="assets/CgqCHl9Ye0KASdaPAAFd8UGlCRY151.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9Ye0KASdaPAAFd8UGlCRY151.png" alt="png" /></p>
<p>JIT 编译之后的二进制代码,是放在 Code Cache 区域里的。这个区域的大小是固定的,而且一旦启动无法扩容。如果 Code Cache 满了JVM 并不会报错但会停止编译。所以编译执行就会退化为解释执行性能就会降低。不仅如此JIT 编译器会一直尝试去优化你的代码,造成 CPU 占用上升。</p>
<p>通过参数 -XX:ReservedCodeCacheSize 可以指定 Code Cache 区域的大小,如果你通过监控发现空间达到了上限,就要适当的增加它的大小。</p>
<h3>编译层次</h3>
@@ -308,7 +308,7 @@ BuilderVsBufferBenchmark.builder thrpt 10 103280.200 ± 76172.538 ops/ms
<pre><code> -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jitdemo.log
</code></pre>
<p>使用 jitwatch 工具,可打开这个文件,看到详细的编译结果。</p>
<p><img src="assets/Ciqc1F9Ye36AMYIDAAcn4iSavAY997.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9Ye36AMYIDAAcn4iSavAY997.png" alt="png" /></p>
<p>下面是一段测试代码:</p>
<pre><code>public class SimpleInliningTest {
public SimpleInliningTest() {
@@ -329,7 +329,7 @@ BuilderVsBufferBenchmark.builder thrpt 10 103280.200 ± 76172.538 ops/ms
}
</code></pre>
<p>从执行后的结果可以看到,热点 for 循环已经使用 JIT 进行了编译,而里面应用的 add 方法,也已经被内联。</p>
<p><img src="assets/CgqCHl9Ye4eAaVmXAATwlN8rh4w940.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9Ye4eAaVmXAATwlN8rh4w940.png" alt="png" /></p>
<h3>小结</h3>
<p>JIT 是现代 JVM 主要的优化点,能够显著地提升程序的执行效率。从解释执行到最高层次的 C2一个数量级的性能提升也是有可能的。但即时编译的过程是非常缓慢的既耗时间也费空间所以这些优化操作会和解释执行同时进行。</p>
<p>值得注意的是JIT 在某些情况下还会出现逆优化。比如一些热部署方式触发的 redefineClass就会造成 JIT 编译结果的失效,相关的内联代码也需要重新生成。</p>

View File

@@ -188,7 +188,7 @@ OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)
</code></pre>
<p>其实,通过 Xmx 指定了的堆内存,只有在 JVM 真正使用的时候,才会进行分配。这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了。在堆比较大的时候,会加大启动时间,但它能够减少内存动态分配的性能损耗,提高运行时的速度。</p>
<p>如下图JVM 的内存,分为堆和堆外内存,其中堆的大小可以通过 Xmx 和 Xms 来配置。</p>
<p><img src="assets/CgqCHl9fOACAW_TIAAClqw0re70194.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9fOACAW_TIAAClqw0re70194.png" alt="png" /></p>
<p>但我们在配置 ES 的堆内存时,通常把堆的初始化大小,设置成物理内存的一半。这是因为 ES 是存储类型的服务,我们需要预留一半的内存给文件缓存(理论参见 <strong>“07 | 案例分析:无处不在的缓存,高并发系统的法宝”</strong>),等下次用到相同的文件时,就不用与磁盘进行频繁的交互。这一块区域一般叫作 <strong>PageCache</strong>,占用的空间很大。</p>
<p>对于计算型节点来说,比如我们普通的 Web 服务,通常会把堆内存设置为物理内存的 2/3剩下的 1/3 就是给堆外内存使用的。</p>
<p>我们这张图,对堆外内存进行了非常细致的划分,解释如下:</p>

View File

@@ -184,7 +184,7 @@ management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
</code></pre>
<p>启动之后,我们就可以通过访问<a href="http://localhost:8080/actuator/prometheus">监控接口</a>来获取监控数据。</p>
<p><img src="assets/CgqCHl9htcuAAw51AAK0O_g_pbM862.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9htcuAAw51AAK0O_g_pbM862.png" alt="png" /></p>
<p>想要监控业务数据也是比较简单的,你只需要注入一个 MeterRegistry 实例即可,下面是一段示例代码:</p>
<pre><code>@Autowired
MeterRegistry registry;
@@ -202,10 +202,10 @@ public String test() {
<pre><code>test_total{from=&quot;127.0.0.1&quot;,method=&quot;test&quot;,} 5.0
</code></pre>
<p>这里简单介绍一下流行的<strong>Prometheus 监控体系</strong>Prometheus 使用拉的方式获取监控数据,这个暴露数据的过程可以交给功能更加齐全的 telegraf 组件。</p>
<p><img src="assets/CgqCHl9htdiAO89HAAK1NRYCNZE604.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9htdiAO89HAAK1NRYCNZE604.png" alt="png" /></p>
<p>如上图,我们通常使用 Grafana 进行监控数据的展示,使用 AlertManager 组件进行提前预警。这一部分的搭建工作不是我们的重点,感兴趣的同学可自行研究。</p>
<p>下图便是一张典型的监控图,可以看到 Redis 的缓存命中率等情况。</p>
<p><img src="assets/Ciqc1F9htd-AXIKHAANYYdIDl6g753.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9htd-AXIKHAANYYdIDl6g753.png" alt="png" /></p>
<h3>Java 生成火焰图</h3>
<p><strong>火焰图</strong>是用来分析程序运行瓶颈的工具。</p>
<p>火焰图也可以用来分析 Java 应用。可以从 <a href="https://github.com/jvm-profiling-tools/async-profiler">github</a> 上下载 async-profiler 的压缩包进行相关操作。比如,我们把它解压到 /root/ 目录,然后以 javaagent 的方式来启动 Java 应用,命令行如下:</p>
@@ -213,11 +213,11 @@ public String test() {
</code></pre>
<p>运行一段时间后,停止进程,可以看到在当前目录下,生成了 profile.svg 文件,这个文件是可以用浏览器打开的。
如下图所示,纵向,表示的是调用栈的深度;横向,表明的是消耗的时间。所以格子的宽度越大,越说明它可能是一个瓶颈。一层层向下浏览,即可找到需要优化的目标。</p>
<p><img src="assets/Ciqc1F9htfOAN3G1AEK7W4TM0AU264.gif" alt="2020-08-21 17-07-35.2020-08-21 17_12_29.gif" /></p>
<p><img src="assets/Ciqc1F9htfOAN3G1AEK7W4TM0AU264.gif" alt="png" /></p>
<h3>优化思路</h3>
<p>对一个普通的 Web 服务来说,我们来看一下,要访问到具体的数据,都要经历哪些主要的环节?</p>
<p>如下图,在浏览器中输入相应的域名,需要通过 DNS 解析到具体的 IP 地址上,为了保证高可用,我们的服务一般都会部署多份,然后使用 Nginx 做反向代理和负载均衡。</p>
<p><img src="assets/Ciqc1F9htgCAcdwGAAIVQmXnOPo885.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F9htgCAcdwGAAIVQmXnOPo885.png" alt="png" /></p>
<p>Nginx 根据资源的特性会承担一部分动静分离的功能。其中动态功能部分会进入我们的SpringBoot 服务。</p>
<p>SpringBoot 默认使用内嵌的 tomcat 作为 Web 容器,使用典型的 MVC 模式,最终访问到我们的数据。</p>
<h3>HTTP 优化</h3>
@@ -349,7 +349,7 @@ Transfer/sec: 1.51MB
<pre><code>java -javaagent:/opt/skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=the-demo-name -jar /opt/test-service/spring-boot-demo.ja --spring.profiles.active=dev
</code></pre>
<p>访问一些服务的链接,打开 Skywalking 的 UI即可看到下图的界面。这些指标可以类比“01 | 理论分析:性能优化,有哪些衡量指标?需要注意什么?”提到的衡量指标去理解,我们就可以从图中找到响应比较慢 QPS 又比较高的接口,进行专项优化。</p>
<p><img src="assets/Ciqc1F9htwyARKqMAAgxG3QYe8A553.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9htwyARKqMAAgxG3QYe8A553.png" alt="png" /></p>
<h3>各个层次的优化方向</h3>
<h4>1.Controller 层</h4>
<p>controller 层用于接收前端的查询参数,然后构造查询结果。现在很多项目都采用前后端分离的架构,所以 controller 层的方法,一般会使用 @ResponseBody 注解,把查询的结果,解析成 JSON 数据返回(兼顾效率和可读性)。</p>
@@ -363,10 +363,10 @@ Transfer/sec: 1.51MB
<p>service 层的代码组织,对代码的可读性、性能影响都比较大。我们常说的设计模式,大多数都是针对 service 层来说的。</p>
<p>service 层会频繁使用更底层的资源,通过组合的方式获取我们所需要的数据,大多数可以通过我们前面课时提供的优化思路进行优化。</p>
<p>这里要着重提到的一点,就是分布式事务。</p>
<p><img src="assets/CgqCHl9htvaAf1S-AAKlZCq3SXg275.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9htvaAf1S-AAKlZCq3SXg275.png" alt="png" /></p>
<p>如上图,四个操作分散在三个不同的资源中。要想达到一致性,需要三个不同的资源 MySQL、MQ、ElasticSearch 进行统一协调。它们底层的协议,以及实现方式,都是不一样的,那就无法通过 Spring 提供的 Transaction 注解来解决,需要借助外部的组件来完成。</p>
<p>很多人都体验过加入了一些保证一致性的代码一压测性能掉的惊掉下巴。分布式事务是性能杀手因为它要使用额外的步骤去保证一致性常用的方法有两阶段提交方案、TCC、本地消息表、MQ 事务消息、分布式事务中间件等。</p>
<p><img src="assets/Ciqc1F9htx6ADeh6AAFoqvxy4eM753.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F9htx6ADeh6AAFoqvxy4eM753.png" alt="png" /></p>
<p>如上图,分布式事务要在改造成本、性能、时效等方面进行综合考虑。有一个介于分布式事务和非事务之间的名词,叫作<strong>柔性事务</strong>。柔性事务的理念是将业务逻辑和互斥操作,从资源层上移至业务层面。</p>
<p><strong>关于传统事务和柔性事务,我们来简单比较一下。</strong></p>
<p><strong>ACID</strong></p>

View File

@@ -191,7 +191,7 @@ function hide_canvas() {
<h4>5.通用</h4>
<p>lsof 命令可以查看当前进程所关联的所有资源sysctl 命令可以查看当前系统内核的配置参数; dmesg 命令可以显示系统级别的一些信息,比如被操作系统的 oom-killer 杀掉的进程就可以在这里找到。</p>
<p>整理了一幅脑图,可供你参考:</p>
<p><img src="assets/Ciqc1F9obKuAe7CEAAEshp5LbOA665.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F9obKuAe7CEAAEshp5LbOA665.png" alt="png" /></p>
<h3>常用工具集合</h3>
<p>为了找到系统的问题,我们会采用类似于神农尝百草的方式,用多个工具、多种手段获取系统的运行状况。</p>
<h4>1.信息收集</h4>
@@ -208,7 +208,7 @@ function hide_canvas() {
<p>skywalking 可以用来分析分布式环境下的调用链问题,可以详细地看到每一步执行的耗时。但如果你没有这样的环境,就可以使用命令行工具 arthas 对方法进行 trace最终也能够深挖找到具体的慢逻辑。</p>
<p>jvm-profiling-tools可以生成火焰图辅助我们分析问题。另外更加底层的针对操作系统的性能测评和调优工具还有perf和SystemTap感兴趣的同学可以自行研究一下。</p>
<p>关于工具方面的内容你可以回顾“04 | 工具实践如何获取代码性能数据”和“05工具实践基准测试 JMH精确测量方法性能”进行回忆复习我整理了一幅脑图可供你参考。</p>
<p><img src="assets/Ciqc1F9obL2AJTQPAAFOXihBiAA696.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F9obL2AJTQPAAFOXihBiAA696.png" alt="png" /></p>
<h3>基本解决方式</h3>
<p>找到了具体的性能瓶颈点,就可以针对性地进行优化。</p>
<h4>1.CPU 问题</h4>
@@ -241,7 +241,7 @@ function hide_canvas() {
<p>网络 I/O 的另外一个问题就是频繁的网络交互,通过将结果集合并,使用批量的方式,可以显著增加性能,但这种方式的使用场景有限,比较适合异步的任务处理。</p>
<p>使用 netstat 命令,或者 lsof 命令可以获取进程所关联的TIME_WAIT 和 CLOSE_WAIT 网络状态的数量,前者可以通过调整内核参数来解决,但后者多是应用程序的 BUG。</p>
<p>我整理了一幅脑图,可供你参考。</p>
<p><img src="assets/CgqCHl9obM2AUI9qAAFueXY-U4s279.png" alt="3.png" /></p>
<p><img src="assets/CgqCHl9obM2AUI9qAAFueXY-U4s279.png" alt="png" /></p>
<p>有了上面的信息收集和初步优化,我想你脑海里应该对要优化的系统已经有了非常详细的了解,是时候改变一些现有代码的设计了。</p>
<p><strong>可以说如果上面的基本解决方式面向的是“面”,那么代码层面的优化,面向的就是具体的“性能瓶颈点”。</strong></p>
<h3>代码层面</h3>
@@ -277,10 +277,10 @@ function hide_canvas() {
<p>并不是说系统的资源利用率越低,我们的代码写得就越好。作为一个编码者,我们要想方设法压榨系统的剩余价值,让所有的资源都轮转起来。尤其在高并发场景下,这种轮转就更加重要——属于在一定压力下系统的最优状态。</p>
<p>资源不能合理的利用,就是一种浪费。比如,业务应用多属于 I/O 密集型业务,如果让请求都阻塞在 I/O 上,就造成了 CPU 资源的浪费。这时候使用并行,就可以在同一时刻承担更多的任务,并发量就能够增加;再比如,我们监控到 JVM 的堆空闲空间,长期处于高位,那就可以考虑加大堆内缓存的容量,或者缓冲区的容量。</p>
<p>我整理了一幅脑图,可供你参考。</p>
<p><img src="assets/CgqCHl9obNuAOt-nAAGiF2SGIDY158.png" alt="4.png" /></p>
<p><img src="assets/CgqCHl9obNuAOt-nAAGiF2SGIDY158.png" alt="png" /></p>
<h3>PDCA 循环方法论</h3>
<p>性能优化是一个循环的过程,需要根据数据反馈进行实时调整。有时候,测试结果表明,有些优化的效果并不好,就需要回滚到优化前的版本,重新寻找突破点。</p>
<p><img src="assets/CgqCHl9obOqAFQ2CAABk4i6nXkU801.png" alt="5.png" /></p>
<p><img src="assets/CgqCHl9obOqAFQ2CAABk4i6nXkU801.png" alt="png" /></p>
<p>如上图,<strong>PDCA 循环</strong>的方法论可以支持我们管理性能优化的过程,它有 4 个步骤:</p>
<ul>
<li>PPlanning计划阶段找出存在的性能问题收集性能指标信息确定要改进的目标准备达到这些目标的具体措施</li>
@@ -289,7 +289,7 @@ function hide_canvas() {
<li>Aact处理阶段将成功的优化经验进行推广由点及面进行覆盖为负面影响提供解决方案将错误的方法形成经验。</li>
</ul>
<p>如此周而复始,应用的性能将会逐步提高,如下图,对于性能优化来说,就可以抽象成下面的方式。</p>
<p><img src="assets/Ciqc1F9obPiANviwAAB2amhgXUU818.png" alt="6.png" /></p>
<p><img src="assets/Ciqc1F9obPiANviwAAB2amhgXUU818.png" alt="png" /></p>
<p>既然叫作循环,就说明这个过程是可以重复执行的。事实上,在我们的努力下,应用性能会螺旋式上升,最终达到我们的期望。</p>
<h3>求职面经</h3>
<h4>1. 关注“性能优化”的副作用问题</h4>