0%

tomcat架构

web需要做的事情:

  • 协议解析
  • 业务处理
    因此tomcat设计了两个核心组件连接器Connector和容器Container来分别做两件事情。连接器负责对外交流,容器负责内部处理。

连接器

连接器和容器要结合起来对外提供服务,tomcat有个组件将他们组合起来service.一个tomcat可以有多个service,可以实现不同的端口号来访问同一台机器上部署的不同应用。
service

Connector处理的工作又可以分为网路通信,协议解析,与容器交互。因此tomcat设计了3个组件和实现这个3个功能,分别是Endpoint,Processor,Adapter
Endpoint负责提供字节流给processor,processor解析相关的协议,封装成Tomcat Request对象给AdapterAdapter负责提供ServletRequest对象给容器。因为io模型和应用层协议可以自由组合,tomcat将EndpointProcessor放在一起考虑,设计了一个组件ProtocolHandler
连接器

tomcatio模型为reactor模式,Endpoint有两个子组件AcceptorSocketProcessorSocketProcessor提交到线程池来执行。

endpoint

容器

tomcat容器采用分层架构,每层处理响应的事情。分别是Engine,Host,Context和Wrapper.
这四种容器不是平行关系,是父子关系。

container

engine

根据Engine类上面的注释我们可以了解到,它在两种场景下产生作用:

  • 想通过拦截器来查询所有引擎对单个请求的处理
  • 想运行一个单http连接器,但是想支持所有的虚拟url请求
    通常我们是不需要使用engine的。

host

域名处理。tomcat可以根据不同的域名访问不同的context。可以根据不同的host转发到不同的context。

context

我们部署的每个web应用其实最后被加载为了一个context。每个context有不同的context-path。context中持有servelet,根据不同的url请求到响应的servlet进行处理。

wapper

具体处理业务逻辑的地方

定位一个servlet

定位一个servlet过程

责任链模式

tomcat所有容器都继承了一个相同的父类Container,从开始父类进行处理,处理完之后调用子Container。采用pipeline模式,pipeline持有一个value的链接。所以pipeline有一个getBasic方法,在value链的末端,负责调用子类的第一个value.

1
2
3
4
5
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}

value看起来跟filter的代码很像,value是tomcat私有的,filter是servlet规范支持的。

pipeline

hashmap 为什么设置桶个数大于8转为红黑树

时间和空间的平衡

为什么要转换

要弄明白这个问题,我们首先要明白为什么要转换,这个问题比较简单,因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
源码中有一段话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million

因为bin的大小几乎不能为8个,如果为8个说明hash取的不好,分配的过于不均匀

hashmap 如何把大小设置为2的幂次方

我们看hashmap源码发现有一个方法,

1
2
3
4
5
6
7
8
9
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

用于把bucket的大小设置为2的幂次方。
先来分析有关n位操作部分:先来假设n的二进制为01xxx…xxx。接着

对n右移1位:001xx…xxx,再位或:011xx…xxx

对n右移2为:00011…xxx,再位或:01111…xxx

此时前面已经有四个1了,再右移4位且位或可得8个1

同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。

最后再让结果n+1,即得到了2的整数次幂的值了。

因为int最大为32位,那么位移31位就可以了。这样从第一位开始所有位数都是1.

hashmap hash方法

我们可以看到,hashmap的hash方法是:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

图中可以看出,hashmap是允许key 为null的。

我们知道,平时hashcode()都是32位,我们看到最后的hash值是hash值的低位与高位做了异或运算。为什么要做额外的处理呢,我们查看下定位值的方法,发现只有hash值的低四位参加了运算,设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

附录:

java位运算

异或运算

^ ,二进制运算,相同为0,不同为1

hashmap计算hash时,有一个异或运算,为了方便把hash变的均匀。

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

与运算

&,二进制计算,只要有一个为0,就为0
常做取余算法。

1
System.out.println((129/128) ==(129 & 127));

或方法

| 二进制计算,两个数只要有一个为1则为1,否则就为0

非方法

~

<< 向左位移

针对二进制,转换成二进制后向左移动n位,后面用0补齐

>>(向右位移)

针对二进制,转换成二进制后向右移动n位,

>>>(无符号右移)

无符号右移,忽略符号位,空位都以0补齐
hash值右移获取高位

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

两数之和

题目见两数之和

最暴力的方法是两次for循环遍历,时间复杂度是O(n^2)
优化点在于怎么快速查询出来值。
采用hashMap的方式,key是hash存储,能够快速搜索到key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TowSum {

public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int j = target - nums[i];
if (map.containsKey(j)) {
return new int[]{map.get(j), i};
}
map.put(nums[i], i);
}
return null;
}
}

附录:

使用springboot redis,配置没有问题,但是一直报错netty.UnResolveAddressException。经过排查发现,springboot redis 需要引用netty 4.1.x 的包,项目中有4.0.x的netty依赖。排除掉依赖恢复正常。

首先我们看下Netflix官方给的图
图

注册中心eureka

开启eureka服务端很简单,只需要在添加注解@EnableEurekaServer即可。一般来说添加一个注解开启一个功能,主要是通过import导入一个configuration.我们可以看到@EnableEurekaServer注解其实是引入了EurekaServerConfiguration配置。我们看下里面配置了哪些内容。

首先我们看到,里面配置了EurekaServerConfigBeanConfiguration,这个类会配置一个一份EurekaServiceConfig,里面包含了所有配置的enruka server属性。
配置了一个eureka的看板,通过看板我们能看到有多少个服务接入了server.
配置了一些eureka nodes,用来做高可用集群使用。
配置了注册中心,我们主要来看下这个注册中心.

注册中心 PeerAwareInstanceRegistryImpl

注册

register()

注销

cancle()

续约

renew()

定时清理

evic()

集群

PeerEurekaNodes 包含了集群其他节点的信息
replicateToPeers.将应用状态更新到其他节点

注册接口

ApplicationResource

首先我们看下Netflix官方给的图
图

eureka client

引入eureka client 会自动配置EurekaClientAutoConfiguration。我们看下里面配置了什么内容。

EurekaClientAutoConfiguration 主要是生成了一个DiscoveryClient类。
DiscoveryClient.initScheduledTasks()初始化的过程中,会启动一些定时任务,来触发客户端操作

服务注册 续约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
private void initScheduledTasks() {
// 如果允许获取注册信息,那么拉取注册信息到本地
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}

//如果允许注册到eureka,
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

//定时续约
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);

// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize

statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}

@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};

if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}


//注册 instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}

服务下线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");

if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}

cancelScheduledTasks();

// If APPINFO was registered
if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}

if (eurekaTransport != null) {
eurekaTransport.shutdown();
}

heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();

logger.info("Completed shut down of DiscoveryClient");
}
}