零碎知识点总结

静态方法 & 非静态方法的区别

  • 本质区别:静态方法与非静态方法的本质区别:静态方法在程序初始化后会一直贮存在内存中,不会被垃圾回收器回收,非静态方法只在该类初始化后贮存在内存中,当该类调用完毕后会被垃圾回收器收集释放。
  • 调用方式区别:在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  • 访问限制不同:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。如果想要在静态方法中访问本类的非静态方法,则需要先实例化类,再实例化类.实例方法。
  • 之所以不允许静态方法直接访问实例成员变量,是因为实例成员变量是属于某个对象的,而静态方法在执行时,并不一定存在对象。同样,因为实例方法可以访问实例成员变量,如果允许静态方法调用实例方法,将间接地允许它使用实例成员变量,所以它也不能调用实例方法。基于同样的道理,静态方法中也不能使用关键字this。
  • 声明方式不同:静态方法声明必须有static,调用时使用类名+.+静态方法名的方式。非静态方法不能使用static关键字,调用时需初始化该方法所属类。

零拷贝

https://www.jianshu.com/p/275602182f39

https://blog.csdn.net/CringKong/article/details/80274148

简而言之,就是减少了拷贝次数,直接使用 DMA(Direct Memory Access,直接内存存取),并且减少了上下文切换的次数,所谓的零拷贝,意思应该是指不需要 CPU 参与拷贝就能完成数据的拷贝过程。

Tip: kafka之所以这么快,就是用了零拷贝技术

  1. 传统的读写(这里假设是将文件中的数据读取到网络上):

    • 首先是从磁盘通过 DMA 加载到内存,第一次拷贝,然后内存将其放到内核缓冲区,同时完成了一次从 用户态到 内核态的上下文切换;
    • 然后是内核缓冲区拷贝到用户缓存区,第二次拷贝,同时又将内核态切回到用户态;
    • 然后是用户缓冲区拷贝至 Socket 缓冲区,第三次拷贝,同时将用户态切回内核态;
    • 然后 Socket 缓冲区通过 DMA 将数据拷贝至网络引擎,然后结束 wirte,此时第四次拷贝,并最终将内核态切回用户态。

    历经了 4 次拷贝,4 次上下文切换,其中包括2次需要 CPU 参与工作(第二步和第三步)。

  2. mmap

    • 对内核缓冲区和用户缓冲区做地址映射,这样的话上述第二步和第三步可以直接归为一步,也就是减少了一次拷贝,但是并没有减少上下文切换的次数。
  3. sendfile

    • 这是真正的零拷贝技术, Java 中的 NIO 就是通过这个底层实现的,也就是第二步中直接将内核缓冲区通过 DMA 发送至网络引擎,将内核态切换为用户态,此时全程不需要 CPU 参与工作,且拷贝次数降为2次,上下文切换次数也同时降为2次。

Spring AOP原理

Spring MVC 过程

Spring Ioc

Spring Bean

Spring Bean 的生命周期

抽象类和接口的区别

抽象类:一个包含抽象方法的类,抽象方法是指用 abstract 修饰,没有实现的方法;

接口:一个方法的集合;

区别:

  1. 抽象类具有类的特性,可以 public、private、protected 修饰类,而接口只能使用 public,且抽象类具有构造函数、成员变量,接口都不具备;
  2. 抽象类和接口都不能实例化,但是抽象类中可以有实现好的方法,可以被继承,继承者可以选择实现部分抽象类的方法,然后交由子类继续去完成抽象类中未完成的抽象方法的重写,但是接口必须把所有定义的方法全部实现;
  3. 抽象类可以被继承,所以必须满足单继承,但是接口不一样,接口没有限制。

Java中public,protected,private以及默认的访问权限作用域

image-20200307185707927

内部类

https://www.cnblogs.com/dolphin0520/p/3811445.html 写的真的太好了…

内部类基础

