期中测试答案 这些问题,你都答对了吗?
你好,我是丁威。
这节课我们来回答一下上节课的问题,希望通过梳理这些问题,可以进一步加深你对知识的理解。
1. MyCat数据库中间件与ShardingJDBC在架构思想上有什么差异?
MyCat数据库中间件的设计理念是代理模式,这是一种高度中心化的设计,所有的路由配置都会存储在MyCat数据库中间件中。具体的工作机制是,所有客户端将所有请求发送到MyCat,然后MyCat根据配置的路由规则将请求发送到真实的后端数据库。
这种架构模式在数据量较少的情况下确实能提升性能,但如果请求数继续增加,就有可能出现问题,你可以先看看下面这张图片:
如果数据库需要存储的数据逐步增加,我们就要对后端数据库进行扩容,这个无可厚非。但如此一来,存储的数据会越来越多,基于代理模式,所有的请求都必须经过MyCat这个中心节点。一旦这个节点出现故障,将导致系统不可用。
而ShardingJDBC采取了去中心化的设计,系统架构如下图所示:
从这张图可以看到,ShardingJDBC是将所有的路由信息嵌入到应用进程中,在客户端进行路由计算,然后连接后端真实的数据库。如果要对后端数据库进行扩容,只需要更新一下路由注册信息就可以了,不会带来额外的资源损耗,也不存在单点故障。
2. 在订单中心有创建订单、查询订单两个微服务。其中,查询订单必须同时支持“按商家”和“按用户”两个维度。为了应对双十一这种大促场景,在数据存储和数据读写方面你会如何进行架构设计?
通常为了应对流量高峰,我们会首先在入口流量引用MQ,然后在数据库存储领域引入分库分表。同时,为了避免查询请求对写入性能的影响,会引入读写分离机制。但是我们不能简单地在数据库层面使用读写分离,因为分库分表后,使用Join等复杂语句通常会遇到性能瓶颈,所以应该将数据库数据实时同步到Elasticsearch中,最后的架构设计大概如下所示:
3. 红黑树的左右旋转、染色其实是不需要死记硬背的。下面这棵二叉树,你会怎样操作让它符合红黑树的定义呢?
红黑树的染色和左右旋转是有窍门的。
我们先看染色,需要变换染色的情况,通常是相关的三个节点组成的结构是一个父节点带两个节点,我们需要将其中一个黑色的叶子结点的颜色传递到父节点。
然后再来谈左旋和右旋。其实它们的原理就是通过降低树的高度来实现平衡,但调整后需要确保根节点比左节点大,比右节点小。左旋或者右旋的触发场景为:连续两个红颜色节点。
我们说回到这道题。0037的左节点为0035,0035的左节点为0025,那这三个节点,从0025视角来看,有3“层”,我们要把它们变成两层。一个简单的方法就是我们从这3个节点中找到中间值节点,这里是0035节点,然后把比中间节点小的节点(0025)放入到0035的左节点,把比中间件节点大的节点(0037)放入到中间值节点(0035)的右子树即可。
经过旋转后,我们可以得到下面这张图:
但这个时候还是有两个连续的红色节点,这样会导致每条链路的黑色节点数量不一致。又因为0025,0037这两个节点已经在同一层次了,这个时候我们可以调整节点的颜色让它符合红黑树的定义,我们只需要将0037的颜色传递给它的父节点就可以了,最终为:
4. JUC定时调度线程池底层的实现原理是什么?如果要管理上万个定时任务,需要怎么处理呢?
JUC的线程调度底层的队列存储结构是PriorityQueue,又叫做最小堆,它的具体的实现原理如下。
- 在将调度任务提交到线程池之前,首先计算出下一次需要执行的时间戳,通过时间戳来计算优先级,将其存入最小堆中,这样就确保了最先需要执行的调度任务位于最小堆的顶部(也就是根节点)。
-
然后开一个定时任务,拿队列中第一个元素和当前时间进行比较:
-
如果下一次执行时间大于等于当前时间,则将队列中第一个元素(调度任务)从队列中移除,投入线程池中执行。
- 如果下一次执行时间小于当前时间,则不处理,因为队列中最小的待执行任务都还没有到执行时间,其他任务一定也是这样。
但是如果需要调度的任务很多(例如上万个),这些任务的触发时间只相隔个几秒,这种通过一个线程一个一个检测任务的方式,很容易导致任务调度执行不精确。
为了解决这个问题,业界引入了时间轮算法,它的意思是引入时间轮,每一个轮代表一个时间刻度,如下图所示:
例如,图中每一个刻度代表1s。因为有8个格子,就代表8秒,也就是说,这个时间轮可以管理延迟调度时间在8s内的任务。如果想要增加延迟调度的时间范围,只要增加格子的数量即可。
具体怎么操作呢?
- 首先计算要调度的任务的延迟时间,将它换算成相应的刻度放到指定的格子里,每一个格子都会维护一个任务列表。
- 然后使用一个线程(类似于钟表中的秒针)以固定频率(单个格子代表的时间长度)驱动指针,指针指向的格子内所有的任务到期后,一次性出发所有的定时任务,执行精度并不会随着要触发的任务数量增加而发生变化。
5. 如何复用线程?如何优雅地停止一个线程?
一个线程走向消亡的触发条件是线程的run方法的结束,因此,要复用一个线程的方法是不让run方法结束。我们通常采用的方法是:“while(true) + 从阻塞队列中获取任务”。因为如果阻塞任务中没有任务可执行,线程会阻塞,不会浪费CPU资源;而一旦阻塞队列中有新的任务加入,就能立马唤醒线程执行对应的任务了。
要停止一个线程,我们不能直接调用线程的stop方法,而是需要在run方法加入中断检测机制。在run方法中如果检测线程中断位被设置,则跳出循环,结束run方法的运行,从而达到停止线程的目的。
6. 多线程编程中,线程与线程之间有两种主要的关系:互斥与协作。你能结合自己的实际工作场景分别举例说明吗?
互斥,通常是在多个线程要访问同一个公共资源,而这个公共资源又不允许多个线程同时访问时出现。这个时候需要引入锁来保护共享资源,达到多线程串行访问的效果。这在实际生产中非常常见,例如,多个线程要更新数据库中的同一行数据时,就涉及到锁的使用。
协作其实看出是对一个任务进行步骤拆解,然后让这些步骤可以并行执行,以此提升性能。
例如,MQ的消息拉取线程与队列负载均衡这两个线程就是典型的协作模式。这两个线程共同完成MQ消息的拉取,它们使用一个公共的阻塞队列相互协作。负载均衡算法负责计算队列的负载情况,向阻塞队列中生产任务;而消息拉取线程负责从阻塞队列中获取任务,并执行具体的消息拉取动作。如果阻塞队列中没有任务,那么消息拉取线程就要阻塞,在生产出新的拉取任务后,负载均衡线程会再通知消息拉取线程。这两者之间是相互协作,相互制约的关系。
7. 锁的底层数据结构是什么?
它包括锁的持有者线程、锁的重入次数、阻塞队列和条件等待队列。
锁的持有者线程拥有对被保护资源的操作权,而且在整个过程中,支持对锁进行多次锁定。
同步阻塞队列存放的都是竞争锁失败的线程,主要表征的是线程之间的竞争和互斥。
条件等待队列中存储的是因为某一个条件不满足而需要阻塞的线程,通常需要被其他线程主动唤醒,主要表征的是线程的协作。
8. 为什么Object.wait方法会释放占用的锁?如果锁没有被释放,会产生什么影响?
我们用前面讲的面包工程的例子加以说明。例如下面一段代码是面包生产者向仓库中生产面包的代码实现:
public void put(Bread bread) throws InterruptedException {
synchronized (breads) {//锁定资源
while (breads.size() == this.maxCapacity) {
// 没有可存储的空间,阻塞生产者,等待有存储空间后再继续
breads.wait();
log();
}
breads.add(bread);
breads.notifyAll();
}
}
生产者线程调用put方法后,是因为仓库(具体是List breads)中没有容量,所以被阻塞。但如果不释放锁,就没有线程能从breads中取出元素,这会导致breads一直没有剩余空间。所以只能是生产者自己释放锁,让其他协作线程(消费者线程)有机会运行。
9. 什么是NIO?为什么NIO能轻松支持上万个连接同时在线?
NIO的全名是同步非阻塞IO模型。NIO能轻松支持上万个连接同时在线,这得益于它的事件选择机制。NIO只需要少量IO线程(每一个IO线程内部会创建一个事件选择器)就可以服务上万个连接。因为每进行一次事件就绪选择,IO线程需要处理的只有那些就绪的连接,在IO层面没有就绪的连接是不需要进行处理的。这就节省了大量的线程资源,不会像BIO那样,一个连接不管当下是否有数据读写,都必须占用一个线程。
10. 我们在使用NIO构建的服务端时,如果服务端处理压力较大,可以在应用层采用快速失败拒绝连接。但是除此之外,在网络层,你还有什么办法限制服务端的流量呢?
在网络层,我们可以暂时停止注册读事件,这样这个连接就不会从网卡中读取数据了,数据会停留在底层Socket的读缓冲区。由于TCP内部拥有拥塞控制,如果接受端没有从网卡中读取数据,也就不会发送ACK确认到源端了。源端无法写入更多数据,这就在网络层实现了拥塞控制,实现了限流。
11. 通过NIO通道向网络中写数据之前,需要注册写事件吗?那什么时候需要注册写事件呢?
写数据之前不需要注册写事件,写事件一般是等底层NioSocketChannel的底层缓存区满了,无法再向网络中写入数据时,再注册通道的写事件,等待缓冲区空闲时通知应用程序继续将剩余数据写入到网络中。
12. 一个网络请求在发送端、接受端通常需要经历哪些步骤,Netty又是采用什么线程模型使这些步骤合理高效运作的?
一个网络请求发送与接收响应结果通常涉及编码、往网络中写数据(Write)、从网络中读取数据(Read)、解码、业务逻辑处理、发送响应结果和接受响应结果等步骤。过程图如下:
Netty的线程模型采取的是业界的主流线程模型,也就是主从多Reactor模型:
它的设计重点主要包括Netty Boss Group、Nettty Work Group、Business Thread Group线程组这三个线程组。 更详细的说明你可以参考第8讲。
好了,我们本节课的答疑就到这里了,如果有其他问题,欢迎留言与我互动,我们下节课再见。
- 秋天 👍(1) 💬(0)
然后开一个定时任务,拿队列中第一个元素和当前时间进行比较:如果下一次执行时间大于等于当前时间,则将队列中第一个元素(调度任务)从队列中移除,投入线程池中执行。如果下一次执行时间小于当前时间,则不处理,因为队列中最小的待执行任务都还没有到执行时间,其他任务一定也是这样。 大佬,感觉着说的有问题呢?大于当前时间不做处理,小于当前时间执行调度加入调度线程中把?
2022-08-03