前言

synchronized和Lock通过互斥保障原子性,能够保护共享数据以实现线程安全,其作用包括保障原子性、可见性、有序性

常见问题

在平时聊天或者面试过程中,可能会被问到,既然已经有了synchronized了,为什么JSR166小组花这么多时间来开发j.u.c的Lock框架呢,换句话说就是内部锁和显示锁之前有什么区别?

分析

synchronized(内部锁)

java平台中的任何一个对象都有唯一一个与之关联的锁,这种锁称为监视器(Monitor)或者内部锁(Intrinsic Lock),内部锁是通过synchronized关键字实现的,可以用来修饰方法(同步方法、静态方法)、代码块(临界区)

Lock(显示锁)

JDK1.5开始引入的排他锁,默认实现是ReetrantLock,作为一种线程同步机制,其拥有和synchronized相同的语义,并且还提供了一些synchronized不具备的特性

差异

从本质上来讲

synchronized是在JVM层面实现的,

ReetrantLock是java API层面实现的排它锁,系统无法自动释放锁,需要在代码中的finally子句中显示释放,否则会出现锁泄漏

从安全上来讲

内部锁在退出临界出时,会自动释放锁,不会导致锁泄漏

外部锁如果未主动释放锁或者释放代码在finally子句中,容易导致锁泄漏

从使用上来讲

synchronized可以修饰方法,修饰代码块,但是内部锁的申请与释放只能在一个方法内进行,因为代码无法跨方法

Lock,只能修饰代码块,但是它可以发挥面向对象编程的灵活性,显示锁的申请在一个方法,在另一个方法里释放锁

在锁的调度方面

内部锁公平锁,显示锁即支持非公平也支持公平锁

在问题定位方面

线程转储可能无法包含显示锁的相关信息,从而导致问题定位困难。比如果在JDK1.5下线程转储中会包含内部锁的相关信息,不包含显示锁的信息

从性能方面方面

等待同一把内部锁的线程,都在同一个等待队列中,等待系统调度,而ReentrantLock锁,可以通过Condition条件变量,实现分组等待的效果,所以性能表现上更好一些

从其它特性方面

当一个线程在等待获取一个锁时,因为线程活性故障导致其永远无法获取得锁时,使用内部锁的线程会一直傻傻的等待一个无法获得的锁,换句话说,内部锁缺少可中断的特性,

显示锁它拥有与内部锁相同的并发性和内存主义,但是添加了轮询锁定时锁等候可中断锁等候一些新特性,使其在激烈争用情况下表现出更好的性能,因为当多线程访问共享资源时,JVM可以将更多的时间用于执行线程上,而不是浪费时间在线程调度上。

  • 轮询锁意味着,ReentrantLock支持公平锁,可以通过轮询的方式依次获取锁

  • 定时锁等候意味着,线程在N长时间之内无法获取到锁,就会返回false ,表示获取锁失败,tryLock方法,不会像内部锁一样痴痴的等待一个没有结果的未来

  • 可中断锁等待,意味着ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

如何选择

如果你使用的是JDK1.5的话,在争用不高的时候可以使用内部锁,在争用高的情况下,建议使用显示锁

如果你使用的是JDK1.5+的版本,随着对内部锁的优化(锁消除、锁粗化、偏向锁、自适应锁),两都之间的性能差异已经缩小了很多,如果后期内部锁的这些优化可以应用到显示锁的话,那性能可能就会有很大差距了。

总体上来说,在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是资源竞争非常激烈的时候,synchronized的性能会下降很多,而ReentrantLock的性能表现仍然比较稳定。

结束语

在工作中,为了保证线程安全我们不一定要使用锁,可以使用一些轻量级的同步工具或者无锁的框架和工具,来提升应用的性能。

前言

在日常工作中,因为编码不规范或者工具类使用不当,会导致cpu负载过高,响应时间变长,面对这样的情况,应该有一套自己的排查方法,下面分享下我个人的排查过程

过程分享

第一步** 寻找病人

​ 通过 ps -ef|grep java 或者 jps -lm 先找出你需要排查的java应用,记录下PID

第二步 找出患病的部位

即找出该进程内最耗费CPU的线程

top -Hp pid推荐使用) 等价于 top -p pid 然后通过shift +h 切换到线程模式

ps -Lfp pid

ps -mp pid -o THREAD, tid, time

第三步 寻问患病部位的情况