内部类就是一个类可以放到另外一个类或者方法里面,总共分为四种情况:

  1. 成员内部类;「放在类中」
  2. 局部内部类;「放在方法中」
  3. 匿名内部类;
  4. 静态内部类。

成员内部类

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
public class InnerClassDemo {
public int age = 18;
public int sex = 1;
// 外部类只能是 public or default
// 跟外部类不一样,这里可以有private、public、protected、不加(default)
class Inner {
public int age = 20; // 当内部类的变量和外部类变量名字一致,注意对象,如果要使用外部对象
// 则是 外部类名.this
public int inner_sex = 1;
public void showAge() {
int age = 25;
System.out.println(sex);
System.out.println(age);//25
System.out.println(this.age);//20
System.out.println(InnerClassDemo.this.age);//18 内部类访问外部类的对象,使用 类名.this
}
}
// 虽然成员内部类可以无条件地访问外部类的成员,
// 而外部类想访问成员内部类的成员却不是这么随心所欲了。
// 在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:
public void get_inner_sex(){
Inner inner = new Inner();
inner.showAge();
System.out.println(inner.inner_sex);
}

}

class Test {
public static void main(String[] args) {
// 注意写法:new 类名().new 内部类名()
InnerClassDemo.Inner innerClassDemo = new InnerClassDemo().new Inner();
innerClassDemo.showAge();

}
}

成员内部类有四点需要注意:

  1. 内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
  2. 当内部类和外部类有着一样的变量时,需要注意的是,如果用 this.变量名 获得的是内部类的成员变量,如果想要获得外部类的成员变量或者成员方法需要使用 外部类名.this.变量名外部类名.this.成员方法名
  3. 虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
  4. 成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。也就是 new 类名().new 内部类名()。

局部内部类

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
package 内部类;

/**
* 与成员内部类基本一致,但是有2处不一样
* 1.局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
* 2. 内部类此时不能有修饰符
*/
public class InnerClassDemo2 {
public int age = 18;
public void example(){
int age = 20;
// 不能有修饰!
class Inner2{
public int age = 25;
public void inner_method(){
System.out.println(age); // 25
System.out.println(this.age); // 25
System.out.println(InnerClassDemo2.this.age); //18
}
}
new Inner2().inner_method();
}

}

class Test2 {
public static void main(String[] args) {
InnerClassDemo2 innerClassDemo2 = new InnerClassDemo2();
innerClassDemo2.example();
}
}

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

匿名内部类

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
package 内部类;

public class InnerClassDemo3 implements Runnable{

@Override
public void run() {
System.out.println("不使用内部类,得先声明一个类并实现接口");
}
}
class Test3{
public static void main(String[] args) {
// 使用内部类,可以加持 lambda 表达式
new Thread(() -> System.out.println("内部类真好用")).start();
// 使用内部类,但是没用 lambda 表达式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("内部类真好用");
}
}).start();

// 不使用内部类,又臭又长
InnerClassDemo3 innerClassDemo3 = new InnerClassDemo3();
new Thread(innerClassDemo3).start();
}
}

很明显,内部类 + lambda表达式 使得代码非常的干净。使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。

当然,需要注意的是:

匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

静态内部类

  1. 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字 static;
  2. 静态内部类是不需要依赖于外部类的实例对象的,所以在别的类中调用内部类时,不需要先去获取外部类的对象,这点和类的静态成员属性有点类似;
  3. 并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

为了对比第一种区别,我直接在第一种代码上做了部分改动:

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
package 内部类;

