0%

什么是CAS

CAS compare and swap的缩写,中文翻译成 比较并替换

CAS 操作包含三个操作数 内存位置(V)、预期原值(A) 和新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回改位置的值。在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。

CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

CAS的目的

利用CPU的 CAS指令,同事接祖JNI来完成java的非阻塞算法。
其他院子操作都是利用类似的特性完成的。
而这个JUC都是建立在CAS之上的,同时对于synchronized阻塞算法,JUC在性能上有了很大的提升。

CAS 存在的问题

CAS 虽然很高效的解决原子操作,但是CAS任然存在三大问题:ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。
  • ABA问题
    因为CAS在操作值得时候需要判断值有没有发生变化,没有发生变化则更新。
    但是如果一直原来是A,变成了B,又变成了A,那么使用CAS进行检查时发现值么有发生变化,但是实际值却是变化了。
    ABA问题的解决思路就是使用版本号。在变量钱追加版本号,每次变量更新的时候版本号加一
    那么A -B -A 就会变成 1A -2B -3A. 随着jdk版本迭代也推出了atomic原子类进行优化。

  • 循环时间长开销大
    自选CAS如果长时间不成功,会给CPU带来很大的开销。
    如果JVM能支持处理的提供的pause指令,那么效率会有一定的提升。
    pause 指令的作用

    • 它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零
    • 它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  • 只能保证一个共享变量的原子操作
    当对一个共享变量进行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。
    解决办法:可以引入锁机制(比如synchronized),或者将多个共享变量合并成一个共享变量来操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class HelloWorld{
    private int data = 0;

    public synchronized void increment(){
    data++;
    }

    // 多个线程同时调用方法:increment();
    }

atmoic原子类及其底层原理

对于简单的data++类的操作,可以换一种做法,JAVA并发包(JUC)下面提供了一系列的Atmoic原子类,比如AtmoicInteger。
他可以保证多线程并发安全的情况下,高性能的并发更新一个数值。
1
2
3
4
5
6
public class HelloWorld {

private AtomicInteger data = new AtomicInteger(0);

//多个线程并发的执行:data.incrementAndGet()
}

多个线程并发的执行AtmoicInteger的incrementAndGet()方法,意思就是给data的值累加1,接着返回累加后最新的值。

Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保证多线程修改一个数值的安全性

上面整个过程就是Atomic原子类的原理,没有基于加锁机制串行化,而是基于CAS机制,。
先获取一个值,然后发起CAS,比较整个值有没有被改过,如果没有,则更新,CAS 是原子的,不会被打断。

java8 如何对CAS 进行了优化

atomic是基于CAS机制来处理数据的,但是CAS也是有缺陷的。
如果大量的线程同事并发修改一个AtomicInteger,可能有很多的线程会不停的自旋,判断值是否有修改,有修改,然后进入一个空循环中,消耗CPU性能。

于是JAVA 8 推出了一个新的类 LongAdder.  
LongAdder是道格·利(Doug Lea的中文名)在java8中发布的类。

LongAdder也有一个volatile修饰的base值,但是当竞争激烈时,多个线程并不会一直自旋来修改这个值,而是采用了分段的思想。  
竞争激烈时,各个线程会分散累加到自己所对应的Cell[]数组的某一个数组对象元素中,而不会大家共用一个。

这样做,可以把不同线程对应到不同的Cell中进行修改,降低了对临界资源的竞争。本质上,是用空间换时间。

LongAdder是尝试使用分段CAS以及自动分段迁移的方式来大幅提升多线程高并发执行CAS操作的性能,降低了线程间的竞争冲突。

但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,  
LongAdder 的吞吐量大约是 AtomicLong 的十倍,不过凡事总要付出代价。  
LongAdder 在保证高效的同时,也需要消耗更多的空间

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
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.LongAdder;

