开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

TCP漫谈之keepalive和time_wait

发表于 2020-04-07

TCP是一个有状态通讯协议,所谓的有状态是指通信过程中通信的双方各自维护连接的状态。

一、TCP keepalive

先简单回顾一下TCP连接建立和断开的整个过程。(这里主要考虑主流程,关于丢包、拥塞、窗口、失败重试等情况后面详细讨论。)

首先是客户端发送syn(Synchronize Sequence Numbers:同步序列编号)包给服务端,告诉服务端我要连接你,syn包里面主要携带了客户端的seq序列号;服务端回发一个syn+ack,其中syn包和客户端原理类似,只不过携带的是服务端的seq序列号,ack包则是确认客户端允许连接;最后客户端再次发送一个ack确认接收到服务端的syn包。这样客户端和服务端就可以建立连接了。整个流程称为三次握手。

建立连接后,客户端或者服务端便可以通过已建立的socket连接发送数据,对端接收数据后,便可以通过ack确认已经收到数据。

数据交换完毕后,通常是客户端便可以发送FIN包,告诉另一端我要断开了;另一端先通过ack确认收到FIN包,然后发送FIN包告诉客户端我也关闭了;最后客户端回应ack确认连接终止。整个流程成为四次挥手。

TCP的性能经常为大家所诟病,除了TCP+IP额外的header以外,它建立连接需要三次握手,关闭连接需要四次挥手。如果只是发送很少的数据,那么传输的有效数据是非常少的。

是不是建立一次连接后续可以继续复用呢?的确可以这样做,但这又带来另一个问题,如果连接一直不释放,端口被占满了咋办。为此引入了今天讨论的第一个话题TCP keepalive。所谓的TCP keepalive是指TCP连接建立后会通过keepalive的方式一直保持,不会在数据传输完成后立刻中断,而是通过keepalive机制检测连接状态。

Linux控制keepalive有三个参数:保活时间net.ipv4.tcp_keepalive_time、保活时间间隔net.ipv4.tcp_keepalive_intvl、保活探测次数net.ipv4.tcp_keepalive_probes,默认值分别是 7200 秒(2 小时)、75 秒和 9 次探测。如果使用 TCP 自身的 keep-Alive 机制,在 Linux 系统中,最少需要经过 2 小时 + 9*75 秒后断开。譬如我们SSH登录一台服务器后可以看到这个TCP的keepalive时间是2个小时,并且会在2个小时后发送探测包,确认对端是否处于连接状态。

之所以会讨论TCP的keepalive,是因为发现服器上有泄露的TCP连接:

1
2
3
4
复制代码# ll /proc/11516/fd/10
lrwx------ 1 root root 64 Jan 3 19:04 /proc/11516/fd/10 -> socket:[1241854730]
# date
Sun Jan 5 17:39:51 CST 2020

已经建立连接两天,但是对方已经断开了(非正常断开)。由于使用了比较老的go(1.9之前版本有问题)导致连接没有释放。

解决这类问题,可以借助TCP的keepalive机制。新版go语言支持在建立连接的时候设置keepalive时间。首先查看网络包中建立TCP连接的DialContext方法中

1
2
3
4
5
6
7
8
9
复制代码if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
setKeepAlive(tc.fd, true)
ka := d.KeepAlive
if d.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(tc.fd, ka)
testHookSetKeepAlive(ka)
}

其中defaultTCPKeepAlive是15s。如果是HTTP连接,使用默认client,那么它会将keepalive时间设置成30s。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

下面通过一个简单的demo测试一下,代码如下:

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
复制代码func main() {

wg := &sync.WaitGroup{}

c := http.DefaultClient
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
r, err := c.Get("http://10.143.135.95:8080")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
fmt.Println(err)
return
}

time.Sleep(30 * time.Millisecond)
}
}()
}
wg.Wait()
}

执行程序后,可以查看连接。初始设置keepalive为30s。

然后不断递减,至0后,又会重新获取30s。

整个过程可以通过tcpdump抓包获取。

1
复制代码# tcpdump -i bond0 port 35832 -nvv -A

其实很多应用并非是通过TCP的keepalive机制探活的,因为默认的两个多小时检查时间对于很多实时系统是完全没法满足的,通常的做法是通过应用层的定时监测,如PING-PONG机制(就像打乒乓球,一来一回),应用层每隔一段时间发送心跳包,如websocket的ping-pong。

二、TCP Time_wait

第二个希望和大家分享的话题是TCP的Time_wait状态。、

为啥需要time_wait状态呢?为啥不直接进入closed状态呢?直接进入closed状态能更快地释放资源给新的连接使用了,而不是还需要等待2MSL(Linux默认)时间。

有两个原因:

一是为了防止“迷路的数据包”,如下图所示,如果在第一个连接里第三个数据包由于底层网络故障延迟送达。等待新的连接建立后,这个迟到的数据包才到达,那么将会导致接收数据紊乱。

第二个原因则更加简单,如果因为最后一个ack丢失,那么对方将一直处于last ack状态,如果此时重新发起新的连接,对方将返回RST包拒绝请求,将会导致无法建立新连接。

为此设计了time_wait状态。在高并发情况下,如果能将time_wait的TCP复用,
time_wait复用是指可以将处于time_wait状态的连接重复利用起来。从time_wait转化为established,继续复用。Linux内核通过net.ipv4.tcp_tw_reuse参数控制是否开启time_wait状态复用。

读者可能很好奇,之前不是说time_wait设计之初是为了解决上面两个问题的吗?如果直接复用不是反而会导致上面两个问题出现吗?这里先介绍Linux默认开启的一个TCP时间戳策略net.ipv4.tcp_timestamps = 1。

时间戳开启后,针对第一个迷路数据包的问题,由于晚到数据包的时间戳过早会被直接丢弃,不会导致新连接数据包紊乱;针对第二个问题,开启reuse后,当对方处于last-ack状态时,发送syn包会返回FIN,ACK包,然后客户端发送RST让服务端关闭请求,从而客户端可以再次发送syn建立新的连接。

最后还需要提醒读者的是,Linux 4.1内核版本之前除了tcp_tw_reuse以外,还有一个参数tcp_tw_recycle,这个参数就是强制回收time_wait状态的连接,它会导致NAT环境丢包,所以不建议开启。

作者:陈晓宇

作者著作《云计算那些事儿:从IaaS到PaaS进阶》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开车了,JMC坐稳 (jfr)

发表于 2020-04-07

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

今天我们要开的车是,江铃汽车。全称Jiangling Motors Corporation,简称JMC。

一车在手,吃喝不愁。车作为男人的第二个老婆,拥有一辆JMC是所有成功男人的梦想。打开小小的车窗,任嘶吼的狂风揉捏自己的面庞,是一种让人无比享受的存在。

列车驶向何处,前方又是何方,全在掌舵者一念之间。
程序员掌舵,当然与众不同。我们今天的列车,就是jmc监控工具。

呸,撞车了。

与JMC撞车的,也是jmc,全称Java Mission Control,是故障排查的一大利器。

在世风日下的今天,强行扭正jmc的坐姿,也算是一股清流。它和jvisualvm一样,都被jdk给抛弃了,因为长得太胖,现在需要单独下载。

先看它的图标。

再看它的界面。

Flight Recorder

你可以看到,它是一个图形化的工具,生产环境中使用必然受到限制,你需要首先开通JMX才可以使用。但我习惯在压测的时候才用它。

因为它有一个Flight Recorder功能。

为什么里面这么多按钮,我仅仅提到Flight Recorder这个东西呢?因为其他东西都是点吧点吧就可以理解的,xjjdog要是再介绍一下的话,就像是智障一样。

但是Flight Recorder必须要详细描述下,因为智商不到80不好理解它。

Flight Recorder源自飞机的黑盒子,一看就是用来录制信息,然后事后分析的。在Java11中,它可以通过jcmd命令进行录制了。

主要有5个命令:configure、check、start、dump、stop。执行顺序的话,先start再dump,最后stop。例如:

1
2
3
4
5
复制代码jcmd <pid> JFR.start

jcmd <pid> JFR.dump filename=recording.jfr

jcmd <pid> JFR.stop

先来看下它的好处:

在保证低开销的基础上,JFR 提供的能力也令人眼前一亮。

例如:我们无需 BCI 就可以进行 Object Allocation Profiling,终于不用担心 BTrace 之类把进程搞挂了。

对锁竞争、阻塞、延迟,JVM GC、SafePoint 等领域,进行非常细粒度分析。甚至深入 JIT Compiler 内部,全面把握热点方法、内联、逆优化等等。

JFR 提供了标准的 Java、C++ 等扩展 API,可以与各种层面的应用进行定制、集成,为复杂的企业应用栈或者复杂的分布式应用,提供 All-in-One 解决方案。而这一切都是内建在 JDK 和 JVM 内部的,并不需要额外的依赖,开箱即用。

录制

录制谁好呢?还是录制一个tomcat吧。下图是录制了一分钟之后的结果,可以看到左边那一串长长的结果列表。

通过这些数据,可以清楚的了解到这一分钟时间内,整个操作系统以及JVM的所有数据情况。这花花绿绿的颜色,一看就知道是很厉害的样子。

线程

点击C2编译器线程,可以看到详细的热点类,以及方法内联后的代码大小。线程的Wait、Idea、Block等各种状态和时序,都能在这里看到。C2此时在疯狂运转。

内存

可以看到种类型的数据,每个时刻所申请的内存大小。这在排查一些内存泄漏问题的时候,不是一般的有用。

锁

锁实例可以看到期间出现的所有Lock实例,以及它的使用信息。

文件和Socket

当然,对文件和Socket的操作也淋漓尽致,一目了然。包括什么时候读了多少数据,从哪里读写的,都可以从栈信息里看到。

方法调用

方法调用信息和排行。

垃圾回收

对垃圾回收的信息监控也是比较详细的。比如什么时候发生了垃圾回收,用的什么垃圾回收器,耗时,甚至是发生的时机等,都可以在这里看到。

JIT

编译这里,显示了更加详细的JIT相关信息,包括生成后的CodeCache大小。

TLAB

神奇的是,它连TLAB区域的申请信息都给录制了,不得不说是一个神器。

TLAB的全称是Thread Local Allocation Buffer,JVM默认给每个线程开辟一个buffer区域,用来加速对象分配。这个buffer,就放在Eden区。

这个道理和Java语言中的ThreadLocal类似,避免了对公共区的操作,以及一些锁竞争。

End

JFR,全称Java Flight Recorder,我们今天对它的介绍,就结束了。

车已经到站了,请下车的乘客做好准备,祝您旅途愉快。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kotlin Coroutines Flow 系列(四) 线

发表于 2020-04-07

photo-of-woman-wearing-denim-jacket-2419423.jpg

七. Flow 线程操作

7.1 更为简化的线程切换

相对于 RxJava 多线程的学习曲线,Flow 对线程的切换友好地多。

在之前的 Kotlin Coroutines Flow 系列(一) Flow 基本使用 一文中曾经介绍过 Flow 的切换线程,以及 flowOn 操作符。

Flow 只需使用 flowOn 操作符,而不必像 RxJava 需要去深入理解 observeOn、subscribeOn 之间的区别。

7.2 flowOn VS RxJava 的 observeOn

RxJava 的 observeOn 操作符,接收一个 Scheduler 参数,用来指定下游操作运行在特定的线程调度器 Scheduler 上。

Flow 的 flowOn 操作符,接收一个 CoroutineContext 参数,影响的是上游的操作。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码fun main() = runBlocking {

flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.collect {
println("${Thread.currentThread().name}: $it")
}
}

flow builder 和 map 操作符都会受到flowOn的影响,并使用 Dispatchers.io 线程池。

再例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码val customerDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()

fun main() = runBlocking {

flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.map {
it+1
}
.flowOn(customerDispatcher)
.collect {
println("${Thread.currentThread().name}: $it")
}
}

flow builder 和两个 map 操作符都会受到两个flowOn的影响,其中 flow builder 和第一个 map 操作符跟上面的例子一样,第二个 map 操作符会切换到指定的 customerDispatcher 线程池。

7.3 buffer 实现并发操作

在 Kotlin Coroutines Flow 系列(二) Flow VS RxJava2 一文中,曾介绍 buffer 操作符对应 RxJava Backpressure 中的 BUFFER 策略。

事实上 buffer 操作符也可以并发地执行任务,它是除了使用 flowOn 操作符之外的另一种方式,只是不能显示地指定 Dispatchers。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码fun main() = runBlocking {
val time = measureTimeMillis {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
.buffer()
.collect { value ->
delay(300)
println(value)
}
}
println("Collected in $time ms")
}

执行结果:

1
2
3
4
5
6
kotlin复制代码1
2
3
4
5
Collected in 1676 ms

