Fork me on GitHub
夸克的博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

一道题的思考

发表于 2019-07-22 | 分类于 并发编程 , 并发基础 | 热度: ℃
字数统计: 1,523 | 阅读时长 ≈ 6

题目

在小马哥的每日一问中看到了一道这个题:输出什么?。当时看错了在static块中的代码,就毫不意外的答错了= =,这个题其实没有看起来那么简单,这里去记录下这个题。小马哥这个每日一题的系列有很多比较”坑”的题,一般第一遍都比较难答对,推荐每天没事的时候可以去思否上看看这个题,也算拾遗一些基础~

阅读全文 »

java枚举拾遗

发表于 2019-06-12 | 分类于 Java基础 | 热度: ℃
字数统计: 1,814 | 阅读时长 ≈ 8

前言

java枚举是在开发过程中用的最多的类,这里对java之前的枚举常量类和枚举做了一个分析,并且对枚举相关知识拾遗。

枚举类

在出现枚举之前,通常是一个final类去表示”可枚举”这个概念,比如下面这个列举数字的枚举类

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
/**
* 模拟枚举类 (枚举类:在enum出现之前的表达 可枚举的含义的类)
* 通常 private 构造函数
* final class
* private static final 本类型 成员
*
*/

final class EnumClass{
public static final EnumClass ONE = new EnumClass(1);
public static final EnumClass TWO = new EnumClass(2);
public static final EnumClass THREE = new EnumClass(3);
public static final EnumClass FOUR = new EnumClass(4);

@Getter
@Setter
private int value;
private EnumClass(int value) {
this.value = value;
}

public void print() {
System.out.println(this.toString());
}

}

可以看到枚举类的特点:

  • 成员用常量来表示,并且类型为当前类型(当前类型)
  • 常被设置为final类
  • 非public构造器(自己内部来创建实例)

这样有些缺点,比如:

  • 枚举的输出打印的时候要怎么做?每个成员是第几个定义的?要想达到这些操作就必须要写一些方法,而每个枚举类去这样写这样的方法是比较蛋疼的,因为他不具有枚举的values方法。

枚举

这里写出对应的java枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
enum CountingEnum {
ONE(1),
TWO(2),
THREE(3),
FOUR(4)
;
@Getter
private int value;

/** private */ CountingEnum (int value) {
this.value = value;
}
}

这里如果想要输出对应的名字和顺序,那么就十分方便了。

1
2
3
4
// 输出 枚举 中的名字、位置、输出所有枚举
Arrays.stream(CountingEnum.values()).forEach(e -> {
System.out.println("输出枚举中的顺序: " + e.ordinal() + "名字:" + e.name() + "value:" + e.getValue());
});

可以看到输出:

这是因为java所有的枚举都是继承Enum抽象类的,而valueOf()方法、ordinal()方法、name()方法都是定义在其中的,可以看下Eunm抽象类。

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

public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {

private final String name;

public final String name() {
return name;
}

private final int ordinal;


public final int ordinal() {
return ordinal;
}


protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}


public String toString() {
return name;
}


public final boolean equals(Object other) {
return this==other;
}


public final int hashCode() {
return super.hashCode();
}


protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}


public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}


@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}


public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

/**
* enum classes cannot have finalize methods.
*/
protected final void finalize() { }

/**
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
}

仔细看也许你会有两个疑问:

  • 没看到显示的定义CountingEnum时继承Enum类的?
  • values方法也没有看到在父类中定义?

对这两个疑问我们可以去看这个类对应的字节码:

可以看到:

  • enum其实也是final class, 虽然没有显示继承,但是其实是继承了Enum<T>类的,所以可以访问到对应的name,ordinal字段,这个设计让我们输出枚举一些信息的时候很便捷。也提供了valueOf方法,也可以在动态判断枚举的时候使用。

在来看下边的字节码:

可以看到是有values方法,其实这个是jvm通过字节码提升的方式去为枚举做的优化。所以使用枚举可以快速遍历并且一些输出之类的操作。

可以总结下枚举的特点:

  • 枚举其实就是final class,并且继承java.lang.Enum抽象类。
  • 枚举可以实现接口。
  • 枚举不能显示的继承和被继承
  • 留个坑:既然是final class,那么枚举里可以定义抽象方法吗?

枚举中抽象方法的设计

我们在看基础语法的时候,总是说final 和 abstract是互斥的,所以想当然的认为枚举中不能定义抽象方法,但结论其实是可以的。

我们先看一个枚举来实现加减操作的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Opration {
PLUS,
DIVIDE
;
public double apply(double x, double y) {
switch (this) {

case PLUS:
return x + y;
case DIVIDE:
return x - y;
}
throw new AssertionError("unknown");
}
}

这个实现其实是通过在枚举中加入了非枚举含义的方法和域来实现的操作的一个类型枚举。但是有个问题,当拓展新的操作符时,需要破坏switch中的逻辑,这个不太符合开闭原则,这时候就可以通过把apply作为抽象方法,使得拓展时只需要实现符合自己的抽象逻辑。

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
/**
* 通过抽象方法, 来实现加入新的操作的时候 能符合开闭原则,只关心自己操作符抽象的实现
*/
enum OperationOptimize {
PLUS("+"){
@Override
public int apply(int x, int y) {
return x + y;
}
},
DIVIDE("-") {
@Override
public int apply(int x, int y) {
return x - y;
}
}
;

@Getter
private String str;
private OperationOptimize(String str) {
this.str = str;
}

// 抽象方法
public abstract int apply(int x, int y);
}