/**
* <pre>
* 程序目的:和 AtomicLong 进行性能对比
* </pre>
* created at 2020/8/11 06:25
* @author lerry
*/
public class LongAdderDemo {
/**
* 线程池内线程数
*/
final static int POOL_SIZE = 1000;

public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();

LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(POOL_SIZE);

ArrayList<Future> futures = new ArrayList<>(POOL_SIZE);
for (int i = 0; i < POOL_SIZE * 100; i++) {
futures.add(service.submit(new LongAdderDemo.Task(counter)));
}

// 等待所有线程执行完
for (Future future : futures) {
try {
future.get();
}
catch (ExecutionException e) {
e.printStackTrace();
}
}

NumberFormat numberFormat = NumberFormat.getInstance();
System.out.printf("统计结果为:[%s]\n", numberFormat.format(counter.sum()));
System.out.printf("耗时:[%d]毫秒", (System.currentTimeMillis() - start));
// 关闭线程池
service.shutdown();
}

/**
* 有一个 LongAdder 成员变量,每次执行N次+1操作
*/
static class Task implements Runnable {

private final LongAdder counter;

public Task(LongAdder counter) {
this.counter = counter;
}

/**
* 每个线程执行N次+1操作
*/
@Override
public void run() {
for (int i = 0; i < 100; i++) {
counter.increment();
}
}// end run
}// end class
}

[来源参考] https://shishan100.gitee.io/docs/#/./docs/page/page2
[微信参考链接] https://mp.weixin.qq.com/s/NMm7NQt9A1oVwmPgLdzIHg
LongAdder实践

基础概念

什么是线程和进程

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程。
  • 进程:是程序的一次执行过程,系统运行程序的基本单位。

    • 系统运行一个程序即是一个进程从创建,运行到消亡的过程。
  • 线程:是比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。

    • 多个线程共享进程的堆和方法区。
    • 每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。

什么是并发和并行

  • 并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行)

  • 并行:单位时间内,多个任务同时执行。

说说线程的生命周期

线程的生命周期主要分为六个状态:初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态。
  • 初始状态(NEW):线程被构建,但是没有调用start方法。

  • 运行状态(RUNNABLE): 线程在操作系统中处于 就绪或运行两种状态。

  • 阻塞状态(BLOCKED): 线程被锁阻塞了。

  • 等待状态(WAITING): 线程进入等待状态。需要其他线程通知或直接中断。

  • 超时等待状态(TIME_WAITING): 线程指定了超时时间,可以在超时时间结束后自行返回。

  • 终止状态(TERMIATEB): 线程执行结束。

    线程不是一直固定在某个状态,而是随着代码的执行在不同状态之间切换。

线程创建之后它将处于 NEW 状态,调用 start() 方法后开始运行,线程这时候处于就绪状态。就绪状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNABLE 状态。

当线程执行 wait() 方法之后,线程进入 WAITING 状态。进入 WAITING 状态的线程需要依靠其他线程的通知才能够返回到 RUNNABLE 状态,
而 TIME_WAITING 状态的线程在超时后会自行回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。

线程在执行完 Runnable 的 run() 方法之后将会进入到 TERMINATED 状态。

线程的结束

  1. 设置退出标志,是线程正常退出,也就是当run()方法执行完成后线程终止。

  2. 使用interrupt()方法中断线程

  3. 使用stop()方法强行中断线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的)

并发编程的三大特性

并发编程的三大特性只要是:原子性、可见性、有序性
  • 原子性:即一个操作或者多个操作,要么一起执行完成,中途不可中断,要么都不执行。

  • 可见性:是在多个线程访问一个共享变量是,其中一个线程修改了这个变量的值,其他线程应该立即看到修改的值。

  • 有序性:程序执行的顺序按照代码的先后顺序执行。一般JVM会自动对其进行优化,使其重排序。

java的内存模型 JMM

用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。

JMM决定一个线程对共享变量的写入时,能对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory).
本地内存中存储了该线程可以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

JAVA 变量的读写

我们在看Volatile关键字的时候先了解一下java变量的读写:
(1)lock:作用于主内存,把变量标识为线程独占状态。

(2)unlock:作用于主内存,解除独占状态。

(3)read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。

(4)load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。

(5)use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。

(6)assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。

(7)store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。

(8)write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

    这里还得提一个概念,as-if-serial:不管怎么重排序,单线程下的执行结果不能被改变。