在上述例子中,所有的 delay 所花费的时间是2000ms。然而通过 buffer 操作符并发地执行 emit,再顺序地执行 collect 函数后,所花费的时间在 1700ms 左右。

如果去掉 buffer 操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码fun main() = runBlocking {
val time = measureTimeMillis {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
.collect { value ->
delay(300)
println(value)
}
}
println("Collected in $time ms")
}

执行结果:

1
2
3
4
5
6
kotlin复制代码1
2
3
4
5
Collected in 2039 ms

所花费的时间比刚才多了300多ms。

7.4 并行操作

在讲解并行操作之前,先来了解一下并发和并行的区别。

并发(concurrency):是指一个处理器同时处理多个任务。
并行(parallelism):是多个处理器或者是多核的处理器同时处理多个不同的任务。并行是同时发生的多个并发事件,具有并发的含义,而并发则不一定是并行。

RxJava 可以借助 flatMap 操作符实现并行,亦可以使用 ParallelFlowable 类实现并行操作。

下面,以 flatMap 操作符为例实现 RxJava 的并行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码        Observable.range(1,100)
.flatMap(new Function<Integer, ObservableSource<String>>() {
@Override
public ObservableSource<String> apply(Integer integer) throws Exception {
return Observable.just(integer)
.subscribeOn(Schedulers.io())
.map(new Function<Integer, String>() {

@Override
public String apply(Integer integer) throws Exception {
return integer.toString();
}
});
}
})
.subscribe(new Consumer<String>() {
@Override
public void accept(String str) throws Exception {

System.out.println(str);
}
});

Flow 也有相应的操作符 flatMapMerge 可以实现并行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码fun main() = runBlocking {

val result = arrayListOf<Int>()
for (index in 1..100){
result.add(index)
}

result.asFlow()
.flatMapMerge {
flow {
emit(it)
}
.flowOn(Dispatchers.IO)
}
.collect { println("$it") }
}

总体而言,Flow 相比于 RxJava 更加简洁一些。

该系列的相关文章:

Kotlin Coroutines Flow 系列(一) Flow 基本使用

Kotlin Coroutines Flow 系列(二) Flow VS RxJava2

Kotlin Coroutines Flow 系列(三) 异常处理

Kotlin Coroutines Flow 系列(五) 其他的操作符

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

想读Spring源码?先从这篇「 极简教程」开始吧

发表于 2020-04-07

为什么要阅读源码?这是一个有趣的问题,类似的问题还有,为什么要看书?为什么要爬山?

这也是一个哲学问题,我想每个人都有不同的答案,下面我是对阅读源码好处的一些思考。
(PS:也欢迎你在评论区留言补充)

阅读源码的好处

1.知其然知其所以然

这是一句 IT 人都很熟悉的话,在开源软件越来越多,并且升级越来越快的今天,如果说还有什么是重要的,那一定是知道它(开源软件)的核心原理和执行流程,这样就能为我们快速定位并修复问题提供宝贵的时间。

2.让自己变得更加优秀

学习源码可以让我们站在巨人的肩膀上,你可以学习源码中的优秀编码技巧和巧妙的设计思路,以及设计模式的落地应用,还有一些经典的编码规范和命名规则等。以此来约束和改进自己的编程代码,才能写成更好的代码。

3.找到更好的工作

在日益竞争激烈的面试中,尤其是在大厂的面试中,对于源码的考察非常看重,只有了解和熟悉这些源码,才能获得自己想要的 offer。

那既然阅读源码有这么多的好处,我们该如何开始呢?

如何阅读源码?

阅读源码的实现思路是这样的,首先我们需要下载 Spring 源码,然后把 Spring 的源码导入到 IDE 中,然后再编译 Spring 源码,再以添加 Module 的方式添加测试代码,再把 Spring 源码部分引入到 Module 中,此时我们就可以使用调试功能进入 Spring 框架的内部,查看方法的执行流程和源码的具体实现。

Spring 源码阅读流程,如下图所示:

源码执行流程.png

搭建Spring源码阅读环境

本文实现环境:Window 10 / JDK 8 / Spring 5.2.2 / IDEA 2019

注意:Spring 4.x 之后需要 JDK 8 以上的环境。

1.安装Gradle

在正式开始之前我们需要先安装 Gradle,因为 Spring 是基于 Gradle 构建的。

Gradle 是一个开源的基于 JVM 构建工具,和 Maven 工具类似。项目的构建经历了三个时代:Apache Ant(2000 年左右)、Maven(2004年)和 Gradle(2012 年左右),Gradle 相比于 Maven 配置更加简单、性能更高、用户体验更好(IDE 支持代码提示)。

例如,使用 maven 时,配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Maven Quick Start Archetype</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

换成 Gradle,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码apply plugin:'java'
group='com.mycompany.app'
archivesBaseName='my-app'
version='1.0-SNAPSHOT'

repositories{
mavenCentral()
}

dependencies{
testCompile 'junit:4.13'
}

Hibernate、Spring 等优秀的开源项目都将自己的项目从 Maven 迁移到了 Gradle,Google 官方 Android 开发的 IDE Android Studio 也默认使用了 Gradle 进行构建。但不可否认的是,Maven 在如今仍然是 Java 项目构建的主流方式。

Gradle 和 Maven 的性能测试,如下图所示:

Gradle性能对比.png

IDEA 对 Gradle 的支持如下图所示:
IDE支持Gradle.png

更多关于 Gradle vs Maven 的文章,可以访问 Gradle 官方提供文章:gradle.org/maven-vs-gr…

首先,我们需要下载 Gradle 安装包,下载地址为:gradle.org/releases/

选择 binary-only 安装包,下载即可。

注意:需要安装 Gradle 5.X 的版本,Spring 最新版不支持 Gradle 6+ 版本。如果安装 6+ 版本会报错,信息如下:

The build scan plugin is not compatible with Gradle 6.0 and later.
Please use the Gradle Enterprise plugin instead.
Open File

下载完成之后,我们需要解压 Gradle 到指定的目录,例如“C:\Gradle\gradle”。

解压完成之后,我们需要配置两个系统变量。

右键计算机 -> 属性 -> 高级系统设置 -> 环境变量,在系统变量区域,先点击“新建”输入变量名为:GRADLE_HOME,变量值为:C:\gradle\gradle-5.6.4(根据自己的路径填写);再找到 Path 环境变量,新增配置“%GRADLE_HOME%\bin”。

到这里 Gradle 的安装就已经完成了,接下来我们使用命令行执行工具,来测试一下 Gradle 安装是否成功。

打开一个新的 cmd 命令窗口,输入命令 gradle -v,如果出现版本消息,则说明配置成功,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码C:\Users\stone>gradle -v

------------------------------------------------------------
Gradle 5.6.4
------------------------------------------------------------

Build time: 2019-11-01 20:42:00 UTC
Revision: dd870424f9bd8e195d614dc14bb140f43c22da98

Kotlin: 1.3.41
Groovy: 2.5.4
Ant: Apache Ant(TM) version 1.9.14 compiled on March 12 2019
JVM: 1.8.0_211 (Oracle Corporation 25.211-b12)
OS: Windows 10 10.0 amd64

出现如上版本信息,则说明 Gradle 已经正常安装完成。

Gradle 加速

和 Maven 的配置相同,我们可以给 Gradle 配置一个阿里的数据源,加速项目的构建(加上下载 Jar 包),找到配置文件 init.gradle,我的默认目录在 C:\Users\用户名.gradle,如果没有找到则新建一个 init.gradle 文件,之后添加如下配置:

1
2
3
4
5
6
7
复制代码allprojects {
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public"
}
}
}

2.下载最新版的Spring源码

Spring 源码地址:github.com/spring-proj…

小贴士:最好 Fork 一个项目,因为这样你就可以修改源码并添加注释信息到你的仓库了,这样就可以把自己学习 Spring 源码的过程记录并管理起来。

源码下载加速

因为 Spring 的源码文件比较多,如果使用 Spring 原地址下载的话时间会非常长,甚至会失败尝试很多次才能完成,此时我们可以使用国内码云(gitee)上的地址,下载速度会快很多,码云的地址为https://gitee.com/mirrors/Spring-Framework

如下图所示,使用官方提供的 Spring 源码地址,每天同步一次。

码云地址.png

3.导入和编译Spring

Spring 源码下载完成之后,我们就可以使用 IDEA 导入源码项目了,为了加快源码编译的速度,在导入之后我们可以编辑 Spring 源码根目录下的 settings.gradle 文件,添加阿里的源配置信息:

maven { url “maven.aliyun.com/nexus/conte…“}

配置如图所示:

添加阿里源.png

接下来我们就可以导入 Spring 源码了,点击 Import Project,选择 Spring 源码的路径,选择 Gradle 方法导入,如下图所示:

导入 gradle.png

点击 Next,选择本地的 Gradle 路径,如下图所示:

本地 gradle.png

然后点击 Finish 就会进入项目的自动构建阶段了。

Spring 构建完成(耗时比较长),如下图所示:

gradle安装成功.png

到现在为止,Spring 源码的搭建就已经完成了。

调试Spring源码

首先我们在 Spring 源码项目中新增一个测试项目,点击 New -> Module… 创建一个 Gradle 的 Java 项目,如下图所示:

创建测试项目.png

创建测试项目-2.png

创建完成之后,我们在 build.gradle 中添加对 Spring 源码的依赖:

1
复制代码api(project(":spring-context"))

如下图所示:

添加引用.png

接着,我们需要在项目中创建一个 bean 和配置文件(application.xml)及启动文件(MyApplication.java)。

bean 实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class Person {
private Integer id;
private String name;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

启动类 MyApplication 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码import org.springframework.beans.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyApplication {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("classpath*:application.xml");
Person person = context.getBean("person", Person.class);
System.out.println(person.getName());
}
}

配置文件 application.xml(在 resources 中)配置如下:

1
2
3
4
5
6
7
8
9
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="org.springframework.beans.Person">
<property name="id" value="1"/>
<property name="name" value="Java"/>
</bean>
</beans>

接下来我们就可以启动并调试 Spring 的源码了。

总结

Spring 源码的阅读并没有我们想的那么难,安装 Spring 源码的调试坏境只是帮你理清楚 Spring 的执行流程。我们只需要下载 Spring 的源码,安装 Gradle 的环境,再导入到编译器中编译 Spring 源码包,最后在 Spring 源码项目中新建一个 Java 项目添加对 Spring 模块的引用,就可以调试并执行 Spring 源码了,这样我们就可以进入 Spring 的内部,进行相关的源码阅读了。

更多精彩内容,请关注微信公众号「Java中文社群」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文解析 MyBatis Generator 的使用及配置

发表于 2020-04-06

MyBatis-Generator 是 MyBatis 提供的一个代码生成工具,可以帮助我们生成数据库表对应的持久化对象(也称作 Model、PO)、操作数据库的接口(dao)、简单 SQL 的 mapper(XML 形式或注解形式)。

MyBatis-Generator (常简写为 MBG 或 mbg)是一个独立工具,你可以下载它的 jar 包来运行,也可以在 Ant 和 Maven 中运行。其官方网址为:mybatis.org/generator/

一、引入 MyBatis-Generator 及环境

对于这篇博文,我是在基于 SpringBoot 的 Maven 项目环境中配置、使用和讲解的,使用的 IDE 是 IntelliJ IDEA。

既然需要使用 MyBatis-Generator,那么在项目中就一定使用了 MyBatis 和某一种数据库,并且这些依赖应该已经在 Maven 中配置好了。例如 pom 文件中的配置:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<dependencies>
<!-- 为了方便,不展示其它配置... -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>

对于在 Maven 项目中引入 MyBatis-Generator,这里介绍两种方式,具体需要用哪种方式取决于你使用哪种方式来运行 MyBatis-Generator 工具:(对于如何运行 MyBatis-Generator 工具,参见本文第三节)

1. 方式一:在 Maven 中导入依赖

这种方式适用于:用 Java 代码来运行 MyBatis-Generator 工具。在 pom 文件中引入 mybatis-generator-core 依赖:

1
2
3
4
5
6
7
8
复制代码<dependencies>
<!-- 为了方便,不展示其它配置... -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
</dependency>
</dependencies>

2. 方式二:在 Maven 中引入插件

这种方式适用于:通过 Maven 项目的 Plugins 或 Maven 的命令行来运行 MyBatis-Generator 工具。在 pom 文件中引入 mybatis-generator-maven-plugin 插件:

1
2
3
4
5
6
7
8
9
10
复制代码<build>
<!-- 为了方便,不展示其它配置... -->
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
</plugin>
</plugins>
</build>

二、MyBatis-Generator 配置文件

MyBatis-Generator 需要一个 xml 配置文件,来详细配置生成代码的各种细节。例如,在项目的 resources 目录下新建一个 mybatis-generator-config.xml 配置文件:

mbg-config-目录路径