所以枚举是可以定义抽象方法的。

jdk中其实也有对应的例子,可以看下TimeUnit这个时间单位枚举,枚举类型都是通过实现抽象方法(其实是返回异常的普通方法,思想是一样的)来实现不同时间单位的转化。

彩蛋

如何给上边的枚举类实现一个values方法?

因为需要遍历所有的字段,所以很自然的想到了反射去实现。这里需要注意,因为枚举类定义的枚举都是public static final,而作为val变量是int的一个修饰符,需要将除了枚举外的val变量排除~

示例代码:

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
final class EnumClass {
public static final EnumClass ONE = new EnumClass(1);
public static final EnumClass TWO = new EnumClass(2);
public static final EnumClass THREE = new EnumClass(3);
public static final EnumClass FOUR = new EnumClass(4);

@Getter
@Setter
private int value;

private EnumClass(int value) {
this.value = value;
}

@Override
public String toString() {
return "EnumClass{" +
"value=" + value +
'}';
}

public void print() {
System.out.println(this.toString());
}

/**
* 为枚举类实现一个values方法
*/

public static EnumClass[] values() {
// 获取枚举类中所有字段
return Stream.of(EnumClass.class.getDeclaredFields())
// 过滤出 public static final的
.filter(field -> {
// 修饰符
int modifiers = field.getModifiers();
return Modifier.isPublic(modifiers)
&& Modifier.isStatic(modifiers)
&& Modifier.isFinal(modifiers);
})
// 取出对应的字段值
.map(field -> {
try {
return field.get(null);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}).toArray(EnumClass[]::new);
}
}

JVM调优工具及优化原则

发表于 2019-05-30 | 分类于 Java基础 , JVM | 热度: ℃
字数统计: 2,046 | 阅读时长 ≈ 7

一些调优工具

jmap

jmap可以查看内存信息、对象实例个数和占用内存大小。

比如下面命令可以查看堆内存中存活的对象实例个数和占用内存大小

1
jmap  -histo:live pid > ./log.txt

image-20220701105039703

还可以通过命令dump JVM的内存

1
jmap -dump:format=b,file=CMSdemo.hprof pid

jstack

jstack可以查看线程堆栈,比如命令:

1
jstack -e pid

jstack能自动输出存在线程死锁的详细信息。(线程状态,互相等待的线程,等待的对象等)
image-20220701105054869

另外在机器CPU飙高之后,可以使用jstack来排查占用cpu时间最多的线程。

  1. top -p pid:此命令是显示pid对应java进程的CPU统计情况。
  2. 按H输出当前进程的线程情况(也可以直接top -H -p pid)
  3. 找到内存和CPU占用最高的线程tid,转为16进制(在jstack线程堆栈中线程id都是16进制的)
  4. 执行jstack -e pid |grep -A 10 16进制线程id即可查看对应的线程堆栈。

当然用Arthas更方便的排查CPU利用率高的线程。

Jinfo

查看正在运行的Java程序的扩展参数。

比如查看jvm参数,可以用命令:

1
jinfo -flags pid

image-20220701105111453

可以用命令,查看jvm的系统参数

1
info -sysprops 75453

jstat

jstast可以看JVM堆内存各部分的使用量,以及加载类的数量。

垃圾回收对象统计(最常用)

1
jstat -gc pid 2000 100 -- 代表输出进程=pid的java堆gc信息,每2000ms打印一次,打印100次

image-20220701105119527