public class InnerClassDemo4 {
public int age = 18;
public int sex = 1;
public static int static_age = 188;
// 外部类只能是 public or default
// 跟外部类不一样,这里可以有private、public、protected、不加(default)
static class Inner {
public int age = 20; // 当内部类的变量和外部类变量名字一致,注意对象,如果要使用外部对象
// 则是 外部类名.this
public int inner_sex = 1;
public void showAge() {
int age = 25;
// System.out.println(sex); // 此时就不能使用了,因为是 非 static
System.out.println(age);//25
System.out.println(this.age);//20
// System.out.println(InnerClassDemo4.this.age);//18 内部类访问外部类的对象,使用 类名.this --->此时会报错
System.out.println(InnerClassDemo4.static_age); //188
}
}
// 虽然成员内部类可以无条件地访问外部类的成员,
// 而外部类想访问成员内部类的成员却不是这么随心所欲了。
// 在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:
public void get_inner_sex(){
Inner inner = new Inner();
inner.showAge();
System.out.println(inner.inner_sex);
}

}

class Test4 {
public static void main(String[] args) {
// 注意写法:new 类名().new 内部类名()
// InnerClassDemo4.Inner innerClassDemo = new InnerClassDemo4().new Inner();// 此时会报错
InnerClassDemo4.Inner innerClassDemo = new InnerClassDemo4.Inner();
innerClassDemo.showAge();

}
}

从上述代码可以看到,有两个地方和第一处不一样:

  1. 内部类中无法调用外部类非 static 变量;
  2. 其他类调用内部类时,无需先实例化外部类了,可以直接外部类名.内部类名()。

深入理解内部类

1.为何在成员内部类中,内部类可以随意访问外部类?

虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

2.静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

内部类的使用场景

为什么在Java中需要内部类?总结一下主要有以下四点:

  1. 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响,内部类使得多继承的解决方案变得完整
  2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏;「ThreadLocal & ThreadLocalMap」
  3. 方便编写线程代码。

最后补充一点知识:关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

  1)成员内部类的引用方式必须为 外部类名.内部类名;

  2)构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。这段代码摘自《Java编程思想》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WithInner {
class Inner{

}
}
class InheritInner extends WithInner.Inner {

// InheritInner() 是不能通过编译的,一定要加上形参
InheritInner(WithInner wi) {
wi.super(); //必须有这句调用
}

public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner obj = new InheritInner(wi);
}
}

反射机制

反射的基本用法:https://blog.csdn.net/a745233700/article/details/82893076

  1. 获取类(第三个方法最常用)
  • // 方法一:通过实例对象获取,不过说实话,都能拿到对象了,还要类对象干嘛..
    // 不过有种用途,就是比如可以越过泛型检查,添加数据
    // 比如一个对象是 ArrayList\,可通过反射添加一个 Integer的数
    // 因为java的泛型是在编译之后擦除的…存在漏洞哈哈哈
    // 例子见: https://blog.csdn.net/sinat_38259539/article/details/71799078

    1
    2
    3
    > InnerClassDemo1 innerClassDemo1 = new InnerClassDemo1();
    > Class innerClass = innerClassDemo1.getClass();
    >
  • 方法二:类名.class,这个需要导包…
1
2
> Class innerClass = InnerClassDemo1.class;
>
  • 方法三:最常用,使用类名(相对路径)
1
2
> Class innerClass = Class.forName("内部类.InnerClassDemo1");
>
  1. 获得实例对象
1
2
3
4
> Class.forname("").getConstructor(String.class).newinstance() // 可用于生成有参的对象
>
> Class.forname("").newInstance() // 可生成无参的对象
>
  1. 拿到实例属性、实例方法
1
2
3
4
5
6
7
8
9
> // 拿到对象的属性
> Field field = innerClass.getField("sex");
> // 如果参数是private,可以擦除
> // field.setAccessible(true);
> int a = (int) field.get(innerClassDemo1);
> // 拿到方法
> Method showAge = innerClass.getMethod("get_inner_sex");
> showAge.invoke(innerClassDemo1);
>
  1. 拿到静态变量或者静态对象
1
2
3
4
5
6
> // 拿到类的属性
> Field ff = innerClass.getField("haha");
> int b = (int) ff.get(innerClass);
> // 或者 int b = (int) ff.get(innerClassDemo1);
> System.out.println("b: " + b);
>