将找到的线程ID转成16进制(printf "%x\n" 18888 //得到16进度的值)通过jstack打印出线程的日志

打出进程相关线程的堆栈信息,进行分析,查看是因为什么原因导致CPU过高

jstack -l 8888 | grep 49c8 -A 100 //得到线程18888相关的后100行日志

**第四步 **分析病因

仔细分析jstack日志,看是否可以定位到出现问题的代码,如果运气好的话, 我们去排查一下相关代码,具体问题具体分析,去解决掉引起问题的代码。

  • 在这一步,我一般会去排查一下wait 和 locked相关的日志,确认是不是线程死锁引起的,从JAVA5之后,线程Dump中可以直接报告出Java级别的死锁

  • 如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。

    在分析jstack日志时,主要观察一下用户级别的线程,线程会处在不同的状态,主要关心以下几个状态

    Wait on condition

    常见情况是该线程在sleep,等待sleep的时间到了时候,将被唤醒。还有一种有可能是网络瓶颈,这需要介绍一些网络工具或网络相关的命令去排查一下

    Waiting for monitor entry

    对象以“Entry Set”中等待的线程状态是“Waiting for monitor entry”

    in Object.wait()

    对象在“Wait Set”中等待的线程状态是“in Object.wait()”

第五步 继续寻找原因

  • jmap –heap 8888 打印堆的统计信息,分析JVM当前使用情况

  • jstat -gcutil 8888 查看一下GC情况,看是不是因为频繁的GC引起,如果是因为GC引起我会通过命令 jmap -histo:live 8888,去看一下目前堆里在TOP-K存活对象的情况,如果发现在TOP-K中发现有一些项目中的类,我会去排查代码中,在使用该类的地方,是否因为使用不当,产生了一些强引用,导致频繁的GC。

第六步 从其它方面寻找病因

  • 分析堆内存使用情况和YGC FULL GC频率,调优应用启动的JVM参数

    Jstat -gc pid /. Stat -gccapacity pid / jmap -heap pid 推荐使用前两个,第三个可能会导致进程状态变为T

  • 在应用启动时候增加相关的GC日志参数,输出更详细的日志,来帮忙我们更准确的找到问题。

  • dump 日志,使用MAT加载堆文件分析日志

  • 当然引起应用响应缓慢的原因很多,除了从JVM方面排查 ,我们应该优先从应用层面排查,比如打印出每个方法的响应时间 ,如果线上应用,可以通过btrace来定位出现问题的地方,针对响应长的链路进行排查,有可能是因为程序编码不当,有可能是因为DB慢查询引起,这个对于不同的应用,可能会有不同的情况 ,具体问题具体分析了。

常用的JVM命令

jmap

jmap -histo 88888 //查看对象创建的数量

jmap -histo 8888 >a.log

jmap -histo:live 8888 查看内存中存活对象的数据和名称

jmap -dump:format=b,file=outfile.bin 8888 //dump出堆内存

jmap –heap 8888 //打印heap的概要信息

jstack

jstack -l 8888

jstat

jstat -gc 8888 2000 20(每隔2秒监控一次GC情况,共做10)

转自:https://www.jianshu.com/p/d7665192aaaf

说起MySQL的查询优化,相信大家收藏了一堆奇技淫巧:不能使用SELECT *、不使用NULL字段、合理创建索引、为字段选择合适的数据类型….. 你是否真的理解这些优化技巧?是否理解其背后的工作原理?在实际场景下性能真有提升吗?我想未必。因而理解这些优化建议背后的原理就尤为重要,希望本文能让你重新审视这些优化建议,并在实际业务场景下合理的运用。

MySQL逻辑架构

如果能在头脑中构建一幅MySQL各组件之间如何协同工作的架构图,有助于深入理解MySQL服务器。下图展示了MySQL的逻辑架构图。

MySQL逻辑架构,来自:高性能MySQL

MySQL逻辑架构整体分为三层,最上层为客户端层,并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。
MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。
最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux下的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。

MySQL查询过程

我们总是希望MySQL能够获得更高的查询性能,最好的办法是弄清楚MySQL是如何优化和执行查询的。一旦理解了这一点,就会发现:很多的查询优化工作实际上就是遵循一些原则让MySQL的优化器能够按照预想的合理方式运行而已。
当向MySQL发送一个请求的时候,MySQL到底做了些什么呢?

MySQL查询过程

客户端/服务端通信协议

MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。
客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置max_allowed_packet参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。
与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

查询缓存

在解析一个查询语句前,如果查询缓存是打开的,那么MySQL会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。
MySQL将缓存存放在一个引用表(不要理解成table,可以认为是类似于HashMap的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。
如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,其查询结果
都不会被缓存。比如函数NOW()或者CURRENT_DATE()会因为不同的查询时间,返回不同的查询结果,再比如包含CURRENT_USER或者CONNECION_ID()的查询语句会因为不同的用户而返回不同的结果,将这样的查询结果缓存起来没有任何的意义。
既然是缓存,就会失效,那查询缓存何时失效呢?MySQL的查询缓存系统会跟踪查询中涉及的每个表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。正因为如此,在任何的写操作时,MySQL必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗,甚至导致系统僵死一会儿。而且查询缓存对系统的额外消耗也不仅仅在写操作,读操作也不例外:

  1. 任何的查询语句在开始之前都必须经过检查,即使这条SQL语句永远不会命中缓存
  2. 如果查询结果可以被缓存,那么执行完成后,会将结果存入缓存,也会带来额外的系统消耗

基于此,我们要知道并不是什么情况下查询缓存都会提高系统性能,缓存和失效都会带来额外消耗,只有当缓存带来的资源节约大于其本身消耗的资源时,才会给系统带来性能提升。但要如何评估打开缓存是否能够带来性能提升是一件非常困难的事情,也不在本文讨论的范畴内。如果系统确实存在一些性能问题,可以尝试打开查询缓存,并在数据库设计上做一些优化,比如:

  1. 用多个小表代替一个大表,注意不要过度设计
  2. 批量插入代替循环单条插入
  3. 合理控制缓存空间大小,一般来说其大小设置为几十兆比较合适
  4. 可以通过SQL_CACHE和SQL_NO_CACHE来控制某个查询语句是否需要进行缓存

最后的忠告是不要轻易打开查询缓存,特别是写密集型应用。如果你实在是忍不住,可以将query_cache_type设置为DEMAND,这时只有加入SQL_CACHE的查询才会走缓存,其他查询则不会,这样可以非常自由地控制哪些查询需要被缓存。
当然查询缓存系统本身是非常复杂的,这里讨论的也只是很小的一部分,其他更深入的话题,比如:缓存是如何使用内存的?如何控制内存的碎片化?事务对查询缓存有何影响等等,读者可以自行阅读相关资料,这里权当抛砖引玉吧。

语法解析和预处理

MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。

查询优化

经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成查询计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在MySQL可以通过查询当前会话的last_query_cost的值来得到其计算当前查询的成本。
mysql> select * from t_message limit 10;
…省略结果集

mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+

示例中的结果表示优化器认为大概需要做6391个数据页的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布情况等等。
有非常多的原因会导致MySQL选择错误的执行计划,比如统计信息不准确、不会考虑不受其控制的操作成本(用户自定义函数、存储过程)、MySQL认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但MySQL值选择它认为成本小的,但成本小并不意味着执行时间短)等等。

MySQL的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:

  • 重新定义表的关联顺序(多张表关联查询时,并不一定按照SQL中指定的顺序进行,但有一些技巧可以指定关联顺序)
  • 优化MIN()和MAX()函数(找某列的最小值,如果该列有索引,只需要查找B+Tree索引最左端,反之则可以找到最大值,具体原理见下文)
  • 提前终止查询(比如:使用Limit时,查找到满足数量的结果集后会立即终止查询)
  • 优化排序(在老版本MySQL会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多)

随着MySQL的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个非常常用且容易理解的优化策略,其他的优化策略,大家自行查阅吧。

查询执行引擎

在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为handler API。查询过程中的每一张表由一个handler实例表示。实际上,MySQL在查询优化阶段就为每一张表创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。

返回结果给客户端

查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。
如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。
结果集返回客户端是一个增量且逐步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足①中所描述的通信协议的数据包发送,再通过TCP协议进行传输,在传输过程中,可能对MySQL的数据包进行缓存然后批量发送。
回头总结一下MySQL整个查询执行过程,总的来说分为6个步骤:

客户端向MySQL服务器发送一条查询请求
服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
服务器进行SQL解析、预处理、再由优化器生成对应的执行计划
MySQL根据执行计划,调用存储引擎的API来执行查询
将结果返回给客户端,同时缓存查询结果

性能优化建议

看了这么多,你可能会期待给出一些优化手段,是的,下面会从3个不同方面给出一些优化建议。但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下通过测试来验证你关于执行计划以及响应时间的假设。

Scheme设计与数据类型优化

选择数据类型只要遵循小而简单的原则就好,越小的数据类型通常会更快,占用更少的磁盘、内存,处理时需要的CPU周期也更少。越简单的数据类型在计算时需要更少的CPU周期,比如,整型就比字符操作代价低,因而会使用整型来存储ip地址,使用DATETIME来存储时间,而不是使用字符串。
这里总结几个可能容易理解错误的技巧:

  1. 通常来说把可为NULL的列改为NOT NULL不会对性能提升有多少帮助,只是如果计划在列上创建索引,就应该将该列设置为NOT NULL。
  2. 对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用32位(4个字节)存储空间,那么它的表示范围已经确定,所以INT(1)和INT(20)对于存储和计算是相同的。
  3. UNSIGNED表示不允许负值,大致可以使正数的上限提高一倍。比如TINYINT存储范围是-128 ~ 127,而UNSIGNED TINYINT存储的范围却是0 - 255。
  4. 通常来讲,没有太大的必要使用DECIMAL数据类型。即使是在需要存储财务数据时,仍然可以使用BIGINT。比如需要精确到万分之一,那么可以将数据乘以一百万然后使用BIGINT存储。这样可以避免浮点数计算不准确和DECIMAL精确计算代价高的问题。
  5. TIMESTAMP使用4个字节存储空间,DATETIME使用8个字节存储空间。因而,TIMESTAMP只能表示1970 - 2038年,比DATETIME表示的范围小得多,而且TIMESTAMP的值因时区不同而不同。
  6. 大多数情况下没有使用枚举类型的必要,其中一个缺点是枚举的字符串列表是固定的,添加和删除字符串(枚举选项)必须使用ALTER TABLE(如果只只是在列表末尾追加元素,不需要重建表)。
  7. schema的列不要太多。原因是存储引擎的API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是非常高的。如果列太多而实际使用的列又很少的话,有可能会导致CPU占用过高。
  8. 大表ALTER TABLE非常耗时,MySQL执行大部分修改表结果操作的方法是用新的结构创建一个张空表,从旧表中查出所有的数据插入新表,然后再删除旧表。尤其当内存不足而表又很大,而且还有很大索引的情况下,耗时更久。当然有一些奇技淫巧可以解决这个问题,有兴趣可自行查阅。

创建高性能索引

索引是提高MySQL查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。应当尽量避免事后才想起添加索引,因为事后可能需要监控大量的SQL才能定位到问题所在,而且添加索引的时间肯定是远大于初始添加索引所需要的时间,可见索引的添加也是非常有技术含量的。
接下来将向你展示一系列创建高性能索引的策略,以及每条策略其背后的工作原理。但在此之前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。

索引相关的数据结构和算法

通常我们所说的索引是指B-Tree索引,它是目前关系型数据库中查找数据最为常用和有效的索引,大多数存储引擎都支持这种索引。使用B-Tree这个术语,是因为MySQL在CREATE TABLE或其它语句中使用了这个关键字,但实际上不同的存储引擎可能使用不同的数据结构,比如InnoDB就是使用的B+Tree。
B+Tree中的B是指balance,意为平衡。需要注意的是,B+树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后得到要查找的数据。
在介绍B+Tree前,先了解一下二叉查找树,它是一种经典的数据结构,其左子树的值总是小于根的值,右子树的值总是大于根的值,如下图①。如果要在这课树中查找值为5的记录,其大致流程:先找到根,其值为6,大于5,所以查找左子树,找到3,而5大于3,接着找3的右子树,总共找了3次。同样的方法,如果查找值为8的记录,也需要查找3次。所以二叉查找树的平均查找次数为(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而顺序查找的话,查找值为2的记录,仅需要1次,但查找值为8的记录则需要6次,所以顺序查找的平均查找次数为:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,因此大多数情况下二叉查找树的平均查找速度比顺序查找要快。

二叉查找树和平衡二叉树

由于二叉查找树可以任意构造,同样的值,可以构造出如图②的二叉查找树,显然这棵二叉树的查询效率和顺序查找差不多。若想二叉查找数的查询性能最高,需要这棵二叉查找树是平衡的,也即平衡二叉树(AVL树)。
平衡二叉树首先需要符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度差不能大于1。显然图②不满足平衡二叉树的定义,而图①是一课平衡二叉树。平衡二叉树的查找性能是比较高的(性能最好的是最优二叉树),查询性能越好,维护的成本就越大。比如图①的平衡二叉树,当用户需要插入一个新的值9的节点时,就需要做出如下变动。

平衡二叉树旋转

通过一次左旋操作就将插入后的树重新变为平衡二叉树是最简单的情况了,实际应用场景中可能需要旋转多次。至此我们可以考虑一个问题,平衡二叉树的查找效率还不错,实现也非常简单,相应的维护成本还能接受,为什么MySQL索引不直接使用平衡二叉树?
随着数据库中数据的增加,索引本身大小随之增加,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级。可以想象一下一棵几百万节点的二叉树的深度是多少?如果将这么大深度的一颗二叉树放磁盘上,每读取一个节点,需要一次磁盘的I/O读取,整个查找的耗时显然是不能够接受的。那么如何减少查找过程中的I/O存取次数?
一种行之有效的解决方法是减少树的深度,将二叉树变为m叉树(多路搜索树),而B+Tree就是一种多路搜索树。理解B+Tree时,只需要理解其最重要的两个特征即可:第一,所有的关键字(可以理解为数据)都存储在叶子节点(Leaf Page),非叶子节点(Index Page)并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,所有的叶子节点由指针连接。如下图为高度为2的简化了的B+Tree。

简化B+Tree

怎么理解这两个特征?MySQL将每个节点的大小设置为一个页的整数倍(原因下文会介绍),也就是在节点空间大小一定的情况下,每个节点可以存储更多的内结点,这样每个结点能索引的范围更大更精确。所有的叶子节点使用指针链接的好处是可以进行区间访问,比如上图中,如果查找大于20而小于30的记录,只需要找到节点20,就可以遍历指针依次找到25、30。如果没有链接指针的话,就无法进行区间查找。这也是MySQL使用B+Tree作为索引存储结构的重要原因。
MySQL为何将节点大小设置为页的整数倍,这就需要理解磁盘的存储原理。磁盘本身存取就比主存慢很多,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度往往是主存的几百万分之一,为了尽量减少磁盘I/O,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存,预读的长度一般为页的整数倍。

页是计算机管理存储器的逻辑块,硬件及OS往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(许多OS中,页的大小通常为4K)。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后一起返回,程序继续运行。

MySQL巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次I/O。假设B+Tree的高度为h,一次检索最多需要h-1次I/O(根节点常驻内存),复杂度O(h) = O(logmN)。实际应用场景中,M通常较大,常常超过100,因此树的高度一般都比较小,通常不超过3。
最后简单了解下B+Tree节点的操作,在整体上对索引的维护有一个大概的了解,虽然索引可以大大提高查询效率,但维护索引仍要花费很大的代价,因此合理的创建索引也就尤为重要。
仍以上面的树为例,我们假设每个节点只能存储4个内节点。首先要插入第一个节点28,如下图所示。

leaf page和index page都没有满

接着插入下一个节点70,在Index Page中查询后得知应该插入到50 - 70之间的叶子节点,但叶子节点已满,这时候就需要进行也分裂的操作,当前的叶子节点起点为50,所以根据中间值来拆分叶子节点,如下图所示。

Leaf Page拆分

最后插入一个节点95,这时候Index Page和Leaf Page都满了,就需要做两次拆分,如下图所示。

Leaf Page与Index Page拆分

拆分后最终形成了这样一颗树。

最终树

B+Tree为了保持平衡,对于新插入的值需要做大量的拆分页操作,而页的拆分需要I/O操作,为了尽可能的减少页的拆分操作,B+Tree也提供了类似于平衡二叉树的旋转功能。当Leaf Page已满但其左右兄弟节点没有满的情况下,B+Tree并不急于去做拆分操作,而是将记录移到当前所在页的兄弟节点上。通常情况下,左兄弟会被先检查用来做旋转操作。就比如上面第二个示例,当插入70的时候,并不会去做页拆分,而是左旋操作。

左旋操作

通过旋转操作可以最大限度的减少页分裂,从而减少索引维护过程中的磁盘的I/O操作,也提高索引维护效率。需要注意的是,删除节点跟插入节点类似,仍然需要旋转和拆分操作,这里就不再说明。

高性能策略

通过上文,相信你对B+Tree的数据结构已经有了大致的了解,但MySQL中索引是如何组织数据的存储呢?以一个简单的示例来说明,假如有如下数据表:
CREATE TABLE People(
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum(m,f) not null,
key(last_name,first_name,dob)
);

对于表中每一行数据,索引中包含了last_name、first_name、dob列的值,下图展示了索引是如何组织数据存储的。

索引如何组织数据存储,来自:高性能MySQL

可以看到,索引首先根据第一个字段来排列顺序,当名字相同时,则根据第三个字段,即出生日期来排序,正是因为这个原因,才有了索引的“最左原则”。

1、MySQL不会使用索引的情况:非独立的列

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。比如:
select * from where id + 1 = 5

我们很容易看出其等价于 id = 4,但是MySQL无法自动解析这个表达式,使用函数是同样的道理。

2、前缀索引

如果列很长,通常可以索引开始的部分字符,这样可以有效节约索引空间,从而提高索引效率。

3、多列索引和索引顺序

在多数情况下,在多个列上建立独立的索引并不能提高查询性能。理由非常简单,MySQL不知道选择哪个索引的查询效率更好,所以在老版本,比如MySQL5.0之前就会随便选择一个列的索引,而新的版本会采用合并索引的策略。举个简单的例子,在一张电影演员表中,在actor_id和film_id两个列上都建立了独立的索引,然后有如下查询:
select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1

老版本的MySQL会随机选择一个索引,但新版本做如下的优化:
select film_id,actor_id from film_actor where actor_id = 1 union all
select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1

  • 当出现多个索引做相交操作时(多个AND条件),通常来说一个包含所有相关列的索引要优于多个独立索引。
  • 当出现多个索引做联合操作时(多个OR条件),对结果集的合并、排序等操作需要耗费大量的CPU和内存资源,特别是当其中的某些索引的选择性不高,需要返回合并大量数据时,查询成本更高。所以这种情况下还不如走全表扫描。

因此explain时如果发现有索引合并(Extra字段出现Using union),应该好好检查一下查询和表结构是不是已经是最优的,如果查询和表都没有问题,那只能说明索引建的非常糟糕,应当慎重考虑索引是否合适,有可能一个包含所有相关列的多列索引更适合。
前面我们提到过索引如何组织数据存储的,从图中可以看到多列索引时,索引的顺序对于查询是至关重要的,很明显应该把选择性更高的字段放到索引的前面,这样通过第一个字段就可以过滤掉大多数不符合条件的数据。

索引选择性是指不重复的索引值和数据表的总记录数的比值,选择性越高查询效率越高,因为选择性越高的索引可以让MySQL在查询时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

理解索引选择性的概念后,就不难确定哪个字段的选择性较高了,查一下就知道了,比如:
SELECT * FROM payment where staff_id = 2 and customer_id = 584

是应该创建(staff_id,customer_id)的索引还是应该颠倒一下顺序?执行下面的查询,哪个字段的选择性更接近1就把哪个字段索引前面就好。
select count(distinct staff_id)/count() as staff_id_selectivity,
count(distinct customer_id)/count(
) as customer_id_selectivity, count(*) from payment

多数情况下使用这个原则没有任何问题,但仍然注意你的数据中是否存在一些特殊情况。举个简单的例子,比如要查询某个用户组下有过交易的用户信息:
select user_id from trade where user_group_id = 1 and trade_amount > 0

MySQL为这个查询选择了索引(user_group_id,trade_amount),如果不考虑特殊情况,这看起来没有任何问题,但实际情况是这张表的大多数数据都是从老系统中迁移过来的,由于新老系统的数据不兼容,所以就给老系统迁移过来的数据赋予了一个默认的用户组。这种情况下,通过索引扫描的行数跟全表扫描基本没什么区别,索引也就起不到任何作用。
推广开来说,经验法则和推论在多数情况下是有用的,可以指导我们开发和设计,但实际情况往往会更复杂,实际业务场景下的某些特殊情况可能会摧毁你的整个设计。

4、避免多个范围条件

实际开发中,我们会经常使用多个范围条件,比如想查询某个时间段内登录过的用户:
select user.* from user where login_time > '2017-04-01' and age between 18 and 30;

这个查询有一个问题:它有两个范围条件,login_time列和age列,MySQL可以使用login_time列的索引或者age列的索引,但无法同时使用它们。

5、覆盖索引

如果一个索引包含或者说覆盖所有需要查询的字段的值,那么就没有必要再回表查询,这就称为覆盖索引。覆盖索引是非常有用的工具,可以极大的提高性能,因为查询只需要扫描索引会带来许多好处:

索引条目远小于数据行大小,如果只读取索引,极大减少数据访问量
索引是有按照列值顺序存储的,对于I/O密集型的范围查询要比随机从磁盘读取每一行数据的IO要少的多

6、使用索引扫描来排序

MySQL有两种方式可以生产有序的结果集,其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的。如果explain的结果中type列的值为index表示使用了索引扫描来做排序。
扫描索引本身很快,因为只需要从一条索引记录移动到相邻的下一条记录。但如果索引本身不能覆盖所有需要查询的列,那么就不得不每扫描一条索引记录就回表查询一次对应的行。这个读取操作基本上是随机I/O,因此按照索引顺序读取数据的速度通常要比顺序地全表扫描要慢。
在设计索引时,如果一个索引既能够满足排序,又满足查询,是最好的。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向也一样时,才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有ORDER BY子句引用的字段全部为第一张表时,才能使用索引做排序。ORDER BY子句和查询的限制是一样的,都要满足最左前缀的要求(有一种情况例外,就是最左的列被指定为常数,下面是一个简单的示例),其他情况下都需要执行排序操作,而无法利用索引排序。

// 最左列为常数,索引:(date,staff_id,customer_id)
select staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id

7、冗余和重复索引

冗余索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应当尽量避免这种索引,发现后立即删除。比如有一个索引(A,B),再创建索引(A)就是冗余索引。冗余索引经常发生在为表添加新索引时,比如有人新建了索引(A,B),但这个索引不是扩展已有的索引(A)。
大多数情况下都应该尽量扩展已有的索引而不是创建新索引。但有极少情况下出现性能方面的考虑需要冗余索引,比如扩展已有索引而导致其变得过大,从而影响到其他使用该索引的查询。

8、删除长期未使用的索引

定期删除一些长时间未使用过的索引是一个非常好的习惯。
关于索引这个话题打算就此打住,最后要说一句,索引并不总是最好的工具,只有当索引帮助提高查询速度带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,简单的全表扫描更高效。对于中到大型的表,索引就非常有效。对于超大型的表,建立和维护索引的代价随之增长,这时候其他技术也许更有效,比如分区表。最后的最后,explain后再提测是一种美德。

特定类型查询优化

优化COUNT()查询

COUNT()可能是被大家误解最多的函数了,它有两种不同的作用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值是非空的,它不会统计NULL。如果确认括号中的表达式不可能为空时,实际上就是在统计行数。最简单的就是当使用COUNT()时,并不是我们所想象的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计行数。
我们最常见的误解也就在这儿,在括号内指定了一列却希望统计结果是行数,而且还常常误以为前者的性能会更好。但实际并非这样,如果要统计行数,直接使用COUNT(
),意义清晰,且性能更好。
有时候某些业务场景并不需要完全精确的COUNT值,可以用近似值来代替,EXPLAIN出来的行数就是一个不错的近似值,而且执行EXPLAIN并不需要真正地去执行查询,所以成本非常低。通常来说,执行COUNT()都需要扫描大量的行才能获取到精确的数据,因此很难优化,MySQL层面还能做得也就只有覆盖索引了。如果不还能解决问题,只有从架构层面解决了,比如添加汇总表,或者使用redis这样的外部缓存系统。

优化关联查询

在大数据场景下,表与表之间通过一个冗余字段来关联,要比直接使用JOIN有更好的性能。如果确实需要使用关联查询的情况下,需要特别注意的是:

  • 确保ON和USING字句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器关联的顺序是A、B,那么就不需要在A表的对应列上创建索引。没有用到的索引会带来额外的负担,一般来说,除非有其他理由,只需要在关联顺序中的第二张表的相应列上创建索引(具体原因下文分析)。
  • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。

要理解优化关联查询的第一个技巧,就需要理解MySQL是如何执行关联查询的。当前MySQL关联执行的策略非常简单,它对任何的关联都执行嵌套循环关联操作,即先在一个表中循环取出单条数据,然后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为为止。然后根据各个表匹配的行,返回查询中需要的各个列。
太抽象了?以上面的示例来说明,比如有这样的一个查询:
SELECT A.xx,B.yy
FROM A INNER JOIN B USING(c)
WHERE A.xx IN (5,6)

假设MySQL按照查询中的关联顺序A、B来进行关联操作,那么可以用下面的伪代码表示MySQL如何完成这个查询:
outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6);
outer_row = outer_iterator.next;
while(outer_row) {
inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c;
inner_row = inner_iterator.next;
while(inner_row) {
output[inner_row.yy,outer_row.xx];
inner_row = inner_iterator.next;
}
outer_row = outer_iterator.next;
}

可以看到,最外层的查询是根据A.xx列来查询的,A.c上如果有索引的话,整个关联查询也不会使用。再看内层的查询,很明显B.c上如果有索引的话,能够加速查询,因此只需要在关联顺序中的第二张表的相应列上创建索引即可。

优化LIMIT分页

当需要分页操作时,通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY字句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。
一个常见的问题是当偏移量非常大的时候,比如:LIMIT 10000 20这样的查询,MySQL需要查询10020条记录然后只返回20条记录,前面的10000条都将被抛弃,这样的代价非常高。
优化这种查询一个最简单的办法就是尽可能的使用覆盖索引扫描,而不是查询所有的列。然后根据需要做一次关联查询再返回所有的列。对于偏移量很大时,这样做的效率会提升非常大。考虑下面的查询:
SELECT film_id,description FROM film ORDER BY title LIMIT 50,5;

如果这张表非常大,那么这个查询最好改成下面的样子:
SELECT film.film_id,film.description
FROM film INNER JOIN (
SELECT film_id FROM film ORDER BY title LIMIT 50,5
) AS tmp USING(film_id);

这里的延迟关联将大大提升查询效率,让MySQL扫描尽可能少的页面,获取需要访问的记录后在根据关联列回原表查询所需要的列。
有时候如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET,比如下面的查询:
SELECT id FROM t LIMIT 10000, 10;
改为:
SELECT id FROM t WHERE id > 10000 LIMIT 10;

其他优化的办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表中只包含主键列和需要做排序的列。

优化UNION

MySQL处理UNION的策略是先创建临时表,然后再把各个查询结果插入到临时表中,最后再来做查询。因此很多优化策略在UNION查询中都没有办法很好的时候。经常需要手动将WHERE、LIMIT、ORDER BY等字句“下推”到各个子查询中,以便优化器可以充分利用这些条件先优化。
除非确实需要服务器去重,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致整个临时表的数据做唯一性检查,这样做的代价非常高。当然即使使用ALL关键字,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。虽然很多时候没有这个必要,比如有时候可以直接把每个子查询的结果返回给客户端。

结语

理解查询是如何执行以及时间都消耗在哪些地方,再加上一些优化过程的知识,可以帮助大家更好的理解MySQL,理解常见优化技巧背后的原理。希望本文中的原理、示例能够帮助大家更好的将理论和实践联系起来,更多的将理论知识运用到实践中。
其他也没啥说的了,给大家留两个思考题吧,可以在脑袋里想想答案,这也是大家经常挂在嘴边的,但很少有人会思考为什么?

有非常多的程序员在分享时都会抛出这样一个观点:尽可能不要使用存储过程,存储过程非常不容易维护,也会增加使用成本,应该把业务逻辑放到客户端。既然客户端都能干这些事,那为什么还要存储过程?
JOIN本身也挺方便的,直接查询就好了,为什么还需要视图呢?

参考资料
[1] 姜承尧 著;MySQL技术内幕-InnoDB存储引擎;机械工业出版社,2013
[2] Baron Scbwartz 等著;宁海元 周振兴等译;高性能MySQL(第三版); 电子工业出版社, 2013
[3] 由 B-/B+树看 MySQL索引结构

TCP的滑动窗口的可靠性也是建立在“确认重传”基础上的。
发送窗口只有收到对端对于本端发送窗口内字节的ACK确认,才会移动发送窗口的左
边界。 接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量
控制。滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗
口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己
被淹没的目的。拥塞窗口是发送方使用的流量控制,而滑动窗口则是接收方使用的流
量控制。

什么是流量控制
防止发送方发的太快,耗尽接收方的资源,从而使接收方来不及处理

流量控制的一些知识点
(1)接收端抑制发送端的依据:接收端缓冲区的大小
(2)流量控制的目标是接收端,是怕接收端来不及处理
(3)流量控制的机制是丢包

在确认应答策略中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能比较差,尤其是数据往返的时间长的时候
使用滑动窗口,就可以一次发送多条数据,从而就提高了性能

拥塞控制的一般原理

在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏-产生拥塞。
出现网络拥塞的条件:对资源需求的总和 > 可用资源
若网络中有许多资源同时产生拥塞,网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增发而下降。

拥塞控制与流量控制的关系

拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路过载。
拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
流量控制往往指在给定的发送端和接受端之间点对点通信量的控制。
流量控制所要做的就是抑制发送端发送数据的速率,以便使接受端来得及接收数据。

窗口滑动关键字

  • 传输层接收方流控措施
  • 防止发送的太快,耗尽消费方资源,导致接收方处理不及时
  • 认应答策略的缺点
    • 一问一答, 性能差,吞吐量小
  • ACK消息TCP首部中的“窗口大小”字段
  • 高效可靠的发送大量的数据

拥塞窗口关键字

  • 传输层发送方使用的流控措施

发送端如何知道已经丢包?

1.tcp超时重传定时器超时
2.收到三个重复的ack  

为什么会有拥塞控制?

流量控制虽然可以高效可靠的传送大量的数据,但是如果在刚开始阶段就发送大量的数据,可能会导致网络拥堵,因为网络上的计算机太多了

TCP的拥塞控制

拥塞控制主要包含四个方面:
img.png

  • 慢开始
    • 指数增加 -> 拥塞避免(线性增加) -> 发生拥塞(除法减少)
  • 拥塞避免
    • 线性增加
  • 快重传
    • 快重传规定,发送端只要一连收到三个重复的ACK即可断定有分组丢失了,就应立即重传丢失的报文而不必继续等待为该报文段设置重传计时器的超时
    • 快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失地报文段
  • 快恢复
    • 当发送端收到连续三个重复地ACK使,就重新设置开始门限 ssthresh。变为原来的一半
    • 与慢开始不同之处使拥塞窗口 cwnd不是设置为 1 ,而是设置为ssrhresh + 3 × MSS (而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法)
    • 若收到地重复地 ACK 为 n个 。则将cwnd 设置为 ssthresh + n × MSS
    • 若发送窗口值还容许发送报文段,就按照拥塞避免算法继续发送报文段
    • 若收到了确认新地报文段地 ACK ,就将 swnd 缩小到 ssthresh。(直至收到对丢失报文段和其后若干报文段的累积确认后,置cwnd=ssthresh,进入拥塞避免阶段。)

指数增长到阈值 -> 线性增加(CWND) -> 出现拥塞 -> 除法减少(1/2) -> 重置阈值

接收窗口RWnd:是指就手段根据其目前的最大缓存大小所许诺的最新的窗口值,是来自接受端的流量控制
拥塞窗口CWnd:是发送端根据自己估计的网络拥塞程序而设置的窗口值,是来自发送端的流量控制。
发送端的发送窗口的上限值应当取为接收端窗口RWnd和拥塞窗口CWnd这两个变量中较小的一个。 即:Swnd = Min(RWnd, CWnd)

流量控制和拥塞控制的区别

相同点

(1)现象都是丢包;
(2)实现机制都是让发送方发的慢一点,发的少一点

不同点

(1)丢包位置不同
流量控制丢包位置是在接收端上
拥塞控制丢包位置是在路由器上
(2)作用的对象不同
流量控制的对象是接收方,怕发送方发的太快,使得接收方来不及处理
拥塞控制的对象是网络,怕发送发发的太快,造成网络拥塞,使得网络来不及处理

联系

拥塞控制

  • 拥塞控制通常表示的是一个全局性的过程,它会涉及到网络中所有的主机、
  • 所有的路由器和降低网络传输性能的所有因素

流量控制

  • 流量控制发生在发送端和接收端之间,只是点到点之间的控制

结尾

参考资料
https://blog.csdn.net/qq_45795744/article/details/123083507
https://blog.csdn.net/A_FEI_LINUX/article/details/114776693

作者:BK

简书地址:https://www.jianshu.com/u/a5230c4f0b7a

鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

介绍

skyWalking是一个OAP(可观测分析平台)和APM系统(应用管理系统),它可以帮助我们理解系统行为,通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路和指标,SkyWalkingAPM会感知应用间关系和服务间关系,并进行相应的指标统计。 以便发生故障的时候,能够快速定位和解决问题。

本文使用的是skywalking8.8.1版本 , skywalking-agent使用的是apache-skywalking-java-agent-8.8.0

开工

资源准备

  • 下载skywalking
  • 下载skywalking-agent

启动apo

修改配置
启动

启动skywalking-UI

修改配置
启动

编写skywalking-dubbo-provider项目

只提供关于skywalking上关的核心配置

  1. 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>8.7.0</version>
    </dependency>
    <!--skyWalking中的traceId记录到logback日志,我的skyWalking版本为8.5.0所以此处也选用了相同的版本-->
    <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>8.7.0</version>
    </dependency>

  2. 编写核心代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * @author BK
    * @description: traceId test service
    * @date 2022/11/2 14:54
    */
    @DubboService
    public class TraceTestServiceImpl implements TraceTestService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TraceTestServiceImpl.class);

    @Override
    public String invokeMethod(String name) {
    LOGGER.info("invokeMethod.receive.request:{}", name);
    return "result:" + TraceContext.traceId();
    }
    }
  3. 配置logback
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
     <!--    控制台直播取tid变量即可-->
    <property name="CONSOLE_LOG_PATTERN" value="[%date{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%tid] [%thread] [%c{10}:%line] %msg%n%rEx"/>
    <!-- 文件中通过MDC获取变量-->
    <property name="MDC_LOG_PATTERN" value="[%date{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%X{tid}] [%thread] [%c{10}:%line] %msg%n%rEx"/>

    <appender name="stdoutAppender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
    <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
    <pattern>${CONSOLE_LOG_PATTERN}</pattern>
    </layout>
    </encoder>
    </appender>

    <appender name="log_file" class="com.yili.marmot.log.ext.core.TraceIdSupportFileRollAppender">
    <File>${LOG_HOME}/warn.log</File>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>WARN</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <FileNamePattern>${LOG_HOME}/warn.%d{yyyy-MM-dd_HH}.%i.log.gz</FileNamePattern>
    <MaxHistory>72</MaxHistory>
    <maxFileSize>1GB</maxFileSize>
    <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
    <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
    <Pattern>${MDC_LOG_PATTERN}</Pattern>
    </layout>
    </encoder>
    </appender>
  4. 配置启动参数
  5. 启动项目