  • S0C:Survivor0的大小,单位kb
  • S1C:S1区的大小
  • S0U:S0的使用大小
  • S1U:S1的使用大小
  • EC:Eden区的大小
  • EU:Eden使用大小
  • OC:Old区当前空间大小
  • OU:Old区使用空间大小
  • CCSC:压缩指针类当前空间大小
  • CCSU:压缩指针类空间使用大小
  • YGC:年轻代GC次数
  • YGCT:年轻代GC消耗时间,单位S
  • FGC:FullGC次数
  • FGCT:FullGC时间
  • GCT:垃圾回收消耗的总时间

堆内存统计

下面的命令可以查看堆内存统计

1
jstat -gccapacity pid

image-20220701105136683

新生代垃圾回收统计

1
jstat -gcnew 75453 1000 10

image-20220701105152448

新生代内存空间

1
jstat -gcnewcapacity 75453 1000 10

image-20220701105203998

老年代内存统计

1
jstat -gcoldcapacity 75453 1000 10

image-20220701105212831

老年代垃圾回收统计

1
jstat -gcold 75453 1000 10

image-20220701105226962

元空间统计

1
jstat -gcmetacapacity 75453  1000 10

image-20220701105248283

JVM运行情况预估

jstat -gc pid命令可以观察到堆内存使用情况和GC情况,则可以通过jstast观测的结果来了解和预估JVM的运行情况。

年轻代对象增长的速率

观察EU(Eden区的使用)来估算每秒eden大概新增多少对象,可以根据负载去调整观察频率。注意系统的高峰期和日长期,在不同时间去观测对象增长速率。

YoungGC触发频率和每次耗时

知道了年轻代对象增长速率,再根据Eden区的大小就可以知道YoungGC的触发频率,还可以根据YGC / YGCT 来计算出YoungGC每次的耗时。

每次YoungGC后有多少对象存活进入到老年代

如果知道了YoungGC没过多久触发了一次,比如1s触发一次,可以用命令
jstat -gc pid 1000 10 来打印最近10次的内存和GC情况,观察SU和OU的增长,因为每次YoungGC之后,存活对象会移动到Survivor区或者晋升到老年代中,所以可以看到每次多少对象进入到老年代。

FullGC触发频率和每次耗时

FullGC的触发频率可以根据每次YoungGC多少对象进入老年代和老年代的大小来大致估算下。(当然老年代有很多参数,比如CMS垃圾回收器中触发CMS回收的阈值;比如元空间不足、手动调用System.gc()也都会触发FullGC)

FullGC的每次耗时可以根据FGCT / FGC来计算得出。

整体优化思路

优化思路主要是根据JVM内存分配和对象流转策略的几个点来的,主要是去优化FullGC的频率。同时要对JVM的内存有一个划分:
image-20220701105257526

优化的原则比如:

  • 选择合适的垃圾回收器
    • CPU单核:Serial可能是最好的选择,单线程进行回收
    • CPU多核:关注吞吐量,那么要选择Parallal Scavenge和Parallel Old的组合
    • CPU多核:关注用户停顿时间,内存不大时可以选择CMS+ParNew
    • CPU多核:关注用户停顿时间,内存大于6G时可以选择G1。
  • 增加内存大小

    • 可能是最有效的减少GC频次的方法,但要注意副作用
      • 内存变大,意味着每次GC都要清除更多的对象,要关注GC时间,且选择停顿时间低的垃圾回收器,比如CMS。
      • 在GC之后回收的很少,且占用率逐步上升,可能是存在内存泄漏,这里要进行排查,不能无脑加内存。
  • 设置合理的内存区域大小和分代晋升年龄

    • 如果Eden区太小,可能会频发触发MinorGC,大多数对象都是在年轻代被GC掉,Eden区域不要太小。
    • 如果Survivor太小,可能会提早晋升到老年代。
    • 分代晋升年龄太小,也会导致本应在年轻代被GC的对象晋升到老年代。
  • 根据对象的动态年龄判定来优化
    对象的动态年龄判定是指的年轻代的对象不一定要在GC分代年龄到达设置阈值才晋升到老年代,如果Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于此年龄的对象可以直接老年代,不需要等待达到晋升老年代的阈值。

这里尽量减少YoungGC之后对象在Survivor区占用的比例,可以适当调大Survivor区的大小,避免生命周期短的对象对象提前晋升到老年代,来增加老年代的压力和FullGC的频率。

  • 大对象直接进入到老年代
    新生代采用复制算法来GC,所以大于阈值的对象会为了避免复制则直接晋升到老年代中。应该尽量避免大对象的产生,因为大多数都是要在年轻代GC掉的对象,进入老年代可能会增加FullGC的频率。(比如未淘汰的缓存对象、未分页查询的SQL结果对象等)

  • 老年代空间分配担保机制
    可能出现的现象:频繁FullGC、FullGC次数比YoungGC次数都多

在MinorGC之前,只要老年代的连续空间小于新生代对象总和或历次晋升的对象平均大小,就会进行一次FullGC。这时如果可预测有生命周期长的对象,那么可以设置较小的晋升阈值或者较小的晋升年龄提前进入老年代;如果对象都是快速被GC的,可以设置较大的老年代空间。都是为了避免因为空间担保机制频繁FullGC。

  • 元空间不足触发FullGC
    可能出现的现象:启动过程频繁fullGC,启动时间变长。

如果不设置元空间的JVM相关参数,那么MetaSpace默认大小21m,在启动过程中会不断FullGC,(自动扩大元空间大小,再次触发FullGC的一个过程)

