今天上午发现调用账号中心接口异常,于是立马去线上查看日志发现了大量的异常信息,异常日志如下:
1 | Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure |
于是上网查了下相关文章,给出如下解释
MySQL默认连接存活时长为28800秒,即8小时。如果在wait_timeout期间内,数据库连接(java.sql.Connection)一直处于等待状态,MySQL就将该连接关闭。此时,数据库连接池仍然合法地持有该连接,当用该连接来进行数据库操作时,就报上述错误。
查了下线上账号中心库的 wait_timeout时间为259200s=3天
确实从异常日志文件也能看到异常信息:1
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 1,536,593,656 milliseconds ago. The last packet sent successfully to the server was 1,536,593,656 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
从日志文件看出有些数据库操作正常的,有些查询是异常的,所有猜测大概是数据库连接池中部分链接其实已经断开。
其中异常日志也给出了原因和响应的解决办法:
1 | or using the Connector/J connection property 'autoReconnect=true' to avoid this problem. |
方法一:
即使用autoReconnect=true来避免面这个这个问题,但是查了下配置文件中发现已经配置了此参数
1 | xxxauth.datasource.url=jdbc:mysql://xxxxx?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&rewriteBatchedStatements=TRUE&useSSL=false |
既然已经配置了那为什么还有这个问题,于是查了想相关文档
autoReconnect=true这个参数对 mysql5以上的版本不生效,现在公司 mysql 使用的都是5.6版本,所以这个方法不行。
方法二:
将数据库 wait_timeout调大,但是就算调大可能链接空闲等待的时间还是会超出等待时间还是不能解决实际问题。
方法三:
数据库连接池配置:testOnBorrow、testOnReturn、testWhileIdle属性,意义分别是取得、返回对象和空闲时,是否进行对象有效性检查,默认都是False关闭状态。只要都设置为True,并提供validationQuery语句即可保证数据库连接始终有效。
检查了下账号中心相关配置
账号中心是有配置但是为啥还是有这个问题?
于是 debug 了下DataSourceConfig这个类加载过程发现这么些个配置文件竟然只加载了前四项
有图有真相
xxxauth.datasource.tomcat开头的都解析不了咯。
继续看,发现如下代码
系统会在家 prefix + “.” + reflaxName的的配置信息,而这里的prefix是在类注解是这样的
1 | @ConfigurationProperties(prefix = "xxxauth.datasource") |
所以只会加载xxxauth.datasource.test-on-borrow这样的配置加载不到xxxauth.datasource.tomcat.test-on-borrow类似这样的配置。
将配置文件修改
1 | xxxauth.datasource.driver-class-name=com.mysql.jdbc.Driver |
再次 debug 发现 这些配置以及加载到了
Broker 整体架构图
消息体结构
消息存放的物理文件,是消息主体以及元数据存储的主体。每个broker上的 CommitLog被本机所有的Consumequeue共享,用于存储Producer端写入的消息主体内容,消息内容不是定长的,文件顺序写,随机读。单个文件大小默认1G 可以配置,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
commitlog存储单元结构图
消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;
consumerQueue 存储单元格结构
消息索引文件,Index 索引文件提供了对 CommitLog 进行数据检索,提供通过 key 或者时间区间来查询 CommitLog 中的消息的方法。在实际的物理存储上,文件名则是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引
IndexFile结构分析
IndexHead:
Hash 槽:
Index 条目列表:
刷盘流程
(1) 同步刷盘:如上图所示,只有在消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。
(2) 异步刷盘:能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。
读取消息的ConsumeQueue文件也会加载到PageCache,读PageCache和内存速度差不多。
消息写到 Broker 后,直接返回客户端成功,消息数据异步到 Slave 节点
优点:性能高,不需要等到消息同步直接返回。适合能容忍消息丢失的场景
缺点:这种可能会导致消息丢失
消息写到Broker 后,需要将消息同步复制到 Slave 节点才返回成功
优点:能够保证消息绝对不丢失,保证高可用,这种适合和金融相关的业务
缺点:性能不高,因为需要将消息同步到 slave 才能能返回成功
名称 | 描述 | 优点 | 缺点 |
---|---|---|---|
单个 Master | 这种方式风险较大,一旦Broker 重启或者宕机时,会导致整个服务不可用,不建议线上环境使用 | ~ | ~ |
多 Master 模式 | 一个集群无 Slave,全是 Master,例如 2 个 Master 或者 3 个 Master, 消息分别写到不同的 master 节点上 | 配置简单,单个Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由与 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高 | 单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性收到影响 |
多 Master 多 Slave 模式,异步复制 | 每个 Master 配置一个 Slave,有多对Master-Slave,HA 采用异步复制方式,主备有短暂消息延迟,毫秒级。消息写入 Master 节点,异步复制到 Slave节点 | 即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为 Master 宕机后,消费者仍然可以从 Slave 消费,此过程对应用透明。不需要人工干预。性能同多 Master 模式几乎一样 | Master 宕机,磁盘损坏情况,会丢失少量消息 |
多 Master 多 Slave 模式,同步双写 | 每个 Master 配置一个 Slave,有多对Master-Slave,HA 采用同步双写方式,master和slave都写成功,向应用返回成功。 | 数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 | 性能比异步复制模式略低,大约低 10%左右,发送单个消息的 RT 会略高。目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能。 |
支持故障转移,自动将 slave 节点提升为 master 提供服务
]]>topic名称-分区数命名1
2
3
4//分布在不同的broker节点上
test-topic-0
test-topic-1
test-topic-2
partition文件存储方式
为了性能考虑,如果不分区每个topic的消息只存在一个broker上,那么所有的消费者都是从这个broker上消费消息,那么单节点的broker成为性能的瓶颈,如果有分区的话生产者发过来的消息分别存储在各个broker不同的partition上,这样消费者可以并行的从不同的broker不同的partition上读消息,实现了水平扩展。
总结一句话:可以多个 broker 同事操作,提高并行度
segment 是个逻辑上的概念,并不存在真实的 segment 文件
Segment 是由一个 .index 和 一个 .log文件组成的。所以从可看到一个上图一个 partition 存在一个或者多个 segment。
通过上面的图,可以了解到一个 partition 下存在多个 segment,一个 segment 由有一个.index和一个.log文件组成,如果不用这种方式,那可以使用一个.index和一个.log文件组成(类似RocketMQ中使用CommitLog文件来保存所有的数据文件,由多个 indexfile 来存储索引文件)。这样的坏处是,随着消息的不断写入这个文件,由于kafka的消息不会做更新操作都是顺序写入的,如果做消息清理的时候只能删除文件的前面部分删除,不符合kafka顺序写入的设计,如果多个segment的话那就比较方便了,直接删除整个文件即可保证了每个segment的顺序写入。
总结一句话:为了提高写入的效率,以及方便清除不需要的数据
存储了对应数据文件的部分offset,以及 position(表示具体消息存储在log中的物理地址)。可以看待 offset 并不是连续的,而是每隔 6 个 offset 存储一条索引数据。1
2
3
4
5
6offset: 1049 position: 16205
offset: 1065 position: 32410
offset: 1081 position: 48615
offset: 1097 position: 64820
offset: 1113 position: 81025
offset: 1129 position: 97230
因为index文件中并没有为数据文件中的每条消息都建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。这种存储方式叫做稀疏索引
也可以配置成稠密索引,带来的问题就是索引文件太大。但是查找效率会高一点
总结一句话:减小 index 文件大小,可以将 index 文件内容加载到内存中,从而减小占用内存空间大小
log数据文件中并不是直接存储数据,而是通过许多的message组成,message包含了实际的消息数据。
假如我们想要读取offset=1066的message,需要通过下面2个步骤查找。
在介绍 Netty之前一定要先搞明白同步和异步、阻塞和非阻塞、BIO、NIO、AIO 都是什么,然后有哪些优缺点,再来看 Netty 出现的原因,有哪些优势,使用场景是什么。这样循序渐进的学习,更容易理解。
同步:同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
举例:你去收发室问老大爷有没快递,老大爷说你等等,我找找,然后你就等啊等啊,可能 5s 分钟就找到了,可能一天才找到,你就一直等着知道大爷回答你
异步:异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
举例:你去收发室问老大爷有没快递,老大爷就说我找一下,找到了我给你打电话。然后你就直接走了,干自己的事去了
阻塞:是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
举例:你去收发室问大爷有没快递,在大爷没回复你之前,你就把自己‘挂起’啥都不干,知道大爷回复你。
非阻塞:在不能立刻得到结果之前,该调用不会阻塞当前线程
举例:你去收发室问大爷有没快递,然后你就跑去玩了,时不时的过来问一下是否有结果。
同步阻塞,服务器实现模式是一个连接一个线程,即客户端有连接请求是服务度就需要启动一个线程进行处理。
优点:编写简单,小请求量可以接受
缺点:针对高并发,超过100000的并发连接来说该方案并不可取,它所需要的线程资源太多,而且任何时候都可能存在大量线程处于阻塞状态,等待输入或者输出数据就绪,整个方案性能太差。
代码示例: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
52public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
System.out.println("启动服务器...");
serverSocket.bind(new InetSocketAddress("127.0.0.1", 8888));
while (true) {
System.out.println("等待客户端链接...");
//accept()方法阻塞,直到有新的连接
final Socket socket = serverSocket.accept();
System.out.println("客户已连接,创建新的线程处理");
new Thread(()->{
handle(socket);
}).start();
}
}
private static void handle(Socket socket) {
try {
byte[] bytes = new byte[1024];
System.out.println("读数据..");
//read block method
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
System.out.println("写数据..");
//write block method
socket.getOutputStream().write(bytes,0,len);
socket.getOutputStream().flush();
}catch (IOException ex) {
ex.printStackTrace();
}
}
}
-------
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
socket.getOutputStream().write("hello server".getBytes());
socket.getOutputStream().flush();
System.out.println("write over, wait for msg back");
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
同步非阻塞,服务器实现是一个线程处理多个请求连接,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮训到连接有 IO 请求就进行处理
NIO核心组件:
优点:使用 Java NIO可以让我们使用较少的线程处理很多连接,较少线程意味着减少了线程创建内存分配和线程上下文切换带来的开销。
缺点:编程复杂,需要处理各种问题,API 使用难度大,在高负载下可靠和高效地处理和调度I/O操作是一项繁琐而且容易出错的任务,使用 NIO编程很容易出错。
代码示例:单线程处理连接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
76
77
78
79
80
81
82
83
84
85
86
87
88public class NioServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8888));
// set block is false
serverSocketChannel.configureBlocking(false);
System.out.println("server is started, listen on :" + serverSocketChannel.getLocalAddress());
Selector selector = Selector.open();
//注册监听客户端连接事件
//注册 accept 监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//轮训查看,阻塞方法。监听是否有事件发生
System.out.println("轮训查看是否有监听事件发生...");
selector.select();
//监听到有哪些 key 事件发生
System.out.println("监听到事件发生...");
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
//需要将 key remove 掉不然下次轮训还会处理
iterator.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) {
if (key.isAcceptable()) {
//获得 channel
System.out.println("accept事件发生,建立一条通道");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
try {
//建立 channel
serverSocketChannel.accept();
//设置通道是否阻塞
serverSocketChannel.configureBlocking(false);
//在通道上放置一个 read 的监听事件
System.out.println("在 channel 通道上注册 read 事件...");
serverSocketChannel.register(key.selector(),SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
if(key.isReadable()) {
System.out.println("read事件发生...");
SocketChannel socketChannel = (SocketChannel) key.channel();
//分配内存
ByteBuffer buffer = ByteBuffer.allocate(512);
buffer.clear();
try {
//从通道读取数据
System.out.println("读取数据...");
int len = socketChannel.read(buffer);
if (len != -1) {
System.out.println(new String(buffer.array(),0,len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("hello client".getBytes());
//向通道写数据
socketChannel.write(bufferToWrite);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
----------
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
socket.getOutputStream().write("hello server".getBytes());
socket.getOutputStream().flush();
System.out.println("write over, wait for msg back");
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
代码示例:线程池处理连接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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103public class NioPoolServer {
ExecutorService pool = Executors.newFixedThreadPool(50);
private Selector selector;
public void initServer(int port) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// set block is false
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
System.out.println("server is started, listen on :" + serverSocketChannel.getLocalAddress());
selector = Selector.open();
//注册监听客户端连接事件
//注册 accept 监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server run success!");
}
public static void main(String[] args) throws IOException {
NioPoolServer server = new NioPoolServer();
server.initServer(8000);
server.listen();
}
public void listen() throws IOException {
while (true) {
System.out.println("selector 轮训是否有事件..");
// block method
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
handle(key);
}
}
}
private void handle(SelectionKey key) throws IOException {
if(key.isAcceptable()) {
System.out.println("有客户端链接..");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(key.selector(),SelectionKey.OP_READ);
}
if(key.isReadable()) {
System.out.println("客户端写入数据...");
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
pool.execute(new ThreadHandleChannel(key));
}
}
class ThreadHandleChannel implements Runnable {
private SelectionKey selectionKey;
public ThreadHandleChannel(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
public void run() {
System.out.println("读取客户端传入数据...");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//分配内存
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteOutputStream byteOutputStream = new ByteOutputStream();
try {
//从通道读取数据
int size = 0;
while ((size = socketChannel.read(buffer)) > 0) {
buffer.flip();
byteOutputStream.write(buffer.array(),0,size);
buffer.clear();
}
byte[] content = byteOutputStream.toByteArray();
ByteBuffer writeBuffer = ByteBuffer.allocate(content.length);
writeBuffer.put(content);
writeBuffer.flip();
//write data
socketChannel.write(writeBuffer);
if(size == -1){
socketChannel.close();
return;
}
selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_READ);
selectionKey.selector().wakeup();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
异步非阻塞,结果返回回调接口
示例代码: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
44public class AioServer {
public static void main(String[] args) throws IOException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService,2);
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
serverSocketChannel.bind(new InetSocketAddress(8888));
//此处非阻塞,执行完成后就干下一步,返回结果会调用CompletionHandler#completed来处理
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
public void completed(AsynchronousSocketChannel channel, Object attachment) {
serverSocketChannel.accept(null,this);
try {
System.out.println(channel.getRemoteAddress());
ByteBuffer allocate = ByteBuffer.allocate(1024);
channel.read(allocate, allocate, new CompletionHandler<Integer, ByteBuffer>() {
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(new String(attachment.array(),0,result));
channel.write(ByteBuffer.wrap("hello client".getBytes()));
}
public void failed(Throwable exc, ByteBuffer attachment) {
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000);
}
}
}
Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护的高性能的面向协议的服务器和客户端。Netty主要是对java的nio包进行的封装。本质是一个NIO框架,用于服务器通信相关的多种应用场景
事件驱动:例如 Client 端发送的是个连接操作或者读请求或者断开连接,Netty 服务端可以读这几种不同的事件做定制的处理
对 NIO 进行封装,开发者不需要关注 NIO 的底层原理,只需要调用 Netty 组件就能够完成工作。
对网络调用透明,从 Socket 建立 TCP 连接到网络异常的处理都做了包装。
对数据处理灵活, Netty 支持多种序列化框架,通过“ChannelHandler”机制,可以自定义“编/解码器”。
对性能调优友好,Netty 提供了线程池模式以及 Buffer 的重用机制(对象池化),不需要构建复杂的多线程模型和操作队列。
统一的API,适用于不同的协议(阻塞和非阻塞)
基于灵活、可扩展的事件驱动模型
高度可定制的线程模型
可靠的无连接数据Socket支持(UDP)
更好的吞吐量,低延迟
更低的资源消耗
最少的内存复制
不再因过快、过慢或超负载连接导致OutOfMemoryError
不再有在高速网络环境下NIO读写频率不一致的问题
完整的SSL/TLS和STARTTLS的支持
可用于受限环境下,如 Applet 和OSGI
详实的Javadoc和大量的示例集
示例代码:
server1
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
67public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
new EchoServer(8888).start();
}
public void start() throws InterruptedException {
final EchoServerHandler serverHandler = new EchoServerHandler();
//创建EventLoopGroup,处理事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker)
//指定所使用的NIO传输 Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//添加一个EchoServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
//EchoServerHandler标志为@Shareable,所以我们可以总是使用同样的实例
socketChannel.pipeline().addLast(serverHandler);
}
});
//异步的绑定服务器,调用sync()方法阻塞等待直到绑定完成
ChannelFuture future = b.bind().sync();
future.channel().closeFuture().sync();
} finally {
//关闭EventLoopGroup,释放所有的资源
group.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
}
//标识一个 ChannelHandler可以被多个Channel安全地共享 .Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
//将消息记录到控制台
System.out.println("Server received: " + buffer.toString(CharsetUtil.UTF_8));
//将接受到消息回写给发送者
ctx.write(buffer);
}
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
hbv ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//打印异常栈跟踪
cause.printStackTrace();
//关闭该Channel
ctx.close();
}
}
Client1
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
54public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture channelFuture = b.connect().sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1", 8888).start();
}
}
.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Client received: "+byteBuf.toString());
}
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks",CharsetUtil.UTF_8));
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
所用技术 | 功能 | 完成度 |
---|---|---|
Spring Boot | 框架 | 已完成 |
MySQL | 数据存储 | 已完成 |
Redis | 二级缓存 | X |
OpenResty | 代理 + 一级缓存 | 已完成 |
EhCache | 三级缓存 | 已完成 |
布隆过滤器 | 判断hash是否存在 | 已完成 |
1 |
|
大家可能都收到过这样的推广短信,点击上面链接后会跳转到指定的地址,大家有没想过背后的实现细节
我们可以从这些短链接请求看下原理
我们可以看到请求的http状态码是 302(重定向) 和 Location值具体跳转目标地址,浏览器拿到了得到这个长链接,发起重定向请求到目标地址
整体交互流程如下
这里有个问题http状态码, 301和302 有什么区别
通过观察发现上面提到的短链接能发现,它是由固定的短链接域名 + 长链接映射成的一串字母组成,那么长链接怎么才能映射成一串字母呢?
哈希算法非常多,我们如何选择呢? 很多人可能会想到MD5, SHA等算法,但是我们选择 hash 算法的时候需要考虑的,生成的性能,
和产生冲突的概率。我们不回去考虑反向解码的性能或者难度,因为我们之直接将生成的 hash 字符串和长链接映射,不需反向生成。
我们项目中使用Google 出品的 MurmurHash 算法,MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。与其它流行的哈希函数相比,对于规律性较强的 key,MurmurHash 的随机分布特征表现更良好
非加密意味着着相比 MD5,SHA 这些函数它的性能肯定更高(实际上性能是 MD5 等加密算法的十倍以上),也正是由于它的这些优点,所以虽然它出现于 2008,但目前已经广泛应用到 Redis、MemCache、Cassandra、HBase、Lucene 等众多著名的软件中
MurmurHash 提供了两种长度的哈希值,32 bit,128 bit,为了让网址尽可通地短,我们选择 32 bit 的哈希值,32 bit 能表示的最大值近 43 亿,对于中小型公司的业务而言绰绰有余。
对长链(https://yefan813.github.io/)做 MurmurHash 计算,得到的哈希值为 849756688,于是我们现在得到的短链为 固定短链域名+哈希值 = http://xxxx.cn/849756688
不过,你可能已经看出来了,通过 MurmurHash 算法得到的短网址还是很长啊,而且跟我们开头那个网址的格式好像也不一样。别着急,我们只需要稍微改变一个哈希值的表示方法,就可以轻松把短网址变得更短些。
我们可以将 10 进制的哈希值,转化成更高进制的哈希值,这样哈希值就变短了。我们知道,16 进制中,我们用 A~E,来表示 10~15。在网址 URL 中,常用的合法字符有 0~9、a~z、A~Z 这样 62 个字符。为了让哈希值表示起来尽可能短,我们可以将 10 进制的哈希值转化成 62 进制。具体的计算过程,我写在这里了。最终用 62 进制表示的短网址就是
解决思路,根据长链接生成hash字符串,然后去查找数据库会出现以下几种情况
其实这里没有做任何的缓存,所以请求全部落到数据库,很容易就能想到不能支持高并发,但还是压测一下装个 X
100线程 并发执行生成短链接接口
短链接跳转接口结果和上面差不多
服务使用 Ehcache 后,压测短链接跳转结果
TODO
TODO
TODO
TODO
问题:
问题1:如果是先修改数据库,再删除缓存,如果删除缓存失败,那么会导致数据库中的数据是新数据,缓存中的是旧数据
,数据不一致。
解决思路:先删除缓存,再修改数据库,如果删除缓存成功了,修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中
读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,先操作数据库,再删除缓存
1.如果先删除缓存,可能因为并发太大,数据库还没更新老的数据又被另外一个查询放入了缓存,这时候缓存是老数据,数据库是新数据。数据不一致。
2.但是如果先操作数据库,后删除缓存,如果删除缓存失败。缓存是老数据,数据库是新数据,但是删除缓存失败的概率比上面并发造成数据不一致的概率要小得多。但是具体还是要结合场景来考虑
1,因为很多时候缓存不是直接数据库直接取出来的值,很可能做了逻辑运算以后再存入数据库
如果这类的数据要更新,则需要将相关的数据都查出来再去计算更新,这样代价太高了。
2,如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存,但是问题在于,这个缓存到底会不会被频繁访问到?
1 | 举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次,100 次; 但是这个缓存在 1 分钟内就被读取了 1 次,有大量的冷数据 |
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算
mybatis、hibernate 就是懒加载思想
问题2:一个比较复杂的数据不一致问题
1,数据发生了变更,先删除缓存,然后要去修改数据库,此时还没修改
2,一个并发请求过来,去读缓存,发现缓存空了,然后去查询数据库,查到了修改前的值,放到缓存
这时候出现数据库和缓存不一致。
解决思路: 导致这种情况出现的原因是读写并发请求造成的,根据某个规则比如商品 ID 相同的请求路由到一台服务器上,可以尝试将读请求和更新请求进行串行化处理,
具体流程如下:
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;
如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
1,一个队列中已经存在一个更新操作 + 一个读取操作,则后续的读请求可以尝试等待一段时间从缓存读取,因为前面的更新会删除缓存,后面的读取请求会将
再次放入到缓存中,后续的读请求可以尝试等待一段时间从缓存读取,如果等待时间过了再尝试从数据库去拿
1,可能 数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。所以务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的
2,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作
3, 会存在热点数据都打到同一台机器上的,可能造成某台机器压力过大的问题。
]]>Spring Boot已经为很多的开源项目提供了很多的 starter项目,你也可以开发你自定义的 starter。再开始之前让我们先理解下Spring Boot是如何自动配置的,如果你已经知道 Spring Boot自动配置的过程可以直接调到创建自定义starter步骤。
当你启动 Spring Boot 应用的时候,Spring Boot 会检测一个特殊的文件,它是 spring-boot-autoconfigure 这个包内的META-INF/spring.factories
它里面的内容我们重点关注这里
这里是利用的spring SPI提供的扩展机制。
这里简单介绍下 SPI (全称Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
通俗一点讲就是SPI 就是服务方提供接口,具体的实现由其他方来实现,只需要将实现了的类全路径加入到文件中,服务方启动的时候就会把这些文件内容解析出来,由于配置了类全路径地址,直接利用反射机制将这些类初始化提供使用。
1 | <dependency> |
1 | public class HelloService { |
1 | //prefix = "hello" 引入 starter 的项目配置文件的前置是 hello |
1 | /** |
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yefan.hello.HelloAutoConfiguration |
1 | <dependency> |
1 | hello: |
1 | @Autowired |
源码地址
项目中每个类的作用在上图中都写得非常清楚,可以下载下载直接运行看下整体效果,最好能 Debug 一下看下详细的流程。
###参考文献:
https://www.cnblogs.com/xrq730/p/5721366.html
https://blog.csdn.net/caihaijiang/article/details/35552859
https://www.cnblogs.com/zrtqsk/p/3735273.html
首先随便打开一个网站
打开 console
输入
1 |
|
先抛出几个问题,然后带着问题一起看下 Mybatis官网如何解释这个问题。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致
1 | <select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”> |
第2种: 通过
1 | <select id="getOrder" parameterType="int" resultMap="orderresultmap"> |
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
这是来自 Mybatis 官网一段描述
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
如下:<mapper namespace="org.mybatis.example.BlogMapper">
Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
- 一种是通过注解绑定,就是在接口的方法上面加上@Select@Update等注解里面包含Sql语句来绑定
- 另外一种就是通过xml里面写SQL来绑定,在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名.
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
第一种是使用
标签,逐一定义列名和对象属性名之间的映射关系。第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
能,Mybatis不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询,多对一查询,其实就是一对一查询,只需要把selectOne()修改为selectList()即可;多对多查询,其实就是一对多查询,只需要把selectOne()修改为selectList()即可。
关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象,赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询,一部分列是A对象的属性值,另外一部分列是关联对象B的属性值,好处是只发一个sql查询,就可以把主对象和其关联对象查出来。
那么问题来了,join查询出来100条记录,如何确定主对象是5个,而不是100个?其去重复的原理是
标签内的 子标签,指定了唯一确定一条记录的id列,Mybatis根据 列值来完成100条记录的去重复功能, 可以有多个,代表了联合主键的语意。
同样主对象的关联对象,也是根据这个原理去重复的,尽管一般情况下,只有主对象会有重复记录,关联对象一般不会重复。
官方 resultMap 文档:
可以
在 Mybatis xml配置文件 <setting name="defaultExecutorType" value="SIMPLE"/>
在 mapper.xml文件中使用
SqlSessionFactory sqlSessionFactory=getSqlSessionFactory();
//可以执行批量操作的sqlSessionSqlSession openSession=sqlSessionFactory.openSession(ExecutorType.BATCH);
resultType: 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同时使用。
resultMap: 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用。
- 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
- 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置
;
- 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
1 | 1>com.roncoo.pay.service.trade.biz.impl.RpTradePaymentManagerBizImpl#completeSuccessOrder 银行返回订单支付成功,后调支付成功,还未执行方法,被拦截器拦截 |
调试对于排查 java 各种异常问题非常重要,相信本地调试大家都很熟悉,今天分享一下如何开启远程调试。
如果需要编译.java文件执行命令javac,生成.class文件件
1 | javac SynchronizedStudy.java |
执行编译过后的.class文件
如果当前类有包路径到包的根路径下执行
1 | java com.xxx.xx.SynchronizedStudy |
如果当前类没有包直接执行
1 | java SynchronizedStudy |
如果要启动远程 debug 端口
如果执行 java 文件
1 | java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5005 com.yefan.study.SynchronizedStudy |
如果执行 jar 文件
1 | java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5005 -jar SynchronizedStudy.jar |
启动成功会显示如下
Listening for transport dt_socket at address: 5005
### 远程调试命令参数说明**-Xdebug:** 启用调试特性。**-Xrunjdwp:** 在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。**transport:** 这里通常使用套接字传输。**server:** 如果值为 y,目标应用程序监听将要连接的调试器应用程序。否则,它将连接到特定地址上的调试器应用程序。**address:** 这是连接的传输地址。如果服务器为 n,将尝试连接到该地址上的调试器应用程序。否则,将在这个端口监听连接。**suspend:**如果值为 y,目标 VM 将暂停,直到调试器应用程序进行连接。如果值为 n,没有调试器连接则继续执行
Filter是servlet规范中定义的java web组件, 在所有支持java web的容器中都可以使用
Filter和Filter Chain是密不可分的, Filter可以实现依次调用正是因为有了Filter Chain
上图是Filter对请求进行拦截的原理图, 那么java web容器(以tomcat为例子)是如何实现这个功能的呢?
下面看下Filter和Filter Chain的源码
1 | // Filter |
正是因为Filter Chain在调用每一个Filter.doFilter()时将自身引用传递进去, 才实现了Filter的依次调用, 在Filter全部调用完之后再调用真正处理请求的servlet, 并且再次逆序回调Filter. 可能这么看还是不太明白是怎么实现Filter的顺序调用, 调用真正的servlet, 逆序调用Filter的, 一起看下Tomcat的源码就一目了然了.
在tomcat中Filter Chain的默认实现是ApplicationFilterChain, 在ApplicationFilterChain中最关键的方法就是internalDoFilter, 整个Filter流程的实现就是由这个方法完成.
1 | // internalDoFilter(只保留关键代码) |
Filter的正序调用的过程和调用真正的servlet的过程了, 但是Filter的逆序调用在哪里体现了呢?
1 | public class LogFilter implements Filter { |
那么在调用chain.doFilter之后就跳过了if语句从而调用了真正的servlet, 然后internalDoFilter方法就结束(出栈)了, 紧接着就是调用Log.info(“after”)了, 然后LogFilter的doFilter就结束了(也出栈了), 紧接着就是internalDoFilter中filter.doFilter(request, response, this)的结束然后return, 然后就是调用上一个filter的chain.doFilter()之后的代码, 以此类推.
因此Filter调用链的实现其实就是一个方法调用链的过程. 刚开始, Filter Chain每调用一个Filter.doFilter()方法就是向方法调用栈中进行压栈操作(代码上的体现就是执行Filter.doFilter之前的代码), 当Filter全部调用完成之后就调用真正处理请求的servlet, 然后由方法调用链自动进行出栈操作(代码上的体现就是执行Filter.doFilter之后的代码), 从而完成整个Filter的调用链. 因为Filter功能实现实际上就是利用了方法的压栈出栈, 所以可以在调用chain.doFilter之前将方法返回, 让容器不在调用servlet方法, 从而实现权限的控制, 关键词的过滤等功能.
Interceptor功能的实现主要是在Spring Mvc的DispatcherServelt.doDispatch方法中, 让我们来看看源码
1 | // Interceptor的源码 |
doDispatch
1 | doDispatch源码(只保留关键代码) |
其实看了doDispatch的关键代码, Spring Mvc对整个请求的处理流程已经很清楚了:
调用拦截器的前置方法 -> 调用处理请求的方法 -> 渲染模版 -> 调用拦截器的后置处理方法 -> 调用拦截器的完成方法
接下来看一看Spring Mvc是如何实现依次调用这么多拦截器的前置方法, 后置方法, 完成方法的。
进入到mapperHandler.applyPreHandle()方法中(调用拦截器的前置方法)
1 | boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { |
进入到mappedHandler.applyPostHandle()方法中(调用拦截器的后置方法)
1 | void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception { |
不管是否出异常triggerAfterCompletion方法始终会被调用
1 | void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) |
注:triggerAfterCompletion会逆序调用afterCompletion方法
看过以上三个方法之后, Spring Mvc如何处理拦截器的前置, 后置, 完成方法就一目了然了. 其实Spring Mvc就是将拦截器统一放到了拦截器数组中, 然后在调用真正的处理请求方法之前和之后正序或者倒序遍历拦截器, 同时调用拦截器的相应的方法. 最后不管是否正常结束这个流程还是出异常都会从成功的最后一个拦截器开始逆序调用afterCompletion方法
Filter 和 Inteceptor 调用流程
参考文档:
Oracle官网
在线工具:http://gceasy.io
GCViewer
##标记清楚算法(Mark-Sweep)
##复制算法(Copying)
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
缺点:没有内存碎片,但是整理内存比较耗时
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。
另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。
垃圾收集算法是垃圾收集器的理论基础,而垃圾收集器就是其具体实现。下面介绍HotSpot虚拟机提供的几种垃圾收集器。
Serial/Serial Old
最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。
ParNew
Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。
Parallel Scavenge
新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。
Parallel Old
Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。
CMS
Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。
G1
G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。
CPU 敏感
浮动垃圾
空间碎片
新生代和老年代垃圾收集器
参考文章:
]]>