一、Volatile
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。二.synchronized与Lock
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
6)可重入锁、可中断锁、公平锁
三、HashMap与HashTbale、ConcurrentHashMap
1)HashTable底层数组+链表实现,无论key还是value都不能为null,线程安全,锁的是整个hashtable,初始size 11
2)hashMap 可以存储null键和null值,线程不安全,初始16,扩容size*2;hashSet是个简化的hashmap。hashmap是按照Key的hash值进行分桶的。相同的hash值放在同一桶里面。jdk1.8使用的是红黑树。TreeMap有序存取
3)ConcurrentHashMap 分段segment,数组+链表,按段进行锁定,1.8里面使用的cas+synchronized锁定的是Node,保证线程安全。ConcurrentLinkMap是有序的
四、悲观锁和悲观锁
悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
五、Atomic原子类操作,
里的类基本都是使用Unsafe实现的包装类,采用的是CAS乐观锁的机制。缺点是ABA的命令。JDK 1.8中线程池:
CountDownLatch,内部实现的是AQS,tryReleaseShared尝试释放锁,doReleaseShared尝试唤醒线程。 使用await,和countdownn。
六、网络:
Http协议和RPC协议对比
HTTP协议是在传输层协议TCP之上的OSI网络7层:
第一层:应用层。定义了用于在网络中进行通信和传输数据的接口;
第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等;
第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断;
第四层:传输层。管理着网络中的端到端的数据传输;
第五层:网络层。定义网络设备间如何传输数据;
第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输;
第七层:物理层。这一层主要就是传输这些二进制数据。TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接口层。从实质上讲,只有上边三层,网络接口层没有什么具体的内容
五层体系结构包括:应用层、运输层、网络层、数据链路层和物理层。
五层协议只是OSI和TCP/IP的综合,实际应用还是TCP/IP的四层结构。为了方便可以把下两层称为网络接口层。七、TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP对系统资源要求较多,UDP对系统资源要求较少。
八、Java的线程生命周期有六种状态:
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。九:JVM:
1、栈区 StackOverFlowError和 OutOfMemoneyError、
2、堆区、 新生代(三个区,Eden区,两个幸存区。Eden:S0:S1==8:1:1,)、老年代、永久代。
3、方法区。
4、 程序计数器
Minor GC触发机制:
3.1 标记-清除算法
3.2 复制算法
3.3 标记-整理算法
3.4 分代收集算法
3.4.1 年轻代(Young Generation)的回收算法
3.4.2 年老代(Old Generation)的回收算法
3.4.3 持久代(Permanent Generation)的回收算法Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
G1(Garbage First)算法,通过参数-XX:+UseG1GC来启用,该算法在JDK 7u4版本被正式推出,G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,下面是官方介绍:GC回收算法:
标记-清除算法。第一次标记,第二次对标记进行清除,会产生内存碎片
标记-整理算法,第一次标记,第二次将未标记的拷贝到另外一个地方,然后清除死亡的。缺点,存活较多会比较慢
复制算法:和整理算法的区别,切分内存,总有一半内存是空的。
分代收集算法:新生代和老年代(新生带8:1:1 Ednn区,Form区,To区)。
申请在新生代的Edn区,满了以后,进行GC,然后将存活的放入Form区,下次Edn再满了,把Form+Edn放入to区,然后再满了,放入Form区,回收器机制:
ParNew 多线程并行回收新生代,使用的复制算法。
CMS老年代回收器,标记清除算法,初始标记,并行标记,重新标记,并发清理。
G1回收器:采用标记整理算法,不产生内存碎片,不那么明显划分新生代和老年代,划分成大小独立的Regeionn段。
1)初始标记 2)并发标记,3)再标记 4)并发清理
十:IO模型
网络IO模型和文件IO模型是一样的,上图是IO的5种模型,包括阻塞IO、非阻塞IO、多路复用IO、信号驱动的IO、异步IO。
一次IO包括两个过程,内核数据准备 、把数据从内核空间copy到用户空间。
1、阻塞IO(recvfrom系统调用)是在两个过程应用都处于阻塞状态。
2、非阻塞IO(recvfrom系统调用)是应用发出IO操作后可以立刻返回,通过轮询盘判断数据是否准备好,在copy数据阶段阻塞应用。
3、多路复用IO(select 、recvfrom系统调用)是阻塞调用select,查找可用的套接字,如果有套接字可用,那么就阻塞调用(recvfrom)完成数据的copy过程。Linux select就是这种模型,缺点是一次select会扫描所有的socket。
4、信号驱动的IO(SIGIO、recvfrom)是应用发出SIG IO后立刻返回,内核中数据准备好后,通知应用,由应用进行阻塞recvfrom调用从内核copy数据。linux epoll就是基于事件的就绪通知方式,省去了所有socket的扫描开销。epoll,kqueue比select高级,select是在内核里做轮询操作, epoll是使用回调机制, 消耗的资源更少. 套接字比较多的时候,每次select()都要通过遍历Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。epoll给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询。
5、异步IO(aio-read)是应用发出aio-read后马上返回,数据准备好后,由操作系统把数据copy到应用,并通知应用数据copy完成。
同步、异步、阻塞、非阻塞一般有这么几种组合:
同步非阻塞,典型代表是Java NIO
异步阻塞,典型代表是select,epoll
异步非阻塞,典型代表是aio
十二、排序、广度优先排序。
冒泡排序
插入排序
选择排序
快速排序和归并排序
归并排序
是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
时间复杂度计算
T(N) = a*T(N/b) + O(N^d)
a是过程发生次数,N/b是子问题,O(N^d)剩下的时间复杂度快速排序
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。时间复杂度为O(nl*ogn),最坏情况仍然是O(n^2),根据数据期望的算法,随机快排的时间复杂度为 O(n*logn)。
归并稳定,但是空间复杂度高
堆排不稳定,但是空间复杂度低
当时间复杂度一样,比较常数项操作,快排在三个中最好,大量数据实验证明快排最快。目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
七、Mysql
1、存储引擎 InnoDB,MYISAM,MEMORY,MRG_MYISAM.
2、索引,都是B+Tree InnoDB 聚集索引,MYISAM 非聚集索引,索引和数据单独存放。B+ Tree 一般深度3~4.内部的自适应hash索引。
3、InnoDB核心、InnoDB通过多个内存块+master Thread。1个Master Thread,lock THread,多个IO Thread
内存汇总,缓冲池 采用LRU算法,重做日志缓冲池,额外内存池。
增删改总是先修改缓冲池的页(产生脏页),再按照一定频率将缓冲池将脏页flush到文件中。4、锁机制: MyIsam 整表锁。InnnoDB 行级锁和表级锁, InnoDB的行锁是实现在索引上的,而不是所在物理行记录上。如果访问没有命中索引,也无法使用行锁,将退化为表锁。所以InnoDB务必建好索引,否则锁粒度较大,会影响并发
共享锁,排它锁,意向共享锁(IS)意向排他锁(IX)
意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
5、缓冲池
缓冲池中数据页的类型:索引页、数据也、Undo页,插入缓冲,InnoDB存储锁信息。
log_buffer,
6、MVCC多版本并发控制提升写性能,事务时读取unDo中的数据
MVCC是在Repeateable Read 和read COMMIT事务级别。
InnoDB实现MVCC,存储的每一行有额外的3个字段。
DB_TRX_ID,6byte,每处理一个事务自增1.
DB_ROW_PTR,7byte,指向回滚段一条undo log记录。
DB_ROWID: 6byte
每次事务开始,DB_TRX_ID都会增加。
在insert操作时,创建时间 = DB_ROW_ID,这时,“删除时间 ”是未定义的。
在update操作时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的DB_ROW_ID。
在delete操作时,相应数据行的“创建时间”不变,删除时间 = 该事务的DB_ROW_ID。
select操作对两者都不修改,只读相应的数据。普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。
Repeatable Read 可重复读 InnoDB默认事务隔离级别,MVCC读取事务刚开始的版本。
关键特性:插入缓冲和两次写(共享表空间)
7、脏页和脏读:脏页是缓冲池已被修改的页,未刷新到磁盘,即实例内存的页和磁盘的页是不一致的,在刷新到磁盘中。
脏读是指在缓冲池中还为提交的数据。脏读是发生在Readn UNCOMMITTED不可重复读,幻度,采用Next KeyLock算法,对索引的索淼不只锁住扫描到的索引而且还锁住这些索引的覆盖范围,对于这个范围的插入修改都不允许,避免不可重复读。
1、ES和Solr区别:
Solr更快一些。但是当实时建立索引的时候,Solr会产生IO阻塞,查询性能变差,此时ES更有优势。
随着数据量的增加,Solr的搜索效率会变低,而ES没有明显的变化。
Solr采用Zookeeper进行分布式管理,而ES自身带有分布式协调管理功能。
Solr支持更多的格式的数据,而ES仅支持JSON格式的数据。
Solr更适用于传统搜索应用(如电商平台),而ES更适用于实时搜索应用2、ES优点:ES采用倒排索引+ FST压缩和PostingList压缩提升索引效率
联合索引采用调表和bist按位与操作获得。
3、读写区别写的时候,去主分片写,读的时候从任意分片读。
4、近实时搜索,动态索引,先放入文件系统缓存(大概1分钟一次),利用translog来持久化(30分钟OR事务日志过大),合并段操作。
1、、Redis 快的主要原因是:
完全基于内存
数据结构简单,对数据操作也简单
使用多路 I/O 复用模型
Redis的持久化,AOP操作
2、Redids是哨兵机制,
1)Raft选举出一个哨兵,
2)每个哨兵每1秒发送一次命令,监测节点存活。
3)每个哨兵节点每2秒和其他哨兵痛点。
4)每个哨兵每10秒向主节点发送Info,获取从节点信息,
Redis Cluster设置集群。Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis管道,批量发送3、Redis的类型:list,map,zset。
4、Redis的用法:分布式锁,消息队列和通知,阻塞队列,位操作,限流
4、Redis主从同步是如何实现的,落盘机制,Redis抛弃机制
1) Redis持久化机制是aof和snapshot。snapshot机制是定时对比差异写入,但是断的时候可能丢失。aof类似binlog形式,aof会导致性能变差,可以在从节点开启aof。
2)Redis主从同步,2.8以前是从向主发个sync命令,然后主执行bgsave 生成.rdb 给从节点,从开始拷贝数据,然后主每次执行命令都采用广播的形式来同步从节点。2.8以后用psync,
3)Redis内存不够了,按配置的规则删除,LRU算法删除有配置失效时间的,随机删除,删除生存时间最近的一个,不删除,报错。5、Redis lua
Redis命令的计算能力并不算很强大,使用Lua语言则可以在很大程度上弥补Redis的这个不足。在Redis中,执行Lua语言是原子性,也就是说Redis执行Lua的时候是不会被中断的,具备原子性,这个特性有助于Redis对并发数据一致性的支持。
6、一致性Hash原理:
一致性hash算法将Hash函数的值域空间组织成一个大的圆环,假设Hash函数的值域空间为0~ 2^32-1(即HASH值是一个32位的无符号整数),整个值域空间按照顺时针方向进行组织。当新节点加入进来的时候,放在相应的位置,只会影响相邻的两个节点。同时引入每个IP地址的虚拟节点来解决Key分布不均匀的问题
6、Redis Cluster
Redis Cluster设计要点
redis cluster在设计的时候,就考虑到了去中心化,去中间件,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
那么redis 是如何合理分配这些节点和数据的呢?
Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做
哈希槽 (hash slot)
的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16
算法来取模得到所属的slot
,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384
。注意的是:必须要
3个以后
的主节点,否则在创建集群时会失败,我们在后续会实践到。所以,我们假设现在有3个节点已经组成了集群,分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用
哈希槽 (hash slot)
的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
- 节点A覆盖0-5460;
- 节点B覆盖5461-10922;
- 节点C覆盖10923-16383.
如下图所示:
那么,现在我想设置一个key ,比如叫
my_name
:set my_name yangyi
按照redis cluster的哈希槽算法:
CRC16('my_name')%16384 = 2412
。 那么就会把这个key 的存储分配到 A 上了。同样,当我连接(A,B,C)任何一个节点想获取
my_name
这个key时,也会这样的算法,然后内部跳转到B节点上获取数据。这种
哈希槽
的分配方式有好也有坏,好处就是很清晰,比如我想新增一个节点D
,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D
上,我会在接下来的实践中实验。大致就会变成这样:
- 节点A覆盖1365-5460
- 节点B覆盖6827-10922
- 节点C覆盖12288-16383
- 节点D覆盖0-1364,5461-6826,10923-12287
同样删除一个节点也是类似,移动完成后就可以删除这个节点了。
所以redis cluster 就是这样的一个形状:
Redis Cluster主从模式
redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。
上面那个例子里, 集群有ABC三个主节点, 如果这3个节点都没有加入从节点,如果B挂掉了,我们就无法访问整个集群了。A和C的slot也无法访问。
所以我们在集群建立的时候,一定要为每个主节点都添加了从节点, 比如像这样, 集群包含主节点A、B、C, 以及从节点A1、B1、C1, 那么即使B挂掉系统也可以继续正确工作。
B1节点替代了B节点,所以Redis集群将会选择B1节点作为新的主节点,集群将会继续正确地提供服务。 当B重新开启后,它就会变成B1的从节点。
不过需要注意,如果节点B和B1同时挂了,Redis集群就无法继续正确地提供服务了。
Redis可扩展集群搭建
1. 主动复制避开Redis复制缺陷既然Redis的复制功能有缺陷,不妨放弃Redis本身提供的复制功能,我们可以采用主动复制的方式来搭建我们的集群环境。所谓主动复制是指由业务端或者通过代理中间件对Redis存储的数据进行双写或多写,通过数据的多份存储来达到与复制相同的目的,主动复制不仅限于 用在Redis集群上,目前很多公司采用主动复制的技术来解决MySQL主从之间复制的延迟问题,比如Twitter还专门开发了用于复制和分区的中间件gizzard(https://github.com/twitter/gizzard) 。
主动复制虽然解决了被动复制的延迟问题,但也带来了新的问题,就是数据的一致性问题,数据写2次或多次,如何保证多份数据的一致性呢?如果你的应用 对数据一致性要求不高,允许最终一致性的话,那么通常简单的解决方案是可以通过时间戳或者vector clock等方式,让客户端同时取到多份数据并进行校验,如果你的应用对数据一致性要求非常高,那么就需要引入一些复杂的一致性算法比如Paxos来保证 数据的一致性,但是写入性能也会相应下降很多。
通过主动复制,数据多份存储我们也就不再担心Redis单点故障的问题了,如果一组Redis集群挂掉,我们可以让业务快速切换到另一组Redis上,降低业务风险。2. 通过presharding进行Redis在线扩容
通过主动复制我们解决了Redis单点故障问题,那么还有一个重要的问题需要解决:容量规划与在线扩容问题。我们前面分析过Redis的适用场景是全部数据存储在内存中,而内存容量有限,那么首先需要根据业务数据量进行初步的容量规划,比如你的业务数据需 要100G存储空间,假设服务器内存是48G,至少需要3~4台服务器来存储。这个实际是对现有 业务情况所做的一个容量规划,假如业务增长很快,很快就会发现当前的容量已经不够了,Redis里面存储的数据很快就会超过物理内存大小,如何进行 Redis的在线扩容呢?Redis的作者提出了一种叫做presharding的方案来解决动态扩容和数据分区的问题,实际就是在同一台机器上部署多个Redis实例的方式,当容量不够时将多个实例拆分到不同的机器上,这样实际就达到了扩容的效果
SpringMVC AOP机制
Aspect 界面, join point连接点,point cut 切点, advice(增强) target 目标。SpringMVC 通过动态代理和反射
1.IOC 依赖注入
2. IOC容器的初始化过程
资源定位,即定义bean的xml-------》载入--------》IOC容器注册,注册beanDefinition
IOC容器的初始化过程,一般不包含bean的依赖注入的实现,在spring IOC设计中,bean的注册和依赖注入是两个过程,,依赖注入一般发生在应用第一次索取be an的时候,但是也可以在xm中配置,在容器初始化的时候,这个bean就完成了初始化。
通过工厂模式创建Bean,通过反射来加载数据注入。,构造器、接口、set注入,我们常用的是set注入.AOP切面,实现的原理是代理
面向切面编程,在我们的应用中,经常需要做一些事情,但是这些事情与核心业务无关,比如,要记录所有update*方法的执行时间时间,操作人等等信息,记录到日志,
过滤器和拦截器
Filter过滤器是依赖于Serverlet容器,跟springmvc框架没有关系
interceptors 是基于AOP切面请求的拦截器
在面试中,经常会问,说说你对spring IOC和AOP的理解,问题很宽泛,似乎不知道从何说起。回答思路:1.先用通俗易懂的话解释下何为IOC和AOP---------》2.各自的实现原理-----------》3.自己的项目中如何使用SpringMVC的流程?
(1)用户发送请求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。SpringMvc初始化过程
1.首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
2.其 次,在web.xml中会提供有
contextLoaderListener
。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener
会监听到这个事件,其contextInitialized
方法会被调用,在这个方法中,spring会初始 化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext
,这是一个接口类,确切的说,其实际的实现类是XmlWebApplicationContext
。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的 context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE
为属性Key,将其存储到ServletContext中,便于获取;3.再 次,
contextLoaderListener
监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这里是DispatcherServlet,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请 求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE
先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个 parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方 法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext
。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为 Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet 就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些 bean。总结
- bean由BeanDefinition定义,BeanDefinition注册到BeanDefinitionRegistry中
- BeanFactory是“懒加载”机制,获取bean的时候才去查找,若无,则创建
- InstantiationAwareBeanPostProcessor 处理时机为bean实例化前后,可以拦截bean的实例化
- Prototype型,在创建bean之前先将beanName放在一个Set中(在ThreadLocal中)创建完成移除。如果Set已存在该beanName,则说明存在循环引用,如A -> B -> ... -> A
- Singleton型,singletonsCurrentlyInCreation集合在BeanFactory中,创建之前若此集合中存在该beanName则循环引用
- 如果bean是含参构造函数,则先解决参数依赖,若此时参数依赖当前bean则直接报错,因当前bean无法实例化,也就无法提前暴露到singletonFactories中,所以容器无法解决
- 属性依赖的bean,通过获取提前暴露到singletonFactories的singletonFactory,调用方法获取bean实例,完成依赖
- Aware接口完成系统注入,BeanFactory包括3个:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware,自定义可以通过BeanPostProcessor完成
- InitializingBean接口及自定义initMethodName属性完成bean的初始化
- BeanPostProcessor 处理时机为bean实例化之后,初始化前后,可以处理并替换bean(AOP实现)
- DisposableBean接口、AutoCloseable接口及destroyMethodName属性完成bean的销毁工作
- 容器中保存的是bean的包装BeanWrapper
服务治理:
所谓的 ServiceMesh,其实本质上就是上面提到的模式三:主机独立进程模式,这个模式其实并不新鲜,业界 (国外的 Airbnb 和国内的唯品会等) 早有实践,那么为什么现在这个概念又流行起来了呢?我认为主要原因如下:
上述模式一和二有一些固有缺陷,模式一相对比较重,有单点问题和性能问题;模式二则有客户端复杂,支持多语言困难,无法集中治理的问题。模式三是模式一和二的折中,弥补了两者的不足,它是纯分布式的,没有单点问题,性能也不错,应用语言栈无关,可以集中治理。
微服务化、多语言和容器化发展的趋势,企业迫切需要一种轻量级的服务发现机制,ServiceMesh 正是迎合这种趋势诞生,当然这还和一些大厂 (如 Google/IBM 等) 的背后推动有关。
模式三 (ServiceMesh) 也被形象称为边车 (Sidecar) 模式,如下图,早期有一些摩托车,除了主驾驶位,还带一个边车位,可以额外坐一个人。在模式三中,业务代码进程 (相当于主驾驶) 共享一个代理 (相当于边车),代理除了负责服务发现和负载均衡,还负责动态路由、容错限流、监控度量和安全日志等功能,这些功能是具体业务无关的,属于跨横切面关注点 (Cross-Cutting Concerns) 范畴。
1、限流的原理和算法:
滑动窗口计数器算法,有突刺的问题,前10ms消耗了所有的请求,AtomicLong算法
漏洞算法:用一个定时的调度任务,每10ms处理一次人那屋,无法应对突发的流量。
令牌桶算法:用Guava RateLimit,在多少秒内生成多少个令牌。acquire和tryAcquire来获取限流。
分布式限流:
利用redis的List,定时任务放入rightpush限流速度+leftpop,获取list。或者使用Zset实现(每个key里面),然后用range。
信号量限流:
Semaphore 控制同时有多少个线程在运行。sentinel 目前抽象出了 Metric 指标统计接口,底层可以有不同的实现,目前默认的实现是基于 LeapArray 的滑动窗口,后续根据需要可能会引入 reactive stream 等实现。
SlotChain 采用链的结构,前3个统计,后面的来判定如何处理。滑动窗口计数器算法+令牌桶算法算法(不断生成令牌、请求获取令牌)+ 漏桶算法(顶部流量随机进入,内部随机写入)。
在SpringAop切面拦截访问,用个单例生成桶,然后不断获取或者计数
漏桶算法 (ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
集群可以利用Redis的inc算法。2、 Hystrix
1)线程隔离
Hystrix在用户请求和服务之间加入了线程池。
Hystrix为每个依赖调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。线程数是可以被设定的。
原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。
2、熔断:
熔断器是位于线程池之前的组件。用户请求某一服务之后,Hystrix会先经过熔断器,此时如果熔断器的状态是打开(跳起),则说明已经熔断,这时将直接进行降级处理,不会继续将请求发到线程池。熔断器相当于在线程池之前的一层屏障。每个熔断器默认维护10个bucket ,每秒创建一个bucket ,每个blucket记录成功,失败,超时,拒绝的次数。当有新的bucket被创建时,最旧的bucket会被抛弃。熔断器的状态机:
Closed:熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;
Open:熔断器打开状态,此时对下游的调用都内部直接返回错误,不走网络,但设计了一个时钟选项,默认的时钟达到了一定时间(这个时间一般设置成平均故障处理时间,也就是MTTR),到了这个时间,进入半熔断状态;
Half-Open:半熔断状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;
一、Dubbo原理:
缺省协议采用单一长连接和NIO异步通讯。程池核心线程数为:200,最大线程数为200
1\连接个数:单连接 连接方式:长连接 传输协议:TCP 传输方式:NIO 异步传输
1)client一个线程调用远程接口,生成一个唯一的ID(比如一段随机字符串,UUID等),Dubbo是使用AtomicLong从0开始累计数字的
2)将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象callback,全部封装在一起,组成一个对象object
3)向专门存放调用信息的全局ConcurrentHashMap里面put(ID, object)
4)将ID和打包的方法调用信息封装成一对象connRequest,使用IoSession.write(connRequest)异步发送出去
5)当前线程再使用callback的get()方法试图获取远程返回的结果,在get()内部,则使用synchronized获取回调对象callback的锁, 再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态。
6)服务端接收到请求并处理后,将结果(此结果中包含了前面的ID,即回传)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到ID,再从前面的ConcurrentHashMap里面get(ID),从而找到callback,将方法调用结果设置到callback对象里。
7)监听线程接着使用synchronized获取回调对象callback的锁(因为前面调用过wait(),那个线程已释放callback的锁了),再notifyAll(),唤醒前面处于等待状态的线程继续执行(callback的get()方法继续执行就能拿到调用结果了),至此,整个过程结束。
Dubbo 从上往下看9层,又分为Biz层,框架核心RPC层,负责远程传输带remoting层。
支持rmi协议,hessian协议、很多协议。二、 Zookeeper工作原理:
CAP理论
在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性(Consistence) (等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
- 分区容错性(Network partitioning)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)
根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。
对于zookeeper来说,它实现了A可用性、P分区容错性、C中的写入强一致性,丧失的是C中的读取一致性。
ZK Node节点属性:
1)持久节点:2)持久顺序节点:2)临时节点:3)临时顺序节点。
持久节点需要手动进行删除,可以存放一些配置类的信息,同时有watch机制,用来进行服务发现。
临时节点是根据会话来的,当会话结束以后,临时节点会删除,临时节点一般用于做分布式锁。
» 领导者(leader),负责进行投票的发起和决议,更新系统状态
» 学习者(learner),包括跟随者(follower)和观察者(observer),follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票
» Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度
» 客户端(client),请求发起方ZooKeeper 的核心是原子广播,这个机制保证了各个 Server 之间的同步。实现这个机制的协 议叫做 ZAB 协议(Zookeeper Atomic BrodCast)。 ZAB 协议有两种模式,它们分别是崩溃恢复模式(选主)和原子广播模式(同步)。
保证事务的顺序一致性,zookeeper 采用了递增的事务 id 号(zxid)来标识事务。所有 的提议(proposal)都在被提出的时候加上了 zxid。
半数选举,所以ZK集群要为奇数台。Zookeeper提供一个多层级的节点命名空间(节点称为znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper不能用于存放大量的数据,每个节点的存放数据上限为1M。
通知Watch:
client端会对某个znode建立一个watcher事件,当该znode发生变化时,这些client会收到zk的通知,然后client可以根据znode变化来做出业务上的改变等。
八、消息队列
- RocketMQ支持异步实时刷盘,同步刷盘,同步Replication,异步Replication
- Kafka使用异步刷盘方式,异步Replication
- kafuka的图
- 这里只有3台机器(b0,b1,b2),每台机器既是Master,也是Slave。具体来说,比如机器b0,对于partition0来说,它可能是Master;对应partition1来说,它可能又是Slave。
- 一台机器同时是Master和Slave。在RocketMQ里面,1台机器只能要么是Master,要么是Slave。这个在初始的机器配置里面,就定死了。其架构拓扑图如下:
Master/Slave/Broker概念上的差异
通过上面2张图,我们已经可以直观看出2者的巨大差异。反映到概念上,虽然2者都有Master/Slave/Broker这3个概念,但其含义是不一样的。
Master/Slave概念差异
Kafka: Master/Slave是个逻辑概念,1台机器,同时具有Master角色和Slave角色。
RocketMQ: Master/Slave是个物理概念,1台机器,只能是Master或者Slave。在集群初始配置的时候,指定死的。其中Master的broker id = 0,Slave的broker id > 0。
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。
RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种
写磁盘方式:
1)异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,
吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入
2)同步刷盘方式:在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻
通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
namesrv VS zk
1、我们可以对比下kafka和rocketMq在协调节点选择上的差异,kafka通过zookeeper来进行协调,而rocketMq通过自身的namesrv进行协调。
2、kafka在具备选举功能,在Kafka里面,Master/Slave的选举,有2步:第1步,先通过ZK在所有机器中,选举出一个KafkaController;第2步,再由这个Controller,决定每个partition的Master是谁,Slave是谁。因为有了选举功能,所以kafka某个partition的master挂了,该partition对应的某个slave会升级为主对外提供服务。
3、rocketMQ不具备选举,Master/Slave的角色也是固定的。当一个Master挂了之后,你可以写到其他Master上,但不能让一个Slave切换成Master。那么rocketMq是如何实现高可用的呢,其实很简单,rocketMq的所有broker节点的角色都是一样,上面分配的topic和对应的queue的数量也是一样的,Mq只能保证当一个broker挂了,把原本写到这个broker的请求迁移到其他broker上面,而并不是这个broker对应的slave升级为主。
4、rocketMq在协调节点的设计上显得更加轻量,用了另外一种方式解决高可用的问题,思路也是可以借鉴的。
特性 ActiveMQ RabbitMQ RocketMQ Kafka 单机吞吐量 万级,比 RocketMQ、Kafka 低一个数量级 同 ActiveMQ 10 万级,支撑高吞吐 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 topic 数量对吞吐量的影响 topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 时效性 ms 级 微秒级,这是 RabbitMQ 的一大特点,延迟最低 ms 级 延迟在 ms 级以内 可用性 高,基于主从架构实现高可用 同 ActiveMQ 非常高,分布式架构 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 消息可靠性 有较低的概率丢失数据 基本不丢 经过参数优化配置,可以做到 0 丢失 同 RocketMQ 功能支持 MQ 领域的功能极其完备 基于 erlang 开发,并发能力很强,性能极好,延时很低 MQ 功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
二分查找算法
/**
* 使用递归的二分查找
*title:recursionBinarySearch
*@param arr 有序数组
*@param key 待查找关键字
*@return 找到的位置
*/
public static int recursionBinarySearch(int[] arr,int key,int low,int high){
if(key < arr[low] || key > arr[high] || low > high){
return -1;
}
int middle = (low + high) / 2; //初始中间位置
if(arr[middle] > key){
//比关键字大则关键字在左区域
return recursionBinarySearch(arr, key, low, middle - 1);
}else if(arr[middle] < key){
//比关键字小则关键字在右区域
return recursionBinarySearch(arr, key, middle + 1, high);
}else {
return middle;
}
}
时间复杂度logN 空间复杂度logN
实现atoi算法
难点:1、实现空判断 2、实现符号判断 3、去除字符串 4、核心判断超过了IntegerMax
public static long atoi(String str) throws Exception {
long value = 0;
boolean negative = false;
if(str == null || "".equals(str) ){
throw new Exception("the str cannot be null!");
}
for(int i = 0; i='0' && str.charAt(i)<='9') {
value = value*10 + str.charAt(i)-'0';
if (value > Integer.MAX_VALUE) {
throw new Exception("OUT OF INTEGER RANGE");
}
}else {
throw new NumberFormatException("not an integer!");
}
}
}
return negative==true ? -1*value:value;
}
有序数组的求值,利用双向指针处理
public int handle (int[] nums) {
if(nums==null || nums.length==0)
return 0;
// result的缩写,最后的返回值
int res = 0;
//i是前指针;j是后指针
int i = 0;
int j = nums.length - 1;
while (i <= j) {
int num1=Math.abs(nums[i]);
int num2=Math.abs(nums[j]);
if (num1 > num2) {//移动i
// 这两个数的绝对值不相等
res += 1;
//过滤掉相邻的绝对值相等的数
while(i<=j && Math.abs(nums[i])==num1)
i++;
} else if (num1 < num2) {// 这两个数的绝对值不相等
res += 1;
while(i<=j && Math.abs(nums[j])==num2) //过滤掉相邻的绝对值相等的数
j--;
} else {
res += 1;
while(i<=j && Math.abs(nums[i])==num1)//去重
i++;
while(i<=j && Math.abs(nums[j])==num2)//去重
j--;
}
}
return res;
}