  • CMS垃圾收集器的concurrent mode failure
    如果在CMS GC的并发标记或者并发清理阶段,再次因为大对象晋升、老年代空间担保机制等原因触发了FullGC,那么会触发Concurrent mode failure,会退化为单线程的Serial Old进行GC,效率低且整个系统会STW。

在确定了大体原因之后,如果要定位有问题的代码,可以使用jmap去看存活对象或者导出内存快照,也可以jstack看占用CPU比较高的线程(一般不断的创建对象代表线程的活跃)区定位具体的问题代码。

innodb的bufferPool

发表于 2019-05-29 | 分类于 mysql | 热度: ℃
字数统计: 299 | 阅读时长 ≈ 1

Buffer Pool的内存结构

image-20220701102240583

  • 由多个缓存数据页和管理这些数据页的链表数据组成。
  • 每个缓存页都有对应的描述数据,各个链表中除了基础节点存储头尾节点、节点个数之外,链表上的都是缓存页的描述数据。
  • LRU链表在缓存页不足时采用一些列冷热数据分离、热区数据分段移动到链表头部等算法来链表尾部的缓存页刷入磁盘。
  • 这三个链表组合使用,BufferPool中的缓存页的使用、刷盘、空闲管理是一个动态的过程。

多个Buffer Pool和Buffer Pool中的chunk划分

image-20220701102249934

  • Buffer Pool可以有多个,这样可以降低线程并发操作链表维护的成本,加大并发能力
  • Buffer Pool中有多个chunk划分,每个chunk是一系列的描述数据块和缓存页,多个chunk共享free、flush、lru链表。这样划分在Buffer Pool动态扩容时可以申请的内存小一点,避免申请多个连续的内存,降低碎片的产生。
  • show Engine innodb status。可以查看Buffer Pool和管理的链表使用情况。

redis命令拾遗(哈希表)

发表于 2019-05-21 | 分类于 redis | 热度: ℃
字数统计: 37 | 阅读时长 ≈ 1

redis哈希表操作命令

redis也支持对hash表结构进行操作,体现在下边几个命令。

hset hash field value

这个命令

JVM参数分类

发表于 2019-05-20 | 分类于 Java基础 , JVM | 热度: ℃
字数统计: 512 | 阅读时长 ≈ 2

JVM参数的分类

常用的JVM参数可以大致的分为三类,下边简单的将JVM的参数做一个分类,作为一个JVM参数的简单总结。

阅读全文 »

redis命令拾遗(字符串操作)

发表于 2019-05-19 | 分类于 redis | 热度: ℃
字数统计: 1,187 | 阅读时长 ≈ 4

前言

前一段时间一直在忙,拉下了一些知识的学习,现在努力追赶修补中。= =

当然也有一些新的知识的学习,但其实更多的是关于一些知识的拾遗。之前在工作当中发现对redis命令掌握的还不是很完善,所以想花比较少的碎片时间去写一下redis常用命令的拾遗。

redis命令

对这些命令的拾遗记录是在网站:http://redisdoc.com上进行学习的,很简单明了,推荐给大家进行学习拾遗。

这里只是把日常会被忽略或者遗忘的点进行一下梳理,并不是每个知识点的一个总结。

字符串操作

####set命令

set可以通过一系列参数进行修改:

  • ex seconds :将键的过期时间设置为seconds秒,具体的命令是set key value EX seconds。等同于执行setex key second value。
  • px milliseconds:和ex一样,只不过单位是毫秒,具体的命令是 set key value PX milliseconds。等同于执行psetex key milliseconds value。
  • nx / xx: set key value nx等价于 setnx key value;set key value xx是当键存在才设置值,没有setxx这个吗命令,这两个设置值失败的时候,set命令会返回nil,而直接使用setnx命令,则返回的是0和1。

setex命令

setex命令效果等价于执行下边两个命令:

1
2
set key value
expire key seconds

但是不同的是,setex是一个原子的操作,它是在同一时间完成设置值和过期时间的操作,经常用在存储缓存时候。

setex设置成功时候 返回ok。

同样psetex只是单位是毫秒而已。

get命令

get命令不用多说,但是注意get命令只是用在字符串操作,如果key对应的值不是字符串类型,那么返回一个错误。

getset命令

此命令的作用是:将key设置为value,并且返回key在被设置之前的值。如果key之前不存在,则返回nil。当键key存在但不是字符串时,会报错。

strlen命令

返回字符串key的长度,当key不是字符串时,返回一个错误。如果key不存在,返回0。

append命令

append命令:如果已经存在key并且它的值是一个字符串,append命令将value追加到key对应值的末尾。如果key不存在,append命令会像执行set key value一样将值设置为对应的key的值。

append命令的返回值是值字符串的长度。

注意append的时间复杂度是平摊o(1)

setrange key offset value

指从偏移量offset开始,用value参数覆写value值。这个命令会确保字符串足够长以便于设置value到对应的偏移量。比如字符串只有5个字符长,但设置的offset是10,那么会在原来字符串值到偏移量之间设置零字节(“\x00”)进行填充。

这个命令的返回值是被修改之后字符串值的长度

getrange key start end

这个命令指的是返回键key对应的字符串值的指定部分,字符串的截取范围由start end两个参数决定(包括start和end在内)。start和end支持负数偏移量,-1代表最后一个字符,-2代表倒数第二个字符。但是注意只能按照字符串顺序获取,不能倒序获取(比如 getrange key -1 -3)

incr key

incr虽然是自增的含义命令,但其实是一个属于字符串的操作,redis并未提供一个专用的整数类型,所以键key存储的值在执行incr命令的时候会被翻译解释为十进制64位有符号整数。

如果incr操作的key值对应不存在,那么先会初始化为0,然后再执行incr命令。

如果key值不能被解释为数字,那么会返回一个错误。

incrby key increment

和incr一样的含义,只不过有递增量为increment。同样的递减是有对应的decr key和decrby key decrement。

incrbyfloat key increment

这个就是针对浮点数的增加计算。注意incrbyfloat命令计算的结果最多只保留小数点后面17位。

mset key value [key value …]

同时为多个键设置值,这个命令是一个原子操作,所有给定键会在同一时间内被设置,并且具有set的特性,会覆盖key对应原来的值。如果仅是在不存在的情况下设置值,可以用msetnx,msetnx也是一个原子操作,如果多个key中有一个key没有设置上,那么所有的key都不会设置对应的值。

mybatis中Mapper接口的代理逻辑

发表于 2019-04-08 | 分类于 mybatis | 热度: ℃
字数统计: 3,575 | 阅读时长 ≈ 16

特点

(1)Mapper文件中有Mapper接口映射关系的唯一标识,比如findById在接口中定义此方法,那么在mapper.xml肯定也有findById标签对应的sql模板,如果写错会在Mybatis启动的时候报错,达到提前校验的目的。

(2)Mapper接口在使用时不用为其实现接口,就可以自动绑定映射其对应的sql模板执行方法。在spring环境中也可以接口注入直接使用。这里注入的是Mapper接口的代理类。

这些功能就在mybatis框架的binding包下。

binging的核心组件及关系如下:

image-20220718170805045

MapperRegistry

MapperRegistry是Mybatis初始化过程中构造的一个对象,主要作用是维护Mapper接口和其对应的MapperProxyFactory。

核心字段:

1
2
3
4
5
6
7
8
public class MapperRegistry {
// 全局唯一的configuration对象,存放解析之后的全部Mybatis配置信息
private final Configuration config;
// 已知的所有映射
// key:mapperInterface,即dao的数据库接口
// value:MapperProxyFactory,即映射器代理工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
}

​

addMapper和getMapper方法

addMapper是为Mapper接口添加对应的代理工厂到kownsMapper中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public <T> void addMapper(Class<T> type) {
// 要加入的肯定是接口,否则不添加
if (type.isInterface()) {
// 加入的是接口
if (hasMapper(type)) {
// 如果添加重复
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

​

getMapper是获取Mapper接口的一个代理对象,也是通过获取到knownMappers map中的MapperFactoryProxy,然后通过newInstance方法来获取新的代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 找出指定映射接口的代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过mapperProxyFactory给出对应代理器的实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

MapperProxyFactory

MapperProxyFactory逻辑很简单,就是生成代理类的工厂。

其中核心字段为:

  • mapperInterface 即要代理的 Mapper接口
  • methodCache 用来存放Method和MapperMethod对象的键值对。 这里MapperMethod就是最终在MethodProxy中用于执行sql的地方。
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
public class MapperProxyFactory<T> {

// 对应SQL的java接口类
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

/**
* MapperProxyFactory构造方法
* @param mapperInterface 映射接口
*/
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

public Class<T> getMapperInterface() {
return mapperInterface;
}

public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 三个参数分别是:
// 创建代理对象的类加载器、要代理的接口、代理类的处理器(即具体的实现)。
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

}

MapperProxy

MapperProxy实现了InvocationHandler接口,用于拦截生成代理类。

代理逻辑是利用Method对应的MapperMethod去执行对应execut方法。

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
public class MapperProxy<T> implements InvocationHandler, Serializable {

private static final long serialVersionUID = -6424540398559729838L;
// 与当前mapperProxy关联的sqlSession对象 用于访问数据库
private final SqlSession sqlSession;
// 被代理的mapper接口
private final Class<T> mapperInterface;
// 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
private final Map<Method, MapperMethod> methodCache;

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) { // 继承自Object的方法
// 直接执行原有方法
return method.invoke(this, args);
} else if (method.isDefault()) { // 默认方法
// 执行默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 找对对应的MapperMethod对象 去执行
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用MapperMethod中的execute方法
return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
}

MapperMethod

MapperMethod是最终执行sql的地方,也是存储了当前执行Mapper接口方法的Method对象。其中包含两个核心字段 sqlCommond、methodSignature。这两个都是其中的静态内部类。

SqlCommand

sqlCommand变量维护了关联sql语句的相关信息。

  • name 即唯一标识
  • type 标识是哪种类型的sql语句

其在构造函数中根据传入的Mapper接口和method方法来初始化SqlCommond。逻辑其实就是从传入接口或其父类中解析出MapperStatement对象,其能标识mapper.xml中的完整的一个sql模板。再从中解析出name和commandType。

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
public static class SqlCommand {

// SQL语句的名称 即唯一标识
private final String name;
// SQL语句的种类,一共分为以下六种:增、删、改、查、清缓存、未知
private final SqlCommandType type;

/**
* 根据传入接口和方法封装sql信息
* @param configuration
* @param mapperInterface
* @param method
*/
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 方法名称
final String methodName = method.getName();
// 方法所在的类。可能是mapperInterface,也可能是mapperInterface的子类
final Class<?> declaringClass = method.getDeclaringClass();
// mapper接口名称、方法名称拼起来做唯一标识
// 到configuration全局配置对象中查找sql语句
// mappedStatement就是Mapper.xml配置文件中一条SQL解析之后得到的对象
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
// 标记flush注解
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}

public String getName() {
return name;
}

public SqlCommandType getType() {
return type;
}

/**
* 找出指定接口指定方法对应的MappedStatement对象
* @param mapperInterface 映射接口
* @param methodName 映射接口中具体操作方法名
* @param declaringClass 操作方法所在的类。一般是映射接口本身,也可能是映射接口的子类
* @param configuration 配置信息
* @return MappedStatement对象
*/
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 数据库操作语句的编号是:接口名.方法名
String statementId = mapperInterface.getName() + "." + methodName;
// configuration保存了解析后的所有操作语句,去查找该语句
if (configuration.hasStatement(statementId)) {
// 从configuration中找到了对应的语句,返回
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 说明递归调用已经到终点,但是仍然没有找到匹配的结果
return null;
}
// 从方法的定义类开始,沿着父类向上寻找。找到接口类时停止
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
// 递归查找父类
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}

​

MethodSignature

MethodSignature主要维护了当前接口方法的信息,如返回值类型、参数和实际入参的绑定关系(运用了ParamNameResolver工具类)等。

在methodSignature.convertArgsToSqlCommandParam方法中,也是处理了@Param注解与sql模板中的参数绑定关系。

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
public static class MethodSignature {

// 返回类型是否为集合类型
private final boolean returnsMany;
// 返回类型是否是map
private final boolean returnsMap;
// 返回类型是否是空
private final boolean returnsVoid;
// 返回类型是否是cursor类型
private final boolean returnsCursor;
// 返回类型是否是optional类型
private final boolean returnsOptional;
// 返回类型
private final Class<?> returnType;
// 如果返回为map,这里记录所有的map的key @MapKey注解
private final String mapKey;
// resultHandler参数的位置
private final Integer resultHandlerIndex;
// rowBounds参数的位置
private final Integer rowBoundsIndex;
// 引用参数名称解析器
private final ParamNameResolver paramNameResolver;

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
// rowBoundsIndex和resultHandlerIndex
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
// 解析参数名称的工具类
this.paramNameResolver = new ParamNameResolver(configuration, method);
}

public Object convertArgsToSqlCommandParam(Object[] args) {
// 通过传入实参获取 实参和参数名称的映射表
return paramNameResolver.getNamedParams(args);
}

public boolean hasRowBounds() {
return rowBoundsIndex != null;
}

public RowBounds extractRowBounds(Object[] args) {
return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
}

public boolean hasResultHandler() {
return resultHandlerIndex != null;
}

public ResultHandler extractResultHandler(Object[] args) {
return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
}

public String getMapKey() {
return mapKey;
}

public Class<?> getReturnType() {
return returnType;
}

public boolean returnsMany() {
return returnsMany;
}

public boolean returnsMap() {
return returnsMap;
}

public boolean returnsVoid() {
return returnsVoid;
}

public boolean returnsCursor() {
return returnsCursor;
}

/**
* return whether return type is {@code java.util.Optional}.
* @return return {@code true}, if return type is {@code java.util.Optional}
* @since 3.5.0
*/
public boolean returnsOptional() {
return returnsOptional;
}

// 返回指定参数的index
private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
Integer index = null;
final Class<?>[] argTypes = method.getParameterTypes();
for (int i = 0; i < argTypes.length; i++) {
if (paramType.isAssignableFrom(argTypes[i])) {
if (index == null) {
index = i;
} else {
throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
}
}
}
return index;
}

private String getMapKey(Method method) {
String mapKey = null;
if (Map.class.isAssignableFrom(method.getReturnType())) {
final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
if (mapKeyAnnotation != null) {
mapKey = mapKeyAnnotation.value();
}
}
return mapKey;
}
}

execute方法

最终sql的执行都是通过MapperMethod的execute方法执行,这里依赖了其中的sqlCommond和methodSignature两个变量。

execute核心逻辑就是根据具体的sqlCommondType来选择执行具体的方法。其中也处理了不同的返回值