编写my-webapp

  1. 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>8.7.0</version>
    </dependency>
    <!--skyWalking中的traceId记录到logback日志,我的skyWalking版本为8.5.0所以此处也选用了相同的版本-->
    <dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>8.7.0</version>
    </dependency>

  2. 配置logback
  3. 配置启动参数
  4. 启动项目

验证结果

  1. 访问url

查看日志

  • my-webapp

  • skywalking-dubbo-provider
    两个项目日志打印的traceId相同, 说明成功了

在本地搭建环境出现的问题

dubbo应用接入skywalking后追踪链路会断

最后统一了版本,就可以了应该是有些版本不兼容, 严格按照上述的版本琰搭建,会避免该问题

本文使用到的代码

git:

延伸阅读

https://skywalking.apache.org/zh/2022-08-30-pingan-jiankang/

TCP的滑动窗口的可靠性也是建立在“确认重传”基础上的。
发送窗口只有收到对端对于本端发送窗口内字节的ACK确认,才会移动发送窗口的左
边界。 接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量
控制。滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗
口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己
被淹没的目的。拥塞窗口是发送方使用的流量控制,而滑动窗口则是接收方使用的流
量控制。

什么是流量控制
防止发送方发的太快,耗尽接收方的资源,从而使接收方来不及处理