其内容如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码<?xml version="1.0" encoding="UTF-8"?>
<!-- MyBatis-Generator 相关配置 -->
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<!-- 所有的配置均在根元素 generatorConfiguration 下 -->
<generatorConfiguration>
...
...
</generatorConfiguration>

在该文件中,所有的配置均在根元素 generatorConfiguration 下。根元素 generatorConfiguration 有 3 个子元素可供配置,这 3 个子元素必须按照下面给出的次数和顺序进行配置:(没错,MyBatis-Generator 对配置的顺序也有严格的要求)

  1. properties (0 or 1),可出现 0 次或 1 次。
  2. classPathEntry (0…N),可出现 0 次或多次。
  3. context (1…N),至少出现 1 次。

其中,properties 和 classPathEntry 元素用于引入外部配置或文件;context 是核心元素,里面包含有各种详细配置。

1. 引入外部配置文件

在配置 MyBatis-Generator 时,是可以引入外部配置或文件的。

properties 元素

元素 properties 可以用于加载外部配置项或配置文件,该元素有两个属性,均用来指定外部配置文件的地址:

  • resource:使 MBG 从 classpath 开始查找;一般可以使用相对于当前 xml 配置文件的相对路径。
  • url:采用 URL 的方式;例如可以使用 file 协议 file:///Users/deecyn/Files/mybatis-generator.properties 从计算机本地查找,也可以使用 http 协议在互联网上查找,等等。

注意,这两个属性只能选择一个使用。例如,引入下图中的 mybatis-generator.properties 配置文件:

mbg-properties

用于引入文件的代码如下:

1
2
复制代码<!-- 引入外部配置文件 -->
<properties resource="mybatis-generator.properties"/>

之后在整个 xml 配置文件中就可以通过 ${propertyKey} 的方式来引用配置项。

classPathEntry 元素

使用 classPathEntry 元素,可以在 MBG 工作的时候,加载额外需要的依赖包。其中,location 属性指明需要加载的 jar/zip 包的全路径。例如:

1
2
复制代码<!--  加载需要的额外的依赖包 -->
<classPathEntry location="/Users/deecyn/Files/db2java.zip"/>

2. 配置 context 核心元素

在 generationConfiguration 的子元素中,context 是核心元素,用于配置生成一组对象的环境。元素 context 有 4 个属性可供配置:

  • id,必填,上下文 id,用于在生成错误时提示;保证多个 context 的 id 不重复就行。
  • targetRuntime,选填项,这个配置会影响生成的 dao 和 mapper.xml 的内容。常见值为:
    1. MyBatis3,默认值,生成基于 MyBatis 3.x 以上版本的内容,包括很多类似 XxxByExample 的 dao 方法。
    2. MyBatis3Simple,类似 MyBatis3,只是不生成类似 XxxByExample 的 dao 方法,一般选择不生成这些繁杂的方法。
    3. 还有其它可配置的值,详情见 官网 。
  • defaultModelType,选填项,用于指定生成对象的样式。其值为:
    1. conditional,默认值,类似于 hierarchical。区别是,不会为只有一个字段的数据库表生成一个单独的类。
    2. hierarchical,主键生成一个 XxxKey 对象(key class),Blob 等字段单独生成一个对象,其它简单属性在一个对象中(record class)。
    3. flat,所有字段(主键、blob 等)全部生成在一个对象中。
  • introspectedColumnImpl,选填项,类全限定名,用于 扩展 MBG 。

示例配置如下:

1
2
3
复制代码<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
...
</context>

context 的子元素

元素 context 中,有多个子元素需要配置。同样的,context 的子元素必须按照下面给出的次数和顺序进行配置:

  1. property (0…N)
  2. plugin (0…N)
  3. commentGenerator (0 or 1)
  4. connectionFactory 和 jdbcConnection,二选一进行配置
  5. javaTypeResolver (0 or 1)
  6. javaModelGenerator(有且仅有 1 次)
  7. sqlMapGenerator (0 or 1)
  8. javaClientGenerator (0 or 1)
  9. table (1…N)

可以看出,javaModelGenerator、table 以及 connection 元素的配置是必需的。

property 元素

用于为代码生成指定属性,或为其它元素指定属性。可以配置零个或多个,常见的 property 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码<!-- 自动识别数据库关键字,默认为 false,一般保留默认值,遇到数据库关键字(Java关键字)时,按照 table 元素中 columnOverride 属性的配置进行覆盖;
如果设置为 true, 则需按照 SqlReservedWords 中定义的关键字列表,对关键字进行定界(分隔);
定界符(分隔符)参见 beginningDelimiter 和 endingDelimiter 的设置-->
<property name="autoDelimitKeywords" value="false"/>