happens-before原则

  • 程序次序原则:在一个线程内,按照程序代码顺序,书写在前面的操作优先发生于书写在后面的操作。
  • 锁定规则:对于一个锁的解锁操作(unLock),优先发生于后续对这个锁的加锁操作(lock)。
  • volatile原则:对一个volatile变量的写操作,优先发生于后续对这个变量的读操作。
  • 传递原则:如果A操作先行发生于操作B,B操作先行发生于操作C,即A操作先行发生于操作C。
  • 线程启动原则:同一个线程的start()优先发生于此线程的其他方法。
  • 线程中断原则:对线程interrupt()方法的调用,优先发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结原则:同一个线程所有的操作都优先于线程的终止检测。
  • 对象创建原则:一个对象的初始化完成,优先于发生于它的 finalize()的开始。

说说 sleep() 和 wait() 的区别?

  • sleep() 和 wait() 都可以暂停线程的执行。
  • sleep() 不释放锁,wait() 释放锁。
  • sleep() 在 Thread 类中声明的,wait() 在 Object 类中声明。
  • sleep() 是静态方法,wait() 是非静态方法(必须由同步锁对象调用)。
  • sleep() 方法导致线程进入阻塞状态后,当时间到了或者 interrupt() 会醒来。
  • wait() 方法导致线程进入阻塞状态后,需要由 notify() 或 notifyAll() 唤醒,或者使用 wait(long timeout) 超时后线程会自动苏醒。

为什么不能直接调用 run() 方法?

  • 调用 run() 方法,会被当做普通方法去执行,不是多线程工作。

  • 调用 start() 方法,会启动线程并使线程进入了就绪状态,当分配到时间片后就可以运行 run() 方法内容了,这是真正的多线程工作

说说 Runnable 和 Callable 的区别?

Runnable 和 Callable 都是接口,都可以编写多线程程序。不同的是:

  • Runnable 接口 run 方法无返回值,Callable 接口 call 方法有返回值。

  • Runnable 接口 run 方法只能直接抛出运行时异常,Callable 接口 call 方法可以捕获异常。

    对于 Calleble 来说,Future 和 FutureTask 均可以用来获取任务执行结果,不过 Future 是个接口,FutureTask 是 Future 的具体实现。

    FutureTask 表示一个异步运算的任务。
    FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
    只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

volatile 于 synchronized 区别

volatile 只保证可见性,不保证原子性,禁止重排序保证了有序性。
synchronized 既可以保证原子性,也能保证可见性。synchronized确保了一次只有一个线程执行,即happens-before的有序原则,也确保了有序性。

volatile 只能保证数据的可见性,不能用于同步,因此多个线程访问volatile修饰的变量不会造成zuse。
n 不仅保证了可见性,也保证了原子性。
因为经n修饰后,只有获得锁的线程才能进入临界区,从而保证了临界区内的所有语句都全部执行。
多个线程争抢n变量时,会出现阻塞情况

volatile是轻量级的,因为只能修饰变量。
n是重量级的,可以修饰变量、代码块、方法。

为什么用volatile

假设 线程1 修改了data的变量为1,然后将这个修改写入到了自己的本地工作内存中。
那么此时,线程1的工作内存中data的值为1,而主内存和线程2中的data的值任然是1!

这可尴尬了,那接下来,在线程 1 的代码运行过程中,他可以直接读到 data 最新的值是 1,但是线程 2 的代码运行过程中读到的 data 的值还是 0!
这就导致,线程 1 和线程 2 其实都是在操作一个变量 data,但是线程 1 修改了 data 变量的值之后,线程 2 是看不到的,
一直都是看到自己本地工作内存中的一个旧的副本的值!

这就是所谓的 java 并发编程中的可见性问题:
多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,但是其他线程看不到!也就是对其他线程不可见!

volatile的作用及背后原理

要解决上面的问题,引入volatile既可以解决并发编程中的可见性问题。

比如下面的这样的代码,在加了 volatile 之后,会有啥作用呢?

1
2
3
4
5
6
7
8
public class HelloWorld {
private volatile int data = 0;

//线程1会读取和修改data变量值

//线程2会读取data变量值

}

volatile的作用

1. volatile修饰的共享变量data,线程1修改data的值,就会在修改本地工作内存的data值之后,强制将data变量最新的值刷回主内存,
让主内存里的data的值立马变成最新的值