流量控制的一些知识点
(1)接收端抑制发送端的依据:接收端缓冲区的大小
(2)流量控制的目标是接收端,是怕接收端来不及处理
(3)流量控制的机制是丢包

在确认应答策略中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能比较差,尤其是数据往返的时间长的时候
使用滑动窗口,就可以一次发送多条数据,从而就提高了性能

拥塞控制的一般原理

在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏-产生拥塞。
出现网络拥塞的条件:对资源需求的总和 > 可用资源
若网络中有许多资源同时产生拥塞,网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增发而下降。

拥塞控制与流量控制的关系

拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路过载。
拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
流量控制往往指在给定的发送端和接受端之间点对点通信量的控制。
流量控制所要做的就是抑制发送端发送数据的速率,以便使接受端来得及接收数据。

窗口滑动关键字

  • 传输层接收方流控措施
  • 防止发送的太快,耗尽消费方资源,导致接收方处理不及时
  • 认应答策略的缺点
    • 一问一答, 性能差,吞吐量小
  • ACK消息TCP首部中的“窗口大小”字段
  • 高效可靠的发送大量的数据

拥塞窗口关键字

  • 传输层发送方使用的流控措施

发送端如何知道已经丢包?

1.tcp超时重传定时器超时
2.收到三个重复的ack  

