浅拷贝,深拷贝
- 对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
线程池
核心,缓冲队列,最大线程数,存活时间,拒绝策略,线程名字
缓冲队列类型
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE
- synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
四种线程池
-
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } // corePoolSize=1,maximumPoolSize=1,keepAliveTime=0s
-
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } // corePoolSize=n,maximumPoolSize=n,keepAliveTime=0s
-
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } // corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s
-
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } // corePoolSize=n,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=0s
线程数大小设置
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
- IO密集型 = 2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)
- CPU密集型 = 核心Ncpu,最大Ncpu+1(常出现于线程中:复杂算法),比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。
优雅关闭线程池
如果你希望线程池中的等待队列中的任务不继续执行,可以使用shutdownNow()
方法,shutdownNow调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。
public class WaitqueueTest {
public static void main(String[] args) {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
for(int i = 1; i <= 100 ; i++){
workQueue.add(new Task(String.valueOf(i)));
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
executor.execute(new Task("0"));
// shutdownNow有返回值,返回被抛弃的任务list
List<Runnable> dropList = executor.shutdownNow();
System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
System.out.println("dropList size = " + dropList.size());
}
static class Task implements Runnable{
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println("task " + name + " is running");
}
System.out.println("task " + name + " is over");
}
}
}
输出结果如下
task 0 is running
workQueue size = 0 after shutdown
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
dropList size = 100
task 0 is over
从上述输出可以看到,只有任务0执行完毕,其他任务都被drop掉了,dropList的size为100。通过dropList我们可以对未处理的任务进行进一步的处理,如log记录,转发等;
线程池停止了,那还有一个运行的线程怎么办呢?怎么停止?
在程序中,我们是不能随便中断一个线程的,因为这是极其不安全的操作,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断导致数据不一致混乱的问题。正因此,JAVA里将Thread的stop方法设置为过时,以禁止大家使用。
一个线程什么时候可以退出呢?当然只有线程自己才能知道。
所以我们这里要说的Thread的interrrupt方法,本质不是用来中断一个线程。是将线程设置一个中断状态。
当我们调用线程的interrupt方法,它有两个作用:
- 如果此线程处于阻塞状态(比如调用了wait方法,io等待),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。
- 如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自己是否被中断,并做退出操作
注意:
-
如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。
-
如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。
线程池状态
shutdown:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
stop:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
分布式事务
-
XA
- 优缺点
- 优点:实现简单易懂
- 缺点:性能不理想,高并发场景下表现不佳
- 优缺点
-
2PC
-
准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
-
提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
-
缺点(同步阻塞、单点问题、脑裂等缺陷)
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
-
-
3PC
-
针对2PC的改进点
1、引入超时机制。同时在协调者和参与者中都引入超时机制。 2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
-
问题
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。
所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
-
CanCommit阶段
询问是否可以执行事务提交操作。yes or no
-
PreCommit阶段
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
-
DoCommit阶段
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。
-
-
TCC
-
优缺点
缺点:因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常恶心。
-
操作模式(类似2PC)
- Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。
-
Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
-
-
本地消息表
-
优缺点
缺点:这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,会导致如果是高并发场景咋办呢?咋扩展呢?
-
操作模式
-
A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
-
接着 A 系统将这个消息发送到 MQ 中去;
-
B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息;
-
B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
-
如果 B 系统处理失败了,那么就不会更新A的消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
-
这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。
-
-
-
可靠消息最终一致性
-
针对本地消息表的改进点
就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。
-
优缺点
-
操作模式
-
Prepared阶段
该阶段主要发一个消息到rocketmq,但该消息只储存在commitlog中,但consumeQueue中不可见,也就是消费端(订阅端)无法看到此消息。
-
确认阶段
该阶段主要是把prepared消息保存到consumeQueue中,即让消费端可以看到此消息,也就是可以消费此消息。
-
问题场景和解决方案
- 实际场景
- 扣款之前,发送预备消息prepare
- 发送预备消息成功后,执行本地扣款事物
- 扣款成功后,再发送确认消息
- 发送端(加钱业务)可以看到确认消息(commit消息或者rollback消息),消费此消息,进行加钱
- 异常1:如果发送预备消息失败,下面的流程不会走下去;
- 异常2:如果发送预备消息成功,但执行本地事务失败;这个也没有问题,因为此预备消息不会被消费端订阅到,消费端不会执行业务。
- 异常3:如果发送预备消息成功,执行本地事务成功,但发送确认消息失败;这个就有问题了,因为用户A扣款成功了,但加钱业务没有订阅到确认消息,无法加钱。这里出现了数据不一致。
RocketMq如何解决上面的问题,核心思路就是【状态回查】,也就是RocketMq会定时遍历commitlog中的预备消息。(解决了异常2和3,异常2是发rollback删除预备消息,异常3是补发确认消息)
回查业务是否成功:设计一张Transaction表,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功时,Transaction中应当已经记录该TransactionId的状态为「已完成」。当RocketMq回查时,只需要检查对应的TransactionId的状态是否是「已完成」就好,而不用关心具体的业务数据。
- 实际场景
-
-
-
最大努力通知
- 操作模式
-
系统 A 本地事务执行完之后,发送个消息到 MQ;
-
这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
-
要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
-
- 操作模式
CAP理论
CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
-
p代表分区容错,它的意思是区间通信可能失败,比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
-
c代表一致性,表示写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1,接下来,用户操作读操作就会得到v1,这叫一致性。
-
Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性
为什么只能保证两个特性?
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
BASE理论
base是cap中ap方向的延申,无法做到强一致(ap中的一致是强一致),采用适合的方式达到最终一致性。思想包含3方面
- 1、Basically Available(基本可用):基本可用是指分布式系统在出现不可 预知的故障的时候,允许损失部分可用性,但不等于系统不可用。
- 2、Soft state(软状态):即是指允许系统中的数据存在中间状态,并认为 该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数 据副本之间进行数据同步的过程存在延时。
- 3、Eventually consistent(最终一致性):强调系统中所有的数据副本,在 经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统 保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式一致算法
太难,回头再说
callable和future
FutureTask实现RunnableFuture,RunnableFuture继承Runnable,Future
FutureTask里面有一个volatile state变量,通过这个值来界定任务是否执行完毕
set:把值设置到outcome,之后unpark唤醒别的线程
get:park挂号线程
reference:https://www.cnblogs.com/wang-meng/p/10149068.html
mybatis
-
工作原理
JDBC有四个核心对象: (1)DriverManager,用于注册数据库连接 (2)Connection,与数据库连接对象 (3)Statement/PrepareStatement,操作数据库SQL语句的对象 (4)ResultSet,结果集或一张虚拟表
而MyBatis也有四大核心对象: (1)SqlSession对象,该对象中包含了执行SQL语句的所有方法【1】。类似于JDBC里面的Connection 【2】 (2)Executor接口,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。类似于JDBC里面的Statement/PrepareStatement。 (3)MappedStatement对象,该对象是对映射SQL的封装,用于存储要映射的SQL语句的id、参数等信息。 (4)ResultHandler对象,用于对返回的结果进行处理,最终得到自己想要的数据格式或类型。可以自定义返回类型。
-
问题
#{}是预编译处理,${}是字符串替换。 Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; Mybatis在处理${}时,就是把${}替换成变量的值。 使用#{}可以有效的防止SQL注入,提高系统安全性。
引用
-
软引用
JVM
在分配空间时,若果Heap
空间不足,就会进行相应的GC
,但是这次GC
并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure
),JVM
会尝试第二次GC
,回收软引用关联的对象。 -
弱引用
弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
高并发三把剑
高并发三把剑:限流,缓存,降级
限流
1.单位时间段内调用量来限流
2.系统的并发调用程度来限流
-
单机
-
rateLimiter
RateLimiter r = RateLimiter.create(5);设置每秒放置的令牌数为5个 boolean acquire = rateLimiter.tryAcquire();
-
漏斗
漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照固定频率从队列头取出请求进行处理。如果请求量大,则会导致队列满,那么新来的请求就会被抛弃。
-
令牌
令牌桶算法则是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不同,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。
-
漏斗和令牌的区别
-
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率;
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;
-
-
-
semaphore
Semaphore semaphore = new Semaphore(1); semaphore.acquire(); semaphore.release();
- 管理一系列许可证,即state共享资源值;
- 每acquire一次则state就减1一次,直到许可证数量小于0则阻塞等待;
- 释放许可的时候要保证唤醒后继结点,以此来保证线程释放他们所持有的信号量;
- 是Synchronized的升级版,因为Synchronized是只有一个许可,而Semaphore就像开了挂一样,可以有多个许可;
-
-
分布式
假设10min5个请求
维护一个redis队列,每次放入的值有过期时间,新来一个请求判断队列数小于限流数,插入;大于限流数,判断队列的第一个和当前时间间隔是不是1min,如果是拒绝,不是,删除以前的数据,同时插入本次请求
hash冲突解决方式
-
开发地址法
- 线性探测
- 平方探测
- 随机探测
-
链地址法
-
建立公共溢出区
溢出区存储存储所有哈希冲突的数据
-
再哈希法
ArrayBlockingQueue原理
final Object[] items;//存储队列元素
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
public ArrayBlockingQueue(int capacity) { this(capacity, false); }
底层是数组,默认采用非公平实现
notEmpty表示”锁的非空条件”(锁的共享资源是队列,也就是队列的非空条件)。当某线程想从队列中获取数据的时候,而此时队列中的数据为空,则该线程通过notEmpty.await()方法进行等待,当其他线程向队列中插入元素之后,就调用notEmpty.signal()方法进行唤醒之前等待的线程。
同理,notFull表示“锁满的条件“。当某个线程向队列中插入元素,而此时队列已满时,该线程等待,即阻塞通过notFull.wait()方法;其他线程从队列中取出元素之后,就唤醒该等待的线程,这个线程调用notFull.signal()方法。
put操作,满了notfull.await,没满插入,插入成功之后notEmpty.signal
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
take操作,队列为空notEmpty.await,不为空取出之后,notfull.signal
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
guaua cache原理
1.核心数据结构
2.读写并发控制
CPU 100%,内存暴涨排查
-
找到最耗费CPU的进程
top -c显示进程运行列表,键入P按照使用率排序,拿到进程的PID===10765
-
找到最耗费CPU进程中的线程
top -Hp 上一步的进程PID拿到线程PID===10804
-
查看线程堆栈,定位线程在做什么,定位对应代码
将线程PID转换为16进制,printf ‘%x\n’ 10804(第二步的线程PID)===0x2a34,查看堆栈信息 jstack 10765 grep ‘0x2a34’ -C5 –color
swap区
当物理内存不足时,拿出部分硬盘空间当swap区使用(swap机制的初衷是为了缓解物理内存用尽而选择直接粗暴OOM进程的尴尬。但是swap效率低,而且满了之后会杀进程,危害太大)
当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在SWAP分区中,这个过程称为SWAP OUT。当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把SWAP分区中的数据交换回物理内存中,这个过程称为SWAP IN。
当然,swap大小是有上限的,一旦swap使用完,操作系统会触发OOM-Killer机制,把消耗内存最多的进程kill掉以释放内存。(危害不是一般的大啊,直接干掉占用最多内存的进程)
http和https
-
http特点
- 无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作
- 无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
- 基于请求和响应:基本的特性,由客户端发起请求,服务端响应
- 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性
-
https特点
1.基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
基于zk的服务发现与注册
通过Znode和Watcher机制
- Znode包含data(znode存储的数据),ACL(Znode的访问权限),stat(Znode的各种源数据包括ZXID,版本号,时间戳,数据长度等),child(子节点引用)
- Watch机制:和指定Znode所绑定的监听器,当这个Znode发生变化,也就是在这个Znode上进行了数据的写操作之后会异步向请求watch的客户端发送通知
/dubbo:这是dubbo在ZooKeeper上创建的根节点;
/dubbo/com.foo.BarService:这是服务节点,代表了Dubbo的一个服务;
/dubbo/com.foo.BarService/providers:这是服务提供者的根节点,其子节点代表了每一个服务真正的提供者;
/dubbo/com.foo.BarService/consumers:这是服务消费者的根节点,其子节点代表每一个服务真正的消费者;
-
工作流程
-
服务提供方启动
服务提供者在启动的时候,会在ZooKeeper上注册服务。所谓注册服务,其实就是在ZooKeeper的/dubbo/com.foo.BarService/providers节点下创建一个子节点,并写入自己的URL地址,这就代表了com.foo.BarService这个服务的一个提供者。
-
服务消费方启动
服务消费者在启动的时候,会向ZooKeeper注册中心订阅自己的服务。其实,就是读取并订阅ZooKeeper上/dubbo/com.foo.BarService/providers节点下的所有子节点并设置Watch,并解析出所有提供者的URL地址来作为该服务地址列表。
同时,服务消费者还会在ZooKeeper的/dubbo/com.foo.BarService/consumers节点下创建一个临时节点,并写入自己的URL地址,这就代表了com.foo.BarService这个服务的一个消费者。
-
消费者远程调用提供者
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一个提供者进行调用,如果调用失败,再选另一个提供者调用。
-
增加服务提供者
增加提供者,也就是在providers下面新建子节点。一旦服务提供方有变动,zookeeper就会把最新的服务列表推送给消费者。
-
减少服务提供者
所有提供者在ZooKeeper上创建的节点都是临时节点,利用的是临时节点的生命周期和客户端会话相关的特性,因此一旦提供者所在的机器出现故障导致该提供者无法对外提供服务时,该临时节点就会自动从ZooKeeper上删除,同样,zookeeper会把最新的服务列表推送给消费者。
-
zk宕机
消费者每次调用服务提供方是不经过ZooKeeper的,消费者只是从zookeeper那里获取服务提供方地址列表。所以当zookeeper宕机之后,不会影响消费者调用服务提供者,影响的是zookeeper宕机之后如果提供者有变动,增加或者减少,无法把最新的服务提供者地址列表推送给消费者,所以消费者感知不到。
-
单位转换
bit(位),b(字节),k(千),M(兆)
1Byte=8Bit
1KB=1024Byte
1M=1024KB
为什么重写toString
Object类的toString方法返回一个字符串,该字符串由类名@类对象的实例的哈希码无符号的十六进制表示组成
如果重写了,就可以输出重写内容了
为什么重写hash和equals
Object类的hashcode返回的是对象的内存地址
public class Object {
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
}
重写
public class Student {
private String name;// 姓名
private String sex;// 性别
private String age;// 年龄
private float weight;// 体重
private String addr;// 地址
// 重写hashcode方法
@Override
public int hashCode() {
int result = name.hashCode();
result = 17 * result + sex.hashCode();
result = 17 * result + age.hashCode();
return result;
}
// 重写equals方法
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Student)) {
// instanceof 已经处理了obj = null的情况
return false;
}
Student stuObj = (Student) obj;
// 地址相等
if (this == stuObj) {
return true;
}
// 如果两个对象姓名、年龄、性别相等,我们认为两个对象相等
if (stuObj.name.equals(this.name) && stuObj.sex.equals(this.sex) && stuObj.age.equals(this.age)) {
return true;
} else {
return false;
}
}
}
逃逸分析优化
-
锁消除
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
-
标量替换
对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
-
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。
四种引用的区别
强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象 。 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
虚拟机栈的两种异常
如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow内存溢出异常。
如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM泄露异常。
泛型
都是概念的东西,可以参考:https://blog.csdn.net/koko2015c/article/details/77619606
频繁full gc
大对象,年轻代放不下,扔到老年代了(对象的年龄有问题,其实这些对象有可能下次gc就没有了),不断的扔老年代,当老年代仍不下了,full gc,循环往复
进程和线程的区别
进程是并发执行的程序在执行过程中分配和管理资源的基本单位。
线程是进程的一个执行单元,是比进程还要小的独立运行的基本单位。
-
地址空间
- 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段,堆栈段和数据段。
- 线程没有独立的地址空间,同一进程的线程共享本进程的地址空间。
-
上下文切换
-
进程
用户空间,系统调用两种,进程切换需要保存当前进程的虚拟内存,栈;
-
线程
在一个进程有多线程的情况下,这些进程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时说不需要修改的。另外线程也有自己的私有数据,比如栈和程序计数器等,这些在上下文切换时需要保存。
-
C10K问题
最初的服务器都是基于进程/线程模型的
,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程又是操作系统最昂贵的资源
,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么操作系统是无法承受的
。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大。这就是C10K问题的本质
。
实际上当时也有异步模式,如:select/poll模型,这些技术都有一定的缺点,如selelct最大不能超过1024,poll没有限制,但每次收到数据需要遍历每一个连接查看哪个连接有数据请求。
解决:epoll模型
多线程的适用场景
一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。 其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。 而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。
- 引出的问题:Redis需不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率呢?
- 首先,我们可以肯定的说,Redis不需要提升CPU利用率,因为Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。
- [redis4.0] Redis确实是一个I/O操作密集的框架,但是,提升I/O利用率,并不是只有采用多线程技术这一条路可以走!Redis并没有采用多线程技术,而是选择了多路复用 I/O技术。
- [redis6.0] Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。 [多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。]
tcp握手[不稳定的信道建立稳定的连接]
- 握手为什么不是两次[如果客户端发送的SYN丢失了或者其他原因导致Server无法处理,是什么原因?]
- 如果SYN包在传输的过程中丢失,此时Client段会触发重传机制,可以通过 tcp_syn_retries 这个配置项来决定。重传时间指数级增长,如果最后还是没有回应,客户端会timeout返回
- 上面说的是一直在尝试连接,如果第一个发syn产生了滞留,又发一次syn,之后服务端响应。这个时候之前的syn包达到服务端,如果是两次握手,那就进入了等待数据状态。这时服务端任务是两次连接,而客户端其实只是一次连接,造成状态不一致。
- 如果是三次握手,服务端收不到ack包,那就不算连接建立,反正不管做什么都是为了网络信道的可靠性。
- 挥手为什么不是三次
- 客户端发送完FIN包,进入等待关闭状态WAIT。服务端收到发送ACK包进入终止等待状态,但是服务端还可以发送未发送的数据,同时客户端也可以继续接收数据。等服务端发送完之后发送FIN包,客户端回复ACK包,客户端进入超时等待状态,超过超时时间客户端关闭连接。服务端收到ACK包,立即关闭连接。
- 如果不是四次,那服务端发送的FIN包,客户端没有回复ACK,或者回复的ACK包状态丢失,那服务端会一直处于最后确认状态
- 还有如果服务端没有收到ACK会重发FIN。客户端会重发ACK包,刷新超时时间/
- 客户端为什么要延迟关闭,确保服务端收到ACK包。
- 丢包 + 乱序
- tcp协议为每个连接建立一个发送缓冲区, 从建立连接后的第一个序列号为0,之后序列号递增。发送数据的时候,从发送缓冲区取一部分数据组成报文,在tcp协议中附带序列号和长度 。
- 接收端在收到报文后回复ACK=序列号+长度=下一包数据的起始序列号。
- 如果丢包,接收端要发送端重传,接收端进行补齐。
- tcp优化[半连接多的原因分析,杜绝这个半连接就是优化]
- 服务端在握手时收到SYN以后没有回复SYN+ACK的连接,那么Server每收到新的SYN包,都会创建一个半连接,然后将这个半连接加入到半连接的队列(syn queue)中,syn queue的长度又是有限的,可以通过tcp_max_syn_backlog进行配置,当队列中积压的半连接数超过了配置的值,新的SYN包就会被抛弃。
- 可能是因为恶意的Client在进行SYN Flood攻击。会造成半连接很多。
- 首先Client以较高频率发送SYN包,且这个SYN包的源IP不停的更换,对于Server来说,这是新的链接,就会给它分配一个半连接
- [解决被攻击的方式]首先Server收到SYN包,不分配资源保存Client的信息,而是根据SYN计算出Cookie值,然后将Cookie记录到SYN ACK并发送出去,Server会根据这个Cookies检查ACK包的合法性,合法则创建连接。
- tcp存在的问题
- 队头阻塞:有的数据包没有达到,接收包一直等待,阻塞后续的请求
- 三次握手的时候,如果距离远会很慢,额外的 1.5RTT