2. 如果此时如果其他的线程中也存有这个data变量的本地缓存,那么会强制让其他线程的工作内存中的 data 变量缓存直接失效过期,不允许再次读取和使用了!

3. 如果其他线程想再次获取data时,尝试获取本地工作内存的data变量值,发现失效了,此时,就必须从主内存中获取data变量最新的值。

volatile的特殊规则

read、load、use动作必须连续出现。
assign、store、write动作必须连续出现。

内存屏障

JVM 中内存屏障是一组处理器指令,用来实现对内存操作的顺序限制(避免了重排序)。它可以分为下面几种:

  • LoadLoad(Load1; LoadLoad;Load2):Load2 及后续读操作之前,保证 Load1 先读取完。

  • StoreStore(Store1; StoreStore; Store2):Store2 及后续写入操作之前,保证 Store1 的写入对其他处理器可见。

  • LoadStore(Load1; LoadLoad;Store2):Store2 及后续写操作之前,保证 Load1 先读取完。

  • StoreLoad(Store1; StoreStore; Load2):Load2 及后续读操作之前,保证 Store1 的写入对其他处理器可见。

    • StoreLoad 是一个“全能型”的屏障,它同时具有其他 3 个屏障的效果。

需要注意的是:volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。

总结

每次读取前必须先从主内存刷新到最新的值。
每次写入后必须立即同步回主内存当中。

最后给大家提一嘴,volatile 主要作用是保证可见性以及有序性。

有序性涉及到较为复杂的指令重排、内存屏障等概念,本文没提及,但是 volatile 是不能保证原子性的!

也就是说,volatile 主要解决的是一个线程修改变量值之后,其他线程立马可以读到最新的值,是解决这个问题的,也就是可见性!

但是如果是多个线程同时修改一个变量的值,那还是可能出现多线程并发的安全问题,导致数据值修改错乱,
volatile 是不负责解决这个问题的,也就是不负责解决原子性问题!

原子性问题,得依赖 synchronized、ReentrantLock 等加锁机制来解决。

Hystrix 断路器

服务降级

服务降级概念:
当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级(返回一个友好提示给客户端,不去执行主业务逻辑,调用fallBack本地方法),以此释放服务器资源以保证核心任务的正常运行。

阅读全文 »

配置

Dubbo 核心配置有哪些

标签 用途 解释
dubbo:service 服务配置 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
dubbo:reference 引用配置 用于创建一个远程服务代理,一个引用可以指向多个注册中心
dubbo:protocol 协议配置 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
dubbo:application 应用配置 用于配置当前应用信息,不管该应用是提供者还是消费者
dubbo:module 模块配置 用于配置当前模块信息,可选
dubbo:registry 注册中心配置 用于配置连接注册中心相关信息
dubbo:monitor 监控中心配置 用于配置连接监控中心相关信息,可选
dubbo:provider 提供方配置 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选
dubbo:consumer 消费方配置 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选
dubbo:method 方法配置 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息
dubbo:argument 参数配置 用于指定方法参数配置

Dubbo 配置原则

Dubbo 推荐在 Provider 上尽量多配置 Consumer 端属性。
1)作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
2)在Provider配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可以作为 Consumer 的缺省值。否则,Consumer 会使用 Consumer 端的全局设置,这对于 Provider 不可控的,并且往往是不合理的

Dubbo 属性配置优先级

属性配置优先级

1)方法级配置别优于接口级别,接口级别优于全局配置,即小Scope优先
2)Consumer端配置优于 Provider配置
3)最后是Dubbo Hard Code的配置值(见配置文档)

Dubbo 配置文件优先级

配置文件优先级

1)JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口。
2)XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效。
3)Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名。

通讯协议

Dubbo 支持哪些通讯协议?

Dubbo 官方文档

支持4种,分别是:Dubbo,Hessian,RMI,HTTP;

Dubbo 2.7.7

通过分析 dubbo 2.7.7版本协议实现,支持11种;
分别是:Dubbo,Hessian,RMI,HTTP,WebService,Thrift,Memcached,Redis,Rest,XmlRpc,Grpc;

Dubbo 通讯协议的特点

Dubbo协议

Dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
Dubbo默认使用Dubbo协议;