为什么会有拥塞控制?

流量控制虽然可以高效可靠的传送大量的数据,但是如果在刚开始阶段就发送大量的数据,可能会导致网络拥堵,因为网络上的计算机太多了

TCP的拥塞控制

拥塞控制主要包含四个方面:
img.png

  • 慢开始
    • 指数增加 -> 拥塞避免(线性增加) -> 发生拥塞(除法减少)
  • 拥塞避免
    • 线性增加
  • 快重传
    • 快重传规定,发送端只要一连收到三个重复的ACK即可断定有分组丢失了,就应立即重传丢失的报文而不必继续等待为该报文段设置重传计时器的超时
    • 快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失地报文段
  • 快恢复
    • 当发送端收到连续三个重复地ACK使,就重新设置开始门限 ssthresh。变为原来的一半
    • 与慢开始不同之处使拥塞窗口 cwnd不是设置为 1 ,而是设置为ssrhresh + 3 × MSS (而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法)
    • 若收到地重复地 ACK 为 n个 。则将cwnd 设置为 ssthresh + n × MSS
    • 若发送窗口值还容许发送报文段,就按照拥塞避免算法继续发送报文段
    • 若收到了确认新地报文段地 ACK ,就将 swnd 缩小到 ssthresh。(直至收到对丢失报文段和其后若干报文段的累积确认后,置cwnd=ssthresh,进入拥塞避免阶段。)