动态代理 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
    package Proxy;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;

    public class test {
    public static void main(String[] args) {
    // 相当于做生意的过程,比如我去医院整容,首先我要确保我有钱(也就是有接口)
    Universities person = new Universities();
    // 其次,我得去医院找到相应的医生,也就是把我想代理的对象,即我本人,告知给医生
    // 医生肯定得有能整容的技术,那就是得继承 InvocationHandler
    InvocationHandler dynamic_person = new Dynamic(person);
    // 进行交易的过程,一手交钱一手交换,医生拿到钱,会返回一个有钱的处理好的美女,也就是代理完成了
    // 这里必须强调代理返回的是 接口对象,也就是医生只会对有钱人进行代理,没钱的代理就失败了
    // 如果最开始我没钱,我就去找医生了,那在这一步交易的过程就会出错,因为医生只会处理有钱人,并且返回有钱人的代理好的对象
    // 交易的三个参数,也就是本人、钱、还有医生。本人就是实例对象,钱就是前文说的接口,医生就是上面的动态代理对象
    // 有钱人得到代理后的对象,就可以为所欲为了

    Person pp = (Person) Proxy.newProxyInstance(person.getClass().getClassLoader(),person.getClass().getInterfaces(),dynamic_person);
    // pp 就是 被代理对象 被 代理对象 代理后的对象
    pp.dance();
    System.out.println("-----------");
    pp.play();



    }
    }
  • Person 接口

1
2
3
4
5
6
7
package Proxy;

public interface Person {
void play();
void dance();

}
  • Universities 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Proxy;

public class Universities implements Person {
@Override
public void play() {
System.out.println("I like play computer");

}

@Override
public void dance() {
System.out.println("I like dance");
}
}
  • 动态代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class Dynamic implements InvocationHandler {
// 整容的人是谁,我得知道
public Object obj;
public Dynamic(Object obj){
this.obj = obj;
}
// 整容的过程就是这的
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始为" + method.getName() + "方法进行代理");
Object result = method.invoke(obj,args);
System.out.println("结束" + method.getName() + "方法的代理");
return result;
}
}

Integer 和 int 的区别

基本对比

  1. Integer是int的包装类;int是基本数据类型;
  2. Integer变量必须实例化后才能使用;int变量不需要;
  3. Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值;
  4. Integer的默认值是null;int的默认值是0。

深入对比 demo

  1. 由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
1
2
3
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
  1. Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
1
2
3
Integer i = new Integer(100);
int j = 100
System.out.print(i == j); //true
  1. 非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象「注意是 -128 ~ 127 才有缓存,其他的数也是会重新开辟空间」,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
1
2
3
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
  1. 对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。
1
2
3
4
5
6
7
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true

Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

缓存部分可以看到源代码的写法:

1
2
3
4
5
6
7
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
  1. “== “ 和 equals() 在 Integer 中的不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Integer vs Int
*/
class Test3{
public static void main(String[] args) {
Integer a1 = 200;
Integer a11 = 200;
Integer b1 = 200;
Integer b11 = 200;
System.out.println(a1.equals(b1)); // true,equals 会自动拆箱
System.out.println(a1 == b1); // false,== 只有遇到了算术运算才会拆箱,此时由于不在缓存范围内,所以是不同的对象
System.out.println(a1 + a11 == b1 + b11); // true,算术运算,所以拆箱了

// 不用 new 得到的 Integer 对象,实际上跟 String.inner 一样
// 常量池没有,就创建一个扔常量池中,但是这里的 Integer 有限制,是 -128 ~ 127
// 所以这里的 127 都是常量池中的数字,也就是所谓的缓存机制,所以是同一个对象
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2.equals(b2)); // true,拆箱了
System.out.println(a2 == b2); // true,虽然未拆箱,但是在缓存范围,是一个对象
}
}