基于Dubbo的远程调用协议:
连接个数:单连接
连接方式:长连接
传输协议:TCP
传输方式:NIO异步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用

Hessian协议

Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。
Hessian是Caucho开源的一个RPC框架:http://hessian.caucho.com,其通讯效率高于WebService和Java自带的序列化。

基于Hessian的远程调用协议:
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
适用场景:页面传输,文件传输,或与原生hessian服务互操作

RMI协议

Java标准的远程调用协议,采用JDK标准的java.rmi.*实现,阻塞式短连接和JDK标准序列化方式

基于RMI协议的远程调用协议:
连接个数:多连接
连接方式:短连接
传输协议:TCP
传输方式:同步传输
序列化:Java标准二进制序列化
适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
适用场景:常规远程服务方法调用,与原生RMI服务互操作

HTTP协议

此协议采用 spring 的HttpInvoker的功能实现,

基于HTTP的远程调用协议:
连接个数:多连接
连接方式:长连接
连接协议:http
传输方式:同步传输
序列化:表单序列化(JSON)
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
适用场景:需同时给应用程序和浏览器JS使用的服务。

Thrift协议

基于Thrift实现PRC协议

Redis协议

基于redis实现RPC协议

Memcached协议

基于Memcached实现RPC协议

Dubbo支持服务多协议吗?

Dubbo 支持多协议;
Dubbo 在不同服务上支持不同协议 或者 同一服务上同时支持多种协议。

序列化

Dubbo 支持哪些序列化

支持的序列化
Dubbo 支持 Hession,Dubbo,Json、Java自带序列化 多种序列化方式。但是 Hessian 是其默认的序列化方式。

Dubbo 序列化特点

Hession

是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的,它是dubbo RPC默认启用的序列化方式。

Dubbo

是阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它。

Json

目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。

Java自带序列化

主要是采用JDK自带的Java序列化实现,性能很不理想。

Hessian 的数据结构

Hessian 的对象序列化机制有 8 种原始类型:

  • 原始二进制数据
  • boolean
  • 64-bit date(64 位毫秒值的日期)
  • 64-bit double
  • 32-bit int
  • 64-bit long
  • null
  • UTF-8 编码的 string

另外还包括 3 种递归类型:

  • list for lists and arrays
  • map for maps and dictionaries
  • object for objects

还有一种特殊的类型:

  • ref:用来表示对共享对象的引用。

什么是 BP?

可能有一些同学比较习惯于 JSON or XML 数据存储格式,对于 Protocol Buffer 还比较陌生。
Protocol Buffer 其实是 Google 出品的一种轻量并且高效的结构化数据存储格式,性能比 JSON、XML 要高很多。

其实 PB 之所以性能如此好;主要得益于两个;
第一,它使用 proto 编译器,自动进行序列化和反序列化,速度非常快,应该比 XML 和 JSON 快上了 20~100 倍;
第二,它的数据压缩效果好,就是说它序列化后的数据量体积小。因为体积小,传输起来带宽和速度上会有优化。

通讯框架

Dubbo 正常哪些通信框架,推荐使用什么?

支持的通讯框架
Dubbo 支持 Netty、Mina、Grizzly 多种通讯框架,Dubbo 推荐并默认使用 Netty。

注册中心

Dubbo 支持哪些注册中心?

支持的注册中心
Zookeeper、Redis、Multicast、Simple 都可以作为Dubbo的注册中心,Dubbo官方推荐使用 Zookeeper。

Dubbo 的注册中心挂掉,服务提供者和服务消费者之间还能通信么?

可以通讯。启动 Dubbo 时,消费者会从注册中心拉取生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。

1
2
3
4
5
6
7
健壮性
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

Dubbo 直连模式

Dubbo 的直连模式,可以完全跳过注册中心,直接指定服务提供者的地址进行通讯;

配置方式:

1
2
<dubbo:reference id="userService" 
interface="com.zang.gmall.service.UserService" url="dubbo://localhost:20880" />

Dubbo 支持多注册中心吗?

Dubbo 支持多注册中心;
Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。

服务容器

Dubbo内置了哪几种服务容器?

Dubbo 官方文档

dubbo 内置了 spring, jetty, log4j 等服务容器;
支持自己扩展服务容器进行加载;