指数增长到阈值 -> 线性增加(CWND) -> 出现拥塞 -> 除法减少(1/2) -> 重置阈值

接收窗口RWnd:是指就手段根据其目前的最大缓存大小所许诺的最新的窗口值,是来自接受端的流量控制
拥塞窗口CWnd:是发送端根据自己估计的网络拥塞程序而设置的窗口值,是来自发送端的流量控制。
发送端的发送窗口的上限值应当取为接收端窗口RWnd和拥塞窗口CWnd这两个变量中较小的一个。 即:Swnd = Min(RWnd, CWnd)

流量控制和拥塞控制的区别

相同点

(1)现象都是丢包;
(2)实现机制都是让发送方发的慢一点,发的少一点

不同点

(1)丢包位置不同
流量控制丢包位置是在接收端上
拥塞控制丢包位置是在路由器上
(2)作用的对象不同
流量控制的对象是接收方,怕发送方发的太快,使得接收方来不及处理
拥塞控制的对象是网络,怕发送发发的太快,造成网络拥塞,使得网络来不及处理

联系

拥塞控制

  • 拥塞控制通常表示的是一个全局性的过程,它会涉及到网络中所有的主机、
  • 所有的路由器和降低网络传输性能的所有因素

流量控制

  • 流量控制发生在发送端和接收端之间,只是点到点之间的控制