Kafka 的相关问题

  1. 为何使用 kafka
  • 解耦,规定各模块输入输出格式就可以了,各模块耦合性下降;
  • 异步,消息来了,不用立即接收,可以慢慢消费,也达到了一个削峰的作用。
  1. 为何不用其他的 MQ?
  1. Kafka 吞吐量最大;
  2. 稳定性好
  1. 为什么 kafka 很快?
  • 和Producer与Consum收发消息快的原因是用了 NIO的socket有关的功能,也就是 I/O 复用技术「底层使用 epoll」,用selector去轮询,不需要在连接producer和consumer之后一直等待,等到producer准备好数据之后才去new一个线程跟他传输消息;
  • 然后消息传输到服务器上之后使用NIO中的File有关的功能,也就是零拷贝技术中的 mmap,内存地址和磁盘地址映射,读写内存就相当于直接读写磁盘,所以很快。
  • 其还提供分区,这样的话可以多个broker同时写磁盘,所以就很快
  • 然后在将broker中数据直接发给consumer中是采用了sendfile这个零拷贝技术,直接通过DMA将内核缓冲区中的数据传送到socket缓冲区「FileChannel.transferTo/transferFrom」

https://www.jianshu.com/p/7863667d5fa7 写的真的好

  1. kakfa会丢消息吗

会的,当leader还在进行同步复制的时候,副本复制给其他follower,还没复制完成,leader就挂了,此时再次选举之前的部分数据就丢失了。

  1. kafka如何保证不丢消息

Producer 端是采用了 ack 机制,发送消息之后会有一个 ack的反馈;

Consumer端是采用了offset进行一个控制,保证不丢消息,每次都记录最后一次读取的lastoffset

  1. kafka消息有序吗

同一个partition是有序的,但是不同partition之间是无序的。

  1. 如何防止数据丢失

多副本冗余的高可用机制。。。。

Leader会跟踪与其保持同步的Replica列表,该列表称为ISR(即in-sync Replica),保持同步的副本。
Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。
而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率。Follower可以批量的从Leader复制数据,这样极大的提高复制性能(批量写磁盘),极大减少了Follower与Leader的差距。 //批量复制数据方式,防止数据丢失1

一条消息只有被ISR里的所有Follower都从Leader复制过去才会被认为已提交。这样就避免了部分数据被写进了Leader,还没来得及被任何Follower复制就宕机了,而造成数据丢失(Consumer无法消费这些数据)。 //防止数据丢失2

  1. 如何判断 kafka 中的broker挂掉
  1. zookeeper 的心跳机制

  2. ISR机制,每个leader会关注follow的同步状况,如果不能及时的更新leader的写操作,延时太久,leader就会将其移除

  1. Kafka 的消息传递语义
  • At most once - 消息传递过程中有可能丢失,丢失的消息也不会重新传递,其实就是保证消息不会重复发送或者重复消费
  • At least once - 消息在传递的过程中不可能会丢失,丢失的消息会重新传递,其实就是保证消息不会丢失,但是消息有可能重复发送或者重新被消费 「默认」
  • Exactly once - 这个是大多数场景需要的语义,其实就是保证消息不会丢失,也不会重复被消费,消息只传递一次

在 Producer 端实现 At least once:

在0.11.0.0版本,Kafka的Producer开始支持了幂等发送消息了,其实说白了,就是重复发送一条消息不会导致broker server中存储两条一样的消息了。这样的话Kafka在消息的发送的语义就可以达到Exactly once了

为了实现幂等发送消息,在0.11.0.0版本,Kafka给每一个Producer一个唯一的ID,以及给发送的每一条消息一个唯一的sequence number,利用这两个信息就可以在broker server段对消息进行去重了。

如果想要实现 Exactly once:

协调好消费者消费的消息offset的保存和处理消息结果的保存之间的关系就可以达到Exactly once的语义,就是说,当消费的消息offset的保存成功以及处理消息结果的保存成功,则算成功,如果两者有一个失败的话,那么就需要回滚两个保存了(和事务有点像)。

  1. kafka中的ack机制