2.7.7 版本

dubbo 内置了 spring, log4j, logback 等服务容器;

1
2
3
spring=org.apache.dubbo.container.spring.SpringContainer
log4j=org.apache.dubbo.container.log4j.Log4jContainer
logback=org.apache.dubbo.container.logback.LogbackContainer

服务启动

Dubbo 服务启动只是一个简单的 Main 方法,加载一个简单的 Spring 容器,用于暴露服务。
在服务启动时,调用容器的 start() 方法,在服务停止时调用 stop() 方法。

高可用

Dubbo 负债均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Random LoadBalance
随机均衡算法,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

RoundRobin LoadBalance
权重轮循均衡算法,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

LeastActive LoadBalance
最少活跃调用数均衡算法,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ConsistentHash LoadBalance
一致性 Hash 均衡算法,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />

Dubbo 集群容错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Failover Cluster
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。

Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。

Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

Dubbo 服务降级

dubbo 服务降级是 mock 机制,即当服务提供者出错时(抛出 RpcException),进行 mock 调用;

有两种策略方式:

  • fail:当服务消费者调用服务提供者失败后,会去执行配置的 mock 策略。 配置方式为 mock=“fail:策略” 或者 mock=“策略”。
  • force:当服务消费者调用服务提供者时,会直接执行 mock 配置的策略,不会进行服务调用。

运维管理

Dubbo 如何优雅停机?

Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。

Dubbo telnet 命令能做什么?

dubbo 服务发布之后,我们可以利用 telnet 命令进行调试、管理。Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令。

服务上线怎么兼容旧版本?

可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。

SPI

SPI 是什么

SPI 全称:Service Provider Interface;
SPI 就是通过动态加载机制实现面向接口编程;
SPI 是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件;

SPI 一般用在哪儿?
主要在框架中使用,用于插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 SPI 思想就用上了。

举个例子:你有一个接口 A。A1/A2/A3 分别是接口A的不同实现。你通过配置 接口 A = 实现 A2,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。

Dubbo SPI 和 Java SPI 区别?

Java SPI

  • JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了

Dubbo SPI:

  • 对 Dubbo 进行扩展,不需要改动 Dubbo 的源码
  • 延迟加载,可以一次只加载自己想要加载的扩展实现。
  • 增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
  • Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。

RPC 架构

详情看 Dubbo-高级(RPC模块)

Dubbo 架构

详情看 Dubbo-高级(Dubbo 底层解析模块)

其他

Dubbo 支持服务降级吗?

dubbo 自己有提供服务降级,但兼容性不是很好。
可以自己整合服务熔断框架,列如:Hystrix。

Dubbo 支持链路追踪吗?

dubbo 目前暂时不支持链路追踪。
可以自己整合链路追踪框架,列如:Skywalking。

Dubbo 支持分布式事务吗?

dubbo 目前暂时不支持分布式事务。
可以自己整合分布式事务框架,列如:Seata。

扩展知识

什么是设计模式