结尾

参考资料
https://blog.csdn.net/qq_45795744/article/details/123083507
https://blog.csdn.net/A_FEI_LINUX/article/details/114776693

作者:BK

简书地址:https://www.jianshu.com/u/a5230c4f0b7a

鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

什么是循环引用

​ 循环引用就是循环依赖,就是两个或多个bean相互之前间的持有对方,比如对象CycleA,CycleB两个对象,如果CycleA引用了CycleB实例,CycleB引用了CycleA实例,它们最终反应为一个环

解决办法

  1. 使用@JsonIgnore标注在属性或对应的get、set方法上,在序列化的时候将该属性忽略,避免循环引用,但是这种方式在反序列化时,此属性同样会被忽略,不会自动注入。如果需要反序列化时注入该属性,则用下面的方法

  2. @JsonBackReference@JsonManagedReference:这两个标注通常配对使用,通常用在父子关系中。

    • 序列化(serialization)

    @JsonBackReference标注的属性在序列化(serialization,即将对象转换为json数据)时,会被忽略(即结果中的json数据不包含该属性的内容)

    @JsonManagedReference标注的属性则会被序列化。在序列化时,@JsonBackReference的作用相当于@JsonIgnore,此时可以没有@JsonManagedReference。

    • 反序列化(deserialization)

    如果没有@JsonManagedReference,则不会自动注入@JsonBackReference标注的属性(被忽略的父或子)

    如果有@JsonManagedReference,则会自动注入自动注入@JsonBackReference标注的属性。

  3. @JsonIgnoreProperties("xxx")标注在属性或对应的get、set方法上,忽略被标对象的某个属性

  4. @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property=”id”)

  • generator:唯一标识的类型

  • Property 对象的唯一标识 ,无特殊需求的,一般都是对象的主键

​ jackson从2.0 增加注解@JsonIdentityInfo解决无限递归的问题,这种方法是,如果发现循环引用,在形成环的最后一步,会将被引用的对象置空,序列化后的结果可能会缺失一部分数据,导致数据不完整。如A->B->A , 最后返回的结果是中,A-B->null

jakson解决循环依赖总结

  • jackson提供的123方法,这种方式没有从根本上解决循环引用,只是避开了循环引用。
  • 第4种方法是将循环引用形成的环断开,来避免序列化失败,会导数据丢失

血案现场回顾

最近在做一个项目,项目中使用到了图形数据库,从图形数据库中查询数据时候,因为查询返回的图中存在在一个环,导致下列异常