第一种选择是把acks参数设置为0,意思就是我的KafkaProducer在客户端,只要把消息发送出去,不管那条数据有没有在哪怕Partition Leader上落到磁盘,我就不管他了,直接就认为这个消息发送成功了。

如果你采用这种设置的话,那么你必须注意的一点是,可能你发送出去的消息还在半路。结果呢,Partition Leader所在Broker就直接挂了,然后结果你的客户端还认为消息发送成功了,此时就会导致这条消息就丢失了。

第二种选择是设置 acks = 1,意思就是说只要Partition Leader接收到消息而且写入本地磁盘了,就认为成功了,不管他其他的Follower有没有同步过去这条消息了。

这种设置其实是kafka默认的设置,大家请注意,划重点!这是默认的设置

也就是说,默认情况下,你要是不管acks这个参数,只要Partition Leader写成功就算成功。

但是这里有一个问题,万一Partition Leader刚刚接收到消息,Follower还没来得及同步过去,结果Leader所在的broker宕机了,此时也会导致这条消息丢失,因为人家客户端已经认为发送成功了。

最后一种情况,就是设置acks=all,这个意思就是说,Partition Leader接收到消息之后,还必须要求ISR列表里跟Leader保持同步的那些Follower都要把消息同步过去,才能认为这条消息是写入成功了。

如果说Partition Leader刚接收到了消息,但是结果Follower没有收到消息,此时Leader宕机了,那么客户端会感知到这个消息没发送成功,他会重试再次发送消息过去。

此时可能Partition 2的Follower变成Leader了,此时ISR列表里只有最新的这个Follower转变成的Leader了,那么只要这个新的Leader接收消息就算成功了。

  1. ack = all 就能保证数据不丢失吗?

当然不是,如果你的Partition只有一个副本,也就是一个Leader,任何Follower都没有,你认为acks=all有用吗?

当然没用了,因为ISR里就一个Leader,他接收完消息后宕机,也会导致数据丢失。

所以说,这个acks=all,必须跟ISR列表里至少有2个以上的副本配合使用,起码是有一个Leader和一个Follower才可以。

  1. 最后总结一下 kafka 如何保证生产者和消费者不丢消息,如何保证生产者和消费者不生产和消费重复消息,如何保证消息有序?
  1. 先回答第一个问题。保证生产者和消费者不丢消息,首先由于 kafka 的默认的消息传递语义是 at least once,就是确保生产者消息不丢,确保的手段就是 ack = all 机制,加上设置尽量多的 broker,达到高可用,这样可以使得就算 leader 挂了,还有其他的 follower 能够顶上去。确保消费者不丢消息,需要我们程序员自己去确保,因为 kafka 并没有提供这样的消息语义,因为它是允许丢消息的,当消费者消息丢了之后,我们无需去改动消费的offset,这样就不会去导致消费不到消息,我们通过这种手动控制 offset 的方式也同样能够确保不会去重复消费,因为我们在客户端已经做了一个事务的原子操作,绑定了更改 offset 和消费消息这两件事。

  2. 第二个问题,如何保证生产者不重复生产同样的消息, kafka 的机制是每个Producer都有一个 id,然后每一条消息都有一个 id,只要服务器收到了,就会记录下该id,然后就算重复发送了也会幂等,所以就解决了该问题,至于消费者如何解决消费重复问题,我在上面已经讲过了,手动控制 offset 就不会重复消费了。

  3. 第三个问题,如何保证有序,Apache Kafka官方保证了partition内部的数据有效性(追加写、offset读);为了提高Topic的并发吞吐能力,可以提高Topic的partition数,并通过设置partition的replica来保证数据高可靠;

    但是在多个Partition时,不能保证Topic级别的数据有序性。

为何使用 MongoDB

  • 需求总是变化,no-sql数据库改动小
  • 速度快,mmap内存映射
  • 文档多,社区资源多
  • 支持范围查询,python和node支持的也很好,mongoengine
Thank you for your accept. mua!
-------------本文结束感谢您的阅读-------------