java知识总结大全
2021-07-07 16:01:09 0 举报
AI智能生成
Java知识整理分解
作者其他创作
大纲/内容
缓存
类别
<b>静态缓存</b>常指的是前端静态页面,html 啊,js等等,常放在静态服务器上,还能通过 CDN 来缩减响应的时间,提高用户访问速度。
<b>分布式缓存</b>常指的是利用 Redis 、Memcached 等分布式缓存中间件来存放一些较为常用的数据,多个应用共享缓存,不仅可以提高访问速率,也算上在高并发下起到保护脆弱的数据库作用,算是高并发利器了!
<b>本地缓存</b>常指的是应用在同一个进程中的缓存组件,交互之间不会有网络开销,当你的项目还用不上分布式缓存,就存一些简单的变量时候可以用本地缓存来解决。最简单的 HashMap 就能作为本地缓存,或者Ehcache、Guava Cache等。
读写策略
并发更新导致的数据覆盖问题
其实不论是先删除再更新,还是先更新再删除,只是后删除出错的概率比较低!一般为了解决最终一致性问题都会设置过期时间,避免脏数据一直存在。
缓存更新
先更新DB,然后删除缓存
删除缓存而不是更新缓存,原因是懒加载的思想,更新缓存代价可能极高,尤其是在读少写多的场景
先删缓存,然后更新DB
并发会导致未更新DB时,其他线程读取旧数据填充缓存,导致不一致
缓存击穿
key 在某个时间突然失效了,那是不是就意味着大量的请求就无法在缓存中获取数据了,而是去请求数据库了,这样很有可能导致数据库被击垮。这就是缓存击穿。
不设置过期时间、互斥锁更新
缓存穿透
缓存穿透意思就是某个不存在的key一直被访问,结果发现数据库中也没有这样的数据,最终导致访问该key的所有请求都直接请求到数据库了。
布隆过滤
缓存雪崩
在某个时间节点,大量的 key 失效,导致大量的请求从缓存中获取不到数据而去请求数据库。
随机离散过期时间
redis
数据类型
string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)
redis过期策略
1、设置过期时间<br>expire key time(以秒为单位)--这是最常用的方式<br><br>setex(String key, int seconds, String value)--字符串独有的方式
三种过期策略
定时删除<br><br>含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除<br><br>优点:保证内存被尽快释放<br><br><br>缺点:<br><br>若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key<br><br>定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重<br><br>没人用<br><br><br>惰性删除<br><br>含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。<br><br>优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)<br><br>缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)<br><br><br>定期删除<br><br>含义:每隔一段时间执行一次删除过期key操作<br><br>优点:<br><br>通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点<br><br>定期删除过期key--处理"惰性删除"的缺点<br><br>缺点<br><br>在内存友好方面,不如"定时删除"<br><br>在CPU时间友好方面,不如"惰性删除"<br><br>难点<br><br>合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)
Redis采用的过期策略
惰性删除+定期删除<br><br><b>惰性删除流程</b><br><br>在进行get或setnx等操作时,先检查key是否过期,<br><br>若过期,删除key,然后执行相应操作;<br><br>若没过期,直接执行相应操作<br><br><b>定期删除流程</b>(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)<br><br>遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)<br><br>检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)<br><br>如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历<br><br>随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key<br><br>判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
RDB对过期key的处理
过期key对RDB没有任何影响<br><br><br><br>从内存数据库持久化数据到RDB文件<br><br>持久化key之前,会检查是否过期,过期的key不进入RDB文件<br><br>从RDB文件恢复数据到内存数据库<br><br>数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
AOF对过期key的处理
过期key对AOF没有任何影响<br><br>从内存数据库持久化数据到AOF文件:<br><br>当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)<br><br>当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)<br><br>AOF重写<br><br>重写时,会先判断key是否过期,已过期的key不会重写到aof文件
redis命令
访问官网:http://try.redis.io/<br>有个练手的平台,不会玩,用help指令,无论是git、linux等各种设计到指令的要学会用help命令,怎么用自己百度,<b>redis这个怎么用,点开子主题</b>
如果嫌难得操作,可以访问中文文档地址:http://redisdoc.com/<br>但是你这种不愿动手的态度不适合写代码,自己反思下,不是谁都会迁就你的
list底层是双向链表(当数据量比较小的时候,数据结构是压缩链表,而当数据量比较多的时候就成为了快速链表),<br>zset是有序集合,使用跳表来实现,跳表实现见左边图
事务
关系型数据库具有ACID
<b>原子性(Atomicity)</b><br>原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。<br><b>一致性(Consistency)</b><br>事务前后数据的完整性必须保持一致。<br><b>隔离性(Isolation)</b><br>事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。<br><b>持久性(Durability)</b><br>持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响<br><br>
Redis 能保证A(原子性)和 I(隔离性),D(持久性)看是否有配置 RDB或者 AOF 持久化操作,但无法保证一致性,因为 Redis 事务不支持回滚。<br><br>可以简单理解为 Redis 中的事务只是比 Pipeline 多了个原子性操作,也就是不会被其他命令给分割<br>
主从复制
实现原理:准备阶段-数据同步阶段-命令传播阶段
复制命令
<b>SYNC 命令是一个非常耗费资源的操作</b><br><br>每次执行 SYNC 命令,主从服务器需要执行如下动作:<br><br><ul><li>主服务器 需要执行 BGSAVE 命令来生成 RDB 文件,这个生成操作会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源;</li><li>主服务器 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 消耗 主服务器 大量的网络资源 (带宽和流量),并对主服务器响应命令请求的时间产生影响;<br></li><li>接收到 RDB 文件的 从服务器 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 会因为阻塞而没办法处理命令请求;<br></li></ul><br>特别是当出现 断线重复制 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 SYNC 命令,显然是不合理的。<br><br><b>PSYNC 命令的引入</b><br><br>所以在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具有两种模式:<br><br><ul><li>全量复制: 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作;</li><li>部分复制: 用于网络中断等情况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。需要注意 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制;</li></ul><br>部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。
部署架构
哨兵
架构图
组成
<ul><li>哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;<br><br></li><li>数据节点: 主节点和从节点都是数据节点</li></ul>
功能
<ul><li><b>监控(Monitoring)</b>: 哨兵会不断地检查主节点和从节点是否运作正常。<br><br></li><li><b>自动故障转移(Automatic failover)</b>: 当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。<br><br></li><li><b>配置提供者(Configuration provider)</b>: 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。<br><br></li><li><b>通知(Notification)</b>: 哨兵可以将故障转移的结果发送给客户端。<br><br>其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。<br></li></ul>
故障转移
<ul><li>对于主从节点: 主要是 slaveof 配置的变化,新的主节点没有了 slaveof 配置,其从节点则 slaveof 新的主节点。<br><br></li><li>对于哨兵节点: 除了主从节点信息的变化,纪元(epoch) (记录当前集群状态的参数) 也会变化,纪元相关的参数都 +1 了。</li></ul>
客户端原理
Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向 Jedis 提供哨兵节点集合和 masterName ,构造 JedisSentinelPool 对象,然后便可以像使用普通 Redis 连接池一样来使用了:通过 pool.getResource() 获取连接,执行具体的命令。<br><br>在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 JedisSentinelPool 的构造器中,进行了相关的工作;主要包括以下两点:<br><br><ol><li>遍历哨兵节点,获取主节点信息: 遍历哨兵节点,通过其中一个哨兵节点 + masterName 获得主节点的信息;该功能是通过调用哨兵节点的 sentinel get-master-addr-by-name 命令实现;<br><br></li><li>增加对哨兵的监听: 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 发布订阅 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。</li></ol>
选举
故障转移操作的第一步 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 slaveof no one 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢?
<ol><li>在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰。<br><br></li><li>在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。<br><br></li><li>在 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。</li></ol>
集群
架构图
slot槽
Redis 集群中内置了 16384 个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 集群的配置信息,当客户端具体对某一个 key 值进行操作时,会计算出它的一个 Hash 值,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量 大致均等 的将哈希槽映射到不同的节点。<br><br>再结合集群的配置信息就能够知道这个 key 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据:
集群作用
<ol><li><b>数据分区</b>: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……<br><br></li><li><b>高可用</b>: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。</li></ol>
数据分区方案简析
哈希值 % 节点数
哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。<br><br>不过该方案最大的问题是,<b>当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要 重新计算映射关系,引发大规模数据迁移。</b>
一致性哈希分区
一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围是 [0 - 232 - 1],对于每一个数据,根据 key 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器
与哈希取余分区相比,一致性哈希分区将 增减节点的影响限制在相邻节点。以上图为例,如果在 node1 和 node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。<br><br>一致性哈希分区的主要问题在于,当 节点数量较少 时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉 node2,node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。
带有虚拟节点的一致性哈希分区
该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。<br><br>在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦 了 数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);<br><br>槽 0-3 位于 node1;4-7 位于 node2;以此类推....<br><br>如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。<br>
节点通信机制简析
哨兵
节点分为 数据节点 和 哨兵节点:前者存储数据,后者实现额外的控制功能
集群
没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个 TCP 端口:<br><br><ul style=""><li style="">普通端口: 即我们在前面指定的端口 (7000等)。普通端口主要用于为客户端提供服务 (与单机节点类似);但在节点间数据迁移时也会使用。<br><br></li><li style="">集群端口: 端口号是普通端口 + 10000 (10000是固定值,无法改变),如 7000 节点的集群端口为 17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。</li></ul>
Gossip 协议
节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。<br><br><ul><li>广播是指向集群内所有节点发送消息。优点 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。<br><br></li><li>Gossip 协议的特点是:<b>在节点数量有限的网络中,每个节点都 “随机” 的与部分节点通信</b> (并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 优点 有负载 (比广播) 低、去中心化、容错性高 (因为通信有冗余) 等;缺点 主要是集群的收敛速度慢。</li></ul>
消息类型
集群中的节点采用 固定频率(每秒10次) 的 定时任务 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。<br><br>节点间发送的消息主要分为 5 种:<b>meet 消息、ping 消息、pong 消息、fail 消息、publish 消息。</b>不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的:
<b>MEET 消息</b>: 在节点握手阶段,当节点收到客户端的 CLUSTER MEET 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
<b>PING 消息</b>: 集群里每个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。PING 消息的内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:<br><br>(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;<br><br>(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的所有节点,防止这些节点长时间未更新。
<b>PONG消息</b>: PONG 消息封装了自身状态数据。可以分为两种:第一种 是在接到 MEET/PING 消息后回复的 PONG 消息;第二种 是指节点向集群广播 PONG 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
<b>FAIL 消息</b>: 当一个主节点判断另一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
<b>PUBLISH 消息</b>: 节点收到 PUBLISH 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。
持久化
RDB:RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。<br><br>优点:<br><br>1 适合大规模的数据恢复。<br><br>2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。<br><br>缺点:<br><br>1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。<br><br>2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。<br><br>所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。<br><br>--------------------------------------------------------------------------------<br><br>AOF:Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。<br><br>优点:数据的完整性和一致性更高<br><br>缺点:因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢。
<ul><li>Redis 默认开启RDB持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。<br><br></li><li>RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。<br><br></li><li>Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。<br><br></li><li>AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。<br><br></li><li>Redis 针对 AOF文件大的问题,提供重写的瘦身机制。<br><br></li><li>若只打算用Redis 做缓存,可以关闭持久化。<br><br></li><li>若打算使用Redis 的持久化。建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。</li></ul>
混合持久化
将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小,于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
Redis 数据备份与恢复
分布式锁
SET NX
REDISSON
重入性
LUA原子性
https://mp.weixin.qq.com/s/HoSdVPGIr76FoiQ2B3Rv9A
REDLOCK
内存回收策略
<ul><li>noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。</li><li>allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。</li><li>allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。</li><li>volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。</li><li>volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。</li><li>volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。</li></ul>
spring全家桶
springBoot
常见注解
JavaConfig
基于xml
<bean id="bookService" class="cn.moondev.service.BookServiceImpl"><br> <property name="dependencyService" ref="dependencyService"/><br></bean><br><br><bean id="otherService" class="cn.moondev.service.OtherServiceImpl"><br> <property name="dependencyService" ref="dependencyService"/><br></bean><br><br><bean id="dependencyService" class="DependencyServiceImpl"/>
基于注解
@Configuration<br>public class MoonBookConfiguration {<br><br> // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可<br><br> // 这里直接调用dependencyService()<br>@Bean<br>public BookService bookService() {<br>return new BookServiceImpl(dependencyService());<br><br> }<br><br>@Bean<br>public OtherService otherService() {<br>return new OtherServiceImpl(dependencyService());<br>}<br><br>@Bean<br>public DependencyService dependencyService() {<br>return new DependencyServiceImpl();<br>}<br><br>}
@ComponentScan
@ComponentScan注解对应XML配置形式中的 <context:component-scan>元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过 basePackages等属性来指定 @ComponentScan自动扫描的范围,如果不指定,默认从声明 @ComponentScan所在类的 package进行扫描。正因为如此,SpringBoot的启动类都默认在 src/main/java下。
@Import
@Import注解用于导入配置类<br>现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助 @Import即可:<br>
@Conditional
@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解标识的类上面,或者由 @Bean标记的方法上。如果一个 @Configuration类标记了 @Conditional,则该类中所有标识了 @Bean的方法和 @Import注解导入的相关类将遵从这些条件。<br><br>在Spring里可以很方便的编写你自己的条件类,所要做的就是实现 Condition接口,并覆盖它的 matches()方法。举个例子,下面的简单条件类表示只有在 Classpath里存在 JdbcTemplate类时才生效:<br>
public class JdbcTemplateCondition implements Condition {<br><br> @Override<br>public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {<br>try {<br>conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");<br>return true;<br>} catch (ClassNotFoundException e) {<br>e.printStackTrace();<br>}<br>return false;<br>}<br>}<br><br>@Conditional(JdbcTemplateCondition.class)<br>@Service<br>public MyService service() {<br>......<br>}<br>
@ConfigurationProperties与@EnableConfigurationProperties
当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,比如下面配置数据源的代码。<br><br>使用 @Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。<br><br>@EnableConfigurationProperties注解表示对 @ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加 @Component注解中。<br>
@EnableAutoConfiguration
注解表示开启Spring Boot自动配置功能,Spring Boot会根据应用的依赖、自定义的bean、classpath下有没有某个类 等等因素来猜测你需要的bean,然后注册到IOC容器中。<br>该注解上有@Import(EnableAutoConfigurationImportSelector.class)<br>@Import注解用于导入类,并将这个类作为一个bean的定义注册到容器中,这里它将把EnableAutoConfigurationImportSelector作为bean注入到容器中,而这个类会将所有符合条件的@Configuration配置都加载到容器中<br>这个类会扫描所有的jar包,将所有符合条件的@Configuration配置类注入的容器中,何为符合条件,看看 META-INF/spring.factories的文件内容:<br>// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories<br><br>// 配置的key = EnableAutoConfiguration,与代码中一致<br><br>org.springframework.boot.autoconfigure.EnableAutoConfiguration=\<br><br>org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\<br><br>org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\<br><br>org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\<br><br>.....<br><br> @EnableAutoConfiguration中导入了EnableAutoConfigurationImportSelector类,而这个类的 selectImports()通过SpringFactoriesLoader得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。<br><br>整个流程很清晰,但漏了一个大问题: EnableAutoConfigurationImportSelector.selectImports()是何时执行的?其实这个方法会在容器启动过程中执行: AbstractApplicationContext.refresh()<br>
SpringFactoriesLoader
loadFactoryNames
从 CLASSPATH下的每个Jar包中搜寻所有 META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。<br>执行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)后,得到对应的一组 @Configuration类,<br>我们就可以通过反射实例化这些类然后注入到IOC容器中,最后容器里就有了一系列标注了 @Configuration的JavaConfig形式的配置类。<br>这就是 SpringFactoriesLoader,它本质上属于Spring框架私有的一种扩展方案<br>
启动的秘密
1、SpringApplication初始化
SpringBoot整个启动流程分为两个步骤:初始化一个SpringApplication对象、执行该对象的run方法。看下SpringApplication的初始化流程,SpringApplication的构造方法中调用initialize(Object[] sources)方法,其代码如下:<br>private void initialize(Object[] sources) {<br><br> if (sources != null && sources.length > 0) {<br><br> this.sources.addAll(Arrays.asList(sources));<br><br> }<br><br> // 判断是否是Web项目<br><br> this.webEnvironment = deduceWebEnvironment();<br><br> setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));<br><br> setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));<br><br> // 找到入口类<br><br> this.mainApplicationClass = deduceMainApplicationClass();<br><br>}<br>初始化流程中最重要的就是通过SpringFactoriesLoader找到 spring.factories文件中配置的ApplicationContextInitializer和 ApplicationListener两个接口的实现类名称,以便后期构造相应的实例。 <br><br>ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。<br><br>实现一个ApplicationContextInitializer非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个ApplicationContextInitializer,即便是Spring Boot框架,它默认也只是注册了两个实现,毕竟Spring的容器已经非常成熟和稳定,你没有必要来改变它。<br><br>而 ApplicationListener的目的就没什么好说的了,它是Spring框架对Java事件监听机制的一种框架实现,具体内容在前文Spring事件监听机制这个小节有详细讲解。这里主要说说,如果你想为Spring Boot应用添加监听器,该如何实现?<br><br>Spring Boot提供两种方式来添加自定义监听器:<br><br>通过 SpringApplication.addListeners(ApplicationListener<?>...listeners)或者 SpringApplication.setListeners(Collection<?extendsApplicationListener<?>>listeners)两个方法来添加一个或者多个自定义监听器<br><br>既然SpringApplication的初始化流程中已经从 spring.factories中获取到 ApplicationListener的实现类,那么我们直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:<br>org.springframework.context.ApplicationListener=\cn.moondev.listeners.xxxxListener\<br>
2、 Spring Boot启动流程
Spring Boot应用的整个启动流程都封装在SpringApplication.run方法中,其整个流程真的是太长太长了,但本质上就是在Spring容器启动的基础上做了大量的扩展,按照这个思路来看看源码: <br><br>END:<br>这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。<br><br>整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。<br>
① 通过SpringFactoriesLoader查找并加载所有的 SpringApplicationRunListeners,通过调用starting()方法通知所有的SpringApplicationRunListeners:应用开始启动了。<br><br>SpringApplicationRunListeners其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication加载了一系列ApplicationListener吗?这个启动流程中没有发现有发布事件的代码,其实都已经在SpringApplicationRunListeners这儿实现了。<br><br>简单的分析一下其实现流程,首先看下SpringApplicationRunListener的源码:<br>public interface SpringApplicationRunListener {<br><br>// 运行run方法时立即调用此方法,可以用户非常早期的初始化工作<br>void starting();<br><br> // Environment准备好后,并且ApplicationContext创建之前调用<br>void environmentPrepared(ConfigurableEnvironment environment);<br><br>// ApplicationContext创建好后立即调用<br>void contextPrepared(ConfigurableApplicationContext context);<br><br> // ApplicationContext加载完成,在refresh之前调用<br>void contextLoaded(ConfigurableApplicationContext context);<br><br>// 当run方法结束之前调用<br>void finished(ConfigurableApplicationContext context, Throwable exception);<br><br>}<br>SpringApplicationRunListener只有一个实现类: EventPublishingRunListener。①处的代码只会获取到一个EventPublishingRunListener的实例,我们来看看starting()方法的内容:<br>public void starting() {<br>// 发布一个ApplicationStartedEvent<br>this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));<br>}<br><br>顺着这个逻辑,你可以在②处的 prepareEnvironment()方法的源码中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二个方法,那不出你所料, environmentPrepared()又发布了另外一个事件 ApplicationEnvironmentPreparedEvent。接下来会发生什么,就不用我多说了吧。<br>
② 创建并配置当前应用将要使用的Environment,Environment用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当Environment准备好后,在整个应用的任何时候,都可以从Environment中获取资源。<br>总结起来,②处的两句代码,主要完成以下几件事:<br><br><ul><li>判断Environment是否存在,不存在就创建(如果是web项目就创建 StandardServletEnvironment,否则创建 StandardEnvironment)<br></li><li>配置Environment:配置profile以及properties<br></li><li>调用SpringApplicationRunListener的 environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好</li></ul>
③、SpringBoot应用在启动时自定义输出
④、根据是否是web项目,来创建不同的ApplicationContext容器。
⑤、创建一系列 FailureAnalyzer,创建流程依然是通过SpringFactoriesLoader获取到所有实现FailureAnalyzer接口的class,然后在创建对应的实例。FailureAnalyzer用于分析故障并提供相关诊断信息。
⑥、初始化ApplicationContext,主要完成以下工作:<br><ul><li>将准备好的Environment设置给ApplicationContext<br></li><li>遍历调用所有的ApplicationContextInitializer的 initialize()方法来对已经创建好的ApplicationContext进行进一步的处理<br></li><li>调用SpringApplicationRunListener的 contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕<br></li><li>将所有的bean加载到容器中<br></li><li>调用SpringApplicationRunListener的 contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕</li></ul>
⑦、调用ApplicationContext的 refresh()方法,完成IoC容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码:<br>// 摘自refresh()方法中一句代码<br>invokeBeanFactoryPostProcessors(beanFactory);<br>看看这个方法的实现:<br>protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {<br> PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());<br> ......<br>}<br>获取到所有的 BeanFactoryPostProcessor来对容器做一些额外的操作。BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作。这里的getBeanFactoryPostProcessors()方法可以获取到3个Processor:<br>ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor<br>SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor<br>ConfigFileApplicationListener$PropertySourceOrderingPostProcessor<br><br>不是有那么多BeanFactoryPostProcessor的实现类,为什么这儿只有这3个?因为在初始化流程获取到的各种ApplicationContextInitializer和ApplicationListener中,只有上文3个做了类似于如下操作:<br>public void initialize(ConfigurableApplicationContext context) {<br> context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));<br>}<br><br>然后你就可以进入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,这个方法除了会遍历上面的3个BeanFactoryPostProcessor处理外,还会获取类型为 BeanDefinitionRegistryPostProcessor的bean: org.springframework.context.annotation.internalConfigurationAnnotationProcessor,对应的Class为 ConfigurationClassPostProcessor。<br><br>ConfigurationClassPostProcessor用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理 @import注解的时候,就会调用<自动配置>这一小节中的 EnableAutoConfigurationImportSelector.selectImports()来完成自动配置功能。<br><br>⑧、查找当前context中是否注册有CommandLineRunner和ApplicationRunner,如果有则遍历执行它们。<br><br>⑨、执行所有SpringApplicationRunListener的finished()方法。<br>
3、tomcat在spring boot中如何启动
https://mp.weixin.qq.com/s/FrtEO6Z0icPqvt6fjY-pwQ
排除tomcat,打出war
<dependency><br> <groupId>org.springframework.boot</groupId><br> <artifactId>spring-boot-starter-web</artifactId><br> <!-- 移除嵌入式tomcat插件 --><br> <exclusions><br> <exclusion><br> <groupId>org.springframework.boot</groupId><br> <artifactId>spring-boot-starter-tomcat</artifactId><br> </exclusion><br> </exclusions><br></dependency><br><!--添加servlet-api依赖---><br><dependency><br> <groupId>javax.servlet</groupId><br> <artifactId>javax.servlet-api</artifactId><br> <version>3.1.0</version><br> <scope>provided</scope><br></dependency>
@SpringBootApplication<br>public class MySpringbootTomcatStarter extends SpringBootServletInitializer {<br> public static void main(String[] args) {<br> Long time=System.currentTimeMillis();<br> SpringApplication.run(MySpringbootTomcatStarter.class);<br> System.out.println("===应用启动耗时:"+(System.currentTimeMillis()-time)+"===");<br> }<br><br> @Override<br> protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {<br> return builder.sources(this.getClass());<br> }<br>}
springCore
IOC
BeanDefinition
容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存bean对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。<br> 不管是是通过xml配置文件的<Bean>标签,还是通过注解配置的@Bean,还是@Compontent标注的类,还是扫描得到的类,它最终都会被解析成一个BeanDefinition对象,最后我们的Bean工厂就会根据这份Bean的定义信息,对bean进行实例化、初始化等等操作。<br><br>
BeanDefinitionRegistry
抽象出 bean 的注册逻辑
BeanFactory
抽象出了 bean 的管理逻辑,BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作
三者关系
工作流程
①、容器启动阶段<br>容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如: BeanDefinitionReader,BeanDefinitionReader 会对加载的 ConfigurationMetaData进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。<br>来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系:<br><br>// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例<br><br>BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory(); <br><br>// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件<br><br>XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry);<br><br>// 加载配置文件<br><br>beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml");<br><br>// 从容器中获取bean实例<br><br>BeanFactory container = (BeanFactory)beanRegistry;<br><br>Business business = (Business)container.getBean("beanName");<br>
②、Bean的实例化阶段<br>经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。<br><br>BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器: ApplicationContext,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。
BeanFactory 和 ApplicationContext关系
BeanFactory:是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;<br><br>ApplicationContext:(<b>这点可以看下代码或者看UML图,applicationcontext继承了多个类,这多个类具有各自功能,所以也就为ac提供了下面几个点</b>)<br>应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;<br>1) 国际化(MessageSource)<br>2) 访问资源,如URL和文件(ResourceLoader)<br>3) 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层 <br>4) 消息发送、响应机制(ApplicationEventPublisher)<br>5) AOP(拦截器)<br><br>两者装载bean的区别<br>BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;<br>ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化; <br>
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,<br> MessageSource, ApplicationEventPublisher, ResourcePatternResolver {<br><br> //标识当前context实例的id,最终会通过native方法来生成:System.identityHashCode<br> String getId();<br><br> //返回该context所属的应用名称,默认为空字符串,在web应用中返回的是servlet的contextpath <br> String getApplicationName();<br><br> //返回当前context的名称<br> String getDisplayName();<br><br> //返回context第一次被加载的时间<br> long getStartupDate();<br><br> //返回该context的parent<br> ApplicationContext getParent();<br><br> //返回具有自动装配能力的beanFactory,默认返回的就是初始化时实例化的beanFactory<br> AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;<br>}<br><br>
Bean的生命周期和钩子函数、spring扩展机制
生命周期图
public interface BeanPostProcessor {<br><br> // 前置处理<br><br> Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;<br><br> // 后置处理<br><br> Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;<br><br>}
postProcessBeforeInitialization()方法与 postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。<br><br>注解、AOP等功能的实现均大量使用了 BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作,想想是不是非常的简单?<br><br>再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的 ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。<br><br>当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。看看代码吧,是不是很简单:<br><br>// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor<br><br>// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法<br><br>private void invokeAwareInterfaces(Object bean) {<br><br> if (bean instanceof EnvironmentAware) {<br><br> ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());<br><br> }<br><br> if (bean instanceof ApplicationContextAware) {<br><br> ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);<br><br> }<br><br> // ......<br><br>}<br>
Bean的作用域
Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。
<ul><li>singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。<br><br></li><li>prototype:bean被定义为在每次注入时都会创建一个新的对象。<br><br></li><li>request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。<br><br></li><li>session:bean被定义为在一个session的生命周期内创建一个单例对象。<br><br></li><li>application:bean被定义为在ServletContext的生命周期中复用一个单例对象。<br><br></li><li>websocket:bean被定义为在websocket的生命周期中复用一个单例对象。</li></ul>
循环依赖
什么是循环依赖?AB相互依赖,A中注入了自己
spring循环依赖的前置条件
1、出现循环依赖的Bean必须要是单例<br>2、依赖注入的方式不能全是构造器注入的方式(很多博客上说,只能解决setter方法的循环依赖,这是错误的)
获取 Bean 流程
获取bean流程图
<ol><li>流程从 getBean 方法开始,getBean 是个空壳方法,所有逻辑直接到 doGetBean 方法中<br><br></li><li>transformedBeanName 将 name 转换为真正的 beanName(name 可能是 FactoryBean 以 & 字符开头或者有别名的情况,所以需要转化下)<br><br></li><li>然后通过 getSingleton(beanName) 方法尝试从缓存中查找是不是有该实例 sharedInstance(单例在 Spring 的同一容器只会被创建一次,后续再获取 bean,就直接从<font color="#c41230">缓存(这儿就开始引申出三级缓存)</font>获取即可)<br><br></li><li>如果有的话,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,所以再经 getObjectForBeanInstance 处理即可返回<br><br></li><li>当然 sharedInstance 也可能是 null,这时候就会执行创建 bean 的逻辑,将结果返回</li></ol>
三级缓存
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry<br>/** Cache of singleton objects: bean name --> bean instance */<br>private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);<br><br>/** Cache of singleton factories: bean name --> ObjectFactory */<br>private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);<br><br>/** Cache of early singleton objects: bean name --> bean instance */<br>private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
<ul><li>singletonObjects:完成初始化的单例对象的 cache,这里的 bean 经历过 实例化->属性填充->初始化 以及各种后置处理(一级缓存)</li><li>earlySingletonObjects:存放原始的 bean 对象(完成实例化但是尚未填充属性和初始化),仅仅能作为指针提前曝光,被其他 bean 所引用,用于解决循环依赖的 (二级缓存)</li><li>singletonFactories:在 bean 实例化完之后,属性填充以及初始化之前,如果允许提前曝光,Spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到 singletonFactories(三级缓存)</li></ul>
三级缓存查找,最开始初始化A肯定缓存是没有的
三级缓存没有则去创建
解决循环依赖
逻辑图
<ul><li>Spring 创建 bean 主要分为两个步骤,创建原始 bean 对象,接着去填充对象属性和初始化</li><li>每次创建 bean 之前,我们都会从缓存中查下有没有该 bean,因为是单例,只能有一个</li><li>当我们创建 beanA 的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了 beanB,接着就又去创建 beanB,同样的流程,创建完 beanB 填充属性时又发现它依赖了 beanA,又是同样的流程,不同的是,这时候可以在三级缓存中查到刚放进去的原始对象 beanA,所以不需要继续创建,用它注入 beanB,完成 beanB 的创建</li><li>既然 beanB 创建好了,所以 beanA 就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成</li></ul>
doCreateBean
Q&A
<b>B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?</b><br><br>虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。<br>
<b>Spring 是如何解决的循环依赖?</b><br><br>Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects),二级缓存为提前曝光对象(earlySingletonObjects),三级缓存为提前曝光对象工厂(singletonFactories)。<br>假设A、B循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 A,这时候从缓存中查找到早期暴露的 A,没有 AOP 代理的话,直接将 A 的原始对象注入 B,完成 B 的初始化后,进行属性填充和初始化,这时候 B 完成后,就去完成剩下的 A 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 A,注入 B,走剩下的流程。<br>
<b>为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?</b><br><br>如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。<br>
<b>什么时候 bean 被放入 3 级缓存</b><br>doCreateBean里面,先执行createBeanInstance,实例化Bean;<br>然后判断是否提前暴露,代码:(mbd.isSingleton() && this.allowCircularReferences &&<br> isSingletonCurrentlyInCreation(beanName));<br>然后addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));这句加入;<br>之后才是填充属性populateBean(beanName, mbd, instanceWrapper);<br><br>//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
spring MVC
和spring的关系
Q & A
<b>为什么需要父子容器?</b><br><br>父子容器的主要作用应该是划分框架边界。有点单一职责的味道。在J2EE三层架构中,在service层我们一般使用spring框架来管理, 而在web层则有多种选择,如spring mvc、struts等。因此,通常对于web层我们会使用单独的配置文件。例如在上面的案例中,一开始我们使用spring-servlet.xml来配置web层,使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts,那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可,而applicationContext.xml不需要改变。<br><br><b>是否可以把所有类都通过Spring父容器来管理?<br><br></b>Spring的applicationContext.xml中配置全局扫描)<br><context:component-scan use-default-filters="false" base-package="cn.javajr"><br> <context:include-filter type="annotation" expression="org.springframework.stereotype.Service" /><br> <context:include-filter type="annotation" expression="org.springframework.stereotype.Component" /><br> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" /><br> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /><br> </context:component-scan><br><b><br></b>很显然这种方式是行不通的,这样会导致我们请求接口的时候产生404。因为在解析@ReqestMapping注解的过程中initHandlerMethods()函数只是对Spring MVC 容器中的bean进行处理的,并没有去查找父容器的bean, 因此不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。所以当请求过来时找不到处理的handler,导致404。<br><br><b>是否可以把我们所需的类都放入Spring-mvc子容器里面来管理(springmvc的spring-servlet.xml中配置全局扫描)?<br></b>这个是把包的扫描配置spring-servlet.xml中这个是可行的。为什么可行因为无非就是把所有的东西全部交给子容器来管理了,子容器执行了refresh方法,把在它的配置文件里面的东西全部加载管理起来来了。虽然可以这么做不过一般应该是不推荐这么去做的,一般人也不会这么干的。如果你的项目里有用到事物、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配置文件来,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物或者AOP不生效。<br>
Spring中设计模式的体现
简单工厂
实现方式
BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
实质
由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
实现原理
bean容器的启动阶段:<br><ul><li>读取bean的xml配置文件,将bean元素分别转换成一个BeanDefinition对象。</li><li>然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中,保存在它的一个ConcurrentHashMap中。</li><li>将BeanDefinition注册到了beanFactory之后,在这里Spring为我们提供了一个扩展的切口,允许我们通过实现接口BeanFactoryPostProcessor 在此处来插入我们定义的代码。</li></ul>典型的例子就是:PropertyPlaceholderConfigurer,我们一般在配置数据库的dataSource时使用到的占位符的值,就是它注入进去的。<br><br>容器中bean的实例化阶段:<br>实例化阶段主要是通过反射或者CGLIB对bean进行实例化,在这个阶段Spring又给我们暴露了很多的扩展点:<br><ul><li>各种的Aware接口,比如 BeanFactoryAware,对于实现了这些Aware接口的bean,在实例化bean时Spring会帮我们注入对应的BeanFactory的实例。</li><li>BeanPostProcessor接口,实现了BeanPostProcessor接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。</li><li>InitializingBean接口,实现了InitializingBean接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。</li><li>DisposableBean接口,实现了BeanPostProcessor接口的bean,在该bean死亡时Spring会帮我们调用接口中的方法。</li></ul>
设计意义
<b>松耦合</b>。可以将原来硬编码的依赖,通过Spring这个beanFactory这个工厂来注入依赖,也就是说原来只有依赖方和被依赖方,现在我们引入了第三方——spring这个beanFactory,由它来解决bean之间的依赖问题,达到了松耦合的效果.<br><br><b>bean的额外处理</b>。通过Spring接口的暴露,在实例化bean的阶段我们可以进行一些额外的处理,这些额外的处理只需要让bean实现对应的接口即可,那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。
单列模式
Spring依赖注入Bean实例默认是单例的。<br><br>Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。<br><br>分析getSingleton()方法
单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。<br><br>spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式
实现方式
SpringMVC中的适配器HandlerAdatper
实质
HandlerAdatper根据Handler规则执行不同的Handler。
实现原理
DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求,处理Handler。<br><br>HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。<br>
设计意义
HandlerAdatper使得Handler的扩展变得容易,只需要增加一个新的Handler和一个对应的HandlerAdapter即可。<br><br>因此Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
装饰器模式
实现方式
Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
实质
动态地给一个对象添加一些额外的职责。<br><br>就增加功能来说,Decorator模式相比生成子类更为灵活。
代理模式
实现方式
AOP底层,就是动态代理模式的实现。
动态代理
在内存中构建的,不需要手动编写代理类
静态代理
需要手工编写代理类,代理类引用被代理对象
实现原理
切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。<br><br>织入:把切面应用到目标对象并创建新的代理对象的过程。<br>
观察者模式
实现方式
spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现。
具体实现
事件机制的实现需要三个部分,事件源,事件,事件监听器<br><br>ApplicationEvent抽象类[事件]<br><br>继承自jdk的EventObject,所有的事件都需要继承ApplicationEvent,并且通过构造器参数source得到事件源.<br><br>该类的实现类ApplicationContextEvent表示ApplicaitonContext的容器事件.<br><br>ApplicationListener接口[事件监听器]<br><br>继承自jdk的EventListener,所有的监听器都要实现这个接口。<br><br>这个接口只有一个onApplicationEvent()方法,该方法接受一个ApplicationEvent或其子类对象作为参数,在方法体中,可以通过不同对Event类的判断来进行相应的处理。<br><br>当事件触发时所有的监听器都会收到消息。<br><br>ApplicationContext接口[事件源]<br><br>ApplicationContext是spring中的全局容器,翻译过来是”应用上下文”。<br><br>实现了ApplicationEventPublisher接口。<br>
职责
负责读取bean的配置文档,管理bean的加载,维护bean之间的依赖关系,可以说是负责bean的整个生命周期,再通俗一点就是我们平时所说的IOC容器。
策略模式
实现方式
Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
Resource 接口介绍
source 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。<br><br>Resource 接口主要提供了如下几个方法:<br><br><ul><li>getInputStream():定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。</li><li><br></li><li>exists():返回 Resource 所指向的资源是否存在。</li><li><br></li><li>isOpen():返回资源文件是否打开,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。</li><li><br></li><li>getDescription():返回资源的描述信息,通常用于资源处理出错时输出该信息,通常是全限定文件名或实际 URL。</li><li><br></li><li>getFile:返回资源对应的 File 对象。</li><li><br></li><li>getURL:返回资源对应的 URL 对象。</li></ul><br>最后两个方法通常无须使用,仅在通过简单方式访问无法实现时,Resource 提供传统的资源访问的功能。<br><br>Resource 接口本身没有提供访问任何底层资源的实现逻辑,<b>针对不同的底层资源,Spring 将会提供不同的 Resource 实现类,不同的实现类负责不同的资源访问逻辑</b>。<br><br><ul><li>Spring 为 Resource 接口提供了如下实现类:</li><li><br></li><li>UrlResource:访问网络资源的实现类。</li><li><br></li><li>ClassPathResource:访问类加载路径里资源的实现类。</li><li><br></li><li>FileSystemResource:访问文件系统里资源的实现类。</li><li><br></li><li>ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.</li><li><br></li><li>InputStreamResource:访问输入流资源的实现类。</li><li><br></li><li>ByteArrayResource:访问字节数组资源的实现类。</li></ul><br>这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
数据库
优化
技巧
1、比较运算符能用 “=”就不用“<>”,“=”增加了索引的使用几率。<br>2、明知只有一条查询结果,那请使用 “LIMIT 1”,“LIMIT 1”可以避免全表扫描,找到对应结果就不会再继续扫描了。<br>3、为列选择合适的数据类型,能用TINYINT就不用SMALLINT,能用SMALLINT就不用INT,道理你懂的,磁盘和内存消耗越小越好嘛。<br>4、将大的DELETE,UPDATE or INSERT 查询变成多个小查询,能写一个几十行、几百行的SQL语句是不是显得逼格很高?然而,为了达到更好的性能以及更好的数据控制,你可以将他们变成多个小查询。<br>5、使用UNION ALL 代替 UNION,如果结果集允许重复的话,因为 UNION ALL 不去重,效率高于 UNION。<br>6、为获得相同结果集的多次执行,请保持SQL语句前后一致,这样做的目的是为了充分利用查询缓冲。<br>7、尽量避免使用 “SELECT *”,如果不查询表中所有的列,尽量避免使用 SELECT *,因为它会进行全表扫描,不能有效利用索引,增大了数据库服务器的负担,以及它与应用程序客户端之间的网络IO开销。<br>8、WHERE 子句里面的列尽量被索引,只是“尽量”哦,并不是说所有的列。因地制宜,根据实际情况进行调整,因为有时索引太多也会降低性能。<br>9、JOIN 子句里面的列尽量被索引,体会下尽量二字的精髓<br>10、ORDER BY 的列尽量被索引<br>11、My sql EXPLAIN 检查索引使用情况以及扫描的行
实例
分页优化
select * from table where type = 2 and level = 9 order by id asc limit 190289,10;<br><br>方案:<br><b>延迟关联</b><br>先通过where条件提取出主键,在将该表与原数据表关联,通过主键id提取数据行,而不是通过原来的二级索引提取数据行<br>例如:select a.* from table a, (select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b where a.id = b.id<br><br><b>书签方式</b><br>书签方式说白了就是找到limit第一个参数对应的主键值,再根据这个主键值再去过滤并limit<br>例如:<br>select * from table where id > (select * from table where type = 2 and level = 9 order by id asc limit 190289, 1) limit 10;<br>
索引优化
建立<b>覆盖索引</b><br><br>select name from test where city='上海'<br><br>alter table test add index idx_city_name (city, name);<br>姓名加上索引,避免回表
避免在 where 查询条件中使用 != 或者 <> 操作符
SQL中,不等于操作符会导致查询引擎放弃索引索引,引起全表扫描,即使比较的字段上有索引<br><br>解决方法:通过把不等于操作符改成or,可以使用索引,避免全表扫描<br><br>例如,把column<>’aaa’,改成column>’aaa’ or column<’aaa’,就可以使用索引了
适当使用前缀索引
MySQL 是支持前缀索引的,也就是说我们可以定义字符串的一部分来作为索引<br><br>我们知道索引越长占用的磁盘空间就越大,那么在相同数据页中能放下的索引值也就越少,这就意味着搜索索引需要的查询时间也就越长,进而查询的效率就会降低,所以我们可以适当的选择使用前缀索引,以减少空间的占用和提高查询效率<br><br>比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引<br><br>alter table test add index index2(email(6));<br><br>使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本<br><br>需要注意的是,前缀索引也存在缺点,MySQL无法利用前缀索引做order by和group by 操作,也无法作为覆盖索引
小表驱动大表
我们要尽量使用小表驱动大表的方式进行查询,也就是如果 B 表的数据小于 A 表的数据,那执行的顺序就是先查 B 表再查 A 表,具体查询语句如下:<br><br>select name from A where id in (select id from B);
优化子查询
尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,同时对于返回结果集比较大的子查询,其对查询性能的影响更大
隐式类型转换导致所有失效
select * from test where skuId=123456<br><br>skuId这个字段上有索引,但是explain的结果却显示这条语句会全表扫描<br><br>原因在于skuId的字符类型是varchar(32),比较值却是整型,故需要做类型转换<br>
mysqlB+树存储
为什么选择B+树作为索引结构
<b>为什么不使用哈希结构?</b><br>我们知道哈希结构,类似k-v结构,也就是,key和value是一对一关系。它用于等值查询还可以,但是范围查询它是无能为力的哦。<br><br><b>为什么不使用二叉树呢?<br></b>如果二叉树只有右子树,将会特殊化为一个链表,相当于全表扫描。那么还要索引干嘛呀?因此,一般二叉树不适合作为索引结构<br><br><b>为什么不使用平衡二叉树呢?<br></b>平衡二叉树插入或者更新是,需要左旋右旋维持平衡,维护代价大<br>如果数量多的话,树的高度会很高。因为数据是存在磁盘的,以它作为索引结构,每次从磁盘读取一个节点,操作IO的次数就多啦。<br>
结构图
索引
类型
非聚簇索引
将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置<br>索引顺序与数据物理排列顺序无关<br>
聚簇索引
将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据<br>聚簇索引的顺序就是数据的物理存储顺序<br>
主键索引
即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;<br>ALTER TABLE 'table_name' ADD PRIMARY KEY pk_index('col');<br>
唯一索引
用来建立索引的列的值必须是唯一的,允许空值<br>ALTER TABLE 'table_name' ADD UNIQUE index_name('col');<br>
复合索引(组合索引)
用多个列组合构建的索引,这多个列中的值不允许有空值<br>ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');
单列索引
B+
最左前缀索引
查询条件包含在了组合索引中,比如存在组合索引(a,b),查询到满足 a 的记录后会直接在索引内部判断 b 是否满足,减少回表次数。同时,如果查询的列恰好包含在组合索引中,即为覆盖索引,无需回表。
索引失效常见原因
<ol><li>where 中使用 != 或 <> 或 or 或表达式或函数(左侧)</li><li>like 语句 % 开头<br></li><li>字符串未加’’<br></li><li>索引字段区分度过低,如性别<br></li><li>未匹配最左前缀<br></li></ol>
函数操作<br>
当在 查询 where = 左侧使用表达式或函数时,如字段 A 为字符串型且有索引, 有 where length(a) = 6查询,这时传递一个 6 到 A 的索引树,不难想象在树的第一层就迷路了。
隐式转换<br>
隐式类型转换和隐式字符编码转换也会导致这个问题。<br><br>隐式类型转换对于 JOOQ 这种框架来说一般倒不会出现。<br><br>隐式字符编码转换在连表查询时倒可能出现,即连表字段的类型相同但字符编码不同<br>
破坏了有序性
至于 Like 语句 % 开头、字符串未加 ’’ 原因基本一致,MySQL 认为对索引字段的操作可能会破坏索引有序性就机智的优化掉了。
怎么建立并用好索引
索引下推:性别字段不适合建索引,但确实存在查询场景怎么办?如果是多条件查询,可以建立联合索引利用该特性优化。
覆盖索引:也是联合索引,查询需要的信息在索引里已经包含了,就不会再回表了。
前缀索引:对于字符串,可以只在前 N 位添加索引,避免不必要的开支。假如的确需要如关键字查询,那交给更合适的如 ES 或许更好。
不要对索引字段做函数操作
对于确定的、写多读少的表或者频繁更新的字段都应该考虑索引的维护成本。
分库分表
IO瓶颈
第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。<br>第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。
CPU瓶颈
第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。<br>第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。
水平分库
概念
以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
结果
每个库的结构都一样;<br>每个库的数据都不一样,没有交集;<br>所有库的并集是全量数据;
场景
系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
分析
库多了,io和cpu的压力自然可以成倍缓解。
水平分表
概念
以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
结果
每个表的结构都一样;<br>每个表的数据都不一样,没有交集;<br>所有表的并集是全量数据;
场景
系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析
分析
表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。
垂直分库
概念
以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
结果
每个库的结构都不一样;<br>每个库的数据也不一样,没有交集;<br>所有库的并集是全量数据;
场景
系统绝对并发量上来了,并且可以抽象出单独的业务模块。
分析
到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。
垂直分表
概念
以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
结果
每个表的结构都不一样;<br>每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;<br>所有表的并集是全量数据;
场景
系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
分析
可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。<br><br>但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。<br>
分库分表工具
sharding-sphere:jar,前身是sharding-jdbc;<br><br>TDDL:jar,Taobao Distribute Data Layer;<br><br>Mycat:中间件。
分库分表后的分页查询
全局视野法
如果要获取第N页的数据(每页S条数据),则将每一个子库的前N页(offset 0,limit N*S)的所有数据都先查出来(有筛选条件或排序规则的话都包含),然后将各个子库的结果合并起来之后,再做查询下top S(可不用带上相同的筛选条件,但还要带上排序规则)即可得出最终结果,这种方式类似es分页的逻辑。<br><br>优点: 数据准确,可以跳页<br>缺点: 深度分页时的性能差,即随着分页参数增加,网络传输数据量越来越大,每个子表每次需要查询的数据越多,性能也越慢<br><br>
禁止跳页查询
如果要获取第N页的数据,第一页时,是和全局视野法一致,但第二页开始后,需要在每一个子库查询时,加上可以排除上一页的过滤条件(如按时间排序时,获取上一页的最大时间后,需要加上time > ${maxTime_lastPage}的条件;如果没有排序规则,由于是默认主键id的排序规则,也可加上 id > ${maxId_lastPage}的条件),然后再limit S,即可获取各个子库的结果,之后再合并后top S即可得到最终结果。在类似app中列表下拉的场景中,业务上可以禁止跳页查询,此时可以使用这种方式。<br><br>优点: 数据准确,性能良好<br>缺点: 不能跳页<br>
分布式事务
解决方案
XA协议、TCC和Saga事务模型、本地消息表、事务消息和阿里开源的Seata。
数据库事务有ACID四个特性
A(Atomicity)原子:指单个事务中的操作要不都执行,要不都不执行<br><br>C(Consistency)一致:指事务前后数据的完整性必须保持一致<br><br>I(Isolation)隔离:指多个事务对数据可见性的规则<br><br>D(Durability)持久:指事务提交后,就会被永久存储下来
2PC/3PC
二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。<br><br>3PC即三阶段提交,它比2PC多了一个阶段,即把原来2PC的准备阶段拆分成CanCommit和PreCommit两个阶段,同时引入超时机制来解决2PC的同步阻塞问题。<br>
XA
XA是一种基于2PC协议实现的规范。在2PC中没有明确资源是什么,以及资源是怎么提交的等等,而XA就是数据库实现2PC的规范,已知常用的支持XA的关系型数据库有Mysql、Oracle等
本地消息表
处理流程
<ul><li>事务发起方把要处理的业务事务和写消息表这两个操作放在同一个本地事务里<br><br></li><li>事务发起方有一个定时任务轮询消息表,把没处理的消息发送到消息中间件<br><br></li><li>事务被动方从消息中间件获取消息后,返回成功<br><br></li><li>事务发起方更新消息状态为已成功</li></ul>
分析
<ul><li>把业务处理和写消息表放在同一个事务是为了失败/异常后可以同时回滚<br><br></li><li>为什么不直接发消息,而是先写消息表?试想,如果发送消息超时了,即不确定消息中间件收到消息没,那么你是重试还是抛异常回滚事务呢?回滚是不行的,因为可能消息中间件已经收到消息,接收方收到消息后做处理,导致双方数据不一致了;重试也是不行的,因为有可能会一直重试失败,导致事务阻塞。<br><br></li><li>基于上述分析,消息的接收方是需要做幂等操作的</li></ul>
缺点
<ul><li>消息数据和业务数据耦合,消息表需要根据具体的业务场景制定,不能公用。就算可以公用消息表,对于分库的业务来说每个库都是需要消息表的。<br><br></li><li>只适用于最终一致的业务场景。例如在 A -> B场景下,在不考虑网络异常、宕机等非业务异常的情况下,A成功的话,B肯定也会成功的。</li></ul>
事务消息
定义
事务消息是通过消息中间件来解耦本地消息表和业务数据表,适用于所有对数据最终一致性需求的场景。现在支持事务消息的消息中间件只有RocketMQ,这个概念最早也是RocketMQ提出的。
流程
<ul><li>发起方发送半事务消息会给RocketMQ ,此时消息的状态prepare,接受方还不能拉取到此消息<br><br></li><li>发起方进行本地事务操作<br><br></li><li>发起方给RocketMQ确认提交消息,此时接受方可以消费到此消息了</li></ul>
异常
步骤1和3失败/异常该如何处理<br><br>RocketMQ会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。<br><br>可以看出事务消息的本质的借鉴了二阶段提交的思想,它跟本地消息表的做法也很像,事务消息做的事情其实就是把消息表的存储和扫描消息表这两个事情放到消息中间件来做,使得消息表和业务表解耦。
TCC(Try-Confirm-Cancel)
核心思想
事务模型采用的是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿操作。<br><br>相当于XA来说,TCC可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的2PC。<br>
三个阶段
<ul><li>Try阶段,对业务资源进行检测和预留<br><br></li><li>Confirm阶段,对Try阶段预留的资源进行确认提交,Try阶段执行成功是Confirm阶段执行成功的前提<br><br></li><li>Cancel阶段,对Try阶段预留的资源进行撤销或释放</li></ul>
缺点
<ul><li>对于Confirm和Cancel阶段失败后要完全靠业务应用自己去处理<br><br></li><li>每个业务都需要实现Try、Confirm、Cancel三个接口,代码量比较多<br><br></li><li>如果是基于现有的业务想使用TCC会比较困难。一是对于原来的接口要拆分为三个接口,入侵性比较大;二是因为要做“预留”资源的操作,有可能需要对原来的业务模型进行改造。</li></ul>
Seata
Seata是一个由阿里做背书的分布式事务框架,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。
线上问题解决
java
JDK、JRE、JVM
JDK(Java Development Kit Java 开发工具包),JDK 是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具,也包括了 JRE。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。<br><br>JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。<br><br>JVM(Java Virtual Machine Java 虚拟机),JVM 可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,
collection
hashMap
hashmap底层扩容线程安全问题 synchronized用在静态和非静态方法的区别
<b>概念</b>:Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值<br><br>两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。<br>
<b>常见的Hash函数</b>:<br><br><ul><li>直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。<br><br></li><li>数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。<br><br></li><li>除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。<br><br></li><li>分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。<br><br></li><li>平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。<br><br></li><li>伪随机数法:采用一个伪随机数当作哈希函数。<br><br><b>衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞。</b><br></li></ul>
解决碰撞的方法:
<ul><li><b>开放定址法</b></li></ul><br>开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。<br><br><ul><li><b>链地址法</b></li></ul><br>将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。<br><br><ul><li><b>再哈希法</b></li></ul><br>当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。<br><br><ul><li><b>建立公共溢出区</b></li></ul><br>将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
hash的实现
int hash(Object k) //该方法主要是将Object转换成一个整型。
int indexFor(int h, int length) //该方法主要是将hash生成的整型转换成链表数组中的下标。
return h & (length-1); //位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
扩容
<b>为什么要扩容</b>:<b>上面说过哈希函数的好坏指标就是看哈希碰撞概率,为了避免碰撞进行扩容,否则,hash表会退化为链表</b>
HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。<br><br>threshold = loadFactor * capacity。<br><br>loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数。<br><br>对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。<br>
1.7以前使用头插法扩容,在线程安全情况下导致死循环
线程安全
总结:
HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。<br><br>hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。<br><br>而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。<br><br>为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。<br><br>首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。<br><br>另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。
ArrayBlockingQueue
具有线程安全性和阻塞性的有界队列
ArrayBlockingQueue的线程安全是通过底层的ReentrantLock保证的
构造方法<br>public ArrayBlockingQueue(int capacity, boolean fair) {<br> if (capacity <= 0) throw new IllegalArgumentException();<br> this.items = new Object[capacity];<br> lock = new ReentrantLock(fair);<br> notEmpty = lock.newCondition();<br> notFull = lock.newCondition();<br>}<br>
<ul><li>Object[] items:队列的底层由数组组成,并且数组的长度在初始化就已经固定,之后无法改变<br><br></li><li>ReentrantLock lock:控制队列操作的独占锁,在操作队列的元素前需要获取锁,保护竞争资源<br><br></li><li>Condition notEmpty:条件对象,如果有线程从队列中获取元素时队列为空,就会在此进行等待,直到其他线程向队列后插入元素才会被唤醒<br><br></li><li>Condition notFull:如果有线程试图向队列中插入元素,且此时队列为满时,就会在这进行等待,直到其他线程取出队列中的元素才会被唤醒</li></ul>
相关操作
ConcurrentHashMap
<b>--java7<br>1、ConcurrentHashMap的哪些操作需要加锁?</b><br>答:只有写入操作才需要加锁,读取操作不需要加锁<br><b>2、ConcurrentHashMap的无锁读是如何实现的?</b><br>答:首先HashEntry中的value和next都是有volatile修饰的,其次在写入操作的时候通过调用UNSAFE库延迟同步了主存,保证了数据的一致性<br><b>3、在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?</b><br>答:size()具有全局的语义,如何能保证在不加全局锁的情况下读取到全局状态的值是一个很大的挑战,ConcurrentHashMap通过查看两次无锁读中间是否发生了写入操作来决定读取到的size()是否可信,如果写入操作频繁,则再退化为全局加锁读取。<br><b>4、在有Segment存在的前提下,是如何扩容的?</b><br>答:segment数组的大小在一开始初始化的时候就已经决定了,扩容主要扩的是HashEntry数组,基本的思路与HashTable一致,但这是一个线程不安全方法,调用之前需要加锁。<br><b>5、为什么 ConcurrentHashMap 的读操作不需要加锁<br></b>get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系,数组用volatile修饰主要是保证在数组扩容的时候保证可见性。<br>
IO
BIO
同步并阻塞,在服务器中实现的模式为<b>一个连接一个线程</b>。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。<b>BIO一般适用于连接数目小且固定的架构</b>,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。<br>
NIO
同步并非阻塞,在服务器中实现的模式为<b>一个请求一个线程</b>,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。<b>NIO一般适用于连接数目多且连接比较短(轻操作)的架构</b>,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
AIO
异步并非阻塞,在服务器中实现的模式为<b>一个有效请求一个线程</b>,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
反射
反射机制:在运行过程中,对于任意一个类,都能知道其所有的属性和方法;对于任意一个对象,都能调用其属性和方法;这种动态获取类信息和调用对象方法的功能,就是 Java 反射机制。
<ul><li>在运行时判断任意一个对象所属的类;<br><br></li><li>在运行时构造任意一个类的对象;<br><br></li><li>在运行时判断任意一个类所具有的成员变量和方法;<br><br></li><li>在运行时调用任意一个对象的成员变量和方法;<br><br></li><li>生成动态代理。</li></ul>
Thread
进程
进程间通信方式
管道<br><br>消息队列<br><br>共享内存<br><br>信号量<br><br>信号<br><br>套接字
线程状态
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。<br><br>2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。<br><br>线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。<br><br>3.阻塞(BLOCKED):表示线程阻塞于锁。<br><br>4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。<br><br>5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。<br><br>6. 终止(TERMINATED):表示该线程已经执行完毕。<br>
线程状态切换:
volatile
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。<br><ul><li>volatile关键字对于基本类型的修改可以在随后对多个线程的读保持一致,但是对于引用类型如数组,实体bean,仅仅保证引用的可见性,但并不保证引用内容的可见性。。</li><li>禁止进行指令重排序。</li></ul>背景:为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。<br><ul><li>如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。</li><li>在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。</li></ul>
总结下来:<br>第一:使用volatile关键字会强制将修改的值立即写入主存;<br>第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);<br>第三:由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。
synchronized
<ul><li>synchronized 修饰实例方法,相当于是对类的实例进行加锁,进入同步代码前需要获得当前实例的锁<br><br></li><li>synchronized 修饰静态方法,相当于是对类对象进行加锁<br><br></li><li>synchronized 修饰代码块,相当于是给对象进行加锁,在进入代码块前需要先获得对象的锁</li></ul>
ThreadLocal
ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。<br><br>ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。<br>
线程池
优点:
<ul><li>降低系统资源消耗。通过复用已存在的线程,降低线程创建和销毁造成的消耗;<br><br></li><li>提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;<br><br></li><li>提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。 </li></ul>
核心参数
public ThreadPoolExecutor(int corePoolSize,<br> int maximumPoolSize,<br> long keepAliveTime,<br> TimeUnit unit,<br> BlockingQueue<Runnable> workQueue,<br> ThreadFactory threadFactory,<br> RejectedExecutionHandler handler)
<ol><li>corePoolSize :核心线程数</li><li>maximumPoolSize: 最大线程数</li><li>keepAliveTime :线程在线程池中不被销毁的空闲时间,如果线程池的线程太多,任务比较小,到这个时间就销毁线程池。</li><li>unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。</li><li>workQueue : 任务队列,存放等待执行的任务</li><li>threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲</li><li>handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器</li><li>allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高</li></ol>
JVM
运行时数据区<br>
方法区(该区域线程共享)<br>
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据(1)线程共享的(2)运行时常量池:
虚拟机栈
<b style="font-size: inherit;">虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。<br></b><span style="font-size: inherit;">(1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接<br>a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。<br>b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。<br>c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。<br>d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。<br>(2)线程私有</span><br>
本地方法栈
<span style="font-size: inherit;">(1)调用本地native的内存模型(2)线程独享</span><br>
堆(该区域线程共享)
Java对象存储的地方<br>(1)Java堆是虚拟机管理的内存中最大的一块<br>(2)Java堆是所有线程共享的区域<br>(3)在虚拟机启动时创建<br>(4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组<br>(5)Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”
堆内存的对象不一定是共享的,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。,这种方案称为:TLAB分配,即Thread Local Allocation Buffer(https://mp.weixin.qq.com/s/-tfs9nkufS6Hh4tSYkkCxQ)
虚拟机编译优化技术:逃逸分析(标量替换、栈上分配https://mp.weixin.qq.com/s/Owlhu5IFpDAyu0WYcK1EhQ)、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除等
程序计数器
指向当前线程正在执行的字节码指令,线程私有的<br><br>正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)<br>
结构示意图
各版本jdk下jvm内存区别
JDK1.6、JDK1.7、JDK1.8 JVM 内存模型主要有以下差异:<br><br>JDK 1.6:有永久代,静态变量存放在永久代上。<br><br>JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。<br><br>JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
堆内存
堆内存的划分图
年轻代、Old Memory(老年代)、Perm(永久代,其中在Jdk1.8中,永久代被移除,使用MetaSpace代替)<br>
1、新生代:<br>(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。<br>(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1<br>(3)内存不足时发生Minor GC<br>2、老年代:<br>(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。<br>3、Perm:用来存储类的元数据,也就是方法区。<br>(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。<br>(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。<br>4、堆内存的划分在JVM里面的示意图:<br>
<br>
回收
判断对象是否要回收的方法
<b>1、可达性分析法</b>:通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)<br>
2、 以下对象会被认为是root对象:<br>(1) 虚拟机栈(栈帧中本地变量表)中引用的对象<br>(2) 方法区中静态属性引用的对象<br>(3) 方法区中常量引用的对象<br>(4) 本地方法栈中Native方法引用的对象
3、 对象被判定可被回收,需要经历两个阶段:<br>(1) 第一个阶段是可达性分析,分析该对象是否可达<br>(2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
4、 方法区中的垃圾回收:<br>(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池<br>(2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:A、 该类的所有实例被回收B、 加载该类的ClassLoader被回收C、 该类的Class对象没有被引用
5、 finalize():<br>(1) GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。<br>(2) 可以在该方法里面,指定一些对象在释放前必须执行的操作。
发现虚拟机频繁full GC时应该怎么办:(full GC指的是清理整个堆空间,包括年轻代和永久代)
(1) 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id<br>(2) 如果是System.gc(),则看下代码哪里调用了这个方法<br>(3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令<br>(4) 如果是GC locker,可能是程序依赖的JNI库的原因
常见的垃圾回收算法
1、Mark-Sweep(标记-清除算法):Major GC(⽼年代GC)<br>(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。<br>(2)优缺点:实现简单,容易产生内存碎片,需要清除的对象过多时,效率较低
2、Copying(复制清除算法):Minor GC(新⽣代GC)<br>(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。<br>(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
3、Mark-Compact(标记-整理算法):Major GC(⽼年代GC)<br>(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。<br>(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):<br>(1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。<br>(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。<br>(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量<br>
5、GC使用时对程序的影响?垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间
6、几种不同的垃圾回收类型:<br>(1)Minor GC:从年轻代(包括Eden、Survivor区)回收内存。<br>A、当JVM无法为一个新的对象分配内存的时候,越容易触发Minor GC。所以分配率越高,内存越来越少,越频繁执行Minor GC<br>B、执行Minor GC操作的时候,不会影响到永久代(Tenured)。从永久代到年轻代的引用,被当成GC Roots,从年轻代到老年代的引用在标记阶段直接被忽略掉。<br>(2)Major GC:清理整个老年代,当eden区内存不足时触发。<br>(3)Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发
HotSpot 虚拟机详解:
1、 Java对象创建过程:
(1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。<br>(2)为该对象分配内存。<br>A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”<br>B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。<br>C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。<br>D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)<br>(3)虚拟机为分配的内存空间初始化为零值(默认值)<br>(4)虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。<br>(5) 执行方法,把对象按照程序员的意愿进行初始化。
2、 对象的定位访问的方式(通过引用如何去定位到堆上的具体对象的位置):
(1)句柄:使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址。
(2)直接指针:使用直接指针的方式,引用中存储的就是对象的地址。Java堆对象的布局必须必须考虑如何去访问对象类型数据。
(3)两种方式各有优点:A、使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。B、使用直接指针,节省了一次指针定位的时间开销。
3、HotSpot的GC算法实现:
(1)HotSpot怎么快速找到GC Root?HotSpot使用一组称为OopMap的数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。
(2)安全点:A、HotSpot只在特定的位置生成OopMap,这些位置称为安全点。B、程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。C、安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint。
(3)中断方式:<br>A、抢占式中断:在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。<br>B、主动式中断:GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。<br>
(4)安全区域:一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。在线程进入安全区域时,它首先标志自己已经进入安全区域,在这段时间里,当JVM发起GC时,就不用管进入安全区域的线程了。在线程将要离开安全区域时,它检查系统是否完成了GC过程,如果完成了,它就继续前行。否则,它就必须等待直到收到可以离开安全区域的信号。
4、 GC时为什么要停顿所有Java线程?因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达,假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。
5、 CMS收集器:
(1)一种以获取最短回收停顿时间为目标的收集器。<br>(2)一般用于互联网站或者B/S系统的服务端<br>(3)基于标记-清除算法的实现,不过更为复杂,整个过程为4个步骤:<br>A、初始标记:标记GC Root能直接引用的对象<br>B、并发标记:利用多线程对每个GC Root对象进行tracing搜索,在堆中查找其下所有能关联到的对象。<br>C、重新标记:为了修正并发标记期间,用户程序继续运作而导致标志产生变动的那一部分对象的标记记录。<br>D、并发清除:利用多个线程对标记的对象进行清除<br>(4)由于耗时最长的并发标记和并发清除操作都是用户线程一起工作,所以总体来说,CMS的内存回收工作是和用户线程一起并发执行的。<br>(5)缺点:<br>A、对CPU资源占用比较多。可能因为占用一部分CPU资源导致应用程序响应变慢。<br>B、CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾,这一部分垃圾出现在标记过程之后,因此,CMS无法清除。这部分垃圾称为“浮动垃圾“<br>C、需要预留一部分内存,在垃圾回收时,给用户程序使用。<br>D、基于标记-清除算法,容易产生大量内存碎片,导致full GC(full GC进行内存碎片的整理)<br>
6、 对象头部分的内存布局:HotSpot的对象头分为两部分,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄等。另外一部分用于指向方法区对象类型数据的指针。
7、 偏向锁:偏向锁偏向于第一个获取它的线程,如果在接下来的执行过程,没有其他线程获取该锁,则持有偏向锁的线程永远不需要同步。(当一个线程获取偏向锁,它每次进入这个锁相关的同步块,虚拟机不在进行任何同步操作。当有另外一个线程尝试获取这个锁时,偏向模式宣告结束)
<b><font color="#f384ae">优化</font></b>
1、一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而<b>避免full GC</b>,使用-Xmn设置年轻代的大小
2、对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。
3、一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代,可以设置比较大的阈值。
4、设置最小堆和最大堆:-Xmx和-Xms稳定的堆大小堆垃圾回收是有利的,获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样,即最大堆和最小堆一样,如果这样子设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC次数,因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数,但是增加每次GC的时间,因为每次GC要把堆的大小维持在一个区间内。
5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。(1)-XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间(2)-XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间。(3)当-Xmx和-Xmx相等时,上面两个参数无效
6、通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。(1)-XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间。(2)-XX:+UseParallelOldGC:设置老年代使用并行垃圾回收收集器。
7、尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。-XX:+LargePageSizeInBytes 设置内存页的大小
8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。
9、-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
10、JVM性能调优的工具:(1)jps(Java Process Status):输出JVM中运行的进程状态信息(现在一般使用jconsole)(2)jstack:查看java进程内线程的堆栈信息。(3)jmap:用于生成堆转存快照(4)jhat:用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)(3)jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。(4)VisualVM:故障处理工具
JMM
JAVA内存模型
1、Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。<br>2、主要目的是定义程序中各个变量的访问规则<br>3、Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。<br>(1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。<br>(2)不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。<br>(3)主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。<br>4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制<br><span style="font-size: inherit;">(1)JMM决定一个线程对变量的写入何时对另一个线程可见。</span><br><span style="font-size: inherit;">(2)线程之间共享变量存储在主内存中</span><br>(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本<br><span style="font-size: inherit;">(4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证</span><br><span style="font-size: inherit;">5、可见性、有序性</span><br style="font-size: inherit;"><span style="font-size: inherit;">(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。</span><br><span style="font-size: inherit;">(2)保证线程的有序执行,这个为有序性。(保证线程安全)</span><br>6、内存间交互操作<br>(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。<br>(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。<br>(3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。<br>(4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中<br>(5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。<br>(6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。<br>(7)store(存储):把工作内存的变量的值传递给主内存<br>(8)write(写入):把store操作的值入到主内存的变量中<br>
内存模型示意图<br>
类加载机制
概念:类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
步骤:<br>1、加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)<br>2、连接:把内存中类的二进制数据合并到虚拟机的运行时环境中<br>(1)验证:确保被加载的类的正确性。包括:<br>A、类文件的结构检查:检查是否满足Java类文件的固定格式<br> B、语义检查:确保类本身符合Java的语法规范<br> C、字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。<br> D、二进制兼容性验证:确保相互引用的类之间是协调一致的。<br>(2)准备:为类的静态变量分配内存,并将其初始化为默认值<br>(3)解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)<br>3、初始化:为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。<br>
双亲委派模型
1、当一个类加载器收到类加载请求的时候,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。
2、意义:<b>提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。(比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了)<br><br>采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。<br></b>
3、定义类加载器:如果某个类加载器能够加载一个类,那么这个类加载器就叫做定义类加载器
4、初始类加载器:定义类加载器及其所有子加载器都称作初始类加载器。
5、运行时包:<br>(1)由同一个类加载器加载并且拥有相同包名的类组成运行时包<br>(2)只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
6、加载两份相同的class对象的情况:A和B不属于父子类加载器关系,并且各自都加载了同一个类。
<b>Tomcat 为何打破双亲委派机制</b>
特点:<br>1、全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。<br>2、缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。
两种类型的类加载器
1、 JVM自带的类加载器(3种):<br>(1)根类加载器(Bootstrap):<br>a、C++编写的,程序员无法在程序中获取该类<br>b、负责加载虚拟机的核心库,比如java.lang.Objectc、没有继承ClassLoader类<br>(2)扩展类加载器(Extension):<br>a、Java编写的,从指定目录中加载类库<br>b、父加载器是根类加载器<br>c、是ClassLoader的子类<br>d、如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。<br>(3)系统加载器(System)或者应用加载器(App):<br>a、Java编写的<br>b、父加载器是扩展类加载器<br>c、从环境变量或者class.path中加载类<br>d、是用户自定义类加载的默认父加载器<br>e、是ClassLoader的子类
2、用户自定义的类加载器:<br>(1)Java.lang.ClassLoader类的子类<br>(2)用户可以定制类的加载方式<br>(3)父类加载器是系统加载器<br>(4)编写步骤:<br>A、继承ClassLoader<br>B、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象<br>(5)为什么要自定义类加载器?<br>A、可以从指定位置加载class文件,比如说从数据库、云端加载class文件<br>B、加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。
权限
RBAC基于角色的访问控制(Role-Based Access Control)
RABC1-角色的继承
单继承
多继承
RABC2-角色的约束控制
互斥角色
互斥角色是指各自权限互相制约的两个角色。比如财务部有会计和审核员两个角色, 他们是互斥角色, 那么用户不能同时拥有这两个角色, 体现了职责分离原则
基数约束
一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配
先决条件角色
即用户想获得某上级角色, 必须先获得其下一级的角色
RABC-3 = 2+1
用户组
用户组设计模型
表设计<br>
分布式与微服务
设计理念
<ol><li>可拓展性</li><li>可用性</li><li>弹性</li><li>独立自主</li><li>分散治理</li><li>故障隔离</li><li>自动配置</li><li>通过DevOps持续交付</li></ol>
分解模式
按业务能力分解
问题:<br>微服务就是让应用服务松散耦合,但是将应用程序分解成较小的部分还必须要在逻辑上实现。那我们如何将应用程序分解为小型服务呢?<br><br>解决方案:<br>一种策略就是按业务能力分解,业务能力是企业业务价值的体现。业务的功能取决于业务的类型。例如,保险公司的业务能力通常包括销售,市场营销,承保,理赔处理,开票,合规性等。每种业务能力都可以视为一种服务,但它面向的是业务而不是技术。
按子域划分
问题:<br>按业务功能来分解应用程序或许是个不错的思路。但是我们可能会遇到某些比较难以分解出来的类(God Classes),这种类在多种服务中通用。比如,订单类用于“订单管理”,“接单”,“订单交付”等业务中。那我们该如何来分解呢?<br><br>解决方案:<br>对于这种难以分解出来的(God Classes)类,使用DDD(即领域驱动设计)可以解决。它使用子域和有界上下文概念来解决此问题。DDD将为企业创建的整个域模型分解为子域。每个子域都有一个模型,该模型的范围称为有界上下文。每个微服务将围绕有界的上下文进行开发。<br><br>注意:确定子域并不是件容易的事,这需要对业务有一定的了解。像业务功能一样,通过分析业务及其组织结构并确定不同的专业领域来标识子域。
扼杀者模式
问题:<br>到目前为止,我们所讨论的设计模式都是分解未开发的应用程序,但是我们所做的工作中有80%是用于已开发的应用程序(brownfield applications)中,这是个大型的整体应用程序。上述所有设计模式并不是适用于它们,因为把它们作为一个整体应用的同时将它们拆分成一个个较小的部分是一项艰巨的任务。<br><br>解决方案:<br>扼杀者模式可以解决此类问题。扼杀者模式是以缠绕类的藤蔓植物作为类比。该解决方案是与Web应用程序配合使用,在Web应用程序之间来回调用,对于每个URL的调用,一个服务可以分为不同的域并作为单独的服务托管。这个想法是一次做一个域,这将会创建两个单独的应用程序,它们并行存在于同一个URL空间中。最终,新重构的应用程序会“扼杀”或者替换原来的应用程序,直到最后可以停止整个应用程序。
领域驱动设计
CAP
属性: CAP 三个字母分别代表了分布式系统中三个相互矛盾的属性
<ul><li>Consistency (一致性):CAP 理论中的副本一致性特指强一致性(1.3.4 );<br><br></li><li>Availiablity(可用性):指系统在出现异常时已经可以提供服务;<br><br></li><li>Tolerance to the partition of network (分区容忍):指系统可以对网络分区(1.1.4.2 )这种异常情 况进行容错处理;</li></ul>
CAP 理论指出:无法设计一种分布式协议,使得同时完全具备CAP 三个属性,即<br>1)该种协议下的副本始终是强一致性,<br>2)服务始终是可用的,<br>3)协议可以容忍任何网络分区异常;<br><br>分布式系统协议只能在CAP 这三者间所有折中。
热力学第二定律说明了永动机是不可能存在的,不要去妄图设计永动机。与之类似,CAP 理论的意义就在于明确提出了不要去妄图设计一种对CAP 三大属性都完全拥有的完美系统,因为这种系统在理论上就已经被证明不存在。<br><br><b>要么就是AP,要么就是CP;思考下为什么</b>
集群与分布式
<ul><li>集群是个物理形态,分布式是个工作方式。</li><li><span style="font-size: inherit;">集群一般是物理集中、统一管理的,而分布式系统则不强调这一点。</span></li><li><span style="font-size: inherit;">集群可能运行着一个或多个分布式系统,也可能根本没有运行分布式系统;分布式系统可能运行在一个集群上,也可能运行在不属于一个集群的多台(2 台也算多台)机器上</span></li><li><span style="font-size: inherit;">分布式是相对中心化而来,强调的是任务在多个物理隔离的节点上进行。中心化带来的主要问题是可靠性,若中心节点宕机则整个系统不可用,分布式除了解决部分中心化问题,也倾向于分散负载,但分布式会带来很多的其他问题,最主要的就是一致性。</span></li><li><span style="font-size: inherit;">集群就是逻辑上处理同一任务的机器集合,可以属于同一机房,也可分属不同的机房。分布式这个概念可以运行在某个集群里面,某个集群也可作为分布式概念的一个节点。</span></li><li><span style="font-size: inherit;">分布式是指将不同的业务分布在不同的地方。而集群指的是将几台服务器集中在一起,实现同一业务。</span></li><li><span style="font-size: inherit;">分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。</span></li></ul><br style="font-size: inherit;"><b style="font-size: inherit;">一句话,就是:“分头做事” 与 “一堆人” 的区别:</b>分布式是指将不同的业务分布在不同的地方。而集群指的是将几台服务器集中在一起,实现同一业务。<br>
小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式,一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群。
副本
副本(replica/copy)指在分布式系统中为数据或服务提供的冗余。对于数据副本指在不同的节点上持久化同一份数据,当出现某一个节点的存储的数据丢失时,可以从副本上读到数据。数据副本是分布式系统解决数据丢失异常的唯一手段。另一类副本是服务副本,指数个节点提供某种相同的服务,这种服务一般并不依赖于节点的本地存储,其所需数据一般来自其他节点。<br><br>副本协议是贯穿整个分布式系统的理论核心。
一致性
<ol><li>强一致性(strong consistency):任何时刻任何用户或节点都可以读到最近一次成功更新的副本数据。强一致性是程度最高的一致性要求,也是实践中最难以实现的一致性。<br><br></li><li>单调一致性(monotonic consistency):任何时刻,任何用户一旦读到某个数据在某次更新后的值,这个用户不会再读到比这个值更旧的值。单调一致性是弱于强一致性却非常实用的一种一致性级别。因为通常来说,用户只关心从己方视角观察到的一致性,而不会关注其他用户的一致性情况。<br><br></li><li>会话一致性(session consistency):任何用户在某一次会话内一旦读到某个数据在某次更新后的值,这个用户在这次会话过程中不会再读到比这个值更旧的值。会话一致性通过引入会话的概念,在单调一致性的基础上进一步放松约束,会话一致性只保证单个用户单次会话内数据的单调修改,对于不同用户间的一致性和同一用户不同会话间的一致性没有保障。实践中有许多机制正好对应会话的概念,例如php 中的session 概念。<br><br></li><li>最终一致性(eventual consistency):最终一致性要求一旦更新成功,各个副本上的数据最终将达 到完全一致的状态,但达到完全一致状态所需要的时间不能保障。对于最终一致性系统而言,一个用户只要始终读取某一个副本的数据,则可以实现类似单调一致性的效果,但一旦用户更换读取的副本,则无法保障任何一致性。<br><br></li><li>弱一致性(week consistency):一旦某个更新成功,用户无法在一个确定时间内读到这次更新的值,且即使在某个副本上读到了新的值,也不能保证在其他副本上可以读到新的值。弱一致性系统一般很难在实际中使用,使用弱一致性系统需要应用方做更多的工作从而使得系统可用。</li></ol>
衡量分布式系统的指标
<ol><li><b>性能</b>:系统的吞吐能力,指系统在某一时间可以处理的数据总量,通常可以用系统每秒处理的总的数据量来衡量;系统的响应延迟,指系统完成某一功能需要使用的时间;系统的并发能力,指系统可以同时完成某一功能的能力,通常也用QPS(query per second)来衡量。上述三个性能指标往往会相互制约,追求高吞吐的系统,往往很难做到低延迟;系统平均响应时间较长时,也很难提高QPS。<br><br></li><li><b>可用性</b>:系统的可用性(availability)指系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。<br><br></li><li><b>可扩展性</b>:系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性。好的分布式系统总在追求“线性扩展性”,也就是使得系统的某一指标可以随着集群中的机器数量线性增长。<br><br></li><li><b>一致性</b>:分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。越是强的一致的性模型,对于用户使用来说使用起来越简单。</li></ol>
数据分布方式
参考redis章节的:哈希、一致性哈性、带虚拟node的一致性哈希,redis-cluter就是采用的最后种来实现的额,有何优点?不知道的话滚去看那个章节
web服务器
nginx
反向代理
反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。<br><br>简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。<br>
负载均衡
当有2台或以上服务器时,根据规则随机的将请求分发到指定的服务器上处理,负载均衡配置一般都需要同时配置反向代理,通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略,还有2种常用的第三方策略。
策略
RR(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
ip_hash
iphash的每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
HTTP服务器(包含动静分离)
正向代理
tomcat
架构图(图中的虚线表示一个请求在 Tomcat 中流转的过程)
连接器
Tomcat 的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用 ProtocolHandler 接口来封装通信协议和 I/O模型的差异,ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint负责底层 Socket 通信,Proccesor 负责应用层协议解析。连接器通过适配器 Adapter调用容器。<br><br>对 Tomcat 整体架构的学习,我们可以得到一些设计复杂系统的基本思路。首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。
容器
运用了组合模式 管理容器、通过 观察者模式 发布启动事件达到解耦、开闭原则。骨架抽象类和模板方法抽象变与不变,变化的交给子类实现,从而实现代码复用,以及灵活的拓展。使用责任链的方式处理请求,比如记录日志等。
类加载器
Tomcat 的自定义类加载器 WebAppClassLoader 为了隔离 Web 应用打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。防止 Web 应用自己的类覆盖 JRE 的核心类,使用 ExtClassLoader 去加载,这样即打破了双亲委派,又能安全加载。
linux
文档属性
ls -al --full-time
第一列<br>共10位,第1位表示文档类型,d表示目录,-表示文件,l表示链接文件,d表示可随机存取的设备,如U盘等,c表示一次性读取设备,如鼠标、键盘等。后9位,依次对应三种身份所拥有的权限,身份顺序为:owner、group、others,权限顺序为:readable、writable、excutable。如:-r-xr-x---的含义为当前文档是一个文件,拥有者可读、可执行,同一个群组下的用户,可读、可写,其他人没有任何权限。<br><br>第二列<br>表示连结数<br><br>第三列<br>表示拥有者<br><br>第四列<br>表示所属群组<br><br>第五列<br>表示文档容量大小,单位字节<br><br>第六列<br>表示文档最后修改时间,注意不是文档的创建时间哦<br><br>第七列<br>表示文档名称。以点(.)开头的是隐藏文档
变更拥有者(owner)
位置:<br>etc/passwd
语法:<br>chown [-R] [帐号名称] [文件或目录]<br>chown [-R] [帐号名称]:[群组名称] [文件或目录] <br><br>备注:此命令也可以顺便变更文档群组,但还是建议使用chgrp命令来变更文档群组。<br>
选项:<br>-R 递归变更,即连同次目录下的所有文件(夹)都要变更<br>
用法:<br>chown daemon test 变更文件夹test账号为daemon<br>chown daemon:root test 变更文件夹test群组为root<br>chown root.users test 变更文件夹账号为root,群组为users<br>chown .root test 单独变更群组为root<br>
变更群组(group)
位置:<br>etc/group<br>
语法:<br>chgrp [-options] [群组名] [文档路径] <br><br>备注:关于options,可以通过man chgrp、info chgrp、chgrp --help等命令查询详细用法。<br>
用法:<br>chgrp -R users test 改变test文件夹及其所有子文件(夹)的群组为users<br><br>群组名称不在位置内,将会报错invalid group<br>
变更权限
Linux文档的基本权限就三个,分别是read/write/execute,加上身份owner/group/others也只有九个。权限变更的方式有2种,分别是符号法和数字法。
- 符号法
分别使用u,g,o来代表三种身份,a表示全部身份;分别使用r、w、x表示三种权限;分别使用+、-、=表示操作行为
语法
chmod | u g o a | +(加入) -(除去) =(设置) | r w x | 文档路径<br><br>设置权限(=)<br><br>变更目录test的权限为任何人都可读、写、执行。<br><br>chmod u=rwx,g=rwx,o=rwx test<br>或<br>chmod ugo=rwx test<br>或<br>chmod a=rwx test<br>
去掉权限(-)
去掉目录test执行权限<br>chmod u-x,g-x,o-x test<br>或<br>chmod ugo-x test<br>或<br>chmod a-x test<br>
添加权限(+)
增加目录test执行权限<br>chmod u+x,g+x,o+x test<br>或<br>chmod ugo+x test<br>或<br>chmod a+x test<br>
网络
七层网络模型OSI(Open System Interconnection)
tcp
三次握手、四次分手
第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;<br><br><br><br>第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;<br><br><br><br>第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。<br><br>完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。通信结束客户端和服务端就断开连接,需要经过四次分手确认。<br><br><br><br>第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;<br><br><br><br>第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;<br><br><br><br>第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;<br><br><br><br>第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。<br><br><br><br>可以看到一次tcp请求的建立及关闭至少进行7次通信,这还不包过数据的通信,而UDP不需3次握手和4次分手。
<b>1.TCP服务器最大并发连接数是多少?<br></b><br>关于TCP服务器最大并发连接数有一种误解就是“因为端口号上限为65535,所以TCP服务器理论上的可承载的最大并发连接数也是65535”。首先需要理解一条TCP连接的组成部分:客户端IP、客户端端口、服务端IP、服务端端口。所以对于TCP服务端进程来说,他可以同时连接的客户端数量并不受限于可用端口号,理论上一个服务器的一个端口能建立的连接数是全球的IP数*每台机器的端口数。实际并发连接数受限于linux可打开文件数,这个数是可以配置的,可以非常大,所以实际上受限于系统性能。通过#ulimit -n 查看服务的最大文件句柄数,通过ulimit -n xxx 修改 xxx是你想要能打开的数量。也可以通过修改系统参数:<br>#vi /etc/security/limits.conf<br>* soft nofile 65536<br>* hard nofile 65536<br><br><b>2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?</b><br><br>这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的Socket可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。<br><br><b>3.TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态会产生什么问题</b><br><br>通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态,TIME_WAIT状态维持时间是两个MSL时间长度,也就是在1-4分钟,Windows操作系统就是4分钟。进入TIME_WAIT状态的一般情况下是客户端,一个TIME_WAIT状态的连接就占用了一个本地端口。一台机器上端口号数量的上限是65536个,如果在同一台机器上进行压力测试模拟上万的客户请求,并且循环与服务端进行短连接通信,那么这台机器将产生4000个左右的TIME_WAIT Socket,后续的短连接就会产生address already in use : connect的异常,如果使用Nginx作为方向代理也需要考虑TIME_WAIT状态,发现系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决。<br>vi /etc/sysctl.conf<br><br>net.ipv4.tcp_syncookies = 1<br>net.ipv4.tcp_tw_reuse = 1<br>net.ipv4.tcp_tw_recycle = 1<br>net.ipv4.tcp_fin_timeout = 30<br><br>然后执行 /sbin/sysctl -p 让参数生效。<br><br>net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;<br><br>net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;<br><br>net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。<br><br>net.ipv4.tcp_fin_timeout 修改系統默认的TIMEOUT时间<br>
HTTP
HTTP协议即超文本传送协议(Hypertext Transfer Protocol )
关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍:“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容。如果想要使传输的数据有意义,则必须使用到应用层协议。应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。
Socket
现在我们了解到TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口,这就是Socket。现在我们知道,Socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,Socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。
软件设计
设计模式
创建型模式
结构型模式
行为模式<br>
0 条评论
下一页