1
2
Servlet.service() for servlet [springDispatcherServlet] in context with path [/Shop] threw exception [Request processing failed; 
nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError) (through reference chain:

看异常内容是因为环导致循环依赖,导致jackson在序列化的时候产生了无限递归。

本文解决方案

因为我遇到的问题是,在查询图形数据库时候存在环,父级子级为同一个对象,数据结构,类似于 GraphNode嵌套GraphNode这样的结构,而且这些结构是接口返回接口的重要数据,无法使用,上述的方法1,2,3,又不允许数据有丢失,所以4的方法也无法使用。

主要解决方法是在第4种方案的基础上,如果发生了A->B-A循环引用的时候,对返回结果整理成A->B->A1,A1是A对象的一个另一个实例,但是A1中保留了接口中必要的属性,引起循环引用的属性置为了空。

查询图时候不使用spring-data-neo4j框架,使用Cypher查询,对查询结果和关系,自己生成一个图结构,然后,对图结果进行遍历,找出图中存在的环,在环最后闭合的时候 ,生成一个新对象A1,被当前对象引用,避开了循环引用。

容易出现循环引用的场景

  • ​ 在一些表设计不合理的系统中存在多对多关系的场景,然后使用JPA、 hibernate去查询时
  • ​ 复杂的接口响应结果,对于一些接口响应结果不够精简,返回复杂的对象,也很容易出现
  • ​ 图形数据库查询,比如,使用spring-data-neo4j 查询结果中有存在环,导致数据返回前端时候序列化失败

解决方案总结

  • fastjson解决循环引用,使用的特殊标识代替循环引用对象

  • Jackson循环依赖的解决方案不能从根本上解决问题,因为JAVA本身就是一个存在引用的语言,

  • 考虑表设计上是否合理,是否存在问题

  • 考虑接口的响应结构是否合理

    比如A依赖B,B依赖A中, 实际场景中,A中是不是可以只保留B的主键,B中保留A的主键。来避开循环引用

延伸阅读

java中引用无时无刻不在,在spring框架实例化对象的时候 ,也会出现循环依赖使容器抛出BeanCurrentlyCreationException,导致容器启动失败

spring检测循环依赖

spring窗口将每一个正在创建的bean标识符放在一个“当前创建bean池“,对象在创建过程中一直存在 于这个池中,创建完毕从池中清除掉相应的标识符,如果发现要创建的对象已经在池里存在,则说明发生了循环依赖

spring中循环依赖包括,构造器循环依赖set方法循环依赖,对于构造器循环依赖是无法解决的,对于 set方法循环依赖,只能解决作用域为单例的循环依赖,通过提前暴露一个刚完成构造器注入,但未完成其它步骤单例工厂方法,让其它bean可以引用到

设计模式是一套被在实际使用中,总结出来的一些代码设计经验的总结,学习设计模式有助于理解框架的结构。成熟的框架通常使用了多种设计模式,如果你熟悉这些设计模式,毫无疑问,对于快速掌握框架的结构有很大的帮助。

主要功能

用来建造一个对象“产品”,主要是用来创建那些构造过程比较复杂并且内部构造顺过程是比较稳定的对象。能够将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表现(主要体现在对象内在不同)

UML类图结构

建造模式的UML的类图结构

装饰模式中的角色

  • 抽象建造者(Builder): 抽象接口,要以是抽象类也可以是接口,一般会有一些buildXX()方法,用于构造”产品”的各个部件,还会有一个方法build(),用于返回构造好的”产品”
  • 具体构造角色(ConcreteBuilder ):实现Builder接口,实现各个部件的具体构造
  • **产品角色(Product)**:被构造的复杂对象
  • 指挥者(Director): 控制复杂对象的建造过程,也用它来隔离建造过程与产品的关联,包含一个具体构造角色,通过Set方法或构造方法传入

对模式的理解

适用场景

  • 需要生产的产品有复杂的内部结构

  • 需要生产的产品对象的属性互相依赖

  • 在构造过程中会用到其它对象

    特点

  • 关注的是零件类型在装配过程中的顺序

  • 封装性,将一个复杂对象的构建过程与它的表示分离,隐藏构造的过程和细节,调用者只需要传入构造者,无需关心建造细节

  • 建造者独立,容易扩展

  • 便于控制细节风险,因为建造者独立,所以可以对建造过程逐步细化,而不对其它的模块产生任何影响

应用场景

  • Apache HttpClient包中的HttpClients
    

结尾

作者:BK

简书地址:https://www.jianshu.com/u/a5230c4f0b7a

鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

设计模式是一套被在实际使用中,总结出来的一些代码设计经验的总结,学习设计模式有助于理解框架的结构。成熟的框架通常使用了多种设计模式,如果你熟悉这些设计模式,毫无疑问,对于快速掌握框架的结构有很大的帮助。

主要功能

​ 如其名,主要功能就是装饰,就是够在不改变原类文件、和不使用继承的情况下,动态扩展一个对象的功能和职责。装饰模式是通过将真实的对象包裹起来,然后对其进行装饰,并一层一层的传递下去,逐层装饰,直到装饰完成。

简单的说就是,在不违背开放-封闭原则的情况下,动态为一个对象增加一系列功能和职责

UML类图结构

装饰模式的UML的类图结构

装饰模式中的角色

  • 抽象构件角色(Component): 一个抽象的接口,用来规范被装饰对象。
  • 具体构造角色(ConcreteComponent ):业务场景比较简单,Component和ConcreteComponent的角色可以合并成一个
  • **装饰角色(Decoretor)**:持有一个抽象构件角色的引用,并通过构造方法或者set方法,对其赋值
  • 具体的装饰角色(ConcreteDecoretor): 业务场景比较简单时,ConcreteDecoretor和Decoretor两个的角色可以合并为一个

对模式的理解

装饰角色持有一个被装饰对象的引用,在转发请求前后增加附加的功能,实现对被装饰对象功能的扩展,并将当前装饰角色再依次传递给下一个具体的装饰角色,完成一系列的装饰,达到最终的效果

关键字

动态扩展* 、 *依次传递

  • 因为需要对”你”进行**动态扩展,不能使用继承(继承是属于静态行为,无法做到动态改变,同时继承会违反开闭原则),所以装饰角色需要持有一个被装饰对象的引用,可以通过set方法构造方法对其初始化和对象的传递**。

  • 装饰角色为什么需要实现和被装饰对象相同的接口,因为需要**依次传递**到不同的具体装饰角色中,方法参数类型必须一致

    特点

  • 装饰模式的对装饰顺序敏感

  • 装饰模式构造过程相比于建造者模式是不稳定的

  • 可以分离对象的核心职责与装饰功能

  • 更容易利用功能,把复杂的功能分散到每一个装饰器中,有利于利用利用,同样也会产生很多细粒度的对象

应用场景

  • ​ 装饰模式在JAVA中最典型的应用应该就是I/O流了
  • ​ 通过装饰模式实现消息的加密解密
  • ​ 通过装饰模式实现类似AOP的功能
  • ​ 其它场景

结尾

作者:BK

简书地址:https://www.jianshu.com/u/a5230c4f0b7a

鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

什么是可见性问题

在多线程环境下,一个线程对某个共享变量更新之后,其它线程访问该变量的线程,是否可以立刻读取到这个变量的更新结果,或者说,线程A对共享变量的修改,是否对线程B可见。这就是线程安全问题的另一个表现形式,可见性。

为什么出现这样的问题

线程是运行在处理器上,现在的计算机大多数都是多核的,计算机有主内存(RAM)、每个处理器有自己的写缓冲器(Store Buffer)、高速缓存,由于读取速度不匹配,所以处理器并不是直接与主内存打交道,内存的读写操作都是通过寄存器、高速缓存、写缓冲器、无效化队列等部件执行内存读写的。这些部件相当于主内存数据的副本,副本存储的份数越多,数据一致性越难保证。多个处理器读写数据也是优先读写当前处理器的副本数据,这就可能导致多个副本之间数据不一致。

既然有这样的问题,处理器是如何屏蔽的呢,这就引入了缓存一致性协议,也就是俗称的缓存同步。缓存同步使得一个处理器上可以读取到另一个处理器对共享变量所做的更新。在这里缓存一致性读者可以下来看一下MESI协议。

所以,缓存同步的是可以保障可见性的。

对变量的更新缓存的层级关系如下:
写缓冲器 -> 高速缓存 -> 主内存

下面我们看两个定义

  • 处理器对共享变量所做的更新操作,从写缓冲器中写入处理器的高速缓存或者主内存中,这个动作被称为冲刷处理器缓存
  • 处理器读取共享变量时,如果其它处理器更新了该值,那么处理器要从其它处理器的高速缓存或者主内存中进行同步相应的变量值到本缓存,这个动作被称为刷处理器缓存
  • 对于上面两个定义,我们提取一下关键字:

    **共享变量**     **更新(写)**      **读取(读)** 

看到这里,我们其实主要提到的关键字是共享变量,主要的操作是读写,也就是说对于共享变量的读写才会有可见性问题。

处理器会在写共享变量的时候冲刷处理器,读取共享变量的时候刷新处理器缓存,保证对变量的更新已经同步到高速缓存或者主内存,然后通过MESI协议,保证共享变量在多线程的可见性。

从这个角度来分析,只要我们告诉处理器,谁是共享变量,处理器就可以保证该变量的可见性了

 但是遗憾的是,JAVA中的变量都是JVM堆中存储的,无法告诉处理器哪个变量是共享变量。但是既然已经知道了处理器是怎么保证可见性的, 那么我们是不是可以定义一个关键字,对于这个关键字做下面这样的处理,就可以保证可见性呢

读的时候去刷处理器缓存,写的时候去冲刷处理器缓存

java中就是这样处理的,在java中,有一个关键字__volatile__大家一定都知道,怎么去理解它

  • 被volatile标识的变量,其实就是告诉处理器,该变量是共享变量,需要保证该变量的可见性
  • 另一方面,被volatile修饰的变量,可以告诉JIT编译器该变量可能会被共享,优化时悠着点儿。

从CPU指令角度来看volatile关键字的实现 ,其实是使用到的内存屏障,通过内存屏障保证在共享变量写的时候冲刷处理器共享变量读的时候刷新处理器缓存。volatile关键字的内存屏障具体是如何实现的, 后续会有文章更新,希望大家继续关注我。

到这里,希望看到本文章的读者是,可以从另一个角度去认识JAVA中的volatile关键字。

##结尾再废话两句
在说起可见性的时候,我们说到了多线程,说到多线程我们一定会与多个处理器想到一起,其实并非在多个处理器下才会出现可见性问题, 在单核处理器也会出现可见性问题

单个处理器,在线程切换的时候 ,当前线程对共享变量的修改会作为线程的上下文保存起来,这就可能导致其它处理器无法看到该线程对共享变量的更新(无法看到该变量的相对新值)。

下面可能可能会写一篇关于JAVA中有序性的文章,请大家继续关注 

希望大家可以继续关注我,一起成长,记录下成长路上的点点滴滴。

0%