设计模式(Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的高内聚和低耦合。

设计模式的三大分类及关键点

设计模式三大分类

创建型模式

对象实例化的模式,创建型模式用于解耦对象实例化的过程
  1. 单列模式: 某个类只有一个实例,提供一个全局的访问点,例如:单点登录。
  2. 工厂模式: 一个工厂类根据传入的参数决定创建哪一种产品类的实例。
  3. 抽象工厂模式 : 创建相关或依赖对象的家族,而无需明确指定具体类。
  4. 建造者模式 : 封装一个复杂对象的创建过程,并可以安步骤构造。
  5. 原型模式 : 通过复制现有的实例来创建新的实例。

结构型模式

把类或对象结合在一起形成一个更大的结构
  1. 装饰模式 : 动态的给对象添加新的功能。
  2. 代理模式 : 未其他对象提供一个代理以便控制这个对象的访问。
  3. 桥接模式 : 将抽象部分和它的实现部分分离,使它们可以独立的变化。
  4. 适配器模式 : 将一个类的方法接口转换到客户希望的另一个接口。
  5. 组合模式 : 将对象组合成树形结构以表示“部分-整体”的结构层次。
  6. 外观模式 : 对外提供一个统一的方法,来访问子系统的一群接口。
  7. 享元模式 : 通过共享技术来有效的支持大量细粒度的对象。

行为型模式

类和对象如何交互,以及划分责任和算法
  1. 策略模式 : 定义一系列算法,把他们封装起来,并且使用它们可以相互替换。
  2. 模板模式 : 定义一个算法结构,而将一些步骤延迟到子类中实现。
  3. 命令模式 : 将命令请求封装为一个对象,是的可以用不同的请求来进行参数化。
  4. 迭代器模式 : 一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
  5. 观察者模式 : 对象间的一对多的依赖关系。
  6. 仲裁者模式 : 用一个中介对象来封装一系列的对象交互。
  7. 备忘录模式 : 在不破坏封装的前提下,保持对象的内部状态。
  8. 解释器模式 : 给定一个语言,定义它的语法的一种表示,并定义一个解释器。
  9. 状态模式 : 允许一个对象在对象内部状态改变时改变它的行为。
  10. 责任链模式 : 将请求的发送者和接受者解耦,使得多个对象都有处理这个请求的机会。
  11. 访问者模式 : 不改变数据结构的其前提下,增加作用一组对象元素的新功能。

设计模式的原则

1. 单一职责原则

对于一个类,只有一个引起该类变化的原因。  
该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。  

2. 接口隔离原则

客户端不应该依赖它不需要的接口。  
一个类对另一个类的依赖应该建立在最小接口上。

3. 依赖倒转原则

程序要依赖于抽象接口,不要依赖于具体实现。  
简单来说就是要求对抽象进行编程,不要对具体实现进行编程,这样就降低了客户与实现模块建的耦合。  

4. 里式代替原则

1. 任何基类出现的地方,子类一定可以出现。  
2. 里式代替原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受影响时,基类才能真正的被复用,而衍生类也能欧股在基类的基础上增加新的行为。  
3. 里式替代原则是对开闭原则的补充。  
4. 实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

5. 开闭原则

  1. 对于扩展是开放的(Open for extension)。这就意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
  2. 对于修改是关闭的(Closed for modification)。对于模块行为进行扩展时,不必改动模块的源代码或者二进制码。模块的二进制码可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。

6. 迪米特法则

迪米特法则又叫最少知识原则,意思是一个对象应当对其它对象尽可能少的了解。  

7. 合成复用原则

1. 合成复用原则要求在软件复用时,要尽量使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
2. 如果要使用继承关系,则必须严格遵循里式替换原则。  
3. 合成复用原则同里式替换原则相辅相成,两者都是开闭原则的具体实现规范。

设计模式七大原则总结

设计模式关系

设计模式之间的关系

设计模式之间的关系

设计模式感想

一共有23种设计模式,可以说都是为了提高代码的可读性、可扩展
性、可复用性、类的可替换性、组件化、可移植性等等特性。通过接口、抽象类、继承、实现、委托、抽象、面向接口编程、多态、重载、重写等方式使得代码的这些特性得以彰显,可以说只有深刻的理解了这些概念背后的哲学思想才能更好的理解设计模式。在设计模式中有很多思想,比如可以使用委托的不要使用继承、开闭原则,面向扩展开放,面向修改关闭,里式代换原则,父类一定能被子类代替并使用,反置则不然,面向接口编程,功能层次和实现层次分离(桥接模式)、高内聚低耦合等思想,这些思想都是宝贵的,正是因为这样的思想的存在才使得代码的更新换代的时候能够尽可能少的甚至不用修改之前的代码,直接加入新的内容。提高软件的开发周期,便于维护和升级,便于查找和纠错,易于扩展和使用。

同样的设计模式主要分为三大类,创建型、行为型、结构型。我们可以简单的这样分类,只不过这样的分类似乎并不准确,不能一语道出所有的本质,设计模式是相互关联的,有的设计模式内部其实是使用了别的设计模式作为支撑的,但是大体上这样的一种划分便于我们去记忆,仅此而已。

设计模式回顾

从迭代器开始,我们将类中数据结构的遍历和类的功能实现分离出来,本质上使用了工厂模式;

其次我们学习了适配器模式,它将不同的接口进行适配,从而便于版本的兼容性以及其他功能;

然后我们学习了模板方法,使用模板面向抽象编程,便于新的子类的实现和管理;

之后学习了工厂模式,其实借用了模板模式来创建产品,是一种非常重要用处很广的一种方法;

然后我们学习了单例模式,有懒汉式、饿汉式等,生成关于某个类全局唯一的对象,注意多线程的影响;

之后是原型模式,用来复制复杂的对象,使用了clone方法,然后是builder模式,用一个新的类对已有的抽象接口进行整合和编程,从而构建出我们想要的东西;

然后是抽象工厂模式,使用了工厂模式,组合模式等模式,面向抽象编程,将抽象零件组装成抽象产品,便于具体工厂的创建,提高了代码的组件化和复用性;

然后是桥接模式,将类的功能层次和实现层次分割开来,便于对应的扩展和使用;

然后是策略模式,可以整体的替换策略,使用也很广泛;然后是组合模式,保证了同根同源,通过委托添加自己构成递归,树形结构,将具有树形特点的对象组合起来;

然后是装饰器模式,和组合模式的结构类似,同样是递归结构,从而可以不断的装饰,增加新的功能,很好用;

接着是visitor访问者模式,通过在类外访问类中的数据结构从而得到想要的结果,便于程序的可扩展性和组件化;

接着是责任链模式,推卸责任,根据问题的大小来考虑自己释放处理,本质是链表,便于职责分明;

然后是外观模式,通过整合各个类之间的调用关系,组建成了统一的接口(API),便于外部类的调用;

接着是仲裁者模式,将很多类之间互相关联的关系交给仲裁者处理,省去了各个类之间的嵌套和调动,有利于高内聚和低耦合,思路清晰,便于扩展;

然后是观察者模式,通过互相委托从而能够在被观察的类发生改变的时候得到相应的改变的信息并且处理;

然后是备忘录模式,通过在某一时刻的状态保存下来,便于恢复,在游戏中使用的比较多;

然后是状态模式,将状态当做类,从而职责分明,解除了很多繁琐的if和else这些分支逻辑,便于扩展;

然后是享元模式,轻量级对象,通过共用不变对象来实现;

然后是代理模式,懒加载真正的服务器,加快访问速度,代理是帮助服务器代理的;

然后是命令模式,将命令当做类,通过保存一些列命令,从而能够随时执行这些命令,需要清除命令的本质就是一些操作和数据;

最后是解释器模式,利用编程原理的方法,来更高层次的封装代码,将自己开发的java代码当做编译系统,从而不用改变java代码只修改更高语言层次的代码就能实现不同的功能。

友情链接

RabbitMQ 基本概念

简介

RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息中间件。

RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,
你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,
一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件,而是接收,存储和转发消息数据。

RabbitMQ官方地址:http://www.rabbitmq.com

阅读全文 »

Markdown 描述

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。

Markdown 语言在 2004 由约翰·格鲁伯(英语:John Gruber)创建。

Markdown 编写的文档可以导出 HTML 、Word、图像、PDF、Epub 等多种格式的文档。

Markdown 编写的文档后缀为 .md, .markdown。

Markdown 实操

我展示的是一级标题

我展示的是二级标题

一级标题

二级标题

三级标题

四级标题

。。。。

Markdown 段落

段落换行是已 两个或以上空格+回车即可
开始新的一个段落

Markdown 字体

斜体文本
斜体文本
粗体文本
粗体文本
粗斜体文本
粗斜体文本

分隔线

可以使用三个以上的 星号、减号、底线来构建一个分隔线 行内不能有其他内容(允许中间插入空格) 不然会破坏格式






删除线

段落需要添加删除线 在文本两端添加 两个波浪线即可
当前版本1.0

下划线

使用与html相同的标签 u
下划线文本

脚注

脚注是对文本的补充说明
有一个 234

Markdown 列表

无序列表 用星号、加好或减号 作为列表标记 标记后面需要添加一个空格

  • 第一列
  • 第二列
  • 第三列

有序列表

Dubbo 高可用

集群

Dubbo集群很简单,只是需要对服务提供者的配置文件进行修改,配置文件中的dubbo.application.name相同,Dubbo则会认为是同一集群。

阅读全文 »