  • 对于Insert、Update、delete类型的sql执行,返回值回采用rowCountResult方法来处理,内部做了对影响行数的处理(可以直接返回boolean类型)
  • 对应Select类型,

    • 如果有executeWithResultHandler类型的参数,会按照resultHandler的回调来处理返回值。
    • 如果方法返回值为集合类型或是数组类型,则会调用 executeForMany() 方法,底层依赖 SqlSession.selectList() 方法进行查询,并将得到的 List 转换成目标集合类型。
    • 如果方法返回值为 Map 类型,则会调用 executeForMap() 方法,底层依赖 SqlSession.selectMap() 方法完成查询,并将结果集映射成 Map 集合。
    • 针对 Cursor 以及 Optional返回值的处理,也是依赖的 SqlSession 的相关方法完成查询的,这里不再展开。
    • 针对单条数据,也会依赖sqlSession.selectOne方法完成查询。
    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
    /**
    * 执行映射接口中的方法 MapperMethod的核心方法
    * execute方法会根据要执行的sql语句的具体类型执行sqlsession的具体方法完成数据库操作
    * @param sqlSession sqlSession接口的实例,通过它可以进行数据库的操作
    * @param args 执行接口方法时传入的参数
    * @return 数据库操作结果
    */
    public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { // 根据SQL语句类型,执行不同操作
    case INSERT: { // 如果是插入语句
    // 将参数顺序与实参对应好
    Object param = method.convertArgsToSqlCommandParam(args);
    // 执行操作并返回结果
    // rowCounntResult方法会根据方法的返回值类型对结果进行转换
    result = rowCountResult(sqlSession.insert(command.getName(), param));
    break;
    }
    case UPDATE: { // 如果是更新语句
    // 将参数顺序与实参对应好
    Object param = method.convertArgsToSqlCommandParam(args);
    // 执行操作并返回结果
    result = rowCountResult(sqlSession.update(command.getName(), param));
    break;
    }
    case DELETE: { // 如果是删除语句MappedStatement
    // 将参数顺序与实参对应好
    Object param = method.convertArgsToSqlCommandParam(args);
    // 执行操作并返回结果
    result = rowCountResult(sqlSession.delete(command.getName(), param));
    break;
    }
    case SELECT: // 如果是查询语句
    /**
    * 如果在方法参数列表中有 ResultHandler 类型的参数存在,则会使用 executeWithResultHandler() 方法完成查询,底层依赖的是 SqlSession.select() 方法,结果集将会交由传入的 ResultHandler 对象进行处理。
    * 如果方法返回值为集合类型或是数组类型,则会调用 executeForMany() 方法,底层依赖 SqlSession.selectList() 方法进行查询,并将得到的 List 转换成目标集合类型。
    * 如果方法返回值为 Map 类型,则会调用 executeForMap() 方法,底层依赖 SqlSession.selectMap() 方法完成查询,并将结果集映射成 Map 集合。
    * 针对 Cursor 以及 Optional返回值的处理,也是依赖的 SqlSession 的相关方法完成查询的,这里不再展开。
    */
    if (method.returnsVoid() && method.hasResultHandler()) { // 方法返回值为void,且有结果处理器
    // 使用结果处理器执行查询
    executeWithResultHandler(sqlSession, args);
    result = null;
    } else if (method.returnsMany()) { // 多条结果查询
    result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) { // Map结果查询
    result = executeForMap(sqlSession, args);
    } else if (method.returnsCursor()) { // 游标类型结果查询
    result = executeForCursor(sqlSession, args);
    } else { // 单条结果查询
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
    if (method.returnsOptional()
    && (result == null || !method.getReturnType().equals(result.getClass()))) {
    result = Optional.ofNullable(result);
    }
    }
    break;
    case FLUSH: // 清空缓存语句
    result = sqlSession.flushStatements();
    break;
    default: // 未知语句类型,抛出异常
    throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    // 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
    throw new BindingException("Mapper method '" + command.getName()
    + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
    }

​

总结

重点介绍了 MyBatis 中的 binding 模块,正是该模块实现了 Mapper 接口与 Mapper.xml 配置文件的映射功能。

首先,介绍了 MapperRegistry 这个注册中心,其中维护了 Mapper 接口与代理工厂对象之间的映射关系。

然后,分析了 MapperProxy 和 MapperProxyFactory,其中 MapperProxyFactory 使用 JDK 动态代理方式为相应的 Mapper 接口创建了代理对象,MapperProxy 则封装了核心的代理逻辑,将拦截到的目标方法委托给对应的 MapperMethod 处理。

最后,详细讲解了 MapperMethod,分析了它是如何根据方法签名执行相应的 SQL 语句。

dubbo基础(五)——dubbo接口的特性设置

发表于 2019-03-31 | 分类于 dubbo | 热度: ℃
字数统计: 1,405 | 阅读时长 ≈ 5

dubbo的一些配置

之前的文章中写了dubbo的初步使用和dubbo和springboot的使用整合,这里来总结下dubbo框架暴露接口常用的配置项。

阅读全文 »

一条sql执行的过程

发表于 2019-03-22 | 分类于 mysql | 热度: ℃
字数统计: 1,152 | 阅读时长 ≈ 4

MySql内部组件的结构

image-20220701101919878

如图所示:客户端发来一条SQL语句之后,Mysql内部组件会:

  • 连接器:管理客户端发来的连接,对其中的用户校验权限,管理MySql内部的连接池。
  • 词法分析器:对Sql语法、词法进行校验、分析。解析出一个语法树让MySql理解要去做什么事情。
  • 优化器:会对SQL可选的索引等条件计算成本,生成执行计划给执行器去执行。
  • 执行器:按照执行计划,去调用存储引擎的接口,来获取SQL语句的结果。

一条sql交互的过程

image-20220701101933226

binlog是什么

binlog是MySqlServer层实现的二进制逻辑日志,和redo log不同,redo log记录的是物理日志(表空间 + 区号 + 数据页 + 偏移量 + 修改内容),binlog的内容大概是(user表id = 1的记录name更新为xxx)是一个逻辑日志。同时redo log是innodb存储引擎实现事务中的持久性特性而存在的,在其他存储引擎不存在,而binlog是mysql都有的。

  • Mysql Server层逻辑日志。(所有引擎共享)
  • Binlog是逻辑日志,记录的是语句的原始逻辑。
  • Binlog是追加写的,不限制大小,不会像redo log覆盖几个文件循环写。

在事务Commit时,会写binlog,这个过程存在于redo log的二阶段提交过程。因为binlog常用于数据恢复和主从同步,所以要保证redo和binlog的一致性采用了两阶段。

Innodb存储引擎执行sql的过程(以更新语句为例)

image-20220701101948332

  1. 从磁盘加载数据到Buffer Pool中,innodb中的sql操作都需要加载数据到Buffer Pool中。加载数据都是以数据页的形式加载到内存中,Buffer Pool中也是数据页的形式存在的。
  2. 写undo log文件,一次更新语句需要写undolog保证事务的原子性(回滚时找到历史版本的数据)。同时undo log形成版本链,和ReadView来完成MVCC的并发控制。
  3. 更新Buffer Pool内存中记录的数据,更新完之后内存数据和磁盘数据不一致,数据页为脏页。
  4. 写redo log到内存中的redo log buffer,为后面redo log刷盘做准备。
  5. 准备提交事务的阶段,redo log刷新到磁盘,有几种策略(立即刷入、刷入OS缓存、不刷入)。这个阶段也可以理解为redo log的prepare阶段。(两阶段来保证redo log和binlog的一致性)
  6. 准备提交事务的阶段,在mysql server层的binlog写入磁盘,也有几种刷盘策略。
  7. 提交事务阶段,redolog写入commit标志,redolog的二阶段,此阶段之后事务才算真正提交。
  8. 之后的Buffer Pool会根据lru、flush链表的刷盘策略将脏页刷入磁盘。(此步不在事务阶段,线程异步刷入)
  • 为什么在事务阶段写这么多日志?
    因为innodb引擎要实现事务,undo log其实保证了事务回滚时的原子性,回滚到undo log版本链上的历史数据。

而redo log用于实现持久性,只要redo log和binlog的两阶段完成,就能保证这次变更是crash safe的,不会丢失。

binlog也会用于数据恢复和主从同步,是server层面的二进制逻辑日志,记录了语句信息。

  • 为什么不直接写入磁盘数据文件?

写那么多的日志都是在文件末尾追加写,相当于是追加写,是顺序IO;而因为更新数据要维护不同的索引树,数据的分布在磁盘上访问是随机IO,效率不是一个数量级的,这样innodb选择去写这些日志,异步线程去刷新内存中的脏页到磁盘上,来提高事务的效率。

  • 在压测数据库可以关注哪些指标?
    • QPS和TPS
    • IOPS:机器的随机IO能力。每秒可以执行多少个随机IO请求。这个指标很关键,访问磁盘中的数据就是随机IO,压测时候可以观察这个性能。
    • 吞吐量:机器的磁盘每秒可以读写的字节数据量。事务过程中会写redo binlog等日志文件,这个指标决定了大量redo log刷盘的性能。
    • latency:每写入一条数据的延迟。越低越好。
    • CPU负载:肯定是重要指标
    • 网络负载:如果带宽打满,肯定也是瓶颈。
    • 内存负载:内存吃紧肯定也是瓶颈。
1…678…12
夸克

夸克

愿赌服输

114 日志
32 分类
121 标签
GitHub E-Mail csdn
© 2022 夸克 | Site words total count: 168.9k
|
主题 — NexT.Muse v5.1.4
博客全站共168.9k字

载入天数...载入时分秒...