<!-- beginningDelimiter 和 endingDelimiter,定界符(分隔符),指明用于标记数据库关键字的符号,默认为为双引号 (");
在 oracle 中是双引号 ("),在 MySQL 中需配置为反引号 (`) -->
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>

<!-- 生成的 Java 文件的编码 -->
<property name="JavaFileEncoding" value="UTF-8"/>

<!-- 格式化 Java 代码 -->
<property name="javaFormatter" value="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>
<!-- 格式化 XML 代码 -->
<property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>

注:SqlReservedWords 关键字列表

plugin 元素

配置插件,可以有零个或多个,常见的 plugin 配置有:

1
2
3
4
5
6
复制代码<!-- 使生成的 Model 实现 Serializable 接口  -->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<!-- 为生成的 Model 覆写 toString() 方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 为生成的 Model 覆写 equals() 和 hashCode() 方法 -->
<plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>

commentGenerator 元素

可以配置 0 个或 1 个,用来配置生成的注释,默认是生成注释的,并且会在注释中添加时间等信息。如果想沿用默认的注释配置的话,可以不用配置 commentGenerator 元素。否则,可以进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
复制代码<commentGenerator>
<!-- 不生成所有注释,默认为 false -->
<property name="suppressAllComments" value="true"/>

<!-- 生成的注释中不包含时间信息,默认为 false -->
<property name="suppressDate" value="true"/>
<!-- 生成的注释中,时间的显示格式 -->
<property name="dateFormat" value="yyyy/MM/dd"/>
<!-- 是否添加数据库表中字段的注释,默认为 false -->
<property name="addRemarkComments" value="true"/>
</commentGenerator>

当然,你也可以 自定注释生成器 。

jdbcConnection 元素

配置数据库连接,具体如下:

1
2
3
4
5
6
7
8
9
复制代码<!--  配置数据库连接  -->
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">

<!-- 若为 8.0 版本以上的 mysql-connector-java 驱动,需要设置 nullCatalogMeansCurrent = true -->
<!-- <property name="nullCatalogMeansCurrent" value="true"/> -->
</jdbcConnection>

其中,${propertyKey} 里面是引用的外部配置文件中的 propertyValue:

mbg-properties

你也可以写死,那么就不用在 <properties resource=""/> 中引入此文件了。

这里面值得注意的是 <property name="nullCatalogMeansCurrent" value="true"/> ,当 mysql-connector-java 驱动在 8.0 版本以上时,如果不配置这一项为 true,会不生成指定数据库中表的 Mapper。具体原因可参考文章:MyBatis Generator踩坑与自救 。

javaTypeResolver 元素

可以配置 0 或 1 个,用来配置 JDBC 到 Java 中的类型转换规则,如果不进行配置,则使用默认的转换规则,默认使用 org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl。

就算要配置,也只能配置 BigDecimal 和时间类型的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<javaTypeResolver>
<!-- 是否强制使用 BigDecimal;
默认为 false,把 JDBC 的 DECIMAL 和 NUMERIC 类型解析为 Integer;
设置为 true 时,把 JDBC 的 DECIMAL 和 NUMERIC 类型解析为 java.math.BigDecimal
-->
<property name="forceBigDecimals" value="true"/>

<!-- 设置时间类型的转换,
默认 false,将所有 JDBC 的时间类型解析为 java.util.Date;
设置为 true 时,将 JDBC 的时间类型按如下规则解析:
DATE -> java.time.LocalDate
TIME -> java.time.LocalTime
TIMESTAMP -> java.time.LocalDateTime
TIME_WITH_TIMEZONE -> java.time.OffsetTime
TIMESTAMP_WITH_TIMEZONE -> java.time.OffsetDateTime
-->
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>

javaModelGenerator 元素

Java 模型生成器,有且仅能配置一个,负责 key 类(见 context 元素的 defaultModelType 属性)、Java Bean 实体类、查询类的生成。

元素 javaModelGenerator 有两个属性:

  • targetPackage:生成的类要放的包,具体的包受子元素 enableSubPackages 影响;
  • targetProject:目标项目,指定一个已存在的目录。(targetProject 的路径配置,对于不同的 MBG 启动方式会有一些区别,详情见第三节:运行 MyBatis-Generator)

在 javaModelGenerator 元素中还可以配置多个 property 子元素,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<!--  配置 Java 模型生成器 -->
<javaModelGenerator targetPackage="deecyn.shop_02.mbg.model" targetProject="src/main/java">
<!-- 自动为每一个生成的类创建一个构造方法,构造方法包含了所有的 field,而不是使用 setter;
默认值为 false -->
<property name="constructorBased" value="false"/>

<!-- 在 targetPackage 的基础上,根据数据库的 schema 再生成一层 package,
最终生成的类放在这个package下;默认为false -->
<property name="enableSubPackages" value="false"/>

<!-- 是否创建一个不可变的类:如果为true,那么 MBG 生成的类会没有 setter 方法,
采用构造函数的方式来接收和设置每个字段的值,此时会忽略 constructorBased 属性的设置;
默认值为 false -->
<property name="immutable" value="false"/>

<!-- 设置在 getter 方法中,是否对 String 类型的字段调用 trim() 方法;默认为 false -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>

sqlMapGenerator 元素

可以配置 0 或 1 个,生成 SQL Map 的 xml 文件生成器。在 MyBatis3 之后,我们可以使用 mapper.xml 文件 + Mapper 接口,或者只使用 Mapper 接口 + Annotation;所以,如果 javaClientGenerator 元素中配置了需要生成 xml 的话,这个元素就必须配置。

该元素有 targetPackage 和 targetProject 两个属性,原理与 javaModelGenerator 元素的相同,只不过这里指的是 resource 目录下存放 mapper.xml 文件的路径。具体代码如下:

1
2
3
4
5
复制代码<!-- SQL Map 的 xml 文件生成器 -->
<sqlMapGenerator targetPackage="mbg-mapper" targetProject="src/main/resources">
<!-- 同 javaModelGenerator 元素中的配置 -->
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>

javaClientGenerator 元素

可以配置 0 或 1 个,用于配置关于 Mapper 接口的生成,如果没有配置该元素,那么默认不会生成 Mapper 接口。

元素 javaClientGenerator 有 3 个属性,其中 targetPackage 和 targetProject 属性的配置与 javaModelGenerator 元素的原理相同,只不过这里指的是 java 目录下存放 Mapper 接口的路径。关于 type 属性,有 3 个可选值:

  • ANNOTATEDMAPPER,按照使用 Mapper 接口 + Annotation 的方式生成文件,SQL 生成在对应的 Annotation 中,不会生成 xml 文件。
  • MIXEDMAPPER,使用混合配置,会生成 Mapper 接口,并适当添加合适的 Annotation,也会有 SQL 生成在 XML 文件中。
  • XMLMAPPER,会生成 Mapper 接口,接口完全依赖 XML 文件。

注意,如果 context 元素的 defaultModelType 属性设置为 MyBatis3Simple,那么就只支持 ANNOTATEDMAPPER 和 XMLMAPPER 的方式。一般建议将 type 设置成 XMLMAPPER。

1
2
3
4
5
6
复制代码<!--  关于 Mapper 接口的生成 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="deecyn.shop_02.mbg.mapper"
targetProject="src/main/java">
<!-- 同 javaModelGenerator 元素中的配置 -->
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>

table 元素

一个 table 元素对应一张数据库表,如果想同时为多张表生成代码,需要配置多个 table 元素;或者可以将 tableName 设置为 % 来为全部表生成代码。

元素 table 除开一个必须的属性 tableName(数据库表名称)需要设置外,还有很多可选的属性,部分属性如下:

  1. schema,数据库的 schema;
  2. catalog,数据库的 catalog;
  3. domainObjectName:生成的 domain 类的名字,如果不设置,直接使用表名的驼峰命名作为 domain 类的名字;可以设置为 somepackage.domainName,那么会自动把 domainName 类再放到 somepackage 包里面;
  4. enableSelectByExample,默认 true,MyBatis3Simple 为 false,指定是否生成动态查询语句;
  5. enableUpdateByPrimaryKey,默认 true,指定是否生成按照主键修改对象的语句(即 update);
  6. enableDeleteByExample,默认 true,MyBatis3Simple 为 false,指定是否生成动态删除语句;
  7. enableCountByExample,默认 true,MyBatis3Simple 为 false,指定是否生成动态查询总条数语句(用于分页的总条数查询);
  8. enableUpdateByExample,默认 true,MyBatis3Simple为false,指定是否生成动态修改语句(只修改对象中不为空的属性);
  9. modelType,参考 context 元素的 defaultModelType,相当于对其进行覆盖。

此外,table 元素中还可以配置多个 property 和 columnOverride 等子元素。示例代码如下:

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
复制代码<!-- 配置需要生成代码的数据库表 -->
<table tableName="pms_brand" domainObjectName="PmsBrand"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">

<!-- 指定是否只生成 domain 类,默认为 false;
如果设置为 true,则只生成 domain 类,如果还配置了sqlMapGenerator,那么
在 mapper.xml 文件中,只生成 resultMap 元素 -->
<property name="modelOnly" value="false"/>

<!-- 默认为 false;如果设置为 true,生成的 model 类会直接使用 column 本身的名字,而不会再使用驼峰命名方法。比如 CREATE_DATE,生成的属性名字就是 CREATE_DATE,而不会是 createDate -->
<property name="useActualColumnNames" value="false"/>

<!-- 生成主键的方法,如果设置了该元素,MBG 会在生成的 <insert> 元素中生成一条正确的 <selectKey> 元素 -->
<generatedKey column="id" sqlStatement="MySql" identity="true"/>

<!-- 用来修改表中某个列的属性,MBG 会根据修改后的配置来生成 domain 的属性;
column:要重新设置的列名;一个 table 元素中可以定义多个 columnOverride 元素哈 -->
<columnOverride column="show_status">
<!-- 使用 property 属性来指定列要生成的属性名称 -->
<property name="property" value="showStatus"/>

<!-- javaType 用于指定生成的 domain 的属性类型,使用类型的全限定名-->
<property name="javaType" value="java.lang.Integer"/>

<!-- jdbcType用于指定该列的JDBC类型
<property name="jdbcType" value=""/>
-->
</columnOverride>
</table>

三、运行 MyBatis-Generator

对于 MyBatis-Generator,不同的运行方式,对项目和文件的配置会有一些区别,本文介绍两种运行 MBG 的方式。

方式一:使用 Java 代码编程运行

通过这种方式运行 MBG,在本文的第一节,引入 MyBatis-Generator 时,需要按照方式一在 Maven 的 pom 文件中引入依赖:

1
2
3
4
5
6
7
8
复制代码<dependencies>
<!-- 为了方便,不展示其它配置... -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
</dependency>
</dependencies>

然后在项目中新建一个 Java 类,代码类似下面:

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
复制代码package deecyn.shop_02.mbg;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class Generator {
public static void main(String[] args) throws Exception {
// MBG 执行过程中的警告信息
List<String> warnings = new ArrayList<String>();
// 当生成的代码重复时,覆盖原代码
boolean overwrite = true;
// 读取我们的 MBG 配置文件
InputStream is = Generator.class.getResourceAsStream("/mybatis-generator-config.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
is.close();

DefaultShellCallback callback = new DefaultShellCallback(overwrite);
//创建 MBG
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
//执行生成代码
myBatisGenerator.generate(null);
//输出警告信息
for (String warning : warnings) {
System.out.println(warning);
}
}
}

运行类中的 main() 方法即可在相应的目录下生成对应的代码。

需要注意的是,当你的项目中有多个 Module 时,在配置 javaModelGenerator、sqlMapGenerator 和 javaClientGenerator 元素的 targetProject 属性时,需要在前面加上当前的 Module 名称。例如当前的 Module 名称为 shop_02 时:

1
2
3
4
复制代码<javaModelGenerator targetPackage="deecyn.shop_02.mbg.model" targetProject="shop_02/src/main/java"/>
<sqlMapGenerator targetPackage="mbg-mapper" targetProject="shop_02/src/main/resources"/>
<javaClientGenerator type="XMLMAPPER" targetPackage="deecyn.shop_02.mbg.mapper"
targetProject="shop_02/src/main/java"/>

否则会提示找不到对应的 java 和 resource 目录。

方式二:通过 Maven 插件运行

通过这种方式运行 MBG,在本文的第一节,引入 MyBatis-Generator 时,需要按照方式二在 Maven 的 pom 文件中引入插件,此外还需要进行一些配置:

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
复制代码<build>
<!-- 为了方便,不展示其它配置... -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>

<configuration>
<!-- 引入 MyBatis-Generator 的配置文件 -->
<configurationFile>./src/main/resources/generatorConfig.xml</configurationFile>
<!-- 允许 MBG 将构建消息写入日志中 -->
<verbose>true</verbose>
<!-- 再次运行 MBG 时,允许覆盖已生成的文件,但是不会覆盖 xml 文件 -->
<overwrite>true</overwrite>
</configuration>

<dependencies>
<!-- 引入 mysql 的 JDBC 驱动,否则会报错找不到驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
</dependencies>
</plugin>
</build>

配置好后,双击 Maven –> Plugins 中的 MyBatis-Generator 运行:

mbg-run

即可在相应的目录下生成对应的代码。

需要注意的是,此时,在配置 javaModelGenerator、sqlMapGenerator 和 javaClientGenerator 元素的 targetProject 属性时,其路径都是相对于当前 Project 或 Module 的,不需要加前缀。例如当前的 Module 名称为 shop_02 时:

1
2
3
4
复制代码<javaModelGenerator targetPackage="deecyn.shop_02.mbg.model" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="mbg-mapper" targetProject="src/main/resources"/>
<javaClientGenerator type="XMLMAPPER" targetPackage="deecyn.shop_02.mbg.mapper"
targetProject="src/main/java"/>

否则会提示找不到对应的 java 和 resource 目录。

四、完整配置文件参考

1. 完整的 pom 配置文件

参考链接:Notes: mybatis-generator-pom

2. 完整的 MyBatis-Generator 配置文件

参考链接:Notes: mybatis-generator-config

五、参考

  • 掘金:MyBatis Generator 超详细配置
  • 简书:Mybatis Generator完整配置详解
  • MyBatis Generator 官网 ,关于更多配置的详情,建议查看官网的说明。

(完)如有问题,欢迎交流~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

结合代码和内存变化图一步步弄懂JVM的FullGC

发表于 2020-04-06

1.年轻代存活的对象太多,老年代了放不下

01.示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class DemoTest1 {
public static void main(String[] args) {
byte[] array1 = new byte[4 * 1024 * 1024];
array1 = null;

byte[] array2 = new byte[2 * 1024 * 1024];
byte[] array3 = new byte[2 * 1024 * 1024];
byte[] array4 = new byte[2 * 1024 * 1024];
byte[] array5 = new byte[128 * 1024];

byte[] array6 = new byte[2 * 1024 * 1024];

}

02.启动JVM参数

1
powershell复制代码-XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=3145728 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log

其中,参数-XX:PretenureSizeThreshold,参数要设置大对象阈值为3MB,也就是超过3MB,就直接进入老年代。

大对象大小是3MB。一旦对象大小超过3MB,不会进入新生代,直接进入老年代。

启动命令:

1
2
powershell复制代码java  -jar -XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:SurvivorRatio=8  -XX:MaxTenuringThre
shold=15 -XX:PretenureSizeThreshold=3145728 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log jvm-demo.jar

03.GC日志

启动之后就得到如下GC日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
powershell复制代码Java HotSpot(TM) 64-Bit Server VM (25.151-b12) for windows-amd64 JRE (1.8.0_151-b12), built on Sep  5 2017 19:33:46 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16703268k(7458748k free), swap 23781156k(9784196k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=3145728 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
0.174: [GC (Allocation Failure) 0.174: [ParNew (promotion failed): 7457K->8328K(9216K), 0.0046949 secs]
0.179: [CMS: 8194K->6962K(10240K), 0.0033396 secs] 11553K->6962K(19456K), [Metaspace: 2970K->2970K(1056768K)], 0.0089224 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
par new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 6962K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2976K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 330K, capacity 386K, committed 512K, reserved 1048576K

04.分析GC日志

先看如下代码:

1
2
java复制代码 byte[] array1 = new byte[4 * 1024 * 1024];
array1 = null;

这行代码直接分配了一个4MB的大对象,此时这个对象会直接进入老年代,接着array1不再引用这个对象。

此时内存分配如下:


紧接着就是如下代码

1
2
3
4
java复制代码byte[] array2 = new byte[2 * 1024 * 1024];
byte[] array3 = new byte[2 * 1024 * 1024];
byte[] array4 = new byte[2 * 1024 * 1024];
byte[] array5 = new byte[128 * 1024];

连续分配了4个数组,其中3个是2MB的数组,1个是128KB的数组,如下图所示,全部会进入Eden区域中。


接着会执行如下代码:byte[] array6 = new byte[2 * 1024 * 1024];。此时还能放得下2MB的对象吗?

不可能了,因为Eden区已经放不下了。因此此时会直接触发一次Young GC。

我们看下面的GC日志:

1
powershell复制代码0.174: [GC (Allocation Failure) 0.174: [ParNew (promotion failed): 7457K->8328K(9216K), 0.0046949 secs]

这行日志显示了,Eden区原来是有7000多KB的对象,但是回收之后发现一个都回收不掉,因为上述几个数组都被变量引用了。

所以此时,一定会直接把这些对象放入到老年代里去,但是此时老年代里已经有一个4MB的数组了,还能放的下3个2MB的数组和1个128KB的数组吗?

明显是不行的,此时一定会超过老年代的10MB大小。

所以此时看gc日志:

1
powershell复制代码0.179: [CMS: 8194K->6962K(10240K), 0.0033396 secs] 11553K->6962K(19456K), [Metaspace: 2970K->2970K(1056768K)], 0.0089224 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

此时执行了CMS垃圾回收器的Full GC,Full GC其实就是会对老年代进行Old GC,同时一般会跟一次Young GC关联,还会触发一次元数据区(永久代)的GC。

在CMS Full GC之前,就已经触发过Young GC了,此时可以看到此时Young GC就已经有了,接着就是执行针对老年代的Old GC,也就是如下日志:

CMS: 8194K->6962K(10240K), 0.0033396 secs

这里看到老年代从8MB左右的对象占用,变成了6MB左右的对象占用,这是怎么个过程呢?

很简单,一定是在Young GC之后,先把2个2MB的数组放入了老年代,如下图。


此时要继续放1个2MB的数组和1个128KB的数组到老年代,一定会放不下,所以此时就会触发CMS的Full GC。

然后此时就会回收掉其中的一个4MB的数组,因为他已经没人引用了,如下图所示。


所以再看CMS的垃圾回收日志:CMS: 8194K->6962K(10240K), 0.0033396 secs,他是从回收前的8MB变成了6MB,就是上图所示。

最后在CMS Full GC执行完毕之后,其实年轻代的对象都进入了老年代,此时最后一行代码要在年轻代分配2MB的数组就可以成功了,如下图。

05.总结

这是一个触发老年代GC的案例,就是年轻代存活的对象太多放不下老年代了,此时就会触发CMS的Full GC。

2.老年代可用空间小于了历次Young GC后升入老年代的对象的平均大小

01.示例代码

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
java复制代码public class DemoTest1 {
public static void main(String[] args) {
byte[] array1 = new byte[1 * 1024 * 1024];
array1 = null;
byte[] array2 = new byte[1 * 1024 * 1024];
array2 = null;
byte[] array3 = new byte[1 * 1024 * 1024];
array3 = null;
byte[] array4 = new byte[1 * 1024 * 1024];//触发YGC 1MB 1

array1 = new byte[1 * 1024 * 1024];
array1 = null;
array2 = new byte[1 * 1024 * 1024];
array2 = null;
array3 = new byte[1 * 1024 * 1024];//触发YGC Y 1MB O 1MB 2
array3 = null;

byte[] array5 = new byte[1 * 1024 * 1024];// Y 2MB O 1MB
array1 = new byte[1 * 1024 * 1024];// Y 3MB
array1 = null;
array2 = new byte[1 * 1024 * 1024];// Y 1MB O 2MB YGC 3

array2 = null;
array3 = new byte[1 * 1024 * 1024];//Y 2MB O 2MB
array3 = null;
byte[] array6 = new byte[1 * 1024 * 1024];//Y 3MB O 2MB
array1 = new byte[1 * 1024 * 1024];//Y 1MB O 3MB YGC 4

array1 = null;
array2 = new byte[1 * 1024 * 1024];//Y 2MB
array2 = null;
array3 = new byte[1 * 1024 * 1024];//Y 3MB
array3 = null;
byte[] array7 = new byte[1 * 1024 * 1024];//YGC 5

}
}

02.启动JVM参数

1
powershell复制代码-XX:NewSize=5M -XX:MaxNewSize=5M -XX:InitialHeapSize=10M -XX:MaxHeapSize=10M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=2M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log

其中,参数-XX:PretenureSizeThreshold,参数要设置大对象阈值为2MB,也就是超过2MB,就直接进入老年代。

大对象大小是3MB。一旦对象大小超过3MB,不会进入新生代,直接进入老年代。

启动命令:

1
powershell复制代码java  -jar -XX:NewSize=5M -XX:MaxNewSize=5M -XX:InitialHeapSize=10M -XX:MaxHeapSize=10M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=2M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log jvm-demo.jar

03.GC日志

启动之后就得到如下GC日志:

老年代

年轻代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
powershell复制代码Java HotSpot(TM) 64-Bit Server VM (25.151-b12) for windows-amd64 JRE (1.8.0_151-b12), built on Sep  5 2017 19:33:46 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16703268k(7221016k free), swap 23781156k(8613656k free)
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:MaxTenuringThreshold=15 -XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=2097152 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
0.121: [GC (Allocation Failure) 0.121: [ParNew: 3155K->512K(4608K), 0.0041165 secs] 3155K->766K(9728K), 0.0042644 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.125: [GC (Allocation Failure) 0.125: [ParNew: 3663K->0K(4608K), 0.0016667 secs] 3917K->1732K(9728K), 0.0017448 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.127: [GC (Allocation Failure) 0.127: [ParNew: 3142K->0K(4608K), 0.0013221 secs] 4875K->2756K(9728K), 0.0013592 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.129: [GC (CMS Initial Mark) [1 CMS-initial-mark: 2756K(5120K)] 4878K(9728K), 0.0004498 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.129: [CMS-concurrent-mark-start]
0.130: [GC (Allocation Failure) 0.130: [ParNew: 3146K->0K(4608K), 0.0005869 secs] 5902K->2756K(9728K), 0.0006362 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.131: [GC (Allocation Failure) 0.131: [ParNew: 3148K->0K(4608K), 0.0007974 secs] 5904K->3780K(9728K), 0.0008262 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 4608K, used 2207K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 53% used [0x00000000ff600000, 0x00000000ff827f38, 0x00000000ffa00000)
from space 512K, 0% used [0x00000000ffa80000, 0x00000000ffa80000, 0x00000000ffb00000)
to space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
concurrent mark-sweep generation total 5120K, used 3780K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2976K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 330K, capacity 386K, committed 512K, reserved 1048576K

04.分析GC日志

(1).代码块1

先看如下代码:

1
2
3
4
5
6
7
java复制代码  byte[] array1 = new byte[1 * 1024 * 1024];
array1 = null;
byte[] array2 = new byte[1 * 1024 * 1024];
array2 = null;
byte[] array3 = new byte[1 * 1024 * 1024];
array3 = null;
byte[] array4 = new byte[1 * 1024 * 1024];

这段代码直接分配了4个1MB的数组,并且在第4个数组的时候,会因为新生代内存不足触发YGC。

此时内存分配如下:


对应如下GC日志:

1
powershell复制代码0.121: [GC (Allocation Failure) 0.121: [ParNew: 3155K->512K(4608K), 0.0041165 secs] 3155K->766K(9728K), 0.0042644 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

此时,可以看到新生代就只剩512K的对象,这个奇怪的512KB的对象进入Survivor From区。

那么大小为1MB的数组对象去哪里呢?肯定不是这个奇怪的512KB的对象。

这1MB的数组首先肯定是准备进入Survivor From区,可是,在我们设置的JVM参数下,只有0.5MB,明显是不够分配的。根据JVM YoungGC的规则,Survivor区放不下GC之后存活的对象,直接进入老年代。

所以,1MB的数组对象是直接进入到老年代了。

此时,内存分配如下:

(2).代码块2

紧接这就是这块代码:

1
2
3
4
5
java复制代码 array1 = new byte[1 * 1024 * 1024];
array1 = null;
array2 = new byte[1 * 1024 * 1024];
array2 = null;
array3 = new byte[1 * 1024 * 1024];

这里再次创建了3个1MB的数组对象,并且会触发一次YoungGC;

对应 GC日志如下:

1
powershell复制代码0.125: [GC (Allocation Failure) 0.125: [ParNew: 3663K->0K(4608K), 0.0016667 secs] 3917K->1732K(9728K), 0.0017448 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

此时,Young GC之后,新生代变成0KB,那么存活的大小为1MB的数组对象去哪里呢?

这1MB的数组首先肯定是准备进入Survivor From区,可是,在我们设置的JVM参数下,只有0.5MB,明显是不够分配的。根据JVM YoungGC的规则,Survivor区放不下GC之后存活的对象,直接进入老年代。

所以,1MB的数组对象是直接进入到老年代了。

之前看到的未知的对象512KB也进入到老年代,此时内存分配如下:

(3).代码块3
1
2
3
4
5
java复制代码array3 = null;
byte[] array5 = new byte[1 * 1024 * 1024];
array1 = new byte[1 * 1024 * 1024];
array1 = null;
array2 = new byte[1 * 1024 * 1024];

这里再次创建了3个1MB的数组对象,并且会触发一次YoungGC;

对应的GC日志如下:

1
powershell复制代码0.127: [GC (Allocation Failure) 0.127: [ParNew: 3142K->0K(4608K), 0.0013221 secs] 4875K->2756K(9728K), 0.0013592 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

此时内存分配如下:

(4).代码块4
1
2
3
4
5
java复制代码array2 = null;
array3 = new byte[1 * 1024 * 1024];//Y 2MB O 2MB
array3 = null;
byte[] array6 = new byte[1 * 1024 * 1024];
array1 = new byte[1 * 1024 * 1024];

这里再次创建了3个1MB的数组对象,并且会触发一次YoungGC;并且在这儿,触发Young GC之前触发了一次CMS的Old GC,触发的条件就是老年代可用空间小于了历次Young GC后升入老年代的对象的平均大小。此时新生代大小变成0KB

对应的GC日志如下:

1
2
3
powershell复制代码0.129: [GC (CMS Initial Mark) [1 CMS-initial-mark: 2756K(5120K)] 4878K(9728K), 0.0004498 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.129: [CMS-concurrent-mark-start]
0.130: [GC (Allocation Failure) 0.130: [ParNew: 3146K->0K(4608K), 0.0005869 secs] 5902K->2756K(9728K), 0.0006362 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

此时内存分配如下:

(5).代码块5
1
2
3
4
5
6
java复制代码array1 = null;
array2 = new byte[1 * 1024 * 1024];//Y 2MB
array2 = null;
array3 = new byte[1 * 1024 * 1024];//Y 3MB
array3 = null;
byte[] array7 = new byte[1 * 1024 * 1024];

此时,再创建3个1MB的数组对象,再次触发一次Young GC,执行完YoungGC,此时新生代大小变成0KB;

对应的GC日志如下:

1
powershell复制代码0.131: [GC (Allocation Failure) 0.131: [ParNew: 3148K->0K(4608K), 0.0007974 secs] 5904K->3780K(9728K), 0.0008262 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

此时内存分配如下:

(6).总结

如下GC堆内存日志我们也可以去验证下上面的推测:

此时新生代使用了53%的大小,我们还有一个1MB的数组,可能还存在一些未知对象。

在老年代中使用了大约3MB的空间,应该就是上图中的对象。

1
2
3
4
5
6
7
8
powershell复制代码Heap
par new generation total 4608K, used 2207K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 53% used [0x00000000ff600000, 0x00000000ff827f38, 0x00000000ffa00000)
from space 512K, 0% used [0x00000000ffa80000, 0x00000000ffa80000, 0x00000000ffb00000)
to space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
concurrent mark-sweep generation total 5120K, used 3780K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2976K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 330K, capacity 386K, committed 512K, reserved 1048576K

3.几个触发Full GC的条件

第一:是老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;注:jDK1.8之后已经取消了-XX:-HandlePromotionFailure 机制

第二:是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;

第三:是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。

上述情况都会导致老年代Full GC。

第四:就是“-XX:CMSInitiatingOccupancyFaction”参数,

如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC。默认92%

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入拆解Tomcat&Jetty(八)

发表于 2020-04-05

Tomcat与Jetty

前面的学习记录已经对Tomcat整体的架构和工作原理有基本了解.但是Servlet容器不止有Tomcat,还有后来者Jetty

Jetty与Tomcat一样都是HTTP服务器+Servlet容器,Jetty与Tomcat的架构上有很多类似的地方,但是Jetty更为轻量,更容易定制.

Google App Engine采用了Jetty作为Web容器

Jetty整体架构

Jetty架构

Jetty主要由多个连接器(Connector),多个处理器(Handler),以及一个线程池组成.

Jetty与Tomcat一样会分为HTTP服务器部分和Servlet容器部分,分别由Connector和Handler实现,而这两个组件会共用同一个线程池的资源.

Jetty可以通过多个不同的Connector接收不同端口的请求,Handler组件也可以是一个或多个,甚至不配置Handler

Jetty提供一个Server类来协调各个组件.


整体架构上Tomcat与Jetty的区别

  • Tomcat会有多个Service用于对应不同的端口请求,Jetty没有Service概念,Connector是被多个Handler共享的
  • Tomcat每个连接器都有自己的线程池,而Jetty所有的Connector都共享一个全局连接池

Connector组件

Connector主要功能是对IO模型和应用层协议的封装,最新的Jetty9只支持NIO,所以底层实现都是基于NIO.Connector主要完成三件事:接收连接,IO事件查询,数据读写.分别对应Acceptor,SelectorManager,Connection

由于Jetty的Connector都是基于NIO,所以NIO是基础.NIO回顾

Acceptor

Acceptor用于接收请求,用线程池处理连接请求,通过阻塞的方式接受连接.

ServerSocketChannel#accepet方法是阻塞的,返回后才能继续非阻塞处理

当连接创建后,会将SocketChannel设置为非阻塞模式,然后交给Selector处理后续任务.

SelectorManager

Jetty内部的Selector由SelectorManager进行管理,而被管理的Selector被包装为ManagedSelector.SelectorManager内部有一个数组用于保存ManagedSelector.

在ManagedSelector内部,会有如下流程

  • 1.将Channel注册到Selector,拿到一个SelectionKey(相当于凭证),
  • 2.创建EndPoint和Connection,和SelectionKey三者绑定在一起.

这个过程相当于下单(注册IO事件),下单后返回一个订单ID(Channel注册到selector,返回SelectionKey),然后通过订单完成后(IO事件触发),通过订单ID通知订单完成(调用绑定在SelectionKey上的EndPoint的方法)

ManagedSelector并不会真正处理处理,而是通过EndPoint的方法返回Runnable,再通过线程池去执行,在Runnable里面才会真正读数据和处理请求

Connection

Jetty的Connection组件类似于Tomcat的Processor,负责具体协议的解析,得到Request对象,调用Handler进行处理.

HttpConnection是Connection的一个实现类,也实现了Runnable,在HTTPConnection中,Endpoint会注册一个回调,当数据到了EndPoint就会回调给HttpConnection,在回调方法里面会调用Endpoint接口读数据,然后让HTTP解析器解析字节流,HTTP解析器将解析后的数据包装成Request对象.

响应处理:Connection调用Handler,Handler通过Response对象操作响应流,往响应内容写数据,HttpConnection通过Endpoint把数据写到Channel,完成响应.

Connector工作流程

Connector工作流程

  • 1.Acceptor监听连接请求,当有请求到达时就接受请求,一个连接对应一个Channel,Acceptor将Channel交给ManagedSelector处理
  • 2.ManagedSelector把Channel注册到Selector上,并创建Endpoint和Connection与Channel绑定,然后不断检测IO事件
  • 3.IO事件到达后调用Endpoint方法拿到Runnable,交给线程池执行
  • 4.线程池执行Runnable
  • 5.Runnable执行,调用回调函数,这个函数是Connection注册到Endpoint中的,
  • 6.回调函数调用EndPoint接口读取数据
  • 7.Connection解析读到的数据,生成Request对象交给Handler组件进行处理.

Jetty小结

Jetty Server在设计上只是多个Connector,多个Handler,以及一个全局线程池组成.

Jetty通过原生的NIO实现自己的IO模型,Jetty将NIO中的Selector封装成ManagedSelector方便使用.

Jetty在线程模型上与Tomcat的NioEndPoint类似,都是持有一个Acceptor数组,用一个Selector监听IO事件进行事件派发,然后用线程池执行请求.

最大的不同是Jetty全局只有一个线程池,所有的多线程任务都是由这个线程池进行处理.

Jetty Connector使用回调函数来模拟异步IO

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文彻底读懂MySQL事务的四大隔离级别

发表于 2020-04-05

前言

之前分析一个死锁问题,发现自己对数据库隔离级别理解还不够清楚,所以趁着这几天假期,整理一下MySQL事务的四大隔离级别相关知识,希望对大家有帮助~

github.com/whx123/Java…

事务

什么是事务?

事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加。所以就需要事务,将A的钱回滚回去,就是这么简单。

事务的四大特性

  • 原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
  • 一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
  • 隔离性: 多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。。
  • 持久性: 表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。

事务并发存在的问题

事务并发执行存在什么问题呢,换句话说就是,一个事务是怎么干扰到其他事务的呢?看例子吧~

假设现在有表:

1
2
3
4
5
6
7
复制代码CREATE TABLE `account` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un_name_idx` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表中有数据:

脏读(dirty read)

假设现在有两个事务A、B:

  • 假设现在A的余额是100,事务A正在准备查询Jay的余额
  • 这时候,事务B先扣减Jay的余额,扣了10
  • 最后A 读到的是扣减后的余额

由上图可以发现,事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读。

不可重复读(unrepeatable read)

假设现在有两个事务A和B:

  • 事务A先查询Jay的余额,查到结果是100
  • 这时候事务B 对Jay的账户余额进行扣减,扣去10后,提交事务
  • 事务A再去查询Jay的账户余额发现变成了90

事务A又被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。

幻读

假设现在有两个事务A、B:

  • 事务A先查询id大于2的账户记录,得到记录id=2和id=3的两条记录
  • 这时候,事务B开启,插入一条id=4的记录,并且提交了
  • 事务A再去执行相同的查询,却得到了id=2,3,4的3条记录了。

事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

事务的四大隔离级别实践

既然并发事务存在脏读、不可重复、幻读等问题,InnoDB实现了哪几种事务的隔离级别应对呢?

  • 读未提交(Read Uncommitted)
  • 读已提交(Read Committed)
  • 可重复读(Repeatable Read)
  • 串行化(Serializable)

读未提交(Read Uncommitted)

想学习一个知识点,最好的方式就是实践之。好了,我们去数据库给它设置读未提交隔离级别,实践一下吧~

先把事务隔离级别设置为read uncommitted,开启事务A,查询id=1的数据

1
2
3
复制代码set session transaction isolation level read uncommitted;
begin;
select * from account where id =1;

结果如下:

这时候,另开一个窗口打开mysql,也把当前事务隔离级别设置为read uncommitted,开启事务B,执行更新操作

1
2
3
复制代码set session transaction isolation level read uncommitted;
begin;
update account set balance=balance+20 where id =1;

接着回事务A的窗口,再查account表id=1的数据,结果如下:

可以发现,在读未提交(Read Uncommitted) 隔离级别下,一个事务会读到其他事务未提交的数据的,即存在脏读问题。事务B都还没commit到数据库呢,事务A就读到了,感觉都乱套了。。。实际上,读未提交是隔离级别最低的一种。

已提交读(READ COMMITTED)

为了避免脏读,数据库有了比读未提交更高的隔离级别,即已提交读。

把当前事务隔离级别设置为已提交读(READ COMMITTED),开启事务A,查询account中id=1的数据

1
2
3
复制代码set session transaction isolation level read committed;
begin;
select * from account where id =1;

另开一个窗口打开mysql,也把事务隔离级别设置为read committed,开启事务B,执行以下操作

1
2
3
复制代码set session transaction isolation level read committed;
begin;
update account set balance=balance+20 where id =1;

接着回事务A的窗口,再查account数据,发现数据没变:

我们再去到事务B的窗口执行commit操作:

1
复制代码commit;

最后回到事务A窗口查询,发现数据变了:

由此可以得出结论,隔离级别设置为已提交读(READ COMMITTED) 时,已经不会出现脏读问题了,当前事务只能读取到其他事务提交的数据。但是,你站在事务A的角度想想,存在其他问题吗?

提交读的隔离级别会有什么问题呢?

在同一个事务A里,相同的查询sql,读取同一条记录(id=1),读到的结果是不一样的,即不可重复读。所以,隔离级别设置为read committed的时候,还会存在不可重复读的并发问题。

可重复读(Repeatable Read)

如果你的老板要求,在同个事务中,查询结果必须是一致的,即老板要求你解决不可重复的并发问题,怎么办呢?老板,臣妾办不到?来实践一下可重复读(Repeatable Read) 这个隔离级别吧~

哈哈,步骤1、2、6的查询结果都是一样的,即repeatable read解决了不可重复读问题,是不是心里美滋滋的呢,终于解决老板的难题了~

RR级别是否解决了幻读问题呢?

再来看看网上的一个热点问题,有关于RR级别下,是否解决了幻读问题?我们来实践一下:

由图可得,步骤2和步骤6查询结果集没有变化,看起来RR级别是已经解决幻读问题了~
但是呢,RR级别还是存在这种现象:

其实,上图如果事务A中,没有update account set balance=200 where id=5;这步操作,select * from account where id>2查询到的结果集确实是不变,这种情况没有幻读问题。但是,有了update这个骚操作,同一个事务,相同的sql,查出的结果集不同,这个是符合了幻读的定义~

这个问题,亲爱的朋友,你觉得它算幻读问题吗?

串行化(Serializable)

前面三种数据库隔离级别,都有一定的并发问题,现在放大招吧,实践SERIALIZABLE隔离级别。

把事务隔离级别设置为Serializable,开启事务A,查询account表数据

1
2
3
4
复制代码set session transaction isolation level serializable;
select @@tx_isolation;
begin;
select * from account;

另开一个窗口打开mysql,也把事务隔离级别设置为Serializable,开启事务B,执行插入一条数据:

1
2
3
4
复制代码set session transaction isolation level serializable;
select @@tx_isolation;
begin;
insert into account(id,name,balance) value(6,'Li',100);

执行结果如下:

由图可得,当数据库隔离级别设置为serializable的时候,事务B对表的写操作,在等事务A的读操作。其实,这是隔离级别中最严格的,读写都不允许并发。它保证了最好的安全性,性能却是个问题~

MySql隔离级别的实现原理

实现隔离机制的方法主要有两种:

  • 读写锁
  • 一致性快照读,即 MVCC

MySql使用不同的锁策略(Locking Strategy)/MVCC来实现四种不同的隔离级别。RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关。

读未提交(Read Uncommitted)

官方说法:

SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. Thus, using this isolation level, such reads are not consistent.

读未提交,采取的是读不加锁原理。

  • 事务读不加锁,不阻塞其他事务的读和写
  • 事务写阻塞其他事务写,但不阻塞其他事务读;

串行化(Serializable)

官方的说法:

InnoDB implicitly converts all plain SELECT statements to SELECT … FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.)

  • 所有SELECT语句会隐式转化为SELECT ... FOR SHARE,即加共享锁。
  • 读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。

MVCC的实现原理

MVCC,中文叫多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。它的实现依赖于隐式字段、undo日志、快照读&当前读、Read View,因此,我们先来了解这几个知识点。

隐式字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列DB_TRX_ID、DB_ROLL_PTR,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列DB_ROW_ID。

  • DB_TRX_ID,记录每一行最近一次修改(修改/更新)它的事务ID,大小为6字节;
  • DB_ROLL_PTR,这个隐藏列就相当于一个指针,指向回滚段的undo日志,大小为7字节;
  • DB_ROW_ID,单调递增的行ID,大小为6字节;

undo日志

  • 事务未提交的时候,修改数据的镜像(修改前的旧版本),存到undo日志里。以便事务回滚时,恢复旧版本数据,撤销未提交事务数据对数据库的影响。
  • undo日志是逻辑日志。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。
  • 存储undo日志的地方,就是回滚段。

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(DB_ROLL_PTR)连一条Undo日志链。

我们通过例子来看一下~

1
2
3
4
5
6
7
复制代码mysql> select * from account ;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | Jay | 100 |
+----+------+---------+
1 row in set (0.00 sec)
  • 假设表accout现在只有一条记录,插入该该记录的事务Id为100
  • 如果事务B(事务Id为200),对id=1的该行记录进行更新,把balance值修改为90

事务B修改后,形成的Undo Log链如下:

快照读&当前读

快照读:

读取的是记录数据的可见版本(有旧的版本),不加锁,普通的select语句都是快照读,如:

1
复制代码select * from account where id>2;

当前读:

读取的是记录数据的最新版本,显示加锁的都是当前读

1
2
复制代码select * from account where id>2 lock in share mode;
select * from account where id>2 for update;

Read View

  • Read View就是事务执行快照读时,产生的读视图。
  • 事务执行快照读时,会生成数据库系统当前的一个快照,记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。
  • Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~

为了下面方便讨论Read View可见性规则,先定义几个变量

  • m_ids:当前系统中那些活跃的读写事务ID,它数据结构为一个List。
  • min_limit_id:m_ids事务列表中,最小的事务ID
  • max_limit_id:m_ids事务列表中,最大的事务ID
  • 如果DB_TRX_ID < min_limit_id,表明生成该版本的事务在生成ReadView前已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
  • 如果DB_TRX_ID > m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
  • 如果 min_limit_id =<DB_TRX_ID<= max_limit_id,需要判断m_ids.contains(DB_TRX_ID),如果在,则代表Read View生成时刻,这个事务还在活跃,还没有Commit,你修改的数据,当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,修改的结果,当前事务是能看见的。

注意啦!! RR跟RC隔离级别,最大的区别就是:RC每次读取数据前都生成一个ReadView,而RR只在第一次读取数据时生成一个ReadView。

已提交读(READ COMMITTED) 存在不可重复读问题的分析历程

我觉得理解一个新的知识点,最好的方法就是居于目前存在的问题/现象,去分析它的来龙去脉~ RC的实现也跟MVCC有关,RC是存在重复读并发问题的,所以我们来分析一波RC吧,先看一下执行流程

假设现在系统里有A,B两个事务在执行,事务ID分别为100、200,并且假设存在的老数据,插入事务ID是50哈~
事务A 先执行查询1的操作

1
2
3
复制代码# 事务A,Transaction ID 100
begin ;
查询1:select * from account WHERE id = 1;

事务 B 执行更新操作,id =1记录的undo日志链如下

1
2
复制代码begin;
update account set balance =balance+20 where id =1;

回到事务A,执行查询2的操作

1
2
3
复制代码begin ;
查询1:select * from account WHERE id = 1;
查询2:select * from account WHERE id = 1;

查询2执行分析:

  • 事务A在执行到SELECT语句时,重新生成一个ReadView,因为事务B(200)在活跃,所以ReadView的m_ids列表内容就是[200]
  • 由上图undo日志链可得,最新版本的balance为1000,它的事务ID为200,在活跃事务列表里,所以当前事务(事务A)不可见。
  • 我们继续找下一个版本,balance为100这行记录,事务Id为50,小于活跃事务ID列表最小记录200,所以这个版本可见,因此,查询2的结果,就是返回balance=100这个记录~~

我们回到事务B,执行提交操作,这时候undo日志链不变

1
2
3
复制代码begin;
update account set balance =balance+20 where id =1;
commit

再次回到事务A,执行查询3的操作

1
2
3
4
复制代码begin ;
查询1:select * from account WHERE id = 1;
查询2:select * from account WHERE id = 1;
查询3:select * from account WHERE id = 1;

查询3执行分析:

  • 事务A在执行到SELECT语句时,重新生成一个ReadView,因为事务B(200)已经提交,不载活跃,所以ReadView的m_ids列表内容就是空的了。
  • 所以事务A直接读取最新纪录,读取到balance =120这个版本的数据。

所以,这就是RC存在不可重复读问题的过程啦有不理解的地方可以多读几遍哈

可重复读(Repeatable Read)解决不可重复读问题的一次分析

我们再来分析一波,RR隔离级别是如何解决不可重复读并发问题的吧~

你可能会觉得两个并发事务的例子太简单了,好的!我们现在来点刺激的,开启三个事务~

假设现在系统里有A,B,C两个事务在执行,事务ID分别为100、200,300,存量数据插入的事务ID是50~

1
2
3
复制代码# 事务A,Transaction ID 100
begin ;
UPDATE account SET balance = 1000 WHERE id = 1;
1
2
复制代码# 事务B,Transaction ID 200
begin ; //开个事务,占坑先

这时候,account表中,id =1记录的undo日志链如下:

1
2
3
复制代码# 事务C,Transaction ID 300
begin ;
//查询1:select * from account WHERE id = 1;

查询1执行过程分析:

  • 事务C在执行SELECT语句时,会先生成一个ReadView。因为事务A(100)、B(200)在活跃,所以ReadView的m_ids列表内容就是[100, 200]。
  • 由上图undo日志链可得,最新版本的balance为1000,它的事务ID为100,在活跃事务列表里,所以当前事务(事务C)不可见。
  • 我们继续找下一个版本,balance为100这行记录,事务Id为50,小于活跃事务ID列表最小记录100,所以这个版本可见,因此,查询1的结果,就是返回balance=100这个记录~~

接着,我们把事务A提交一下:

1
2
3
4
复制代码# 事务A,Transaction ID 100
begin ;
UPDATE account SET balance = 1000 WHERE id = 1;
commit;

在事务B中,执行更新操作,把id=1的记录balance修改为2000,更新完后,undo 日志链如下:

1
2
3
复制代码# 事务B,Transaction ID 200
begin ; //开个事务,占坑先
UPDATE account SET balance = 2000 WHERE id = 1;

回到事务C,执行查询2

1
2
3
4
复制代码# 事务C,Transaction ID 300
begin ;
//查询1:select * from account WHERE id = 1;
//查询2:select * from account WHERE id = 1;

查询2:执行分析:

  • 在RR 级别下,执行查询2的时候,因为前面ReadView已经生成过了,所以直接服用之前的ReadView,活跃事务列表为[100,200].
  • 由上图undo日志链可得,最新版本的balance为2000,它的事务ID为200,在活跃事务列表里,所以当前事务(事务C)不可见。
  • 我们继续找下一个版本,balance为1000这行记录,事务Id为100,也在活跃事务列表里,所以当前事务(事务C)不可见。
  • 继续找下一个版本,balance为100这行记录,事务Id为50,小于活跃事务ID列表最小记录100,所以这个版本可见,因此,查询2的结果,也是返回balance=100这个记录~~

锁相关概念补充(附):

共享锁与排他锁

InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。

  • 共享锁(S锁):允许持锁事务读取一行。
  • 排他锁(X锁):允许持锁事务更新或者删除一行。

如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:

  • T2 请求 s 锁立即被允许,结果 T1 T2 都持有 r 行的 s 锁
  • T2 请求 x 锁不能被立即允许

如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才可以,因为X锁与任何的锁都不兼容。

记录锁(Record Locks)

  • 记录锁是最简单的行锁,仅仅锁住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
  • 记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。
  • 会阻塞其他事务对其插入、更新、删除

记录锁的事务数据(关键词:lock_mode X locks rec but not gap),记录如下:

1
2
3
4
5
6
复制代码RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` 
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;

间隙锁(Gap Locks)

  • 间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。
  • 使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
  • 间隙锁只阻止其他事务插入到间隙中,他们不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock 有相同的作用。

Next-Key Locks

  • Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。

RC级别存在幻读分析

因为RC是存在幻读问题的,所以我们先切到RC隔离级别,分析一波~

假设account表有4条数据。

  • 开启事务A,执行当前读,查询id>2的所有记录。
  • 再开启事务B,插入id=5的一条数据。
  • 事务B插入数据成功后,再修改id=3的记录
  • 回到事务A,再次执行id>2的当前读查询

  • 事务B可以插入id=5的数据,却更新不了id=3的数据,陷入阻塞。证明事务A在执行当前读的时候在id =3和id=4这两条记录上加了锁,但是并没有对 id > 2 这个范围加锁~
  • 事务B陷入阻塞后,切回事务A执行当前读操作时,死锁出现。因为事务B在 insert 的时候,会在新纪录(id=5)上加锁,所以事务A再次执行当前读,想获取id> 3 的记录,就需要在 id=3,4,5 这3条记录上加锁,但是 id = 5这条记录已经被事务B 锁住了,于是事务A被事务B阻塞,同时事务B还在等待 事务A释放 id = 3上的锁,最终产生了死锁。

因此,我们可以发现,RC隔离级别下,加锁的select, update, delete等语句,使用的是记录锁,其他事务的插入依然可以执行,因此会存在幻读~

RR 级别解决幻读分析

因为RR是解决幻读问题的,怎么解决的呢,分析一波吧~

假设account表有4条数据,RR级别。

  • 开启事务A,执行当前读,查询id>2的所有记录。
  • 再开启事务B,插入id=5的一条数据。

可以发现,事务B执行插入操作时,阻塞了~因为事务A在执行select … lock in share mode的时候,不仅在 id = 3,4 这2条记录上加了锁,而且在id > 2 这个范围上也加了间隙锁。

因此,我们可以发现,RR隔离级别下,加锁的select, update, delete等语句,会使用间隙锁+ 临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录。

参考与感谢

  • 解决死锁之路 - 学习事务与隔离级别
  • 五分钟搞清楚MySQL事务隔离级别
  • 4种事务的隔离级别,InnoDB如何巧妙实现?
  • MySQL事务隔离级别和MVCC
  • MySQL InnoDB MVCC 机制的原理及实现
  • MVCC多版本并发控制

个人公众号

  • 觉得写得好的小伙伴给个点赞+关注啦,谢谢~
  • 如果有写得不正确的地方,麻烦指出,感激不尽。
  • 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻
  • github地址:github.com/whx123/Java…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试都在问的微服务、服务治理、RPC、下一代微服务 一

发表于 2020-04-05

文章每周持续更新,「三连」让更多人看到是对我最大的肯定。可以微信搜索公众号「 后端技术学堂 」最新文章提前看(一般比博客早更新一到两篇)

单体式应用程序

与微服务相对的另一个概念是传统的「单体式应用程序」( Monolithic application ),单体式应用内部包含了所有需要的服务。而且各个服务功能模块有很强的耦合性,也就是相互依赖彼此,很难拆分和扩容。

说在做的各位都写过单体程序,大家都没意见吧?给大家举个栗子,刚开始写代码你写的helloworld程序就是单体程序,一个程序包含所有功能,虽然helloworld功能很简单。

单体应用程序的优点

  • 开发简洁,功能都在单个程序内部,便于软件设计和开发规划。
  • 容易部署,程序单一不存在分布式集群的复杂部署环境,降低了部署难度。
  • 容易测试,没有各种复杂的服务调用关系,都是内部调用方便测试。

单体应用程序的缺点

单体程序的缺点一开始不是特别明显,项目刚开始需求少,业务逻辑简单,写代码一时爽,一直爽。噩梦从业务迭代更新,系统日益庞大开始,前期的爽没有了,取而代之的是软件维护和迭代更新的无尽痛苦。

单体架构

单体架构

由于单体式应用程序就像一个大型容器一样,里面放置了许多服务,且他们都是密不可分的,这导致应用程序在扩展时必须以「应用程序」为单位。

当里面有个业务模块负载过高时,并不能够单独扩展该服务,必须扩展整个应用程序(就是这么霸道),这可能导致额外的资源浪费。

此外,单体式应用程序由于服务之间的紧密度、相依性过高,这将导致测试、升级有所困难,且开发曲线有可能会在后期大幅度地上升,令开发不易。相较之下「微服务架构」能够解决这个问题。

微服务

微服务 (Microservices) 就是一些协同工作小而自治的服务。

❝
2014年,Martin Fowler 与 James Lewis 共同提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模的集中管理 (例如 Docker) 能力,服务可以用不同的编程语言与数据库等组件实现 。「维基百科」

❞

举例


还是拿前面的 helloworld 程序来举栗子,想象一下你是 helloworld 公司的 CTO(老板还缺人吗?会写代码的那种),假设你们公司的 helloworld 业务遍布全球,需要编写不同语种的 helloworld 版本,分别输出英语、日语、法语、俄语…现在世界有6000多种语言(奇怪的知识又增加了)。

有人会说这还不简单我用switch case语句就完事了,同学,不要较真我就是举个例子,现实中的业务比 helloworld 复杂多了。好了,我们姑且认为按语言输出是个庞大复杂的工作,这时候就可以用微服务架构了,架构图如下:

微服务架构

微服务架构

微服务与SOA

「面向服务的体系结构」 SOA (Service-Oriented Architecture) 听起来和微服务很像,但 SOA 早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间,最终 SOA 看起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。

此外,实施SOA时会遇到很多问题,比如通信协议(例如SOAP)的选择、第三方中间件如何选择、服务粒度如何确定等,目前也存在一些关于如何划分系统的指导性原则,但其中有很多都是错误的。SOA并没有告诉你如何划分单体应用成微服务,所以在实施SOA时会遇到很多问题。

这些问题再微服务框架中得到很好的解决,你可以认为微服务架构是SOA的一种特定方法。

微服务架构

合久必分,鉴于「单体应用程序」有上述的缺点,单个应用程序被划分成各种小的、互相连接的微服务,一个微服务完成一个比较单一的功能,相互之间保持独立和解耦合,这就是微服务架构。

微服务优点

相对于单体服务,微服务有很多优点,这里列举几个主要的好处

技术异构性

不同服务内部的开发技术可以不一致,你可以用java来开发helloworld服务A,用golang来开发helloworld服务B,大家再也不用为哪种语言是世界上最好的语言而争论不休。
微服务架构-多技术

为不同的服务选择最适合该服务的技术,系统中不同部分也可以使用不同的存储技术,比如A服务可以选择redis存储,B服务你可以选择用MySQL存储,这都是允许的,你的服务你做主。

隔离性

一个服务不可用不会导致另一个服务也瘫痪,因为各个服务是相互独立和自治的系统。这在单体应用程序中是做不到的,单体应用程序中某个模块瘫痪,必将导致整个系统不可用,当然,单体程序也可以在不同机器上部署同样的程序来实现备份,不过,同样存在上面说的资源浪费问题。

可扩展性

庞大的单体服务如果出现性能瓶颈只能对软件整体进行扩展,可能真正影响性能的只是其中一个很小的模块,我们也不得不付出升级整个应用的代价。这在微服务架构中得到了改善,你可以只对那些影响性能的服务做扩展升级,这样对症下药的效果是很好的。

简化部署

如果你的服务是一个超大的单体服务,有几百万行代码,即使修改了几行代码也要重新编译整个应用,这显然是非常繁琐的,而且软件变更带来的不确定性非常高,软件部署的影响也非常大。在微服务架构中,各个服务的部署是独立的,如果真出了问题也只是影响单个服务,可以快速回滚版本解决。

易优化

微服务架构中单个服务的代码量不会很大,这样当你需要重构或者优化这部分服务的时候,就会容易很多,毕竟,代码量越少意味着代码改动带来的影响越可控。

微服务缺点

我们上面一直在强调微服务的好处,但是,微服务架构不是万能的,并不能解决所有问题,其实这也是微服务把单体应用拆分成很多小的分布式服务导致的,所谓人多手杂,服务多起来管理的不好各种问题就来了。

为了解决微服务的缺点,前辈们提出了下面这些概念。

服务注册与发现

微服务之间相互调用完成整体业务功能,如何在众多微服务中找到正确的目标服务地址,这就是所谓「服务发现」功能。

常用的做法是服务提供方启动的时候把自己的地址上报给「服务注册中心」,这就是「服务注册」。服务调用方「订阅」服务变更「通知」,动态的接收服务注册中心推送的服务地址列表,以后想找哪个服务直接发给他就可以。

服务发现

服务发现

服务监控

单体程序的监控运维还好说,大型微服务架构的服务运维是一大挑战。服务运维人员需要实时的掌握服务运行中的各种状态,最好有个控制面板能看到服务的内存使用率、调用次数、健康状况等信息。

这就需要我们有一套完备的服务监控体系,包括拓扑关系、监控(Metrics)、日志监控(Logging)、调用追踪(Trace)、告警通知、健康检查等,防患于未然。

服务容错

任何服务都不能保证100%不出问题,生产环境复杂多变,服务运行过程中不可避免的发生各种故障(宕机、过载等等),工程师能够做的是在故障发生时尽可能降低影响范围、尽快恢复正常服务。

程序员为此避免被祭天,需要引入「熔断、隔离、限流和降级、超时机制」等「服务容错」机制来保证服务持续可用性。

服务安全

有些服务的敏感数据存在安全问题,「服务安全」就是对敏感服务采用安全鉴权机制,对服务的访问需要进行相应的身份验证和授权,防止数据泄露的风险,安全是一个长久的话题,在微服务中也有很多工作要做。

服务治理

说到「治理」一般都是有问题才需要治理,我们平常说环境治理、污染治理一个意思,微服务架构中的微服务越来越多,上面说的那些问题就更加显现,为了解决上面微服务架构缺陷「服务治理」就出现了。


微服务的那些问题都要公司技术团队自己解决的话,如果不是大型公司有成熟的技术团队,估计会很头大。幸好,有巨人的肩膀可以借给我们站上去,通过引入「微服务框架」来帮助我们完成服务治理。

微服务框架

介绍一些业界比较成熟的微服务框架。

Dubbo

是阿里巴巴公司开源的一个Java高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现 。2011 年末对外开源,仅支持 Java 语言。

官网:http://dubbo.apache.org/zh-cn/

Dubbo架构图|图片来源dubbo.apache.org

Dubbo架构图|图片来源dubbo.apache.org

Tars

腾讯内部使用的微服务架构 TAF(Total Application Framework)多年的实践成果总结而成的开源项目。 仅支持 C++ 语言,目前在腾讯内部应用也非常广泛。2017 年对外开源,仅支持 C++ 语言。

源码: https://github.com/TarsCloud/Tars/

TARS架构图|来源github.com/TarsCloud

TARS架构图|来源github.com/TarsCloud

「本命鹅厂 TARS 框架介绍 PPT 已下载,不想自己麻烦去找的同学,在我公众号「后端技术学堂」回复「tars」获取。」

Motan

是新浪微博开源的一个Java 框架。Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。于 2016 年对外开源,仅支持 Java 语言。

官方指南: https://github.com/weibocom/motan/wiki/zh_userguide

Motan框架|图片来源github.com/weibocom/motan

Motan框架|图片来源github.com/weibocom/motan

gRPC

是Google开发的高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发。本身它不是分布式的,所以要实现上面的框架的功能需要进一步的开发。2015 年对外开源的跨语言 RPC 框架,支持多种语言。

中文教程:https://doc.oschina.net/grpc?t=58008

gRPC架构图|图片来源www.grpc.io

gRPC架构图|图片来源www.grpc.io

thrift

最初是由 Facebook 开发的内部系统跨语言的高性能 RPC 框架,2007 年贡献给了 Apache 基金,成为 Apache 开源项目之一, 跟 gRPC 一样,Thrift 也有一套自己的接口定义语言 IDL,可以通过代码生成器,生成各种编程语言的 Client 端和 Server 端的 SDK 代码,支持多种语言。

thrift架构 | 图片来源wikimedia

thrift架构 | 图片来源wikimedia

微服务框架和RPC

很多人对这两个概念有点混淆,微服务框架上面我们说过了,我们再来看下RPC的概念。

什么是RPC

RPC (Remote Procedure Call)远程过程调用是一个计算机通信协议。我们一般的程序调用是本地程序内部的调用,RPC允许你像调用本地函数一样去调用另一个程序的函数,这中间会涉及网络通信和进程间通信,但你无需知道实现细节,RPC框架为你屏蔽了底层实现。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过「发送请求-接受回应」进行信息交互的系统。

两者关系

RPC和微服务框架的关系我的理解,微服务框架一般都包含了RPC的实现和一系列「服务治理」能力,是一套软件开发框架。我们可以基于这个框架之上实现自己的微服务,方便的利用微服务框架提供的「服务治理」能力和RPC能力,所以微服务框架也被有些人称作RPC框架。

下一代微服务架构

Service Mesh(服务网格)被认为是下一代微服务架构,Service Mesh并没有给我们带来新的功能,它是用于解决其他工具已经解决过的服务网络调用、限流、熔断和监控等问题,只不过这次是在Cloud Native 的 kubernetes 环境下的实现。

特点

Service Mesh 有如下几个特点:

  • 应用程序间通讯的中间层
  • 轻量级网络代理
  • 应用程序无感知
  • 解耦应用程序的重试/超时、监控、追踪和服务发现

目前两款流行的 Service Mesh 开源软件 [Istio](https://istio.io/) 和 [Linkerd](https://linkerd.io/)都可以直接在kubernetes 中集成,其中Linkerd已经成为云原生计算基金会 CNCF (Cloud Native Computing Foundation) 成员。

Why Service Mesh

为什么现有微服务架构已经解决的问题还要用Service Mesh呢?这个问题问的好。


回答问题之前,先看下istio.io上对service mesh的解释,我觉得挺好的,摘抄出来:

❝
As a service mesh grows in size and complexity, it can become harder to understand and manage. Its requirements can include discovery, load balancing, failure recovery, metrics, and monitoring. A service mesh also often has more complex operational requirements, like A/B testing, canary rollouts, rate limiting, access control, and end-to-end authentication.

makes it easy to create a network of deployed services with load balancing, service-to-service authentication, monitoring, and more, **with few or no code changes in service code. **

❞

试着总结一下:随着微服务的增多复杂程度也增加,管理变得更加困难,微服务架构虽然解决了「网络调用、限流、熔断和监控」等问题,但大多数框架和开源软件对原有业务是侵入式的,也就是需要在业务服务程序中集成相关的「服务治理」组件。

Service Mesh之于微服务,就像TCP/IP之于互联网,TCP/IP为网络通信提供了面向连接的、可靠的、基于字节流的基础通信功能,你不再需要关心底层的重传、校验、流量控制、拥塞控制。

用了Service Mesh你也不必去操心「服务治理」的细节,不需要对服务做特殊的改造,所有业务之外的功能都由Service Mesh帮你去做了。它就像一个轻量级网络代理 对应用程序来说是透明,所有应用程序间的流量都会通过它,所以对应用程序流量的控制都可以在 serivce mesh 中实现 。

Service Mesh架构|图片来自:Pattern: Service Mesh

Service Mesh架构|图片来自:Pattern: Service Mesh

写在最后

在IT世界没有什么技术是永不过时的,微服务架构的演进就是一个例子,从单体程序到微服务架构,再到service mesh架构,我不知道下一个技术迭代点是什么时候,但我知道微服务架构肯定还会更新,IT人更应该建立终身学习习惯。

当然更重要的是拥有对技术的热情,热于拥抱变化、接受新技术,当我看到新技术我是兴奋的,内心os是厉害了,还能这么玩!,希望你也有这般热情,而不仅仅是面向工资编程,生活会有趣很多。

老规矩。感谢各位的阅读,文章的目的是分享对知识的理解,技术类文章我都会反复求证以求最大程度保证准确性,若文中出现明显纰漏也欢迎指出,我们一起在探讨中学习。

「原创不易,看到这里动动手指,各位的「三连」是对我持续创作的最大支持,我们下篇文章再见。」

可以微信搜索公众号「 后端技术学堂 」回复「资料」「1024」有我给你准备的各种编程学习资料。文章每周持续更新,我们下期见!

reference

https://www.cnblogs.com/Zachary-Fan/p/service\_manage\_discovery.html

https://www.zhihu.com/question/56125281

http://dockone.io/article/3687

https://www.infoq.cn/article/micro-service-technology-stack

https://segmentfault.com/a/1190000010224335

https://book.douban.com/subject/26772677/

https://jimmysong.io/blog/what-is-a-service-mesh/

https://github.com/weibocom/motan/wiki/zh\_userguide

原创不易,看到这里动动手指,各位的「三连」是对我持续创作的最大支持。

可以微信搜索公众号「 后端技术学堂 」回复「资料」有我给你准备的各种编程学习资料。文章每周持续更新,我们下期见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谁要是再敢用Map传参,我过去就是一JIO

发表于 2020-04-05

image


还记得上次我写过一篇关于实际项目代码分层和规划的文章《看完这篇,别人的开源项目结构应该能看懂了》,
在文尾处提到过一些注意事项,其中第一条就是:

  • Contorller层参数传递建议不要使用HashMap,推荐使用数据模型定义

私信里竟然有很多小伙伴提问说,为什么不能这样做?

我心里暗自寻思:难道这么做的小伙伴都没有被同事捶吗?(滑稽)

得嘞,今天咱们就掰扯掰扯这件事,这是实际写代码时常忽略的一个问题


是不是有人也这么写过?

我自己曾经接手过一个前人留下来的老项目,拿到代码,导入IDEA的那一刻,我哭出了声。

image

因为它的Controller层代码都是类似这样写的:

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
复制代码@RestController
@RequestMapping("/index")
public class IndexController {

// 获取App首页内容
@PostMapping("/getIndexContent")
public ResponseWrapper getIndexContent( @RequestBody Map<String, Object> paramMap ) {

ResponseWrapper res = new ResponseWrapper();

// 下面开始做传参有效性的校验
if (!paramMap.containsKey("article_id")) {
res.setCode(500);
res.setMsg("缺少 article_id 信息");
return res;
}

if (!paramMap.containsKey("page")) {
res.setCode(500);
res.setMsg("缺少 page 信息");
return res;
}

if (!paramMap.containsKey("size")) {
res.setCode(500);
res.setMsg("缺少 size 信息");
return res;
}

if (!paramMap.containsKey("version")) {
res.setCode(500);
res.setMsg("缺少 version 信息");
return res;
}

// ...... 此处省略

}

// ...... 此处省略

}

别的咱先不说,居然明目张胆地在Controller层里方法里用Map传参?!简直丧心病狂了。幸亏下面还有一波传参有效性的验证,对于传递的参数,我好歹也能猜个大概,不然那真是喵了个咪了。

接下来,我们就好好唠一唠:为什么不要在Controller层传参时使用Map类型!


Map一时爽,维护爽歪歪

正好,这地方有一个咱小伙伴活生生的例子。

记得之前有个小伙伴提问,问过一个这样的问题,说他接手了一个别人的老项目,问了我一个类似这样的问题:

image

看到没!

用Map传参的第一个(也是最大的一个)弊端就是:这会导致后续接手和维护的人怀疑自己的人生,因为他根本不知道代码传的啥参数,想要构造参数去调试接口只能靠脑补、摸瞎、以及猜测了。

试想一下,其实我们代码里任何一个地方的传参都可以使用Map来传,如果真的这么做了,代码中连任何数据模型类都不需要定义了,果真如此的话,这样的代码咱能看懂吗?

而且这位小伙伴接手的项目居然还用的是LinkedHashMap参数,可以说很秀了。

image

除此之外,紧接着还会带来下面这个问题。


好用的API工具与你无缘了

我之前写过一篇文章《前后端都分离了,该搞个好用的API管理系统了!》,聊过现在市面上一些比较好用的、能极大提升前后端开发效率的API管理工具,这对于前后端开发来说,简直是莫大的福音。

我们就以Swagger这个API工具为例,如果Controller传参使用Map的话:

1
2
3
4
5
6
7
8
复制代码// 获取App首页内容
@ApiOperation("获取App首页内容")
@PostMapping("/getIndexContent")
public ResponseWrapper getIndexContent( @RequestBody Map<String, Object> paramMap ) {

// ...... 此处省略

}

则API工具无法读取具体参数项目和参数类型,所以传参什么的也看不出来:

image

换言之,我如果将上面的Map传参改为自定义数据模型类IndexQueryDto来传参的话:

1
2
3
4
5
6
7
8
复制代码// 获取App首页内容
@ApiOperation("获取App首页内容(改造后)")
@PostMapping("/getIndexContent")
public ResponseWrapper getIndexContent( @RequestBody IndexQueryDto indexQueryDto ) {

// ...... 此处省略

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码@ApiModel(value = "App首页内容请求参数实体对象")
class IndexQueryDto {

@ApiModelProperty(value = "文章ID号")
@NotNull(message = "缺少 article_id 信息")
private Long article_id;


@ApiModelProperty(value = "页面数")
@NotNull(message = "缺少 page 信息")
private Integer page;

@ApiModelProperty(value = "每页条目数")
@NotNull(message = "缺少 size 信息")
private Integer size;

@ApiModelProperty(value = "App版本号")
@NotNull(message = "缺少 version 信息")
private String version;

// ...... 此处省略set/get方法

}

则类似Swagger这种API工具就非常方便地能帮助我们管理参数了:

image

这样不管是自己调试,还是前、后端对接口都会方便得多。


同理,除了Swagger这种API管理工具之外,像在我的前文《没用过这些IDEA插件?怪不得写代码头疼》中推荐过的一个非常好用的接口管理插件RestfulToolkit也无法识别出Map类型所盛放的具体参数:

image

但是对于数据模型的定义参数,就能非常清晰的给出参数细节,并方便地提供接口测试:

image


优秀的注解没法使用了

还是以文章开头举例的代码来说,不管怎么样,写这段代码的哥们还是负责的,毕竟兢兢业业地用手工连环if()判断完成了所有参数的有效性校验:

image

但问题是,我们真的需要这种辣眼睛的手工连环if()判断来做参数校验吗?

image

同样在前文《啥?听说你还在手写复杂的参数校验?》中也说过了,我们其实可以通过注解来方便地规避繁杂的参数校验工作,但前提是不能使用Map类型传参,需要使用数据模型的定义,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码class IndexQueryDto {

@NotNull(message = "缺少 article_id 信息")
private Long article_id;

@NotNull(message = "缺少 page 信息")
private Integer page;

@NotNull(message = "缺少 size 信息")
private Integer size;

@NotNull(message = "缺少 version 信息")
private String version;

// ...... 此处省略get/set方法
}

一个NotNull注解即可搞定,它不香吗?


Map传参真的一无是处吗?

有些小伙伴表示用Map传参的好处就是可以随意扩展,后期变动灵活,想往里面塞几个参数就塞几个参数;而且也省去了各种对象定义和命名的烦恼。

image


如果非要用Map传参

如果实在不能避免用Map传参,也麻请配备完备的测试用例吧,省得让后来接手维护的人天天看着代码怀疑人生了。

通过测试用例,后来接手维护的人也能快速搞清代码间的参数传递和调用,不然真的只能靠脑补画面去调试了。


嘘…

好了,说了这么多,如果你项目的Controller层代码还在使用Map传参的话,答应我,二话别说,赶快全部偷偷去改掉,快!速度!跑步前进!

image

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…822823824…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%